🚀

MiPAというMisskeyのBotフレームワークを作った話

2023/02/25に公開約6,200字

はじめに

この記事では私が作成しているBotフレームワークであるMiPAとそれのコアに当たるMiPACを紹介します。

MiPAという名前について

この名前になる前に実は Mi.py という名前で開発していました。discord.pyのようにmisskey.pyとしたかったという気持ちは無くもないのですが、既に使用されていたため、Mi.pyにし、コードに大幅な書き換えが必要になり、2つのライブラリに分けることになったため、MisskeyPythonAPIMisskeyPythonAPICoreの2つにしました。結果的にMiPAMiPACという名前になった感じです。

なんで作ろうと思ったか

私がMisskeyを始めたのは3年程前になりますが、当時DiscordのBotを作ることにハマっており、Misskeyでも同じようにBotを作りたいなと思ったのが始まりです。その時点で Misskey.pyを使いBotを作る事には成功しましたが、あくまでMisskey.pyがやってくれるのはAPIへのアクセスのみで、戻り値などは自身でドキュメントを見る必要があり、またWebSocketは自分で作成する必要がありました。そこでDiscord.pyみたいに決まった形に書けば動くフレームワークが欲しいと思い作成を始めました。

Misskey.pyとMiPACの違い

項目 misskey.py MiPAC
戻り値に型があるか
トークンの生成をサポートしているか
ドキュメントが充実しているか
エンドポイントがすべてサポートされているか
最新のエンドポイントと古いエンドポイントの互換性があるか
テストがあるか
LICENSE MIT MIT
必要なPythonのバージョン Python3.7以上 Python3.11以上

大きな違いとしてはMiPACにはモデルがあるということです。これはどういうことかというと以下のようなコードが書けるということです。

async def main():
     client = Client("url", "token")
     await client.http.login()
     api = client.api
     created_note = await api.note.action.send('hello world')
     await created_note.api.favorite.action.add()
     await created_note.api.reaction.action.add('😊')
     await created_note.api.action.delete()

MiPACの大きな特徴2つ目はメソッドチェーンである事です。何故、apiactionという紛らわしいものが2つも出てきているのかというと、actionにあたる場所はclient.api.*で使用している場所と同じものを使用しているため、note.action.action.deleteのようにactionが2重になってしまいます。そのため、モデルが持つ apiへのアクセスはapiというプロパティになっています。

また、必要なPythonのバージョンが極めて高いのはよりよい型を提供することで開発者体験を向上させる為です。

実際にMiPACを用いた開発の補完の例

MiPAについて

MiPAはDiscord.pyのソースコードを読みながら、少しずつDiscord.pyのコードを移植し作成しました。そのため、ファイルの中には大量にDiscord.pyのライセンスが書いてあります。完全にコピーしているわけではなく、私好みに編集しているため、Discord.pyが丁寧にやってるか所を省略したりしています。また、Cog機能ももちろん実装しています。これが一番大変でした。

インストール

# 恐らく安定しているもの
pip install mipa

## 最新の成果物(gitが必要です)
#  また、MiPACを先にインストールしたのちにMiPAをインストールする必要があります
pip install git+https://github.com/yupix/MiPAC.git
pip install git+https://github.com/yupix/MiPA.git

MiPAでノートを受信してみる

まずは何も言わずに以下のコードをコピーし、トークンやインスタンスのURLを入力して実行してい見ましょう

import asyncio

from aiohttp import ClientWebSocketResponse
from mipac import NoteDeleted, PartialReaction, NoteReaction, Note

from mipa.ext.commands.bot import Bot


class MyBot(Bot):
    def __init__(self):
        super().__init__()

    async def on_ready(self, ws: ClientWebSocketResponse):
        await self.router.connect_channel(['main', 'home'])
        print(self.user.username)

    async def on_note(self, message: Note):
        print(message.author.name, message.content)

if __name__ == '__main__':
    bot = MyBot()
    asyncio.run(bot.start('instance url', 'token'))

Discord.pyっぽくないですか?結構似てると思うんですけど

