A simple BlueSky profile labeler that can be ran on Cloudflare Workers github.com/SocksTheWolf/SimpleBSkyLabeler
cf bsky profile label bluesky cloudflare workers

Refactoring

+43 -49
+6 -4
README.md
··· 1 - # Simple account-only Bluesky labelling service 1 + # Simple account-only Bluesky labeler 2 2 3 - Can I create a labeller that only applies labels to accounts and save myself from 4 - the complexity of implementing the `com.atproto.label.subscribeLabels` and 5 - `/xrpc/com.atproto.label.queryLabels` endpoints? 3 + This is a very barebones Cloudflare Worker which acts as a Bluesky labeler 4 + service. 5 + 6 + It omits some features like signatures and support for the `queryLabels` endpoint, 7 + but seems to work just fine with the native bsky.app web and iOS app.
+37 -45
src/index.ts
··· 2 2 import type { At } from "@atcute/client/lexicons"; 3 3 import { concat as ui8Concat } from "uint8arrays"; 4 4 5 - function frameToBytes(type: "error", body: unknown): Uint8Array; 6 - function frameToBytes(type: "message", body: unknown, t: string): Uint8Array; 7 - function frameToBytes(type: "error" | "message", body: unknown, t?: string): Uint8Array { 8 - const header = type === "error" ? { op: -1 } : { op: 1, t }; 5 + function createErrorFrame(body: unknown): Uint8Array { 6 + const header = { op: -1 }; 7 + return ui8Concat([cborEncode(header), cborEncode(body)]); 8 + } 9 + 10 + function createFrame(body: unknown, type?: string): Uint8Array { 11 + const header = { op: 1, t: type }; 9 12 return ui8Concat([cborEncode(header), cborEncode(body)]); 10 13 } 11 14 12 15 const LABEL_VERSION = 1; 13 16 14 - // TODO: Signatures. But do I really need them? Guess not. 15 - 16 - // export function formatLabel( 17 - // label: UnsignedLabel & { sig?: ArrayBuffer | Uint8Array | At.Bytes }, 18 - // ): FormattedLabel { 19 - // const sig = label.sig instanceof ArrayBuffer 20 - // ? toBytes(new Uint8Array(label.sig)) 21 - // : label.sig instanceof Uint8Array 22 - // ? toBytes(label.sig) 23 - // : label.sig; 24 - // if (!sig || !("$bytes" in sig)) { 25 - // throw new Error("Expected sig to be an object with base64 $bytes, got " + sig); 26 - // } 27 - // return { ...label, ver: LABEL_VERSION, neg: !!label.neg, sig }; 28 - // } 29 - 30 - // export function signLabel(label: UnsignedLabel, signingKey: Uint8Array): SignedLabel { 31 - // const toSign = formatLabelCbor(label); 32 - // const bytes = cborEncode(toSign); 33 - // const sig = k256Sign(signingKey, bytes); 34 - // return { ...toSign, sig }; 35 - // } 36 - 37 17 async function replay(sub: WebSocket, cursor: number | null) { 38 - // XXX: Read from your DB any rows after `cursor`. 18 + // TODO: Read from your DB any rows after `cursor`. The below is some dummy data. 39 19 const rows = [ 40 20 { 41 - 21 + id: 0, 22 + src: "did:plc:3og4uthwqpnlasfb4hnlyysr", // @labelertest42.bsky.social 23 + uri: "did:plc:z72i7hdynmk6r22z27h6tvur", // @bsky.app 24 + val: "verified-human", 25 + cts: "2024-12-21T19:45:01.398Z", 26 + }, 27 + { 28 + id: 1, 29 + src: "did:plc:3og4uthwqpnlasfb4hnlyysr", // @labelertest42.bsky.social 30 + uri: "did:plc:oc6vwdlmk2kqyida5i74d3p5", // @support.bsky.team 31 + val: "verified-human", 32 + cts: "2024-12-22T19:45:01.398Z", 42 33 } 43 34 ]; 44 35 45 36 for (const row of rows) { 37 + if (row.id < (cursor ?? 0)) { 38 + continue; 39 + } 40 + 46 41 // https://atproto.com/specs/label#schema-and-data-model 47 42 const label = { 48 43 ver: LABEL_VERSION, 49 - src: "did:plc:3og4uthwqpnlasfb4hnlyysr" as At.DID, // @labelertest42.bsky.social 50 - uri: "did:plc:z72i7hdynmk6r22z27h6tvur", // @bsky.app 51 - val: "verified-human", 44 + src: row.src as At.DID, // @labelertest42.bsky.social 45 + uri: row.uri, // @bsky.app 46 + val: row.val, 52 47 neg: false, 53 - cts: "2024-12-21T19:45:01.398Z", 48 + cts: row.cts, 54 49 }; 55 - const bytes = frameToBytes("message", { 56 - seq: 0, // XXX: Row ID 57 - labels: [/*formatLabel(*/label/*)*/], 58 - }, "#labels"); 50 + 51 + const bytes = createFrame( 52 + { 53 + seq: row.id, 54 + labels: [label], 55 + }, 56 + "#labels" 57 + ); 59 58 sub.send(bytes); 60 59 } 61 60 } ··· 69 68 console.log("Request: ", JSON.stringify(new Map(request.headers))); 70 69 console.log("Text: ", await request.text()); 71 70 72 - if (url.pathname == "/init" && request.method == "POST") { 73 - // Set up labelling service and label defs. 74 - // 75 - // 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 76 - // 77 - // Meh, it's just easier to run `npx @skyware/labeler setup` and it sets everything up for you. 78 - 79 - } else if (url.pathname == "/xrpc/com.atproto.label.subscribeLabels") { 71 + if (url.pathname == "/xrpc/com.atproto.label.subscribeLabels") { 80 72 // Set up WS connection. 81 73 const upgradeHeader = request.headers.get('Upgrade'); 82 74 if (!upgradeHeader || upgradeHeader !== 'websocket') {