docker+discordgoで自作discord Bot ~環境構築編
背景
Discordの小規模な鯖運営(身内向け)に関わる機会があったので,Botなるものにも手を出してみようと思い,調べたことをメモしてみました.また,実際に動作を確認するため,発言をそのままオウム返しするBotを作成しました.
Discord Botの作成
Discord Botは,Botというだけあって,基本的には一般ユーザアカウントと同様に振舞います.そのため,discordのディベロッパーサイトからbotを作成し,トークンによって認証を行う必要があります.以下の記事が参考になります.
docker環境の構築
早速本題から外れますが,dockerによる環境構築をします.dockerに興味がない人は飛ばしてください.
Dockerfile
dockerfileの記載内容は以下です.今回は簡単のため,プログラムはmain.goのみとしています.
From golang:1.24.6-alpine
WORKDIR /root/src
COPY ./main.go /root/src
RUN go mod init sample_bot
RUN go mod tidy
RUN go build -o /app/myapp ./main.go
この中で注意すべき点は go mod init <プロジェクト名> と go mod tidy の2行です.
golangはビルド時に go.mod ファイルに記載されたパッケージやバージョンを参照します.このファイルを作成するのがgo mod initです.ただ,簡単な例においては少し冗長なので,プログラムファイル (main.go) でインポートしているファイルを勝手に記載してほしいです.これを実現するのが go mod tidy です.
また,dockerfile内でビルドを完了しておくことで,コンテナ起動時に実行するだけでbotを起動できるようにしています.
docker-compose.yml
services:
app:
build: .
command: /app/myapp
ports:
- 8080:8080
tty: true
env_file:
- ./.env
ymlファイルでは,コンテナ起動と同時にbotを実行するように記述しています.また,環境変数(botが用いるトークンなど)をまとめて反映させるようにしています.
discordgo
Discord Botをgolangで自作する場合,discordgoを使うことになります.
認証
discordgo.New() では,トークンを用いてBotの認証を行います.また, discord.Open()では,そこで作成したインターフェースを用いてwebsocketでセッションを構築します.
discord, err := discordgo.New("Bot " + Token)
err = discord.Open()
イベントハンドラ
イベントハンドラを実装するため,以下のコールバック関数を作成した.ここでは,セッションとイベントを表す2つの引数( s と m)が必要です.セッションを表すオブジェクトは,セッション内で実行可能な関数を含んでおり,イベントを表すオブジェクトは,受信したメッセージの情報(送信者や内容など)を含んでいます.そのため, if u.ID != clientIdでは,送信者のIDがBot自身のID出ない場合にリプライを返すようにしています.
func onMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
clientId := os.Getenv("CLIENT_ID")
u := m.Author
fmt.Printf("%20s %20s(%20s) > %s\n", m.ChannelID, u.Username, u.ID, m.Content)
if u.ID != clientId {
sendReply(s, m.ChannelID, m.Content, m.Reference())
}
}
この実装では,以下の記事が非常に参考にしました.全体の実装は最後に.
実行結果
以下のようにチャット内容を繰り返します.

まとめ
とりあえず動かすことを目的に,簡単なdiscord botをdocker+golangで作成しました.今後はbotの機能追加や,それを前提としたgolangやdiscordgoの勉強を進めたいと思います.discordgo はどうやら様々なインターフェースが構造体になっているようなので(golangってそういうもの?)そのあたり注意したいです.
参考(次回記事参考予定)
付録:今回の実装
package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/bwmarrin/discordgo"
"github.com/joho/godotenv"
)
func main() {
Token := os.Getenv("TOKEN")
fmt.Println("Token",Token)
discord, err := discordgo.New("Bot " + Token)
if err != nil {
fmt.Println("ログインに失敗", err)
}
// Add Event Handler
discord.AddHandler(onMessageCreate)
err = discord.Open()
if err != nil {
fmt.Println("セッションのオープンに失敗", err)
}
defer discord.Close()
fmt.Println("Listening...")
waitForExitSignal()
}
func waitForExitSignal() {
stopBot := make(chan os.Signal, 1)
signal.Notify(stopBot, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
<-stopBot
}
func onMessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
clientId := os.Getenv("CLIENT_ID")
u := m.Author
fmt.Printf("%20s %20s(%20s) > %s\n", m.ChannelID, u.Username, u.ID, m.Content)
if u.ID != clientId {
sendReply(s, m.ChannelID, m.Content, m.Reference())
}
}
func sendMessage(s *discordgo.Session, channelID string, msg string) {
_, err := s.ChannelMessageSend(channelID, msg)
log.Println(">>> " + msg)
if err != nil {
log.Println("Error sending message: ", err)
}
}
func sendReply(s *discordgo.Session, channelID string, msg string, reference *discordgo.MessageReference) {
_, err := s.ChannelMessageSendReply(channelID, msg, reference)
if err != nil {
log.Println("Error sending message: ", err)
}
}
Discussion