scripts/replay/core/db.ts

import { Database } from "bun:sqlite"; import { normalizeSettings } from "../../../src/store/settingsNormalization.ts"; import type { ActionRow, MessageRow, ReplayBaseArgs } from "./types.ts"; import { parseJsonSafe } from "./utils.ts";

export function openReadOnlyDb(dbPath: string) { return new Database(dbPath, { readonly: true, create: false }); }

function queryRows<T extends Record<string, unknown>>( db: Database, sql: string, params: unknown[] = [] ): T[] { const stmt = db.prepare(sql); const rows = stmt.all(...params) as T[]; return Array.isArray(rows) ? rows : []; }

export function loadRuntimeSettings(db: Database) { const settingsRow = db .prepare("SELECT value FROM settings WHERE key = 'runtime_settings' LIMIT 1") .get() as { value?: string } | undefined; if (!settingsRow?.value) { throw new Error("runtime_settings not found in DB"); } return normalizeSettings(parseJsonSafe(String(settingsRow.value || "")) || {}); }

export function loadMessagesForReplay( db: Database, { contextSince, since, until, channelId, maxTurns }: { contextSince: string; since: string; until: string; channelId: string; maxTurns: number; } ) { const messages = queryRows( db, SELECT message_id, created_at, guild_id, channel_id, author_id, author_name, is_bot, content, referenced_message_id FROM messages WHERE created_at >= ? AND (? = '' OR created_at <= ?) AND (? = '' OR channel_id = ?) ORDER BY created_at ASC , [contextSince, until, until, channelId, channelId] );

const boundedMaxTurns = Math.max(0, Math.floor(maxTurns) || 0); const candidateMessages = messages.filter( (row) => Number(row.is_bot) !== 1 && String(row.created_at || "") >= since ); const replayMessages = boundedMaxTurns > 0 ? candidateMessages.slice(0, boundedMaxTurns) : candidateMessages;

return { messages, replayMessages }; }

export function resolveBotUserId(messages: MessageRow[]) { const botCounts = new Map<string, number>(); for (const row of messages) { const authorId = String(row.author_id || "").trim(); if (!authorId) continue; const current = botCounts.get(authorId) || 0; botCounts.set(authorId, current + (Number(row.is_bot) === 1 ? 1 : 0)); } const sortedBotCandidates = [...botCounts.entries()].sort((a, b) => b[1] - a[1]); const botUserId = sortedBotCandidates[0]?.[0] || ""; if (!botUserId) { throw new Error("could not resolve bot user id from messages table"); } return botUserId; }

export function loadInitiativeChannelIds(runtimeSettings: Record<string, unknown>) { const permissions = runtimeSettings.permissions as { replies?: { discoveryChannelIds?: unknown[] }; } | null; const discoveryList = permissions?.replies?.discoveryChannelIds; if (!Array.isArray(discoveryList)) return new Set(); return new Set(discoveryList.map((value) => String(value))); }

export function primeReplayHistory(messages: MessageRow[], since: string) { const historyByChannel = new Map<string, MessageRow[]>(); const historyByMessageId = new Map<string, MessageRow>();

for (const row of messages) { if (String(row.created_at || "") >= since) continue; const channelId = String(row.channel_id || ""); if (!channelId) continue; const history = historyByChannel.get(channelId) || []; history.push(row); historyByChannel.set(channelId, history); historyByMessageId.set(String(row.message_id), row); }

return { historyByChannel, historyByMessageId }; }

export function parseMetadataObject(row: ActionRow) { const raw = String(row.metadata || "").trim(); if (!raw) return {}; const parsed = parseJsonSafe(raw); if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {}; return parsed as Record<string, unknown>; }

export function queryRecordedActions( db: Database, args: ReplayBaseArgs, contextSince: string, kinds: string[], extraWhere = "", includeMessageId = false ) { const selectedColumns = includeMessageId ? "id, created_at, channel_id, kind, content, metadata, message_id" : "id, created_at, channel_id, kind, content, metadata";

const kindPlaceholders = kinds.map(() => "?").join(", "); return queryRows( db, SELECT ${selectedColumns} FROM actions WHERE created_at >= ? AND (? = '' OR created_at <= ?) AND (? = '' OR channel_id = ?) AND kind IN (${kindPlaceholders}) ${extraWhere} ORDER BY id ASC , [ contextSince, args.until, args.until, args.channelId, args.channelId, ...kinds ] ); }