this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add `label add` and `label delete` commands

futur 6a0781a9 233441dc

+140 -53
+101 -42
src/bin.ts
··· 1 1 #!/usr/bin/env node 2 2 import { ComAtprotoLabelDefs } from "@atproto/api"; 3 + import { LabelValueDefinition } from "@atproto/api/dist/client/types/com/atproto/label/defs.js"; 3 4 import { IdResolver } from "@atproto/identity"; 4 5 import prompt from "prompts"; 5 6 import { 6 7 declareLabeler, 7 8 deleteLabelerDeclaration, 9 + getLabelerLabelDefinitions, 8 10 plcClearLabeler, 9 11 plcRequestToken, 10 12 plcSetupLabeler, 13 + setLabelerLabelDefinitions, 11 14 } from "./scripts/index.js"; 12 15 13 16 const args = process.argv.slice(2); 14 - const [command] = args; 17 + const [command, subcommand] = args; 15 18 16 19 const idResolver = new IdResolver(); 17 20 18 21 if (command === "create" || command === "delete") { 19 - const did = await promptAndResolveDidOrHandle(); 22 + const { did, password, pds } = await promptAuthInfo(); 20 23 21 - const { password, pds, endpoint, privateKey } = await prompt([{ 22 - type: "password", 23 - name: "password", 24 - message: "Account password (cannot be an app password):", 25 - }, { 26 - type: "text", 27 - name: "pds", 28 - message: "URL of the PDS where the account is located:", 29 - initial: "https://bsky.social", 30 - }, { 24 + const { endpoint, privateKey } = await prompt([{ 31 25 type: command === "create" ? "text" : undefined, 32 26 name: "endpoint", 33 27 message: "URL where the labeler will be hosted:", ··· 71 65 "Next, you will need to define a name, description, and settings for each of the labels you want this labeler to apply.", 72 66 ); 73 67 const labelDefinitions = await promptLabelDefinitions(); 74 - await declareLabeler({ identifier: did, password, pds }, labelDefinitions, true); 68 + if (labelDefinitions.length) { 69 + await declareLabeler({ identifier: did, password, pds }, labelDefinitions, true); 70 + } else { 71 + console.log( 72 + "No labels were defined. You can use the `label add` command later to define new labels.", 73 + ); 74 + } 75 75 76 76 console.log("Labeler setup complete!"); 77 77 } else { ··· 82 82 } catch (error) { 83 83 console.error("Error setting up labeler:", error); 84 84 } 85 + } else if (command === "label" && (subcommand === "add" || subcommand === "delete")) { 86 + const { did, password, pds } = await promptAuthInfo(); 87 + const labelDefinitions = await getLabelerLabelDefinitions({ identifier: did, password, pds }) 88 + ?? []; 89 + 90 + if (subcommand === "add") { 91 + console.log( 92 + "Now define a name, description, and settings for each of the labels you want to add.", 93 + ); 94 + const newDefinitions = await promptLabelDefinitions(); 95 + if (newDefinitions.length) { 96 + await setLabelerLabelDefinitions({ identifier: did, password, pds }, [ 97 + ...labelDefinitions, 98 + ...newDefinitions, 99 + ]); 100 + } 101 + } else { 102 + const { identifiers } = await prompt({ 103 + type: "multiselect", 104 + name: "identifiers", 105 + message: "Select the labels to remove", 106 + min: 1, 107 + choices: labelDefinitions.map((def) => ({ 108 + title: def.locales[0].name, 109 + value: def.identifier, 110 + })), 111 + }, { onCancel: () => process.exit(1) }); 112 + 113 + await setLabelerLabelDefinitions( 114 + { identifier: did, password, pds }, 115 + labelDefinitions.filter((def) => !identifiers.includes(def.identifier)), 116 + ); 117 + 118 + console.log("Deleted label(s):", identifiers.join(", ")); 119 + } 85 120 } else { 86 121 console.log("Usage: npx @skyware/labeler [command]"); 87 122 console.log("Commands:"); 88 123 console.log(" create - Initialize an account as a labeler."); 89 124 console.log(" delete - Restore a labeler account to normal."); 125 + console.log(" label add - Add new label declarations to a labeler account."); 126 + console.log(" label delete - Remove label declarations from a labeler account."); 90 127 } 91 128 92 - async function promptAndResolveDidOrHandle() { 129 + async function promptAuthInfo() { 93 130 let did: string | undefined; 94 131 while (!did) { 95 132 const { did: didOrHandle } = await prompt({ ··· 99 136 validate: (value) => 100 137 value.startsWith("did:") || value.includes(".") || "Invalid DID or handle.", 101 138 format: (value) => value.startsWith("@") ? value.slice(1) : value, 102 - }); 139 + }, { onCancel: () => process.exit(1) }); 103 140 if (!didOrHandle) continue; 104 141 did = didOrHandle.startsWith("did:") 105 142 ? didOrHandle ··· 108 145 console.log(`Could not resolve "${didOrHandle}" to a valid account. Please try again.`); 109 146 } 110 147 } 111 - return did; 148 + 149 + const { password, pds } = await prompt([{ 150 + type: "password", 151 + name: "password", 152 + message: "Account password (cannot be an app password):", 153 + }, { 154 + type: "text", 155 + name: "pds", 156 + message: "URL of the PDS where the account is located:", 157 + initial: "https://bsky.social", 158 + validate: (value) => value.startsWith("https://") || "Must be a valid HTTPS URL.", 159 + }], { onCancel: () => process.exit(1) }); 160 + 161 + return { did, password, pds }; 112 162 } 113 163 114 164 async function confirm(message: string) { ··· 119 169 } 120 170 } 121 171 122 - async function promptLabelDefinitions() { 123 - const definitions: Array<ComAtprotoLabelDefs.LabelValueDefinition> = []; 124 - let addAnother = true; 125 - while (addAnother) { 126 - console.log("Enter the details for the next label you would like this labeler to apply."); 127 - console.log("Press Esc or Ctrl+C to exit at any time with the labels defined so far."); 128 - const { 129 - identifier, 130 - name, 131 - description, 132 - adultOnly, 133 - severity, 134 - blurs, 135 - defaultSetting, 136 - addAnother: _addAnother, 137 - } = await prompt([{ 172 + async function promptLabelDefinition( 173 + existing?: Array<string>, 174 + ): Promise<LabelValueDefinition | null> { 175 + let canceled = false; 176 + const { identifier, name, description, adultOnly, severity, blurs, defaultSetting } = 177 + await prompt([{ 138 178 type: "text", 139 179 name: "identifier", 140 180 message: "Identifier (non-user-facing, must be unique, 100 characters max):", 141 181 validate: (value) => { 142 182 if (!value) return "Required."; 143 183 if (value.length > 100) return "Must be <= 100 characters."; 144 - if (definitions.some((d) => d.identifier === value)) return "Must be unique."; 184 + if (existing?.some((id) => id === value)) return "Must be unique."; 145 185 if (/[^a-z-]/.test(value)) return "Must be lowercase letters and hyphens only."; 146 186 return true; 147 187 }, ··· 192 232 }, 193 233 { title: "Hide", description: "(hide labeled content from feed)", value: "hide" }, 194 234 ], 195 - }, { 196 - type: "confirm", 197 - name: "addAnother", 198 - message: "Add another label definition?", 199 - initial: true, 200 - }]); 201 - addAnother = _addAnother; 235 + }], { 236 + onCancel: () => { 237 + canceled = true; 238 + }, 239 + }); 202 240 203 - definitions.push({ 241 + return canceled 242 + ? null 243 + : { 204 244 identifier, 205 245 adultOnly, 206 246 severity, 207 247 blurs, 208 248 defaultSetting, 209 249 locales: [{ lang: "en", name, description }], 210 - }); 250 + }; 251 + } 252 + 253 + async function promptLabelDefinitions() { 254 + const definitions: Array<ComAtprotoLabelDefs.LabelValueDefinition> = []; 255 + let addAnother = true; 256 + while (addAnother) { 257 + console.log("Enter the details for the next label you would like this labeler to apply."); 258 + console.log("Press Esc or Ctrl+C to exit at any time with the labels defined so far."); 259 + 260 + const definition = await promptLabelDefinition(definitions.map((d) => d.identifier)); 261 + if (!definition) break; 262 + definitions.push(definition); 263 + 264 + ({ addAnother } = await prompt({ 265 + type: "confirm", 266 + name: "addAnother", 267 + message: "Add another label definition?", 268 + initial: true, 269 + })); 211 270 } 212 271 213 272 return definitions;
+39 -11
src/scripts/declareLabeler.ts
··· 1 - import { AtpAgent, type ComAtprotoLabelDefs } from "@atproto/api"; 1 + import { type AppBskyLabelerService, AtpAgent, type ComAtprotoLabelDefs } from "@atproto/api"; 2 2 import { loginAgent, LoginCredentials } from "./util.js"; 3 3 4 4 /** ··· 16 16 const labelValues = labelDefinitions.map(({ identifier }) => identifier); 17 17 18 18 const existing = await getLabelerLabelDefinitions(credentials); 19 - if (existing.length && !overwriteExisting) { 19 + if (existing?.length && !overwriteExisting) { 20 20 if (overwriteExisting === false) return; 21 - throw new Error( 22 - "Label definitions already exist. Use `overwriteExisting: true` to update them, or `overwriteExisting: false` to silence this error.", 23 - ); 21 + else if (overwriteExisting === undefined) { 22 + throw new Error( 23 + "Label definitions already exist. Use `overwriteExisting: true` to update them, or `overwriteExisting: false` to silence this error.", 24 + ); 25 + } 24 26 } 25 27 26 - await agent.app.bsky.labeler.service.create({ repo: agent.accountDid }, { 27 - policies: { labelValues, labelValueDefinitions: labelDefinitions }, 28 - createdAt: new Date().toISOString(), 29 - }); 28 + // We check if existing is truthy because an empty array means the record exists, but contains no definitions. 29 + if (existing) { 30 + await agent.com.atproto.repo.putRecord({ 31 + collection: "app.bsky.labeler.service", 32 + rkey: "self", 33 + repo: agent.accountDid, 34 + record: { 35 + $type: "app.bsky.labeler.service", 36 + policies: { labelValues, labelValueDefinitions: labelDefinitions }, 37 + createdAt: new Date().toISOString(), 38 + } satisfies AppBskyLabelerService.Record, 39 + }); 40 + } else { 41 + await agent.app.bsky.labeler.service.create({ repo: agent.accountDid }, { 42 + policies: { labelValues, labelValueDefinitions: labelDefinitions }, 43 + createdAt: new Date().toISOString(), 44 + }); 45 + } 30 46 } 31 47 32 48 /** ··· 36 52 */ 37 53 export async function getLabelerLabelDefinitions( 38 54 agentOrCredentials: AtpAgent | LoginCredentials, 39 - ): Promise<Array<ComAtprotoLabelDefs.LabelValueDefinition>> { 55 + ): Promise<Array<ComAtprotoLabelDefs.LabelValueDefinition> | null> { 40 56 const agent = agentOrCredentials instanceof AtpAgent 41 57 ? agentOrCredentials 42 58 : await loginAgent(agentOrCredentials); ··· 44 60 rkey: "self", 45 61 repo: agent.accountDid, 46 62 }).catch(() => ({ value: { policies: null } })); 47 - return policies?.labelValueDefinitions ?? []; 63 + return policies?.labelValueDefinitions ?? null; 64 + } 65 + 66 + /** 67 + * Set the label definitions for this labeler account. 68 + * @param credentials The credentials of the labeler account. 69 + * @param labelDefinitions The label definitions to set. 70 + */ 71 + export async function setLabelerLabelDefinitions( 72 + credentials: LoginCredentials, 73 + labelDefinitions: Array<ComAtprotoLabelDefs.LabelValueDefinition>, 74 + ) { 75 + return declareLabeler(credentials, labelDefinitions, true); 48 76 } 49 77 50 78 /**