227 lines
5.5 KiB
Python
227 lines
5.5 KiB
Python
import re
|
||
import unicodedata
|
||
from typing import Optional
|
||
|
||
_OBFUSCATION_CHARS = " .\\-/\\\\•﹒٫_․·∙‧ꞏ‒–—﹘﹣⁻−"
|
||
|
||
_URL_PATTERNS = [
|
||
re.compile(r"(?i)https?://\S+"),
|
||
re.compile(r"(?i)www\.\S+"),
|
||
re.compile(r"(?i)tg://\S+"),
|
||
re.compile(r"(?i)telegram\.me\S*"),
|
||
re.compile(r"(?i)t\.me/\+\S*"),
|
||
re.compile(r"(?i)joinchat\S*"),
|
||
]
|
||
|
||
_OBFUSCATED_DOMAIN_PATTERNS = [
|
||
re.compile(
|
||
r"(?i)[tт][\s{}\u2022]*[\.{}\u2022]*[\s{}\u2022]*[mм][eе]".format(
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
)
|
||
),
|
||
re.compile(
|
||
r"(?i)[tт][{}\s]*[eе][{}\s]*[lłl1i|][{}\s]*[eе]"
|
||
r"[{}\s]*[gɢgqг][{}\s]*[rр][{}\s]*[aа]"
|
||
r"[{}\s]*(?:[mм]|rn)".format(
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
re.escape(_OBFUSCATION_CHARS),
|
||
)
|
||
),
|
||
re.compile(r"(?i)t\.me\S*"),
|
||
]
|
||
|
||
_ENGLISH_SERVICE_PATTERNS = [
|
||
re.compile(r"(?i)telegram"),
|
||
re.compile(r"(?i)teleqram"),
|
||
re.compile(r"(?i)teiegram"),
|
||
re.compile(r"(?i)teieqram"),
|
||
re.compile(r"(?i)telegrarn"),
|
||
re.compile(r"(?i)service"),
|
||
re.compile(r"(?i)notif(?:ication)?"),
|
||
re.compile(r"(?i)system"),
|
||
re.compile(r"(?i)security"),
|
||
re.compile(r"(?i)safety"),
|
||
re.compile(r"(?i)support"),
|
||
re.compile(r"(?i)moderation"),
|
||
re.compile(r"(?i)review"),
|
||
re.compile(r"(?i)compliance"),
|
||
re.compile(r"(?i)abuse"),
|
||
re.compile(r"(?i)spam"),
|
||
re.compile(r"(?i)report"),
|
||
]
|
||
|
||
_RUSSIAN_SERVICE_PATTERNS = [
|
||
re.compile(r"(?i)телеграм\w*"),
|
||
re.compile(r"(?i)служебн\w*"),
|
||
re.compile(r"(?i)уведомлен\w*"),
|
||
re.compile(r"(?i)поддержк\w*"),
|
||
re.compile(r"(?i)безопасн\w*"),
|
||
re.compile(r"(?i)модерац\w*"),
|
||
re.compile(r"(?i)жалоб\w*"),
|
||
re.compile(r"(?i)абуз\w*"),
|
||
]
|
||
|
||
_PRE_LOWER_TRANSLATION = str.maketrans(
|
||
{
|
||
"I": "l",
|
||
"İ": "l",
|
||
"Q": "g",
|
||
"@": " ",
|
||
}
|
||
)
|
||
|
||
_POST_LOWER_TRANSLATION = str.maketrans(
|
||
{
|
||
"а": "a",
|
||
"б": "b",
|
||
"в": "v",
|
||
"г": "g",
|
||
"д": "d",
|
||
"е": "e",
|
||
"ё": "e",
|
||
"ж": "zh",
|
||
"з": "z",
|
||
"и": "i",
|
||
"і": "i",
|
||
"й": "i",
|
||
"к": "k",
|
||
"л": "l",
|
||
"м": "m",
|
||
"н": "n",
|
||
"о": "o",
|
||
"п": "p",
|
||
"р": "r",
|
||
"с": "s",
|
||
"т": "t",
|
||
"у": "u",
|
||
"ф": "f",
|
||
"х": "h",
|
||
"ц": "c",
|
||
"ч": "ch",
|
||
"ш": "sh",
|
||
"щ": "sh",
|
||
"ъ": "",
|
||
"ы": "y",
|
||
"ь": "",
|
||
"э": "e",
|
||
"ю": "yu",
|
||
"я": "ya",
|
||
"_": "_",
|
||
}
|
||
)
|
||
|
||
_NORMALIZED_BANNED_TOKENS = {
|
||
"tme",
|
||
"telegram",
|
||
"teleqram",
|
||
"teiegram",
|
||
"teieqram",
|
||
"telegrarn",
|
||
"joinchat",
|
||
"http",
|
||
"https",
|
||
"www",
|
||
"tg",
|
||
"service",
|
||
"notification",
|
||
"system",
|
||
"security",
|
||
"safety",
|
||
"support",
|
||
"moderation",
|
||
"review",
|
||
"compliance",
|
||
"abuse",
|
||
"spam",
|
||
"report",
|
||
}
|
||
|
||
_USERNAME_PLACEHOLDER = "клиент"
|
||
|
||
|
||
def _normalize_for_detection(value: str) -> str:
|
||
if not value:
|
||
return ""
|
||
|
||
normalized = unicodedata.normalize("NFKD", value)
|
||
normalized = normalized.translate(_PRE_LOWER_TRANSLATION)
|
||
normalized = normalized.lower()
|
||
normalized = "".join(
|
||
ch for ch in normalized if unicodedata.category(ch) != "Mn"
|
||
)
|
||
normalized = normalized.translate(_POST_LOWER_TRANSLATION)
|
||
normalized = normalized.replace("rn", "m")
|
||
|
||
pattern = rf"[{re.escape(_OBFUSCATION_CHARS)}\s]+"
|
||
normalized = re.sub(pattern, "", normalized)
|
||
normalized = re.sub(r"[^a-z0-9]+", "", normalized)
|
||
return normalized
|
||
|
||
|
||
def _remove_patterns(value: str) -> str:
|
||
updated = value
|
||
for pattern in (
|
||
_URL_PATTERNS
|
||
+ _OBFUSCATED_DOMAIN_PATTERNS
|
||
+ _ENGLISH_SERVICE_PATTERNS
|
||
+ _RUSSIAN_SERVICE_PATTERNS
|
||
):
|
||
updated = pattern.sub(" ", updated)
|
||
return updated
|
||
|
||
|
||
def _finalize(value: str) -> Optional[str]:
|
||
compacted = re.sub(r"\s+", " ", value)
|
||
compacted = compacted.strip(" \t\r\n-_.,/\\")
|
||
compacted = compacted.strip()
|
||
if not compacted:
|
||
return None
|
||
|
||
normalized = _normalize_for_detection(compacted)
|
||
if any(token in normalized for token in _NORMALIZED_BANNED_TOKENS):
|
||
return None
|
||
return compacted
|
||
|
||
|
||
def sanitize_display_name(value: Optional[str]) -> Optional[str]:
|
||
if value is None:
|
||
return None
|
||
clean = value.replace("@", " ")
|
||
clean = _remove_patterns(clean)
|
||
return _finalize(clean)
|
||
|
||
|
||
def sanitize_username(value: Optional[str]) -> Optional[str]:
|
||
if value is None:
|
||
return None
|
||
clean = value.strip()
|
||
clean = clean.lstrip("@")
|
||
clean = _remove_patterns(clean)
|
||
return _finalize(clean)
|
||
|
||
|
||
def username_for_display(username: Optional[str], with_at: bool = False) -> str:
|
||
sanitized = sanitize_username(username)
|
||
if not sanitized:
|
||
return _USERNAME_PLACEHOLDER
|
||
return f"@{sanitized}" if with_at else sanitized
|
||
|
||
|
||
def display_name_or_fallback(
|
||
first_name: Optional[str],
|
||
fallback: Optional[str] = None,
|
||
) -> str:
|
||
sanitized = sanitize_display_name(first_name)
|
||
if sanitized:
|
||
return sanitized
|
||
if fallback is not None:
|
||
return fallback
|
||
return _USERNAME_PLACEHOLDER
|