Add EGE support prototype v0.1.0
This commit is contained in:
1 parent
9944a41cca
commit
24cae23262
19 files changed
+2581
-2
No files matched your search
+158
@@ -0,0 +1,158 @@
|
||||
const denial = 'Административный доступ и закрытые данные сайта через чат пользователей не предоставляются. Если нужна помощь с учёбой или личным кабинетом, опишите проблему.';
|
||||
const politeness = 'Пожалуйста, общайтесь вежливо и без нецензурных выражений.';
|
||||
const banWarning = 'Пожалуйста, прекратите использовать нецензурные выражения. При дальнейшем нарушении доступ к чату может быть заблокирован.';
|
||||
|
||||
const adminTargets = [
|
||||
/(?<![\p{L}\d])(?:админк[а-яё]*|админ(?:ск|истраторск)[а-яё]*|панел[а-яё]*\s+администрирования|суперпользовател[а-яё]*|суперадмин[а-яё]*)(?![\p{L}\d])/iu,
|
||||
/административн[а-яё]*\s+(?:прав|доступ|полномочи)[а-яё]*|(?:прав|доступ|рол|полномочи)[а-яё]*\s+(?:(?:к|для|как\s+у|уровня)\s+)?(?:админ[а-яё]*|модератор[а-яё]*|панел[а-яё]*\s+управления)|полн[а-яё]*\s+доступ[а-яё]*\s+ко\s+всем\s+(?:разделам|функциям)\s+сайта/iu,
|
||||
/\b(?:root|sudo|superuser|admin\s+(?:access|rights|privileges|role))\b/iu,
|
||||
];
|
||||
const adminRequests = [
|
||||
/(?<![\p{L}\d])(?<!не )(?:дай(?:те)?|выдай(?:те)?|предоставь(?:те)?|открой(?:те)?|назначь(?:те)?|присвой(?:те)?|повысь(?:те)?|разреши(?:те)?|добавь(?:те)?|сделай(?:те)?|хочу|прошу|требую)(?![\p{L}\d])/iu,
|
||||
/(?<![\p{L}\d])(?<!зачем )(?<!почему )(?<!не )нуж(?:ен|на|но|ны)(?![\p{L}\d])/iu,
|
||||
/(?:как\s+(?:мне\s+)?(?:получить|стать|попасть|зайти|открыть)|где\s+(?:находится\s+)?админка|можно\s+(?:ли\s+)?(?:мне\s+)?(?:дать|выдать|получить|стать|зайти|в\s+админку|доступ|админск[а-яё]*|прав[а-яё]*)|могли\s+бы\s+(?:дать|выдать|предоставить))(?![\p{L}\d])/iu,
|
||||
/\b(?:give|grant|assign|elevate|make)\b/iu,
|
||||
];
|
||||
const makeAdmin = /(?<![\p{L}\d])(?:сделай(?:те)?|назнач[а-яё]*|как\s+(?:мне\s+)?стать|хочу\s+стать)\s+(?:(?:мне|меня|себя)\s+)?админ(?:ом|истратором)(?![\p{L}\d])/iu;
|
||||
const secretTargets = [
|
||||
/(?<![\p{L}\d])(?:парол[а-яё]*|токен[а-яё]*|api[\s-]*(?:key|ключ)|ключ[а-яё]*\s+(?:api|доступа)|код[а-яё]*\s+(?:из\s+смс|доступа|подтверждения|авторизации)|системн[а-яё]*\s+промпт[а-яё]*|секрет[а-яё]*)(?![\p{L}\d])/iu,
|
||||
/исходн[а-яё]*\s+код[а-яё]*|код[а-яё]*\s+(?:сайта|сервера)|баз[а-яё]*\s+(?:данных|пользовател[а-яё]*|ученик[а-яё]*|клиент[а-яё]*)|(?:лог[а-яё]*|журнал[а-яё]*)\s+(?:сервера|сайта|админки|приложения)/iu,
|
||||
/(?:данн[а-яё]*|спис[а-яё]*|переписк[а-яё]*)\s+(?:(?:всех|других)\s+)?(?:пользовател[а-яё]*|ученик[а-яё]*|сотрудник[а-яё]*|клиент[а-яё]*)/iu,
|
||||
/(?:закрыт|конфиденциальн|внутренн|приватн|непубличн|персональн|чуж)[а-яё]*\s+(?:данн|информаци|сведени|документ|файл|отч[её]т|статистик|переписк|лог|настройк|инструкц)[а-яё]*/iu,
|
||||
/\b(?:(?:private|confidential|internal)\s+(?:data|information|logs|documents)|passwords?|api[\s-]*tokens?|system\s+prompt)\b/iu,
|
||||
];
|
||||
const secretRequests = [
|
||||
/(?<![\p{L}\d])(?<!не )(?:дай(?:те)?|выдай(?:те)?|покажи(?:те)?|предоставь(?:те)?|пришли(?:те)?|вышли(?:те)?|отправь(?:те)?|выведи(?:те)?|раскрой(?:те)?|выгрузи(?:те)?|слей(?:те)?|скинь(?:те)?|сообщи(?:те)?|назови(?:те)?|поделись|поделитесь|открой(?:те)?|скачай(?:те)?|расскажи(?:те)?)(?![\p{L}\d])/iu,
|
||||
/(?:хочу|можно(?:\s+ли)?|можете|могли\s+бы|прошу)\s+(?:мне\s+)?(?:дать|выдать|показать|предоставить|прислать|выслать|отправить|выгрузить|сообщить|поделиться|раскрыть|узнать|увидеть|получить|посмотреть|найти|скачать)(?![\p{L}\d])/iu,
|
||||
/(?:как|где)\s+(?:мне\s+)?(?:узнать|увидеть|получить|посмотреть|найти|скачать|взять)(?![\p{L}\d])|\b(?:give|send|show|reveal|dump|expose)\b/iu,
|
||||
/(?:хочу|мне\s+нуж(?:ен|на|но|ны))\s+(?:парол|токен|секрет|закрыт|конфиденциальн|приватн|внутренн|непубличн|баз)[а-яё]*/iu,
|
||||
];
|
||||
const ownData = /(?:(?:мо|сво)[ийяеюхм][а-яё]*\s+(?:персональн[а-яё]*\s+)?(?:данн[а-яё]*|профил[а-яё]*|аккаунт[а-яё]*)|(?:персональн[а-яё]*\s+)?данн[а-яё]*\s+(?:моего|своего)\s+(?:аккаунта|профиля))/giu;
|
||||
const recoveryGuide = /(?:покажи(?:те)?|подскажи(?:те)?|расскажи(?:те)?|объясни(?:те)?)\s*[,—:]?\s*как\s+(?:мне\s+)?(?:восстановить|сбросить|сменить|поменять|изменить|настроить)(?![\p{L}\d])/giu;
|
||||
const previousRequest = /^(?:пожалуйста\s+)?(?:дайте|выдайте|выдать|покажите|пришлите|поделитесь|предоставьте|откройте|отправьте|скиньте|сообщите|хочу получить|мне нужен|можно получить)(?:\s+(?:мне|нам|доступ|базу|данные|информацию)){0,3}$/iu;
|
||||
const failure = /(?:не\s+(?:работ|открыва|загружа|мог|получа)|ошибк|сбой|недоступ)/iu;
|
||||
|
||||
const profanePatterns = [
|
||||
/^бля(?:д(?:ь|и|ей|ям|ью|ск\p{L}*|ств\p{L}*)|т(?:ь|и|ей))?$/u,
|
||||
/^(?:(?:а|на|по|ни|не|до|с|о|пере|за|от|рас|в|при))?ху(?:й|е|я|и)\p{L}*$/u,
|
||||
/^(?:раз|рас|за|на|от|вы|под|по|сп|про)?пизд\p{L}*$/u,
|
||||
/^(?:в|вы|вз|до|за|на|об|от|пере|под|по|про|раз|с|у)?[ъь]?еб(?:а\p{L}*|у(?:ч|т|щ)\p{L}*|н\p{L}*|ет\p{L}*|еш\p{L}*|л[яеи]\p{L}*|ис[ьт]\p{L}*|л)?$/u,
|
||||
/^(?:долбоеб\p{L}*|сук(?:а|и|у|ой|ою|ин)|мудак\p{L}*|мудил[аоуы]|говн(?:о|а|у|е|ом|ян\p{L}*)|пид(?:ор|ар)\p{L}*)$/u,
|
||||
/^(?:бл[*#@_]+(?:[дт][ьи])?|п[*#@_]+зд\p{L}*|(?:на|по)?х[*#@_]+[йеяи]\p{L}*|[*#@_]+бан\p{L}*)$/u,
|
||||
/^(?:fuck(?:ing|er|ed|s)?|motherfuck(?:er|ing)|f[*#@_]+ck|(?:bull)?shit|bitch(?:es)?|asshole(?:s)?)$/u,
|
||||
];
|
||||
const abuseFiller = /^(?:(?:да|ну|эй|а|и|ты|вы|же|то|тебя|тебе|вас|вам|твоя|твой|ваш|ваша|сам|сама|сами|все|это|просто|полный|полная|иди|идите|пош[её]л|пошла|пошли|туп[а-яё]*|дебил[а-яё]*|урод[а-яё]*|мраз[а-яё]*|you|your|it|that|go|off|the|out|just)(?:\s+|$))*$/iu;
|
||||
const keyboardRuns = [
|
||||
'qwertyuiop', 'asdfghjkl', 'zxcvbnm', 'qazwsxedcrfvtgbyhnujm',
|
||||
'йцукенгшщзхъ', 'фывапролджэ', 'ячсмитьбю', 'йфяцычувскамепинртгоьшлб',
|
||||
].flatMap((run) => [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;
|
||||
}
|
||||
Reference in new issue
Block a user