GoでDiscordBot

こんにちは。nanaseです。
以前書いていたGolfBotというDiscordBotをnode.jsからGoにフルリプレースしました。
その時の知見などをまとめ。

開発環境

移行前
node.js v14.4.0
discord.js v12.2.0

移行後
Go 1.15.0
discordgo v0.22.0

モチベーション

Goに書き換えるモチベーションとしては以下

・JSの動的型付けが辛い
 実際実行して始めてクラッシュするとかがあった

・純粋にGoの記法が好きだから
 シンプルで良いですよね

さて、以下に知見などをまとめていきます。

起動

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/bwmarrin/discordgo"
)

func main() {
	dg, err := discordgo.New("Bot " + os.Getenv("BOT_TOKEN"))
	if err != nil {
		fmt.Println("error creating Discord session,", err)
		return
	}

	dg.AddHandler(handlers.MessageCreate)

	err = dg.Open()
	if err != nil {
		fmt.Println("error opening connection,", err)
		return
	}

	fmt.Println("Bot is now running. Press CTRL-C to exit.")
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
	<-sc

	dg.Close()
}

起動はこのような感じ。

dg, err := discordgo.New("Bot " + os.Getenv("BOT_TOKEN"))

ここでdiscordgoの初期化を行います。
環境変数 "BOT_TOKEN" にTokenを入れます。

で、以下でハンドラの設定をしています。

dg.AddHandler(handlers.MessageCreate)

ハンドラはhandler interfaceに準拠しているものを設定できます。
handlers.MessageCreateは以下のインタフェースです。

func MessageCreate(s *discordgo.Session, m *discordgo.MessageCreate)

基本的にhandler interfaceは以下の構造になっています。

func(s *discordgo.Session, m *discordgo.<イベント名>)

イベントの一覧は こちらにあります。

付けたいハンドラをAddしたら後はOpenするとBotが起動します。
後はSignal検知するまで待機して、検知したらCloseして終了という流れです。

ボイスチャンネルに入った時に処理をする

Goへのリプレースで一番困った部分。
ボイスチャンネルのアップデートハンドラは以下のIFなのですが

func VoiceStateUpdate(s *discordgo.Session, v *discordgo.VoiceStateUpdate)

discordgo.VoiceStateUpdateからは現在の状態しか取得できません。
つまりdiscord.jsのようにoldStateを取得することができないのです。

そのため、各ユーザーが現在どのボイスチャンネルにインしているか、ということを記録しておく必要があります。

保存のために以下のstructを用意しました。

type UserState struct {
	User        *discordgo.User
	CurrentVCID string
}

で、これをhandlerパッケージ内で保持する仕組みを採用しました。

package handlers

import (
	"fmt"
	"github.com/bwmarrin/discordgo"
)

var (
	usermap = map[string]*models.UserState{}
)

func VoiceStateUpdate(s *discordgo.Session, v *discordgo.VoiceStateUpdate) {
	_, ok := usermap[v.UserID]

	// 新規ユーザー
	if !ok {
		usermap[v.UserID] = new(models.UserState)
		user, err := s.User(v.UserID)
		if err != nil {
			fmt.Println(err)
			return
		}
		usermap[v.UserID].User = user
	}

    oldVCID := usermap[v.UserID].CurrentVCID  // 前回のボイスチャンネルのID
	usermap[v.UserID].CurrentVCID = v.ChannelID  // 今回のボイスチャンネルのIDをセット
}

このようにすることで、前回VoiceStateUpdateが走った時のVoiceChannelとの比較が可能になります。
oldVCIDとv.ChannelIDの値が違った場合には新規でボイスチャンネルに入ったことと判定が可能です。

〇〇をプレイ中と表示する

こういうやつ

画像1

とりあえず簡易的に表示するには以下でOK

dg.UpdateStatus(1, "Golf It!")

Herokuで動かす

今回もインフラにはHerokuを採用しました。
Herokuで動かすにはまず依存関係の解決のため、gomodを使います。

$ go mod init
$ go build

こちらで初期化して、go.mod/go.sumを生成します。

あとはProcfileにWorkerにバイナリの実行を指定しておきます。

worker: golf-bot

これでHerokuにpushして起動すれば完了です!

まとめ

DiscordBotをnode.jsからGoに移行してみて

良かったこと
・静的型付けによってコンパイル時にエラー検知ができるようになった。
・純粋にコード書くモチベーションが上がった

悪かった(?)こと
・jsに比べて日本語の情報量がかなり減った

という印象です。
実際日本語情報は少なくて、公式のGoDocを一番参考にしました。

そんなわけでGoへのリプレースが終わってやる気も十分なので今後もBot開発していこうと思います!
インフラもDocker乗せたりCloudRunに移行したりする予定?

この記事が気に入ったらサポートをしてみませんか?