Wire the media archive into the daemon: command, service, and API
This commit is contained in:
+98
-1
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user