Files
RedditBot/src/services/conversation-boundaries.ts
T

82 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)];
}