#!/usr/bin/env python3 # -*- coding: utf-8 -*- # flake8: noqa # pylint: disable=broad-exception-raised, raise-missing-from, too-many-arguments, redefined-outer-name # pylance: disable=reportMissingImports, reportMissingModuleSource, reportGeneralTypeIssues # type: ignore import logging import copy import re import os import json import html from datetime import datetime from typing import Union, Dict, Any, List from pyrogram.types import Message from pyrogram.enums import MessageMediaType from bleach.css_sanitizer import CSSSanitizer from bleach import clean as HTMLSanitizer from config import get_settings from url_signer import generate_media_digest Config = get_settings() logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) if not logger.handlers: handler = logging.StreamHandler() handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) logger.addHandler(handler) #tests #http://127.0.0.1:8000/post/html/DragorWW_space/114 — video #http://127.0.0.1:8000/post/html/DragorWW_space/20 - many photos #http://127.0.0.1:8000/post/html/DragorWW_space/58 - photos+video #http://127.0.0.1:8000/post/html/DragorWW_space/44 - poll #http://127.0.0.1:8000/post/html/DragorWW_space/46 - photo #http://127.0.0.1:8000/post/html/DragorWW_space/49, http://127.0.0.1:8000/post/html/DragorWW_space/63 — webpage #http://127.0.0.1:8000/post/html/deckru/826 - animation #http://127.0.0.1:8000/post/html/DragorWW_space/61 — links #http://127.0.0.1:8000/post/html/theyforcedme/3577 - video note #http://127.0.0.1:8000/post/html/theyforcedme/3572 - audio #http://127.0.0.1:8000/post/html/theyforcedme/3558 - audio-note #http://127.0.0.1:8000/html/vvzvlad_lytdybr/426 - sticker #http://127.0.0.1:8000/html/wrkshprn/634 # http://127.0.0.1:8000/html/ni404head/1278 # http://127.0.0.1:8000/html/fieryfiles/4840 — links without #http://127.0.0.1:8000/html/ru2ch_ban/26586 - large video #http://127.0.0.1:8000/html/smallpharm/4828 - forwarded from channel #http://127.0.0.1:8000/html/vvzvlad_lytdybr/659 - forwarded from user #http://127.0.0.1:8000/html/ufjqk/1070 - reply to #http://127.0.0.1:8000/html/tetstststststststffd/4 - forwarded from channel without name #http://127.0.0.1:8000/html/tetstststststststffd/14 - forwarded from hidden user #https://t.me/smallpharm/4802 many pics and text # https://t.me/webstrangler/3987 many pics without text # https://t.me/teslacoilpro/7117 # https://t.me/red_spades/1222 many media + text #https://t.me/ni404head/1283 file #http://127.0.0.1:8000/rss/-1002069358234 channel with numeric ID class PostParser: def __init__(self, client): self.client = client def channel_name_prepare(self, channel: str): if isinstance(channel, str) and channel.startswith('-100'): # Convert numeric channel ID to int channel_id = int(channel) return channel_id else: return channel async def get_post(self, channel: str, post_id: int, output_type: str = 'json', debug: bool = False) -> Union[str, Dict[Any, Any]]: print(f"Getting post {channel}, {post_id}") try: channel = self.channel_name_prepare(channel) message = await self.client.get_messages(channel, post_id) if Config["debug"]: print(message) if not message: logger.error(f"post_not_found: channel {channel}, post_id {post_id}") return None if output_type == 'html': return self._format_html(message, debug) elif output_type == 'json': return self.process_message(message) else: logger.error(f"Invalid output type: {output_type}") return None except Exception as e: logger.error(f"post_parsing_error: channel {channel}, post_id {post_id}, error {str(e)}") raise def _get_author_info(self, message: Message) -> str: if message.sender_chat: title = getattr(message.sender_chat, 'title', None) username = getattr(message.sender_chat, 'username', None) title = title.strip() if title else '' username = username.strip() if username else '' return f"{title} (@{username})" if username else title elif message.from_user: first = getattr(message.from_user, 'first_name', None) last = getattr(message.from_user, 'last_name', None) username = getattr(message.from_user, 'username', None) first = first.strip() if first else '' last = last.strip() if last else '' username = username.strip() if username else '' name = ' '.join(filter(None, [first, last])) return f"{name} (@{username})" if username else name return "Unknown author" def _generate_title(self, message: Message) -> str: # Priority 1: Media Type (if present and not just a webpage preview/poll) if message.media and message.media != MessageMediaType.WEB_PAGE and message.media != MessageMediaType.POLL: if message.media == MessageMediaType.PHOTO: return "📷 Photo" elif message.media == MessageMediaType.VIDEO: return "🎥 Video" elif message.media == MessageMediaType.ANIMATION: return "🎞 GIF" elif message.media == MessageMediaType.AUDIO: return "🎵 Audio" elif message.media == MessageMediaType.VOICE: return "🎤 Voice" elif message.media == MessageMediaType.VIDEO_NOTE: return "📱 Video circle" elif message.media == MessageMediaType.STICKER: return "🎯 Sticker" elif message.media == MessageMediaType.POLL: return "📊 Poll" elif message.media == MessageMediaType.DOCUMENT: # Distinguish PDF documents if hasattr(message.document, 'mime_type') and 'pdf' in message.document.mime_type.lower(): return "📄 PDF Document" else: return "📎 Document" # Priority 2: Service Messages if getattr(message, "channel_chat_created", False): return "✨ Channel created" text = message.text or message.caption or '' text_stripped = text.strip() # Priority 3: Text Content (if media didn't determine the title) if text_stripped: # Check if the text contains only URL(s) text_without_urls = re.sub(r'https?://[^\s<>"\']+', '', text_stripped) if not text_without_urls.strip(): # If only URL(s) are present after removing them # Check for specific domains like YouTube if re.search(r'(?:youtube\.com|youtu\.be)', text_stripped.lower()): return "🎥 YouTube Link" # More specific title for YouTube links return "🔗 Web link" # Generic title for other links # Use first line of text if it's not just URL(s) # Clean the text by removing URLs and HTML tags first clean_text = re.sub(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', '', text) clean_text = re.sub('<[^<]+?>', '', clean_text) # Remove empty lines resulting from cleaning clean_text = '\\n'.join(line.strip() for line in clean_text.split('\\n') if line.strip()) if clean_text: # If there is meaningful text left first_line = clean_text.split('\\n', maxsplit=1)[0] max_length = 100 # Max title length if len(first_line) <= max_length: return first_line.strip() # Use the first line if it's short enough # Trim longer lines intelligently trimmed = first_line[:max_length] last_space = trimmed.rfind(' ') if last_space != -1: # Trim at the last space found trimmed = trimmed[:last_space] # Append ellipsis if trimmed return f"{trimmed.strip()}..." if trimmed else "📝 Text Post" # Fallback if trimming results in empty string # Priority 4: Fallbacks for specific types if text is empty or was just URL(s) # This handles cases where only a web_page preview exists without significant text/caption if message.web_page: return "🔗 Web link" # Final fallback if no other condition met return "�� Unknown Post" def _format_forward_info(self, message: Message) -> Union[str, None]: if forward_from_chat := getattr(message, "forward_from_chat", None): forward_title = getattr(forward_from_chat, "title", "Unknown channel") forward_username = getattr(forward_from_chat, "username", None) if forward_username: forward_link = f'{forward_title} (@{forward_username})' return f'
{debug_json}')
html_content = '\n'.join(html_content)
return html_content
def _format_flags(self, message: Message) -> str:
if not Config['show_post_flags']:
return ''
flags = self._extract_flags(message)
if flags:
flags_html = ['')
return ' '.join(flags_html)
return ''
def process_message(self, message: Message) -> Dict[Any, Any]:
result = {
'channel': self.get_channel_username(message),
'message_id': message.id,
'date': datetime.timestamp(message.date),
'date_formatted': message.date.strftime('%Y-%m-%d %H:%M:%S'),
'text': message.text or message.caption or '',
'html': {
'title': self._generate_title(message),
'header': self._generate_html_header(message),
'body': self._generate_html_body(message),
'media': self._generate_html_media(message),
'footer': self._generate_html_footer(message)
},
'flags': self._extract_flags(message),
'author': self._get_author_info(message),
'views': message.views,
'reactions': self._extract_reactions(message),
'media_group_id': message.media_group_id,
'service': getattr(message, "service", None)
}
# Add webpage data if available
if webpage := getattr(message, "web_page", None):
result['webpage'] = {
'type': getattr(webpage, 'type', None),
'url': getattr(webpage, 'url', None),
'display_url': getattr(webpage, 'display_url', None),
'site_name': getattr(webpage, 'site_name', None),
'title': getattr(webpage, 'title', None),
'description': getattr(webpage, 'description', None),
'has_large_media': getattr(webpage, 'has_large_media', False),
'is_telegram_link': getattr(webpage, 'type', '') == 'telegram_message'
}
return result
def _sanitize_html(self, html_raw: str) -> str:
allowed_tags = ['p', 'a', 'b', 'i', 'strong', 'em', 'ul', 'ol', 'li', 'br', 'div', 'span', 'img', 'video', 'audio', 'source']
allowed_attributes = {
'a': ['href', 'title', 'target'],
'img': {'src': True, 'alt': True, 'style': True},
'video': {'controls': True, 'src': True, 'style': True},
'audio': {'controls': True, 'style': True},
'source': ['src', 'type'],
'div': {'class': True, 'style': True},
'span': ['class']
}
try:
css_sanitizer = CSSSanitizer(
allowed_css_properties=["max-width", "max-height", "object-fit", "width", "height"]
)
sanitized_html = HTMLSanitizer(
html_raw,
tags=allowed_tags,
attributes=allowed_attributes,
protocols=['http', 'https', 'tg'],
css_sanitizer=css_sanitizer,
strip=True,
)
return sanitized_html
except Exception as e:
logger.error(f"html_sanitization_error: {str(e)}")
return html_raw
def _generate_html_header(self, message: Message) -> str:
content_header = []
# Add forwarded from or reply info if present
if forward_html := self._format_forward_info(message): content_header.append(forward_html)
elif reply_html := self._format_reply_info(message): content_header.append(reply_html)
html_header = '\n'.join(content_header)
html_header = self._sanitize_html(html_header)
return html_header
def _generate_html_body(self, message: Message) -> str:
content_body = []
if message.text: text = message.text.html
elif message.caption: text = message.caption.html
else: text = ''
text = text.replace('\n', '