const denial = 'Административный доступ и закрытые данные сайта через чат пользователей не предоставляются. Если нужна помощь с учёбой или личным кабинетом, опишите проблему.'; const politeness = 'Пожалуйста, общайтесь вежливо и без нецензурных выражений.'; const banWarning = 'Пожалуйста, прекратите использовать нецензурные выражения. При дальнейшем нарушении доступ к чату может быть заблокирован.'; const adminTargets = [ /(? [run, [...run].reverse().join('')]); const unlikelyCyrillicPair = /[жш]ы|[чщ]я|[чщ]ю|[ъь][ъьы]|[ыэ][ыэ]/gu; const exactNoise = /^(?:абракадабр[а-я]*|асдф|(?:бла|ла|траля|лол|тесттест){2,})$/u; const fillerWord = /^(?:ну|ээ+|эм+|мм+|бла|лол|blah|uh|um)$/u; const emptyUtterance = /^(?:(?:просто )?(?:пишу|написал[аи]?|набираю) (?:просто так|что попало|что угодно|случайные (?:буквы|слова)|набор букв)|просто так|что попало|набор букв|(?:нет|без) (?:запроса|смысла)|(?:бла|blah)(?: (?:бла|blah))+|(?:ла ){2,}ла|(?:тест|лол|lol)(?: (?:тест|лол|lol))*)$/u; const inanimateSubject = /^(?:котлет[а-я]*|холодильник[а-я]*|стол(?:ы)?|табурет[а-я]*|картошк[а-я]*|пельмен[а-я]*|банан[а-я]*|огур[а-я]*|помидор[а-я]*|кирпич[а-я]*|тапк[а-я]*|носк[а-я]*|облак[а-я]*|время|времена|радуг[а-я]*|солнц[а-я]*|луна|луны)$/u; const incongruousVerb = /^(?:умножа[а-я]*|дел(?:ит|ят)|вычита[а-я]*|жу(?:ет|ют)|ест|едят|поеда[а-я]*|разговарива[а-я]*|по(?:ет|ют)|танцу[а-я]*|гавка[а-я]*|мяука[а-я]*|пиш(?:ет|ут)|чита[а-я]*|лета[а-я]*|леч(?:ит|ат)|звон(?:ит|ят))$/u; const offTopicFragment = /^(?:син[а-я]*|красн[а-я]*|зелен[а-я]*|фиолетов[а-я]*|красив[а-я]*|вкусн[а-я]*) (?:котлет[а-я]*|кот[а-я]*|собак[а-я]*|дерев[а-я]*|пельмен[а-я]*|банан[а-я]*|огур[а-я]*|помидор[а-я]*|тап[а-я]*|носк[а-я]*)$/u; const supportClue = /(?:вебинар|трансляци|задани|урок|экзамен|личн[а-я]* кабинет|ошибк|доступ|парол|профил|аккаунт|плеер|файл|загруз|не (?:могу|работает|открывается)|https?:\/\/)/u; export function isAdvertising(text) { return /рекламное предложение|купите базу|накрутк[аиу] подписчик|гарантированный заработок/iu.test(text); } function asksForRestrictedAccess(text) { const restrictedDataRequest = text.replace(ownData, '').replace(recoveryGuide, ''); return (adminTargets.some((pattern) => pattern.test(text)) && adminRequests.some((pattern) => pattern.test(text))) || makeAdmin.test(text) || (secretTargets.some((pattern) => pattern.test(restrictedDataRequest)) && secretRequests.some((pattern) => pattern.test(restrictedDataRequest))); } function isRestrictedRequest(text, history) { if (asksForRestrictedAccess(text)) return true; const previous = history.at(-1)?.trim().replace(/[.?!,:]+$/u, ''); if (!previous || text.split(/\s+/u).length > 7) return false; const priorWasIncomplete = previousRequest.test(previous); const currentIsIncomplete = previousRequest.test(text.replace(/[.?!,:]+$/u, '')); if (!priorWasIncomplete && (!currentIsIncomplete || failure.test(previous))) return false; return asksForRestrictedAccess(`${previous} ${text}`); } function isProfane(word) { const normalized = word.toLowerCase().replaceAll('ё', 'е'); if (profanePatterns.some((pattern) => pattern.test(normalized))) return true; // Check visually similar letters only inside a token already using Cyrillic. if (!/[а-я]/u.test(normalized)) return false; const lookalikes = { a: 'а', c: 'с', e: 'е', o: 'о', p: 'р', x: 'х', y: 'у' }; const cyrillic = normalized.replace(/[aceopxy]/gu, (letter) => lookalikes[letter]); return cyrillic !== normalized && profanePatterns.some((pattern) => pattern.test(cyrillic)); } function isNoiseWord(word) { const lower = word.toLowerCase().replaceAll('ё', 'е'); const repeatedCluster = /^([\p{L}]{3,5})\1+$/u.exec(lower)?.[1]; if (exactNoise.test(lower) || /^([\p{L}]{1,4})\1{2,}$/u.test(lower) || (repeatedCluster && keyboardRuns.some((run) => run.includes(repeatedCluster))) || (lower.length >= 4 && keyboardRuns.some((run) => run.includes(lower))) || /(?:asdf|qwer|йцуке|фыва)\p{L}*/u.test(lower)) return true; // Short uppercase abbreviations often identify formats, devices or departments. if (word === word.toUpperCase() && lower.length <= 7) return false; return (/^[а-я]{5,}$/u.test(lower) && (/[бвгджзйклмнпрстфхцчшщъь]{5}/u.test(lower) || /^[ъь]/u.test(lower) || [...lower.matchAll(unlikelyCyrillicPair)].length >= 2)) || (/^[a-z]{6,}$/u.test(lower) && !/[aeiouy]/u.test(lower)); } function isNonsense(text, hasContext) { const cleaned = text.normalize('NFKC').replace(/[\u200B-\u200D\u2060\uFEFF]/gu, ''); if (/https?:\/\//iu.test(cleaned)) return false; const codes = [...cleaned.matchAll(/\b([a-z]{2,})[-_]\d+\b/giu)]; if (/\b(?:err|error)\d+\b/iu.test(cleaned) || codes.some(([, prefix]) => !isNoiseWord(prefix))) return false; const words = cleaned.match(/\p{L}+/gu) || []; if (!words.length) { return !/\p{N}/u.test(cleaned) && (!hasContext || !/^(?:[?!]{1,3}|\p{Extended_Pictographic}\p{Variation_Selector}?)$/u.test(cleaned)); } const normalizedWords = words.map((word) => word.toLowerCase().replaceAll('ё', 'е')); const phrase = normalizedWords.join(' '); const noise = words.map(isNoiseWord); if (noise.some(Boolean) && normalizedWords.every((word, index) => noise[index] || fillerWord.test(word))) return true; if (/\p{N}/u.test(cleaned)) return false; if (emptyUtterance.test(phrase) || (!hasContext && normalizedWords.every((word) => fillerWord.test(word)))) return true; if (words.length >= 4 && normalizedWords.every((word) => word.length === 1)) { return new Set(normalizedWords).size <= 2 || isNoiseWord(normalizedWords.join('')); } // Recognize a short incongruous clause while keeping requests that mention a lesson or a technical issue. if (words.length > 8 || supportClue.test(phrase)) return false; const predicate = normalizedWords.findIndex((word) => incongruousVerb.test(word)); const absurdClause = predicate > 0 && predicate < 4 && normalizedWords.slice(0, predicate).some((word) => inanimateSubject.test(word)); return absurdClause || (!hasContext && offTopicFragment.test(phrase)); } export function moderateChatMessage({ actor, text, history = [], profanityCount = 0 }) { const trimmed = text.trim(); let profanity = false; const withoutProfanity = trimmed.replace(/[\p{L}*#@_]+/gu, (token) => { if (!isProfane(token)) return token; profanity = true; return ' '; }); const warning = profanity ? (profanityCount > 0 ? banWarning : politeness) : null; const result = { block: false, reason: null, reply: warning, profanity, text: trimmed }; if (actor === 'student' && isRestrictedRequest(trimmed, history)) { const reply = warning ? `${denial}\n\n${warning}` : denial; return { ...result, block: true, reason: 'Запрос административного доступа или закрытой информации из чата пользователей.', reply }; } if (isAdvertising(trimmed)) { return { ...result, block: true, reason: 'Текст похож на явное рекламное предложение.' }; } const remaining = withoutProfanity.replace(/[^\p{L}\p{N}]+/gu, ' ').trim(); if (profanity && abuseFiller.test(remaining)) { return { ...result, block: true, reason: 'Нецензурное сообщение без обращения за помощью.' }; } if (isNonsense(withoutProfanity.trim(), history.length > 0)) { return { ...result, block: true, reason: 'Сообщение не содержит осмысленного текста или деталей обращения.' }; } return result; }