export interface BoundaryMessage { text: string; isSelf: boolean; } const MEDIA_NOUN = String.raw`(?:pic|pics|picture|pictures|photo|photos|selfie|selfies|snap|snaps|image|images|video|videos|nude|nudes|content)`; const MEDIA_VERB = String.raw`(?:send(?:ing)?|shar(?:e|ing)|show(?:ing)?|giv(?:e|ing)|drop(?:ping)?|swap(?:ping)?|trad(?:e|ing)|snap(?:ping)?|tak(?:e|ing)|attach(?:ed|ing)?|upload(?:ed|ing)?)`; const EXPLICIT_MEDIA_REFUSAL_PATTERNS = [ new RegExp(String.raw`\b(?:no|not|never|won't|wouldn't|don't|do not|can't|cannot)\b[^.\n]{0,60}\b${MEDIA_NOUN}\b`, 'i'), new RegExp(String.raw`\b(?:won't|wouldn't|don't|do not|not going to|never)\b[^.\n]{0,40}\b${MEDIA_VERB}\b`, 'i'), new RegExp(String.raw`\b(?:zero|no) interest\b[^.\n]{0,60}\b(?:${MEDIA_VERB}|${MEDIA_NOUN}|anything|something)\b`, 'i'), new RegExp(String.raw`\b(?:rather not|not comfortable)\b[^.\n]{0,60}\b(?:${MEDIA_VERB}|${MEDIA_NOUN})\b`, 'i'), ]; const CONTEXTUAL_MEDIA_REFUSAL_PATTERNS = [ /^(?:no|nope|nah|no thanks|not really|rather not|pass)(?:,? thanks)?[.!\s]*$/i, /^(?:i(?:'m| am)|im|we(?:'re| are)|we're) good(?: thanks)?[.!\s]*$/i, /\b(?:zero interest|not interested|no interest)\b/i, /\b(?:not sending|won't send|wouldn't send|don't send|do not send)\b/i, ]; const FULL_STOP_PATTERNS = [ /\bstop\b/i, /\bleave me alone\b/i, /\b(?:do not|don't) message\b/i, /\bback off\b/i, /^(?:(?:i'm|i am|im)\s+)?not interested[.!\s]*$/i, /\b(?:creepy|uncomfortable|weirdest (?:reddit )?approach)\b/i, ]; export function containsMediaRequest(text: string): boolean { return new RegExp(String.raw`\b(?:can|could|would|will) you\b[^.!?\n]{0,60}\b(?:${MEDIA_VERB}\b[^.!?\n]{0,30})?\b${MEDIA_NOUN}\b`, 'i').test(text) || new RegExp(String.raw`\byou (?:can|could|may)\b[^.!?\n]{0,40}\b${MEDIA_VERB}\b[^.!?\n]{0,40}\b${MEDIA_NOUN}\b`, 'i').test(text) || new RegExp(String.raw`\b(?:send|share|show|give|drop) me\b[^.!?\n]{0,40}\b${MEDIA_NOUN}\b`, 'i').test(text) || new RegExp(String.raw`(?:^|[.!?]\s+|,\s+)(?:please\s+)?${MEDIA_VERB}\b[^.!?\n]{0,60}\b${MEDIA_NOUN}\b`, 'i').test(text) || new RegExp(String.raw`\bgot any (?:more )?${MEDIA_NOUN}\b`, 'i').test(text); } /** A generic promise or invitation to share our own approved media, not a request for theirs. */ export function containsMediaOffer(text: string): boolean { return new RegExp( String.raw`\b(?:i (?:can|could|would|will|might)|i['’]ll|want me to|how about i)\b[^.!?\n]{0,40}\b${MEDIA_VERB}\b[^.!?\n]{0,40}\b${MEDIA_NOUN}\b`, 'i', ).test(text) || new RegExp(String.raw`\bi(?:['’]ve| have) got\b[^.!?\n]{0,40}\b${MEDIA_NOUN}\b[^.!?\n]{0,30}\b(?:if you want|to share|to show you)\b`, 'i').test(text); } export function isFullConversationStop(text: string): boolean { return FULL_STOP_PATTERNS.some((pattern) => pattern.test(text)); } export function isMediaRefusal(text: string, mediaWasRequested: boolean): boolean { const normalized = text.replaceAll('’', "'"); if (EXPLICIT_MEDIA_REFUSAL_PATTERNS.some((pattern) => pattern.test(normalized))) return true; return mediaWasRequested && CONTEXTUAL_MEDIA_REFUSAL_PATTERNS.some((pattern) => pattern.test(normalized.trim())); } /** * Detect recipient limits in sequence so short phrases such as "I'm good" * only become a media boundary when they directly follow a media request. */ export function detectConversationBoundaries(messages: readonly BoundaryMessage[]): string[] { const boundaries: string[] = []; let lastSelfRequestedMedia = false; for (const message of messages) { if (message.isSelf) { lastSelfRequestedMedia = containsMediaRequest(message.text); continue; } if (isMediaRefusal(message.text, lastSelfRequestedMedia)) { boundaries.push('does not want to send photos or videos'); } if (isFullConversationStop(message.text)) { boundaries.push('does not want further sexual escalation'); } } return [...new Set(boundaries)]; }