私は去年の9月からフィヨルドブートキャンプというスクールでプログラミングの勉強をしており、最終課題でDiscord Botを使った連携サービスを作成しました。本記事ではその紹介をしたいと思います。

経緯

フィヨルドブートキャンプは現在完全にフルリモートでスクールが運営されおり、メンターや生徒同士のコミュニケーション手段としてDiscordを利用しています。

新しく入ってきてくれる受講生が増えていく中で、オンラインで積極的にコミュニケーションをとっていくきっかけがなかなか掴めないな、という人もいると思います。サーバー内でコミュニケーションのきっかけになるBotがあると良いなという声があったので、最終課題として作ることにしました。

具体的には、サービス上に色々な質問を用意しておいて、サーバーのメンバーに好きな質問に答えてもらい、その回答をBotがDiscordに自動投稿するというようなものです。

構成

簡単に構成をかくとこんな感じです。githubのリポジトリはこちら↓

https://github.com/misosoup160/discord-bot-f

discordbotf

主な機能

指定したサーバーのメンバーのみがログインできる

今回はフィヨルドブートキャンプというプログラミングスクールで使うということに特化した方が面白くなっていきそうだなと思ったので、利用者は特定のサーバーに限定するような仕様にして、フィヨルドブートキャンプ専用サービスとして作りました。

login画面

サーバーのメンバーは答えたい質問を選んで回答を入力する

ユーザーは、SKIPボタンで答えたい質問を選んで、それに対する回答を入力できます。

質問に答える画面

回答はDiscordの指定したチャンネルに毎日投稿される

Botによって毎日指定したチャンネルにランダムに選ばれた回答が投稿されます。 サービスへのリンクとランダムに一つ選ばれた質問が例として同時に送信されます。 チャンネルと一度に送る回答の数は環境変数で指定し、投稿時間はHeroku Schedulerで設定しています。

メッセージ画面

自分の回答と過去に投稿されたメンバーの回答を見れる

過去の回答は、自分以外のものはDiscordに投稿済みのもののみ、自分のものは未投稿も含め確認することができます。未投稿の回答は編集削除ができます。

回答一覧画面

管理者は質問の登録・編集・削除ができます

サーバーの管理人(サーバーを作った人)は、サービスの管理者として登録されます。管理者は、任意のユーザーを管理者にすることができます。また、質問の登録・編集・削除ができます。

質問一覧画面

サーバーから去ったメンバーは自動的にサービスから削除される

退会するなどでDiscord側のサーバーからいなくなった場合は、こちらのサービス側からもそのユーザーが自動削除されるようになっています。

Discord APIについて

DiscordのBotや連携サービスをrubyやrailsで書いてる例が少ない気がしており、今回のサービスを作成するにあたって一番苦労したところでもあるので、Discord APIを利用した部分について少し詳しく書いておこうと思います。まだまだ初学者なので情報あやしいところあったらぜひコメントください。今回はWebhookは利用していないので、そちらの話は省略します。

概要

Discord APIには大きく分けて二つのレイヤーがあって、一つはHTTPリクエストを送ってレスポンスとしてDiscordサーバーの情報が返ってくるもの(HTTP API)、もう一つは、リアルタイムのイベントに対して反応できるWebSocketベースの接続で、例えばキーワードを含む発言をするとBotが反応してくれたりするような機能の実装に利用できるものです(Gateway API)。

HTTP APIの方でどんな情報が取れるのかはDiscordのDeveloper Potalから見れるドキュメントのResourcesのところ、WebSocketベースのGateway APIでどんなイベントをキャッチできて、どんなオブジェクトが返ってくるのかはTopicsのGatewayのところあたりを見ていくのかなと思います。必要に応じて、Botに権限を持たせないといけないものもあります。

Discord APIにはそれぞれの言語でいい感じに書けるようにしたラッパーが各種あります。rubyはDiscordrbというgemがあるのでそれを使っています。こちらのドキュメントをDiscordのドキュメントと照らし合わせながら見て書いていきました。

APIを使うにはまずBotを作ってサーバーに招待しないといけないですがここはいろいろなサイトで紹介されていると思うので省略します〜。

実装

今回Discord関連でやりたかった実装は以下の4つ。

  1. Discord認証でログインする
  2. ログインするときその人がサーバーのオーナーかどうか知りたい
  3. ログインするときにサーバー以外の人をログインさせない
  4. Discordサーバーにメッセージを自動投稿する
  5. サーバーから去ったユーザーをサービス側からも削除する

2〜4はHTTP API、5はGatway APIを利用しています。

ログインするときその人がサーバーのオーナーかどうか知りたい

オーナー(owner)というのはDiscordの用語でサーバーを作った人(管理人)のことです。この人だけとりあえずサービスの管理者としたかったために、ログイン時にオーナーかどうか確認するようにしました。

HTTP APIで取得できるサーバー情報の中にowner_idという項目があります。これがオーナーのユーザーIDです。Discord認証でログインさせるときに取得できるuidがこれと一致すればその人がオーナー。

DiscordのサーバーはDiscordrbだとserverだけど、Discordのドキュメントだとguild(ギルド)です。たまに用語が違うのでややこしい。

guild_info = Discordrb::API::Server.resolve("Bot #{ENV['DISCORD_BOT_TOKEN']}", ENV['DISCORD_SERVER_ID'])
owner_id = JSON.parse(guild_info)['owner_id']

ログインするときにサーバー以外の人をログインさせない

