From 6fe8334311700a8fafa75dbbd3f17a10956616fc Mon Sep 17 00:00:00 2001 From: deadzilla Date: Mon, 25 May 2026 12:04:44 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D1=83=20!ne?= =?UTF-8?q?ws=20=D0=B4=D0=BB=D1=8F=20AI-=D0=BD=D0=BE=D0=B2=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B5=D0=B9=20=D1=81=20Habr=20-=20=D0=9F=D0=B0=D1=80?= =?UTF-8?q?=D1=81=D0=B8=D0=BD=D0=B3=20RSS=20=D1=87=D0=B5=D1=80=D0=B5=D0=B7?= =?UTF-8?q?=20ElementTree=20(RSS=202.0=20/=20Atom)=20-=20=D0=94=D0=B0?= =?UTF-8?q?=D0=BD=D0=BD=D1=8B=D0=B5:=20title,=20dc:creator,=20guid=20isPer?= =?UTF-8?q?maLink,=20pubDate,=20category=20-=20=D0=A4=D0=BE=D1=80=D0=BC?= =?UTF-8?q?=D0=B0=D1=82:=20=D0=B7=D0=B0=D0=B3=D0=BE=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=D0=BE=D0=BA=20=D0=B4=D0=BE=2060=20=D1=81=D0=B8=D0=BC=D0=B2?= =?UTF-8?q?=D0=BE=D0=BB=D0=BE=D0=B2,=20=D0=B4=D0=B0=D1=82=D0=B0=20=D0=B4?= =?UTF-8?q?=D0=B4.=D0=BC=D0=BC.=D0=B3=D0=B3=D0=B3=D0=B3,=20=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D0=B8=20-=20=D0=A1=D1=81=D1=8B=D0=BB=D0=BA=D0=B8=20?= =?UTF-8?q?=D0=B1=D0=B5=D0=B7=20https://,=20=D0=BA=D0=BB=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=D0=B1=D0=B5=D0=BB=D1=8C=D0=BD=D1=8B=D0=B5=20-=20=D0=9A=D0=BE?= =?UTF-8?q?=D0=BD=D1=81=D0=BE=D0=BB=D1=8C=D0=BD=D0=B0=D1=8F=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0:=20!news?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot.py | 19 ++++--- commands/__init__.py | 3 +- commands/news.py | 105 +++++++++++++++++++++++++++++++++++ console_commands/__init__.py | 2 + console_commands/news.py | 94 +++++++++++++++++++++++++++++++ 5 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 commands/news.py create mode 100644 console_commands/news.py diff --git a/bot.py b/bot.py index 1164cc9..f907ef8 100644 --- a/bot.py +++ b/bot.py @@ -47,16 +47,19 @@ def console_input(): while not stop_event.is_set(): try: cmd = input().strip().lower() - if cmd == "stop": - print("\nОстановка бота...") - if "stop" in ALL_CONSOLE_COMMANDS: - ALL_CONSOLE_COMMANDS["stop"](stop_event, bot) - break - elif cmd: - if cmd in ALL_CONSOLE_COMMANDS: - ALL_CONSOLE_COMMANDS[cmd](stop_event, bot) + if cmd.startswith("!"): + cmd_name = cmd[1:] + if cmd_name == "stop": + print("\nОстановка бота...") + if "stop" in ALL_CONSOLE_COMMANDS: + ALL_CONSOLE_COMMANDS["stop"](stop_event, bot) + break + elif cmd_name in ALL_CONSOLE_COMMANDS: + ALL_CONSOLE_COMMANDS[cmd_name](stop_event, bot) else: print(f"Неизвестная команда: {cmd}") + elif cmd: + print(f"Неизвестная команда: {cmd}") except (EOFError, KeyboardInterrupt): stop_event.set() try: diff --git a/commands/__init__.py b/commands/__init__.py index bc47f31..6ffd849 100644 --- a/commands/__init__.py +++ b/commands/__init__.py @@ -1,3 +1,4 @@ from .pogoda import Pogoda +from .news import News -ALL_COMMANDS = [Pogoda] +ALL_COMMANDS = [Pogoda, News] diff --git a/commands/news.py b/commands/news.py new file mode 100644 index 0000000..69491a5 --- /dev/null +++ b/commands/news.py @@ -0,0 +1,105 @@ +import discord +from discord.ext import commands +import requests +from xml.etree import ElementTree + + +RSS_URL = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/rated10/?fl=ru" + + +class News(commands.Cog): + """Команда !news — свежие статьи по AI с Habr""" + + @commands.command(name="news") + async def news(self, ctx): + """Топ-5 свежих статей по AI с Habr""" + articles = self._fetch_rss() + if articles is None: + await ctx.send("Не удалось получить новости. Попробуйте позже.") + return + + if not articles: + await ctx.send("Новостей пока нет.") + return + + await self._format_and_send(ctx, articles) + + def _fetch_rss(self): + """Скачать и распарсить RSS-ленту (RSS 2.0 / Atom).""" + try: + response = requests.get(RSS_URL, timeout=10) + response.raise_for_status() + root = ElementTree.fromstring(response.content) + + # RSS 2.0 + ns_dc = {"dc": "http://purl.org/dc/elements/1.1/"} + items = root.findall(".//item") + if not items: + # Atom + ns = {"atom": "http://www.w3.org/2005/Atom"} + items = root.findall("atom:entry", ns) + if not items: + return [] + + articles = [] + for entry in items: + # RSS 2.0 + title_el = entry.find("title") + date_el = entry.find("pubDate") + creator_el = entry.find("dc:creator", ns_dc) + categories = entry.findall("category") + + # guid с isPermaLink="true" для чистого URL + guid_el = entry.find("guid[@isPermaLink='true']") + link = guid_el.text if guid_el is not None else "" + + # Atom fallback + if title_el is None: + ns = {"atom": "http://www.w3.org/2005/Atom"} + title_el = entry.find("atom:title", ns) + link_el = entry.find("atom:link", ns) + link = link_el.get("href", "") if link_el is not None else "" + date_el = entry.find("atom:published", ns) + creator_el = entry.find("atom:author/atom:name", ns) + categories = entry.findall("atom:category", ns) + + title = title_el.text if title_el is not None else "Без названия" + pub_date = date_el.text if date_el is not None else "" + creator = creator_el.text if creator_el is not None else "" + tags = [cat.text for cat in categories if cat.text] if categories else [] + + articles.append({ + "title": title, + "link": link, + "pub_date": pub_date, + "creator": creator, + "tags": tags, + }) + return articles[:10] + except requests.RequestException: + return None + + async def _format_and_send(self, ctx, articles): + """Сформировать текст и отправить в чат.""" + lines = ["**🤖 AI-новости с Habr**"] + for i, article in enumerate(articles[:5], 1): + from datetime import datetime + date_str = "" + if article["pub_date"]: + try: + d = article["pub_date"].replace(" GMT", " +0000") + dt = datetime.strptime(d, "%a, %d %b %Y %H:%M:%S %z") + date_str = dt.strftime("%d.%m.%Y") + except ValueError: + date_str = article["pub_date"][:10].replace("-", ".") + tags_str = ", ".join(article["tags"][:3]) if article["tags"] else "" + title = article["title"] + if len(title) > 60: + title = title[:60] + "..." + lines.append(f"{i}. {title}") + lines.append(f" {article['creator']} | {date_str} | {tags_str} ") + link = article["link"].replace("https://", "") + lines.append(f" {link}") + + message = "\n".join(lines).rstrip() + await ctx.send(message, allowed_mentions=discord.AllowedMentions.none()) diff --git a/console_commands/__init__.py b/console_commands/__init__.py index e540de2..19941b4 100644 --- a/console_commands/__init__.py +++ b/console_commands/__init__.py @@ -1,5 +1,7 @@ from .stop import stop +from .news import news ALL_CONSOLE_COMMANDS = { "stop": stop, + "news": news, } diff --git a/console_commands/news.py b/console_commands/news.py new file mode 100644 index 0000000..d31efcc --- /dev/null +++ b/console_commands/news.py @@ -0,0 +1,94 @@ +import requests +from xml.etree import ElementTree + + +RSS_URL = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/rated10/?fl=ru" + + +def news(stop_event, bot): + """Вывести топ-5 свежих статей по AI с Habr""" + articles = _fetch_rss() + if articles is None: + print("Не удалось получить новости.") + return + + if not articles: + print("Новостей пока нет.") + return + + from datetime import datetime + print("**AI-новости с Habr**") + for i, article in enumerate(articles[:5], 1): + date_str = "" + if article["pub_date"]: + try: + d = article["pub_date"].replace(" GMT", " +0000") + dt = datetime.strptime(d, "%a, %d %b %Y %H:%M:%S %z") + date_str = dt.strftime("%d.%m.%Y") + except ValueError: + date_str = article["pub_date"][:10].replace("-", ".") + tags_str = ", ".join(article["tags"][:3]) if article["tags"] else "" + link = article["link"].replace("https://", "") + title = article["title"] + if len(title) > 60: + title = title[:60] + "..." + print(f"{i}. {title}") + print(f" {article['creator']} | {date_str} | {tags_str}") + print(f" {link}") + print() + + +def _fetch_rss(): + """Скачать и распарсить RSS-ленту (RSS 2.0 / Atom).""" + try: + response = requests.get(RSS_URL, timeout=10) + response.raise_for_status() + root = ElementTree.fromstring(response.content) + + # RSS 2.0 + ns_dc = {"dc": "http://purl.org/dc/elements/1.1/"} + items = root.findall(".//item") + if not items: + # Atom + ns = {"atom": "http://www.w3.org/2005/Atom"} + items = root.findall("atom:entry", ns) + if not items: + return [] + + articles = [] + for entry in items: + # RSS 2.0 + title_el = entry.find("title") + date_el = entry.find("pubDate") + creator_el = entry.find("dc:creator", ns_dc) + categories = entry.findall("category") + + # guid с isPermaLink="true" для чистого URL + guid_el = entry.find("guid[@isPermaLink='true']") + link = guid_el.text if guid_el is not None else "" + + # Atom fallback + if title_el is None: + ns = {"atom": "http://www.w3.org/2005/Atom"} + title_el = entry.find("atom:title", ns) + link_el = entry.find("atom:link", ns) + link = link_el.get("href", "") if link_el is not None else "" + date_el = entry.find("atom:published", ns) + creator_el = entry.find("atom:author/atom:name", ns) + categories = entry.findall("atom:category", ns) + + title = title_el.text if title_el is not None else "Без названия" + pub_date = date_el.text if date_el is not None else "" + creator = creator_el.text if creator_el is not None else "" + tags = [cat.text for cat in categories if cat.text] if categories else [] + + articles.append({ + "title": title, + "link": link, + "pub_date": pub_date, + "creator": creator, + "tags": tags, + }) + return articles[:10] + except requests.RequestException: + return None