Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

Pulsate Specification は Pulsate プロジェクトに関するAPIエンドポイントや設計などの仕様を記述したドキュメントです.

プロジェクトの概要については こちら をご覧ください.

Note

この仕様書は現在開発中の Pulsate v0.1 となります. 今後のバージョンアップにより仕様が変更される可能性があります.

プロジェクト概要

プロジェクトの目標

高速で安全な ActivityPub 実装

  1. パフォーマンス重視

  2. セキュリティ重視

  3. ActivityPub 仕様の重視

実装方針

パフォーマンス・セキュリティの強化

  • Misskey や Mastodon よりも高速に動作する次世代の ActivityPub 実装.
    • 各APIリクエストに対するレスポンスを始めとするパフォーマンスの向上(タイムラインの読み込み, メディアのレスポンスなど).
  • SQL Injection や XSS などのセキュリティに関わる脆弱性の発生可能性を減少させる. 見つかったとしてもすぐに修正できる仕組みづくり.
    • パスワードレスログイン, 2FA など.

ActivityPub 実装の重視

  • 無駄な機能は実装せずあくまで ActivityPub の実装として忠実に従う.
  • MFM など特定のソフトウェア固有の実装に従わない.
    • ただし, Mastodon や Misskey などの主要な実装が互いに実装している機能は実装する. (引用など)

クライアント, App 周りのサポートの強化

  • ターゲットはモバイルユーザーが大半だと考え, Pulsate ではネイティブアプリとして iOS App, Android App をリリースする(構想段階).
    • PWAはChromium系ブラウザは概ね対応しているが, Firefox や Safari は対応していない.
    • また、大部分のユーザーはアプリストアで検索すると考えられるため、マーケティング的な観点からもネイティブアプリの実装は必要.
  • コミュニティが開発するサードパーティクライアントも尊重し, API リファレンスなどのリソースを多く用意しコミュニティの開発を支援する.

開発手法の厳格化 / 改善

  • semver (セマンティックバージョニング) に従い, 破壊的変更をなるべく少なくする. 破壊的変更が行われる場合は予めコミュニティに通告する.

コミュニティの声を聞く

  • 簡単に言えば Discord や GitHub Discussions の採用.
  • バグ報告は基本的に インスタンス管理者 , ユーザー から受けるのが多いと考えるため, Discord での報告も受け付ける. (GitHub Issues には Discord Bot を使ってうまく Import する)

プロジェクトコードネーム

Pulsate

  • 意味: 脈打つ, 鼓動する; 〈光・音などが〉振動する; ドキドキする; など.
  • 由来: indigo la End のアルバム “PULSATE” から.

バージョンコードネーム

v0, v1 - Antenna

v0 (開発途上バージョン), v1 (最初の安定版) のコードネーム.

由来は Mrs. GREEN APPLE の ANTENNA から.

ユーザーストーリー

この文書は, Pulsate プロジェクトの主要成果物である分散型 SNS サーバーソフトウェア pulsate がエンドユーザーから見たときに pulsate 以外の外部システムも含めてどのように振る舞うべきかを定義する.

pulsate はいくつかのアクターとやり取りすることを想定している. そのため, ユーザーストーリーで登場するアクターを以下のように定義する.

  • ユーザー User: pulsate がやり取りするなかで最も一般的な, SNS サービスを享受するエンドユーザー.
    • 別々の User を表すとき, メタ変数として Alice と Bob を用いる.
  • モデレーター Moderator: pulsate およびその上で運営されるコミュニティを管理するエンドユーザー. Moderator is-a User の関係にある.

各ユーザーストーリーは, 他文書から参照しやすいように ID を割り振っている. ID の文法構造は US<2桁の連番> としている (US は User Story の略).

US01: ユーザー登録

  • 事前条件: そのユーザーが保持するメールアドレスのアカウントが登録されていない.
  • 事後条件: そのユーザーが保持するメールアドレスのアカウントが登録されている.
  • 主要シナリオ:
    1. ユーザーが Pulsate へ, メールアドレスを付けて新規登録したいことをリクエストする.
    2. Pulsate がメールアドレスのメールアカウントへ, 検証メールを送信する.
    3. ユーザーがそのメールアカウントのメールサービスへ問い合わせて, 検証メールが来たことを確かめる.
    4. 検証メールの内容の検証コードを取得し, そのコードを付けて新規登録したいことをリクエストする.
    5. Pulsate がユーザへ, 新規登録に成功したことを応答する.
  • 3 で検証メールが届かなかったときの代替シナリオ:
    1. ユーザーが検証メールの再送信をリクエストする.
    2. Pulsate がメールアドレスのメールアカウントへ, 検証メールを送信する.
    3. これで届けば主要シナリオの 4 へ遷移する. 届いていなければ, この代替シナリオの 1 へ遷移する, あるいはユーザー登録を中止する.
  • 5 で新規登録に失敗したときの例外シナリオ:
    1. Pulsate がユーザへ, 新規登録に失敗したことを応答する. このユーザー登録を中止する.
  • 制約:
    • 短時間に大量のユーザー登録がリクエストされた時に, メール送信サービスのレートリミットに引っかかる可能性が高い.
sequenceDiagram
  User ->> Pulsate: メールアドレスで登録をリクエスト
  Pulsate ->> Email: 検証メールを送信
  User ->> Email: 検証コードを取得
  User ->> Pulsate: コードでメールアカウントを検証
  Pulsate -->> User: 登録完了

US02: フォロー/フォロワー

  • 事前条件: Alice と Bob が Pulsate 上でユーザー登録を完了している. Alice が Bob をフォローしていない.
  • 事後条件: Alice が Bob をフォローしている.
  • 主要シナリオ:
    1. Alice が Pulsate 上で, 未フォローのユーザー Bob の投稿を発見する.
    2. Alice が Pulsate に, 未フォローのユーザー Bob のアカウント情報を問い合わせる.
    3. Pulsate が Alice へ, Bob の情報を提供する.
    4. Alice が Pulsate に, Bob をフォローの対象として追加することをリクエストする.
    5. Pulsate が, Alice から Bob へのフォローリクエストを記録する.
    6. Pulsate が Bob へ, Alice からのフォローリクエストを通知する.
    7. Bob が Pulsate 上で, そのフォローリクエストを承認する.
    8. Pulsate が, Alice から Bob へフォローの関係があることを記録する.
    9. Pulsate が Alice へ, フォローリクエストが承認されたことを通知する.
  • 7 で Bob が承認しなかった場合の代替シナリオ:
    1. Pulsate は Alice から Bob へのフォローリクエストを記録し続けて何もしない.
  • 4 でフォロー対象にできない場合の例外シナリオ:
    1. Pulsate が Alice へ, Bob をフォローできないことを応答する.
sequenceDiagram
  Alice ->> Pulsate: 未フォローのユーザー Bob を発見
  Pulsate -->> Alice: Bob の情報を提供
  Alice ->> Pulsate: Bob のフォローを要求
  Pulsate ->> Bob: フォローリクエストを通知
  alt Bob が Alice からのフォローを承認する場合
    Bob ->> Pulsate: リクエストを承認
    Pulsate -->> Alice: フォロー完了
  else Bob が Alice からのフォローを承認しない場合
    Bob ->> Pulsate: リクエストを拒否
    Pulsate -->> Pulsate: 何もしない
  end

US03: 投稿

  • 事前条件: Bob が Pulsate 上でユーザー登録を完了している. Bob が Alice をフォローしている.
  • 事後条件: Alice が作成した新たな投稿が, Bob のホームタイムラインに現れる.
  • 主要シナリオ:
    1. Alice が Pulsate へ, 新規投稿の作成をリクエストする.
    2. Pulsate が新規の投稿を記録する.
    3. Pulsate が Alice へ, 投稿の作成に成功したことを応答する.
    4. Pulsate が Bob のタイムラインへ非同期で追加する.
    5. Bob が Pulsate 上で自身のタイムラインを取得する.
  • 2 で新規の投稿の作成に失敗した場合の例外シナリオ:
    1. Pulsate が Alice へ, 投稿の作成に失敗したことを応答する.
  • 制約:
    • あるユーザーのフォローされているユーザー数が非常に多い時, そのユーザーが投稿した際にそのユーザーをフォローしているユーザーのホームタイムラインすべてを生成する処理に負荷が集中する可能性が高い.
    • Alice は Pulsate 内ではない, 外部の分散型 SNS サービスからの連合と呼ばれる機能で新規投稿を Pulsate へ転送していることがある.
sequenceDiagram
  participant Alice
  participant Pulsate
  participant Bob
  Note over Alice, Bob: Bob は Alice をフォローしている

  Alice ->> Pulsate: 投稿を送信
  Pulsate -->> Alice: 投稿の成功を報告
  Pulsate ->> Bob: 新規投稿のイベントを配送
  Bob -->> Pulsate: 新規投稿を取得

US04: ブックマーク (お気に入り)

  • 事前条件: ユーザーが Pulsate 上でユーザー登録を完了している. ユーザーがその投稿をブックマークしていない.
  • 事後条件: ユーザーがその投稿をブックマークしている.
  • 主要シナリオ:
    1. ユーザーが Pulsate 上で投稿を発見する.
    2. ユーザーが Pulsate へ, その投稿をブックマークするようにリクエストする.
    3. Pulsate が, ユーザーがその投稿をブックマークしていることを非同期で記録する.
    4. ユーザーはブックマークに成功したと見なして Pulsate の利用を続行する.
  • 3 で記録に失敗した場合の例外シナリオ:
    1. ユーザーが先程の投稿がブックマークされていないことに気づく.
    2. ユーザーが Pulsate へ, その投稿をブックマークするようにリクエストする.
  • 制約:
    • ユーザーがある投稿をブックマークしているかどうかは, 他のユーザーから秘匿しなければならない.
  • 備考:
    • ブックマークに失敗したとき, 後でその失敗を通知する仕組みをシナリオに組み込んだほうがよいかもしれない.

US05: 検索

  • 事前条件: なし (ユーザー登録なしで利用できる).
  • 事後条件: なし (システムの状態を変化させない).
  • 主要シナリオ:
    1. ユーザーが Pulsate へ, クエリ条件を送って検索を開始する.
    2. Pulsate がユーザーへ, 検索結果を応答する.
  • 制約:
    • キャッシュや結果整合的非同期処理により, 作成された投稿が検索結果へすぐには反映されないことがある.
    • 多くの複雑な検索に対して高速に処理できることが望ましい.
  • 備考:
    • この検索のインターフェイスについては, SNS としてのユーザー体験を向上させるために熟考が必要である. 多機能, インタラクティブ性, 可読性, 充実したヘルプといった要素を重視することが望ましい.

モデルリファレンス

Pulsate のドメインモデルに共通する仕様と,新しいモデルを追加するパターンをまとめる. 各モジュールのモデルと,Timelineモジュールに含まれるListモデルの詳細は以下を参照すること.

ID

Twitter の Snowflake をベースに, 次のようなビットフィールド (図はビッグエンディアン) からなる 64 ビット整数です.

先頭に時刻を含んでいるため, そのまま時刻順にソートが可能です.

111111111111111111111111111111111111111111 1111111111 111111111111
64                                         22         12          0
timestamp                                  worker id  incremental

各ビットフィールドの意味は以下のとおりです.

フィールドビット範囲意味
timestamp[64, 22)Pulsate エポックからのミリ秒数. エポックは 2022 年 1 月 1 日 0 時 0 分 0.000 秒. UNIX 時間のミリ秒に 1640995200000 を足すことでこれに変換可能.
worker id[22, 12)この ID を生成したワーカーの識別子
incremental[12, 0)ワーカーにて同一時刻で ID を生成する度に増える値

TypeScript 上ではこのような型として表現します. 型引数によりエンティティ間で ID を取り違えるようなミスを防ぎます.

declare const snowflakeNominal: unique symbol;
export type ID<T> = string & { [snowflakeNominal]: T };

このコードへのリンク (pkg/id/type.ts)

生成と利用の方法

モデル側での定義

モデルごとにこのような形でモデル固有のIDの型を定義します

export type AccountID = ID<Account>;

ID生成方法

IDはidパッケージのSnowflakeIDGeneratorクラスを利用して生成します。

Important

このとき、SnowflakeIDGeneratorは必ずコンストラクタで受け取るようにしてください。

export class RegisterService {
  /** 省略 **/
  private readonly snowflakeIDGenerator: SnowflakeIDGenerator;

  constructor(arg: {
    /** 省略 **/
    idGenerator: SnowflakeIDGenerator;
  }) {
    /** 省略 **/
    this.snowflakeIDGenerator = arg.idGenerator;
  }

このコードへのリンク (pkg/accounts/service/register.ts)

IDを生成するにはSnowflakeIDGenerator.generate<T>()メソッドを呼び出します。 この時の型引数 T は、生成したいIDの型を入れてください

const idRes = this.snowflakeIDGenerator.generate<AccountID>();

ID生成時にエラーが発生する場合があるので、必ずエラーハンドリングが必要です。

IDの利用方法

テストなどで利用する際にIDを静的に定義したい場合や、APIなどでユーザーからstring形式で受け取ったIDはアサーションすることでIDに変換できます。

const accountID = "31415926535" as AccountID;

認証トークン

JSON Web Token の一種で, ペイロードには 更新トークン など以下の情報を含みます.

更新トークンも JWT なので, JWT がネストしています.

  • sub: 発行対象のアカウント名
  • iat: 発行時刻 (UNIX エポックの秒数)
{
  "sub": "hogehoge-user",
  "iat": 1640995201
}

このトークンの有効期限は発行時刻から 15 分 (900 秒) です.

生成や検証の自作は実装ミスによる脆弱性を誘発しますのでライブラリを利用します.

更新トークン

JSON Web Token の一種で, ペイロードには以下の情報を含みます.

{
  "sub": "3e1644833000002",
  "iat": 1640995201
}

このトークンの有効期限は発行時刻から 30 日 (2,592,000 秒) です. 特に再発行はされず, 手動でのログイン時にのみ発行されます.

なおこのトークンは長命なので, 利用後に無効化しておかないと奪取されて再利用されるリスクがあります. 危険度は高いですがネットワーク上に流れることがほとんどないため, このリスクには対処せず保有することにします.

アカウント

自然人, 団体, ボットなど, このシステム上でオブジェクトを発行する主体となるものです. これは次の制約を持つ属性を備えます.

