Fediverse用のclient libraryを探す旅

この記事はFediverse (2) Advent Calendar 2020 の9日目です.大遅刻です. ちょうどこの週に引っ越しをすることになってしまって,「書く時間くらいあるやろー」と思ってたら,全然時間がなかったです.片付けて引っ越しして荷解きして,仕事できる状態までPC周りを復元して……とやっていたら全然書く時間が取れずにここまで遅刻しました.

それはともかくとして…….

Fediverseというとサーバの話をすることがどうしても多くなってしまうけど,クライアントアプリケーション開発者として日々APIに触れていると,クライアントライブラリを調べることが多い.

というわけで,FediverseのAPIを叩くクライアントライブラリを調査したときのメモを残しておく.

ちなみに,Fediverseが対象なので,Mastodon以外のクライアントライブラリも調べた.調べたのだが,正直Mastodon,Misskey以外は非常に数が少ない.クライアントアプリケーションもそこまで多くはないので,仕方ないのだろうが.

なお,俺がクライアントアプリケーションを作るときはJavaScript(TypeScript)で書いているので,普段はJSのライブラリ調査しかしていない.というわけで若干JS成分が多めである.

megalodonの話を最初にしておく

俺が作っているWhalebirdも,もちろん内部ではAPIを叩いているのだが,この部分はすべて外に出して外部公開している.これがmegalodonというAPIクライアントライブラリとして公開されている.

www.npmjs.com

こいつは,

に対応しており,TypeScriptで書かれている.

AccessTokenを使ってHome Timelineを取得するコードはこんな感じになる.

import generator from "megalodon";

const main = async () => {
  const client = generator(
    "mastodon",
    "https://fedibird.com",
    process.env.MASTODON_ACCESS_TOKEN
  );
  const timeline = await client.getPublicTimeline();
  timeline.data.forEach((status) => {
    console.log(status.id);
  });
};

main();

こいつはあくまでWhalebirdで使うことを前提に作ったので,上記3つのSNSを同じInterfaceで扱えるようにしてある.そのために,generator 呼び出し時にSNSの指定をしている. 必ずしも使いどころが多いとは思っていないが,そういう用途ではめちゃくちゃ便利なので,JSでFediverseのAPIを叩きたいときは是非.

mastodon

Mastodonに関しては,公式のドキュメントでクライアントライブラリも紹介されている.

docs.joinmastodon.org

それなりの言語であれば実装されているように見えるが,これらを全部試す暇はなかったので,代表的なものをいくつか動かしてみた.

masto.js

github.com

JSでは以前にmastodon-apiを使っていたのだが,今は使っていない.あまり更新もされなくなってしまったようだし.

というわけで,masto.jsを触ってみた.

基本的にすべて同じこと,AccessTokenを使ってHome Timelineを取得するコードを書いてみる.

const masto = require("./node_modules/masto");

const main = async () => {
  const client = await masto.Masto.login({
    uri: "https://fedibird.com",
    accessToken: process.env.MASTODON_ACCESS_TOKEN,
  });

  const timeline = await client.fetchHomeTimeline();
  for await (const statuses of timeline) {
    Object.keys(statuses).forEach((key) => {
      console.log(statuses[key].id);
    });
  }
};

main();

クライアントの初期化部分は良いのだが,fetchHomeTimelineiteratorを返すという仕様が目立つ.TypeScriptで書かれているので,エンティティにはすべて型がついた状態で得られる.

なお,今回はaccess tokenを予め用意してからアクセスしたが,プログラム側からcreateApp することもできるようになっている.

用意されているメソッドはだいたいmegalodonと変わらない.違うのは,上記のiteratorの部分と,各関数が取る引数くらいだろうか.masto.jsはSearchAccountsParams みたいな形で,パラメータをまとめたオブジェクトを引数に取る形にしているものが多い.megalodonは,あまり専用のオブジェクトを引数に取るような形はとっていない.

go-mastodon

github.com

こちらはGolang用.

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mattn/go-mastodon"
)

func main() {
    c := mastodon.NewClient(&mastodon.Config{
        Server:      "https://fedibird.com",
        AccessToken: os.Getenv("MASTODON_ACCESS_TOKEN"),
    })
    timeline, err := c.GetTimelineHome(context.Background(), nil)
    if err != nil {
        log.Fatal(err)
    }
    for i := len(timeline) - 1; i >= 0; i-- {
        fmt.Printf("%#v\n", timeline[i])
    }
}

同じことをするコード.さすがにgoは素直に実装されている.言語が静的型付なので,もちろんエンティティはすべて型の情報が含まれている.というか,普段goばかり書いているせいか,違和感なく普通に使えるので,ライブラリのインターフェースが結構良いのではないだろうか.

