murlog ドメインモデル
murlog の世界を構成する概念と、それらがどう振る舞うかを記述する。 技術的な実装方法は murlog API / ActivityPub 仕様 を、設計判断の背景は プロダクト概要 を参照。
概念マップ
┌─────────────────────────────────────────────────────┐
│ インスタンス │
│ 1人のオーナーが所有する murlog サーバー │
│ │
│ ┌──────────┐ ┌──────────┐ │
│ │ ペルソナA │ │ ペルソナB │ (通常は1つ、追加可能) │
│ │ @alice │ │ @photos │ │
│ └────┬─────┘ └──────────┘ │
│ │ │
│ ┌────┴────────────────────┐ │
│ │ 投稿 添付 リプライ │ │
│ │ メンション CW │ │
│ │ フォロー ブロック │ │
│ │ 通知 タイムライン │ │
│ │ ピン留め │ │
│ └─────────────────────────┘ │
└──────────────────────────┬──────────────────────────┘
│ ActivityPub
┌────────────┴────────────┐
│ リモートサーバー群 │
│ Mastodon / GoToSocial │
│ Misskey / 他の murlog │
└─────────────────────────┘インスタンス
murlog の1つのインストール。1人のオーナーが所有し、1つのドメインに紐づく。
- マルチユーザーではない。ログインする人間は常に1人
- 外部からは通常の ActivityPub サーバーと区別がつかない
- 初回アクセス時にセットアップウィザードが走り、ドメイン・ペルソナ・パスワードを設定して運用開始
ペルソナ
インスタンス上の ActivityPub Actor。外部からは独立したアカウントに見える。
- デフォルトはプライマリペルソナ 1 人。必要に応じて追加できるオプトイン構造(例: 本アカウントと写真用アカウント)
- 外部からは 1 ペルソナ = 1 アカウントに見える(通常のマルチユーザーサーバーと区別なし)
- ペルソナごとにユーザー名・表示名・自己紹介・アバター・ヘッダー画像・カスタムフィールド・公開設定を持つ
Locked: 承認制フォロー(manuallyApprovesFollowers)。false なら自動承認ShowFollows: フォロー・フォロワー一覧の公開/非公開Discoverable: 検索結果への表示可否- ペルソナごとに RSA 鍵ペアを持ち、ActivityPub の署名に使う
- フォロー・フォロワー・通知はペルソナごとに完全独立
- DB は共有(テーブルレベルで
persona_idにより分離) - 認証はインスタンス全体で共通。1 回のログインで全ペルソナを操作できる
カスタムフィールド
ペルソナのプロフィールに表示する key-value メタデータ。
- 最大 4 件(Mastodon の慣習に合わせる)
- 名前と値のペア(例: 「Webサイト」「https://example.com」)
- 値にはリンクを含められる
- ActivityPub の Actor
attachment配列にPropertyValueタイプとして含める - リモート Actor のカスタムフィールドも受信時に保存し、プロフィール表示で使う
ペルソナが1つのとき
- アカウント切り替え UI は表示しない
- 単一アカウントの ActivityPub サーバーとして振る舞う
ペルソナが2つ以上のとき
- グローバルスイッチャー(Twitter 風)が有効化される
- 投稿・フォローなど全操作が「どのペルソナとして行うか」の文脈を持つ
- 各画面にローカルなペルソナ選択 UI は置かない(切り替えはグローバルスイッチャーに集約)
投稿
テキスト(HTML)+ メディア添付で構成されるコンテンツ。ローカル投稿とリモート投稿がある。
ローカル投稿
オーナーが自分のペルソナとして作成する投稿。
- 作成 → 編集 → 削除のライフサイクル
- 公開範囲を指定できる(公開 / 未収載 / フォロワー限定)
- 多言語コンテンツ(
contentMap)を持てる - 作成するとフォロワー全員に配送される
- 編集するとフォロワー全員に更新が配送される
- 削除するとフォロワー全員に削除が配送される
リモート投稿
フォローしているリモート Actor から Inbox に届いた投稿。
- 受信時に URI ベースで重複排除し、HTML をサニタイズして保存
- ローカル投稿と同じテーブルに格納し、タイムラインで統合表示される
- 投稿者の Actor URI を保持し、プロフィール参照に使う
- リモート側で削除されると Delete Activity が届き、ローカルからも消える
添付ファイル
投稿に紐づくメディアファイル。
- 画像をアップロードすると、リサイズ・サムネイル生成・EXIF 除去・HEIC→JPEG 変換が行われる
- ファイルはメディアストア(ローカルファイルシステム or S3 互換)に保存
- 代替テキスト(alt)を設定できる
- 投稿に紐づく前(アップロード直後)は PostID がゼロ値の孤立状態で存在する
リプライ
他の投稿への返信。ローカル投稿・リモート投稿の両方にリプライできる。
- 投稿は
in_reply_to(返信先の投稿 URI)を持てる - リプライ先がローカル投稿の場合は投稿 ID で参照、リモート投稿の場合は URI で参照
- リプライを作成すると、通常のフォロワー配送に加えてリプライ先の Actor の Inbox にも配送される
- ActivityPub の
inReplyToプロパティにマッピング
スレッド表示
- 投稿の詳細画面で、その投稿に連なるリプライチェーンをスレッドとして表示する
- 祖先(自分の返信先を遡ったチェーン)と子孫(自分への返信ツリー)を取得して表示
- リモート投稿のスレッドは、手元にあるデータの範囲で表示する(未取得の投稿は辿れない)
CW(Content Warning)と sensitive メディア
投稿の内容を折りたたむ機能と、メディアをぼかす機能。それぞれ独立した概念だが連動する。
CW
- 投稿は
summary(警告テキスト)を持てる summaryが設定された投稿は、タイムライン上で本文が折りたたまれ、警告テキストと展開ボタンのみ表示される- ユーザーが展開ボタンを押すと本文と添付が表示される
- ActivityPub の
summaryプロパティにマッピング - リモート投稿の
summaryも受信時に保存し、同様に折りたたみ表示する
sensitive
- 投稿は
sensitiveフラグを持てる sensitiveが true の投稿は、添付メディアがぼかし表示され、タップで表示される- CW を設定すると
sensitiveも自動的に true になる(Mastodon 準拠) - CW なしで
sensitiveのみを設定することもできる(メディアだけセンシティブにしたい場合) - ActivityPub の
sensitiveプロパティにマッピング - リモート投稿の
sensitiveも受信時に保存し、同様にぼかし表示する
メンション
投稿内で他の Actor を参照する機能。
- 投稿本文に
@user@domainを記述すると、その Actor へのメンションとして認識される - メンション先の Actor は投稿作成時に WebFinger で解決し、Actor URI を取得する
- メンション先の Actor の Inbox に投稿が配送される(フォロー関係がなくても届く)
- ActivityPub の
tag配列にMentionタイプとして含め、ccにメンション先の Actor URI を追加する - リモート投稿に自分へのメンションが含まれる場合、
mention通知が生成される(既存の通知定義と連動)
公開範囲
投稿がどこまで届くかを制御する。
| 範囲 | ローカルでの見え方 | 連合での振る舞い |
|---|---|---|
| 公開 | 公開タイムラインに載る | フォロワー全員 + Public 宛に配送 |
| 未収載 | 公開タイムラインには載らない | フォロワー全員に配送、Public は cc |
| フォロワー限定 | フォロワーのみ閲覧可能 | フォロワー全員に配送、Public なし |
| ダイレクト | 指定受信者のみ閲覧可能 | 指定受信者のみに配送、Public なし、フォロワーコレクションなし |
ダイレクト (DM) はタイムラインから除外される。
フォロー
ペルソナが他の Actor をフォローすること、またはその逆。方向によって異なる概念。
送信フォロー(自分 → リモート)
自分のペルソナがリモート Actor をフォローする。
- ユーザーが
@user@remote.exampleを指定してフォロー操作 - WebFinger でリモート Actor の URI と Inbox を解決
- Follow Activity を相手の Inbox に配送するジョブがキューに入る
- この時点では「申請中」(未承認)状態
- 相手が Accept を返すと「承認済み」になる
- 承認後、相手の投稿が自分の Inbox に届くようになる
- 相手が Reject を返すとフォローレコードが削除される
受信フォロー(リモート → 自分)
リモート Actor が自分のペルソナをフォローする。ペルソナの Locked 設定により動作が変わる。
Locked = false(自動承認):
- 相手が Follow Activity を自分の Inbox に POST
- フォロワーレコードを作成(
Approved = true) - Accept Activity が自動的に相手に配送される
- 以降、自分が投稿するとこのフォロワーにも配送される
Locked = true(承認制):
- 相手が Follow Activity を自分の Inbox に POST
- フォロワーレコードを作成(
Approved = false、保留中) - Accept は送らない。Actor JSON の
manuallyApprovesFollowers: trueで承認制を宣言 - オーナーが
followers.pendingで未承認リストを確認 followers.approve→Approved = true+ Accept 配送followers.reject→ フォロワー削除 + Reject 配送
フォロー解除
- 自分から: Undo Follow を相手の Inbox に配送し、ローカルのフォローレコードを削除
- 相手から: Undo Follow が Inbox に届き、フォロワーレコードを削除
ブロック
特定のリモート Actor またはドメインとのやり取りを拒否する機能。インスタンス全体に適用される(ペルソナごとではない)。一人用サーバーなので、ブロック判断はオーナー個人の意思決定として扱う。
アクターブロック
特定のリモート Actor をブロックする。
送信ブロック(自分 → リモート)
- ブロックレコードを作成
- 既存のフォロー関係を双方向とも削除(全ペルソナの送信フォロー + 受信フォロワー)
- Block Activity を相手の Inbox に配送
- 以降、その Actor からの Activity を Inbox 入口で拒否
受信ブロック(リモート → 自分)
- Block Activity が Inbox に届く
- 既存のフォロー関係を双方向とも削除
- ブロックレコードは保存しない(こちらから再フォローしても相手側で拒否されるだけ)
ブロック解除
- 自分から: ブロックレコードを削除し、Undo Block を相手に配送。フォロー関係は自動復元しない
- 相手から: Undo Block が届いても特に処理なし(レコードを保存していないため)
ドメインブロック
特定ドメインのサーバー全体とのやり取りを拒否する。
- ドメインブロックレコードを作成
- そのドメインに属する全フォロー関係を一括削除(全ペルソナ)
- Activity 配送はしない(サイレント拒否)
- 以降、そのドメインの任意の Actor からの Activity を Inbox 入口で拒否
ドメインブロック解除時は、レコードを削除するのみ。フォロー関係は自動復元しない。
ブロック中の振る舞い
| 操作 | 振る舞い |
|---|---|
| ブロック済みアクター/ドメインからの Follow | Inbox で拒否(202 返却、処理しない) |
| ブロック済みアクター/ドメインからの Like / Announce | 同上 |
| ブロック済みアクター/ドメインからの Create (Note) | 同上 |
| ブロック済みアクター/ドメインの既存投稿 | タイムラインに残す(受信済みデータは削除しない) |
| ブロック済みアクターへのフォロー操作 | API でエラーを返す |
将来の拡張(v2 以降)
- ブロックリストの CSV インポート / エクスポート(Mastodon 互換フォーマット: アカウント
user@domain、ドメインdomainの1行1エントリ)
タイムライン
ペルソナに紐づく投稿の時系列表示。
ホームタイムライン
- 自分のローカル投稿 + フォロー先から受信したリモート投稿を時系列で統合表示
- 内部的には、そのペルソナの
persona_idで紐づく投稿を新しい順に取得しているだけ- ローカル投稿:
persona_id= 投稿者(自分) - リモート投稿:
persona_id= 受信先ペルソナ(自分)
- ローカル投稿:
- カーソルベースのページネーション
通知
ペルソナに対して発生するイベントの記録。
| 種類 | トリガー |
|---|---|
| follow | リモート Actor が自分をフォローした |
| mention | リモート投稿で自分がメンションされた |
| reblog | 自分の投稿がリブログされた |
| favourite | 自分の投稿がお気に入りされた |
- 既読 / 未読の状態を持つ
- 個別既読、全既読の操作ができる
- WebSocket 接続中はリアルタイムプッシュ、CGI 環境ではポーリングで取得
ピン留め投稿
プロフィールページの先頭に固定表示する投稿。
- ペルソナごとに最大 1 件のローカル投稿をピン留めできる
- ピン留めした投稿はプロフィールページの投稿一覧の先頭に表示される
- ActivityPub の
Featuredコレクション(/users/:id/collections/featured)として公開する - リモートサーバーがプロフィールを表示する際に
Featuredを取得してピン留め投稿を表示できる
リブログ / お気に入り
リモート Actor が自分のローカル投稿に対して行うリアクション。
- リブログ(Announce): 相手が自分の投稿をリブログした記録。通知を生成
- お気に入り(Like): 相手が自分の投稿をお気に入りした記録。通知を生成
- Undo Announce / Undo Like が届くと記録を削除
リモート Announce の受信(リブログ投稿)
フォロー中のユーザが第三者の投稿をリブログした場合の処理。
- Announce Activity の Object URI がローカル投稿でない場合、
FetchNoteSignedでリモート Note を取得 storeRemoteNoteでorigin="remote"の投稿として保存。reblogged_by_uriにリブログ元 Actor URI をセットensureRemoteActorCachedで投稿者・リブログ元の Actor 情報を自動フェッチ+キャッシュ- フロントでは
reblogged_by_uriがある投稿にのみリブログラベルを表示 - 冪等性: 同じ Note URI が既に存在する場合はスキップ
リモート Actor
フォロー・フォロワー関係にあるリモート ActivityPub Actor のキャッシュ。
- Actor URI をキーに、ユーザー名・表示名・Inbox URL・公開鍵・アバター URL を保持
- HTTP Signature の検証やプロフィール表示に使う
FetchedAtでキャッシュの鮮度を管理
認証
インスタンスのオーナーを認証する仕組み。ログインする人間は1人。
ログイン
- パスワードで認証し、HttpOnly Cookie(セッション)を発行
- 以降のリクエストで Cookie が自動送信される
- セッションは DB に保存され、有効期限を持つ
パスワードリセット
メールに依存しない物理アクセスベースのリセットフロー。
- サーバー上で
murlog resetを実行(またはmurlog.resetファイルを手動作成) - トークン入りのリセット URL でブラウザからアクセス
- 新パスワードを設定
FTP/SSH でサーバーにアクセスできる = オーナーである、という前提に立つ。
セットアップ
インスタンスを初めて使い始めるまでの流れ。2ステップで完了する。
- サーバー設定 — データの保存先を決める
- サイトセットアップ — ドメイン名・ペルソナ・パスワードを設定して運用開始
未完了の場合、全アクセスがセットアップ画面に誘導される。
ハッシュタグ
投稿にハッシュタグを付けて分類する機能。
- 投稿本文の
#tagを解析し、HashtagsJSONに保存(hashtag/パッケージ) - タグ別の投稿一覧を
posts.list_by_tagRPC で取得できる - 公開タグページ
/tags/:tagで SSR + SPA 表示(handler/tags.go) - ActivityPub の
tag配列にHashtagタイプとして含める
v2 以降の拡張
- ダイレクトメッセージ(direct visibility)
- ミュート(ブロックより柔軟なフィルタリング)
- ブックマーク(ローカル保存、連合しない)
- 全文検索