import re from typing import Optional, Union, Tuple from datetime import datetime import html ALLOWED_HTML_TAGS = { 'b', 'strong', 'i', 'em', 'u', 'ins', 's', 'strike', 'del', 'code', 'pre', 'a', 'blockquote' } SELF_CLOSING_TAGS = { 'br', 'hr', 'img' } def validate_email(email: str) -> bool: pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' return re.match(pattern, email) is not None def validate_phone(phone: str) -> bool: pattern = r'^\+?[1-9]\d{1,14}$' cleaned_phone = re.sub(r'[\s\-\(\)]', '', phone) return re.match(pattern, cleaned_phone) is not None def validate_telegram_username(username: str) -> bool: if not username: return False username = username.lstrip('@') pattern = r'^[a-zA-Z0-9_]{5,32}$' return re.match(pattern, username) is not None def validate_promocode(code: str) -> bool: if not code or len(code) < 3 or len(code) > 20: return False return code.replace('_', '').replace('-', '').isalnum() def validate_amount(amount_str: str, min_amount: float = 0, max_amount: float = float('inf')) -> Optional[float]: try: amount = float(amount_str.replace(',', '.')) if min_amount <= amount <= max_amount: return amount return None except (ValueError, TypeError): return None def validate_positive_integer(value: Union[str, int], max_value: int = None) -> Optional[int]: try: num = int(value) if num > 0 and (max_value is None or num <= max_value): return num return None except (ValueError, TypeError): return None def validate_date_string(date_str: str, date_format: str = "%Y-%m-%d") -> Optional[datetime]: try: return datetime.strptime(date_str, date_format) except ValueError: return None def validate_url(url: str) -> bool: pattern = r'^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$' return re.match(pattern, url) is not None def validate_uuid(uuid_str: str) -> bool: pattern = r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' return re.match(pattern, uuid_str.lower()) is not None def validate_traffic_amount(traffic_str: str) -> Optional[int]: traffic_str = traffic_str.upper().strip() if traffic_str in ['UNLIMITED', 'БЕЗЛИМИТ', '∞']: return 0 units = { 'MB': 1, 'GB': 1024, 'TB': 1024 * 1024, 'МБ': 1, 'ГБ': 1024, 'ТБ': 1024 * 1024 } for unit, multiplier in units.items(): if traffic_str.endswith(unit): try: value = float(traffic_str[:-len(unit)].strip()) return int(value * multiplier) except ValueError: break try: return int(float(traffic_str)) except ValueError: return None def validate_subscription_period(days: Union[str, int]) -> Optional[int]: try: days_int = int(days) if 1 <= days_int <= 3650: return days_int return None except (ValueError, TypeError): return None def sanitize_html(text: str) -> str: if not text: return text text = html.escape(text) allowed_tags = ALLOWED_HTML_TAGS.union(SELF_CLOSING_TAGS) for tag in allowed_tags: text = re.sub( f'<(/?{tag}\\b[^>]*)>', lambda m: "<" + ( m.group(1) .replace(""", "\"") .replace("'", "'") .replace("&", "&") ) + ">", text, flags=re.IGNORECASE ) return text def sanitize_telegram_name(name: Optional[str]) -> Optional[str]: """Санитизация Telegram-имени для безопасной вставки в HTML и хранения. Заменяет угловые скобки и амперсанд на безопасные визуальные аналоги. """ if not name: return name try: return ( name.replace('<', '‹') .replace('>', '›') .replace('&', '&') .strip() ) except Exception: return name def validate_device_count(count: Union[str, int]) -> Optional[int]: try: count_int = int(count) if 1 <= count_int <= 10: return count_int return None except (ValueError, TypeError): return None def validate_referral_code(code: str) -> bool: if not code: return False if code.startswith('ref') and len(code) > 3: user_id_part = code[3:] return user_id_part.isdigit() return validate_promocode(code) def validate_html_tags(text: str) -> Tuple[bool, str]: if not text: return True, "" tag_pattern = r'<(/?)([a-zA-Z][a-zA-Z0-9-]*)[^>]*>' tags = re.findall(tag_pattern, text) for is_closing, tag_name in tags: tag_name_lower = tag_name.lower() if tag_name_lower not in ALLOWED_HTML_TAGS and tag_name_lower not in SELF_CLOSING_TAGS: return False, f"Неподдерживаемый тег: <{tag_name}>" return validate_html_structure(text) def validate_html_structure(text: str) -> Tuple[bool, str]: tag_pattern = r'<(/?)([a-zA-Z][a-zA-Z0-9-]*)[^>]*?/?>' matches = re.finditer(tag_pattern, text) tag_stack = [] for match in matches: full_tag = match.group(0) is_closing = bool(match.group(1)) tag_name = match.group(2).lower() if full_tag.endswith('/>') or tag_name in SELF_CLOSING_TAGS: continue if not is_closing: tag_stack.append(tag_name) else: if not tag_stack: return False, f"Закрывающий тег без открывающего: " last_tag = tag_stack.pop() if last_tag != tag_name: return False, f"Неправильная вложенность тегов: ожидался , найден " if tag_stack: return False, f"Незакрытый тег: <{tag_stack[-1]}>" return True, "" def fix_html_tags(text: str) -> str: if not text: return text fixes = [ (r']+)>', r''), (r'<(br|hr|img[^>]*?)>', r'<\1 />'), (r'<<([^>]+)>>', r'<\1>'), (r'<\s+([^>]+)\s+>', r'<\1>'), ] result = text for pattern, replacement in fixes: result = re.sub(pattern, replacement, result, flags=re.IGNORECASE) return result def get_html_help_text() -> str: return """Поддерживаемые HTML теги:<b>жирный</b> или <strong>жирный</strong><i>курсив</i> или <em>курсив</em><u>подчеркнутый</u><s>зачеркнутый</s><code>моноширинный</code><pre>блок кода</pre><a href="url">ссылка</a><blockquote>цитата</blockquote> ⚠️ Важные правила: • Каждый открывающий тег должен быть закрыт • Теги должны быть правильно вложены • Атрибуты ссылок берите в кавычки ❌ Неправильно: <b>жирный <i>курсив</b></i> <a href=google.com>ссылка</a> ✅ Правильно: <b>жирный <i>курсив</i></b> <a href="https://google.com">ссылка</a>""" def validate_rules_content(text: str) -> Tuple[bool, str, Optional[str]]: if not text or not text.strip(): return False, "Текст правил не может быть пустым", None if len(text) > 4000: return False, f"Текст слишком длинный: {len(text)} символов (максимум 4000)", None is_valid_html, html_error = validate_html_tags(text) if not is_valid_html: fixed_text = fix_html_tags(text) fixed_is_valid, _ = validate_html_tags(fixed_text) if fixed_is_valid and fixed_text != text: return False, html_error, fixed_text else: return False, html_error, None return True, "", None