From 5843fb81be661ccd81edb8c3e42e026d55aea022 Mon Sep 17 00:00:00 2001 From: Tyler Sommer Date: Sat, 5 Mar 2022 18:10:11 -0700 Subject: [PATCH] Add discord plugin --- README.md | 2 + config.toml.dist | 8 ++ go.mod | 1 + go.sum | 9 +++ plugins/discord/discord.go | 132 +++++++++++++++++++++++++++++++++ plugins/discord/plugin.go | 74 ++++++++++++++++++ plugins/discord/plugin/main.go | 14 ++++ 7 files changed, 240 insertions(+) create mode 100644 plugins/discord/discord.go create mode 100644 plugins/discord/plugin.go create mode 100644 plugins/discord/plugin/main.go diff --git a/README.md b/README.md index bc8aaf1..38a1344 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ as shared libraries and loaded at runtime using the Go plugin API. - `squircy2_compat` provides a compatibility layer with [squircy2](https://squircy.com). - `script` loads javascript files from a configured folder at app startup. +- `discord` provides integration with + [discordgo](https://github.com/bwmarrin/discordgo). #### Linking extra plugins at compile-time diff --git a/config.toml.dist b/config.toml.dist index b96c272..45842f4 100644 --- a/config.toml.dist +++ b/config.toml.dist @@ -14,6 +14,9 @@ extra_plugins=[ # script is a plugin that loads scripts from a directory and executes them during application # startup. "script.so", + + # discord is a plugin that enables discord interaction, ie. discord bot functionality. + "discord.so", ] [irc] @@ -48,3 +51,8 @@ data_path="data" # set enable_exec to true to allow scripts to spawn child processes. enable_exec=false +[discord] +# bot authorization token +#token="" +#owner="" +#activity="" diff --git a/go.mod b/go.mod index f37365e..11cd98d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/BurntSushi/toml v0.3.1 + github.com/bwmarrin/discordgo v0.24.0 github.com/dop251/goja v0.0.0-20210630164231-8f81471d5d0b github.com/fatih/structtag v1.2.0 github.com/gobuffalo/logger v1.0.4 // indirect diff --git a/go.sum b/go.sum index 1948417..d4483d0 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bwmarrin/discordgo v0.24.0 h1:Gw4MYxqHdvhO99A3nXnSLy97z5pmIKHZVJ1JY5ZDPqY= +github.com/bwmarrin/discordgo v0.24.0/go.mod h1:NJZpH+1AfhIcyQsPeuBKsUtYrRnjkyu0kIVMCHkZtRY= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= @@ -53,6 +55,8 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -147,6 +151,8 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -158,6 +164,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -177,11 +184,13 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b h1:9zKuko04nR4gjZ4+DNjHqRlAJqbJETHwiNKDqTfOjfE= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/plugins/discord/discord.go b/plugins/discord/discord.go new file mode 100644 index 0000000..05a4907 --- /dev/null +++ b/plugins/discord/discord.go @@ -0,0 +1,132 @@ +package discord + +import ( + "sync" + + "code.dopame.me/veonik/squircy3/event" + "github.com/bwmarrin/discordgo" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +var ErrNotConnected = errors.New("not connected") + +type Config struct { + Token string `toml:"token"` + ActivityName string `toml:"activity"` + OwnerID string `toml:"owner"` + + enabled bool +} + +type Manager struct { + conf Config + + ev *event.Dispatcher + session *discordgo.Session + + channels map[string]*discordgo.Channel + + mu sync.Mutex +} + +func NewManager(ev *event.Dispatcher) *Manager { + return &Manager{ev: ev, channels: make(map[string]*discordgo.Channel)} +} + +func (m *Manager) Configure(c Config) error { + c.enabled = len(c.Token) > 0 + m.mu.Lock() + defer m.mu.Unlock() + m.conf = c + return nil +} + +func (m *Manager) Connect() error { + m.mu.Lock() + defer m.mu.Unlock() + if m.session != nil { + return errors.New("already connected") + } + if !m.conf.enabled { + return errors.New("not enabled") + } + s, err := discordgo.New("Bot " + m.conf.Token) + if err != nil { + return err + } + m.session = s + m.session.AddHandler(m.onMessageCreate) + m.session.Identify.Intents = discordgo.IntentsGuildMessages | discordgo.IntentGuildMessageTyping | discordgo.IntentsDirectMessages + m.session.Identify.Presence.Game = discordgo.Activity{Name: m.conf.ActivityName} + return m.session.Open() +} + +func (m *Manager) Disconnect() error { + m.mu.Lock() + defer m.mu.Unlock() + if m.session == nil { + return ErrNotConnected + } + s := m.session + m.session = nil + return s.Close() +} + +func userToMap(user *discordgo.User) map[string]interface{} { + return map[string]interface{}{ + "ID": user.ID, + "Username": user.Username, + } +} + +func (m *Manager) onMessageCreate(s *discordgo.Session, e *discordgo.MessageCreate) { + ch, err := m.getChannel(e.ChannelID) + isDM := false + if err != nil { + logrus.Warnf("%s: failed to get channel for %s: %s", PluginName, e.ChannelID, err) + } else { + isDM = ch.Type == discordgo.ChannelTypeDM + } + m.ev.Emit("discord.MESSAGE", map[string]interface{}{ + "ID": e.ID, + "Content": e.Content, + "ChannelID": e.ChannelID, + "GuildID": e.GuildID, + "Author": userToMap(e.Author), + "FromSelf": e.Author.ID == s.State.User.ID, + "IsDM": isDM, + }) +} + +func (m *Manager) MessageChannel(channelID, message string) error { + _, err := m.session.ChannelMessageSend(channelID, message) + return err +} + +func (m *Manager) MessageChannelTTS(channelID, message string) error { + _, err := m.session.ChannelMessageSendTTS(channelID, message) + return err +} + +func (m *Manager) CurrentUsername() (string, error) { + return m.session.State.User.Username, nil +} + +func (m *Manager) OwnerID() string { + return m.conf.OwnerID +} + +func (m *Manager) getChannel(id string) (*discordgo.Channel, error) { + m.mu.Lock() + defer m.mu.Unlock() + if ch, ok := m.channels[id]; ok { + return ch, nil + } + ch, err := m.session.Channel(id) + if err != nil { + return nil, err + } + m.channels[id] = ch + return ch, nil +} diff --git a/plugins/discord/plugin.go b/plugins/discord/plugin.go new file mode 100644 index 0000000..d91710d --- /dev/null +++ b/plugins/discord/plugin.go @@ -0,0 +1,74 @@ +package discord + +import ( + "code.dopame.me/veonik/squircy3/config" + "code.dopame.me/veonik/squircy3/event" + "code.dopame.me/veonik/squircy3/plugin" + "github.com/dop251/goja" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +const PluginName = "discord" + +func Initialize(m *plugin.Manager) (plugin.Plugin, error) { + ev, err := event.FromPlugins(m) + if err != nil { + return nil, errors.Wrapf(err, "%s: required dependency missing (event)", PluginName) + } + return &discordPlugin{NewManager(ev)}, nil +} + +type discordPlugin struct { + manager *Manager +} + +func (p *discordPlugin) Configure(c config.Config) error { + if gcv, ok := c.Self().(*Config); ok { + return p.manager.Configure(*gcv) + } + cf := Config{} + cf.Token, _ = c.String("token") + cf.OwnerID, _ = c.String("owner") + cf.ActivityName, _ = c.String("activity") + return p.manager.Configure(cf) +} + +func (p *discordPlugin) Options() []config.SetupOption { + return []config.SetupOption{config.WithInitValue(&Config{})} +} + +func (p *discordPlugin) Name() string { + return PluginName +} + +// must logs the given error as a warning +func must(what string, err error) { + if err != nil { + logrus.Warnf("%s: error %s: %s", PluginName, what, err) + } +} + +func (p *discordPlugin) HandleRuntimeInit(gr *goja.Runtime) { + v := gr.NewObject() + must("setting connect", v.Set("connect", p.manager.Connect)) + must("setting messageChannel", v.Set("messageChannel", p.manager.MessageChannel)) + must("setting messageChannelTTS", v.Set("messageChannelTTS", p.manager.MessageChannelTTS)) + must("setting getCurrentUsername", v.Set("getCurrentUsername", p.manager.CurrentUsername)) + must("setting getOwnerID", v.Set("getOwnerID", p.manager.OwnerID)) + if err := gr.Set("discord", v); err != nil { + logrus.Warnf("%s: error initializing runtime: %s", PluginName, err) + } +} + +func (p *discordPlugin) HandleShutdown() { + if p.manager == nil { + logrus.Warnf("%s: shutting down uninitialized plugin", PluginName) + return + } + if err := p.manager.Disconnect(); err != nil { + if err != ErrNotConnected { + logrus.Warnf("%s: failed to disconnect before shutting down: %s", PluginName, err) + } + } +} diff --git a/plugins/discord/plugin/main.go b/plugins/discord/plugin/main.go new file mode 100644 index 0000000..d4763ed --- /dev/null +++ b/plugins/discord/plugin/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "code.dopame.me/veonik/squircy3/plugin" + "code.dopame.me/veonik/squircy3/plugins/discord" +) + +func main() { + plugin.Main(discord.PluginName) +} + +func Initialize(m *plugin.Manager) (plugin.Plugin, error) { + return discord.Initialize(m) +}