  • name: アカウント名
    • 1 文字以上の URL 安全な ASCII 文字からなる
  • nickname: 表示名, アカウントの表示に用いる自然言語の名称
    • 初期値は空文字列
    • 空文字列の場合はアカウント名にフォールバックする
    • RTL 制御文字を除く任意の UTF-8 シーケンス
  • created_at: このアカウントが作成された時刻
  • mail: メールアドレス
    • 有効なメールアドレスであることが検証済み
  • passphrase_hash: パスフレーズのハッシュ
    • 元となったパスフレーズは以下の性質を満たします
      • 文字種は正規化された UTF-8 シーケンス
      • 連続する空白タイプの文字 (スペース, タブ, 全角スペース, 改行文字など) は 1 つの半角スペースへと置き換えられる
      • 長さは Unicode スカラー値で 8 つぶん以上
  • salt: パスフレーズのハッシュに使ったソルト
    • パスフレーズの平文にこれを連結したもののハッシュは, passphrase_hash に等しい

なお, 基本情報はアカウント ID に関連付けられるようにし, このモデル自体は基本情報を保持しません.

アカウント状態

アカウント状態図

Mermaid code
stateDiagram-v2
	NOT_ACTIVATED --> ACTIVE
	ACTIVE --> FROZEN
	ACTIVE --> SILENCED
	SILENCED --> FROZEN
	FROZEN --> SILENCED
	SILENCED --> ACTIVE
	FROZEN --> ACTIVE

登録中アカウント

まだ実際にアカウントが発行されておらず, メールアドレスを検証するスキームに入れられているアカウントです. このアカウントは通常のアカウントとは別の領域に永続化されます.

メールアドレスの検証に用いるトークンはこのモデルに含まれず, AccountVerifyTokenRepository にアカウント ID と対にして別途保存されます.

なお, 登録中アカウントが追加されてから 168 時間(7days) が経過したものは無効とみなします.

アカウント関係

アカウントが他のアカウントに対してどのような関係性を持っているかを表すステートマシンです. あるアカウントから他のアカウントに対しては次のような関係が存在します.

  • NONE: なし
  • REQUESTING_FOLLOW: フォローリクエスト中
  • FOLLOWING: フォロー中
  • BLOCKING: ブロック中

そしてこれらの関係は次の遷移図のように遷移できます.

アカウント関係図

Mermaid code
stateDiagram-v2
    NONE --> REQUESTING_FOLLOW
    REQUESTING_FOLLOW --> NONE
    REQUESTING_FOLLOW --> FOLLOWING
    FOLLOWING --> NONE
    FOLLOWING --> BLOCKING
    NONE --> BLOCKING
    BLOCKING --> NONE

新しいモデルを追加する

ID 型の定義

新しいエンティティには必ずそのエンティティ専用の ID 型を定義する. ID<T> の型引数にエンティティのクラスを渡すことで,異なるエンティティの ID を混同するコンパイルエラーを得られる.

export type ListID = ID<List>;

エラー型の定義

モジュール内で発生するエラーは model/errors.ts にまとめて定義する. エラー名は PascalCase で,エラーの内容が一目でわかる名称をつける.

export class AccountNotFoundError extends Error {}
export class AlreadyFollowingError extends Error {}

Repository インタフェースの定義

モデルの永続化操作は model/repository.ts にインタフェースとして定義し,実装は adaptor/repository/ に置く. Service はこのインタフェースに依存し,具体的な永続化実装には依存しない.

export interface AccountRepository {
  findByID(id: AccountID): Promise<Option.Option<Account>>;
  create(account: Account): Promise<void>;
}

SnowflakeIDGenerator の受け取り方

Service から ID を生成する際は SnowflakeIDGenerator をコンストラクタで受け取る. 直接インポートして使うと,テスト時に差し替えられなくなる.

export class CreateNoteService {
  private readonly idGenerator: SnowflakeIDGenerator;

  constructor(arg: { idGenerator: SnowflakeIDGenerator }) {
    this.idGenerator = arg.idGenerator;
  }
}

Account モジュール

特に断りがない限り,文字列長 L以下のコードを実行して計算できる文字数 とする.

const count = (s: string) => {
  const segmenter = new Intl.Segmenter("ja-JP", { granularity: "grapheme" });
  return [...segmenter.segment(s)].length;
};

Account

Pulsate 上で投稿やフォローなどの行動を行う主体. 自然人・団体・ボットなど種別を問わず,このシステムでオブジェクトを発行するすべての主体が Account として表現される.

TypeScript 上ではアカウント固有の ID 型を次のように定義する.

export type AccountID = ID<Account>;
export type AccountName = `@${string}@${string}`;

Account は次の属性を持つ.

  • id:AccountID
    • このアカウントを一意に識別する Snowflake ID
  • name:AccountName
    • @username@domain の形式をとる
    • username は英数字で始まり,[a-zA-Z0-9_\-.] のみからなる
    • domain は RFC1035 のサブドメイン形式に従う
  • nickname:表示名
    • RTL 制御文字を除く任意の UTF-8 シーケンス
    • 初期値は空文字列
    • 空文字列の場合は name にフォールバックして表示される
    • 文字長は L ≦ 256(空文字列は許容)
  • bio:自己紹介文
    • 任意の UTF-8 文字列
    • 文字長は L ≦ 1024
  • mail:メールアドレス
    • 有効なメールアドレスであることが検証済み
    • 文字長は 7 ≦ L ≦ 319
  • passphraseHash:パスフレーズのハッシュ
    • ハッシュの元となるパスフレーズは次の性質を満たす
      • 正規化された UTF-8 シーケンス
      • 連続する空白文字(スペース・タブ・全角スペース・改行文字など)は 1 つの半角スペースに置き換えられている
      • 長さは Unicode スカラー値で 8 つぶん以上
  • salt:パスフレーズのハッシュに使ったソルト
    • passphraseHash は,salt をパスフレーズの平文に連結したもののハッシュに等しい
  • createdAt:アカウント作成日時
  • role:アカウントのロール(後述の AccountRole を参照)
  • frozen:凍結状態(後述の AccountFrozen を参照)
  • silenced:サイレンス状態(後述の AccountSilenced を参照)
  • status:アクティベーション状態(後述の AccountStatus を参照)

Note

アバター画像やヘッダー画像といったプロフィール関連の基本情報は,アカウント ID に関連付けられた別のエンティティとして管理される. Account モデル自体はこれらを保持しない.

AccountRole

Account に付与されるロール. 管理操作(凍結やサイレンスなど)を実行できるかどうかはこのロールによって決まる.

  • normal:通常のアカウント
  • moderator:モデレーター権限を持つアカウント
  • admin:管理権限を持つアカウント

AccountStatus (アクティベーション状態)

メールアドレスの検証が完了しているかを表す.

  • notActivated:メールアドレスの検証が完了していない
  • active:メールアドレスの検証が完了し,通常の活動が可能な状態

AccountFrozen (凍結状態)

管理者によるアカウント凍結の状態を表す. 凍結されたアカウントはログインできない.

  • normal:凍結されていない通常の状態
  • frozen:凍結された状態

AccountSilenced (サイレンス状態)

管理者によるアカウントサイレンスの状態を表す. サイレンスされたアカウントはパブリックタイムラインへの投稿ができない.

  • normal:サイレンスされていない通常の状態
  • silenced:サイレンスされた状態

アカウントのライフサイクル

AccountStatusAccountFrozenAccountSilenced を合わせたアカウント全体のライフサイクルを次の状態図に示す.

アカウント状態図

Mermaid code
stateDiagram-v2
	NOT_ACTIVATED --> ACTIVE
	ACTIVE --> FROZEN
	ACTIVE --> SILENCED
	SILENCED --> FROZEN
	FROZEN --> SILENCED
	SILENCED --> ACTIVE
	FROZEN --> ACTIVE

InactiveAccount (登録中アカウント)

メールアドレスの検証が完了しておらず,まだ有効な Account として発行されていない中間的なエンティティ. 通常の Account とは別の領域に永続化される.

InactiveAccount は次の属性を持つ.

  • id:AccountID
  • name:AccountName
  • mail:メールアドレス
  • passphraseHash:パスフレーズのハッシュ
  • role:AccountRole

Note

メールアドレスの検証に用いるトークンは InactiveAccount モデルに含まれない. トークンは AccountVerifyTokenRepository に accountID と対にして別途保存される.

登録中アカウントが作成されてから 168 時間(7 日間)が経過したものは無効とみなし,削除される.

AccountRelationship (アカウント関係)

あるアカウントから他のアカウントに対して成立する有向の関係を表すステートマシン. この関係は送信側(アクター)を起点とする一方向の状態であり,相手側の状態とは独立している.

アカウント関係図

Mermaid code
stateDiagram-v2
    NONE --> REQUESTING_FOLLOW
    REQUESTING_FOLLOW --> NONE
    REQUESTING_FOLLOW --> FOLLOWING
    FOLLOWING --> NONE
    FOLLOWING --> BLOCKING
    NONE --> BLOCKING
    BLOCKING --> NONE

各状態の意味は次のとおり.

  • NONE:特定の関係がない状態
  • REQUESTING_FOLLOW:フォローリクエストを送信し,相手の承認を待っている状態
  • FOLLOWING:フォローしている状態
  • BLOCKING:ブロックしている状態

フォロー関係の永続化エンティティは AccountFollow で,フォロー元(fromID),フォロー先(targetID),作成日時(createdAt)を持つ.

Note モジュール

特に断りがない限り,文字列長 L以下のコードを実行して計算できる文字数 とする.

const count = (s: string) => {
  const segmenter = new Intl.Segmenter("ja-JP", { granularity: "grapheme" });
  return [...segmenter.segment(s)].length;
};

Note

  • ユーザーによって行われる投稿.
  • ノートは本文を持つ.
    • UTF-8 文字列のみ許容する.
    • 文字長は 1 ≦ L ≦ 3,000
    • ノートがメディアファイルを含む場合のみ,0 ≦ L ≦ 3,000
  • ノートの本文がセンシティブであるなどの理由で折りたたむ場合は CW 注釈を追加する.
    • UTF-8 文字列のみ許容する.
    • 文字長は 1 ≦ L ≦ 256
  • ノートにはメディアファイルを添付できる.
    • メディアファイルは 16 個まで添付できる.
    • メディアファイルの詳細な仕様はDrive::Mediumを参照せよ.
  • ノートは公開範囲を指定して行う.
    • 公開範囲は以下の通り.
    • 公開:
      • パブリックタイムライン,ホームタイムライン,アカウントタイムラインに表示される.
    • ホーム:
      • パブリックタイムラインには表示されない.
      • 自分およびフォロワーのホームタイムラインに表示される.
      • アカウントのタイムラインに表示される.
    • フォロワー限定:
      • フォロワーのみ閲覧できる.
      • リノートは投稿者自身のみ行える.
  • ノートはリノートできる.
    • リノート:投稿者自身または他のユーザーのノートをそのまま再投稿すること
      • 本文:
        • UTF-8 文字列,L = 0
      • CW 注釈:
        • UTF-8 文字列,L = 0
      • メディアファイル:
        • 添付できない.
    • 引用:リノートのうち,再投稿に追加で本文を付け加えたもの.
      • 本文,CW 注釈,メディアファイルは通常のノートと同じである.
      • ただし,メディアファイルを含む投稿であっても文字長は 1 ≦ L ≦ 3,000
    • リノートも公開範囲を指定できる.
      • ただし,元の公開範囲より広い範囲は指定できない.

DirectNote (ダイレクト投稿)

  • 宛先としてアカウントを指定し,宛先と投稿者自身のみ閲覧できるノート.
    • ダイレクト投稿は宛先を必ず 1 つ指定する.
    • 投稿者自身と宛先アカウントの対,およびその間で送受信されたノートの集合を会話と呼ぶ.
      • どちらかのアカウントが削除された場合,会話は削除される.
  • ダイレクト投稿はリノートも引用もできない.
  • ダイレクト投稿は公開範囲を指定できない.
  • ダイレクト投稿の本文,CW 注釈,メディアファイルの仕様はノートと同じである.

Reaction (リアクション)

  • ノートに対して行うリアクション.
  • リアクションは UTF-8 絵文字を 1 つ持つ.
    • 絵文字の UTF-8 範囲は厳密に /[\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF]/ とする.
    • 複合絵文字は 1 文字として扱う.
  • リアクションは 1 つのノートに対して 1 つしか付けられない.

Bookmark (ブックマーク)

  • ノートを後で読むために保存するもの.
    • ただし,ダイレクト投稿はブックマークできない.
  • ブックマークは 1 つのノートに対して 1 つしか付けられない.
  • ブックマークは付けた本人しか取得できない.

Drive モジュール

Drive モジュールはメディアファイルのアップロード・変換・保存を担う. ノートへの添付に使えるメディアファイルはすべてこのモジュールを経由して管理される.

Medium (メディアファイル)

ノートに添付できるメディアファイルを表すエンティティ. アップロード時に適切なフォーマットへの変換とサムネイル生成が行われる.

TypeScript 上ではメディアファイル固有の ID 型を次のように定義する.

export type MediumID = ID<Medium>;

Medium は次の属性を持つ.

