経緯
いつもはPythonでDiscordのBotを作っているのですが、
公開するBotとしてはパフォーマンスが良さそうなGoで書いたら高速で動作させられるんでね?
と浅はかな私はdiscordgoに触れてみました。
本記事はGoでDiscordのSlashcommandを作成してするまでの内容です。
Go初心者が書くものなので間違っている部分もあるかもしれないです。
discordgoに関する日本語の情報がほとんどないです
とりあえずdiscordgoに関する記事として以下を参考をしましたが、
Slashcommandはわりと最近のため、その類の情報が乗っていませんでした。
公式のExampleを参考にして書いてみる
GitHubに公開されている公式のExampleの中に書き方が乗っているので、それを参考にコーディングしていきます。
ただ、書いてみたのですがイマイチ書きづらいです。
以下Exampleのコマンド登録部分を要約したものです。
func init() {
// コマンドのフロント側を定義
commands = []*discordgo.ApplicationCommand{
{
Name: "hi",
Description: "説明",
},
}
// mapにフロント側のコマンドの名称で関数を定義
commandHandlers = map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate){
"hi": func(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "hi",
},
})
},
}
}
これはPythonで書くとこれぐらい短い単純なコードです。
@bot.slash_command(description="説明")
async def hi(ctx):
ctx.respond("hi")
Goで書いた方はフロント部と関数が分かれており、関数を呼び出すmapの文字列とコマンドの文字列が別々に定義されているためあまり好ましい実装ではないと思います。
対処法としては変数に"hi"文字列を入れて共通させれば良い気もしますが、公式のExampleの様にまとめてフロント部を書いて関数のmapを書いた場合では、どちらにせよ可読性が悪くなってしまいます。
とりあえずまとめて定義する方法を考える
とりあえずPycordのフレームワークを参考に同時に定義できれば少しはマシになるかなと思い以下の様にaddCommand関数を定義してみました。
package commands
import (
"fmt"
"github.com/bwmarrin/discordgo"
)
var (
Commands = make([]*discordgo.ApplicationCommand, 0)
CommandHandlers = make(map[string]func(s *discordgo.Session, i *discordgo.InteractionCreate))
)
func addCommand(command *discordgo.ApplicationCommand, fn func(s *discordgo.Session, i *discordgo.InteractionCreate)) {
_, exist := CommandHandlers[command.Name]
if exist {
panic(fmt.Sprintf("[%s] ← このコマンド名が重複しています!", command.Name))
}
// コマンド部分のNameをそのままmapのKeyとして設定しておく
CommandHandlers[command.Name] = fn
Commands = append(Commands, command)
}
package commands
import (
"github.com/bwmarrin/discordgo"
)
func init() {
// Nameで定義された文字列がKeyになるので同時に書ける
addCommand(
&discordgo.ApplicationCommand{
Name: "hi",
Description: "説明",
},
func(s *discordgo.Session, i *discordgo.InteractionCreate) {
s.InteractionRespond(i.Interaction, &discordgo.InteractionResponse{
Type: discordgo.InteractionResponseChannelMessageWithSource,
Data: &discordgo.InteractionResponseData{
Content: "hi!",
},
})
},
)
}
DiscordのSlashcommandの仕様として登録時に同名のコマンドを送信した際に400エラーを返す仕様なので、予めこちらで重複するコマンドは弾いています。
Golangを初めて触るのでイマイチpackageの扱いなどがわかっていないですが、
以下の様なディレクトリ構造で考えています。
・
┣ main.go
┗ commands
┣ base.go
┣ hello.go
┗ ...
discord.pyなどのCogsの様にファイルごとに機能が分かれるようなイメージで構築できるので、まぁまぁ実装しやすい気がします。
mainから呼び出す際は以下のような形にしています。
package main
import (
"fmt"
"os"
"os/signal"
"testbot/commands"
"github.com/bwmarrin/discordgo"
"github.com/joho/godotenv"
)
// .envファイルで定義します
var (
GuildID string
BotToken string
RemoveCommands bool
)
var s *discordgo.Session
func init() {
err := godotenv.Load(".env")
if err != nil {
panic(fmt.Sprintf("Dotenv read error: %v", err))
}
GuildID = os.Getenv("GUILDID")
BotToken = os.Getenv("TOKEN")
RemoveCommands = os.Getenv("REMOVECOMMAND") == "true"
s, err = discordgo.New("Bot " + BotToken)
if err != nil {
panic(fmt.Sprintf("Invalid bot parameters: %v", err))
}
}
var (
commandList = commands.Commands
commandHandlers = commands.CommandHandlers
)
func init() {
s.AddHandler(func(s *discordgo.Session, i *discordgo.InteractionCreate) {
if h, ok := commandHandlers[i.ApplicationCommandData().Name]; ok {
h(s, i)
}
})
}
func main() {
s.AddHandler(func(s *discordgo.Session, r *discordgo.Ready) {
fmt.Printf("fmtged in as: %v#%v", s.State.User.Username, s.State.User.Discriminator)
})
err := s.Open()
if err != nil {
panic(fmt.Sprintf("Cannot open the session: %v", err))
}
fmt.Println("Adding commands...")
registeredCommands := make([]*discordgo.ApplicationCommand, len(commandList))
for i, v := range commandList {
cmd, err := s.ApplicationCommandCreate(s.State.User.ID, GuildID, v)
if err != nil {
s.Close()
panic(fmt.Sprintf("Cannot create '%v' command: %v", v.Name, err))
}
registeredCommands[i] = cmd
}
defer s.Close()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
fmt.Println("Press Ctrl+C to exit")
<-stop
if RemoveCommands {
fmt.Println("Removing commands...")
for _, v := range registeredCommands {
err := s.ApplicationCommandDelete(s.State.User.ID, GuildID, v.ID)
if err != nil {
fmt.Printf("Cannot delete '%v' command: %v", v.Name, err)
}
}
}
fmt.Println("Gracefully shutting down.")
}
基本的にはExampleの内容で、.env
ファイルから情報を取得するように変更しているような形です。
ここまで書いてみてわかったのはPythonで書けるBotの手軽さが素晴らしいということでした。