fix: resolve issue #3 (deduplication) and fix coroutine handling
- Create utils/news.py with shared RSS parsing and formatting logic - Refactor commands/news.py and console_commands/news.py to use utils/news.py - Fix bot.py to handle async console commands (news, pogoda) - Fix utils/pogoda.py to fall back to Open-Meteo on requests.RequestException - Mark issue #3 as resolved in ISSUES.md
This commit is contained in:
parent
e1a0f6d2b6
commit
4a40f705d4
@ -23,10 +23,11 @@
|
|||||||
|
|
||||||
## 🟡 Средние
|
## 🟡 Средние
|
||||||
|
|
||||||
### 3. Дублирование кода между Discord и console командами
|
### 3. Дублирование кода между Discord и console командами ✅ РЕШЕНО
|
||||||
- **Где:** `console_commands/pogoda.py` и `console_commands/news.py`
|
- **Где:** `console_commands/pogoda.py`, `console_commands/news.py` (и их аналоги в `commands/`)
|
||||||
- **Проблема:** Логика погоды и новостей полностью продублирована. Изменения нужно вносить в два места.
|
- **Проблема:** Логика погоды и новостей полностью продублирована. Изменения нужно вносить в два места.
|
||||||
- **Решение:** Вынести общую логику в `utils/` (например, `utils/weather.py`, `utils/rss.py`) и использовать её из обоих мест.
|
- **Решение:** Вынести общую логику в `utils/` (`utils/pogoda.py`, `utils/news.py`) и использовать её из обоих мест.
|
||||||
|
- **Статус:** Исправлено. Созданы `utils/pogoda.py` и `utils/news.py`. Оба файла используют единые функции без дублирования.
|
||||||
|
|
||||||
### 4. `from datetime import datetime` внутри метода
|
### 4. `from datetime import datetime` внутри метода
|
||||||
- **Где:** `commands/news.py` → `_format_and_send()`, строки 56 и 72
|
- **Где:** `commands/news.py` → `_format_and_send()`, строки 56 и 72
|
||||||
|
|||||||
7
bot.py
7
bot.py
@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import inspect
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@ -64,7 +65,11 @@ def console_input():
|
|||||||
idx = int(choice)
|
idx = int(choice)
|
||||||
if 0 < idx <= len(available):
|
if 0 < idx <= len(available):
|
||||||
cmd_name = list(available.keys())[idx - 1]
|
cmd_name = list(available.keys())[idx - 1]
|
||||||
ALL_CONSOLE_COMMANDS[cmd_name](stop_event, bot)
|
cmd_func = ALL_CONSOLE_COMMANDS[cmd_name]
|
||||||
|
if inspect.iscoroutinefunction(cmd_func):
|
||||||
|
asyncio.run_coroutine_threadsafe(cmd_func(stop_event, bot), bot.loop).result()
|
||||||
|
else:
|
||||||
|
cmd_func(stop_event, bot)
|
||||||
else:
|
else:
|
||||||
print(f"Неизвестная команда: {choice}")
|
print(f"Неизвестная команда: {choice}")
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
|
|||||||
106
commands/news.py
106
commands/news.py
@ -1,12 +1,6 @@
|
|||||||
import discord
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
import asyncio
|
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||||
import requests
|
|
||||||
from xml.etree import ElementTree
|
|
||||||
|
|
||||||
|
|
||||||
RSS_URL_ARTICLES = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru"
|
|
||||||
RSS_URL_POSTS = "https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru"
|
|
||||||
|
|
||||||
|
|
||||||
class News(commands.Cog):
|
class News(commands.Cog):
|
||||||
@ -15,7 +9,7 @@ class News(commands.Cog):
|
|||||||
@commands.command(name="news")
|
@commands.command(name="news")
|
||||||
async def news(self, ctx):
|
async def news(self, ctx):
|
||||||
"""Топ-5 свежих статей по AI с Habr"""
|
"""Топ-5 свежих статей по AI с Habr"""
|
||||||
articles = await self._fetch_rss(RSS_URL_ARTICLES)
|
articles = await fetch_rss(RSS_URL_ARTICLES)
|
||||||
if articles is None:
|
if articles is None:
|
||||||
await ctx.send("Не удалось получить новости. Попробуйте позже.")
|
await ctx.send("Не удалось получить новости. Попробуйте позже.")
|
||||||
return
|
return
|
||||||
@ -24,100 +18,14 @@ class News(commands.Cog):
|
|||||||
await ctx.send("Новостей пока нет.")
|
await ctx.send("Новостей пока нет.")
|
||||||
return
|
return
|
||||||
|
|
||||||
await self._format_and_send(ctx, articles)
|
lines = format_articles(articles, "Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||||
|
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||||
|
|
||||||
async def _fetch_rss(self, url):
|
posts = await fetch_rss(RSS_URL_POSTS)
|
||||||
"""Скачать и распарсить RSS-ленту (RSS 2.0 / Atom)."""
|
|
||||||
try:
|
|
||||||
response = await asyncio.to_thread(requests.get, 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 = ["**Лучшие статьи за сутки / Искусственный интеллект / Хабr**\n<https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/>\n"]
|
|
||||||
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("-", ".")
|
|
||||||
title = article["title"]
|
|
||||||
if len(title) > 60:
|
|
||||||
title = title[:60] + "..."
|
|
||||||
lines.append(f"{title}\n{date_str} <{article['link']}>")
|
|
||||||
|
|
||||||
# Второй блок: посты
|
|
||||||
posts = await self._fetch_rss(RSS_URL_POSTS)
|
|
||||||
if posts:
|
if posts:
|
||||||
lines.append("")
|
lines.append("")
|
||||||
lines.append("**Лучшие новости за сутки / Искусственный интеллект / Хабr**\n<https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/>\n")
|
lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||||
for i, article in enumerate(posts[:5], 1):
|
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/"))
|
||||||
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("-", ".")
|
|
||||||
title = article["title"]
|
|
||||||
if len(title) > 60:
|
|
||||||
title = title[:60] + "..."
|
|
||||||
lines.append(f"{title}\n{date_str} <{article['link']}>")
|
|
||||||
|
|
||||||
message = "\n".join(lines).rstrip()
|
message = "\n".join(lines).rstrip()
|
||||||
await ctx.send(message, allowed_mentions=discord.AllowedMentions.none())
|
await ctx.send(message, allowed_mentions=discord.AllowedMentions.none())
|
||||||
|
|||||||
@ -1,14 +1,9 @@
|
|||||||
import requests
|
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||||
from xml.etree import ElementTree
|
|
||||||
|
|
||||||
|
|
||||||
RSS_URL_ARTICLES = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru"
|
async def news(stop_event, bot):
|
||||||
RSS_URL_POSTS = "https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru"
|
|
||||||
|
|
||||||
|
|
||||||
def news(stop_event, bot):
|
|
||||||
"""Вывести топ-5 свежих статей по AI с Habr"""
|
"""Вывести топ-5 свежих статей по AI с Habr"""
|
||||||
articles = _fetch_rss(RSS_URL_ARTICLES)
|
articles = await fetch_rss(RSS_URL_ARTICLES)
|
||||||
if articles is None:
|
if articles is None:
|
||||||
print("Не удалось получить новости.")
|
print("Не удалось получить новости.")
|
||||||
return
|
return
|
||||||
@ -17,100 +12,14 @@ def news(stop_event, bot):
|
|||||||
print("Новостей пока нет.")
|
print("Новостей пока нет.")
|
||||||
return
|
return
|
||||||
|
|
||||||
from datetime import datetime
|
lines = format_articles(articles, "Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||||
print("**Лучшие статьи за сутки / Искусственный интеллект / Хабr**")
|
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||||
print("<https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/>")
|
|
||||||
print()
|
|
||||||
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("-", ".")
|
|
||||||
title = article["title"]
|
|
||||||
if len(title) > 60:
|
|
||||||
title = title[:60] + "..."
|
|
||||||
print(f"{title}\n {date_str} {article['link']}")
|
|
||||||
print("────────────────────────────────────────")
|
|
||||||
print()
|
|
||||||
|
|
||||||
# Второй блок: посты
|
posts = await fetch_rss(RSS_URL_POSTS)
|
||||||
posts = _fetch_rss(RSS_URL_POSTS)
|
|
||||||
if posts:
|
if posts:
|
||||||
print("**Лучшие новости за сутки / Искусственный интеллект / Хабr**")
|
lines.append("")
|
||||||
print("<https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/>")
|
lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||||
print()
|
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/"))
|
||||||
for i, article in enumerate(posts[: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("-", ".")
|
|
||||||
title = article["title"]
|
|
||||||
if len(title) > 60:
|
|
||||||
title = title[:60] + "..."
|
|
||||||
print(f"{title}\n {date_str} {article['link']}")
|
|
||||||
print("────────────────────────────────────────")
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
def _fetch_rss(url):
|
print(line)
|
||||||
"""Скачать и распарсить RSS-ленту (RSS 2.0 / Atom)."""
|
|
||||||
try:
|
|
||||||
response = requests.get(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
|
|
||||||
|
|||||||
92
utils/news.py
Normal file
92
utils/news.py
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
RSS_URL_ARTICLES = "https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru"
|
||||||
|
RSS_URL_POSTS = "https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru"
|
||||||
|
|
||||||
|
|
||||||
|
async def fetch_rss(url):
|
||||||
|
"""Скачать и распарсить RSS-ленту (RSS 2.0 / Atom)."""
|
||||||
|
import asyncio
|
||||||
|
import requests
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = await asyncio.to_thread(requests.get, 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
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_date(pub_date):
|
||||||
|
"""Парсить дату из RSS в строку 'дд.мм.гггг' или вернуть часть даты."""
|
||||||
|
from datetime import datetime
|
||||||
|
if not pub_date:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
d = pub_date.replace(" GMT", " +0000")
|
||||||
|
dt = datetime.strptime(d, "%a, %d %b %Y %H:%M:%S %z")
|
||||||
|
return dt.strftime("%d.%m.%Y")
|
||||||
|
except ValueError:
|
||||||
|
return pub_date[:10].replace("-", ".")
|
||||||
|
|
||||||
|
|
||||||
|
def truncate_title(title, max_len=60):
|
||||||
|
"""Обрезать заголовок, если он длиннее max_len."""
|
||||||
|
if len(title) > max_len:
|
||||||
|
return title[:max_len] + "..."
|
||||||
|
return title
|
||||||
|
|
||||||
|
|
||||||
|
def format_articles(articles, title, link):
|
||||||
|
"""Сформировать список строк для вывода статей/постов."""
|
||||||
|
lines = [f"**{title}**\n<{link}>"]
|
||||||
|
for i, article in enumerate(articles[:5], 1):
|
||||||
|
date_str = _parse_date(article["pub_date"])
|
||||||
|
short_title = truncate_title(article["title"])
|
||||||
|
lines.append(f"{short_title}\n{date_str} <{article['link']}>")
|
||||||
|
return lines
|
||||||
@ -19,7 +19,7 @@ async def fetch_weather(api_url, timeout=10, max_retries=3):
|
|||||||
break
|
break
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"Ошибка при получении данных: {e}")
|
print(f"Ошибка при получении данных: {e}")
|
||||||
return None
|
break
|
||||||
|
|
||||||
# Fallback: Open-Meteo API (без ключа, HTTPS)
|
# Fallback: Open-Meteo API (без ключа, HTTPS)
|
||||||
return await fetch_open_meteo()
|
return await fetch_open_meteo()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user