  • id:MediumID
    • このメディアファイルを一意に識別する Snowflake ID
  • name:ファイル名
  • authorId:このメディアファイルをアップロードしたアカウントの AccountID
  • hashBlurhash によるプレースホルダー文字列
    • ファイルの読み込み完了前に表示するぼかしプレースホルダーとして利用される
    • 画像以外の MIME タイプでは空文字列となる場合がある
  • mime:MIME タイプ(例:image/jpeg
  • nsfw:センシティブコンテンツのフラグ
    • true の場合,クライアントは閲覧前に警告を表示することが期待される
  • url:変換後のメディアファイルの URL
  • thumbnailUrl:サムネイル画像の URL

List モデル

List

アカウントをグループ化するエンティティ. リストタイムライン(GET /lists/{list_id}/notes)を通じて,アサインされたアカウントの投稿をまとめて閲覧できる.

TypeScript 上ではリスト固有の ID 型を次のように定義する.

export type ListID = ID<List>;

List は次の属性を持つ.

  • id:ListID
    • このリストを一意に識別する Snowflake ID
  • ownerId:作成者の AccountID
  • title:リストのタイトル
    • 文字長は 1 ≦ L ≦ 100
  • public:公開フラグ
    • true の場合,アサイン操作の際にアサインされたアカウントへ通知が送られ,リスト作成者以外のアカウントからもリストを参照できる
    • false の場合,アサイン通知は送られず,リスト作成者のみが参照できる
  • assignees:アサインされたアカウントの AccountID の集合

Note

一度の操作でアサインまたは削除できるアカウント数は最大 30.

Notification モジュール

Announcement (お知らせ)

インスタンス管理者がユーザー全体に向けて発信するメッセージ.

TypeScript 上ではお知らせ固有の ID 型を次のように定義する.

export type AnnouncementID = ID<Announcement>;

Announcement は次の属性を持つ.

  • id:AnnouncementID
  • title:タイトル
  • description:本文
  • typeAnnouncementKind(後述)
  • createdAt:作成日時
  • updatedAt:最終更新日時(省略可)
  • unread:未読フラグ

AnnouncementKind (お知らせの種類)

  • info:一般的なお知らせ
  • warn:ユーザー全体への警告

Notification (通知)

特定のアカウントに向けて発生する通知.

TypeScript 上では通知固有の ID 型を次のように定義する.

export type NotificationID = ID<Notification>;

Notification は次の属性を持つ.

  • id:NotificationID
  • typeNotificationKind(後述)
  • actorNotificationActor(後述)
  • createdAt:通知発生日時
  • noteId:対象ノートの Snowflake ID
    • mentionedrenotedreacted のときのみ存在する
  • content:関連テキスト
    • renoted ではリノート元ノートの本文(CW 設定時は空文字列),reacted ではリアクション絵文字
    • renoted および reacted のときのみ存在する

NotificationKind (通知の種類)

  • followed:フォローされた
  • followRequested:フォローをリクエストされた
  • followAccepted:自分のフォローリクエストが承認された
  • mentioned:メンションされた
  • renoted:リノートされた
  • reacted:リアクションされた

NotificationActor (通知の発生源)

通知を発生させた主体を表す判別共用体.

  • account:アカウントによる操作(ボットを含む)
    • id:AccountID
    • name:AccountName
    • nickname:表示名
    • avatar:アバター画像の URL
  • system:システムによる通知(モデレーター警告など)
    • 詳細は未定義

ドメインイベント

Pulsate は,ユーザーの操作や内部的な処理を非同期化し,リトライを可能にするためにイベント駆動アーキテクチャを採用している.

ドメインイベントは,操作によって生じた差分を記録し,他の処理を駆動するために必要なデータ(ペイロード)を運ぶオブジェクトである.

このページでは,モジュールごとにドメインイベントの発生条件,ペイロード,トリガーされる後続処理の一覧を示す.

共通のイベント形式

ドメインイベントは,次の共通フィールドを持つ.

export type EventID = ID<DomainEvent<unknown>>;

export interface DomainEvent<Payload> {
  id: EventID;
  name: string;
  occurredAt: Date;
  version: number;
  payload: Payload;
}
  • id:EventID
    • このイベントを一意に識別する Snowflake ID
  • name:イベント名
    • <ModuleName>.<ModelName>.<Action> の形式で表される,イベントの種類を示す文字列
    • 例:account.registerednote.bookmark.created
  • occurredAt:イベント発生時刻
  • version:ペイロードスキーマのバージョン番号
    • payload の構造に後方互換性のない変更を加えたときにインクリメントする
    • 購読側はこの番号を見て,対応していないバージョンのペイロードを判別できる
  • payload:イベント固有のデータ
    • 各イベントのペイロード型は <PascalCase のイベント名>EventPayload と命名する(例:AccountRegisteredEventPayload

各イベントのペイロードは,操作を行ったアカウントを示すフィールドとして actor(AccountID)を持つことを基本とする. 凍結,サイレンスなど,モデレーターが他のアカウントに対して行う操作では,actor は操作を行ったモデレーター自身の AccountID を指し,操作対象のアカウントとは別のフィールドで表す. account.registered のように,イベントの結果として初めて識別される対象(InactiveAccount)についても,その対象自身の AccountID を actor として扱う.

後続処理の一覧では,発火先を次の 2 種類で区別する.

  • (モジュール間)Intermodule パッケージを介して他モジュールが購読し,駆動される処理(内部フローとモジュール間通信を参照)
  • (外部):メール送信など,システム外部のサービスと連携する処理

ドメインモデルでの扱い

基本的にドメインモデルはドメインイベントを発行する。 ドメインイベントはドメインモデルの内部状態を変更したときに発行され、ドメインイベントの内部の状態として保持される。

class Account {
  // AccountEventsはAccountモデルで発生するDomainEventの型の集合
  #events: AccountEvent[];
  setBio(actor: AccountID, text: string) {
    // ...略
    this.#events.push(
      accountEventFactory.updateName({
        actor,
        target: this.#id,
        eventName: "account.bio.updated",
        bio: text,
      }),
    );
  }
}

ドメインイベントは明示的な操作で取り出すことができる。 このとき、ドメインイベントはすべて取り出され、ドメインモデル内部には残らない。

pullEvents(): AccountEvent[] {
  // 破壊的に読み出される
  return this.#events.splice(0);
}

取り出されたドメインイベントは,Application Serviceによってイベントバスに発行(publish)される. イベントバスは非同期にイベントを配送し,購読しているIntermodule パッケージへ届ける.発行から後続処理までの一連の流れは,次のようになる.

sequenceDiagram
    participant Service as Application Service
    participant Account as Account(ドメインモデル)
    participant EventBus as イベントバス
    participant Intermodule

    Service ->> Account: setBio(actor, text)
    activate Account
    Account ->> Account: 内部状態を変更しイベントを追加
    deactivate Account
    Service ->> Account: pullEvents()
    Account -->> Service: AccountEvent[](内部の一覧は空になる)
    Service ->> EventBus: 取り出したイベントを発行
    EventBus ->> 他モジュール: 購読しているモジュールへ配送
    他モジュール ->> Intermodule: 後続処理を実行

モジュール間のイベント

各イベントから後続処理への関係を,モジュール単位でまとめると次のようになる. 図中のラベルはイベント名と後続処理を示す.

flowchart LR
    Account["Accountモジュール"]
    Note["Noteモジュール"]
    Drive["Driveモジュール"]
    Timeline["Timelineモジュール"]
    Notification["Notificationモジュール"]
    Mail["メールサービス(外部)"]

    Account -->|"account.registered: 検証コードのメール送信"| Mail
    Account -->|"account.activated: タイムライン生成<br/>account.follow.blocked: タイムラインからの除去"| Timeline
    Account -->|"account.follow.requested: 通知発火<br/>account.follow.accepted: フォロー成功通知発火<br/>account.follow.rejected: フォロー失敗通知発火"| Notification
    Note -->|"note.created: タイムラインへの配信<br/>note.deleted: タイムラインからの除去"| Timeline
    Note -->|"note.renoted: リノート通知<br/>note.reaction.created: リアクション通知"| Notification
    Timeline -->|"list.member.appended: 通知発火"| Notification

Account モジュール

Account

account.registered:アカウント登録(仮登録状態)

  • 発生条件:ユーザーが登録フォームを送信し,InactiveAccount が作成されたとき
  • ペイロードAccountRegisteredEventPayload):
    • actor:登録するアカウント自身の AccountID(InactiveAccountid
    • name:AccountName
    • mail:メールアドレス
  • 後続処理
    • (外部)検証コードのメール送信

account.activated:アカウントの有効化

  • 発生条件:メールアドレスの検証が完了し,InactiveAccount から Account へ昇格したとき
  • ペイロードAccountActivatedEventPayload):
    • actor:有効化されるアカウント自身の AccountID
  • 後続処理
    • (モジュール間)タイムライン生成

account.bio.updatedaccount.nickname.updatedaccount.email.updated:プロフィール属性の更新

  • 3 つのイベントは,更新された属性が異なるだけで同じ構造を持つため,まとめて記載する.
  • 発生条件:ユーザーがプロフィール編集 API 経由で bionicknamemail のいずれかを更新したとき
  • ペイロードactor(AccountID,更新を行った本人)に加えて,更新後の値を 1 件持つ
    • account.bio.updatedAccountBioUpdatedEventPayload):bio:自己紹介文
    • account.nickname.updatedAccountNicknameUpdatedEventPayload):nickname:表示名
    • account.email.updatedAccountEmailUpdatedEventPayload):mail:メールアドレス

account.admin.frozenaccount.admin.unfrozen:管理者操作によるアカウントの凍結と解除

  • 発生条件moderator 以上のロールを持つアカウントが,対象アカウントを凍結/解除したとき
  • ペイロード(それぞれ AccountAdminFrozenEventPayloadAccountAdminUnfrozenEventPayload):
    • actor:操作を行った管理者の AccountID
    • accountId:凍結/解除対象の AccountID

account.admin.silencedaccount.admin.unsilenced:管理者操作によるサイレンス化と解除

  • 発生条件moderator 以上のロールを持つアカウントが,対象アカウントをサイレンス化/解除したとき
  • ペイロード(それぞれ AccountAdminSilencedEventPayloadAccountAdminUnsilencedEventPayload):
    • actor:操作を行った管理者の AccountID
    • accountId:サイレンス化/解除対象の AccountID

AccountAvatar

account.avatar.updated:アバター設定

  • 発生条件:ユーザーがアバター画像を設定または変更したとき
  • ペイロードAccountAvatarUpdatedEventPayload):
    • actor:AccountID
    • mediumId:設定されたメディアファイルの MediumID(Drive::Mediumを参照)

account.header.updated:ヘッダー設定

  • 発生条件:ユーザーがヘッダー画像を設定または変更したとき
  • ペイロードAccountHeaderUpdatedEventPayload):
    • actor:AccountID
    • mediumId:設定されたメディアファイルの MediumID

AccountFollow

account.follow.requested:フォローリクエスト

  • 発生条件:あるアカウントが他のアカウントに対してフォローリクエストを送信したとき(AccountRelationshipNONE から REQUESTING_FOLLOW へ遷移したとき)
  • ペイロードAccountFollowRequestedEventPayload):
    • actor:フォローリクエストを送信したアカウントの AccountID
    • targetId:フォローリクエストを受信したアカウントの AccountID
  • 後続処理
    • (モジュール間)通知発火

account.follow.accepted:フォロー承認

  • 発生条件:フォローリクエストが承認されたとき(REQUESTING_FOLLOW から FOLLOWING へ遷移したとき)
  • ここでの actor は「このイベントを実際に発生させた側」を指し,承認を行った受信側のアカウントである.Account モジュールAccountRelationship でいう「アクター」(関係の起点となる送信側,fromId)とは別の概念であり,account.follow.requested とは送信側と受信側が入れ替わる.
  • ペイロードAccountFollowAcceptedEventPayload):
    • fromId:フォローリクエストを送信したアカウントの AccountID
    • actor:フォローリクエストを承認したアカウントの AccountID
  • 後続処理
    • (モジュール間)フォロー成功通知発火

account.follow.rejected:フォロー拒否

  • 発生条件:フォローリクエストが拒否されたとき(REQUESTING_FOLLOW から NONE へ遷移したとき)
  • actor は拒否を行った受信側のアカウントであり,account.md の「アクター」(fromId)とは異なる(account.follow.accepted と同様).
  • ペイロードAccountFollowRejectedEventPayload):
    • fromId:フォローリクエストを送信したアカウントの AccountID
    • actor:フォローリクエストを拒否したアカウントの AccountID
  • 後続処理
    • (モジュール間)フォロー失敗通知発火

account.follow.unfollowed:フォロー解除

  • 発生条件:フォロー中のアカウントに対してフォローを解除したとき(FOLLOWING から NONE へ遷移したとき)
  • ペイロードAccountFollowUnfollowedEventPayload):
    • actor:フォローを解除したアカウントの AccountID
    • targetId:フォローを外されたアカウントの AccountID

account.follow.blocked:ブロック操作

  • 発生条件:あるアカウントが他のアカウントをブロックしたとき(AccountRelationshipBLOCKING へ遷移したとき)
  • ペイロードAccountFollowBlockedEventPayload):
    • actor:ブロックを行ったアカウントの AccountID
    • targetId:ブロックされたアカウントの AccountID
  • 後続処理
    • (モジュール間)タイムラインからの除去

account.follow.unblocked:ブロック解除

  • 発生条件:ブロックを解除したとき(BLOCKING から NONE へ遷移したとき)
  • ペイロードAccountFollowUnblockedEventPayload):
    • actor:ブロックを解除したアカウントの AccountID
    • targetId:ブロックを解除されたアカウントの AccountID

Note モジュール

Note

note.created:投稿作成

  • 発生条件:ユーザーがノートを投稿したとき
  • ペイロードNoteCreatedEventPayload):
    • noteId:NoteID
    • actor:投稿者の AccountID
    • text:本文
    • cw:CW 注釈(存在する場合のみ)
    • mediaIds:添付メディアファイルの MediumID の配列
    • visibility:公開範囲(publichomefollowers のいずれか)
  • 後続処理
    • (モジュール間)タイムラインへの配信
  • (検討中)visibilitydirect のダイレクト投稿(DirectNoteを参照)作成時に対応するイベントは本ページに未定義.本イベントの対象に含めるか,別イベントとするかは要検討

