GoでDiscordのメッセージリンクを展開してくれるBotを開発した

2024/06/04に公開

はじめに

僕は普段,限界開発鯖でメンバー達とワイワイ遊んだり,開発をしたりしているのですがこのサーバーではルールを守っていれば自分の作成した Discord Bot を自由にサーバーに追加してもいいという文化があります.
参加して数ヶ月ほど経ちますが,自分で Bot を開発したりといったことはまだしていませんでした.ですが,先日 Bot を Go で作る機会があったので,その時のことを少し書いていけたらと思います.

どんな Bot?

Botの動作デモ

この Bot は、同じサーバー内で送信されたメッセージのリンクを貼り付けると、そのリンク先のメッセージを展開してリプライします。

なぜ作ったのか

限界開発鯖では,以前も似たような機能を提供している Bot がいたのですが,いなくなってしまいました.ですが,その Bot の機能がとても便利で無いと少し不便だなと感じることがあり,他のメンバーとも「あの Bot ほしいねぇ~.」という話をしていました.
その話をしていたころ,ちょうど僕も新年度の忙しい期間が終わり余裕ができてきたのでなにか作りたいなと思っていました.元となった Bot のソースコードは公開されていたためそれを参考にすれば自分で作れそうと思い,自分で実装し直すことにしました.

使用したライブラリ等

今回,Discord Bot を作るにあたって以下のライブラリ等を使用しました.

言語 …  Go 1.22
ライブラリ …  discordgo

discordgo は、Go で Discord の Bot を開発するためのライブラリです。スラッシュコマンドなどは作りにくい部分もありますが、今回はメッセージに反応するだけなので、問題なく使用できました。
また、Bot は Dockerfile としてまとめて、自宅サーバーの Docker 上で実行しています。

実装した機能

今回の Bot はシンプルな機能のみを提供することにしました。以下が実装した機能です。

  • サーバーのルールを守る機能
  • NSFW チャンネルからのメッセージをスキップする機能
  • メッセージの展開機能

以下で詳しく解説します。

鯖のルールを守るための機能

限界開発鯖では,ルールを守れば自分の作成した Bot を導入できるのですがその時に守るべきルールの中で今回の開発で特に気をつけないといけないものが「Bot からのメッセージに反応しない」です.何も考えずに,メッセージ作成のイベントが来たときに展開するようにするとこのルールに触れてしまいます.
特に,このサーバーでは特定の単語に対して別のメッセージを返すBotがいるため,対策する必要がありました.

実装は簡単で,discordgo でイベントハンドラを作成する際に渡される構造体にはメッセージの作成者を格納しているフィールドがあります.このフィールドには,送信したユーザーが Bot かどうかを格納しているフィールドがあるため,このフィールドを参照し,Bot の場合はスキップするように実装しました.

NSFW チャンネルからのメッセージをスキップする機能

この鯖では,自由を重んじているため基本的にどのような内容の話でも OK ですが,やはり人前では見にくいものを貼ろうとする人もいます.そういった人のために,専用の チャンネルがあり NSFW の画像などは,そのチャンネルに貼り付けられています.
故意にメインチャンネルに貼り付ける人はいないですが,間違って貼り付けることが起こらないとは言い切れないためメッセージが NSFW チャンネルから送信された場合は展開しないようにしました.

この機能を実現するには,メッセージが送信されたチャンネルの情報を取得する必要があるのですが,discordgo にメッセージが送信されたチャンネルのデータを取得する関数があります.
今回は,その関数を使いチャンネルのデータを保存する構造体を取得し,その構造体の中にあるNSFWというチャンネルを参照することで判定しました.

展開機能

この Bot のメインとなる機能です.この機能を実装するにあたって以下のようにすることに決めました.ただ,実装している中でこの方が良いなと変えた部分も有るのでこの限りじゃないです.

  1. 同じサーバー内のメッセージの場合のみ展開する
  2. 画像が含まれている場合,画像を Embed のサムネイルとして使用する.
  3. 複数の画像が含まれている場合,1枚目の画像をサムネイルにする
  4. 展開時に,メッセージを送信した人にメンションし対象メッセージへの返信とする.

実装

