Skip to content

Commit 2fbd0a6

Browse files
committed
update label declaration publishing for atcute v4
1 parent 5a6f737 commit 2fbd0a6

3 files changed

Lines changed: 155 additions & 7 deletions

File tree

src/labeler/declare-labeler.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import '@atcute/bluesky'
2+
import { Client, CredentialManager } from '@atcute/client'
3+
import { ORGANIZATION_COLLECTION, PROFILE_COLLECTION } from '../lib/config'
4+
5+
export interface LoginCredentials {
6+
pds?: string
7+
identifier: string
8+
password: string
9+
code?: string
10+
}
11+
12+
export interface LabelValueDefinition {
13+
identifier: string
14+
[key: string]: unknown
15+
}
16+
17+
export interface DeclareLabelerOptions {
18+
subjectTypes?: string[]
19+
subjectCollections?: string[]
20+
reasonTypes?: string[]
21+
}
22+
23+
const DEFAULT_DECLARE_OPTIONS: Required<Pick<DeclareLabelerOptions, 'subjectTypes' | 'subjectCollections'>> = {
24+
subjectTypes: ['record'],
25+
subjectCollections: [PROFILE_COLLECTION, ORGANIZATION_COLLECTION],
26+
}
27+
28+
let xrpc: Client | undefined
29+
let credentialManager: CredentialManager | undefined
30+
31+
type XrpcAgent = {
32+
get: (name: string, options?: unknown) => Promise<{ data?: { value?: unknown } }>
33+
post: (name: string, options?: unknown) => Promise<XrpcPostResponse>
34+
}
35+
36+
type XrpcPostResponse = {
37+
ok: boolean
38+
status: number
39+
data?: {
40+
error?: string
41+
message?: string
42+
}
43+
}
44+
45+
async function loginAgent({ pds, ...credentials }: LoginCredentials) {
46+
credentialManager ??= new CredentialManager({ service: pds || 'https://bsky.social' })
47+
xrpc ??= new Client({ handler: credentialManager })
48+
49+
if (credentialManager.session && credentialsMatchSession({ pds, ...credentials }, credentialManager.session)) {
50+
return { agent: xrpc as unknown as XrpcAgent, session: credentialManager.session }
51+
}
52+
53+
const session = await credentialManager.login(credentials)
54+
return { agent: xrpc as unknown as XrpcAgent, session }
55+
}
56+
57+
function credentialsMatchSession(
58+
credentials: LoginCredentials,
59+
session: { did: string; handle: string; email?: string | null; pdsUri?: string },
60+
) {
61+
return (!!credentials.pds ? credentials.pds === session.pdsUri : true)
62+
&& [session.did, session.handle, session.email].includes(credentials.identifier)
63+
}
64+
65+
function buildDeclarationRecord(
66+
labelDefinitions: LabelValueDefinition[],
67+
options?: DeclareLabelerOptions,
68+
) {
69+
const labelValues = labelDefinitions.map(({ identifier }) => identifier)
70+
const mergedOptions = {
71+
...DEFAULT_DECLARE_OPTIONS,
72+
...options,
73+
}
74+
75+
return {
76+
$type: 'app.bsky.labeler.service',
77+
policies: {
78+
labelValues,
79+
labelValueDefinitions: labelDefinitions,
80+
},
81+
createdAt: new Date().toISOString(),
82+
...(mergedOptions.reasonTypes ? { reasonTypes: mergedOptions.reasonTypes } : {}),
83+
...(mergedOptions.subjectTypes ? { subjectTypes: mergedOptions.subjectTypes } : {}),
84+
...(mergedOptions.subjectCollections ? { subjectCollections: mergedOptions.subjectCollections } : {}),
85+
}
86+
}
87+
88+
function assertPostSucceeded(endpoint: string, response: XrpcPostResponse) {
89+
if (response.ok) return
90+
91+
const details = [response.data?.error, response.data?.message].filter(Boolean).join(': ')
92+
throw new Error(`XRPC post failed for ${endpoint} with status ${response.status}${details ? `: ${details}` : ''}`)
93+
}
94+
95+
export async function getLabelerDeclaration(credentials: LoginCredentials): Promise<Record<string, unknown> | null> {
96+
const { agent, session } = await loginAgent(credentials)
97+
98+
const { data } = await agent.get('com.atproto.repo.getRecord', {
99+
params: { collection: 'app.bsky.labeler.service', rkey: 'self', repo: session.did },
100+
}).catch(() => ({ data: { value: null } }))
101+
102+
return (data?.value as Record<string, unknown> | null) ?? null
103+
}
104+
105+
export async function getLabelerLabelDefinitions(credentials: LoginCredentials): Promise<LabelValueDefinition[] | null> {
106+
const declaration = await getLabelerDeclaration(credentials)
107+
return (declaration?.policies as { labelValueDefinitions?: LabelValueDefinition[] } | undefined)?.labelValueDefinitions ?? null
108+
}
109+
110+
export async function declareLabeler(
111+
credentials: LoginCredentials,
112+
labelDefinitions: LabelValueDefinition[],
113+
overwriteExisting?: boolean,
114+
options?: DeclareLabelerOptions,
115+
): Promise<void> {
116+
const { agent, session } = await loginAgent(credentials)
117+
const existing = await getLabelerDeclaration(credentials)
118+
119+
if (existing && !overwriteExisting) {
120+
if (overwriteExisting === false) return
121+
throw new Error('Label definitions already exist. Use `overwriteExisting: true` to update them, or `overwriteExisting: false` to silence this error.')
122+
}
123+
124+
const data = {
125+
collection: 'app.bsky.labeler.service',
126+
rkey: 'self',
127+
repo: session.did,
128+
record: buildDeclarationRecord(labelDefinitions, options),
129+
validate: true,
130+
}
131+
132+
if (existing) {
133+
const response = await agent.post('com.atproto.repo.putRecord', { input: data })
134+
assertPostSucceeded('com.atproto.repo.putRecord', response)
135+
} else {
136+
const response = await agent.post('com.atproto.repo.createRecord', { input: data })
137+
assertPostSucceeded('com.atproto.repo.createRecord', response)
138+
}
139+
}
140+
141+
export async function setLabelerLabelDefinitions(
142+
credentials: LoginCredentials,
143+
labelDefinitions: LabelValueDefinition[],
144+
options?: DeclareLabelerOptions,
145+
): Promise<void> {
146+
return declareLabeler(credentials, labelDefinitions, true, options)
147+
}

