Add the media MCP tools and live-verified slopbot path
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user