note.deleted:投稿削除

  • 発生条件:投稿者自身または管理者がノートを削除したとき
  • ペイロードNoteDeletedEventPayload):
    • noteId:NoteID
    • actor:削除を行ったアカウントの AccountID(投稿者自身の場合と,管理者の場合がある)
    • authorId:投稿者の AccountID(actor と異なる場合がある)
  • 後続処理
    • (モジュール間)タイムラインからの除去

note.renoted:リノート

  • 発生条件:投稿者自身または他のユーザーがノートをリノート(引用を含む)したとき
  • リノートの解除には専用のイベントを設けない.解除操作は,リノート自体を対象とした note.deleted として扱う.
  • ペイロードNoteRenotedEventPayload):
    • renoteId:新規に作成されたリノート自身の NoteID
    • targetId:リノート元ノートの NoteID
    • actor:リノートを行ったアカウントの AccountID
  • 後続処理
    • (モジュール間)リノート通知

Bookmark

note.bookmark.created:ブックマーク作成

  • 発生条件:ユーザーがノートをブックマークしたとき
  • ペイロードNoteBookmarkCreatedEventPayload):
    • actor:ブックマークを行ったアカウントの AccountID
    • noteId:対象ノートの NoteID

note.bookmark.deleted:ブックマーク削除

  • 発生条件:ユーザーがブックマークを解除したとき
  • ペイロードNoteBookmarkDeletedEventPayload):
    • actor:ブックマークを解除したアカウントの AccountID
    • noteId:対象ノートの NoteID

Reaction

note.reaction.created:リアクション作成

  • 発生条件:ユーザーがノートにリアクションを付けたとき
  • ペイロードNoteReactionCreatedEventPayload):
    • actor:リアクションを行ったアカウントの AccountID
    • noteId:対象ノートの NoteID
    • reaction:リアクションの絵文字
  • 後続処理
    • (モジュール間)リアクション通知

note.reaction.deleted:リアクション解除

  • 発生条件:ユーザーがリアクションを解除したとき
  • ペイロードNoteReactionDeletedEventPayload):
    • actor:リアクションを解除したアカウントの AccountID
    • noteId:対象ノートの NoteID

Drive モジュール

Medium

medium.created:メディア作成

  • 発生条件:ユーザーがメディアファイルをアップロードしたとき
  • ペイロードMediumCreatedEventPayload):
    • mediumId:MediumID
    • actor:アップロードしたアカウントの AccountID
    • mime:MIME タイプ
  • 後続処理
    • (検討中)サムネイル生成.Drive::Medium の説明ではアップロード時に同期的にサムネイル生成が行われるとしており,本イベントを契機とした非同期処理が別途必要かは未確認

medium.deleted:メディア削除

  • 発生条件:アップロードしたアカウント自身または管理者がメディアファイルを削除したとき
  • ペイロードMediumDeletedEventPayload):
    • mediumId:MediumID
    • actor:削除を行ったアカウントの AccountID(アップロード者自身の場合と,管理者の場合がある)
    • authorId:アップロードしたアカウントの AccountID(actor と異なる場合がある)

medium.admin.flaggedmedium.admin.unflagged:管理者によるフラグの付与と解除

  • 発生条件moderator 以上のロールを持つアカウントが,メディアファイルにフラグを付与/解除したとき
  • ペイロード(それぞれ MediumAdminFlaggedEventPayloadMediumAdminUnflaggedEventPayload):
    • actor:操作を行った管理者の AccountID
    • mediumId:MediumID

Timeline モジュール

List

list.created:リスト作成

  • 発生条件:ユーザーがリストを作成したとき
  • ペイロードListCreatedEventPayload):
    • listId:ListID
    • actor:作成者の AccountID

list.deleted:リスト削除

  • 発生条件:ユーザーがリストを削除したとき
  • ペイロードListDeletedEventPayload):
    • listId:ListID
    • actor:作成者の AccountID

list.member.appended:リストメンバー追加

  • 発生条件:リスト作成者がアカウントをリストにアサインしたとき
  • ペイロードListMemberAppendedEventPayload):
    • listId:ListID
    • actor:アサインを行ったリスト作成者の AccountID
    • accountId:アサインされたアカウントの AccountID
  • 後続処理
    • (モジュール間,publictrue のリストのみ)アサインされたアカウントへの通知発火

list.member.removed:リストメンバー削除

  • 発生条件:リスト作成者がアカウントをリストから外したとき
  • ペイロードListMemberRemovedEventPayload):
    • listId:ListID
    • actor:削除を行ったリスト作成者の AccountID
    • accountId:リストから外されたアカウントの AccountID

Notification モジュール

なし.

Notification モジュールは,他モジュールが発行するドメインイベントを購読して通知(Notification)を生成する側であり,自身はドメインイベントを発行しない. NotificationKindNotification モジュールを参照)を増やす場合は,対応する購読元イベントが本ページに存在するかをあわせて確認すること.

APIについて

main ブランチの最新 API リファレンスは api.pulsate.dev/reference で閲覧可能.

APIのバージョニング

APIの規定パス(Base Path)は /api/v0 である.

APIのバージョンは以下の通り.

バージョンステータス
v0策定途中

資格情報が必要なエンドポイントの実行方法

Pulsate v0 APIでは殆どのエンドポイントで資格情報が必要である.

資格情報が必要なエンドポイントへアクセスするためには, 以下の方法でエンドポイントにアクセスする必要がある.

エンドポイントへアクセスするときのAuthorization HTTPヘッダーに

Bearer <token>

の形式で 認証トークン をつけて送信する必要がある.

例:

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

資格情報が必要ないエンドポイントについて Pulsate v0 APIでは、資格情報が必要なエンドポイントであっても、資格情報がない場合にアクセス可能なエンドポイントが存在する。
例えば、あるアカウントのタイムラインを取得するエンドポイントの場合:

  • 有効な資格情報がある場合
    • フォローしている場合は FOLLOWER 公開範囲の投稿も返す
    • フォローしていない場合は資格情報がない場合と変わらない
  • 資格情報が無効な場合
    • 認証トークンが期限切れや改ざんされている場合
    • 認証エラーを返す
  • 資格情報がない場合
    • Authorizationヘッダーが空の場合
    • PUBLIC/HOME 公開範囲の投稿を返す

“文字数”の扱い

特に明記しない限り、文字数は 以下のコードを実行して計算できる文字数 とする.

const count = (s: string) => {
  const segmenter = new Intl.Segmenter("ja-JP", { granularity: "grapheme" });
  return [...segmenter.segment(s)].length;
};

このAPIを実装する実行環境のリストはMDNを参照せよ.

Intl.Segmenter - JavaScript | MDN

“時刻/時間”の扱い

特に明記しない限り、システム上の時刻の扱いは以下の通りである.

  • 日時は例外なくタイムゾーンをUTCとして扱う
  • 時刻のフォーマットは ISO 8601 とする.
    • "yyyy-MM-DDTHH:mm:ss.SSSZ"形式
    • タイムゾーン指定されていた場合は指定したタイムゾーンの日時をUTCに変換する.
      • 例:
        • 2023-11-21T09:00:00.000+09:00 : 日本時間(JST, UTC+9) 2023年11月21日09:00:00.000 → 2023-11-21T00:00:00.000Z : 世界協定時(UTC) 2023年11月21日00:00:00.000
    • 以上の形式に合致しない形で日時の表現がされた場合はエラー終了するものとする.

絵文字の扱い

絵文字とは, Unicodeによって定義される絵文字とカスタム絵文字のことである.

その他の記号や環境依存文字は絵文字ではないとする.

Unicode絵文字

Unicode絵文字とは,Unicodeによって定義される絵文字である.

APIとやり取りするテキストデータでは特殊な記法を用いずUnicodeで表現する.

例:

{
  "content": "🎉"
}

カスタム絵文字

カスタム絵文字を文字列内に埋め込むための形式は以下の通りである.

以下の形式に沿わない場合は単なる文字列であると解釈する.

  • 全体を<> で囲む
    • カスタム絵文字のエイリアスは::で囲む
    • カスタム絵文字に関する詳細な定義はここでは行わない. (ToDo)
  • エイリアスのあとに(絵文字があるインスタンスではなく,APIを提供するインスタンスでの)カスタム絵文字のIDを表記する.

例:

<:alias:2949583895994> <:ii_hanashi:284745363>

特殊なテキスト埋め込みオブジェクト

投稿本文, CW注釈, アカウントbio のテキスト内にカスタム絵文字やメンションなど, Unicodeで規定された文字以外のデータを表現するときの記法

カスタム絵文字は カスタム絵文字 を参照せよ.

メンション

表記法は以下の通り

<@[email protected]>
  • 宛先のホスト名(FQDN)は 省略不可
<@[email protected]> <@[email protected]> <@[email protected]>

基本エラーオブジェクト

APIにおいて何らかのエラーが発生した際には以下のようなエラーオブジェクトを返却する.

{
  "error": "TEST_ERROR_CODE"
}

errorの中にはエラーコード が入る. エラーコードはエラーを返却するAPIのエンドポイントでどのようなコードを返すか指定するものとする.

エラーオブジェクトはエラーコードのみをメンバーとして含むオブジェクトである.

エラーコード以外の情報をエラーオブジェクトとして返却することは許容されない.

NSFWフラグ

言葉の意味:

NSFWとは、英語圏で用いられるインターネットスラングで、「職場では見ない方がいい」(職場での閲覧は危険)という意味の略語である。日本語の「閲覧注意」におおむね相当する。

NSFWは画像がメインの掲示板や、掲示板中の画像へのリンクなどに付けて用いられやすい。たいていは卑猥(エロ画像)・残酷(グロ画像)といった要素の強い、不用意な閲覧ははばかられる類のコンテンツが掲載されている。

  • NSFWフラグとは,画像/音声/動画の内容がNSFWであることを示すためのフラグである.
  • 大抵の場合画像/音声/動画にぼかし(ブラー)がかけられたり,閲覧注意の文言が(システムのUI上で)表示される.

ContentsWarning/CWフラグ

  • CWフラグとは,投稿本文に何らかのコンテンツ警告があることを示すフラグである.
  • 大抵の場合,投稿の注釈(投稿本文より短い,本文の内容を端的に表した文)のみが表示され,本文は[続きを読む]といったボタンを押さないと見ることができない

アカウントAPI

POST /accounts

新規アカウントを作成する 正確には登録中の状態のアカウントを作成し,アカウント登録スキームを開始するものである.

入力

body: application/json

項目名制約/説明文字数制約
namestring文字は A-Z a-z 0-9 - . _である必要がある. 先頭,及び最後の文字は A-Z a-z 0-9 のみとする.1 ≤ N ≤ 64 [文字]john ※登録される情報は@[email protected]
emailstringメールアドレスとして正しい形式 (メールアドレスを受信可能であるかは問わない)7≤N≤319[文字][email protected]
passphrasestringスペース,タブ,全角スペース,改行,ヌルを除くUTF-8文字列8≤N≤512[文字]じゃすた・いぐざんぽぅ, just~@_examp1e!
captcha_tokenstringCloudflare Turnstile などの手動操作検証のトークン

入力例

{
  "name": "example",
  "email": "[email protected]",
  "passphrase": "じゃすた・いぐざんぽぅ",
  "captcha_token": "hogehogehgoe"
}

出力

200 OK

{
  "id": "38477395",
  "name": "example",
  "email": "[email protected]"
}

body: application/json

項目名説明文字数
idsnowflakeアカウントのID-30848577730000
namestringユーザー名8≤N≤512[文字]@[email protected]
emailstringメールアドレスとして正しい形式 (メールアドレスを受信可能であるかは問わない)7≤N≤319[文字][email protected]

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_ACCOUNT_NAME: 使用できない文字が含まれている,文字の使用制限に違反している
  • TOO_LONG_ACCOUNT_NAME: アカウント名が長すぎる
  • EMAIL_IN_USE: メールアドレスが既に使用されている
  • YOU_ARE_BOT: captcha_token の検証に失敗した

409 Conflict

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NAME_IN_USE: アカウント名が既に使用されている
  • EMAIL_IN_USE : メールアドレスが既に使用されている

PATCH /accounts/{account_name}

アカウント情報を指定のパラメータで編集するエンドポイント.

入力の body から次の情報のいずれか 1 つ以上を受け付け、それらを同時に適用できるかを検証し,反映する.

競合を防ぐために「更新前のニックネームと更新前のメールアドレスを結合した文字列」のハッシュを ETag (Entity Tag) として用いる.

これをMD5でハッシュするものとする.

メールアドレスを更新した場合はそのメールアドレスの確認スキームが開始される.

そしてそのメールアドレスが確認されるまで,メールアドレスの更新処理は遅延される.

ニックネームも同時に更新した場合,ニックネームの更新は先に反映される.

Warning

ETag が一致しないときは 412 Precondition Failed のエラーとなる. 結合方法は 更新前のニックネーム:更新前のメールアドレスである

入力

  • body: application/json
項目名制約/説明文字数制約
namestring文字は A-Z a-z 0-9 - . _である必要がある. 先頭,及び最後の文字は A-Z a-z 0-9 のみとする.1 ≤ N ≤ 64 [文字]john ※登録される情報は@[email protected]
emailstringメールアドレスとして正しい形式 (メールアドレスを受信可能であるかは問わない)7≤N≤319[文字][email protected]
passphrasestringスペース,タブ,全角スペース,改行,ヌルを除くUTF-8文字列8≤N≤512[文字]じゃすた・いぐざんぽぅ, just~@_examp1e!
captcha_tokenstringCloudflare Turnstile などの手動操作検証のトークン
biostring自己紹介文. 0文字である場合はnullundefinedではなく空の文字列""である必要がある.0≤N≤1024"" (空文字列), いい感じの自己紹介🆓, This is bio hello^~ <:javascript:358409384>

入力例

{
  "nickname": "example",
  "email": "[email protected]",
  "passphrase": "じゃすた・いぐざんぽぅ",
  "bio": "テストユーザーなのぜ"
}

出力

200 OK

編集に成功しました.

