Getting started¶
By the end of this you'll have a bot that reads a Discord channel and answers in a thread, with no Discord SDK anywhere in your own code. Then you'll make it read-only, and make it tell you when a reconnect lost messages.
Allow about 15 minutes, plus Discord setup if the bot does not exist yet.
Before you start¶
You'll need:
- Go 1.27 or later.
- A Discord application with a bot user, and its token.
- A test guild the bot is a member of. Use one you own, because this posts messages.
- The Message Content intent enabled for your application in the Discord
developer portal. Without it every message arrives with an empty
Content, which looks exactly like a bot that is not working. - The guild's snowflake and one channel's snowflake. Turn on Developer Mode in Discord to copy them.
Install¶
The contract and the provider are separate modules. You depend on the contract; the provider is a blank import.
go get gitlab.com/phpboyscout/go/chat-platform
go get gitlab.com/phpboyscout/go/chat-platform-discord
Connect and read¶
package main
import (
"context"
"log"
"os"
chatplatform "gitlab.com/phpboyscout/go/chat-platform"
_ "gitlab.com/phpboyscout/go/chat-platform-discord"
)
func main() {
ctx := context.Background()
// One client is one connection. Declare what you will use: the provider
// asks the platform for that and nothing more.
c, err := chatplatform.NewClient(ctx, "discord", chatplatform.ClientConfig{
Token: os.Getenv("DISCORD_TOKEN"),
Needs: []chatplatform.Need{chatplatform.NeedMessages},
})
if err != nil {
log.Fatal(err)
}
defer c.Close()
// One provider is one space. Serving several means several of these, over
// the same connection.
p, err := c.Provider(ctx, "1531227937678954747",
chatplatform.WithAllowedChannels("1531227938622800055"),
)
if err != nil {
log.Fatal(err)
}
defer p.Reader.Close()
if err := p.Reader.Connect(ctx); err != nil {
log.Fatal(err)
}
for msg := range p.Reader.Messages() {
if _, err := p.Actor.ReplyInThread(ctx, msg.Ref(), "Re: "+msg.Author.Name, "Looking into it."); err != nil {
log.Print(err)
}
}
}
Three things are worth noticing.
NewClient takes a name. The provider registered itself when it was
imported, and NewClient looks it up; it returns ErrNotFound if nothing is
registered under that name. Swapping platforms is a configuration change and an
import, not a code change.
AllowedChannels is enforced by the provider. An empty allowlist permits
nothing. A watchlist that silently means everywhere is the wrong default for
reading people's messages, so it fails closed.
Connect is where the network happens. NewClient and Provider validate
configuration and nothing else, so you can construct a provider, inspect what it
supports, and test all of it without credentials.
Run it, then post something in the allowlisted channel. A thread appears on your
message, named Re: <you>, with Looking into it. inside it.
If nothing happens, the two usual causes are an empty AllowedChannels (which
permits nothing rather than everything) and the Message Content intent not
being enabled.
Read without being able to write¶
Pass WithReadOnly and the provider comes back with no Actor at all.
p, _ := c.Provider(ctx, guild,
chatplatform.WithAllowedChannels(channels...),
chatplatform.WithReadOnly(),
)
if p.Actor != nil {
panic("a read-only scope has no Actor")
}
It is per scope, so one tenant can observe while another acts on the same connection.
There is no flag to check and no method that refuses at call time. A deployment that cannot post cannot delete a message either, and that is a property of the types rather than of a check somebody remembers to write.
Use an optional capability¶
Moderation, member lookup, interactive components and slash commands are optional. Ask for them by type assertion:
if mod, ok := chatplatform.AsModerator(p); ok {
if err := mod.DeleteMessage(ctx, ref, "off-topic"); err != nil {
log.Print(err)
}
}
A provider that has none of them is legitimate, which is why they are not part
of Actor. On a read-only provider the answer is always false for these,
because there is no Actor to assert against. VoiceParticipants and
ReactionObserver are the two that survive, since they hang off the Reader.
See Optional capabilities for why, and the capability reference for what each one offers.
Notice a reconnect that lost messages¶
if st := p.Reader.State(); st.LastReconnectLostEvents {
log.Print("reconnect re-identified; messages during the gap were dropped")
}
This is the failure worth alerting on, and it is invisible without being told. See Reconnects and lost events.
Where to go next¶
- Get a moderator's approval before the bot posts: buttons, a prefilled form, and a human gate in front of everything the bot says.
- Configuration: every
ClientConfigandScopefield, its default, and what happens when it is wrong. - What this contract does not do: worth reading before you plan anything around it.
- Author a provider for a platform not covered yet.
- Run the conformance harness against it.