- Sprint 1 (Quick Wins): Remove dead analytics modules, fix 4 unresolved imports, replace 3 console.warn with logger, remove mock-crc import - Sprint 2 (Architecture): Create MascotChatRepository, AnalysisRepository, 3 Zod schemas (mascot-chat, analysis, voice), deduplicate error classes, move 3 SQL queries from routes to repository - Sprint 3 (Complexity): Replace 7 any types with proper interfaces, extract 6 helpers from prepareMediaMessage (CC 85 -> ~15) - Sprint 4 (Config): Remove 22 dead env vars from .env, add 30 missing vars to .env.example, standardize naming Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
181 lines
4.5 KiB
TypeScript
181 lines
4.5 KiB
TypeScript
import { createChildLogger } from "@bete/shared/logger";
|
|
import { getPool } from "../../shared/database/index.js";
|
|
|
|
const logger = createChildLogger("mascot-chat.repository");
|
|
|
|
export interface MascotChatContext {
|
|
messageCount?: number;
|
|
activeParticipants?: number;
|
|
lastActivity?: string;
|
|
topicsDiscussed?: string[];
|
|
guildId?: string;
|
|
channelId?: string;
|
|
}
|
|
|
|
export interface SaveConversationInput {
|
|
userId: string;
|
|
userMessage: string;
|
|
mascotResponse: string;
|
|
context?: MascotChatContext;
|
|
timestamp: Date;
|
|
}
|
|
|
|
export interface MascotChatHistoryRow {
|
|
id: string;
|
|
user_id: string;
|
|
user_message: string;
|
|
mascot_response: string;
|
|
context: MascotChatContext | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ServerInsights {
|
|
total_messages: number;
|
|
active_users: number;
|
|
flagged: number;
|
|
warned: number;
|
|
}
|
|
|
|
export class MascotChatRepository {
|
|
private initialized = false;
|
|
|
|
async ensureSchema(): Promise<void> {
|
|
if (this.initialized) return;
|
|
|
|
const pool = getPool();
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS mascot_chat_messages (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
user_id TEXT NOT NULL,
|
|
user_message TEXT NOT NULL,
|
|
mascot_response TEXT NOT NULL,
|
|
context JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
)
|
|
`);
|
|
await pool.query(`
|
|
CREATE INDEX IF NOT EXISTS idx_mascot_chat_messages_user_created
|
|
ON mascot_chat_messages (user_id, created_at DESC)
|
|
`);
|
|
|
|
this.initialized = true;
|
|
logger.info("Mascot chat schema ready");
|
|
}
|
|
|
|
async saveConversation(input: SaveConversationInput): Promise<void> {
|
|
await this.ensureSchema();
|
|
const pool = getPool();
|
|
|
|
await pool.query(
|
|
`
|
|
INSERT INTO mascot_chat_messages
|
|
(user_id, user_message, mascot_response, context, created_at)
|
|
VALUES ($1, $2, $3, $4::jsonb, $5)
|
|
`,
|
|
[
|
|
input.userId,
|
|
input.userMessage,
|
|
input.mascotResponse,
|
|
JSON.stringify(input.context ?? {}),
|
|
input.timestamp.toISOString(),
|
|
],
|
|
);
|
|
|
|
logger.debug({ userId: input.userId }, "Conversation saved");
|
|
}
|
|
|
|
async getChatHistory(
|
|
userId: string,
|
|
limit: number,
|
|
): Promise<MascotChatHistoryRow[]> {
|
|
await this.ensureSchema();
|
|
const pool = getPool();
|
|
|
|
const { rows } = await pool.query<MascotChatHistoryRow>(
|
|
`
|
|
SELECT id, user_id, user_message, mascot_response, context, created_at
|
|
FROM mascot_chat_messages
|
|
WHERE user_id = $1
|
|
ORDER BY created_at DESC
|
|
LIMIT $2
|
|
`,
|
|
[userId, limit],
|
|
);
|
|
|
|
logger.debug({ userId, count: rows.length }, "Chat history fetched");
|
|
return rows.reverse();
|
|
}
|
|
|
|
async clearChatHistory(userId: string): Promise<void> {
|
|
await this.ensureSchema();
|
|
const pool = getPool();
|
|
|
|
const { rowCount } = await pool.query(
|
|
`DELETE FROM mascot_chat_messages WHERE user_id = $1`,
|
|
[userId],
|
|
);
|
|
|
|
logger.info({ userId, deletedRows: rowCount ?? 0 }, "Chat history cleared");
|
|
}
|
|
|
|
async getServerInsights(
|
|
guildId?: string,
|
|
channelId?: string,
|
|
): Promise<ServerInsights> {
|
|
await this.ensureSchema();
|
|
const pool = getPool();
|
|
|
|
try {
|
|
const params: string[] = [];
|
|
const clauses: string[] = [];
|
|
|
|
if (guildId) {
|
|
params.push(guildId);
|
|
clauses.push(`guild_id = $${params.length}`);
|
|
}
|
|
if (channelId) {
|
|
params.push(channelId);
|
|
clauses.push(`channel_id = $${params.length}`);
|
|
}
|
|
|
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
|
|
const { rows } = await pool.query<ServerInsights>(
|
|
`
|
|
SELECT
|
|
COUNT(*)::int AS total_messages,
|
|
COUNT(DISTINCT user_id)::int AS active_users,
|
|
COUNT(*) FILTER (WHERE ai_status = 'flagged')::int AS flagged,
|
|
COUNT(*) FILTER (WHERE ai_status = 'warn')::int AS warned
|
|
FROM messages
|
|
${where}
|
|
`,
|
|
params,
|
|
);
|
|
|
|
const insights = rows[0] ?? {
|
|
total_messages: 0,
|
|
active_users: 0,
|
|
flagged: 0,
|
|
warned: 0,
|
|
};
|
|
|
|
logger.debug({ guildId, channelId, insights }, "Server insights fetched");
|
|
return insights;
|
|
} catch (error) {
|
|
logger.warn(
|
|
{ error, guildId, channelId },
|
|
"Failed to load server insights",
|
|
);
|
|
return {
|
|
total_messages: 0,
|
|
active_users: 0,
|
|
flagged: 0,
|
|
warned: 0,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
export const mascotChatRepository = new MascotChatRepository();
|