Restructure chat service, web routes, and add test suite
Split monolithic chat service into modular structure with Matrix client support, extract web server into per-resource route modules, introduce bootstrap/main-loop separation, and add comprehensive vitest coverage across services, repositories, and utilities. Adds project docs and CLAUDE.md agent instructions.
This commit is contained in:
1 parent
dfa0580859
commit
633bb0ec04
84 files changed
+19278
-3888
No files matched your search
@@ -57,12 +57,23 @@ export function runMigrations(db: Database.Database): void {
|
||||
created_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
timestamp INTEGER NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
message TEXT NOT NULL,
|
||||
recipient TEXT,
|
||||
image_url TEXT
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_subreddit
|
||||
ON processed_posts(subreddit);
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_processed_at
|
||||
ON processed_posts(processed_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_posts_message_sent
|
||||
ON processed_posts(message_sent);
|
||||
CREATE INDEX IF NOT EXISTS idx_activity_log_timestamp
|
||||
ON activity_log(timestamp);
|
||||
`);
|
||||
|
||||
// Add new columns to existing tables (safe to run multiple times)
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface SentMessageRecord {
|
||||
subreddit?: string;
|
||||
postTitle?: string;
|
||||
postImageUrl?: string;
|
||||
postImageAnalysis?: string;
|
||||
}
|
||||
|
||||
export interface SuccessfulMessageRecord {
|
||||
@@ -60,6 +61,80 @@ export interface DailyTrend {
|
||||
responseRate: number;
|
||||
}
|
||||
|
||||
export interface ActivityLogRecord {
|
||||
timestamp: number;
|
||||
type: string;
|
||||
message: string;
|
||||
recipient: string | null;
|
||||
imageUrl: string | null;
|
||||
}
|
||||
|
||||
export class ActivityLogRepository {
|
||||
private insertStmt: Database.Statement;
|
||||
private getRecentStmt: Database.Statement;
|
||||
private pruneStmt: Database.Statement;
|
||||
private countSentSkippedStmt: Database.Statement;
|
||||
|
||||
constructor(private db: Database.Database) {
|
||||
this.insertStmt = db.prepare(`
|
||||
INSERT INTO activity_log (timestamp, type, message, recipient, image_url)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
this.getRecentStmt = db.prepare(`
|
||||
SELECT timestamp, type, message, recipient, image_url
|
||||
FROM activity_log
|
||||
ORDER BY id DESC
|
||||
LIMIT ?
|
||||
`);
|
||||
|
||||
this.pruneStmt = db.prepare(`
|
||||
DELETE FROM activity_log
|
||||
WHERE id NOT IN (SELECT id FROM activity_log ORDER BY id DESC LIMIT ?)
|
||||
`);
|
||||
|
||||
this.countSentSkippedStmt = db.prepare(`
|
||||
SELECT COUNT(*) AS count FROM activity_log WHERE type IN ('sent', 'skipped')
|
||||
`);
|
||||
}
|
||||
|
||||
insert(item: {
|
||||
timestamp: number;
|
||||
type: string;
|
||||
message: string;
|
||||
recipient?: string;
|
||||
imageUrl?: string;
|
||||
}): void {
|
||||
this.insertStmt.run(
|
||||
item.timestamp,
|
||||
item.type,
|
||||
item.message,
|
||||
item.recipient ?? null,
|
||||
item.imageUrl ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
getRecent(limit: number): ActivityLogRecord[] {
|
||||
const rows = this.getRecentStmt.all(limit) as Record<string, unknown>[];
|
||||
return rows.map((row) => ({
|
||||
timestamp: row.timestamp as number,
|
||||
type: row.type as string,
|
||||
message: row.message as string,
|
||||
recipient: (row.recipient as string) ?? null,
|
||||
imageUrl: (row.image_url as string) ?? null,
|
||||
}));
|
||||
}
|
||||
|
||||
prune(keep: number): void {
|
||||
this.pruneStmt.run(keep);
|
||||
}
|
||||
|
||||
countSentSkipped(): number {
|
||||
const row = this.countSentSkippedStmt.get() as { count: number };
|
||||
return row.count;
|
||||
}
|
||||
}
|
||||
|
||||
export class PostsRepository {
|
||||
private insertStmt: Database.Statement;
|
||||
private existsStmt: Database.Statement;
|
||||
@@ -166,6 +241,7 @@ export class PostsRepository {
|
||||
export class MessagesRepository {
|
||||
private insertStmt: Database.Statement;
|
||||
private getByPostIdStmt: Database.Statement;
|
||||
private getLatestByRecipientStmt: Database.Statement;
|
||||
private getRecentStmt: Database.Statement;
|
||||
private getUncheckedStmt: Database.Statement;
|
||||
private markResponseStmt: Database.Statement;
|
||||
@@ -180,8 +256,19 @@ export class MessagesRepository {
|
||||
|
||||
this.getByPostIdStmt = db.prepare(`SELECT * FROM sent_messages WHERE post_id = ?`);
|
||||
|
||||
this.getLatestByRecipientStmt = db.prepare(`
|
||||
SELECT sm.*, pp.subreddit, pp.title AS post_title, pp.image_url AS post_image_url,
|
||||
pp.image_analysis AS post_image_analysis
|
||||
FROM sent_messages sm
|
||||
LEFT JOIN processed_posts pp ON sm.post_id = pp.id
|
||||
WHERE sm.recipient = ? COLLATE NOCASE
|
||||
ORDER BY sm.sent_at DESC
|
||||
LIMIT 1
|
||||
`);
|
||||
|
||||
this.getRecentStmt = db.prepare(`
|
||||
SELECT sm.*, pp.subreddit, pp.title AS post_title, pp.image_url AS post_image_url
|
||||
SELECT sm.*, pp.subreddit, pp.title AS post_title, pp.image_url AS post_image_url,
|
||||
pp.image_analysis AS post_image_analysis
|
||||
FROM sent_messages sm
|
||||
LEFT JOIN processed_posts pp ON sm.post_id = pp.id
|
||||
ORDER BY sm.sent_at DESC
|
||||
@@ -235,6 +322,12 @@ export class MessagesRepository {
|
||||
return this.mapRow(row);
|
||||
}
|
||||
|
||||
getLatestByRecipient(recipient: string): SentMessageRecord | undefined {
|
||||
const row = this.getLatestByRecipientStmt.get(recipient) as Record<string, unknown> | undefined;
|
||||
if (!row) return undefined;
|
||||
return this.mapRow(row);
|
||||
}
|
||||
|
||||
getRecent(limit: number = 10): SentMessageRecord[] {
|
||||
const rows = this.getRecentStmt.all(limit) as Record<string, unknown>[];
|
||||
return rows.map((row) => this.mapRow(row));
|
||||
@@ -340,6 +433,7 @@ export class MessagesRepository {
|
||||
subreddit: (row.subreddit as string) || undefined,
|
||||
postTitle: (row.post_title as string) || undefined,
|
||||
postImageUrl: (row.post_image_url as string) || undefined,
|
||||
postImageAnalysis: (row.post_image_analysis as string) || undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -358,7 +452,7 @@ export class SuccessfulMessagesRepository {
|
||||
`);
|
||||
|
||||
this.getRecentStmt = db.prepare(`
|
||||
SELECT * FROM successful_messages ORDER BY created_at DESC LIMIT ?
|
||||
SELECT * FROM successful_messages ORDER BY created_at DESC, id DESC LIMIT ?
|
||||
`);
|
||||
|
||||
// Get examples prioritizing same subreddit, then falling back to others
|
||||
|
||||
Reference in new issue
Block a user