Persist comment source monitoring
This commit is contained in:
1 parent
5e1fc5def9
commit
8d932fcf36
16 files changed
+376
-24
No files matched your search
@@ -0,0 +1,110 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import type Database from 'better-sqlite3';
|
||||
import { z } from 'zod';
|
||||
import type { CommentReactionVerdict } from '../ai/openrouter.js';
|
||||
import type { BrowsingThing, RedditPost } from '../reddit/api.js';
|
||||
|
||||
const StoredVerdictSchema = z.object({
|
||||
id: z.string(),
|
||||
qualifies: z.boolean(),
|
||||
expressedGender: z.enum(['male', 'female', 'other', 'unknown']),
|
||||
confidence: z.number().min(0).max(1),
|
||||
evidence: z.string(),
|
||||
});
|
||||
|
||||
export interface CommentWatchRefreshResult {
|
||||
added: number;
|
||||
active: number;
|
||||
expired: number;
|
||||
}
|
||||
|
||||
/** Durable source-post rotation and prompt-free comment classification cache. */
|
||||
export class CommentSourceWatchRepository {
|
||||
constructor(private readonly db: Database.Database) {}
|
||||
|
||||
refreshSources(posts: readonly RedditPost[], watchWindowMs: number, now = Date.now()): CommentWatchRefreshResult {
|
||||
const expired = this.db.prepare('DELETE FROM comment_source_watches WHERE expires_at <= ?').run(now).changes;
|
||||
const existing = this.db.prepare('SELECT 1 FROM comment_source_watches WHERE source_post_id = ?');
|
||||
const upsert = this.db.prepare(`
|
||||
INSERT INTO comment_source_watches (
|
||||
source_post_id, subreddit, payload_json, post_created_at,
|
||||
discovered_at, updated_at, expires_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(source_post_id) DO UPDATE SET
|
||||
subreddit = excluded.subreddit,
|
||||
payload_json = excluded.payload_json,
|
||||
updated_at = excluded.updated_at,
|
||||
expires_at = MAX(comment_source_watches.expires_at, excluded.expires_at)
|
||||
`);
|
||||
let added = 0;
|
||||
this.db.transaction(() => {
|
||||
for (const post of posts) {
|
||||
const createdAt = Math.round(post.created_utc * 1_000);
|
||||
const expiresAt = createdAt + watchWindowMs;
|
||||
if (expiresAt <= now) continue;
|
||||
if (!existing.get(post.id)) added += 1;
|
||||
upsert.run(post.id, post.subreddit, JSON.stringify(post), createdAt, now, now, expiresAt);
|
||||
}
|
||||
})();
|
||||
const active = Number((this.db.prepare(`
|
||||
SELECT COUNT(*) AS count FROM comment_source_watches WHERE expires_at > ?
|
||||
`).get(now) as { count: number }).count);
|
||||
return { added, active, expired };
|
||||
}
|
||||
|
||||
nextSource(subreddit: string, now = Date.now()): RedditPost | null {
|
||||
const row = this.db.prepare(`
|
||||
SELECT payload_json FROM comment_source_watches
|
||||
WHERE subreddit = ? COLLATE NOCASE AND expires_at > ?
|
||||
ORDER BY COALESCE(last_checked_at, 0) ASC, post_created_at DESC
|
||||
LIMIT 1
|
||||
`).get(subreddit, now) as { payload_json: string } | undefined;
|
||||
if (!row) return null;
|
||||
return JSON.parse(row.payload_json) as RedditPost;
|
||||
}
|
||||
|
||||
markChecked(sourcePostId: string, now = Date.now()): void {
|
||||
this.db.prepare(`
|
||||
UPDATE comment_source_watches SET last_checked_at = ?, updated_at = ?
|
||||
WHERE source_post_id = ?
|
||||
`).run(now, now, sourcePostId);
|
||||
}
|
||||
|
||||
getVerdict(comment: Pick<BrowsingThing, 'fullname' | 'text'>): CommentReactionVerdict | null {
|
||||
const row = this.db.prepare(`
|
||||
SELECT body_sha256, verdict_json FROM comment_source_comment_observations
|
||||
WHERE comment_id = ?
|
||||
`).get(comment.fullname) as { body_sha256: string; verdict_json: string } | undefined;
|
||||
if (!row || row.body_sha256 !== hashBody(comment.text)) return null;
|
||||
try {
|
||||
const parsed = StoredVerdictSchema.safeParse(JSON.parse(row.verdict_json));
|
||||
return parsed.success ? parsed.data : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
recordVerdict(comment: BrowsingThing, verdict: CommentReactionVerdict, now = Date.now()): void {
|
||||
this.db.prepare(`
|
||||
INSERT INTO comment_source_comment_observations (
|
||||
comment_id, source_post_id, body_sha256, verdict_json, first_seen_at, updated_at
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(comment_id) DO UPDATE SET
|
||||
source_post_id = excluded.source_post_id,
|
||||
body_sha256 = excluded.body_sha256,
|
||||
verdict_json = excluded.verdict_json,
|
||||
updated_at = excluded.updated_at
|
||||
`).run(
|
||||
comment.fullname,
|
||||
comment.parentPostId,
|
||||
hashBody(comment.text),
|
||||
JSON.stringify({ ...verdict, evidence: '' }),
|
||||
now,
|
||||
now,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function hashBody(body: string): string {
|
||||
return createHash('sha256').update(body).digest('hex');
|
||||
}
|
||||
Reference in new issue
Block a user