hunter

なんとElixir用があったので,うっかり動かしてみた.

defmodule PleromaHunter do
  def main do
    auth()
    |> home_timeline()
    |> Enum.map(fn %Hunter.Status{id: id} -> id end)
    |> Enum.map(fn id -> IO.puts(id) end)
  end

  defp auth do
    Hunter.new(
      base_url: "https://fedibird.com",
      bearer_token: System.get_env("MASTODON_ACCESS_TOKEN")
    )
  end

  defp home_timeline(conn, %{limit: limit}) do
    conn
    |> Hunter.home_timeline(limit: limit)
  end

  defp home_timeline(conn) do
    conn
    |> Hunter.home_timeline()
  end
end

はーパターンマッチは最高だぜ.すべての言語に導入してほしい,この快感.

それはともかくとして,ちゃんと叩けた.

Pleroma

Pleromaに関してはクライアント自体があまり見当たらない.これは公式が「ほぼMastodonと同じAPIだよ」と言っているから,そもそもみんな専用のクライアントライブラリが必要だとは思っていないのかもしれない.

ただ,最近は結構独自のAPIが増えてきているので,俺は専用のクライアントライブラリがあった方がいいとは思っている.

docs-develop.pleroma.social

そういう意味では,JSではmegalodonを使うのが一番良い気がする.

pleroma-api

公式のgit.pleromaにホスティングされている.が,こちらはnpm等で公開されていないため,自前でビルドする必要がある.

git.pleroma.social

おそらくこれはpleroma本体の開発用に作られたAPIクライアントであり,Node.js向けではなくブラウザ上で実行することを前提に作られている.そのためnodeでビルド&実行することができない.

用途がかなり限られる気がするが,勝手にビルドして使ってみる.ビルドして実行できる状態に持っていくまでが,結構面倒なので,後々紹介する今回のソースを詰め込んだリポジトリを参考にしてほしい.

import "babel-polyfill";
import pleroma from "../pleroma-api";

const config = {
  instance: "https://pleroma.io",
  accessToken: "your access token",
};

const main = async () => {
  const res = await pleroma.api.timelines.home({ config });
  console.log(res.data);
};

main();

なお,TypeScriptではないので,帰ってくるエンティティに型のサポートはない. まぁ使えないとはないのだが,やはり第三者に使ってもらうように公開されているライブラリではないので,気軽に利用できるものではないと思う.せめてビルドしたものがnpmあたりに公開されていれば楽なんだが.

あと,JSのライブラリは,たいていNode.jsを前提に作っているので,これはちょっと異端かもしれない.

Disboard

github.com

こちらはPleroma以外にもMastodon, Misskeyに対応している.ここは結構megalodonに近いものを感じる.ただし,megalodonとの違いとしては,megalodonが3つのSNSを同じインターフェースで叩けるようにしているのに対し,DisboardはSNSそれぞれのクラスを用意して,その中のメソッドはSNSごとに別れている.

そのため,同じパッケージだが,用途が違うものが3つ同梱されているといった形だろうか.

C#であり,今回試していないが,これは動かせそうなら後々試してみたい.

Misskey

Misskeyも公式のドキュメントでクライアントライブラリの紹介がある.

join.misskey.page

このおかげなのかはわからないが,Mastodonを除くと,Misskeyは最もAPIクライアントライブラリが豊富だった.これ以外にもあると思うので,欲しい人は自分で調べてみるといい.

misskey-rs

github.com

比較的新しいライブラリだと思われる.

use std::time::Duration;
use std::env;
use std::process;

use futures::stream::TryStreamExt;
use misskey::prelude::*;
use misskey::HttpClient;

#[tokio::main]
async fn main() -> anyhow::Result<()> {

    let token = match env::var("MISSKEY_ACCESS_TOKEN") {
        Ok(val) => val,
        Err(err) => {
            println!("{}: MISSKEY_ACCESS_TOKEN", err);
            process::exit(1);
        },
    };
  let client = HttpClient::builder("https://misskey.io/api/")
      .token(token)
      .build()?;

    let mut notes = client.home_notes(..);
    notes.set_interval(Duration::from_secs(10));
    notes.set_page_size(20);

    while let Some(note) = notes.try_next().await?{
        if let Some(text) = note.text {
            println!("{}", text)
        }
    }

  Ok(())
}

タイムラインを返すメソッドがfutureを返すというのがミソか.Vecを返すわけではないので,try_nextとかで都度アクセスしてやる必要がある.確かにAPI呼び出しは効率化されそうだけどね……. 実装が結構隠蔽されていて,これはちょっと読みにくい.

corekey

github.com

こちらはTypeScript実装.

