#!/usr/bin/env bun // Appview moderation CLI — hides content from our reads without touching any PDS. // Runs against the DB at DB_PATH (prod: /var/lib/lichen/lichen.db). See USAGE below. import { resolveHandleToDid } from "../src/lib/profile.ts"; import { banDid, getWiki, hideWiki, listModeration, unbanDid, unhideWiki, } from "../src/server/db/queries/index.ts"; const USAGE = `Usage: bun run moderate hide <@handle/slug | did:…/slug> [--reason=…] [--by=…] bun run moderate hide <@handle | did:…> --user [--reason=…] [--by=…] bun run moderate unhide <@handle/slug | did:…/slug> bun run moderate unhide <@handle | did:…> --user bun run moderate list`; interface ParsedArgs { command: "hide" | "unhide" | "list"; target?: string; user: boolean; reason: string | null; by: string; } function parseArgs(argv: string[]): ParsedArgs { const command = argv[0]; if (command !== "hide" && command !== "unhide" && command !== "list") { throw new Error(`unknown command: ${command ?? "(none)"}`); } let target: string | undefined; let user = false; let reason: string | null = null; let by = process.env["USER"] ?? "cli"; for (let i = 1; i < argv.length; i++) { const arg = argv[i] ?? ""; if (arg === "--user") { user = true; } else if (arg.startsWith("--reason=")) { reason = arg.slice("--reason=".length); } else if (arg === "--reason") { reason = argv[++i] ?? null; } else if (arg.startsWith("--by=")) { by = arg.slice("--by=".length); } else if (arg === "--by") { by = argv[++i] ?? by; } else if (arg.startsWith("--")) { throw new Error(`unknown flag: ${arg}`); } else if (target === undefined) { target = arg; } else { throw new Error(`unexpected argument: ${arg}`); } } return { command, ...(target ? { target } : {}), user, reason, by }; } // "@handle/slug", "did:…/slug", "@handle", or a raw "did:…" → owner + optional slug. function splitTarget(target: string): { owner: string; slug?: string } { const slash = target.indexOf("/"); if (slash === -1) return { owner: target }; return { owner: target.slice(0, slash), slug: target.slice(slash + 1) }; } async function resolveDid(owner: string): Promise { const did = await resolveHandleToDid(owner); if (!did) throw new Error(`could not resolve "${owner}" to a DID`); return did; } async function run(args: ParsedArgs): Promise { if (args.command === "list") { const rows = listModeration(); if (rows.length === 0) { console.log("No moderation actions."); return; } for (const r of rows) { const meta = [ r.moderator && `by ${r.moderator}`, r.reason && `"${r.reason}"`, ] .filter(Boolean) .join(" "); console.log( `${r.created_at} ${r.subject_type.padEnd(4)} ${r.subject}${meta ? ` ${meta}` : ""}`, ); } return; } if (!args.target) throw new Error(`${args.command} requires a target`); const { owner, slug } = splitTarget(args.target); const did = await resolveDid(owner); if (args.user) { if (args.command === "hide") { banDid(did, args.reason, args.by); console.log( `Banned DID ${did} (all wikis hidden, future writes dropped).`, ); } else { unbanDid(did); console.log(`Lifted DID ban for ${did}.`); } return; } if (!slug) { throw new Error( `${args.command} needs a wiki slug (got "${args.target}"). Use --user to act on the whole account.`, ); } // Construct the at_uri when absent so a not-yet-seen wiki can be pre-hidden (rkey = slug). const atUri = getWiki(did, slug)?.at_uri ?? `at://${did}/wiki.lichen.wiki/${slug}`; if (args.command === "hide") { hideWiki(atUri, args.reason, args.by); console.log(`Hidden wiki ${atUri}.`); } else { unhideWiki(atUri); console.log(`Un-hidden wiki ${atUri}.`); } } async function main(): Promise { let args: ParsedArgs; try { args = parseArgs(process.argv.slice(2)); } catch (err) { console.error(`${String(err)}\n\n${USAGE}`); return 2; } try { await run(args); return 0; } catch (err) { console.error(String(err)); return 1; } } process.exit(await main());