skip to content
ひびくらのーと

CloudflareでBlueskyの簡易的なフィードを作る

/ 15 min read

Updated:
目次

こんにちは、ひびくらです!

CloudflareのWorkersを使って簡易的なBlueskyのフィードを作成する方法を説明します。
特定のポストを表示するだけのフィードですが、本格的なフィードを作成する前の最初の一歩として読んでください。

今回はTypeScriptで作成する方法と、Rustのaxumで作成する方法の2通り説明します。

事前準備

いくつか事前に準備しておくことがあります。

環境構築

予め以下をインストールしておいてください。ここではインストール方法を説明しません。

  • Node.js
    • Cloudflare Workersのプロジェクトの作成やデバッグ、Blueskyのフィードの登録に使用します。
    • TypeScriptを使用する場合はもちろん、Rustを使用する場合もインストールしてください。
  • Rustを使用する場合、Rust
    • TypeScriptで作成する場合は不要です。

サービスの登録

まだ登録していない場合は、BlueskyCloudflareのアカウントを登録しておいてください。

また、Blueskyのアプリパスワードを生成しておいてください。Blueskyにフィードを登録するときに必要になります。

確認すること

以下を事前に確認してください。

  • Blueskyの自分のアカウントのDID
  • WorkersのFQDN
    • FQDNはホスト名+ドメイン名のことです。
    • まだワーカーを作成していない場合は後でワーカーを作成したときに確認してください。
    • ワーカー名が workers-name 、サブドメイン名が subdomain なら workers-name.subdomain.workers.dev です。
  • 好きなポストをフィードで表示したい場合、そのポストのAT URI
    • at://did:plc:osec57qucip46vc4vm4vdtu6/app.bsky.feed.post/3l7vemwqj7i2f のように、 at:// から始まる形式です。
    • 公式クライアントでのポストのURLがわかる場合、以下のようにAT URIに変換できます。
      • ポストのURL: https://bsky.app/profile/<投稿者のDIDまたはハンドル>/post/<ポストのRKEY>
      • AT URI: at://<投稿者のDID>/app.bsky.feed.post/<ポストのRKEY>

決めておくこと

フィードのショートネームを決めておいてください。以下に箇条書きで説明します。

  • フィードのIDのようなものになります。
  • エンドユーザーに見えるタイトルとは違うものです。
  • 1つのサーバー(Cloudflare Worker)内で被らなければ恐らく大丈夫だと思います。(他人と被っても大丈夫です。)
  • フィードのURLに使用されます。また、サーバー(Cloudflare Worker)内でフィード毎に処理を分岐するために使用します。
  • 基本的には英数字と - (ハイフン)を使用するのが良いでしょう。
  • 公式のFeed Generatorでは例として whats-alf が使用されています。

このページでは short-name をフィードのショートネームの例として使用します。

フィードのショートネーム以外には以下を決めておく必要があります。ただし、以下はフィードをBlueskyに登録するときに決めても構いません。

  • フィードのタイトル
  • フィードの説明
  • フィードのアイコン
    • ただし、アイコンはなくても構いません。

コードを書く

それではコードを書きましょう。

Cloudflare公式のページを参考に、TypeScriptのHello World exampleプロジェクトを作成してください。複数のフィードを作成したい場合もプロジェクトは1つで大丈夫です。(勿論複数のプロジェクトに分けても構いません。)

プロジェクトを作成したら、以下のように /.well-known/did.json/xrpc/app.bsky.feed.getFeedSkeleton の2つのパスに対応させます。

src/index.ts
/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run `npm run dev` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `npm run deploy` to publish your worker
*
* Bind resources to your worker in `wrangler.toml`. After adding bindings, a type definition for the
* `Env` object can be regenerated with `npm run cf-typegen`.
*
* Learn more at https://developers.cloudflare.com/workers/
*/
/** `/xrpc/app.bsky.feed.getFeedSkeleton` にアクセスされたときの処理 */
const feed = async (url: URL): Promise<Response> => {
// クエリパラメータの feed には以下の形式で渡される
// at://<フィード作成者のDID>/app.bsky.feed.generator/<フィードのショートネーム>
// そこからフィードのショートネームを抽出し、変数 feedName に格納している
const feedParam = url.searchParams.get('feed');
const prefix = 'at://did:plc:osec57qucip46vc4vm4vdtu6/app.bsky.feed.generator/';
if (!feedParam?.startsWith(prefix)) {
return new Response('正しい形式のフィード名が必要です。', { status: 400 });
}
const feedName = feedParam.substring(prefix.length);
// 1つのWorkerで複数のフィードを登録できるため、ショートネームで分岐してフィード毎の処理を行う
switch (feedName) {
case 'short-name':
return Response.json({
feed: [
// post には AT URI の形式を渡す
{ post: 'at://did:plc:osec57qucip46vc4vm4vdtu6/app.bsky.feed.post/3lczymzg6xs2z' }
// フィードに複数のポストがある場合、上記の形式のオブジェクトが複数ある
],
});
default:
return new Response('不明なフィードです。', { status: 400 });
}
};
export default {
async fetch(request, env, ctx): Promise<Response> {
const url = new URL(request.url);
switch (url.pathname) {
// 2つのパスにGETメソッドでアクセスされるため、対応する
case '/.well-known/did.json':
// 以下のコメントが付いていない部分は固定の文字列で良い
return Response.json({
'@context': ['https://www.w3.org/ns/did/v1'],
// id は did:web:<WorkersのFQDN> の形式
id: 'did:web:workers-name.subdomain.workers.dev',
service: [
{
id: '#bsky_fg',
type: 'BskyFeedGenerator',
// serviceEndPoint は https://<WorkersのFQDN> の形式
serviceEndPoint: 'https://workers-name.subdomain.workers.dev',
},
],
});
case '/xrpc/app.bsky.feed.getFeedSkeleton':
return feed(url);
default:
return new Response('Hello World!');
}
},
} satisfies ExportedHandler<Env>;