import { App, AuthSession } from "corekey";

const main = async () => {
  const app = await App.create(
    "misskey.io",
    "fediverse-client",
    "description",
    ["read:notes"]
  );
  const session = await AuthSession.generate(app);
  console.log("open in your browser: ", session.url);
  const account = await session.waitForAuth();

  const res = await account.request("notes", {});
  console.log(res);
};

main();

かなり薄いラッパーしかない上に,TypeScriptだけど特にエンティティの型定義はない.そのためあまりTypeScriptで嬉しい意味がないし,使う意味もかなり薄いのではないかと思う.エンドポイントも文字列で指定するだけなので,自由度は確かに高いが,それなら自分で書くのと大差ないのではないだろうか.

また,どういう要求で作られたのかはよくわからないが,アクセストークンを直指定してAPIクライアントを叩く術が存在しない.毎回アプリケーションの作成からやって,認証を通してやらないとAPIがたたけないようになっており,これはどういう用途で使うんだろうか.試しに動かしてみる程度にしか役に立たないのではないだろうか.別のプログラムに組み込んで,毎回プログラムから叩くというAPIクライアントライブラリの用途には不十分過ぎると思われる.

Misskey.py

github.com

Python実装.

from Misskey import Misskey
import os

misskey = Misskey("misskey.io", i=os.environ["MISSKEY_ACCESS_TOKEN"])

res = misskey.notes()
print(res)

こういうとき,pythonはとにかくコード量が少なく済むのでよい.まぁ個人的にはあまり好きな言語ではないが…….

割とマトモなドキュメントがある.

misskeypy.readthedocs.io

なのでコメントにそれなりに丁寧に書かれており,ソースやドキュメントを見ると普通に使えると思う.が,型注釈等が書いてあるわけではないので,例えばレスポンスで帰ってきたエンティティの型などはわからない.この辺はまぁPythonなので仕方ない.エンドポイントも文字列ではなくちゃんとエンドポイントごとにメソッドを用意しており,パラメータ定義もされているので,わりかし使いやすいのではないだろうか.

Pixelfed

見つからなかった.これはマジで見つからない.もし知っている人がいたら教えてください.特に言語は問いません(実行できるか自信はないが……).

PeerTube

PeertubeClient

github.com

ひとつだけ見つかったが少し古い…….

import { Peertube } from "peertube-client";

//------------------------------
// To resolve fetch in node js
//------------------------------
import Global = NodeJS.Global;
export interface GlobalWithCognitoFix extends Global {
  fetch: any;
}
declare const global: GlobalWithCognitoFix;
global.fetch = require("node-fetch").default;
//------------------------------

declare var process: {
  env: {
    PEERTUBE_USER: string;
    PEERTUBE_PASSWORD: string;
  };
};

const peertube = new Peertube({
  instance: "peertube.fr",
  user: process.env.PEERTUBE_USER,
  password: process.env.PEERTUBE_PASSWORD,
});

const main = async () => {
  const videos = await peertube.getVideos();
  console.log(videos);
};

main();

おそらくこれはブラウザ向けに作られているライブラリのため,globalにfetchがあることを期待している.nodeで動かす都合に合わせて,node-fetchを差し込んでいる.

元はTypeScriptで書かれているので,こちらもTypeScriptで利用してみた.ちゃんとレスポンスのエンティティは型で守られたオブジェクトが帰ってくるので,良い.

こいつだけはOAuthによるAccessTokenではなく,username + passwordの認証になっている.PeerTube自体はOAuth2のtoken認証をサポートしてると思うので,おそらくこのライブラリ実装以降にOAuthが実装されたとか,そんな感じな気がする.

docs.joinpeertube.org

リクエストもレスポンスもふつうのTypeScriptのAPIクライアントなので,特に違和感なく使えて良いんじゃないかな.

まとめ

今回試したコードは全て以下のリポジトリに公開してある.

github.com

そういえば,Mastodonの公式クライアントライブラリ一覧にmegalodonを載せていなかった.というわけでPRを作ろうと思ったんだけど,果たしてjoinmastodon.orgはどの程度メンテされているんだろうか…….

Fediverseといえど,単体ではただのSNSなので,普通にクライアントライブラリは作ろうと思えば作れる.ここにActivityPubは無関係なので,作りはどれも簡単で,だいたいコードを読めば何をやっているかはわかるだろう.なので,薄いAPIのラッパーよりは,静的型付でエンティティに型のサポートが入っているとクライアントライブラリを使う意義が大きくなるじゃないだろうか.

というわけで,実装が少なかったSNSAPIクライアントライブラリは狙いめだぞ.

なんでMisskeyだけやたら多いのかは最後まで謎だった…….