nazo6 notememo

ActivityPub互換のものを作りたい

作成:2022/11/25

更新:2023/02/14

(2022-11-25)

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

(2022-11-25)

用語

ActivityPub (2022-11-25)

仕様

ActivityPub

w3c.github.io

ActivityPub

argrath.github.io

mastodonの仕様

ActivityPub - Mastodon documentation

A decentralized social networking protocol based upon the ActivityStreams 2.0 data format and JSON-LD.

docs.joinmastodon.org

WebFinger (2022-11-25)

RFC 7033: WebFinger

www.rfc-editor.org

WebFinger - Mastodon documentation

Translate `user@domain` mentions to actor profile URIs.

docs.joinmastodon.org

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

Activity Streams 2.0

www.w3.org

Activity Vocabulary

www.w3.org

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

json-ld (2022-11-25)

JSON-LD 1.1

www.asahi-net.or.jp

JSON-LD 1.1

www.w3.org

最初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)

Mastodonにアカウントとして認識されるActivityPubを実装してみる - Qiita

Mastodonの検索エリアにURLを突っ込んだ際にアカウント化(?)させるにはどういう実装が必要なのか、気になって調べて実装してみた。結局ActivityPub対応WebFinger対応H…

qiita.com

とりあえずアカウントを認識させるところまで (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とは?
これは[email protected]という一意のindetityについて関連するURLを取りにいくもののようでmastodonやmisskeyでは/.well-known/webfinger?resource=に取りにくる。
このときresouceにはacct:[email protected]というリクエストが来る。
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)

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

ActivityPub

argrath.github.io

この内容からすると認証なしでリクエストが来た場合にはパブリックな投稿のみを返すという仕様だと思われる
そもそも鍵垢というのはどういうことかというとフォローを自由にできないというだけであって投稿の公開範囲には関係がないはずだ