さて、説明に入ります。
イベントなどは見ればある程度分かると思うので、省かせていただきます。
self.router.connect_channelというメソッドはチャンネル名を入力することで指定したチャンネルに接続できます。チャンネルって何?って方のために説明するとMisskeyにはホームタイムラインやグローバルタイムライン、ローカルタイムラインといった様々なタイムラインがあります。そういったタイムラインに接続するのがこのメソッドの役割です。

cogを使ってみる

今回は試しに @botの名前 helloと打つと こんにちは!○○ さんと返すコマンドを作ってみます。
また、以下のようなディレクトリ構造で作成していきます。

.
├── cogs
│   └── basic.py
└── main.py
main.py
import asyncio

from aiohttp import ClientWebSocketResponse
from mipac import Note

from mipa.ext.commands.bot import Bot

COGS = ["cogs.basic"]

class MyBot(Bot):
    def __init__(self):
        super().__init__()

    async def on_ready(self, ws: ClientWebSocketResponse):
        for cog in COGS:
            self.load_extension(cog)
        await self.router.connect_channel(['main', 'home'])
        print(self.user.username)

    async def on_note(self, message: Note):
        print(message.author.username, message.content)


if __name__ == '__main__':
    bot = MyBot()
    asyncio.run(bot.start('url', 'token'))
basic.py
class BasicCog(commands.Cog):
    def __init__(self, bot: Bot) -> None:
        self.bot = bot
    
    @commands.mention_command(text='hello')
    async def hello(self, ctx: Context):
        await ctx.message.api.action.reply(f'こんにちは! {ctx.message.author.username} さん')

def setup(bot: Bot):
    bot.add_cog(BasicCog(bot))

このように、Discord.pyと似た感じでコマンドを実装できます。

正規表現を用いてタイマーコマンドを作ってみる

先ほど作成したbasic.pyを編集する形で作成します。

class BasicCog(commands.Cog):
    ...

    @commands.mention_command(regex=r'(\d+)秒タイマー')
    async def timer(self, ctx: Context, time: str):
        await ctx.message.api.action.reply(f'{time}秒ですね。よーいドン!')
        await asyncio.sleep(int(time))
        await ctx.message.api.action.reply(f'{time}秒経ちました!')

MiPACでは正規表現で見つかったgroupを展開してそのコマンドのメソッドへと送信します。そのため (.*)は(.*)であると書くと2つ分の引数が必要になります。

今回はタイマーを作成しましたが、このコマンドはまだ未完成です。正規表現で以外にも等を受け取れるようにし、更に利便性を高めるといった使い方をしてみましょう!

この記事の執筆中におふざけでJavaのLongMaxValueを入れるなどして遊びましたが、こういった明らかに生きてる間に来ないような物も防いだりしてもいいかもしれません

MiPACについて

インストール

# 恐らく安定しているもの
pip install mipac

## 最新の成果物(gitが必要です)
pip install git+https://github.com/yupix/MiPAC.git

ノートを投稿してみる

MiPAを使うにしても内部で使っているのはMiPACです。MiPACの知識は知っても損しません!
MiPACの主な使い道としてはCLI等のBotとは違い、一時的にリクエストを送る際に使用することを想定しています。もちろんMiPACを組み込んでMiPAのようなフレームワークを作ることも可能です

main.py
import asyncio

from mipac.client import Client


async def main():
    client = Client("url", "token")
    await client.http.login()
    api = client.api
    await api.note.action.send('hello world')

    await client.close_session()


if __name__ == '__main__':
    asyncio.run(main())

ドキュメントについて

ドキュメントは現在最新のものが存在しません(作成中の為)。そのため、MiPAの前身であるMi.pyのドキュメントが少し役に立つ場合があります。また、古くなっていますが、MiPAのドキュメントもあります

リポジトリ

http://github.com/yupix/mipa
http://github.com/yupix/mipac

最後に

長くなりましたが、以上です。ここまで読んでくださり、ありがとうございました。
他にもたくさんの機能があるのですが、今回はあくまで紹介メインということでこの程度にしておきます。記事にコメントを付けてくだされば記事の内容に追加するかもしれません。
また、私のMisskeyアカウントDiscordサーバーにてサポートはいつでもするので、良ければフォローや参加をご検討ください。改めて、お読みいただきありがとうございました!

Discussion

ログインするとコメントできます