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