Files
RedditBot/src/web/routes/outreach.ts
T

126 lines
4.5 KiB
TypeScript

import { Router } from 'express';
import type { CandidateStatus } from '../../database/candidate-queue-repository.js';
import type { WebContext } from '../context.js';
const ACTIVE_STATUSES = ['discovered', 'screening', 'ready', 'sending', 'retryable'] as const;
type ActiveCandidateStatus = Exclude<CandidateStatus, 'sent' | 'rejected'>;
interface OutreachCandidateView {
postId: string;
subreddit: string;
author: string;
title: string;
postUrl: string;
sourceType: 'image' | 'chat';
status: ActiveCandidateStatus;
priority: number;
postCreatedAt: number;
discoveredAt: number;
updatedAt: number;
availableAt: number;
attemptCount: number;
hasPreparedMessage: boolean;
physiqueScore: number | null;
preparation: {
message: string;
imageAnalysis: string;
detectedGender: 'male' | 'female' | 'other' | 'unknown';
attractivenessScore: number | null;
physiqueScore: number | null;
physiqueTargeting: { enabled: boolean; minScore: number; maxScore: number } | null;
sourceImageUrl: string | null;
strategyId: string | null;
experimentId: string | null;
promptVersion: string | null;
selectionReason: 'explore' | 'exploit' | null;
modelId: string;
toneId: string;
scripted: boolean;
} | null;
note: string | null;
owner: { accountId: string; redditUsername: string } | null;
}
function redditPostUrl(permalink: string): string {
try {
return new URL(permalink, 'https://www.reddit.com').toString();
} catch {
return 'https://www.reddit.com';
}
}
export function createOutreachRouter(ctx: WebContext): Router {
const router = Router();
router.get('/outreach', (_req, res) => {
const queue = ctx.services.candidateQueue;
if (!queue) {
res.status(503).json({ error: 'Outreach queue not initialized' });
return;
}
const stats = queue.getStats();
const records = queue.listActive();
const accountById = new Map((ctx.services.redditAccounts?.list() ?? [])
.map((account) => [account.accountId, account.redditUsername] as const));
const candidates: OutreachCandidateView[] = records.map((candidate) => ({
postId: candidate.postId,
subreddit: candidate.subreddit,
author: candidate.author,
title: candidate.post.title,
postUrl: redditPostUrl(candidate.post.permalink),
sourceType: candidate.sourceType,
status: candidate.status as ActiveCandidateStatus,
priority: candidate.priority,
postCreatedAt: candidate.post.created_utc * 1_000,
discoveredAt: candidate.discoveredAt,
updatedAt: candidate.updatedAt,
availableAt: candidate.availableAt,
attemptCount: candidate.attemptCount,
hasPreparedMessage: candidate.prepared !== null,
physiqueScore: candidate.prepared?.physiqueScore ?? null,
preparation: candidate.prepared ? {
message: candidate.prepared.generatedMessage,
imageAnalysis: candidate.prepared.imageAnalysis,
detectedGender: candidate.prepared.detectedGender,
attractivenessScore: candidate.prepared.attractivenessScore ?? null,
physiqueScore: candidate.prepared.physiqueScore ?? null,
physiqueTargeting: candidate.prepared.physiqueTargeting ?? null,
sourceImageUrl: candidate.prepared.sourceImageUrl ?? null,
strategyId: candidate.prepared.strategyId,
experimentId: candidate.prepared.experimentId,
promptVersion: candidate.prepared.promptVersion,
selectionReason: candidate.prepared.selectionReason,
modelId: candidate.prepared.modelId,
toneId: candidate.prepared.toneId,
scripted: candidate.prepared.scripted,
} : null,
note: candidate.lastError,
owner: candidate.assignedAccountId
? {
accountId: candidate.assignedAccountId,
redditUsername: accountById.get(candidate.assignedAccountId) ?? candidate.assignedAccountId,
}
: null,
}));
const stages = {
discovered: stats.byStatus.discovered,
screening: stats.byStatus.screening,
ready: stats.byStatus.ready,
sending: stats.byStatus.sending,
retryable: stats.byStatus.retryable,
};
const response = {
generatedAt: Date.now(),
activeCount: ACTIVE_STATUSES.reduce((total, status) => total + stats.byStatus[status], 0),
stages,
outcomes: { sent: stats.byStatus.sent, rejected: stats.byStatus.rejected },
oldestActiveAt: queue.getOldestActiveAt(),
candidates,
};
res.json(response);
});
return router;
}