src/labeler/set-labels.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
import { BSKY_IDENTIFIER, BSKY_PASSWORD } from '../lib/config'
44
import { LABELS } from '../lib/constants'
5-
import { setLabelerLabelDefinitions } from '@skyware/labeler/scripts'
65
import { resolvePds } from '../lib/resolve-pds'
6+
import { setLabelerLabelDefinitions } from './declare-labeler'
77

88
async function main() {
99
const identifier = process.argv[2] || BSKY_IDENTIFIER

src/labeler/setup.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
// Run with: npx tsx src/labeler/setup.ts <handle> <password> [labeler-endpoint] [--token PLC_TOKEN]
33
// Or: npm run setup -- satyam2.climateai.org mypassword https://labeler.example.com
44

5-
import { plcRequestToken, plcSetupLabeler, declareLabeler } from "@skyware/labeler/scripts"
5+
import { plcRequestToken, plcSetupLabeler } from '@skyware/labeler/scripts'
66
import 'dotenv/config'
7-
import { LABELS } from "../lib/constants"
8-
import { resolvePds } from "../lib/resolve-pds"
9-
import * as readline from "node:readline"
10-
import * as fs from "node:fs"
11-
import { secp256k1 } from "@noble/curves/secp256k1"
7+
import { secp256k1 } from '@noble/curves/secp256k1'
8+
import * as fs from 'node:fs'
9+
import * as readline from 'node:readline'
10+
import { LABELS } from '../lib/constants'
11+
import { declareLabeler } from './declare-labeler'
12+
import { resolvePds } from '../lib/resolve-pds'
1213

1314
// --- Arg / env parsing ---
1415

0 commit comments

Comments
 (0)