この Bot のソースコードは,以下のリポジトリで公開しているので興味のあるかたは覗いてみてください.
https://github.com/aqyuki/expand-bot

メインとなる展開機能は,discord/hook.go内で実装しています.
discordgo から,MessageCreate のハンドラが呼び出されるとまず最初にメッセージの作成者が Bot かどうかのチェックが入ります.Bot の場合,このあとの処理は行う必要がないのでreturnで関数を終わらせています.
その後,extractMessageLinksという関数を呼び出します.

discord/hook.go
var rgx = regexp.MustCompile(`https://(?:ptb\.|canary\.)?discord\.com/channels/(\d+)/(\d+)/(\d+)`)

func extractMessageLinks(s string) []string {
	return rgx.FindAllString(s, -1)
}

extractMessageLinks という関数内では,送信されたメッセージからメッセージリンクを取得しています.もし含まれていなかった場合は空のスライスが返されます.
その後,取得したリンクからサーバー ID,チャンネル ID,メッセージ ID を取得しています.取得する処理は,extractMessageInfo関数内で正規表現などを使わずに以下のようにして実装しています.

discord/hook.go
type message struct {
	guild   string
	channel string
	message string
}

func extractMessageInfo(link string) (info message, err error) {
	segments := strings.Split(link, "/")
	if len(segments) >= 4 {
		return message{
			guild:   segments[len(segments)-3],
			channel: segments[len(segments)-2],
			message: segments[len(segments)-1],
		}, nil
	}
	return message{}, errors.New("invalid message link")
}

メッセージリンクは,https://(?:ptb\.|canary\.)?discord\.com/channels/(\d+)/(\d+)/(\d+)で表せるのですが,このとき/channels以降の(\d+)は左から順にサーバー ID,チャンネル ID,メッセージ ID となっています.そのため,strings.Splitで分解して返された配列の後ろ 3 個を取得する方針で実装しました.

サーバー ID、チャンネル ID、メッセージ ID を取得した後、同じサーバーから送信されたメッセージかを確認し、送信されたチャンネルの情報を取得します。送信されたチャンネルが NSFW に指定されている場合、この後の処理を終了します。その後、展開したいメッセージのデータを取得し、画像が含まれているかを確認します。画像が保存されているフィールドはスライスになっているため、スライス長が 1 より大きい場合、最初の要素をサムネイル画像として使用します。
取得したメッセージの構造体から本文を取得し、Embed として組み立てます。組み立てられた Embed は embeds というスライスに保存されます。すべてのリンクを Embed に変換した後、返信するメッセージを作成し、送信します。

discord/hook.go
replyMsg := discordgo.MessageSend{
	Embeds:    embeds,
	Reference: m.Reference(),
	AllowedMentions: &discordgo.MessageAllowedMentions{
		RepliedUser: true,
	},
}
if _, err := s.ChannelMessageSendComplex(m.ChannelID, &replyMsg); err != nil {
	logger.Error("failed to send message", zap.Error(err))
	return
}

ここで,s.ChannelMessageSendComplexは引数としてdiscordgo.MessageSendという構造体を取ります.
この構造体は DiscordAPI でメッセージを送信するための JSON と対応しておりこの構造体を直接指定することで Discord API に記載されているオプションを自分ですべて指定して送信することができます.

課題

ひとまず,解決したい課題はこの Bot によって解決することができました.そのため今後は少しづつ内部のコードの品質を上げていきたいと思っています.
特に,テストを一切かけていないため少しずつ追加していけたらと思います.

まとめ

今回 Go を使って Discord の Bot を作成しました.今回の開発を進めていく中でdiscordgoの日本語の情報がほとんどなかったのはとても辛かったです.
最終的に GoDoc や公式の Examples,ソースコードを読んで実装したい機能を作成することができましたが ChatGPT などの力を使わなかったら今回の倍近い時間がかかっていた気がします.
この Bot を作成してサーバーに導入してもうすぐ 2 週間ほど経ちますが,サーバーのメンバーから便利であったり,助かるなどと言われることもあり,作ってよかったなぁと思います.また,今後も機会があれば Discord の Bot を作りたいなと思います.

GitHubで編集を提案

Discussion