これは Bluesky / ATProtocol Advent Calendar 2025 16日目の記事です。
前日のmaril氏の記事 でさらに高度なことをされているので興味がある方はそちらもご覧ください。
ブラウザ上でCARファイルを開いてBlueskyのRepoの中身を見たい(過去ポストを高速検索したい)と思って、公式の @atproto/repo を試したことがあるのですが、こちらはNode.jsを必要とするためブラウザで使用することができませんでした。
仕方なく自前でNode.js非依存のCARパーサを実装してログ検索に使っていたのですが、現在活発に開発されているらしいTypeScriptのパッケージmary-ext/atcute にRepo操作用のパッケージも含まれており、これを試してみたところかなり軽快に動作したので簡単な使用例を紹介します。
- A. Repoのトップレベル(Repoに関連付けられたアカウントDIDやリビジョンなど)へのアクセス方法です。
- B. Repo内のレコードをcollection別に処理する方法です。
// サンプルコード
import { decode } from "@atcute/cbor";
import { fromUint8Array as carFromUint8Array } from "@atcute/car";
import { fromString as cidFromString, equals as cidEquals } from "@atcute/cid";
import { fromUint8Array as repoFromUint8Array } from "@atcute/repo";
//for lexicon schema check
import { is } from "@atcute/lexicons";
import { AppBskyFeedPost, AppBskyActorProfile } from "@atcute/bluesky";
const arraybuf: ArrayBuffer = {/*CAR data from file or /xrpc/com.atproto.sync.getRepo */ }
const uint8data = new Uint8Array(arrayBuf);
// A. Parse CAR to extract top-level data object
// https://atproto.com/specs/repository#commit-objects
const car = carFromUint8Array(uint8data);
const roots = car.roots;
const rootCid = cidFromString(roots[0].$link);
for (const entry of car) {
if (roots.length > 0 && cidEquals(entry.cid, rootCid)) {
const decoded = decode(entry.bytes);
console.log("Repo DID:", decoded["did"]);
console.log("Repo Rev:", decoded["rev"]);
break;
}
}
// B. Parse repo entries
const repo = repoFromUint8Array(uint8data);
for (const entry of repo) {
// ^? RepoEntry { collection: 'app.bsky.feed.post', rkey: '3lprcc55bb222', ... }
switch (entry.collection) {
case "app.bsky.feed.post": {
const record = decode(entry.bytes);
if (is(AppBskyFeedPost.mainSchema, record)) {
// 各ポストに対する処理
} else {
console.warn(`Skipping invalid post record at rkey: ${entry.rkey}`);
}
break;
}
case "app.bsky.actor.profile": {
const record = decode(entry.bytes);
if (is(AppBskyActorProfile.mainSchema, record)) {
console.log("profile record:", profile);
} else {
console.warn(
`Skipping invalid profile record at rkey: ${entry.rkey}`
);
}
break;
}
default:
// do nothing
break;
}
}
良いライブラリなので今後も利用したいと思います。
これを使って抽出したポストをDuckDB Wasmに保存し、ブラウザ上で完結する自分用の高速検索ログビューアを作って使っています。その様子がこちらです。
https://bsky.app/profile/nus.bsky.social/post/3m7umeiqpek2q
以上、簡単な紹介でした。