Persist t2→username mappings in reddit_users table

Adds a durable mapping so the UI never has to show raw t2_xxx IDs or
MXIDs just because the in-memory cache is cold after a restart.
ChatService seeds from the table on construction and writes through
every resolution path; cached chats whose username is still t2_xxx
get upgraded from the DB on loadCache.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.7 committed 2026-05-22 20:34:06 +01:00
1 parent 160172403b
commit 1b6dfd67e3
7 files changed
+407 -18

No files matched your search

+9
View File
@@ -66,6 +66,15 @@ export function runMigrations(db: Database.Database): void {
image_url TEXT
);
CREATE TABLE IF NOT EXISTS reddit_users (
t2_id TEXT PRIMARY KEY,
username TEXT NOT NULL,
mxid TEXT,
updated_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_reddit_users_username
ON reddit_users(username);
CREATE INDEX IF NOT EXISTS idx_posts_subreddit
ON processed_posts(subreddit);
CREATE INDEX IF NOT EXISTS idx_posts_processed_at
+80
View File
@@ -566,3 +566,83 @@ export class SuccessfulMessagesRepository {
};
}
}
export interface RedditUserRecord {
t2Id: string;
username: string;
mxid: string | null;
updatedAt: number;
}
/**
* Durable mapping of Reddit account IDs (`t2_xxx`) to current usernames.
* Populated as the chat sync resolves IDs via Reddit's alfred/user_data
* endpoints; read back on startup so we never have to show raw `t2_xxx`
* or MXIDs in the UI just because the in-memory cache is cold.
*/
export class RedditUsersRepository {
private upsertStmt: Database.Statement;
private getByT2Stmt: Database.Statement;
private getByUsernameStmt: Database.Statement;
private getAllStmt: Database.Statement;
constructor(private db: Database.Database) {
this.upsertStmt = db.prepare(`
INSERT INTO reddit_users (t2_id, username, mxid, updated_at)
VALUES (@t2Id, @username, @mxid, @updatedAt)
ON CONFLICT(t2_id) DO UPDATE SET
username = excluded.username,
mxid = COALESCE(excluded.mxid, reddit_users.mxid),
updated_at = excluded.updated_at
WHERE excluded.username != ''
`);
this.getByT2Stmt = db.prepare(`SELECT * FROM reddit_users WHERE t2_id = ?`);
this.getByUsernameStmt = db.prepare(`SELECT * FROM reddit_users WHERE username = ? COLLATE NOCASE`);
this.getAllStmt = db.prepare(`SELECT * FROM reddit_users`);
}
upsert(record: { t2Id: string; username: string; mxid?: string | null }): void {
if (!record.t2Id || !record.username) return;
if (record.username.startsWith('t2_') || record.username.startsWith('@')) return;
const t2Id = record.t2Id.startsWith('t2_') ? record.t2Id : `t2_${record.t2Id}`;
this.upsertStmt.run({
t2Id,
username: record.username,
mxid: record.mxid ?? null,
updatedAt: Date.now(),
});
}
upsertMany(records: Array<{ t2Id: string; username: string; mxid?: string | null }>): void {
const tx = this.db.transaction((items: typeof records) => {
for (const r of items) this.upsert(r);
});
tx(records);
}
getByT2(t2Id: string): RedditUserRecord | null {
const normalized = t2Id.startsWith('t2_') ? t2Id : `t2_${t2Id}`;
const row = this.getByT2Stmt.get(normalized) as Record<string, unknown> | undefined;
return row ? this.mapRow(row) : null;
}
getByUsername(username: string): RedditUserRecord | null {
const row = this.getByUsernameStmt.get(username) as Record<string, unknown> | undefined;
return row ? this.mapRow(row) : null;
}
/** Bulk load — used to seed the in-memory cache on startup. */
getAll(): RedditUserRecord[] {
const rows = this.getAllStmt.all() as Record<string, unknown>[];
return rows.map((r) => this.mapRow(r));
}
private mapRow(row: Record<string, unknown>): RedditUserRecord {
return {
t2Id: row.t2_id as string,
username: row.username as string,
mxid: (row.mxid as string | null) ?? null,
updatedAt: row.updated_at as number,
};
}
}