···11-# Simple account-only Bluesky labelling service
11+# Simple account-only Bluesky labeler
2233-Can I create a labeller that only applies labels to accounts and save myself from
44-the complexity of implementing the `com.atproto.label.subscribeLabels` and
55-`/xrpc/com.atproto.label.queryLabels` endpoints?33+This is a very barebones Cloudflare Worker which acts as a Bluesky labeler
44+service.
55+66+It omits some features like signatures and support for the `queryLabels` endpoint,
77+but seems to work just fine with the native bsky.app web and iOS app.
+37-45
src/index.ts
···22import type { At } from "@atcute/client/lexicons";
33import { concat as ui8Concat } from "uint8arrays";
4455-function frameToBytes(type: "error", body: unknown): Uint8Array;
66-function frameToBytes(type: "message", body: unknown, t: string): Uint8Array;
77-function frameToBytes(type: "error" | "message", body: unknown, t?: string): Uint8Array {
88- const header = type === "error" ? { op: -1 } : { op: 1, t };
55+function createErrorFrame(body: unknown): Uint8Array {
66+ const header = { op: -1 };
77+ return ui8Concat([cborEncode(header), cborEncode(body)]);
88+}
99+1010+function createFrame(body: unknown, type?: string): Uint8Array {
1111+ const header = { op: 1, t: type };
912 return ui8Concat([cborEncode(header), cborEncode(body)]);
1013}
11141215const LABEL_VERSION = 1;
13161414-// TODO: Signatures. But do I really need them? Guess not.
1515-1616-// export function formatLabel(
1717-// label: UnsignedLabel & { sig?: ArrayBuffer | Uint8Array | At.Bytes },
1818-// ): FormattedLabel {
1919-// const sig = label.sig instanceof ArrayBuffer
2020-// ? toBytes(new Uint8Array(label.sig))
2121-// : label.sig instanceof Uint8Array
2222-// ? toBytes(label.sig)
2323-// : label.sig;
2424-// if (!sig || !("$bytes" in sig)) {
2525-// throw new Error("Expected sig to be an object with base64 $bytes, got " + sig);
2626-// }
2727-// return { ...label, ver: LABEL_VERSION, neg: !!label.neg, sig };
2828-// }
2929-3030-// export function signLabel(label: UnsignedLabel, signingKey: Uint8Array): SignedLabel {
3131-// const toSign = formatLabelCbor(label);
3232-// const bytes = cborEncode(toSign);
3333-// const sig = k256Sign(signingKey, bytes);
3434-// return { ...toSign, sig };
3535-// }
3636-3717async function replay(sub: WebSocket, cursor: number | null) {
3838- // XXX: Read from your DB any rows after `cursor`.
1818+ // TODO: Read from your DB any rows after `cursor`. The below is some dummy data.
3919 const rows = [
4020 {
4141-2121+ id: 0,
2222+ src: "did:plc:3og4uthwqpnlasfb4hnlyysr", // @labelertest42.bsky.social
2323+ uri: "did:plc:z72i7hdynmk6r22z27h6tvur", // @bsky.app
2424+ val: "verified-human",
2525+ cts: "2024-12-21T19:45:01.398Z",
2626+ },
2727+ {
2828+ id: 1,
2929+ src: "did:plc:3og4uthwqpnlasfb4hnlyysr", // @labelertest42.bsky.social
3030+ uri: "did:plc:oc6vwdlmk2kqyida5i74d3p5", // @support.bsky.team
3131+ val: "verified-human",
3232+ cts: "2024-12-22T19:45:01.398Z",
4233 }
4334 ];
44354536 for (const row of rows) {
3737+ if (row.id < (cursor ?? 0)) {
3838+ continue;
3939+ }
4040+4641 // https://atproto.com/specs/label#schema-and-data-model
4742 const label = {
4843 ver: LABEL_VERSION,
4949- src: "did:plc:3og4uthwqpnlasfb4hnlyysr" as At.DID, // @labelertest42.bsky.social
5050- uri: "did:plc:z72i7hdynmk6r22z27h6tvur", // @bsky.app
5151- val: "verified-human",
4444+ src: row.src as At.DID, // @labelertest42.bsky.social
4545+ uri: row.uri, // @bsky.app
4646+ val: row.val,
5247 neg: false,
5353- cts: "2024-12-21T19:45:01.398Z",
4848+ cts: row.cts,
5449 };
5555- const bytes = frameToBytes("message", {
5656- seq: 0, // XXX: Row ID
5757- labels: [/*formatLabel(*/label/*)*/],
5858- }, "#labels");
5050+5151+ const bytes = createFrame(
5252+ {
5353+ seq: row.id,
5454+ labels: [label],
5555+ },
5656+ "#labels"
5757+ );
5958 sub.send(bytes);
6059 }
6160}
···6968 console.log("Request: ", JSON.stringify(new Map(request.headers)));
7069 console.log("Text: ", await request.text());
71707272- if (url.pathname == "/init" && request.method == "POST") {
7373- // Set up labelling service and label defs.
7474- //
7575- // As an example, here is a labeller's service record https://api.bsky.app/xrpc/com.atproto.repo.getRecord?repo=skywatch.blue&collection=app.bsky.labeler.service&rkey=self
7676- //
7777- // Meh, it's just easier to run `npx @skyware/labeler setup` and it sets everything up for you.
7878-7979- } else if (url.pathname == "/xrpc/com.atproto.label.subscribeLabels") {
7171+ if (url.pathname == "/xrpc/com.atproto.label.subscribeLabels") {
8072 // Set up WS connection.
8173 const upgradeHeader = request.headers.get('Upgrade');
8274 if (!upgradeHeader || upgradeHeader !== 'websocket') {