Wire the media archive into the daemon: command, service, and API

This commit is contained in:
Fisto
2026-09-16 08:08:12 +02:00
parent 91782f0b80
commit 8b7b5b675d
13 changed files with 421 additions and 18 deletions
+98 -1
View File
@@ -110,6 +110,24 @@ export interface PuppetAudio {
stop(): void;
}
/** The scrape state a chat command reads. */
export interface PuppetScrape {
id: string;
channelId: string;
state: string;
}
/** The media actions a chat command can trigger. */
export interface PuppetMedia {
startScrape(input: {
channelId: string;
guildId: string | null;
mode: "links" | "download";
}): Promise<{ id: string }>;
stopScrape(id: string): void;
listScrapes(): PuppetScrape[];
}
interface PollState {
channelId: string;
authorId: string;
@@ -130,6 +148,15 @@ const WHITELIST_HELP = [
" whitelist remove @user Remove a user"
].join("\n");
const SCRAPE_HELP = [
"scrape media — archive this channel's media.",
"Usage:",
" scrape media Index links (default)",
" scrape media download Also download attachment bytes (admin)",
" scrape media status Show this channel's latest scrape",
" scrape media stop Stop it (admin)"
].join("\n");
function clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
@@ -164,7 +191,8 @@ export class Puppet {
constructor(
private readonly core: PuppetTransport,
private readonly audio?: PuppetAudio
private readonly audio?: PuppetAudio,
private readonly media?: PuppetMedia
) {
try {
const state = JSON.parse(readFileSync(STATE_FILE, "utf8")) as PersistedState;
@@ -531,6 +559,8 @@ export class Puppet {
await this.handleWhitelistCommand(command, author.id, tier, channelId);
} else if (command?.name === "speak") {
await this.handleSpeakCommand(command, author.id, channelId);
} else if (command?.name === "scrape") {
await this.handleScrapeCommand(command, author.id, channelId, guildId, tier);
} else if (POLL_ENABLED && command?.name === "poll") {
await this.handlePollCommand(author.id, channelId, text);
}
@@ -671,6 +701,73 @@ export class Puppet {
}
}
private async handleScrapeCommand(
command: ParsedCommand,
authorId: string,
channelId: string,
guildId: string | undefined,
tier: TrustTier
) {
const [kind, sub] = command.args;
const knownSub = sub === undefined || sub === "download" || sub === "status" || sub === "stop";
if (kind !== "media" || sub === "help" || !knownSub) {
return this.respond(authorId, channelId, "scrape", SCRAPE_HELP);
}
if (!this.media) {
return this.respond(authorId, channelId, "scrape", "The media archive is not available.");
}
if (sub === "status") {
const scrape = this.media.listScrapes().find(entry => entry.channelId === channelId);
return this.respond(
authorId,
channelId,
"scrape",
scrape
? `Media scrape ${scrape.id}: ${scrape.state}.`
: "No media scrape for this channel yet."
);
}
if (sub === "stop") {
if (!hasTier(tier, "admin")) {
return this.respond(authorId, channelId, "scrape", "You do not have permission for this command.");
}
const scrape = this.media
.listScrapes()
.find(entry => entry.channelId === channelId && ["queued", "running", "paused"].includes(entry.state));
if (!scrape) {
return this.respond(authorId, channelId, "scrape", "No active media scrape for this channel.");
}
this.media.stopScrape(scrape.id);
this.actionLog.append({ type: "media", action: "stop", channelId, scrapeId: scrape.id });
return this.respond(authorId, channelId, "scrape", `Stopping media scrape ${scrape.id}.`);
}
const mode = sub === "download" ? "download" : "links";
if (mode === "download" && !hasTier(tier, "admin")) {
return this.respond(authorId, channelId, "scrape", "Download mode needs the admin tier.");
}
try {
const scrape = await this.media.startScrape({ channelId, guildId: guildId ?? null, mode });
this.actionLog.append({ type: "media", action: "scrape", channelId, mode, scrapeId: scrape.id });
return this.respond(authorId, channelId, "scrape", `Media scrape started (${mode}): ${scrape.id}.`);
} catch (error) {
this.actionLog.append({ type: "error", scope: "media", channelId, message: String(error) });
return this.respond(
authorId,
channelId,
"scrape",
`Media scrape failed: ${error instanceof Error ? error.message : String(error)}`
);
}
}
/* Polls */
private async handlePollCommand(authorId: string, channelId: string, text: string) {