Discord Bot 開発あれこれ

2020-10-10

身内コミュニティ向け discord bot を複数人で開発しているので,有用そうなレシピを書いていきます.



環境

python の discordAPI ラッパーである discordpy 製 bot を github で管理し,heroku 上で動かしています.



heroku の設定


基本


時間を正しく扱うために


Heroku の仕様

  • 24 時間に 1 回再起動
  • ファイルは/tmp/以下でのみ書き込み可能.
  • 再起動時にファイルなどはリポジトリの状態に初期化される



Cog で機能を分けて開発

Cog というのは,discordpy で提供されている,機能を分割するためのクラスです. メインの Bot に Cog を追加していく形になります.
最低限の要素だけで構成されたテンプレートです.


構成

run-bot.py

bot.add_cog(cog)で Cog を追加します

bot = Bot(command_prefix=["!"])
bot.add_cog(YourCog(bot))
bot.run(TOKEN)


your_cog.py

Cog クラスを継承した YourCog クラスを定義します. コンストラクタ__init__に取り込まれる先の bot を与えて保持します. これにより YourCog 内でも bot にアクセスできます.例えば,bot 自身の User は,self.bot.userでアクセスできるわけです.

class YourCog(commands.Cog):
    def __init__(self,bot):
        self.bot = bot



Help のカスタマイズ


デフォルトのヘルプは英語だし見にくいので,おしゃれにしてあげたいです. commands.HelpCommand を継承したクラスのメソッドをオーバーライドして実装します. クラスは,Bot クラスのコンストラクタで指定します.
send_bot_help メソッドを上書きすれば,!helpコマンドが呼ばれたときの動作を指定できます.今回は Embed を使ってリッチにしてみました.

class Help(commands.HelpCommand):
    def __init__(self):
        super().__init__()

    async def send_bot_help(self,mapping):
        description = ""
        for cog,commands in mapping.items():
            if not cog or type(cog) == Core:
                continue
            for command in commands:
                description += "`?{}` : {}\n".format(command.name,command.description if command.description else cog.qualified_name)

        embed=Embed(title="コマンド一覧", description=description, color=0xedff66)
        await self.get_destination().send(embed=embed)

bot = Bot(command_prefix=PREFIX,help_command=Help())
bot.run(TOKEN)

その他にも書き換えられる要素があるので,以下を参考にしてみてください. Discord.py Bot Commands Framework の help コマンドについて - Qiita



Reaction をボタンとして利用


例えば,〇と ✖ のリアクションが押されたメッセージを用意して,YES か NO を訪ねたいときに使います. タイムアウト時間も指定できます.押された絵文字が返されます.

"""
messageにリアクションをし,最初に押されたリアクションを返す.
allow_userにのみ反応.
"""
async def wait_button(bot,message: Message,emoji_strs: List[str],allow_user=None,timeout=None) -> Emoji:
    for e in emoji_strs:
        await message.add_reaction(e)

    def check(react,user):
        if allow_user:
            is_author = user == allow_user
        else:
            is_author = True
        return str(react.emoji) in emoji_strs\
            and react.message.id == message.id\
            and is_author
    reaction, user = await bot.wait_for('reaction_add',check=check,timeout=timeout)
    return reaction.emoji



文字入り画像を生成

pillow(PIL の後継)で文字入れした画像を投稿します. 以下は最低限文字入れするならの記事です. pillow で図形に文字を挿入 - Qiita
文字数に合わせていい感じの大きさにしたり,縁取りなどの装飾や縦書き対応もしたんですが,長くなりそうなので後日別記事で書きます.



bot にユーザーとして発言させたい

  1. A さんが「おわりだ」と発言
  2. Bot が検知
  3. 投稿が画像に変更される


アプローチ

  1. 特定のフレーズを検知
  2. A さんの投稿を削除
  3. A さんのアイコンと名前を取得
  4. 取得した情報で Webhook を送信
HOOK_NAME = "BOT_HOOK"

async def on_message(msg):
if msg.content == "おわりだ":
    hooks = await msg.channel.webhooks()
    bot_hook = next(filter(lambda h:h.name==HOOK_NAME,hooks),None)
    if not bot_hook:
        bot_hook = await channel.create_webhook(name=HOOK_NAME)

    await  msg.delete()

    await  bot_hook.send(
    username=msg.author.display_name,
    avatar_url=msg.author.avatar_url,
    file=img)



おわりだ

適宜追加していく予定です.
おわり