82 lines
3.9 KiB
TypeScript
82 lines
3.9 KiB
TypeScript
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)];
|
||
}
|