{
  "id": "38477395",
  "name": "@[email protected]",
  "nickname": "John Doe",
  "bio": "テストユーザーなのぜ",
  "email": "[email protected]"
}
項目名説明文字数
idsnowflakeアカウントのID-30848577730000
namestringユーザー名8≤N≤512[文字]@[email protected]
emailstringメールアドレスとして正しい形式 (メールアドレスを受信可能であるかは問わない)7≤N≤319[文字][email protected]
nicknamestring表示名,アカウントの表示に用いる短い文字列1≤N≤256JohnDoe<:json:299384730049>
ジョン・ドゥ🚉

202 Accepted

メールアドレスが更新された場合

{
  "id": "38477395",
  "name": "@[email protected]",
  "nickname": "John Doe",
  "bio": "テストユーザーなのぜ",
  "email": "[email protected]"
}

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_SEQUENCE: パラメータ内に使用不可能な文字種を含んでいる
  • VULNERABLE_PASSPHRASE: 新しいパスフレーズがパスフレーズとしての要件を満たしていない

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: 名前が account_name のアカウントが見つからない

412 Precondition Failed

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_ETAG: ETagが不正である

PUT /accounts/{account_name}/freeze

アカウントを凍結するエンドポイント.

モデレータ以上の権限を持つアカウント認証情報が必要である.

凍結されると以下のような挙動になる.

  • ログインできなくなる.
    • ログインしようとするとエラー終了する.
  • 投稿できなくなる.
  • (公開APIを除く)いかなるAPIへの操作も受け付けなくなる.

入力

  • パスパラメータ
    • account_name: string
      • アカウント名
  • body: application/json
    • 空のオブジェクトを送信せよ.
      • 空でない場合のメンバーは無視される.

入力例

{}

出力

204 No Content

凍結が完了した.

※レスポンスボディは空になる.

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • ALREADY_FROZEN: すでに凍結済みである.

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • NO_PERMISSION: アカウントを凍結できる権限がない.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: アカウントが見つからない.

DELETE /accounts/{account_name}/freeze

アカウントを凍結解除する(解凍と表記することもある).

モデレータ以上の権限の持つアカウント認証情報が必要.

入力

  • パスパラメータ
    • account_name: string
      • アカウント名
  • body: application/json
    • 空のオブジェクトを送信せよ.
      • 空でない場合のメンバは無視される.

入力例

{}

出力

204 No Content

凍結解除した.

※レスポンスボディは空になる.

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • NO_PERMISSION: アカウントを凍結解除できる権限がない.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: アカウントが見つからない.

POST /accounts/{account_name}/resend_verify_email

メールアドレスの検証コードを再送

入力

  • パスパラメータ
    • account_name: string
      • アカウント名

body: application/json

項目名制約/説明文字数
captcha_tokenstringCloudflare Trunstileなどの検証トークン

入力例

{
  "captcha_token": "hogehogehgoe"
}

出力

204 No Content

再送した

※レスポンスボディは空になる.

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_ALREADY_VERIFIED: アカウントのメールアドレスはすでに検証されている.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: アカウントが見つからない

POST /accounts/{account_name}/verify_email

メールアドレス認証が終了していないアカウントに対して,

メールアドレスに送信された認証トークンを検証することでメールアドレス認証を行う.

