Skip to content

Commit

Permalink
feature: adds discord integration to GoMud (#245)
Browse files Browse the repository at this point in the history
This PR adds a very simple discord integration to GoMud.

To use the integration:
1. Setup a discord webhook: https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks
2. Set the environment variable "DISCORD_WEBHOOK_URL" prior to running GoMud. (`DISCORD_WEBHOOK_URL=https://www.mywebhook.com ./gomud` on linux).

All feedback is encouraged and welcome, learning Golang slowly.
  • Loading branch information
colinsenner authored Mar 4, 2025
1 parent 7967ca7 commit 1351d6e
Show file tree
Hide file tree
Showing 5 changed files with 196 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ private-notes.txt
_datafiles/**/users/*
**/config-overrides.yaml
**/.roundcount
vendor/
116 changes: 116 additions & 0 deletions internal/integrations/discord/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Package discord
//
// This package provides programatic access to discord messages integrated with GoMud.
//
// References:
// https://leovoel.github.io/embed-visualizer/
// https://birdie0.github.io/discord-webhooks-guide/discord_webhook.html
package discord

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"strconv"
"strings"

"github.com/volte6/gomud/internal/events"
)

var (
WebhookUrl string
initialized bool
)

// Initializes and sets the webhook so we can send messages to discord
// and registers listeners to listen for events
func Init(webhookUrl string) {
if initialized {
return
}

WebhookUrl = webhookUrl
registerListeners()
initialized = true
}

func registerListeners() {
events.RegisterListener(events.PlayerSpawn{}, HandlePlayerSpawn)
events.RegisterListener(events.PlayerDespawn{}, HandlePlayerDespawn)
}

// Sends an embed message to discord which includes a colored bar to the left
// hexColor should be specified as a string in this format "#000000"
func SendRichMessage(message string, hexColor string) error {
if !initialized {
return errors.New("Discord client was not initialized.")
}

if strings.HasPrefix(hexColor, "#") {
hexColor = hexColor[1:]
}

color, err := strconv.ParseInt(hexColor, 16, 32)
if err != nil {
message := fmt.Sprintf("Invalid color specified, expected format #000000")
return errors.New(message)
}

payload := richMessage{
Embeds: []embed{
{
Description: message,
Color: int32(color),
},
},
}

marshalled, err := json.Marshal(payload)
if err != nil {
message := fmt.Sprintf("Couldn't marshal discord message")
return errors.New(message)
}

return send(marshalled)
}

// Sends a simple message to discord
func SendMessage(message string) error {
if !initialized {
return errors.New("Discord client was not initialized.")
}

payload := simpleMessage{
Content: message,
}

marshalled, err := json.Marshal(payload)
if err != nil {
message := fmt.Sprintf("Couldn't marshal discord message")
return errors.New(message)
}

return send(marshalled)
}

func send(marshalled []byte) error {
request, err := http.NewRequest("POST", WebhookUrl, bytes.NewReader(marshalled))
request.Header.Set("Content-Type", "application/json; charset=UTF-8")

client := &http.Client{}
response, err := client.Do(request)
if err != nil {
message := fmt.Sprintf("Couldn't send POST request to discord.")
return errors.New(message)
}

// Expect 204 No Content reply
if response.StatusCode != 204 {
message := fmt.Sprintf("Expected discord to send status code 204, got %v.", response.StatusCode)
return errors.New(message)
}

return nil
}
50 changes: 50 additions & 0 deletions internal/integrations/discord/listeners.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package discord

import (
"fmt"

"github.com/volte6/gomud/internal/events"
"github.com/volte6/gomud/internal/users"
)

// Player enters the world event
func HandlePlayerSpawn(e events.Event) bool {
evt, typeOk := e.(events.PlayerSpawn)
if !typeOk {
return false
}

user := users.GetByUserId(evt.UserId)
if user == nil {
return false
}

message := fmt.Sprintf(":white_check_mark: **%v** connected", user.Character.Name)
err := SendMessage(message)
if err != nil {
return false
}

return true
}

// Player leaves the world event
func HandlePlayerDespawn(e events.Event) bool {
evt, typeOk := e.(events.PlayerDespawn)
if !typeOk {
return false
}

user := users.GetByUserId(evt.UserId)
if user == nil {
return false
}

message := fmt.Sprintf(":x: **%v** disconnected", user.Character.Name)
err := SendMessage(message)
if err != nil {
return false
}

return true
}
20 changes: 20 additions & 0 deletions internal/integrations/discord/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package discord

// Reference: https://birdie0.github.io/discord-webhooks-guide/discord_webhook.html

// Discord message payload for non-rich content
// This object has many more fields, see reference
type simpleMessage struct {
Content string `json:"content"`
}

// Discord message payload for rich content
type richMessage struct {
Embeds []embed `json:"embeds"`
}

// These objects have many more fields, see reference
type embed struct {
Description string `json:"description"`
Color int32 `json:"color"`
}
9 changes: 9 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/volte6/gomud/internal/gametime"
"github.com/volte6/gomud/internal/hooks"
"github.com/volte6/gomud/internal/inputhandlers"
"github.com/volte6/gomud/internal/integrations/discord"
"github.com/volte6/gomud/internal/items"
"github.com/volte6/gomud/internal/keywords"
"github.com/volte6/gomud/internal/leaderboard"
Expand Down Expand Up @@ -119,6 +120,14 @@ func main() {
os.Exit(1)
}

// Discord integration
if webhookUrl := os.Getenv("DISCORD_WEBHOOK_URL"); webhookUrl != "" {
discord.Init(webhookUrl)
mudlog.Info("Discord", "info", "integration is enabled")
} else {
mudlog.Warn("Discord", "info", "integration is disabled")
}

hooks.RegisterListeners()

// Load all the data files up front.
Expand Down

0 comments on commit 1351d6e

Please sign in to comment.