import { z } from 'zod'; export const MAX_CONFIGURED_AUTOMATED_CONVERSATION_TURNS = 20; export const ToneKeySchema = z.union([ z.literal('default'), z.literal('dominant'), z.literal('submissive'), z.literal('playful'), z.literal('direct'), z.literal('gentle'), z.literal('kink-friendly'), z.literal('teasing'), ]); export const RateLimitConfigSchema = z.strictObject({ minDelayBetweenMessagesMs: z.number(), maxMessagesPerHour: z.number(), jitterMs: z.number(), }); export const RuntimeRedditConfigSchema = z.strictObject({ prioritizeRecentPosts: z.boolean(), recentWindowMinutes: z.number(), maxReadyCandidates: z.number().int().min(1).max(500), physiqueTargeting: z.strictObject({ enabled: z.boolean(), minScore: z.number().int().min(1).max(10), maxScore: z.number().int().min(1).max(10), }).refine((targeting) => targeting.minScore <= targeting.maxScore, { message: 'Minimum physique score cannot exceed maximum physique score.', path: ['minScore'], }).default({ enabled: false, minScore: 1, maxScore: 10 }), }); export const RuntimeConfigSchema = z.strictObject({ rateLimit: RateLimitConfigSchema, browser: z.strictObject({ dryRun: z.boolean() }), reddit: RuntimeRedditConfigSchema, }); export const ConversationBehaviourSchema = z.strictObject({ mode: z.union([z.literal('legacy'), z.literal('r4r'), z.literal('r4r-media'), z.literal('manual')]).default('legacy'), maxAutomatedTurns: z.number().int().min(1).max(MAX_CONFIGURED_AUTOMATED_CONVERSATION_TURNS).default(3), rapportTurnsBeforeMediaAsk: z.number().int().min(1).max(MAX_CONFIGURED_AUTOMATED_CONVERSATION_TURNS - 1).default(3), }).superRefine((behaviour, context) => { if (behaviour.mode === 'r4r-media' && behaviour.rapportTurnsBeforeMediaAsk >= behaviour.maxAutomatedTurns) { context.addIssue({ code: 'custom', path: ['rapportTurnsBeforeMediaAsk'], message: 'Rapport turns must be lower than the maximum automated turns.', }); } }).default({ mode: 'legacy', maxAutomatedTurns: 3, rapportTurnsBeforeMediaAsk: 3 }); export const SubredditConfigSchema = z.strictObject({ name: z.string(), enabled: z.boolean().default(true), type: z.union([z.literal('image'), z.literal('chat')]), targetingMode: z.union([z.literal('post-author'), z.literal('comment-author')]).default('post-author'), weight: z.number().optional(), tone: ToneKeySchema, scripted: z.string().optional(), conversation: ConversationBehaviourSchema, }).superRefine((subreddit, context) => { if (subreddit.type === 'chat' && subreddit.targetingMode === 'comment-author') { context.addIssue({ code: 'custom', path: ['targetingMode'], message: 'Comment-author targeting is available only for image posts.', }); } }); export const ConfigResponseSchema = RuntimeConfigSchema.extend({ subreddits: z.array(SubredditConfigSchema), tones: z.array(ToneKeySchema), advanced: z.record(z.string(), z.unknown()).optional(), }); export const ConfigUpdateRequestSchema = z.strictObject({ rateLimit: RateLimitConfigSchema.partial().optional(), browser: z.strictObject({ dryRun: z.boolean().optional() }).optional(), reddit: RuntimeRedditConfigSchema.partial().optional(), subreddits: z.array(SubredditConfigSchema).min(1).optional(), advanced: z.record(z.string(), z.unknown()).optional(), }); export const ConfigUpdateResponseSchema = z.strictObject({ success: z.literal(true), config: RuntimeConfigSchema, subreddits: z.array(SubredditConfigSchema), advanced: z.record(z.string(), z.unknown()).optional(), }); export type ToneKey = z.infer; export type RuntimeConfig = z.infer; export type ConfigResponse = z.infer; export type ConfigUpdateRequest = z.infer;