feat: добавить команду !news для AI-новостей с Habr

- Парсинг RSS через ElementTree (RSS 2.0 / Atom)
- Данные: title, dc:creator, guid isPermaLink, pubDate, category
- Формат: заголовок до 60 символов, дата дд.мм.гггг, теги
- Ссылки без https://, кликабельные
- Консольная команда: !news
This commit is contained in:
deadzilla 2026-05-25 12:04:44 +05:00
parent 6553c9140f
commit 6fe8334311
5 changed files with 214 additions and 9 deletions

11
bot.py
View File

@ -47,16 +47,19 @@ def console_input():
while not stop_event.is_set(): while not stop_event.is_set():
try: try:
cmd = input().strip().lower() cmd = input().strip().lower()
if cmd == "stop": if cmd.startswith("!"):
cmd_name = cmd[1:]
if cmd_name == "stop":
print("\nОстановка бота...") print("\nОстановка бота...")
if "stop" in ALL_CONSOLE_COMMANDS: if "stop" in ALL_CONSOLE_COMMANDS:
ALL_CONSOLE_COMMANDS["stop"](stop_event, bot) ALL_CONSOLE_COMMANDS["stop"](stop_event, bot)
break break
elif cmd: elif cmd_name in ALL_CONSOLE_COMMANDS:
if cmd in ALL_CONSOLE_COMMANDS: ALL_CONSOLE_COMMANDS[cmd_name](stop_event, bot)
ALL_CONSOLE_COMMANDS[cmd](stop_event, bot)
else: else:
print(f"Неизвестная команда: {cmd}") print(f"Неизвестная команда: {cmd}")
elif cmd:
print(f"Неизвестная команда: {cmd}")
except (EOFError, KeyboardInterrupt): except (EOFError, KeyboardInterrupt):
stop_event.set() stop_event.set()
try: try:

View File

@ -1,3 +1,4 @@
from .pogoda import Pogoda from .pogoda import Pogoda
from .news import News
ALL_COMMANDS = [Pogoda] ALL_COMMANDS = [Pogoda, News]

105
commands/news.py Normal file
View File

@ -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())

View File

@ -1,5 +1,7 @@
from .stop import stop from .stop import stop
from .news import news
ALL_CONSOLE_COMMANDS = { ALL_CONSOLE_COMMANDS = {
"stop": stop, "stop": stop,
"news": news,
} }

94
console_commands/news.py Normal file
View File

@ -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