先日卒業課題でつくったDiscordの連携サービス、「Discord Bot F」のフィヨルドブートキャンプ のサーバーでの運用が開始されました。やっぱり実際に利用してくださると嬉しいですね。

他の受講生の方もBot作りに挑戦してみてくれる方が出てきて、良い流れのような気がしています。プラクティスだけだとどうしてもずっと勉強という感じで実際自分が使いたいものを作って楽しむ機会ってあまりなくなってしまうのですが、Discord botは実際にサーバーのメンバーに使ってもらえて達成感もあるしちょうど良いのかもしれないですね。

ただ、やっぱりRubyでDiscord APIを扱う情報がかなり少ないのが大変なところなのかなと思いました。私もはじめの理解に結構時間かかりましたし、あまり時間に余裕がない方には勧めにくいかもしれません。お手軽感はないかもしれないですけど、何もわからないところから一生懸命ドキュメントを読んだりしたことは、すごく良い勉強になったなーと私は思っています。

さて、私の作ったサービスも早速ミスや改善した方がよいことが出てくるもので、最近その修正をしたのでそれも記事に残しておこうと思います。

Discordで変更したユーザー情報を反映させる

これ、必要だろうなと思いつつ実装してなくて(え)、やっぱり不具合出たので即修正しました。すいません。

Discordのサーバーから取得しているユーザー情報は今のところ、ユーザーネーム、#以降の4桁の数字(discriminator)、アイコン画像のurl、なんですけども、これらをDsicord上で変更してもサービス上ではそれが反映されるようになっていなかったために画像が非表示になってしまったりしました。

メンバー情報が変更された時のイベントを取得できるので、そのイベントが発生した時にこれらの情報が更新されていたら変更する、というコードを追加しました。

class DiscordBot
  def initialize
    @bot = Discordrb::Bot.new token: ENV['DISCORD_BOT_TOKEN']
  end

  def start
    settings
    @bot.run
  end

  def settings
    # banや脱退などサーバーからメンバーがいなくなったらそのユーザーを消去
    @bot.member_leave do |event|
      user_id = event.user.id
      user = User.find_by(uid: user_id)
      user&.destroy
    end

    # メンバーのusername, discriminator, avatarの更新を反映する
    @bot.member_update do |event|
      uid = event.user.id
      updated_member = JSON.parse(Discordrb::API::User.resolve("Bot #{ENV['DISCORD_BOT_TOKEN']}", uid))

      name = updated_member['username']
      discriminator = updated_member['discriminator']
      avatar = avatar_url(uid, updated_member['avatar'], discriminator)

      user = User.find_by(uid: uid)
      if user
        user.update!(name: name) if user.name != name
        user.update!(avatar: avatar) if user.avatar != avatar
        user.update!(discriminator: discriminator) if user.discriminator != discriminator
      end
    end
  end

  private

  def avatar_url(uid, avatar_id, discriminator)
    if avatar_id
      Discordrb::API::User.avatar_url(uid, avatar_id)
    else
      Discordrb::API::User.default_avatar(discriminator)
    end
  end
end

ついでにクラスの構成もちょっと変えてます。member_updateでメンバー情報の変更イベントを取得できるんですけど、eventに入ってくるユーザー情報は変更前のものだったので、そこで取得したユーザーIDからもう一度ユーザー情報を取得して、変更があったものを更新するようにしてます。

ユーザーアイコンの情報(avatar)はidだけが取れるのでそこからDiscordrb::API::User.avatar_url(uid, avatar_id)でurlを作っています。また、Discord上のアイコン画像が未設定、つまりavatarがnilの場合はDiscordのデフォルト画像urlが取得できるメソッドがあったのでそれでurlを作るようにしました。discriminatorで色が決まってたんですね。

member_updateってドキュメントSent when a guild member is updated. This will also fire when the user object of a guild member changes.とあるけど、どの情報の更新まで入るのかなぁ。

Discordでアイコン画像を未設定の場合の修正

上の修正をしててDiscordのデフォルトアイコンurl、取れるんじゃんって気が付いたんですね。私はDiscordのアイコンが未設定の場合は用意したデフォルト画像が表示されるように書いていました。ですが、それもomniauth-discordの挙動をちゃんと確認していなくて、思うような挙動にならないコードになっていたので修正しました。

元々、アイコン画像のurlはavatar = auth_hash[:info][:image]で取得していました。このimageがアイコン画像が未設定だったらnilになると思っていたのですが、無効なurlができるような仕様になっていました。

そこで、avatar = auth_hash[:extra][:raw_info][:avatar]でavatarのidを取得して先ほどのようにurlを作るようにしました(以下のコードの部分)。こちらだとアイコン画像が未登録の時はnilが入ってくるのでうまくいきます。

  def avatar_url(uid, avatar_id, discriminator)
    if avatar_id
      Discordrb::API::User.avatar_url(uid, avatar_id)
    else
      Discordrb::API::User.default_avatar(discriminator)
    end
  end

この変更でDiscordのデフォルト画像を利用するようになり、こちらで用意したデフォルト画像を使わなくなったので、環境変数IMAGE_URL_HOSTを設定する必要がなくなりました。

このomniauth-discordの挙動はちょっとおかしいのでは?と思ったらissueがたっているようですね。対応はされてないけど…。

今後もなにか不具合や、Discord APIの勘違い等ありましたら報告してくださると嬉しいです。