Record sent chats in messages repo and blur gallery by default

Threads MessagesRepository through ChatService so first outgoing chat
messages are persisted for stats, and extends the repo + DB client to
support chat-room-keyed lookups. Adds tests covering the new behavior.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jjbruntonandClaude Opus 4.7 committed 2026-05-22 18:48:14 +01:00
1 parent 2260388dc8
commit 05c95db6bd
10 files changed
+197 -6

No files matched your search

+2
View File
@@ -206,6 +206,8 @@ export async function bootstrap(config: Config, logger: Logger): Promise<AppCont
mediaService,
tokenRefresher,
usernameResolver,
undefined,
messagesRepo,
);
const mediaSyncService = new MediaSyncService(chatService, logger, config.media.syncIntervalMs);
+37
View File
@@ -98,6 +98,43 @@ export function runMigrations(db: Database.Database): void {
// Add subreddit column to successful_messages for smarter example selection
addColumnIfNotExists('successful_messages', 'subreddit', 'TEXT');
// Track chats sent via chat.reddit.com so dashboard stats include them.
addColumnIfNotExists('sent_messages', 'chat_room_id', 'TEXT');
db.exec(`CREATE INDEX IF NOT EXISTS idx_sent_messages_chat_room ON sent_messages(chat_room_id)`);
// Migration: Make post_id nullable in sent_messages (chat-originated rows
// have no associated processed_post). SQLite can't ALTER COLUMN, so recreate.
const sentMsgColumns = db.prepare(`PRAGMA table_info(sent_messages)`).all() as { name: string; notnull: number }[];
const postIdCol = sentMsgColumns.find((c) => c.name === 'post_id');
if (postIdCol && postIdCol.notnull === 1) {
db.exec(`
CREATE TABLE sent_messages_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
post_id TEXT,
recipient TEXT NOT NULL,
subject TEXT NOT NULL,
message TEXT NOT NULL,
sent_at INTEGER NOT NULL,
got_response INTEGER DEFAULT 0,
response_checked_at INTEGER,
strategy_id TEXT,
chat_room_id TEXT,
FOREIGN KEY (post_id) REFERENCES processed_posts(id)
);
INSERT INTO sent_messages_new (id, post_id, recipient, subject, message, sent_at, got_response, response_checked_at, strategy_id, chat_room_id)
SELECT id, post_id, recipient, subject, message, sent_at, got_response, response_checked_at, strategy_id, chat_room_id
FROM sent_messages;
DROP TABLE sent_messages;
ALTER TABLE sent_messages_new RENAME TO sent_messages;
CREATE INDEX IF NOT EXISTS idx_sent_messages_response ON sent_messages(got_response);
CREATE INDEX IF NOT EXISTS idx_sent_messages_recipient ON sent_messages(recipient);
CREATE INDEX IF NOT EXISTS idx_sent_messages_chat_room ON sent_messages(chat_room_id);
`);
}
// Migration: Make sent_message_id nullable in successful_messages
// SQLite doesn't support ALTER COLUMN, so we need to recreate the table
const successfulMsgColumns = db.prepare(`PRAGMA table_info(successful_messages)`).all() as { name: string; notnull: number }[];
+43 -2
View File
@@ -15,7 +15,7 @@ export interface ProcessedPostRecord {
export interface SentMessageRecord {
id: number;
postId: string;
postId: string | null;
recipient: string;
subject: string;
message: string;
@@ -23,6 +23,7 @@ export interface SentMessageRecord {
gotResponse: boolean;
responseCheckedAt: number | null;
strategyId: string | null;
chatRoomId: string | null;
subreddit?: string;
postTitle?: string;
postImageUrl?: string;
@@ -240,7 +241,9 @@ export class PostsRepository {
export class MessagesRepository {
private insertStmt: Database.Statement;
private insertChatStmt: Database.Statement;
private getByPostIdStmt: Database.Statement;
private getByChatRoomIdStmt: Database.Statement;
private getLatestByRecipientStmt: Database.Statement;
private getRecentStmt: Database.Statement;
private getUncheckedStmt: Database.Statement;
@@ -254,6 +257,20 @@ export class MessagesRepository {
VALUES (?, ?, ?, ?, ?, ?)
`);
this.insertChatStmt = db.prepare(`
INSERT INTO sent_messages (post_id, recipient, subject, message, sent_at, chat_room_id)
VALUES (NULL, ?, ?, ?, ?, ?)
`);
this.getByChatRoomIdStmt = 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.chat_room_id = ?
LIMIT 1
`);
this.getByPostIdStmt = db.prepare(`SELECT * FROM sent_messages WHERE post_id = ?`);
this.getLatestByRecipientStmt = db.prepare(`
@@ -316,12 +333,35 @@ export class MessagesRepository {
return result.lastInsertRowid as number;
}
insertChat(message: {
recipient: string;
subject: string;
message: string;
sentAt: number;
chatRoomId: string;
}): number {
const result = this.insertChatStmt.run(
message.recipient,
message.subject,
message.message,
message.sentAt,
message.chatRoomId,
);
return result.lastInsertRowid as number;
}
getByPostId(postId: string): SentMessageRecord | undefined {
const row = this.getByPostIdStmt.get(postId) as Record<string, unknown> | undefined;
if (!row) return undefined;
return this.mapRow(row);
}
getByChatRoomId(chatRoomId: string): SentMessageRecord | undefined {
const row = this.getByChatRoomIdStmt.get(chatRoomId) as Record<string, unknown> | undefined;
if (!row) return undefined;
return this.mapRow(row);
}
getLatestByRecipient(recipient: string): SentMessageRecord | undefined {
const row = this.getLatestByRecipientStmt.get(recipient) as Record<string, unknown> | undefined;
if (!row) return undefined;
@@ -422,7 +462,7 @@ export class MessagesRepository {
private mapRow(row: Record<string, unknown>): SentMessageRecord {
return {
id: row.id as number,
postId: row.post_id as string,
postId: (row.post_id as string) ?? null,
recipient: row.recipient as string,
subject: row.subject as string,
message: row.message as string,
@@ -430,6 +470,7 @@ export class MessagesRepository {
gotResponse: Boolean(row.got_response),
responseCheckedAt: row.response_checked_at as number | null,
strategyId: (row.strategy_id as string) || null,
chatRoomId: (row.chat_room_id as string) ?? null,
subreddit: (row.subreddit as string) || undefined,
postTitle: (row.post_title as string) || undefined,
postImageUrl: (row.post_image_url as string) || undefined,
+23 -1
View File
@@ -1,5 +1,5 @@
import type { Logger } from '../../utils/logger.js';
import type { SuccessfulMessagesRepository } from '../../database/repositories.js';
import type { MessagesRepository, SuccessfulMessagesRepository } from '../../database/repositories.js';
import type { MediaService } from '../media.js';
import { ChatCache } from './cache.js';
import { MatrixClient, MatrixAuthError, MatrixRateLimitError } from './matrix/client.js';
@@ -114,6 +114,7 @@ export class ChatService {
private tokenRefresher?: TokenRefresher,
private usernameResolver?: UsernameResolver,
cache?: ChatCache,
private messagesRepo?: MessagesRepository,
) {
this.cache = cache ?? new ChatCache(logger);
}
@@ -701,6 +702,27 @@ export class ChatService {
if (firstSelf) chat.originMessage = firstSelf.text;
const hasReply = chat.messages.some((m) => !m.isSelf);
if (firstSelf && chat.roomId && this.messagesRepo) {
try {
const existing = this.messagesRepo.getByChatRoomId(chat.roomId);
const id = existing
? existing.id
: this.messagesRepo.insertChat({
recipient: chat.username,
subject: chat.username,
message: firstSelf.text,
sentAt: firstSelf.timestamp,
chatRoomId: chat.roomId,
});
if (hasReply && !existing?.gotResponse) {
this.messagesRepo.markGotResponse(id);
}
} catch (err) {
this.logger.warn('[chat] failed to record sent_message for stats:', err);
}
}
if (firstSelf && hasReply && this.successfulMessagesRepo) {
if (!this.successfulMessagesRepo.existsByMessage(firstSelf.text)) {
const firstReply = chat.messages.find((m) => !m.isSelf);
+1 -1
View File
@@ -56,7 +56,7 @@ export class ResponseCheckerService {
this.messagesRepo.markGotResponse(message.id);
// Get the original post info
const post = this.postsRepo.getById(message.postId);
const post = message.postId ? this.postsRepo.getById(message.postId) : undefined;
// Store as successful message for learning
this.successfulMessagesRepo.insert({
+1 -1
View File
@@ -81,7 +81,7 @@ export function createConversationsRouter(ctx: WebContext): Router {
} | null = null;
try {
const sent = ctx.repos.messages.getLatestByRecipient(conversation.username);
if (sent) {
if (sent && sent.postId) {
const url = sent.subreddit
? `https://www.reddit.com/r/${sent.subreddit}/comments/${sent.postId}/`
: `https://redd.it/${sent.postId}`;