icon

nazo6.dev

一覧に戻る
2022/11/25 2023/2/14 6 min read

ActivityPub互換のものを作りたい


#(2022-11-25)

RustでMastodonやMisskeyのサーバーと通信することをとりあえずの目標に

#(2022-11-25)

用語

#ActivityPub (2022-11-25)

#仕様

#mastodonの仕様

#WebFinger (2022-11-25)

#Activity Streams / Activity Vocabulary (2022-11-25)

ActivityPubはActivityStreamのデータ形式を使用していてそのデータ型がActivity Vocabularyってことだと思う

#json-ld (2022-11-25)

最初JSON-LDってJSON Schemaと何が違うのと思ったけどそれは表面的なもの。 JSON SchemaはJSONのバリデーションをするがJSON-LDは要素にそれが何であるかの情報を与える。

それとapplication/ld+jsonapplication/activity+jsonの違いがよくわからない json-ldで表されたデータの一種がacitivity streamってことでいいのかな? W3C wikiによると「 What is the exact relation between JSON-LD and Activity Streams ? ("compatible" is not precise enough)」らしい。

#(2022-11-25)

参考文献

#(2022-11-25)

#とりあえずアカウントを認識させるところまで (2022-11-25)

use axum::{
    extract::{Host, Path, Query},
    routing::get,
    Json, Router,
};
use serde::{Deserialize, Serialize};
use tracing::info;
 
static HOST: &str = "https://example.com";
static HOSTNAME: &str = "example.com";
 
#[tokio::main]
async fn main() {
    tracing_subscriber::fmt::init();
 
    let app = Router::new()
        .route("/", get(|| async { "Hello, World!" }))
        .route("/users/:id", get(user_get_handler))
        .route("/.well-known/webfinger", get(webfinger_get_handler));
 
    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}
 
#[derive(Serialize)]
struct PersonActivity {
    #[serde(rename = "@context")]
    context: String,
    #[serde(rename = "type")]
    _type: String,
    id: String,
    name: String,
    #[serde(rename = "preferredUsername")]
    preferred_username: String,
    summary: String,
    inbox: String,
    outbox: String,
    url: String,
}
async fn user_get_handler(Path(user_id): Path<String>, host: Host) -> Json<PersonActivity> {
    info!("user_get: query: {:?}", user_id);
    info!("from: {:?}", host);
    Json(PersonActivity {
        context: "https://www.w3.org/ns/activitystreams".to_string(),
        _type: "Person".to_string(),
        id: format!("{}/users/{}", HOST, user_id),
        name: user_id.clone(),
        preferred_username: user_id.clone(),
        summary: "".to_string(),
        inbox: format!("{}/users/{}/inbox", HOST, user_id),
        outbox: format!("{}/users/{}/outbox", HOST, user_id),
        url: format!("{}/users/{}", HOST, user_id),
    })
}
 
#[derive(Deserialize, Debug)]
struct WebFingerQuery {
    resource: String,
}
#[derive(Serialize)]
struct WebFingerResponse {
    subject: String,
    aliases: Vec<String>,
    links: Vec<WebFingerResponseLink>,
}
#[derive(Serialize)]
struct WebFingerResponseLink {
    #[serde(skip_serializing_if = "Option::is_none")]
    rel: Option<String>,
    #[serde(rename = "type")]
    _type: String,
    #[serde(skip_serializing_if = "Option::is_none")]
    href: Option<String>,
}
async fn webfinger_get_handler(query: Query<WebFingerQuery>) -> Json<WebFingerResponse> {
    info!("webfinger_get query: {:?}", query);
    let query_cap = regex::Regex::new(r"^acct:([^@]+)@(.+)$")
        .unwrap()
        .captures(&query.resource)
        .unwrap();
    let user_name = query_cap.get(1).unwrap().as_str();
    let response = WebFingerResponse {
        subject: format!("acct:{}@{}", user_name, HOSTNAME),
        aliases: vec![format!("{}/user/{}", HOST, user_name)],
        links: vec![WebFingerResponseLink {
            rel: Some("self".to_string()),
            _type: "application/activity+json".to_string(),
            href: Some(format!("{HOST}/users/{}", user_name)),
        }],
    };
 
    Json(response)
}
 

色々つっこみ所が多いがまあとりあえず

#ハマったところ (2022-11-25)

#webfingerとは?

これはusername@example.comという一意のindetityについて関連するURLを取りにいくもののようでmastodonやmisskeyでは/.well-known/webfinger?resource=に取りにくる。 このときresouceにはacct:username@example.comというリクエストが来る。 WebFinger仕様にはacctスキーマについて

   WebFinger requests include a "resource" parameter (see Section 4.1)
   specifying the query target (URI) for which the client requests
   information.  WebFinger is neutral regarding the scheme of such a
   URI: it could be an "acct" URI [18], an "http" or "https" URI, a
   "mailto" URI [19], or some other scheme.

とある。 webfingerで決まっているわけではないがこの形式の一意のIDを扱うときはacctスキームを付けるのが普通みたい?

#エンドポイントのURLは?

mastodonだとURL(example.com/users/xxx)をつっこんでも取りにきてくれなかった。 misskeyだとこれを入れるとこのURLにactivitystreamを取りにきた。そりゃそうか。

#(2022-11-25)

というかよく考えたらjson-ldにちゃんと対応させるとめちゃくちゃ大変では serde用のパーサはあるけど静的に型を付けるのとすごく相性が悪いような・・・ このクレートだとattribute macroでいろいろ頑張ってくれてるみたいだけどそれでもキツそう

#(2022-11-25)

と思ったけどどうやらjson-ldに対応している必要は一応ないみたい ただスキーマを詠み込めるといろいろいいことがある感じかな

#(2022-11-25)

あと思ったのがフロントを分離しづらいということ フロントはNext.jsでVercelとかにデプロイして別にバックエンドサーバーを作るつもりだったけど フロントがexample.com、APIサーバーがapi.example.comにあったとしてインスタンスとして認識されるのはexample.comだからそっちにFederationの情報を取りにくる まあWorkersとかでリダイレクトさせればいいのかもしれないけどフロントにそれを意識させたくないのでstaticなファイルもaxumから配信するしかない

#(2023-02-14)

そういえば鍵垢ってどうやって実現してるんだろうか

この内容からすると認証なしでリクエストが来た場合にはパブリックな投稿のみを返すという仕様だと思われる

そもそも鍵垢というのはどういうことかというとフォローを自由にできないというだけであって投稿の公開範囲には関係がないはずだ

Share this article:
一覧に戻る

© 2025 nazo6. All rights reserved.