diff --git a/ISSUES.md b/ISSUES.md
index 7fc1c17..2b898bd 100644
--- a/ISSUES.md
+++ b/ISSUES.md
@@ -23,10 +23,11 @@
## 🟡 Средние
-### 3. Дублирование кода между Discord и console командами
-- **Где:** `console_commands/pogoda.py` и `console_commands/news.py`
+### 3. Дублирование кода между Discord и console командами ✅ РЕШЕНО
+- **Где:** `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` внутри метода
- **Где:** `commands/news.py` → `_format_and_send()`, строки 56 и 72
diff --git a/bot.py b/bot.py
index 66066d0..a830998 100644
--- a/bot.py
+++ b/bot.py
@@ -1,4 +1,5 @@
import asyncio
+import inspect
import os
import sys
import threading
@@ -64,7 +65,11 @@ def console_input():
idx = int(choice)
if 0 < idx <= len(available):
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:
print(f"Неизвестная команда: {choice}")
except (ValueError, IndexError):
diff --git a/commands/news.py b/commands/news.py
index 1ae8566..6ddc616 100644
--- a/commands/news.py
+++ b/commands/news.py
@@ -1,12 +1,6 @@
import discord
from discord.ext import commands
-import asyncio
-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"
+from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
class News(commands.Cog):
@@ -15,7 +9,7 @@ class News(commands.Cog):
@commands.command(name="news")
async def news(self, ctx):
"""Топ-5 свежих статей по AI с Habr"""
- articles = await self._fetch_rss(RSS_URL_ARTICLES)
+ articles = await fetch_rss(RSS_URL_ARTICLES)
if articles is None:
await ctx.send("Не удалось получить новости. Попробуйте позже.")
return
@@ -24,100 +18,14 @@ class News(commands.Cog):
await ctx.send("Новостей пока нет.")
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):
- """Скачать и распарсить 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\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)
+ posts = await fetch_rss(RSS_URL_POSTS)
if posts:
lines.append("")
- lines.append("**Лучшие новости за сутки / Искусственный интеллект / Хабr**\n\n")
- for i, article in enumerate(posts[: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']}>")
+ lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr",
+ "https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/"))
message = "\n".join(lines).rstrip()
await ctx.send(message, allowed_mentions=discord.AllowedMentions.none())
diff --git a/console_commands/news.py b/console_commands/news.py
index 11e35ff..acb5261 100644
--- a/console_commands/news.py
+++ b/console_commands/news.py
@@ -1,14 +1,9 @@
-import requests
-from xml.etree import ElementTree
+from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
-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"
-
-
-def news(stop_event, bot):
+async def news(stop_event, bot):
"""Вывести топ-5 свежих статей по AI с Habr"""
- articles = _fetch_rss(RSS_URL_ARTICLES)
+ articles = await fetch_rss(RSS_URL_ARTICLES)
if articles is None:
print("Не удалось получить новости.")
return
@@ -17,100 +12,14 @@ def news(stop_event, bot):
print("Новостей пока нет.")
return
- from datetime import datetime
- print("**Лучшие статьи за сутки / Искусственный интеллект / Хабr**")
- print("")
- 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()
+ lines = format_articles(articles, "Лучшие статьи за сутки / Искусственный интеллект / Хабr",
+ "https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
- # Второй блок: посты
- posts = _fetch_rss(RSS_URL_POSTS)
+ posts = await fetch_rss(RSS_URL_POSTS)
if posts:
- print("**Лучшие новости за сутки / Искусственный интеллект / Хабr**")
- print("")
- print()
- 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()
+ lines.append("")
+ lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr",
+ "https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/"))
-
-def _fetch_rss(url):
- """Скачать и распарсить 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
+ for line in lines:
+ print(line)
diff --git a/utils/news.py b/utils/news.py
new file mode 100644
index 0000000..3395126
--- /dev/null
+++ b/utils/news.py
@@ -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
diff --git a/utils/pogoda.py b/utils/pogoda.py
index 46c413a..33fd56f 100644
--- a/utils/pogoda.py
+++ b/utils/pogoda.py
@@ -19,7 +19,7 @@ async def fetch_weather(api_url, timeout=10, max_retries=3):
break
except requests.RequestException as e:
print(f"Ошибка при получении данных: {e}")
- return None
+ break
# Fallback: Open-Meteo API (без ключа, HTTPS)
return await fetch_open_meteo()