MiPAというMisskeyのBotフレームワークを作った話
はじめに
この記事では私が作成しているBotフレームワークであるMiPAとそれのコアに当たるMiPACを紹介します。
MiPAという名前について
この名前になる前に実は Mi.py
という名前で開発していました。discord.py
のようにmisskey.py
としたかったという気持ちは無くもないのですが、既に使用されていたため、Mi.py
にし、コードに大幅な書き換えが必要になり、2つのライブラリに分けることになったため、MisskeyPythonAPI
とMisskeyPythonAPICore
の2つにしました。結果的にMiPA
とMiPAC
という名前になった感じです。
なんで作ろうと思ったか
私が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つ目はメソッドチェーンである事です。何故、api
とaction
という紛らわしいものが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
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'))
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のようなフレームワークを作ることも可能です
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のドキュメントもあります
リポジトリ
最後に
長くなりましたが、以上です。ここまで読んでくださり、ありがとうございました。
他にもたくさんの機能があるのですが、今回はあくまで紹介メインということでこの程度にしておきます。記事にコメントを付けてくだされば記事の内容に追加するかもしれません。
また、私のMisskeyアカウントやDiscordサーバーにてサポートはいつでもするので、良ければフォローや参加をご検討ください。改めて、お読みいただきありがとうございました!
Discussion