diff --git a/README.md b/README.md index 0363cb2..2fad663 100644 --- a/README.md +++ b/README.md @@ -81,3 +81,23 @@ Or, use my https://github.com/vvzvlad/miniflux-tg-add-bot to add channel to mini ``` curl https://pgbridge.example.com/html/DragorWW_space/87 ``` ``` curl https://pgbridge.example.com/json/DragorWW_space/87 ``` + +## Exclude flags + +Exclusion flags are a way to filter channel content based on pre-defined (by me) criteria. It's not a universal regexp-based filtering engine, for example, but it does 99% of my tasks of filtering the content of some toxic tg channels (mostly with fresh memes). + +There are several flags: +video - presence of video and small text in the post +stream, donat - words like "стрим", "донат" +clown, poo - emoticons in post reactions (>30) +advert - tag #реклама +hid_channel, foreign_channel - links to closed tg channels (https://t.me/+S0OfKyMDRi) and to open channels (https://t.me/suprechannel) that do not equal the name of the current channel. + +You can use exclude_flags parameter in rss/html/json urls to exclude posts with certain flags. For example, to exclude all posts with the "video" flag, you can use: + +``` curl https://pgbridge.example.com/rss/DragorWW_space?exclude_flags=video,stream,donat,clown ``` + +Or use meta-flag "all" to exclude all posts: + +``` curl https://pgbridge.example.com/rss/DragorWW_space?exclude_flags=all ``` + diff --git a/api_server.py b/api_server.py index 80010f2..fede967 100644 --- a/api_server.py +++ b/api_server.py @@ -520,21 +520,20 @@ async def get_media(channel: str, post_id: int, file_unique_id: str, digest: str @app.get("/rss/{channel}", response_class=Response) @app.get("/rss/{channel}/{token}", response_class=Response) -async def get_rss_feed(channel: str, token: str | None = None, limit: int = 50, output_type: str = 'rss'): +async def get_rss_feed(channel: str, token: str | None = None, limit: int = 50, output_type: str = 'rss', exclude_flags: str | None = None): if Config["token"]: if token != Config["token"]: logger.error(f"Invalid token for RSS feed: {token}, expected: {Config['token']}") raise HTTPException(status_code=403, detail="Invalid token") else: logger.info(f"Valid token for RSS feed: {token}") - while True: try: if output_type == 'rss': - rss_content = await generate_channel_rss(channel, client=client.client, limit=limit) + rss_content = await generate_channel_rss(channel, client=client.client, limit=limit, exclude_flags=exclude_flags) return Response(content=rss_content, media_type="application/xml") elif output_type == 'html': - rss_content = await generate_channel_html(channel, client=client.client, limit=limit) + rss_content = await generate_channel_html(channel, client=client.client, limit=limit, exclude_flags=exclude_flags) return Response(content=rss_content, media_type="text/html") except ValueError as e: error_message = f"Invalid parameters for RSS feed generation: {str(e)}" diff --git a/post_parser.py b/post_parser.py index 548636c..8f1018a 100644 --- a/post_parser.py +++ b/post_parser.py @@ -196,6 +196,60 @@ class PostParser: return {r.emoji: r.count for r in reactions.reactions} return None + def _extract_flags(self, message: Message) -> List[str]: + # Check if there is no text or caption and add all applicable flags. + message_text = self._generate_html_body(message) + flags = [] + # Add flag "video" if the message media is VIDEO and the body text is up to 100 characters. + if message.media == MessageMediaType.VIDEO and len(message_text.strip()) <= 100: + flags.append("video") + + # Check if the message text contains variations of the word "стрим" + # (e.g., "стрим", "стримы", etc.) or "livestream" in a case-insensitive manner. + if re.search(r'(?i)\b(стрим\w*|livestream)\b', message_text): + flags.append("stream") + + # Check if the message text contains the word "донат" in a case-insensitive manner. + if re.search(r'(?i)\bдонат\w*\b', message_text): + flags.append("donat") + + # Check if the post's reactions contain more than 5 clown emojis (🤡). + if getattr(message, "reactions", None): + for reaction in message.reactions.reactions: + if reaction.emoji == "🤡" and reaction.count >= 30: + flags.append("clown") + break + + # Check if the post's reactions contain more than 5 poo emojis (💩). + if getattr(message, "reactions", None): + for reaction in message.reactions.reactions: + if reaction.emoji == "💩" and reaction.count >= 30: + flags.append("poo") + break + + # Check if the message text contains "#реклама" (advertisement tag) in a case-insensitive manner. + if re.search(r'(?i)#реклама', message_text): + flags.append("advert") + + # New: Check for t.me links: hidden channel and open (foreign) channel. + try: + # Find links with a '+' after t.me/ indicating a hidden channel link. + hidden_links = re.findall(r'https?://(?:www\.)?t\.me/\+([A-Za-z0-9]+)', message_text) + # Find links without a '+' after t.me/ indicating an open (foreign) channel link. + open_links = re.findall(r'https?://(?:www\.)?t\.me/(?!\+)([A-Za-z0-9_]+)', message_text) + if hidden_links: + flags.append("hid_channel") + + current_channel = self.get_channel_username(message) + # Add the flag "foreign_channel" if there's an open link that differs from the current channel. + for open_link in open_links: + if current_channel is None or open_link.lower() != current_channel.lower(): + flags.append("foreign_channel") + break + except Exception as e: + logger.error(f"tme_link_extraction_error: message_id {message.id}, error {str(e)}") + + return flags def _format_html(self, message: Message) -> str: html_content = [] @@ -208,6 +262,11 @@ class PostParser: html_content = '\n'.join(html_content) return html_content + def _format_flags(self, message: Message) -> str: + flags = self._extract_flags(message) + if flags: + return f'
' + return '' def process_message(self, message: Message) -> Dict[Any, Any]: result = { @@ -223,6 +282,7 @@ class PostParser: '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), @@ -342,6 +402,8 @@ class PostParser: content_footer = [] if reactions_views_html := self._reactions_views_links(message): # Add reactions, views and links content_footer.append(reactions_views_html) + if flags_html := self._format_flags(message): # Add flags + content_footer.append(flags_html) html_footer = '\n'.join(content_footer) html_footer = self._sanitize_html(html_footer) return html_footer @@ -399,14 +461,22 @@ class PostParser: try: parts = [] - if reactions := getattr(message, "reactions"): - reactions_html = '' + reactions_html = "" + views_html = "" + if reactions := getattr(message, "reactions", None): for reaction in reactions.reactions: reactions_html += f'{reaction.emoji} {reaction.count} ' - parts.append(reactions_html.rstrip()) + reactions_html = reactions_html.rstrip() if views := getattr(message, "views", None): views_html = f'{views} views' + + # If both reactions and views exist, insert a line break between them. + if reactions_html and views_html: + parts.append(reactions_html + "