APIは以下の3つのグループに分かれています。
/oauth/*) :
ユーザーを認証しセッションを管理する
/fg/*) :
フィードジェネレーター本体のロジックをサーバー上に登録・削除する
/api/*) :
PDSに対して直接レコード(フィード宣言や画像など)を読み書きする
Content-Type: application/json 等を指定してください。
session_id)
を保持して各APIにリクエストを送信する必要があります。
fetch や
axios)を使用してリクエストを送る場合、必ず
credentials: 'include' (fetch) または
withCredentials: true (axios)
を設定してください。
/fg/*, /api/*)
にアクセスした場合、本サーバーの実装では
HTTPステータス 401 (Unauthorized)
にて、JSONボディに
{ "error": "need authorization" } が返ります。
error フィールドが
"need authorization"
であることを検知した場合、未ログインまたはセッション切れと判断し、/oauth/login
への誘導(再ログイン処理)を行う必要があります。
/api/*) 等を利用する際は、CSRF対策として
HTTPヘッダーに X-Requested-With: XMLHttpRequest
を含める必要があります。ヘッダーが含まれていない場合、サーバーから
HTTPステータス 403 (Forbidden) が返されます。
{ "error": "エラー詳細メッセージ" } の形式が返ります。
/api/*)
については、Bluesky(PDS)が返すエラーフォーマット(例:
{"error": "...", "message": "..."})がそのままプロキシされて返る場合があります。
condition)がどのようなポストを抽出するかをWebアプリ上で直接プレビュー取得するAPIは現在提供されていません。デプロイ後、Bluesky公式クライアント等から動作を確認してください。
/oauth/*)Webアプリを利用するユーザーのBlueskyアカウントを認証するためのエンドポイントです。
GET /oauth/loginhandle (string) : ユーザーのハンドル名 (例:
alice.bsky.social)
redirect (string) :
認証完了後に戻ってくるWebアプリのURL
session_id が設定されます。
GET /oauth/logoutsession_id){ "ok": true }session_id Cookieを削除(クリア)します。
/fg/*)
サーバーのRedisやElasticsearchに対してフィード抽出ロジックを展開するためのエンドポイントです。
エンドポイント: GET /fg/list
Cookie: 送信が必要 (session_id)
レスポンス (JSON):
{
"success": true,
"feeds": [
{
"repo": "did:plc:xxx...", // 作成者のDID
"rkey": "my-feed", // フィードのレコードキー
"filter": "keyword(猫)" // フィルター型 (Filter) の場合の条件文字列。存在しない場合は省略されます
},
{
"repo": "did:plc:xxx...",
"rkey": "my-search-feed",
"search": "source { postedBy($me, newest) }" // 検索型 (Search) の場合の条件文字列。存在しない場合は省略されます
}
]
}
【補足】フィード型の判別について: レスポンスに含まれる
feedsの各オブジェクトは、型に応じて"filter"または"search"のいずれか片方のキーのみを持ちます。クライアント側では、オブジェクト内にどちらのキーが存在するか(もしくはnullでないか)でフィードの種類(Filter / Search)を判別してください。
エンドポイント: POST /fg/put
Cookie: 送信が必要 (session_id)
Content-Type: application/json
リクエストボディ:
{
"type": "Filter", // または "Search" (必須)
"rkey": "my-feed", // フィードのレコードキー。URLの一部になります。
// 半角英数字、ドット(.)、ハイフン(-)のみ使用可能 (Regex: ^[a-zA-Z0-9.-]+$)。
// 1文字以上30文字以下を推奨(クライアント側では30文字以内のバリデーション制限あり)
"condition": "keyword(猫)" // サーバー側でパースされる独自のクエリ文字列(必須)
}
レスポンス (JSON):
{ "success": true }{ "error": "エラー詳細メッセージ" }
{ "error": "need authorization" }
rkey
のフィードが存在する場合は、抽出ロジックが上書き更新されます。
エンドポイント: POST /fg/delete
Cookie: 送信が必要 (session_id)
Content-Type: application/json
リクエストボディ:
{
"rkey": "my-feed" // 削除対象のレコードキー
}
レスポンス (JSON):
{ "success": true }{ "error": "エラーメッセージ" }
/api/*)
Blueskyネットワーク上でカスタムフィードをユーザーのプロフィールに表示させるためには、PDSに
app.bsky.feed.generator
というレコードを書き込む必要があります。このAPIは認証情報(DPoP等)を付与してPDSへリクエストをプロキシします。
注意: このパス以下の全てのリクエストに
X-Requested-With: XMLHttpRequestヘッダーが必須です。 参考: リクエスト・レスポンスの厳密なスキーマについては、Bluesky公式の Lexicon (AT Protocol HTTP API Reference) を参照してください。
エンドポイント: GET /api/get_session
ヘッダー:
X-Requested-With: XMLHttpRequest (必須)
Cookie: 送信が必要 (session_id)
レスポンス (JSON):
com.atproto.server.getSession
の結果。以下のオブジェクトが返されます。
{
"did": "did:plc:xxx...",
"handle": "alice.bsky.social",
"email": "[email protected]", // オプショナル
"emailConfirmed": true, // オプショナル
"active": true // オプショナル
}
エンドポイント: POST /api/upload_blob
ヘッダー:
X-Requested-With: XMLHttpRequest (必須)
Cookie: 送信が必要 (session_id)
Content-Type: アップロードする画像のMIMEタイプ (例:
image/png, image/jpeg,
image/webp 等)
リクエストボディ: 画像等のバイナリデータ
レスポンス (JSON):
アップロードされたBlobのメタデータ。レスポンスは以下の構造のように
"blob" フィールドでラップされています。
{
"blob": {
"$type": "blob",
"ref": {
"$link": "bafybeih..." // アップロードされた画像のCID文字列
},
"mimeType": "image/png",
"size": 12345
}
}
【補足】Content-Typeの自動判別について: クライアント側で
Content-Typeを明示的に指定しない場合、またはapplication/octet-streamを指定した場合、サーバー側は送信されたバイナリデータの先頭数バイト(マジックナンバー)から自動的に画像フォーマット(PNG、JPEG、GIF、WEBP)を判別し、適切な MIME タイプを付与して PDS にアップロードを行います。
GET /api/get_blobX-Requested-With: XMLHttpRequest (必須)
session_id)did (string, 必須) : Blobを所有するアカウントのDID (例:
did:plc:xxx...)
cid (string, 必須) : BlobのCID (例:
bafybeih...)
Content-Type(例: image/png)や
Content-Length が付与されます。
エンドポイント: POST /api/put_record
ヘッダー:
X-Requested-With: XMLHttpRequest (必須)
Cookie: 送信が必要 (session_id)
Content-Type: application/json
リクエストボディ:
com.atproto.repo.putRecord のスキーマに準拠。
パラメータ構造:
{
"repo": "did:plc:xxx...", // ユーザーのDID (ログイン中のDID)
"collection": "app.bsky.feed.generator", // コレクション名固定
"rkey": "my-feed", // フィードのレコードキー
"record": {
"$type": "app.bsky.feed.generator",
"did": "did:web:fg.shigepon.net", // フィードジェネレーターのDID。本システムでは "did:web:fg.shigepon.net" に固定
"displayName": "My Custom Feed", // 表示名(必須)
"description": "猫の画像だけを集めたフィードです", // 説明(オプショナル)
"createdAt": "2024-06-08T14:14:00.000Z", // 作成日時 (ISO-8601フォーマット)
"avatar": { // アバター画像がある場合のみ指定するBlobオブジェクト(オプショナル)
"$type": "blob",
"ref": {
"$link": "bafybeih..." // アップロード時に取得したCID文字列
},
"mimeType": "image/png",
"size": 12345
}
}
}
レスポンス (JSON):
com.atproto.repo.putRecord
の結果。以下のオブジェクトが返されます。
{
"uri": "at://did:plc:xxx.../app.bsky.feed.generator/my-feed",
"cid": "bafyreih..."
}
エンドポイント: POST /api/delete_record
ヘッダー:
X-Requested-With: XMLHttpRequest (必須)
Cookie: 送信が必要 (session_id)
Content-Type: application/json
リクエストボディ:
{
"repo": "did:plc:xxx...", // ユーザーのDID
"collection": "app.bsky.feed.generator", // コレクション名固定
"rkey": "my-feed" // 削除対象レコードキー
}
レスポンス (JSON): 成功時は空オブジェクト、もしくはステータスを含んだオブジェクトが返ります。
{}
エンドポイント: GET /api/list_records
ヘッダー:
X-Requested-With: XMLHttpRequest (必須)
Cookie: 送信が必要 (session_id)
クエリパラメータ:
repo (string, 必須) :
レコード一覧を取得するユーザーのDID (例:
did:plc:xxx...)
collection (string, デフォルト:
app.bsky.feed.generator) : 取得対象のコレクション
limit (number, オプショナル) : 取得件数の上限cursor (string, オプショナル) :
次ページ取得用のカーソル
レスポンス (JSON):
com.atproto.repo.listRecords
の結果。以下のオブジェクトが返されます。
{
"records": [
{
"uri": "at://did:plc:xxx.../app.bsky.feed.generator/my-feed",
"cid": "bafyreih...",
"value": {
"$type": "app.bsky.feed.generator",
"did": "did:web:fg.shigepon.net",
"displayName": "My Custom Feed",
"description": "説明文",
"createdAt": "2024-06-08T14:14:00.000Z",
"avatar": { // 設定されている場合のみ存在
"$type": "blob",
"ref": {
"$link": "bafybeih..."
},
"mimeType": "image/png",
"size": 12345
}
}
}
],
"cursor": "..." // 次ページがある場合のみ設定
}
GET /oauth/login?handle=...
へリダイレクトしてBlueskyでOAuth認証を行う。
POST /fg/put
を叩き、サーバー(バックエンド)の抽出ロジック(フィルタなど)を登録する。
POST /api/upload_blob
でPDSに画像をアップロードしてBlob参照を受け取る。
POST /api/put_record
を使い、app.bsky.feed.generator
レコードをユーザーのPDSに作成する。これでBlueskyの公式クライアント等からフィードが見えるようになる。
condition フィールドの仕様)
/fg/put APIで送信する
condition フィールドには、フィード의 型 (type)
に応じて独自のクエリ言語を使用します。
文字のエスケープとダブルクォーテーションについて: - フィルター型 (
"type": "Filter"): クエリ内で文字列(例:keyword(豚肉))を指定する際、ダブルクォーテーションは使用しません。また、カンマで区切ることで複数の単語を論理和(いずれかに一致)として一度に指定できます(例:keyword(猫,ねこ,ネコ))。 - 検索型 ("type": "Search"): 比較対象の文字列(例:"1")をダブルクォーテーション"で囲む必要があります。文字列内に"や\が含まれる場合は\でエスケープしてください。/fg/putはJSON形式でリクエストを送信するため、JSONの文字列表現としてのエスケープと二重になる点にご注意ください(例: JSONデータとしては"condition": "filter { text.raw equals \\\"1\\\" }"のように記述します)。 - 文字列リストの指定(containsAny,containsAll等)をする際は、JSON風のブラケット[]で囲み、各要素をダブルクォーテーションで囲んでカンマ区切りにします(例:text containsAny ["cat", "dog"])。
"type": "Filter")
Jetstreamからリアルタイムに流れてくるポストをフィルタリングするための条件式です。
論理演算子 && (AND)、|| (OR)、!
(NOT) と括弧 () を組み合わせて定義します。
利用可能なすべての関数:
word(単語...) :
ポストのテキストを形態素解析した結果(名詞以外も含む)に、指定した単語のいずれかが含まれていれば一致
keyword(単語...) :
ポストのテキストを形態素解析した結果(名詞のみ)に、指定した単語のいずれかが含まれていれば一致
sequence([単語...], ...) :
形態素解析結果(名詞以外も含む)に、指定した複数の単語リストの条件が順番通りに連続して出現すれば一致(例:
sequence([私,僕], [は,が]))
chars(文字列...) :
ポストのテキスト(単純な部分文字列検索)に、指定した文字列のいずれかが含まれていれば一致
regex(正規表現) :
ポストのテキストが指定した正規表現にマッチすれば一致
hasminor() :
全体の中で出現頻度の低い(レアな)単語を含んでいれば一致
hasmajor() : 出現頻度の高い単語を含んでいれば一致
minwords(数値) :
名詞を、指定した数値の個数以上含んでいれば一致
from(DID...) :
ポストを投稿したアカウントのDIDが、指定したDIDのいずれかであれば一致
mention(DID...) :
ポストに含まれるメンション先のDIDが、指定したDIDのいずれかであれば一致
link(文字列...) :
ポストに含まれるリンク(URL)に、指定した文字列のいずれかが含まれていれば一致
tag(タグ...) :
ポストに含まれるタグが、指定したタグのいずれかであれば一致
label(ラベル...) :
ポストに付与されているラベルが、指定したラベルのいずれかであれば一致
record(文字列...) :
引用している他ポスト(レコード)のURLに、指定した文字列のいずれかが含まれていれば一致
external(文字列...) :
引用している外部サイトのURLに、指定した文字列のいずれかが含まれていれば一致
image(文字列...) :
添付された画像のALT(代替テキスト)に、指定した文字列のいずれかが含まれていれば一致
video(文字列...) :
添付された動画のALT(代替テキスト)に、指定した文字列のいずれかが含まれていれば一致
hasmention() : メンションを含んでいれば一致haslink() : リンク(URL)を含んでいれば一致hastag() : タグを含んでいれば一致hasrecord() :
他のポスト(レコード)を引用していれば一致
hasexternal() :
外部サイトのリンク(カード)を引用していれば一致
hasimage() : 画像が添付されていれば一致hasvideo() : 動画が添付されていれば一致haslabel() : 何らかのラベルが付与されていれば一致
isreply() : 他のポストへのリプライであれば一致例:
keyword(豚肉) && keyword(もやし)
!hasmajor() && hasminor() && minwords(2) && !isreply()
"type": "Search")
過去に蓄積されたレコードやユーザー自身の関係性から動的にポストを取得・構築するための定義式です。
以下の最大4つのセクション(params, source,
filter, sort)で構成されます。
変数の定義を行います。
$変数名 = 値; (例:
$my_list = ["foo", "bar"];)
$now : 現在の日付時刻$me : カスタムフィードにアクセスをしたアカウントのDID
ベースとなるポストの集合を定義します。和集合 |、積集合
& で結合可能です。
postedBy(引数, ソート順) :
指定したDIDのユーザーが投稿したポスト
mentionedBy(引数, ソート順) :
指定したDIDのユーザーが行ったメンション
mentionedTo(引数, ソート順) :
指定したDIDのユーザー宛てのメンション
likedBy(引数, ソート順) :
指定したDIDのユーザー(本システムアクセス済みのもの)が「いいね」したポスト
repostedBy(引数, ソート順) :
指定したDIDのユーザー(本システムアクセス済みのもの)がリポストしたポスト
followedBy(引数, ソート順) :
指定したDIDのユーザーにフォローされているアカウントによるポスト
all(ソート順) : 全ポスト
※ 引数 にはDID文字列、DIDのリスト、または
$me などの変数を指定できます。
ソースに対する絞り込み条件を定義します。and、or、not
と括弧 () で結合可能です。
text (形態素),
text.raw (生テキスト), langs,
labels, facets.features.tag
createdAt, createdAt.year,
createdAt.month, createdAt.day,
createdAt.hour, createdAt.minute,
createdAt.second
reply, embed.images,
embed.video, embed.external,
embed.record
repo (投稿者のDID),
symbol (ポスト内のレアな単語)
@source.フィールド名
(ソースセクションで取得した元のポストの値を参照。例:@source.symbol)
==, !=, >=,
<=, >, <
equals (一致),
notEquals (不一致), startsWith (で始まる),
endsWith (で終わる), contains (含む),
matches (正規表現一致),
notMatches (正規表現不一致)
containsAny (いずれかを含む),
containsAll (すべてを含む)
exists (存在する),
notExists (存在しない)
最終的なポストの並び順と出力件数を指定します。抽出する件数を引数に指定できます(省略時または0の場合は最大1000件)。
newest(N) : 新しい方から古い方へソートoldest(N) : 古い方から新しい方へソートrandom(N) :
ランダムにソート(ただし同じ日の間に同じユーザーがアクセスすれば同じ結果を返す)
urandom(N) :
完全ランダムにソート(リロードする度に違う結果を返す)
mix(N, ソート順) :
source で抽出したポストを1つ置き、その後に
filter で抽出したポストを
N 個置くという構成を繰り返す(例:
mix(3, newest))。件数制限と併用する場合は
mix(3, newest(100)) のように記述します。
例:
source { postedBy($me, random) } filter { embed.images exists } sort { random }
source { likedBy($me, random) & repostedBy($me, random) }
source { postedBy($me, newest) } filter { embed.images exists } sort { mix(3, newest(100)) }