これはどういう方法でできるかなーと考えた結果、Discord認証でログインさせるときに取得できるuid(ユーザーID)を利用して、そのIDを持つユーザー情報を取得するリクエストを送り、それが成功するかどうかでそのサーバーにいるユーザーかどうか確かめる、という方法で実装しました。

Discordrb::API::Server.resolve_member("Bot #{ENV['DISCORD_BOT_TOKEN']}", ENV['DISCORD_SERVER_ID'], uid)

これを認証のコードの間に書いておいてリクエストが失敗するとエラーでログインできなくしました。

Discordサーバーにメッセージを自動投稿する

みんなが質問に回答してくれたものをサーバーに送りたい。今回はHTTP APIの方で実装しました。ドキュメントにあるようにBotを作成するときにPermissionsのSendMessagesを許可しておかないといけません。

まずどんな感じのメッセージを送るか考える。

こんなふうに埋め込み(embed)を使ってみたかったので、こちらのembed visualizerを使ってとりあえず構成を考えました。

discordbotf

それをrake taskにします。例えばこんな感じ…?実際中身はもっと長くなったのでモデル以下にファイルを作ってそこにコードを移しています。

namespace :discord_bot do
  desc 'send message to discord guild'
  task send_messages: :environment do
    Discordrb::API::Channel.create_message(
      "Bot #{ENV['DISCORD_BOT_TOKEN']}",
      ENV['DISCORD_CHANNEL_ID'],
      'こんにちは!こちらは毎日サーバーのメンバーのことを紹介するBotです!',
      false,
      {
      	title: '好きな寿司ネタはなんですか?',
      	description: '質問に回答するにはここにアクセスしてね。'
    	}
    )
  end
end

メッセージを毎日同じ時間にDiscordに送信したいので、このタスクをHeroku Schedulerに登録しました。

サーバーから去ったユーザーをサービス側からも削除する

これはGatway APIの方を利用して、メンバーがサーバーから去るイベント(脱退、追放、BAN)をハンドルしています。

このようにメンバーの変更イベントを監視するタイプのものは、デフォルトでは制限がかかかるようになっているらしいので、Botを登録する画面にあるSERVER MEMBERS INTENTをONにしていないと利用できない…はずです。多分(この辺ちょっと理解があやしい)。

bot = Discordrb::Bot.new token: ENV['DISCORD_BOT_TOKEN']
  bot.member_leave do |event|
    user_id = event.user.id
    user = User.find_by(uid: user_id)
    user&.destroy
  end
bot.run

bot.runでBotが起動されますが、サーバーのメンバーが去るイベントを常に監視するためにずっと起動状態(Discordの画面でいうとオンライン状態)にしといてあげないといけない。そのために、これをrake taskにして、Procfileという名前のファイルを作り、そこに適当なプロセス名とこのタスクのコマンドを書いておきます。

bot: rails discord_bot:start

デプロイしたらHerokuのDynosにこのプロセスが表示されるのでONにしてあげるとBotが起動します。

discordbotf

ただ、Herokuの無料枠で使ってるので30分間サービスにアクセスがないとスリープしてしまい、Botもオフライン状態になってしまうようなのでHeroku Schedulerで10分毎に叩き起こすようにしています。

Discord APIを使ったところはこんな感じでやりました〜。

苦労したところ

Discord BotはpythonかJavaScriptで作っている記事が多いのかなと思います。rubyでDiscord Botを作っている例が検索してもあまり出てこないし、Discord APIを理解して、何がどういうふうにできるから、アプリの仕様をどうしたらいいか?と考えるところで結構時間がかかったなーと思います。RailsとBotを別スレッドで同時に立ち上げておくみたいなところが一番分かってなかったかもしれない。また、API関連のテストを書くところでかなり助けてもらいました。難しかったー。

楽しかったところ

実際にフィヨルドブートキャンプで欲しい!という声が多かったものを作ったので、ちゃんと完成させれば実際にこのコミュニティの人たちに使ってもらえるんだというのは、自分にとってはモチベーションになったと思います。

実際仕様を考えるところは、メンターの方を利用者としてヒアリングして、検討しながら進めていたのですが、それも面白い経験でした。今回はその側面はそんなに大きくなかったかもですが、やっぱり問題や需要に対してどういう対処ができるのか、というのを考えていくのは自分が楽しいと思ってる部分なんだなぁと思います。

あとは、フィヨルドブートキャンプのプラクティスで、最小限の機能に限ってつくる、小さいサイクルで回していく、というアジャイルやスクラムの考え方についても勉強していたので、それをなんとなく意識して進めていけたのも良かったかなと思います。はじめにAPIについて調べて仕様を考えるのに2週間くらい、そのあと一気に完成までに必要なissueを登録して見通しを立て、初めの一週間で大まかな画面を全て作り、次の一週間で抜けているところを埋めていき、三週目で完成度を上げていく、という感じで進めました。Discord APIのことがわかってくるといろいろ他にも機能をつけたくなってくるんですが、そこは堪えてとりあえずみんながちゃんと使える状態のものを一度完成させようと思ってカリカリ書きました。

今後

やってみたいなという機能としては、一度に投稿する回答の数やチャンネルをコマンドなどで変更できるようにするとか。Discordサーバー側でついているロールの変更もリアルタイムで取得できるようなので、ロールを取得、記録してそれを用いて管理者権限を変えたり質問を分けて管理したりなどできても面白いかなと思っています。

また、フィヨルドブートキャンプの中でもまだ他にDiscordに欲しいなという機能がちらほらあるように思うので、これを機会にDiscord Botや連携サービスが色々作られていくと楽しいなと思っています。