処理が完了すると,検証済みアカウントになりログインなどの各種操作が行えるようになる(ToDo 行うことのできる操作の一覧へのリンク

入力

  • パスパラメータ
    • account_name: string
      • アカウント名
  • body: application/json
項目名制約/説明文字数
tokenstring認証トークン: モデル を参照

入力例

{
  "token": "vq34rvyanho10q9hbc98ydbvaervna43r0varhj"
}

出力

204 No Content

検証に成功

※ レスポンスボディは空になります

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_TOKEN: トークンの検証に失敗.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: 指定した名前のアカウントは存在しない.

POST /login

ログインして認証/更新トークンを作成します.

Warning

認証トークンの有効期限は15分(900秒), 更新トークンの有効期限は30日(2,592,000秒)である. どちらのトークンも,APIサーバーが再起動されると無効になり,その場合は再度ログインする必要がある.

入力

  • body: application/json
項目名制約/説明文字数
namestringアカウント名8≤N≤512[文字]@[email protected]
passphrasestringパスフレーズ8≤N≤512じゃすた・いぐざんぽぅ, just~@_examp1e!
captcha_tokenstringCloudflare Trunstileなどの検証トークン

入力例

{
  "name": "@[email protected]",
  "passphrase": "じゃすた・いぐざんぽぅ",
  "captcha_token": "hogehogehoge"
}

出力

200 OK

ログインしました

{
  "authorization_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  eyJzdWIiOiIzZTE2NDQ4MzMwMDAwMDIiLCJpYXQiOjE2NDA5OTUyMDEsInJlZnJlc2hfdG9rZW4iOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKemRXSWlPaUl6WlRFMk5EUTR  Nek13TURBd01ESWlMQ0pwWVhRaU9qRTJOREE1T1RVeU1ERjkud2Q4cWJVcWowWGtCU1hud0FxM0lRYU1nQS1RTFd2MHVKU1NLX3BIVTZCYyJ9.mRUfLIYOGlLuC9D72zBriVvrHYrQgVHW7ntQ-bp5SHs",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.  eyJzdWIiOiIzZTE2NDQ4MzMwMDAwMDIiLCJpYXQiOjE2NDA5OTUyMDEsInJlZnJlc2hfdG9rZW4iOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKemRXSWlPaUl6WlRFMk5EUTR  Nek13TURBd01ESWlMQ0pwWVhRaU9qRTJOREE1T1RVeU1ERjkud2Q4cWJVcWowWGtCU1hud0FxM0lRYU1nQS1RTFd2MHVKU1NLX3BIVTZCYyJ9.mRUfLIYOGlLuC9D72zBriVvrHYrQgVHW7ntQ-bp5SHs",
  "expires_in": 1672498800
}
項目名制約/説明文字数
authorization_tokenstring認証トークン
refresh_tokenstring更新トークン
expires_innumber有効期限. Pulsate Epochからの秒数: モデル を参照.

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • FAILED_TO_LOGIN: パスフレーズかアカウント名が間違っている
  • YOU_ARE_BOT: captchaトークンの検証に失敗

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • YOU_ARE_FROZEN: ログインしようとしたアカウントは凍結されている

POST /refresh

更新トークンで認証トークンを再発行

入力

  • body: application/json
    • refresh_token: string
      • 更新トークン
項目名制約/説明文字数
refresh_tokenstring更新トークン 参照

入力例

{
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzZTE2NDQ4MzMwMDAwMDIiLCJpYXQiOjE2NDA5OTUyMDEsInJlZnJlc2hfdG9rZW4iOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKemRXSWlPaUl6WlRFMk5EUTRNek13TURBd01ESWlMQ0pwWVhRaU9qRTJOREE1T1RVeU1ERjkud2Q4cWJVcWowWGtCU1hud0FxM0lRYU1nQS1RTFd2MHVKU1NLX3BIVTZCYyJ9.mRUfLIYOGlLuC9D72zBriVvrHYrQgVHW7ntQ-bp5SHs"
}

出力

200 OK

ログインに成功した

{
  "authorization_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzZTE2NDQ4MzMwMDAwMDIiLCJpYXQiOjE2NDA5OTUyMDEsInJlZnJlc2hfdG9rZW4iOiJleUpoYkdjaU9pSklVekkxTmlJc0luUjVjQ0k2SWtwWFZDSjkuZXlKemRXSWlPaUl6WlRFMk5EUTRNek13TURBd01ESWlMQ0pwWVhRaU9qRTJOREE1T1RVeU1ERjkud2Q4cWJVcWowWGtCU1hud0FxM0lRYU1nQS1RTFd2MHVKU1NLX3BIVTZCYyJ9.mRUfLIYOGlLuC9D72zBriVvrHYrQgVHW7ntQ-bp5SHs"
}
項目名制約/説明文字数
autorization_tokenstring認証トークン
共通 を参照

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_TOKEN: 更新トークンが不正
  • EXPIRED_TOKEN: トークンの有効期限切れ

GET /accounts/{account_name}

アカウント情報を取得

入力

  • パスパラメータ
    • account_name: string
      • アカウント名

出力

200 OK

取得に成功

{
  "id": "2874987398",
  "name": "@[email protected]",
  "nickname": "John Doe",
  "bio": "I am Test User.",
  "avatar": "https://example.com/images/avatar.png",
  "header": "https://example.com/images/header.png",
  "followed_count": 200,
  "following_count": 10,
  "note_count": 20000
}
項目名制約/説明文字数
idsnowflakeアカウントID2934002842
namestringアカウント名8≤N≤512[文字]@[email protected]
nicknamestring表示名,アカウントの表示に用いる短い文字列1≤N≤256JohnDoe<:json:299384730049>, ジョン・ドゥ🚉
biostring自己紹介文. 0文字である場合はnullやundefinedではなくからの文字列である必要がある.0≤N≤1024"" (空文字列), いい感じの自己紹介🆓,This is bio hello^~ <:javascript:358409384>
avatarstringアカウントのアイコン画像のURL
headerstringアカウントのヘッダー画像のURL
followed_countnumberそのアカウントをフォローしている人数
following_countnumberそのアカウントがフォローしている人数
note_countnumberそのアカウントの投稿数

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: 指定した名前のアカウントは存在しない

PUT /accounts/{account_name}/silence

アカウントをサイレンスする.モデレータ以上の権限を持つアカウント認証情報が必要.

サイレンスされると以下のような挙動になる.

  • 公開投稿(投稿範囲がpublicになる投稿)ができなくなる

入力

  • パスパラメータ
    • account_name: string
      • アカウント名
  • body: application/json
    • ボディは空オブジェクトである必要がある.
    • 空でない場合,メンバは無視される.

入力例

{}

出力

204 No Content

凍結に成功

※レスポンスボディは空になる.

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • NO_PERMISSION: アカウントをサイレンスできる権限がない.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: 指定した名前のアカウントは存在しない.

DELETE /accounts/{account_name}/silence

アカウントをサイレンス解除

モデレータ以上の権限の持つアカウント認証情報が必要.

入力

  • パスパラメータ
    • account_name: string
      • アカウント名
  • body: application/json
    • オブジェクトは空である必要がある.
    • 空でない場合,メンバは無視される.

入力例

{}

出力

204 No Content

サイレンスを解除

※レスポンスボディは空になる.

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • NO_PERMISSION: アカウントをサイレンス解除できる権限がない

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: 指定した名前のアカウントは存在しない

POST /accounts/{account_name}/follow

指定したアカウントをフォロー

入力

  • パスパラメータ
    • account_name: string
      • フォローしたいアカウント名
  • body: application/json
    • 空オブジェクトを送信せよ
      • 空でない場合のメンバは無視される

入力例

{}

出力

201 Accepted

フォローを受け付けた

{
  "pending": true
}
  • フォローする相手がフォローを手動承認制にしている場合はpendingがtrueになる.
    • フォローする相手がフォローを手動承認制にしていない場合もある.
      • この場合はpendingは必ずfalseになる.
  • v2以降の予定: フォローする相手が同じインスタンスに所属していない場合はすべての場合でpendingがtrueになる.

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • ALREADY_FOLLOWING: アカウントをすでにフォローしている
  • YOU_ARE_BLOCKED: 相手にブロックされている

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: 指定した名前のアカウントは存在しない.

DELETE /accounts/{account_name}/follow

フォローを解除します

入力

  • パスパラメータ
    • account_name: string
      • フォロー解除したいアカウント名
  • body: application/json
    • リクエストボディは空オブジェクトを送信せよ.
      • 空でない場合のメンバは無視される.

入力例

{}

出力

204 No Content

フォローを解除した.

※レスポンスボディは空になる.

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • YOU_ARE_NOT_FOLLOW_ACCOUNT: 指定したアカウントをフォローしていない

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: 指定した名前のアカウントは存在しない

ドライブAPI

GET /drive

自分がアップロードした画像のリストを取得します

入力

なし

出力

200 OK

[
  {
    "id": "2938492384",
    "name": "image.jpeg",
    "author_id": "384880009940302",
    "hash": "nf9:e;g711*c@drgj55",
    "mime": "image/jpeg",
    "nsfw": false,
    "url": "https://images.example.com/image.jpeg",
    "thumbnail": "https://images.example.com/thumb-image.jpeg"
  }
]
項目名説明制約
idsnowflakeファイルのID
namestringファイル名1≤N≤256[文字]
author_idsnowflakeアップロードしたアカウントのID
hashstring画像のblurhash
mimestringmimeタイプ
nsfwbooleanNSFWフラグ
urlstring画像へのリンク
thumbnailstring, undefined(利用可能な場合のみ) 縮小版のサムネイル画像

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • FILE_NOT_FOUND: ファイルが存在しません

POST /drive/

添付ファイルをアップロードします

入力

  • body: multipart/form-data
項目名説明制約
namestringファイル名拡張子が必須1≤N≤256[文字]neko.png, 箱根駅伝.mp3
file(ファイルの実体)アップロードするファイル. アップロード可能なファイル種類は後述1≤100 [MB]
nsfwstring ( "true", "false" )NSFWフラグ
アップロード可能なファイル種類(mimeタイプ) mimeタイプについて詳しくは [https://www.iana.org/assignments/media-types/media-types.xhtml](https://www.iana.org/assignments/media-types/media-types.xhtml) を参照

画像:

  • image/apng
  • image/avif
  • image/gif
  • image/jpeg
  • image/png
  • image/webp

音声、動画:

  • audio/wave , audio/wav
  • audio/webm
  • audio/mpeg
  • video/mpeg
  • video/webm
  • audio/ogg

出力

200 OK

アップロードが完了しました.

{
  "id": "2938492384",
  "name": "image.jpeg",
  "author_id": "493094050",
  "hash": "nf9:e;g711*c@drgj55",
  "mime": "image/jpeg",
  "nsfw": false,
  "url": "https://images.example.com/image.jpeg",
  "thumbnail": "https://images.example.com/thumb-image.jpeg"
}
項目名説明制約
idsnowflakeファイルのID
namestringファイル名1≤N≤256[文字]
author_idsnowflakeアップロードしたアカウントのID
hashstring画像のblurhash
mimestringmimeタイプ
nsfwbooleanNSFWフラグ
urlstring画像へのリンク
thumbnailstring, undefined(利用可能な場合のみ) 縮小版のサムネイル画像

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • FILE_SIZE_TOO_BIG: ファイルサイズが大きすぎます
  • FILE_NAME_TOO_LONG: ファイル名が長すぎます

DELETE /drive/{file_id}

指定したファイルを削除します

Warning

ファイルを削除すると,紐付けられているすべてのノートに影響します.

入力

  • パスパラメータ
    • file_id: snowflake
      • ファイルのID

出力

204 No Content

削除しました.

※レスポンスボディは空になります.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • FILE_NOT_FOUND: ファイルが存在しません

GET /drive/{file_id}

ファイルのメタ情報を取得します

入力

  • パスパラメータ
    • file_id: snowflake
      • ファイルのID

出力

200 OK

{
  "id": "2938492384",
  "name": "image.jpeg",
  "author_id": "384880009940302",
  "hash": "nf9:e;g711*c@drgj55",
  "mime": "image/jpeg",
  "nsfw": false,
  "url": "https://images.example.com/image.jpeg",
  "thumbnail": "https://images.example.com/thumb-image.jpeg"
}
項目名説明制約
idsnowflakeファイルのID
namestringファイル名1≤N≤256[文字]
author_idsnowflakeアップロードしたアカウントのID
hashstring画像のblurhash
mimestringmimeタイプ
nsfwbooleanNSFWフラグ
urlstring画像へのリンク
thumbnailstring, undefined(利用可能な場合のみ) 縮小版のサムネイル画像

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • FILE_NOT_FOUND: ファイルが存在しません

PUT /drive/{file_id}

ファイルの情報を変更します

Warning

情報を変更すると、そのファイルが紐付けられているすべてのノートに影響します

入力

  • パスパラメータ

    • file_id: snowflake
      • ファイルのID
  • body: application/json

項目名説明制約
nsfwbooleanNSFWフラグ

入力例

{
  "nsfw": false
}

出力

200 OK

{
  "id": "2938492384",
  "name": "image.jpeg",
  "author_id": "384880009940302",
  "hash": "nf9:e;g711*c@drgj55",
  "mime": "image/jpeg",
  "nsfw": false,
  "url": "https://images.example.com/image.jpeg",
  "thumbnail": "https://images.example.com/thumb-image.jpeg"
}
項目名説明制約
idsnowflakeファイルのID
namestringファイル名1≤N≤256[文字]
author_idsnowflakeアップロードしたアカウントのID
hashstring画像のblurhash
mimestringmimeタイプ
nsfwbooleanNSFWフラグ
urlstring画像へのリンク
thumbnailstring, undefined(利用可能な場合のみ) 縮小版のサムネイル画像

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • FILE_NOT_FOUND: ファイルが存在しません
項目名説明制約
idsnowflakeファイルのID
namestringファイル名1≤N≤256[文字]
author_idsnowflakeアップロードしたアカウントのID
hashstring画像のblurhash
mimestringmimeタイプ
nsfwbooleanNSFWフラグ
urlstring画像へのリンク
thumbnailstring, undefined(利用可能な場合のみ) 縮小版のサムネイル画像

リストAPI

POST /lists/

リストを作成します

入力

  • body: application/json
項目名説明制約
titlestringリストのタイトル1≤N≤100[文字]
publicbooleanデフォルト: false (非公開) 公開:  リストにアサインされたアカウントには通知が飛びます. 非公開: 通知は飛ばず、自分以外のアカウントからは見えません

入力例

{
  "title": "Pulsate Developers",
  "public": false
}

出力

200 OK

{
  "id": "18342938400393",
  "title": "Pulsate Developers",
  "public": false
}

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • TITLE_TOO_LONG: タイトルが長すぎます

PATCH /lists/{list_id}

リスト情報を編集します

入力

  • パスパラメータ

    • list_id: string
      • 編集したいリストのID
  • body: application/json

項目名説明制約
titlestringリストのタイトル1≤N≤100[文字]
publicboolean公開・非公開のフラグ

入力例

{
  "title": "Edited Title",
  "public": true
}

出力

200 OK

{
  "id": "18342938400393",
  "title": "Edited Title",
  "public": true,
  "assignees": [
    {
      "id": "1838933554",
      "name": "@[email protected]"
    }
  ]
}

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • LIST_NOTFOUND: リストが見つかりません

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • TITLE_TOO_LONG: タイトルが長すぎます

GET /lists/{list_id}

リスト情報を取得します

入力

  • パスパラメータ
    • list_id: string
      • 取得したいリストのID

出力

200 OK

{
  "id": "18342938400393",
  "title": "Pulsate Developers",
  "public": false,
  "assignees": [
    {
      "id": "1838933554",
      "name": "@[email protected]"
    }
  ]
}

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • LIST_NOTFOUND: リストが見つかりません

GET /lists/accounts/{account_id}

アカウントが持つリストを取得します

入力

  • パスパラメータ
    • account_id: snowflake
      • アカウントのID

出力

200 OK

[
  {
    "id": "18342938400393",
    "title": "Pulsate Developers",
    "public": false,
    "assignees": [
      {
        "id": "1838933554",
        "name": "@[email protected]"
      }
    ]
  }
]

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND : ユーザーが存在しません
  • LIST_NOTFOUND: リストが見つかりません

POST /lists/{list_id}

リストにアカウントをアサインします

入力

  • パスパラメータ
    • list_id: string
      • リストのID
  • body: application/json
    • account_id: Array<snowflake>
      • アカウントID
      • 一度にアサインできる最大アカウント数: 30

出力

200 OK

{
  "account_id": [
    "389384553329569",
    "586039500493885",
    "4847377595"
  ]
}

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • TOO_MANY_TARGETS: アサインするアカウント数が多すぎます

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: アカウントが存在しません
  • LIST_NOTFOUND: リストが見つかりません

DELETE /lists/{list_id}

リストからアカウントを削除します

入力

  • パスパラメータ
    • list_id: string
      • リストのID
  • body: application/json
    • account_id: Array<snowflake>
      • 削除するアカウントのID
      • 一度に削除できるアカウント数: 30

入力例

{
  "account_id": [
    "389384553329569",
    "586039500493885",
    "4847377595"
  ]
}

出力

204 No Content

削除しました.

※レスポンスボディは空になります.

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • TOO_MANY_TARGETS : 削除するアカウント数が多すぎます

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ACCOUNT_NOT_FOUND: アカウントが存在しません
  • LIST_NOTFOUND: リストが見つかりません

DELETE /lists/{list_id}

リストを削除します.

入力

  • パスパラメータ
    • list_id: string
      • リストのID

出力

204 No Content

削除しました.

※レスポンスボディは空になります.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • LIST_NOTFOUND: リストが見つかりません

GET /lists/{list_id}/notes

リストのノートを取得します.

入力

  • パスパラメータ
    • list_id: snowflake
      • リストのID
  • クエリパラメータ
    • has_attachment: bool | undefined
      • デフォルト: false
      • ファイルを含む投稿のみを返します
    • no_nsfw : bool | undefined
      • デフォルト: false
      • NSFWフラグの立っているファイルを含む投稿を返さなくなります
    • before_id : ``snowflake| undefined
      • デフォルト: undefined
        • デフォルトでは現在取得できる最新の投稿から20件取得します.
      • 指定したIDより古い投稿を返します.指定したIDの投稿は含まれません

出力

200 OK

取得に成功しました

[
  {
    "id": "3893974892",
    "content": "hello world!",
    "cw_comment": "",
    "visibility": "public",
    "created_at": "2023-09-27T14:17:29.169Z",
    "attachment_files": [
      {
        "id": "204980348583",
        "filename": "hello.png",
        "content_type": "image/png",
        "url": "https://example.com/images/hello.png",
        "blur": "eoig:woi!our@nj/d",
        "nsfw": false
      }
    ],
    "reactions": [
      {
        "emoji": "<:alias:11938437>",
        "reacted_by": "3085763644"
      },
      {
        "emoji": "🎉",
        "reacted_by": "494984128"
      }
    ],
    "author": {
      "id": "2874987398",
      "name": "@[email protected]",
      "display_name": "John Doe",
      "bio": "I am Test User.",
      "avatar": "https://example.com/images/avatar.png",
      "header": "https://example.com/images/header.png",
      "followed_count": 200,
      "following_count": 10
    }
  }
]

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • LIST_NOTFOUND: リストが見つかりません
  • NOTHING_LEFT: これ以上古い投稿はありません
    • 1つでも古い投稿がある場合は投稿を返します

投稿API

Note

投稿は”ノート”, 再投稿は”リノート”と言い換えられている場合があります.

ダイレクト投稿: ノートのうち,公開範囲がダイレクトに指定されているもの

POST /notes

ノートを作成

入力

  • body: application/json
項目名制約/説明数制約
contentstring投稿本文添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring, undefined投稿の公開範囲 デフォルト: public とり得る値: public home followers, direct-
attachment_file_idsArray<snowflake>, undefined投稿の添付ファイルID 1つでもファイルが存在しない場合はエラー終了する0≤N≤16[個]
cw_commentstringCW時の注釈. CW/CWフラグ 参照デフォルトは空文字列0≤N≤256[文字]
send_tosnowflake, undefinedダイレクト投稿の宛先公開範囲がdirectのときのみ指定可能

入力例

{
  "content": "hello world!",
  "visibility": "public",
  "attachment_file_ids": [
    "11938472"
  ],
  "cw_comment": ""
}
{
  "content": "hello world!",
  "visibility": "direct",
  "attachment_file_ids": [
    "11938472"
  ],
  "cw_comment": "",
  "send_to": "8585030584"
}

出力

201 Created

投稿を作成しました.

{
  "id": "3893974892",
  "content": "hello world!",
  "cw_comment": "",
  "visibility": "public",
  "created_at": "2023-09-27T14:17:29.169Z",
  "attachment_files": [
    {
      "id": "11938472",
      "filename": "hello.png",
      "content_type": "image/png",
      "url": "https://example.com/images/hello.png",
      "blur": "eoig:woi!our@nj/d",
      "nsfw": false
    }
  ]
}
項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
send_toSnowflake, undefinedダイレクト投稿の宛先. 公開範囲がdirectのときのみ指定可能
created_atstring投稿の送信日時
  • 添付ファイル (attchment_files)
項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名3≤N≤256[文字]
content_typestringmimeタイプ
urlstring添付ファイルのURL
blurhashstring, undefined添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • TOO_MANY_ATTACHMENTS : 添付ファイルが制限を超過
  • TOO_MANY_CONTENT`` : CW注釈/投稿本文の文字数制限を超過
  • NO_DESTINATION: 公開範囲がdirectのノートでsend_toが指定されていない
  • INVALID_VISIBILITY: 公開範囲が正しい形式でない

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • YOU_ARE_SILENCED : サイレンスされている際に公開範囲をpublicに指定した場合

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ATTACHMENT_NOT_FOUND: 添付したファイルが存在しない
  • ACCOUNT_NOT_FOUND: 宛先(send_to)に指定したアカウントが存在しない

GET /notes/{note_id}

特定の投稿を取得します.

入力

  • パスパラメータ
    • note_id: string
      • 取得したい投稿のID

出力例

200 OK

投稿を取得しました.

{
  "id": "3893974892",
  "content": "hello world!",
  "cw_comment": "",
  "visibility": "public",
  "created_at": "2023-09-27T14:17:29.169Z",
  "attachment_files": [
    {
      "id": "204980348583",
      "filename": "hello.png",
      "content_type": "image/png",
      "url": "https://example.com/images/hello.png",
      "blur": "eoig:woi!our@nj/d",
      "nsfw": false
    }
  ],
  "reactions": [
    {
      "emoji": "<:alias:11938437>",
      "reacted_by": "3085763644"
    },
    {
      "emoji": "🎉",
      "reacted_by": "494984128"
    }
  ],
  "author": {
    "id": "2874987398",
    "name": "@[email protected]",
    "display_name": "John Doe",
    "bio": "I am Test User.",
    "avatar": "https://example.com/images/avatar.png",
    "header": "https://example.com/images/header.png",
    "followed_count": 200,
    "following_count": 10
  }
}
項目名説明数制約
idsnowflake投稿のID

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTE_NOT_FOUND: 指定したIDのノートが存在しない
    • 指定したIDのノートを(アクセスしたユーザーが)取得できない場合もこのエラーを返す.

POST /notes/{note_id}/renote

指定したIDのノートをリノートします.

入力

  • パスパラメータ
    • note_id: string
      • 取得したい投稿のID
  • body: application/json
項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
send_toSnowflake, undefinedダイレクト投稿の宛先. 公開範囲がdirectのときのみ指定可能
created_atstring投稿の送信日時

attachment_files:

項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名ToDo
content_typestringmimeタイプ
ToDo
urlstring添付ファイルのURL
blurstring添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo

入力例

{
  "content": "hello world!",
  "visibility": "public",
  "attachment_file_ids": [
    "11938472"
  ],
  "cw_comment": ""
}

出力

200 OK

リノートしました

{
  "id": "3893974892",
  "content": "hello world!",
  "cw_comment": "",
  "visibility": "public",
  "renote_id": "4973874850",
  "created_at": "2023-09-27T14:17:29.169Z",
  "attachment_files": [
    {
      "id": "11938472",
      "filename": "hello.png",
      "content_type": "image/png",
      "url": "https://example.com/images/hello.png",
      "blur": "eoig:woi!our@nj/d",
      "nsfw": false
    }
  ]
}
項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
send_toSnowflake, undefinedダイレクト投稿の宛先. 公開範囲がdirectのときのみ指定可能
created_atstring投稿の送信日時
項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名ToDo
content_typestringmimeタイプ
ToDo
urlstring添付ファイルのURL
blurstring添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • TOO_MANY_CHAR_LENGTH : CW注釈/投稿本文の文字数制限を超過
  • INVALID_VISIBILITY: 公開範囲が正しい形式でない
  • NO_DESTINATION: 公開範囲がdirectのノートでsend_toが指定されていない

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • YOU_ARE_SILENCED : サイレンスされている際に公開範囲をpublicに指定した

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ATTACHMENT_NOT_FOUND: 添付したファイルが存在しない
  • NOTE_NOT_FOUND: ノートが存在しない

POST /notes/{note_id}/reply

指定したIDのノートに返信します.

入力

  • パスパラメータ
    • note_id: string
      • 取得したい投稿のID
  • body: application/json
項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
created_atstring投稿の送信日時

attachment_files:

項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名ToDo
content_typestringmimeタイプ
ToDo
urlstring添付ファイルのURL
blurstring添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo

入力例

{
  "content": "hello world!",
  "visibility": "public",
  "attachment_file_ids": [
    "11938472"
  ],
  "cw_comment": ""
}

出力

200 OK

投稿を作成しました.

{
  "id": "3893974892",
  "content": "hello world!",
  "cw_comment": "",
  "reply_to": "2948933000",
  "visibility": "public",
  "created_at": "2023-09-27T14:17:29.169Z",
  "attachment_files": [
    {
      "id": "11938472",
      "filename": "hello.png",
      "content_type": "image/png",
      "url": "https://example.com/images/hello.png",
      "blur": "eoig:woi!our@nj/d",
      "nsfw": false
    }
  ]
}
項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
send_toSnowflake, undefinedダイレクト投稿の宛先. 公開範囲がdirectのときのみ指定可能
created_atstring投稿の送信日時
項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名ToDo
content_typestringmimeタイプ
ToDo
urlstring添付ファイルのURL
blurstring添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • TOO_MANY_CHAR_LENGTH : CW注釈/投稿本文の文字数制限を超過
  • INVALID_VISIBILITY: 公開範囲が正しい形式でない

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • YOU_ARE_SILENCED : サイレンスされている際に公開範囲をpublicに指定した
  • YOU_ARE_BLOCKED: 返信先ユーザーにブロックされている

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • ATTACHMENT_NOT_FOUND: 添付したファイルが存在しない
  • NOTE_NOT_FOUND: ノートが存在しない

DELETE /notes/{note_id}

投稿を削除します.

NOTICE: 自分以外のノートを削除する場合はモデレータ以上の資格情報が必要です.

入力

  • パスパラメータ
    • note_id: string
      • 削除するノートのID

出力

204 No Content

削除しました.

※ レスポンスボディは空になります

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • NO_PERMISSION: ノートの投稿者でないため削除できない

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTE_NOT_FOUND: 削除するノートが存在しない

POST /notes/{note_id}/reaction

指定したノートにリアクションします.

入力

  • パスパラメータ
    • note_id: string
      • リアクションしたい投稿のID

body: application/json

項目名説明
emojistring絵文字
表記法は カスタム絵文字 を参照<:alias:11938437>
🎉

入力例

{
  "emoji": "🎉"
}
{
  "emoji": "<:awesome:489395643749>"
}

出力

200 OK

リアクションしました.

{
  "id": "3893974892",
  "content": "hello world!",
  "cw_comment": "",
  "visibility": "public",
  "created_at": "2023-09-27T14:17:29.169Z",
  "attachment_files": [
    {
      "id": "204980348583",
      "filename": "hello.png",
      "content_type": "image/png",
      "url": "https://example.com/images/hello.png",
      "blur": "eoig:woi!our@nj/d",
      "nsfw": false
    }
  ],
  "reactions": [
    {
      "emoji": "<:alias:11938437>",
      "reacted_by": "3085763644"
    },
    {
      "emoji": "🎉",
      "reacted_by": "494984128"
    }
  ],
  "author": {
    "id": "2874987398",
    "name": "@[email protected]",
    "display_name": "John Doe",
    "bio": "I am Test User.",
    "avatar": "https://example.com/images/avatar.png",
    "header": "https://example.com/images/header.png",
    "followed_count": 200,
    "following_count": 10
  }
}
項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
send_toSnowflake, undefinedダイレクト投稿の宛先. 公開範囲がdirectのときのみ指定可能
created_atstring投稿の送信日時

attachment_files:

項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名ToDo
content_typestringmimeタイプ
ToDo
urlstring添付ファイルのURL
blurstring添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo

reactions:

項目名説明
emojistring絵文字. 表記法は 絵文字の扱い を参照🎉, <:alias:11938437>
reacted_bysnowflakeリアクションしたアカウントID48499372

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • ALREADY_REACTED: すでにリアクション済み
  • EMOJI_NOT_FOUND: 指定した絵文字が存在しない(カスタム絵文字のみ)/複数指定している(Unicode絵文字

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTE_NOT_FOUND: リアクションするノートが存在しない

DELETE /notes/{note_id}/reaction

指定したノートにつけたリアクションを解除します

入力

  • パスパラメータ
    • note_id: string
      • リアクションしたい投稿のID

出力

204 No Content

削除しました.

※ レスポンスボディは空になります.

400 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • NOT_REACTED: リアクションしていない

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTE_NOT_FOUND: ノートが存在しない

POST /notes/{note_id}/bookmark

指定した投稿をブックマークします

入力

  • パスパラメータ
    • note_id: string
      • リアクションしたい投稿のID

出力

200 OK

ブックマークしました

{
  "id": "3893974892",
  "content": "hello world!",
  "cw_comment": "",
  "visibility": "public",
  "created_at": "2023-09-27T14:17:29.169Z",
  "attachment_files": [
    {
      "id": "204980348583",
      "filename": "hello.png",
      "content_type": "image/png",
      "url": "https://example.com/images/hello.png",
      "blur": "eoig:woi!our@nj/d",
      "nsfw": false
    }
  ],
  "reactions": [
    {
      "emoji": "<:alias:11938437>",
      "reacted_by": "3085763644"
    },
    {
      "emoji": "🎉",
      "reacted_by": "494984128"
    }
  ],
  "author": {
    "id": "2874987398",
    "name": "@[email protected]",
    "display_name": "John Doe",
    "bio": "I am Test User.",
    "avatar": "https://example.com/images/avatar.png",
    "header": "https://example.com/images/header.png",
    "followed_count": 200,
    "following_count": 10
  }
}
項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
send_toSnowflake, undefinedダイレクト投稿の宛先. 公開範囲がdirectのときのみ指定可能
created_atstring投稿の送信日時

attachment_files:

項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名ToDo
content_typestringmimeタイプ
ToDo
urlstring添付ファイルのURL
blurstring添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo

reactions:

項目名説明
emojistring絵文字
表記法は 絵文字の扱い を参照<:alias:11938437
🎉
reacted_bysnowflakeリアクションしたアカウントID48499372

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTE_NOT_FOUND: ノートが存在しない

DELETE /notes/{note_id}/bookmark

指定した投稿をブックマーク解除します

入力

  • パスパラメータ
    • note_id: string
      • ブックマーク解除したい投稿のID

出力

204 No Content

ブックマーク解除しました

※レスポンスボディは空になります.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTE_NOT_FOUND: ノートが存在しない

項目名説明数制約
idsnowflake投稿のID
contentstring投稿本文. 添付ファイルが存在する場合は0文字を許容1≤3000[文字]
visibilitystring投稿の公開範囲. デフォルト: public. とり得る値: public home followers direct-
attachment_filesArray<object>投稿の添付ファイルのオブジェクト (後述)0≤N≤16[個]
cw_commentstringCW時の注釈, 参照 デフォルトは空文字列0≤N≤256[文字]
send_toSnowflake, undefinedダイレクト投稿の宛先. 公開範囲がdirectのときのみ指定可能
created_atstring投稿の送信日時
項目名説明数制約
idsnowflake投稿のID
filenamestringファイル名ToDo
content_typestringmimeタイプ
ToDo
urlstring添付ファイルのURL
blurstring添付ファイルが画像であるときのサムネイルの blurhash
nsfwbooleanToDo
項目名説明
emojistring絵文字
表記法は 絵文字の扱い を参照<:alias:11938437
🎉
reacted_bysnowflakeリアクションしたアカウントID48499372

通知API

お知らせ(Announce)の種類

種類コード説明
info一般的なお知らせ
warnユーザー全体への警告

通知の種類

種類コード通知が発生する条件
followedフォローされたとき
followRequestedフォローをリクエストされたとき
followAccepted自分が行ったフォローリクエストが承認されたとき
mentionedメンションされたとき
renotedリノートされたとき
reactedリアクションされたとき

followed - フォローされた

取り得るactorの種類:

  • account
{
  "id": "20923084093774",
  "type": "followed",
  "actor": {
    "type": "account",
    "account": {
      "id": "209384",
      "name": "@[email protected]",
      "nickname": "John Doe🌤",
      "avatar": "https://cdn.example.com/johndoe/avatar"
    }
  },
  "createdAt": "2024-08-01T00:00:00.000Z"
}

followRequested - フォローをリクエストされた

取り得るactorの種類:

  • account
{
  "id": "20923084093774",
  "type": "followRequested",
  "actor": {
    "type": "account",
    "account": {
      "id": "209384",
      "name": "@[email protected]",
      "nickname": "John Doe🌤",
      "avatar": "https://cdn.example.com/johndoe/avatar"
    }
  },
  "createdAt": "2024-08-01T00:00:00.000Z"
}

followAccepted - (自分が行った)フォローリクエストが承認されたとき

取り得るactorの種類:

  • account
{
  "id": "20923084093774",
  "type": "followAccepted",
  "actor": {
    "type": "account",
    "account": {
      "id": "209384",
      "name": "@[email protected]",
      "nickname": "John Doe🌤",
      "avatar": "https://cdn.example.com/johndoe/avatar"
    }
  },
  "createdAt": "2024-08-01T00:00:00.000Z"
}

mentioned - メンションされた

取り得るactorの種類:

  • account
{
  "id": "20923084093774",
  "type": "mentioned",
  // 言及元ノートID
  "noteId": "29847304533478",
  "actor": {
    "type": "account",
    "account": {
      "id": "209384",
      "name": "@[email protected]",
      "nickname": "John Doe🌤",
      "avatar": "https://cdn.example.com/johndoe/avatar"
    }
  },
  "createdAt": "2024-08-01T00:00:00.000Z"
}

renoted - リノートされた

取り得るactorの種類:

  • account
{
  "id": "20923084093774",
  "type": "renoted",
  // リノートされたノートID
  "noteId": "1032809844545437574",
  // 投稿本文(CW指定の場合は空になる)
  "content": "",
  "actor": {
    "type": "account",
    "account": {
      "id": "209384",
      "name": "@[email protected]",
      "nickname": "John Doe🌤",
      "avatar": "https://cdn.example.com/johndoe/avatar"
    }
  },
  "createdAt": "2024-08-01T00:00:00.000Z"
}

reacted - リアクションされた

取り得るactorの種類:

  • account
{
  "id": "20923084093774",
  "type": "reacted",
  // リアクションされたノートのID
  "noteId": "3094320840856",
  // リアクションの内容
  "content": "🐭",
  "actor": {
    "type": "account",
    "account": {
      "id": "209384",
      "name": "@[email protected]",
      "nickname": "John Doe🌤",
      "avatar": "https://cdn.example.com/johndoe/avatar"
    }
  },
  "createdAt": "2024-08-01T00:00:00.000Z"
}

actorについて

通知を発生させた主体の種類

  • account: (内部外部問わず) (ユーザー)アカウント
    • botの場合も含む
  • system: システム通知
    • モデレーター等からの警告通知など

account:

{
  "type": "account",
  "account": {
    // アカウントID
    "id": "209384",
    // アカウント名
    "name": "@[email protected]",
    // アカウントニックネーム
    "nickname": "John Doe🌤",
    // アバター画像のURL
    "avatar": "https://cdn.example.com/johndoe/avatar"
  }
}

system:

{
  "type": "system",
  "system": {
    // ToDo
  }
}

APIエンドポイント一覧

GET /notifications

届いているすべての通知を取得します。

入力

パスパラメータ:

  • limit: number, 返す通知の最大数
    • デフォルト: 30 / 最大: 50
  • after: string(date), この日以降の通知を返します
    • デフォルト: “1970-01-01”
  • include_read: boolean, trueの時は既読の通知も返します
    • デフォルト: false

出力

  • body: application/json
{
  // インスタンス全体へのお知らせ
  "announcements": [
    {
      "id": "308205359",
      "title": "Service maintenance",
      "description": "scheduled: 2024 Sep. 10 00:00 ~ 01:00(UTC)\nduring this period, all services will be unavailable.",
      // お知らせの種類
      "type": "info",
      "createdAt": "2024-08-01T00:00:00.000Z",
      // optional
      "updatedAt": "2024-08-01T10:00:00.000Z",
      // 既読か
      "unread": false
    }
  ],
  // 自分宛ての通知
  "notifications": [
    {
      "id": "20923084093774",
      "type": "followed",
      "actor": {
        "type": "account",
        "account": {
          "id": "209384",
          "name": "@[email protected]",
          "nickname": "John Doe🌤",
          "avatar": "https://cdn.example.com/johndoe/avatar"
        }
      },
      "createdAt": "2024-08-01T00:00:00.000Z"
    }
  ]
}

POST /notifications/{id}/read

通知を既読にします

入力

  • パスパラメータ

    • id: string
      • 既読にする通知ID
  • body: application/json

{}

出力

204 No Content 通知を既読にしました。

検索API

クエリ

ベースは Twitter v2 のクエリ.

オペレータ

Operator意味単独使用可能
keyword特定のキーワードを含む松江 AND どこyes
""文字列の完全一致"造幣局 桜"yes
from:特定ユーザーによるノートfrom:@[email protected] or from:@[email protected]yes
is:renoteリノートhello world -is:renoteno
is:quote引用リノート#筑後川花火大会 is:quoteno
has:link本文にリンクを含むノートnowplaying has:linkno
has:media添付ファイルを含むノート#徳川家康 has:mediano

演算子

演算子説明
ANDスペースを挟んで連続する演算子はAND 論理演算となり、両方の条件を満たしたノートが返されます
OROR を挟んで連続する演算子はOR論理演算となり、どちらかの条件を満たしたノートが返されます.
-キーワードの前にハイフンをつけると、そのキーワードを論理否定することができます.
()カッコでくくると演算子をグループ化できます. AND>ORの順で適用されます.

GET /search/notes

投稿を検索します. 返す最大件数は100件です

入力

  • クエリパラメータ
    • query: string
      • 検索クエリ
      • 上記の内容を受け取ります

出力

200 OK

検索に成功

出力の内容はタイムラインと同一である.

[
  {
    "id": "3893974892",
    "content": "hello world!",
    "cw_comment": "",
    "visibility": "public",
    "created_at": "2023-09-27T14:17:29.169Z",
    "attachment_files": [
      {
        "id": "204980348583",
        "filename": "hello.png",
        "content_type": "image/png",
        "url": "https://example.com/images/hello.png",
        "blur": "eoig:woi!our@nj/d",
        "nsfw": false
      }
    ],
    "reactions": [
      {
        "emoji": "<:alias:11938437>",
        "reacted_by": "3085763644"
      },
      {
        "emoji": "🎉",
        "reacted_by": "494984128"
      }
    ],
    "author": {
      "id": "2874987398",
      "name": "@[email protected]",
      "display_name": "John Doe",
      "bio": "I am Test User.",
      "avatar": "https://example.com/images/avatar.png",
      "header": "https://example.com/images/header.png",
      "followed_count": 200,
      "following_count": 10
    }
  }
]

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_QUERY: クエリが正しくありません

タイムラインAPI

Warning

取得した投稿は時系列順にソートされた状態で返されます.
一度に取得できる件数は最大20件です.

GET /timeline/{timeline_type}

タイムラインを取得します.

入力

  • パスパラメータ
    • timeline_type: string, undefined
      • とり得る値: home / global
      • デフォルト: home
  • クエリパラメータ
    • has_attachment: bool | undefined
      • デフォルト: false
      • ファイルを含む投稿のみを返します
    • no_nsfw : bool | undefined
      • デフォルト: false
      • NSFWフラグの立っているファイルを含む投稿を返さなくなります
    • before_id : snowflake| undefined
      • デフォルト: undefined
        • デフォルトでは現在取得できる最新の投稿から20件取得します.
      • 指定したIDより古い投稿を返します.指定したIDの投稿は含まれません

出力

200 OK

タイムラインを取得します.

[
  {
    "id": "3893974892",
    "content": "hello world!",
    "cw_comment": "",
    "visibility": "public",
    "created_at": "2023-09-27T14:17:29.169Z",
    "attachment_files": [
      {
        "id": "204980348583",
        "filename": "hello.png",
        "content_type": "image/png",
        "url": "https://example.com/images/hello.png",
        "blur": "eoig:woi!our@nj/d",
        "nsfw": false
      }
    ],
    "reactions": [
      {
        "emoji": "<:alias:11938437>",
        "reacted_by": "3085763644"
      },
      {
        "emoji": "🎉",
        "reacted_by": "494984128"
      }
    ],
    "author": {
      "id": "2874987398",
      "name": "@[email protected]",
      "display_name": "John Doe",
      "bio": "I am Test User.",
      "avatar": "https://example.com/images/avatar.png",
      "header": "https://example.com/images/header.png",
      "followed_count": 200,
      "following_count": 10
    }
  }
]

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_TIMELINE_TYPE: 指定したタイムラインタイプは存在しません

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTHING_LEFT: これ以上古い投稿はありません
    • 1つでも古い投稿がある場合は投稿を返します

GET /timeline/accounts/{account_id|account_name}

特定のユーザーの投稿を取得します.

入力

  • パスパラメータ
    • account_id,account_name: snowflake|string
      • アカウント名かアカウントのIDを指定できます.
  • クエリパラメータ
    • has_attachment: bool | undefined
      • デフォルト: false
      • ファイルを含む投稿のみを返します
    • no_nsfw : bool | undefined
      • デフォルト: false
      • NSFWフラグの立っているファイルを1つでも含む投稿を返さなくなります
    • before_id : snowflake| undefined`
      • デフォルト: undefined
        • デフォルトでは現在取得できる最新の投稿から20件取得します.
      • 指定したIDより古い投稿を返します.指定したIDの投稿は含まれません

出力

200 OK

タイムラインを取得します.

[
  {
    "id": "3893974892",
    "content": "hello world!",
    "cw_comment": "",
    "visibility": "public",
    "created_at": "2023-09-27T14:17:29.169Z",
    "attachment_files": [
      {
        "id": "204980348583",
        "filename": "hello.png",
        "content_type": "image/png",
        "url": "https://example.com/images/hello.png",
        "blur": "eoig:woi!our@nj/d",
        "nsfw": false
      }
    ],
    "reactions": [
      {
        "emoji": "<:alias:11938437>",
        "reacted_by": "3085763644"
      },
      {
        "emoji": "🎉",
        "reacted_by": "494984128"
      }
    ],
    "author": {
      "id": "2874987398",
      "name": "@[email protected]",
      "display_name": "John Doe",
      "bio": "I am Test User.",
      "avatar": "https://example.com/images/avatar.png",
      "header": "https://example.com/images/header.png",
      "followed_count": 200,
      "following_count": 10
    }
  }
]

400 Bad Request

{
  "error": "TEST_ERROR_CODE"
}
  • INVALID_TIMELINE_TYPE: 指定したタイムラインタイプは存在しません

403 Forbidden

{
  "error": "TEST_ERROR_CODE"
}
  • YOU_ARE_BLOCKED: 指定したアカウントにブロックされています.

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTHING_LEFT: これ以上古い投稿はありません
    • 1つでも古い投稿がある場合は投稿を返します
  • ACCOUNT_NOT_FOUND : 指定したアカウントが見つかりませんでした

GET /timeline/conversations

ダイレクト投稿を受け取った、もしくは送った相手を取得します.

入力

なし

出力

200 OK

[
  {
    "account": {
      "accountId": "20983985",
      "accountName": "@[email protected]",
      "nickname": "John Doe",
      "avatar": "https://example.com/images/avatar.png"
    },
    "updatedAt": "2023-09-10T00:00:00.000Z"
  }
]

GET /timeline/conversations/{account_id}

指定したアカウントとのダイレクト投稿を取得します.

入力

  • パスパラメータ
    • account_id: snowflake
      • ダイレクト投稿を取得したいアカウントID.
  • クエリパラメータ
    • before_id: snowflake | undefined
      • デフォルト: undefined
      • 指定したIDより古い投稿を返します.指定したIDの投稿は含まれません

出力

200 OK

タイムラインを取得します.

[
  {
    "id": "3893974892",
    "content": "hello world!",
    "cw_comment": "",
    "visibility": "public",
    "created_at": "2023-09-27T14:17:29.169Z",
    "attachment_files": [
      {
        "id": "204980348583",
        "filename": "hello.png",
        "content_type": "image/png",
        "url": "https://example.com/images/hello.png",
        "blur": "eoig:woi!our@nj/d",
        "nsfw": false
      }
    ],
    "reactions": [
      {
        "emoji": "<:alias:11938437>",
        "reacted_by": "3085763644"
      },
      {
        "emoji": "🎉",
        "reacted_by": "494984128"
      }
    ],
    "author": {
      "id": "2874987398",
      "name": "@[email protected]",
      "display_name": "John Doe",
      "bio": "I am Test User.",
      "avatar": "https://example.com/images/avatar.png",
      "header": "https://example.com/images/header.png",
      "followed_count": 200,
      "following_count": 10
    }
  }
]

404 Not Found

{
  "error": "TEST_ERROR_CODE"
}
  • NOTHING_LEFT: これ以上古い投稿はありません
    • 1つでも古い投稿がある場合は投稿を返します
  • ACCOUNT_NOT_FOUND : 指定したアカウントが見つかりませんでした

内部フロー・モジュール間通信

モジュール間通信を示す Mermaid ダイアグラム

Mermaid code
sequenceDiagram
    autonumber
    actor ユーザー
    ユーザー ->> NoteModule: 投稿

    opt ダイレクト投稿の場合
        NoteModule ->> AccountModule: 送信先アカウントの存在チェック
    end

    NoteModule ->> AccountModule: 投稿者情報取得

    opt リノートの場合
        NoteModule ->> NoteModule: 引用 or リノート先情報取得
    end

    NoteModule ->> ユーザー: 投稿完了
    NoteModule ->> SearchModule: 検索インデックス投入
    NoteModule ->> TimelineModule: 投稿作成イベント発火
    TimelineModule ->> AccountModule: フォロー関係情報取得
    TimelineModule ->> NotificationModule: 通知イベント発火
    TimelineModule ->> ActivityPubModule: 投稿作成イベント発火

モジュールの概要

Pulsateのバックエンドでは“モジュール“と呼ばれる単位にプログラムを大まかに分け、機能や責任範囲を分離しています.
現在時点でPulsateのバックエンドには以下のモジュールが存在しています.

Note Module

  • ノート(投稿)の作成を行う.
  • ノートの削除を行う.
  • ノート情報を返す.
  • 投稿に紐づいたメディアの管理をする

Account Module

  • アカウント情報の管理を行う.
  • アカウントを凍結/解除する.
  • アカウントをサイレンスする.
  • ログイン時に発行されるトークンの生成/検証を行う.
  • アカウントのフォロー/フォロー解除を行う.

Timeline Module

  • (ホーム/リスト)タイムラインを構築する.
    • 特定のアカウントが見ることのできる投稿をフォロー関係と公開範囲から決定する.

Drive Module

  • メディアファイルの投稿を受け付ける
  • メディアファイルを適切に圧縮する
  • メディアファイル情報を返す
  • メディアファイルの保存を行う

モジュールの内部構造

モジュールの内部は以下のようになっています

.
├── adaptor/
│   ├── controller コントローラー
│   ├── presenter APIのエラー定義
│   ├── repository Repositoryの実装
│   └── validator APIスキーマ定義ファイル
├── model/
│   ├── errors.ts モジュール内で使うエラーの定義
│   ├── <moduleName>.ts モジュールで使うモデル
│   └── repository.ts RepositoryのInterface定義
├── service 実際の処理を行うApplication Serviceの実装
├── mod.ts APIのハンドラ (エントリーポイント)
└── router.ts APIのルーティング定義

処理の大まかな流れは以下の通りです

  1. mod.ts がトップレベル(pkg/main.ts)から呼び出され、APIのリクエストを受ける
  2. router.tsの定義に従ってmod.tsで定義されたハンドラーにリクエストの内容が渡される
  3. adaptor/validatorで定義されたスキーマに沿ったリクエストかを判定する
  4. ハンドラーからadaptor/controllerを呼び出す
  5. controllerからserviceを呼び出す
  6. serviceが処理を行い、model/repository.tsで定義されたInterfaceを呼び出し、データの取得や保存を行う
    1. Intermoduleパッケージを利用して他のモジュールの機能を呼び出して使うこともできる

Intermoduleパッケージ (モジュール間通信)

モジュールが他のモジュールのServiceをimportすることは禁止されています. そのため, 他のモジュールの機能を利用したい場合には Intermodule パッケージを利用します.

Intermodule パッケージはそれぞれのモジュールごとに定義されるインタフェースで、モジュールが他のモジュールから参照される機能を公開するようになっています. Intermodule の内部ではそのモジュールのServiceのメソッドを呼び出すよう実装されています.

認可制御

このドキュメントでは,Pulsate API(v0) における認可制御について記述する.

用語

  • Actor: アクションを実行する主体.Accountが該当する.
  • Action: リソースに対して行う何らかの操作のこと.
    • read: 読み取り
    • write: 書き込み,更新(リソースが更新可能な場合),リソースの削除
  • Resource: Actionの対象となるもの.
  • Target: 操作が許可されたときに使用する,操作を行うまたは操作後のリソースを保存するもの.
  • Policy: ActorActionを実行するための条件.

全体像

  • Pulsate API での認可制御は Policy を接尾辞にもつクラス群によって定義される.
  • この Policy クラス群の各クラスは withCheck static メソッドを持ち,actor, action, resource, targetの3値,および関数 fn を要求する.
    • withCheck メソッドはジェネリクス <Target,Res> を受け取る.Target は target の型,Resfn の返値である.
interface PolicyArgs<Actor, Action, Resource> {
  actor: Actor;
  action: Action;
  resource: Resource;
}

type NotePolicyArgs = PolicyArgs<Account, NotePolicyAction, Note>;

class NotePolicy {
  static withCheck<Target, Res>(
    target: Target,
  ): (
    args: NotePolicyArgs,
    fn: (target: Target) => Promise<Result.Result<Error, Res>>,
  ) => Promise<Result.Result<Error, Res>> {
    return async (
      args: AccountPolicyArgs,
      fn: (target: Target) => Promise<Result.Result<Error, Res>>,
    ): Promise<Result.Result<Error, Res>> => {
      if (!args.actor) return Result.err(Error("ログインしないと使えません"));
      // 条件を満たしたときだけ実行する
      return await fn(target);
    };
  }
}

PolicyAction

Action は識別子 PolicyAction を用いて識別する.

  • PolicyAction は3つの要素からなる文字列である.
  • 要素は以下の通り.
    • 1: ポリシー名
    • 2: モデル名 (例: account, note)
      • モデルではないものも含む (例: follow ).
      • タイムラインの場合はタイムライン種別名とする (例: home, conversation)
    • 3: アクション名
  • これらは以下で示す型で表現できる形式に従って結合される.
type PolicyAction = `${PolicyName}.${ModelName}:${ActionName}`;
  • withCheck メソッドは,PolicyAction のポリシー名が自分が管理するものでない場合,エラーを返す.