処理を書いてデバッグも大丈夫そうでしたら、デプロイを行ってください。

フィードを登録する

プロジェクトをCloudflareにデプロイしたら、フィードを登録します。

今回は新たにフィード登録用のJavaScriptプロジェクトを作成します。以下の手順でプロジェクトを作成し、フィードを登録します。

  1. 適当な場所にディレクトリを作成し、 cd コマンドでそのディレクトリに移動します。

  2. npm init コマンドを行ってください。質問はすべてデフォルトで大丈夫です。

  3. npm i @atproto/api コマンドで @atproto/api パッケージを追加します。

  4. 以下の内容で publish.mjs を作成します。

    publish.mjs
    import fs from "fs/promises";
    import { AtpAgent/*, BlobRef*/ } from "@atproto/api";
    const main = async () => {
    const agent = new AtpAgent({ service: "https://bsky.social" });
    await agent.login({
    // identifier は自分のDIDまたはハンドル
    // (ハンドルの場合、先頭に @ は不要)
    identifier: "did:plc:************************",
    // password にはアプリパスワードを渡す
    password: "****-****-****-****",
    });
    // avatar にフィードのアイコンのローカルパスを格納しておく。
    // フィードにアイコンを登録しない場合は空文字にする。
    const avatar = "";
    // 以下の if 文はフィードにアイコンを登録する場合に必要
    let avatarRef/*: BlobRef | undefined*/ = undefined;
    if (avatar) {
    let encoding/*: string*/;
    if (avatar.endsWith("png")) {
    encoding = "image/png";
    } else if (avatar.endsWith("jpg") || avatar.endsWith("jpeg")) {
    encoding = "image/jpeg";
    } else {
    throw new Error("expected png or jpeg");
    }
    const img = await fs.readFile(avatar);
    const blobRes = await agent.com.atproto.repo.uploadBlob(img, {
    encoding,
    });
    avatarRef = blobRes.data.blob;
    }
    // 以下のコメントが付いていない部分は固定の文字列で良い
    await agent.com.atproto.repo.putRecord({
    repo: agent.session?.did ?? "",
    collection: "app.bsky.feed.generator",
    // rkey はフィードのショートネーム
    rkey: "short-name",
    record: {
    // did は did:web:<WorkersのFQDN> の形式
    did: "did:web:workers-name.subdomain.workers.dev",
    // displayName はフィードのタイトル
    displayName: "テストフィード",
    // description はフィードの説明
    description: "これはテストフィードです。",
    // avatar は上部で変換した avatarRef を渡す
    // アイコンを登録しない場合は undefined を渡す
    avatar: avatarRef,
    createdAt: new Date().toISOString(),
    },
    });
    console.log('All done 🎉');
    }
    main();
  5. フィードのショートネームやタイトルなどに間違いがないことをもう一度確認してください。

  6. node publish.mjs コマンドを実行し、フィードを登録します。

無事にフィードを登録できたら、自分のプロフィールからフィードにアクセスして、フィードが正常に動作しているか確認してみましょう。

フィードの登録を解除

フィードの登録を解除したくなった場合、登録するときと同じようにスクリプトを書いて実行します。

publish.mjs があるディレクトリに、以下の内容で unpublish.mjs を作成します。

unpublish.mjs
import { AtpAgent } from "@atproto/api";
const main = async () => {
const agent = new AtpAgent({ service: "https://bsky.social" });
await agent.login({
identifier: "did:plc:************************",
password: "****-****-****-****",
});
// 以下のコメントが付いていない部分は固定の文字列で良い
await agent.com.atproto.repo.deleteRecord({
repo: agent.session?.did ?? "",
collection: "app.bsky.feed.generator",
// rkey はフィードのショートネーム
rkey: "short-name",
});
console.log('All done 🎉');
}
main();

スクリプトを書いたら node unpublish.mjs コマンドで登録を解除します。

無事に実行できたら、自分のプロフィールにアクセスしてフィードが消えているか確認してください。

最後に

これで簡易的なフィードを作成することができました。
フィードは色々な方法でポストを集められるので非常に便利な機能です。ここでの説明に加えて getFeedSkeletoncursor の受け渡しに対応したり、独自に処理を加えたりして便利なフィードを作っていきましょう!