import html import re from html.parser import HTMLParser def to_html(obj): return str(obj).replace("<", "<").replace(">", ">") class TelegramHTMLSanitizer(HTMLParser): ALLOWED_TAGS = {"b", "i", "u", "s", "code", "pre", "a"} TAG_ALIASES = {"strong": "b", "em": "i"} ALLOWED_HREF_PREFIXES = ("http://", "https://", "tg://", "mailto:") def __init__(self) -> None: super().__init__(convert_charrefs=False) self.parts: list[str] = [] self.tag_stack: list[str] = [] def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: normalized_tag = self.TAG_ALIASES.get(tag, tag) if normalized_tag not in self.ALLOWED_TAGS: return if normalized_tag == "a": href = next((value for key, value in attrs if key == "href" and value), None) if not href or not href.startswith(self.ALLOWED_HREF_PREFIXES): return safe_href = html.escape(href, quote=True) self.parts.append(f'') self.tag_stack.append(normalized_tag) return self.parts.append(f"<{normalized_tag}>") self.tag_stack.append(normalized_tag) def handle_endtag(self, tag: str) -> None: normalized_tag = self.TAG_ALIASES.get(tag, tag) if normalized_tag not in self.ALLOWED_TAGS: return for index in range(len(self.tag_stack) - 1, -1, -1): if self.tag_stack[index] == normalized_tag: del self.tag_stack[index] self.parts.append(f"") break def handle_data(self, data: str) -> None: self.parts.append(html.escape(data, quote=False)) def handle_entityref(self, name: str) -> None: self.parts.append(f"&{name};") def handle_charref(self, name: str) -> None: self.parts.append(f"&#{name};") def get_html(self) -> str: while self.tag_stack: self.parts.append(f"") return "".join(self.parts) def markdown_to_telegram_html(text: str) -> str: prepared = text.replace("\r\n", "\n").strip() prepared = re.sub( r"\[([^\]]+)\]\((https?://[^\s)]+)\)", r'\1', prepared, ) prepared = re.sub(r"\*\*(.+?)\*\*", r"\1", prepared, flags=re.DOTALL) prepared = re.sub(r"__(.+?)__", r"\1", prepared, flags=re.DOTALL) prepared = re.sub(r"(?m)^[ \t]*[*-]\s+", "• ", prepared) return prepared def format_telegram_html(text: str) -> str: prepared = markdown_to_telegram_html(str(text)) sanitizer = TelegramHTMLSanitizer() sanitizer.feed(prepared) sanitizer.close() return sanitizer.get_html() def parse_links_to_inline_markup(message: str) -> list: """ Парсит сообщение с форматированными ссылками и возвращает список рядов кнопок. Формат входного сообщения: - [Текст кнопки + Ссылка] для одной кнопки. - [Кнопка1 + Ссылка1][Кнопка2 + Ссылка2] для нескольких кнопок в одном ряду. - Каждая строка представляет отдельный ряд кнопок. Пример: [Кнопка1 + https://example.com] [Кнопка2 + https://example.org][Кнопка3 + https://example.net] :param message: Строка с отформатированными ссылками. :return: Список рядов кнопок, где каждый ряд — это список кортежей (Текст, Ссылка). """ # Исправленное регулярное выражение для поиска [Текст + Ссылка] pattern = re.compile(r"\[([^\[\]+]+)\s*\+\s*(https?://[^\[\]]+)\]") # Инициализируем список рядов кнопок keyboard_rows = [] # Разбиваем сообщение на строки lines = message.strip().split("\n") for line in lines: # Находим все совпадения в строке matches = pattern.findall(line) if matches: row = [] for text, url in matches: button = (text.strip(), url.strip()) row.append(button) keyboard_rows.append(row) return keyboard_rows