feat: track received image outcomes

This commit is contained in:
jjbrunton committed 2026-08-08 18:53:11 +01:00
1 parent 50de1c180d
commit d819a0f1e6
15 files changed
+145 -21

No files matched your search

+18
View File
@@ -187,6 +187,24 @@ export async function bootstrap(
const mediaDownloader = new MatrixMediaDownloader(activeMatrixClient, logger);
const mediaService = new MediaService(mediaDownloader, logger);
const indexedMedia = await mediaService.getMediaItems();
const receivedImageRooms = new Set(
indexedMedia
.filter((item) => item.sender.toLowerCase() !== 'me' && item.chatRoomId)
.map((item) => item.chatRoomId as string),
);
let backfilledImageOutcomes = 0;
for (const roomId of receivedImageRooms) {
const message = messagesRepo.getByChatRoomId(roomId);
const hasPerformanceContext = Boolean(message?.postId || message?.strategyId || message?.subreddit);
if (!message || !hasPerformanceContext || message.gotImage) continue;
messagesRepo.markGotImage(message.id);
backfilledImageOutcomes += 1;
}
if (backfilledImageOutcomes > 0) {
logger.info(`Backfilled ${backfilledImageOutcomes} received-image performance outcome(s) from the media index`);
}
// A 401 waits for a new extension dump instead of opening a browser login
// or token-sniff flow.
const tokenRefresher = new MatrixTokenRefresher();
+6
View File
@@ -192,6 +192,12 @@ export function runMigrations(db: Database.Database): void {
updated_at INTEGER NOT NULL
);
`),
() => {
addColumnIfNotExists(db, 'sent_messages', 'got_image', 'INTEGER NOT NULL DEFAULT 0');
db.exec(`
CREATE INDEX IF NOT EXISTS idx_sent_messages_image ON sent_messages(got_image);
`);
},
];
let currentVersion = db.pragma('user_version', { simple: true }) as number;
+40 -4
View File
@@ -28,6 +28,7 @@ export interface SentMessageRecord {
message: string;
sentAt: number;
gotResponse: boolean;
gotImage: boolean;
responseCheckedAt: number | null;
strategyId: string | null;
chatRoomId: string | null;
@@ -47,6 +48,7 @@ export interface SuccessfulMessageRecord {
responseSnippet: string | null;
subreddit: string | null;
createdAt: number;
gotImage?: boolean;
}
export interface StrategyStats {
@@ -54,6 +56,8 @@ export interface StrategyStats {
totalSent: number;
totalResponses: number;
responseRate: number;
totalImages: number;
imageRate: number;
}
export interface SubredditStats {
@@ -61,6 +65,8 @@ export interface SubredditStats {
totalSent: number;
totalResponses: number;
responseRate: number;
totalImages: number;
imageRate: number;
}
export interface DailyTrend {
@@ -68,6 +74,8 @@ export interface DailyTrend {
totalSent: number;
totalResponses: number;
responseRate: number;
totalImages: number;
imageRate: number;
}
export interface ActivityLogRecord {
@@ -417,6 +425,7 @@ export class MessagesRepository {
private getRecentStmt: Database.Statement;
private getUncheckedStmt: Database.Statement;
private markResponseStmt: Database.Statement;
private markImageStmt: Database.Statement;
private markCheckedStmt: Database.Statement;
private hasSentToStmt: Database.Statement;
@@ -475,6 +484,12 @@ export class MessagesRepository {
UPDATE sent_messages SET got_response = 1, response_checked_at = ? WHERE id = ?
`);
this.markImageStmt = db.prepare(`
UPDATE sent_messages
SET got_image = 1, got_response = 1, response_checked_at = ?
WHERE id = ?
`);
this.markCheckedStmt = db.prepare(`
UPDATE sent_messages SET response_checked_at = ? WHERE id = ?
`);
@@ -565,6 +580,10 @@ export class MessagesRepository {
this.markResponseStmt.run(Date.now(), messageId);
}
markGotImage(messageId: number): void {
this.markImageStmt.run(Date.now(), messageId);
}
markChecked(messageId: number): void {
this.markCheckedStmt.run(Date.now(), messageId);
}
@@ -578,7 +597,8 @@ export class MessagesRepository {
SELECT
strategy_id,
COUNT(*) as total_sent,
SUM(CASE WHEN got_response = 1 THEN 1 ELSE 0 END) as total_responses
SUM(CASE WHEN got_response = 1 THEN 1 ELSE 0 END) as total_responses,
SUM(CASE WHEN got_image = 1 THEN 1 ELSE 0 END) as total_images
FROM sent_messages
WHERE strategy_id IS NOT NULL
GROUP BY strategy_id
@@ -591,6 +611,10 @@ export class MessagesRepository {
responseRate: (row.total_sent as number) > 0
? Math.round(((row.total_responses as number) / (row.total_sent as number)) * 100)
: 0,
totalImages: row.total_images as number,
imageRate: (row.total_sent as number) > 0
? Math.round(((row.total_images as number) / (row.total_sent as number)) * 100)
: 0,
}));
}
@@ -599,7 +623,8 @@ export class MessagesRepository {
SELECT
pp.subreddit,
COUNT(*) as total_sent,
SUM(CASE WHEN sm.got_response = 1 THEN 1 ELSE 0 END) as total_responses
SUM(CASE WHEN sm.got_response = 1 THEN 1 ELSE 0 END) as total_responses,
SUM(CASE WHEN sm.got_image = 1 THEN 1 ELSE 0 END) as total_images
FROM sent_messages sm
JOIN processed_posts pp ON sm.post_id = pp.id
GROUP BY pp.subreddit
@@ -613,6 +638,10 @@ export class MessagesRepository {
responseRate: (row.total_sent as number) > 0
? Math.round(((row.total_responses as number) / (row.total_sent as number)) * 100)
: 0,
totalImages: row.total_images as number,
imageRate: (row.total_sent as number) > 0
? Math.round(((row.total_images as number) / (row.total_sent as number)) * 100)
: 0,
}));
}
@@ -622,7 +651,8 @@ export class MessagesRepository {
SELECT
date(sent_at / 1000, 'unixepoch') as day,
COUNT(*) as total_sent,
SUM(CASE WHEN got_response = 1 THEN 1 ELSE 0 END) as total_responses
SUM(CASE WHEN got_response = 1 THEN 1 ELSE 0 END) as total_responses,
SUM(CASE WHEN got_image = 1 THEN 1 ELSE 0 END) as total_images
FROM sent_messages
WHERE sent_at > ?
AND (post_id IS NOT NULL OR strategy_id IS NOT NULL)
@@ -637,6 +667,10 @@ export class MessagesRepository {
responseRate: (row.total_sent as number) > 0
? Math.round(((row.total_responses as number) / (row.total_sent as number)) * 100)
: 0,
totalImages: row.total_images as number,
imageRate: (row.total_sent as number) > 0
? Math.round(((row.total_images as number) / (row.total_sent as number)) * 100)
: 0,
}));
}
@@ -649,6 +683,7 @@ export class MessagesRepository {
message: row.message as string,
sentAt: row.sent_at as number,
gotResponse: Boolean(row.got_response),
gotImage: Boolean(row.got_image),
responseCheckedAt: row.response_checked_at as number | null,
strategyId: (row.strategy_id as string) || null,
chatRoomId: (row.chat_room_id as string) ?? null,
@@ -680,7 +715,7 @@ export class SuccessfulMessagesRepository {
`);
this.getAttributedRecentStmt = db.prepare(`
SELECT successful_messages.*
SELECT successful_messages.*, COALESCE(sm.got_image, 0) AS got_image
FROM successful_messages
LEFT JOIN sent_messages sm ON sm.id = successful_messages.sent_message_id
WHERE successful_messages.subreddit IS NOT NULL
@@ -762,6 +797,7 @@ export class SuccessfulMessagesRepository {
responseSnippet: row.response_snippet as string | null,
subreddit: row.subreddit as string | null,
createdAt: row.created_at as number,
...(row.got_image === undefined ? {} : { gotImage: Boolean(row.got_image) }),
};
}
}
+23
View File
@@ -737,6 +737,9 @@ export class ChatService {
try {
const withMedia = await this.mediaService.attachMedia(chat, mediaMessages);
this.mergeNewMessages(chat, withMedia);
if (withMedia.some((message) => !message.isSelf && (message.media?.length ?? 0) > 0)) {
this.recordReceivedImage(chat);
}
} catch (err) {
this.logger.warn(
`[chat] delta media attach failed for ${roomId}: ${(err as Error).message}`,
@@ -848,6 +851,9 @@ export class ChatService {
if (firstSelf) chat.originMessage = firstSelf.text;
const hasReply = chat.messages.some((m) => !m.isSelf);
const hasReceivedImage = chat.messages.some(
(message) => !message.isSelf && (message.media?.length ?? 0) > 0,
);
let performanceMessage: SentMessageRecord | undefined;
if (firstSelf && chat.roomId && this.messagesRepo) {
@@ -861,6 +867,9 @@ export class ChatService {
if (hasReply && !existing.gotResponse) {
this.messagesRepo.markGotResponse(existing.id);
}
if (hasReceivedImage && !existing.gotImage) {
this.messagesRepo.markGotImage(existing.id);
}
}
} catch (err) {
this.logger.warn('[chat] failed to associate reply with performance context:', err);
@@ -897,6 +906,20 @@ export class ChatService {
}
}
private recordReceivedImage(chat: ActiveChat): void {
if (!chat.roomId || !this.messagesRepo) return;
try {
const existing = this.messagesRepo.getByChatRoomId(chat.roomId);
const hasPerformanceContext = Boolean(
existing?.postId || existing?.strategyId || existing?.subreddit,
);
if (!existing || !hasPerformanceContext) return;
if (!existing.gotImage) this.messagesRepo.markGotImage(existing.id);
} catch (err) {
this.logger.warn('[chat] failed to associate received image with performance context:', err);
}
}
/**
* Project a Matrix room (from /sync) into an ActiveChat. If `prev` is
* supplied, fields it carries (messages, pendingReply, originMessage)
+2
View File
@@ -83,6 +83,8 @@ export class StrategySelector {
totalSent: s?.totalSent || 0,
totalResponses: s?.totalResponses || 0,
responseRate: s?.responseRate || 0,
totalImages: s?.totalImages || 0,
imageRate: s?.imageRate || 0,
configured: true,
});
}