Add the media MCP tools and live-verified slopbot path

This commit is contained in:
Fisto
2026-09-16 09:54:22 +02:00
parent c81ae0d0fe
commit d483a2fa47
9 changed files with 179 additions and 17 deletions
+10 -1
View File
@@ -3,7 +3,7 @@
* Setup and security: docs/slopbot.md.
*/
import { Client, GatewayIntentBits } from "discord.js";
import { Client, Events, GatewayIntentBits } from "discord.js";
import type { SlopbotConfig } from "./config.js";
import type { SlopbotFetcher } from "./backend.js";
@@ -23,7 +23,16 @@ export async function startSlopbot(config: SlopbotConfig): Promise<Client> {
}
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
// login() resolves before the READY dispatch, so wait for it: the guild cache
// and client.user are only populated then.
const ready = new Promise<void>((resolve, reject) => {
client.once(Events.ClientReady, () => resolve());
client.once(Events.Error, reject);
});
await client.login(config.token);
await ready;
return client;
}
+24 -3
View File
@@ -135,6 +135,8 @@ CREATE TABLE IF NOT EXISTS media (
preview_url TEXT,
file_hash TEXT,
preview_hash TEXT,
file_bytes INTEGER,
preview_bytes INTEGER,
created_at TEXT NOT NULL
);
@@ -155,6 +157,16 @@ export class ScrapeStore {
constructor(readonly dir: string) {
this.db = new DatabaseSync(join(dir, "index.db"));
this.db.exec(SCHEMA);
this.migrate();
}
/** Add columns introduced after a scrape was created. */
private migrate(): void {
const columns = this.db.prepare("PRAGMA table_info(media)").all() as unknown as Array<{ name: string }>;
const names = new Set(columns.map(column => column.name));
if (!names.has("file_bytes")) this.db.exec("ALTER TABLE media ADD COLUMN file_bytes INTEGER");
if (!names.has("preview_bytes")) this.db.exec("ALTER TABLE media ADD COLUMN preview_bytes INTEGER");
}
close(): void {
@@ -216,10 +228,12 @@ export class ScrapeStore {
return added;
}
/** Sum of the recorded source sizes per kind. Used by the storage bar. */
sumBytesByKind(): Record<MediaKind, number> {
/** Bytes actually on disk per kind (downloaded files and cached previews). */
storedBytesByKind(): Record<MediaKind, number> {
const rows = this.db
.prepare("SELECT kind, COALESCE(SUM(size), 0) AS bytes FROM media GROUP BY kind")
.prepare(
"SELECT kind, COALESCE(SUM(COALESCE(file_bytes, 0) + COALESCE(preview_bytes, 0)), 0) AS bytes FROM media GROUP BY kind"
)
.all() as unknown as Array<{ kind: string; bytes: number }>;
const result = emptyMediaCounts();
@@ -241,6 +255,13 @@ export class ScrapeStore {
return result;
}
getMedia(id: number): MediaRow | null {
const row = this.db.prepare("SELECT * FROM media WHERE id = ?").get(id) as unknown as
| MediaRow
| undefined;
return row ?? null;
}
queryMedia(options: { kind?: MediaKind; limit?: number; offset?: number } = {}): MediaRow[] {
const limit = Math.max(1, Math.min(options.limit ?? 50, 500));
const offset = Math.max(0, options.offset ?? 0);
+1 -1
View File
@@ -205,7 +205,7 @@ void (async () => {
assert.equal(stored.counts().image, 1);
assert.equal(stored.queryMedia({ kind: "image" }).length, 1);
assert.equal(stored.queryMedia({ kind: "video" }).length, 0);
assert.equal(stored.sumBytesByKind().image, 10);
assert.equal(stored.storedBytesByKind().image, 0);
stored.close();
assert.equal(listScrapes(root).length, 1);