Compare commits
No commits in common. "a277865e33e7fb531ec813d85f098e07eae6f890" and "4b02fe8a35a3f82aa45a4258416e76371fbc9893" have entirely different histories.
a277865e33
...
4b02fe8a35
83
AGENTS.md
83
AGENTS.md
@ -1,13 +1,18 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Проект
|
||||
Discord-бот на Python (discord.py)
|
||||
Discord-бот на Python (discord.py) для Магнитогорска.
|
||||
- Команды Discord: `!msg` (повтор текста), `!pogoda` (прогноз погоды)
|
||||
- Консольные команды: `stop` (остановка бота)
|
||||
|
||||
## Запуск
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
python bot.py
|
||||
```
|
||||
|
||||
Введите `stop` в терминал для остановки бота.
|
||||
|
||||
## Токен
|
||||
Токен бота хранится в `.env` (переменная `DISCORD_TOKEN`). Получи токен на [Discord Developer Portal](https://discord.com/developers/applications). Шаблон в `.env.example`.
|
||||
|
||||
@ -20,51 +25,40 @@ python bot.py
|
||||
- **Все git-коммиты согласовывать с пользователем перед созданием.**
|
||||
- **Все сообщения git-коммитов писать на русском языке.**
|
||||
|
||||
## Команды
|
||||
## Архитектура
|
||||
```
|
||||
bot.py # Точка входа, инициализация бота, console_input()
|
||||
commands/ # Discord команды (cogs)
|
||||
__init__.py # ALL_COMMANDS — явные импорты
|
||||
pogoda.py # !pogoda — погода с retry + fallback (wttr.in → Open-Meteo)
|
||||
console_commands/ # Консольные команды
|
||||
__init__.py # ALL_CONSOLE_COMMANDS — явные импорты
|
||||
stop.py # stop — остановка бота
|
||||
```
|
||||
|
||||
| Команда | Описание | Формат вывода |
|
||||
|---------|----------|---------------|
|
||||
| `!pogoda` | Погода в Магнитогорске | Температура, ощущается, описание, влажность, ветер, давление |
|
||||
| `!news` | Топ-5 статей и топ-5 новостей AI с Habr | Два блока: статьи и новости |
|
||||
| `!morning` | Погода + топ-5 статей + топ-5 новостей + котик | Embed: котик thumbnail, погода, статьи, новости |
|
||||
| `!cat` | Случайный котик | Embed с изображением |
|
||||
| `!msg <текст>` | Повторяет текст | Текст |
|
||||
### Добавление Discord команды
|
||||
1. Создать файл `commands/имя.py` с классом, наследующим `commands.Cog`
|
||||
2. Добавить импорт в `commands/__init__.py`
|
||||
3. Добавить класс в `ALL_COMMANDS`
|
||||
|
||||
## Структура проекта
|
||||
|
||||
| Каталог | Назначение |
|
||||
|---------|------------|
|
||||
| `commands/` | Discord-команды (Cogs) |
|
||||
| `utils/` | Утилиты (API-клиенты, конвертации) |
|
||||
| `console_commands/` | Консольные команды (для терминала) |
|
||||
| `bot.py` | Точка входа, загрузка Cogs, обработка ошибок |
|
||||
### Добавление консольной команды
|
||||
1. Создать файл `console_commands/имя.py` с функцией `func(stop_event, bot)`
|
||||
2. Добавить импорт в `console_commands/__init__.py`
|
||||
3. Добавить функцию в `ALL_CONSOLE_COMMANDS`
|
||||
|
||||
## API и внешние сервисы
|
||||
|
||||
### Погода (!pogoda, !morning)
|
||||
- **Основной**: `wttr.in/Magnitogorsk` (бесплатный, без ключа)
|
||||
### Погода (!pogoda)
|
||||
- **Основной**: `wttr.in` (бесплатный, без ключа)
|
||||
- **Fallback**: `api.open-meteo.com` (бесплатный, без ключа)
|
||||
- Retry: 3 попытки с экспоненциальной задержкой при SSL/Connection/Timeout ошибках
|
||||
- Fallback срабатывает автоматически при неуспешных попытках
|
||||
- WMO weather codes → русский перевод в `wmo_to_russian()`
|
||||
- WMO weather codes → русский перевод в `_wmo_to_russian()`
|
||||
|
||||
### Конвертации
|
||||
- Давление: hPa → мм рт. ст. (`* 0.750062`)
|
||||
- Ветер: км/ч → м/с (`/ 3.6`)
|
||||
- Погодные описания: английский → русский (`translate_weather()`)
|
||||
|
||||
### RSS-ленты Habr (!news, !morning)
|
||||
| Тип | URL |
|
||||
|-----|-----|
|
||||
| Статьи | `https://habr.com/ru/rss/hubs/artificial_intelligence/articles/top/daily/?fl=ru` |
|
||||
| Новости | `https://habr.com/ru/rss/hubs/artificial_intelligence/news/top/daily/?fl=ru` |
|
||||
|
||||
Парсятся RSS 2.0 / Atom, извлекаются title, pubDate, creator, category, guid.
|
||||
|
||||
### Cat API (!cat, !morning)
|
||||
- **URL**: `https://api.thecatapi.com/v1/images/search`
|
||||
- Возвращает JSON: `[{"url": "..."}]`
|
||||
- Используется для случайных изображений котиков
|
||||
- Погодные описания: английский → русский (`_translate_weather()`)
|
||||
|
||||
## Конфигурация
|
||||
| Переменная | Описание | Где взять |
|
||||
@ -81,24 +75,13 @@ requests>=2.31.0
|
||||
## Структура данных погоды
|
||||
Команда `!pogoda` возвращает:
|
||||
```
|
||||
Температура: X°C (ощущается как Y°C)
|
||||
Описание: Z
|
||||
Влажность: X%
|
||||
Ветер: X м/с
|
||||
Давление: X мм рт. ст.
|
||||
[TEMP] Температура: X°C (ощущается как Y°C)
|
||||
[DESC] Описание: Z
|
||||
[HUMID] Влажность: X%
|
||||
[WIND] Ветер: X м/с
|
||||
[PRESS] Давление: X мм рт. ст.
|
||||
```
|
||||
|
||||
## Консольные команды
|
||||
Запускаются через терминал при старте бота. Список выводится при запуске.
|
||||
|
||||
| Команда | Описание |
|
||||
|---------|----------|
|
||||
| `pogoda` | Вывести погоду в Магнитогорск |
|
||||
| `news` | Вывести топ-5 статей AI с Habr |
|
||||
| `morning` | Вывести погоду, топ-5 статей и котик |
|
||||
| `cat` | Заглушка |
|
||||
| `stop` | Остановить бота |
|
||||
|
||||
## Безопасность
|
||||
- `.env` в `.gitignore` — токен никогда не должен попадать в репозиторий.
|
||||
- Используй `.env.example` как шаблон.
|
||||
|
||||
84
ISSUES.md
84
ISSUES.md
@ -1,4 +1,4 @@
|
||||
# Tasks — список задач проекта
|
||||
# Issues — список проблем проекта
|
||||
|
||||
> Состояние на: 2026-05-26
|
||||
|
||||
@ -6,41 +6,75 @@
|
||||
|
||||
## 🔴 Критические
|
||||
|
||||
### 1. Убрать эмодзи из `console_commands/cat.py`
|
||||
- [x] Заменить `print("🐱 тут должен быть котик")` на `print("Заглушка: тут должен быть котик")`
|
||||
### 1. Retry без задержки между попытками ✅ РЕШЕНО
|
||||
- **Где:** `commands/pogoda.py`, `console_commands/pogoda.py`
|
||||
- **Проблема:** Цикл `for attempt in range(3)` не содержит `time.sleep()`. Все 3 попытки выполняются мгновенно, что бессмысленно при временных сетевых сбоях.
|
||||
- **AGENTS.md обещает:** "Retry: 3 попытки с экспоненциальной задержкой" — но задержки нет.
|
||||
- **Решение:** Добавить `time.sleep(2 ** attempt)` внутри цикла retry.
|
||||
- **Статус:** Исправлено. Добавлена экспоненциальная задержка: 2 сек, 4 сек между попытками.
|
||||
|
||||
### 2. Блокировка event loop в async-методе ✅ РЕШЕНО
|
||||
- **Где:** `commands/news.py`, `commands/cat.py`, `commands/pogoda.py`
|
||||
- **Проблема:** Асинхронные методы вызывали синхронный `requests.get()`, блокируя весь event loop.
|
||||
- **Решение:** Обернуть `requests.get()` в `await asyncio.to_thread()` и заменить `time.sleep()` на `await asyncio.sleep()`.
|
||||
- **Статус:** Исправлено. В `news.py`, `cat.py`, `pogoda.py` все HTTP-запросы выполняются в отдельных потоках, а ожидания — неблокирующие.
|
||||
|
||||
---
|
||||
|
||||
## 🟡 Средние
|
||||
|
||||
### 2. Убрать пустой embed в `!morning` при всех ошибках
|
||||
- [ ] `commands/morning.py`: добавить fallback-сообщение в `description_lines`, если все API вернули `None`
|
||||
- [ ] Проверить, что `embed.description` никогда не пустой
|
||||
### 3. Дублирование кода между Discord и console командами ✅ РЕШЕНО
|
||||
- **Где:** `console_commands/pogoda.py`, `console_commands/news.py` (и их аналоги в `commands/`)
|
||||
- **Проблема:** Логика погоды и новостей полностью продублирована. Изменения нужно вносить в два места.
|
||||
- **Решение:** Вынести общую логику в `utils/` (`utils/pogoda.py`, `utils/news.py`) и использовать её из обоих мест.
|
||||
- **Статус:** Исправлено. Созданы `utils/pogoda.py` и `utils/news.py`. Оба файла используют единые функции без дублирования.
|
||||
|
||||
### 3. Убрать дублирование парсинга погоды
|
||||
- [ ] `console_commands/pogoda.py`: вынести форматирование результата в `utils/pogoda.py` как `format_weather_data()`
|
||||
- [ ] `console_commands/morning.py`: использовать `format_weather_data()` вместо inline-парсинга
|
||||
- [ ] `commands/morning.py`: использовать `format_weather_data()` вместо inline-парсинга
|
||||
### 4. `from datetime import datetime` внутри метода
|
||||
- **Где:** `commands/news.py` → `_format_and_send()`, строки 56 и 72
|
||||
- **Проблема:** Импорт внутри метода — антипаттерн. Замедляет выполнение, нарушает PEP 8.
|
||||
- **Решение:** Перенести `from datetime import datetime` в начало файла.
|
||||
|
||||
### 6. Вынести `self.api_url` в константу класса
|
||||
- [ ] `commands/pogoda.py`: заменить `self.api_url = ...` на `API_URL = "..."` на уровне класса
|
||||
- [ ] `commands/morning.py`: аналогично
|
||||
- [ ] `console_commands/pogoda.py`: вынести URL в `utils/pogoda.py` как константу
|
||||
### 5. Консольная команда `cat` — заглушка
|
||||
- **Где:** `console_commands/cat.py`
|
||||
- **Проблема:** Функция просто печатает `"🐱 тут должен быть котик"`. Заглушка видна пользователю.
|
||||
- **Решение:** Либо реализовать (вызвать API котиков), либо убрать из `ALL_CONSOLE_COMMANDS` и `README.md`.
|
||||
|
||||
### 6. `on_command_error` не сообщает пользователю об ошибке
|
||||
- **Где:** `bot.py` → `on_command_error()`
|
||||
- **Проблема:** Ошибки команд просто печатаются в `stdout`. Пользователь в чате не видит, что команда выполнилась с ошибкой.
|
||||
- **Решение:** Добавить `await ctx.send("Произошла ошибка при выполнении команды.")` или отправить embed с деталями (если `ctx` не None).
|
||||
|
||||
### 7. Нет `.gitignore` ✅ РЕШЕНО
|
||||
- **Где:** проект
|
||||
- **Проблема:** Отсутствовал `.gitignore`, `.env` мог случайно попасть в репозиторий.
|
||||
- **Решение:** Создать `.gitignore` с правилами для Python (`__pycache__/`, `*.pyc`, `.env`, `venv/`, `*.egg-info/`), логов, временных файлов, кэша ОС и IDE.
|
||||
- **Статус:** Исправлено. `.gitignore` создан и обновлён.
|
||||
|
||||
### 8. `requests` без Session — нет переиспользования соединений
|
||||
- **Где:** все файлы (`pogoda.py`, `news.py`)
|
||||
- **Проблема:** Каждый `requests.get()` создаёт новое TCP-соединение. Для частых запросов это расточительно.
|
||||
- **Решение:** Создать `requests.Session()` и использовать `session.get()`.
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Малые улучшения
|
||||
|
||||
### 4. Вынести `api_url` в константу
|
||||
- [ ] `commands/pogoda.py`: заменить `self.api_url = ...` на `API_URL = "..."` на уровне класса
|
||||
- [ ] `commands/morning.py`: аналогично
|
||||
- [ ] `console_commands/pogoda.py`: вынести URL в `utils/pogoda.py` как константу
|
||||
### 9. Linear search в `_translate_weather`
|
||||
- **Где:** `commands/pogoda.py`, `console_commands/pogoda.py`
|
||||
- **Проблема:** Метод `_translate_weather()` перебирает весь словарь `mapping` циклом `for key, value in mapping.items()`. При большом словаре — неэффективно.
|
||||
- **Решение:** Сортировать ключи по длине (убывание) и использовать `re` или `any(key.lower() in text.lower() for key in sorted_keys)`.
|
||||
|
||||
### 5. Добавить `__all__` в `__init__.py`
|
||||
- [ ] `commands/__init__.py`: `__all__ = ["ALL_COMMANDS"]`
|
||||
- [ ] `console_commands/__init__.py`: `__all__ = ["ALL_CONSOLE_COMMANDS"]`
|
||||
- [ ] `utils/__init__.py`: добавить `__all__` с основными функциями
|
||||
### 10. Нет `__all__` в `__init__.py`
|
||||
- **Где:** `commands/__init__.py`, `console_commands/__init__.py`
|
||||
- **Проблема:** Нет явного экспорта публичного API модулей.
|
||||
- **Решение:** Добавить `__all__ = ["ALL_COMMANDS"]` / `__all__ = ["ALL_CONSOLE_COMMANDS"]`.
|
||||
|
||||
### 6. Добавить `raise_exception=True` в `bot.run()`
|
||||
- [ ] `bot.py`: добавить `raise_exception=True` для видимого падения при ошибках токена/сети
|
||||
- [ ] Добавить логирование в `KeyboardInterrupt` блок
|
||||
### 11. Жёстко заданные URL в разных файлах
|
||||
- **Где:** `commands/news.py` и `console_commands/news.py`
|
||||
- **Проблема:** Константы `RSS_URL_ARTICLES` и `RSS_URL_POSTS` продублированы.
|
||||
- **Решение:** Вынести в `config.py` и импортировать оттуда.
|
||||
|
||||
### 12. Потенциальная гонка при остановке бота
|
||||
- **Где:** `bot.py`
|
||||
- **Проблема:** `stop_event.set()` и `bot.close()` вызываются из `console_input()`, `KeyboardInterrupt` и `console_commands/stop.py`. Возможна гонка.
|
||||
- **Решение:** Добавить флаг `is_stopping` или использовать `asyncio.Lock`.
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Discord Bot
|
||||
# Discord Bot — Магнитогорск
|
||||
|
||||
Discord-бот для Магнитогорска. Команды погоды, новостей и развлечений.
|
||||
|
||||
|
||||
35
bot.py
35
bot.py
@ -1,6 +1,5 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
@ -13,8 +12,6 @@ from dotenv import load_dotenv
|
||||
from commands import ALL_COMMANDS
|
||||
from console_commands import ALL_CONSOLE_COMMANDS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
load_dotenv()
|
||||
|
||||
intents = discord.Intents.default()
|
||||
@ -40,21 +37,7 @@ async def on_ready():
|
||||
async def on_command_error(ctx, error):
|
||||
if isinstance(error, CommandNotFound):
|
||||
return
|
||||
|
||||
# Терминал — детали для разработчика
|
||||
logger.error(
|
||||
f"Ошибка команды {ctx.command.name if ctx else '?'}: {error}",
|
||||
exc_info=True,
|
||||
)
|
||||
|
||||
# Discord — только если команда не ответила сама
|
||||
if ctx and ctx.interaction and ctx.interaction.response.is_done():
|
||||
return
|
||||
|
||||
try:
|
||||
await ctx.send("Не удалось выполнить команду. Попробуйте позже.")
|
||||
except (discord.NotFound, discord.Forbidden):
|
||||
pass # Бот не может писать в канал — игнорируем
|
||||
print(f"Ошибка команды: {error}")
|
||||
|
||||
|
||||
@bot.command(name="msg")
|
||||
@ -63,29 +46,22 @@ async def msg(ctx, *, text: str):
|
||||
await ctx.send(text)
|
||||
|
||||
|
||||
def _print_commands():
|
||||
"""Вывести список доступных консольных команд."""
|
||||
def console_input():
|
||||
bot_ready.wait()
|
||||
print("Доступные команды:")
|
||||
available = {k: v for k, v in ALL_CONSOLE_COMMANDS.items() if k != "stop"}
|
||||
print("\nДоступные команды:")
|
||||
for idx, (name, func) in enumerate(available.items(), 1):
|
||||
print(f" {idx}. {name}")
|
||||
print(" 0. stop")
|
||||
|
||||
|
||||
def console_input():
|
||||
bot_ready.wait()
|
||||
_print_commands()
|
||||
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
choice = input("\nВыберите команду (номер): ").strip()
|
||||
if choice == "0":
|
||||
print("\nОстановка бота...")
|
||||
stop_event.set()
|
||||
asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result(timeout=5)
|
||||
break
|
||||
try:
|
||||
available = {k: v for k, v in ALL_CONSOLE_COMMANDS.items() if k != "stop"}
|
||||
idx = int(choice)
|
||||
if 0 < idx <= len(available):
|
||||
cmd_name = list(available.keys())[idx - 1]
|
||||
@ -97,8 +73,7 @@ def console_input():
|
||||
else:
|
||||
print(f"Неизвестная команда: {choice}")
|
||||
except (ValueError, IndexError):
|
||||
print(f"Неверный формат: {choice}")
|
||||
_print_commands()
|
||||
print(f"Неизвестная команда: {choice}")
|
||||
except (EOFError, KeyboardInterrupt):
|
||||
stop_event.set()
|
||||
try:
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
from .pogoda import Pogoda
|
||||
from .news import News
|
||||
from .cat import Cat
|
||||
from .morning import Morning
|
||||
|
||||
ALL_COMMANDS = [Pogoda, News, Cat, Morning]
|
||||
ALL_COMMANDS = [Pogoda, News, Cat]
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
|
||||
from utils.cat import fetch_cat
|
||||
import asyncio
|
||||
import requests
|
||||
|
||||
|
||||
class Cat(commands.Cog):
|
||||
@ -10,14 +10,21 @@ class Cat(commands.Cog):
|
||||
@commands.command(name="cat")
|
||||
async def cat(self, ctx):
|
||||
"""Получить случайного котика"""
|
||||
url = await fetch_cat()
|
||||
if url is None:
|
||||
await ctx.send("Не удалось получить котика. Попробуйте позже.")
|
||||
return
|
||||
try:
|
||||
response = await asyncio.to_thread(
|
||||
requests.get,
|
||||
"https://api.thecatapi.com/v1/images/search",
|
||||
timeout=10
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
url = data[0]["url"]
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Котик для тебя!",
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
embed.set_image(url=url)
|
||||
await ctx.send(embed=embed)
|
||||
embed = discord.Embed(
|
||||
title="🐱 Котик для тебя!",
|
||||
color=discord.Color.orange()
|
||||
)
|
||||
embed.set_image(url=url)
|
||||
await ctx.send(embed=embed)
|
||||
except requests.RequestException:
|
||||
await ctx.send("Не удалось получить котика. Попробуйте позже.")
|
||||
|
||||
@ -1,95 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
import discord
|
||||
from discord.ext import commands
|
||||
from utils.pogoda import fetch_weather, pressure_to_mmhg, translate_weather
|
||||
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||
from utils.cat import fetch_cat
|
||||
|
||||
|
||||
class Morning(commands.Cog):
|
||||
"""Команда !morning — погода и новости утром"""
|
||||
|
||||
def __init__(self):
|
||||
self.api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
|
||||
|
||||
@commands.command(name="morning")
|
||||
async def morning(self, ctx):
|
||||
"""Погода, лучшие статьи за сутки и котик"""
|
||||
# Параллельный запрос погоды, новостей и котика
|
||||
weather_data, articles, posts, cat_url = await asyncio.gather(
|
||||
fetch_weather(self.api_url),
|
||||
fetch_rss(RSS_URL_ARTICLES),
|
||||
fetch_rss(RSS_URL_POSTS),
|
||||
fetch_cat(),
|
||||
)
|
||||
|
||||
# --- Формируем embed ---
|
||||
embed = discord.Embed(title="Доброе утро!", color=0xF4A460)
|
||||
|
||||
# Котик как thumbnail (маленький в углу)
|
||||
if cat_url:
|
||||
embed.set_thumbnail(url=cat_url)
|
||||
|
||||
description_lines = []
|
||||
|
||||
# --- Погода ---
|
||||
if weather_data is not None:
|
||||
current = weather_data.get("current_condition", [{}])[0]
|
||||
if current:
|
||||
temp = current.get("temp_C", "\u2014")
|
||||
feels_like = current.get("FeelsLikeC", "\u2014")
|
||||
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "\u2014"))
|
||||
humidity = current.get("humidity", "\u2014")
|
||||
wind_kmh = current.get("windspeedKmph", "\u2014")
|
||||
try:
|
||||
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "\u2014" else "\u2014"
|
||||
except (ValueError, TypeError):
|
||||
wind = "\u2014"
|
||||
pressure_mb = current.get("pressure", "\u2014")
|
||||
pressure_mm = pressure_to_mmhg(pressure_mb)
|
||||
|
||||
description_lines.append(
|
||||
f"**Погода в Магнитогорске:**\n"
|
||||
f"Температура: {temp}\u00b0C (ощущается как {feels_like}\u00b0C)\n"
|
||||
f"Описание: {description}\n"
|
||||
f"Влажность: {humidity}%\n"
|
||||
f"Ветер: {wind} м/с\n"
|
||||
f"Давление: {pressure_mm} мм рт. ст."
|
||||
)
|
||||
else:
|
||||
description_lines.append("Не удалось получить данные о погоде.")
|
||||
else:
|
||||
description_lines.append("Не удалось получить данные о погоде.")
|
||||
|
||||
description_lines.append("") # пустая строка-разделитель
|
||||
|
||||
# --- Новости: статьи ---
|
||||
if articles is not None:
|
||||
if articles:
|
||||
lines = format_articles(articles,
|
||||
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||
description_lines.append("\n".join(lines))
|
||||
else:
|
||||
description_lines.append("Новостей пока нет.")
|
||||
else:
|
||||
description_lines.append("Не удалось получить новости.")
|
||||
|
||||
description_lines.append("") # пустая строка-разделитель
|
||||
|
||||
# --- Новости: посты ---
|
||||
if posts is not None:
|
||||
if posts:
|
||||
lines = format_articles(posts,
|
||||
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
|
||||
description_lines.append("\n".join(lines))
|
||||
else:
|
||||
description_lines.append("Новостей пока нет.")
|
||||
else:
|
||||
description_lines.append("Не удалось получить новости.")
|
||||
|
||||
embed.description = "\n".join(description_lines)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
@ -8,7 +8,7 @@ class News(commands.Cog):
|
||||
|
||||
@commands.command(name="news")
|
||||
async def news(self, ctx):
|
||||
"""Топ-5 свежих статей и новостей по AI с Habr"""
|
||||
"""Топ-5 свежих статей по AI с Habr"""
|
||||
articles = await fetch_rss(RSS_URL_ARTICLES)
|
||||
if articles is None:
|
||||
await ctx.send("Не удалось получить новости. Попробуйте позже.")
|
||||
@ -18,43 +18,14 @@ class News(commands.Cog):
|
||||
await ctx.send("Новостей пока нет.")
|
||||
return
|
||||
|
||||
articles_text = format_articles(articles,
|
||||
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||
lines = format_articles(articles, "Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||
|
||||
posts = await fetch_rss(RSS_URL_POSTS)
|
||||
if posts:
|
||||
lines.append("")
|
||||
lines.extend(format_articles(posts, "Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/"))
|
||||
|
||||
embed = discord.Embed(
|
||||
title="Новости AI с Habr",
|
||||
colour=discord.Color.orange(),
|
||||
)
|
||||
|
||||
embed.add_field(
|
||||
name="Статьи",
|
||||
value="\n".join(articles_text),
|
||||
inline=False,
|
||||
)
|
||||
|
||||
if posts is None:
|
||||
embed.add_field(
|
||||
name="Новости",
|
||||
value="Не удалось получить новости.",
|
||||
inline=False,
|
||||
)
|
||||
elif posts:
|
||||
posts_text = format_articles(posts,
|
||||
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
|
||||
embed.add_field(
|
||||
name="Новости",
|
||||
value="\n".join(posts_text),
|
||||
inline=False,
|
||||
)
|
||||
else:
|
||||
embed.add_field(
|
||||
name="Новости",
|
||||
value="Новостей пока нет.",
|
||||
inline=False,
|
||||
)
|
||||
|
||||
await ctx.send(embed=embed)
|
||||
message = "\n".join(lines).rstrip()
|
||||
await ctx.send(message, allowed_mentions=discord.AllowedMentions.none())
|
||||
|
||||
@ -34,9 +34,9 @@ class Pogoda(commands.Cog):
|
||||
pressure_mm = pressure_to_mmhg(pressure_mb)
|
||||
|
||||
await ctx.send(
|
||||
f"Температура: {temp}°C (ощущается как {feels_like}°C)\n"
|
||||
f"Описание: {description}\n"
|
||||
f"Влажность: {humidity}%\n"
|
||||
f"Ветер: {wind} м/с\n"
|
||||
f"Давление: {pressure_mm} мм рт. ст."
|
||||
f"[TEMP] Температура: {temp}°C (ощущается как {feels_like}°C)\n"
|
||||
f"[DESC] Описание: {description}\n"
|
||||
f"[HUMID] Влажность: {humidity}%\n"
|
||||
f"[WIND] Ветер: {wind} м/с\n"
|
||||
f"[PRESS] Давление: {pressure_mm} мм рт. ст."
|
||||
)
|
||||
|
||||
@ -2,12 +2,10 @@ from .stop import stop
|
||||
from .news import news
|
||||
from .cat import cat
|
||||
from .pogoda import pogoda
|
||||
from .morning import morning
|
||||
|
||||
ALL_CONSOLE_COMMANDS = {
|
||||
"stop": stop,
|
||||
"news": news,
|
||||
"cat": cat,
|
||||
"pogoda": pogoda,
|
||||
"morning": morning,
|
||||
}
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
def cat(stop_event, bot):
|
||||
"""Заглушка: тут должен быть котик"""
|
||||
print("Заглушка: тут должен быть котик")
|
||||
print("🐱 тут должен быть котик")
|
||||
|
||||
@ -1,81 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
from utils.pogoda import fetch_weather, pressure_to_mmhg, translate_weather
|
||||
from utils.news import fetch_rss, format_articles, RSS_URL_ARTICLES, RSS_URL_POSTS
|
||||
from utils.cat import fetch_cat
|
||||
|
||||
|
||||
async def morning(stop_event, bot):
|
||||
"""Вывести погоду, лучшие статьи за сутки и котика"""
|
||||
api_url = "https://wttr.in/Magnitogorsk?format=j1&lang=ru"
|
||||
|
||||
# Параллельный запрос погоды, новостей и котика
|
||||
weather_data, articles, posts, cat_url = await asyncio.gather(
|
||||
fetch_weather(api_url),
|
||||
fetch_rss(RSS_URL_ARTICLES),
|
||||
fetch_rss(RSS_URL_POSTS),
|
||||
fetch_cat(),
|
||||
)
|
||||
|
||||
print("Доброе утро!\n")
|
||||
|
||||
# --- Котик ---
|
||||
if cat_url:
|
||||
print(f"Котик: {cat_url}\n")
|
||||
else:
|
||||
print("Котика получить не удалось.\n")
|
||||
|
||||
# --- Погода ---
|
||||
if weather_data is not None:
|
||||
current = weather_data.get("current_condition", [{}])[0]
|
||||
if current:
|
||||
temp = current.get("temp_C", "—")
|
||||
feels_like = current.get("FeelsLikeC", "—")
|
||||
description = translate_weather(current.get("weatherDesc", [{}])[0].get("value", "—"))
|
||||
humidity = current.get("humidity", "—")
|
||||
wind_kmh = current.get("windspeedKmph", "—")
|
||||
try:
|
||||
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "—" else "—"
|
||||
except (ValueError, TypeError):
|
||||
wind = "—"
|
||||
pressure_mb = current.get("pressure", "—")
|
||||
pressure_mm = pressure_to_mmhg(pressure_mb)
|
||||
|
||||
print(f"**Погода в Магнитогорске:**")
|
||||
print(f"Температура: {temp}°C (ощущается как {feels_like}°C)")
|
||||
print(f"Описание: {description}")
|
||||
print(f"Влажность: {humidity}%")
|
||||
print(f"Ветер: {wind} м/с")
|
||||
print(f"Давление: {pressure_mm} мм рт. ст.")
|
||||
else:
|
||||
print("Не удалось получить данные о погоде.")
|
||||
else:
|
||||
print("Не удалось получить данные о погоде.")
|
||||
|
||||
print()
|
||||
|
||||
# --- Новости: статьи ---
|
||||
if articles is not None:
|
||||
if articles:
|
||||
lines = format_articles(articles,
|
||||
"Лучшие статьи за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/articles/top/daily/")
|
||||
print("\n".join(lines))
|
||||
else:
|
||||
print("Новостей пока нет.")
|
||||
else:
|
||||
print("Не удалось получить новости.")
|
||||
|
||||
print()
|
||||
|
||||
# --- Новости: посты ---
|
||||
if posts is not None:
|
||||
if posts:
|
||||
lines = format_articles(posts,
|
||||
"Лучшие новости за сутки / Искусственный интеллект / Хабr",
|
||||
"https://habr.com/ru/hubs/artificial_intelligence/news/top/daily/")
|
||||
print("\n".join(lines))
|
||||
else:
|
||||
print("Новостей пока нет.")
|
||||
else:
|
||||
print("Не удалось получить новости.")
|
||||
@ -27,8 +27,8 @@ async def pogoda(stop_event, bot):
|
||||
pressure_mb = current.get("pressure", "—")
|
||||
pressure_mm = pressure_to_mmhg(pressure_mb)
|
||||
|
||||
print(f"Температура: {temp}°C (ощущается как {feels_like}°C)")
|
||||
print(f"Описание: {description}")
|
||||
print(f"Влажность: {humidity}%")
|
||||
print(f"Ветер: {wind} м/с")
|
||||
print(f"Давление: {pressure_mm} мм рт. ст.")
|
||||
print(f"[TEMP] Температура: {temp}°C (ощущается как {feels_like}°C)")
|
||||
print(f"[DESC] Описание: {description}")
|
||||
print(f"[HUMID] Влажность: {humidity}%")
|
||||
print(f"[WIND] Ветер: {wind} м/с")
|
||||
print(f"[PRESS] Давление: {pressure_mm} мм рт. ст.")
|
||||
|
||||
18
utils/cat.py
18
utils/cat.py
@ -1,18 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
import requests
|
||||
|
||||
CAT_API_URL = "https://api.thecatapi.com/v1/images/search"
|
||||
|
||||
_session = requests.Session()
|
||||
|
||||
|
||||
async def fetch_cat() -> str | None:
|
||||
"""Получить URL случайного котика. Вернуть None при ошибке."""
|
||||
try:
|
||||
response = await asyncio.to_thread(_session.get, CAT_API_URL, timeout=10)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data[0]["url"]
|
||||
except requests.exceptions.RequestException:
|
||||
return None
|
||||
@ -1,20 +1,15 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
import requests
|
||||
|
||||
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"
|
||||
|
||||
_session = requests.Session()
|
||||
|
||||
|
||||
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(_session.get, url, timeout=10)
|
||||
response = await asyncio.to_thread(requests.get, url, timeout=10)
|
||||
response.raise_for_status()
|
||||
root = ElementTree.fromstring(response.content)
|
||||
|
||||
@ -63,12 +58,13 @@ async def fetch_rss(url):
|
||||
"tags": tags,
|
||||
})
|
||||
return articles[:10]
|
||||
except requests.exceptions.RequestException:
|
||||
except requests.RequestException:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_date(pub_date):
|
||||
"""Парсить дату из RSS в строку 'дд.мм.гггг' или вернуть часть даты."""
|
||||
from datetime import datetime
|
||||
if not pub_date:
|
||||
return ""
|
||||
try:
|
||||
|
||||
@ -2,14 +2,12 @@ import asyncio
|
||||
import requests
|
||||
from requests.exceptions import ConnectionError, Timeout, SSLError
|
||||
|
||||
_session = requests.Session()
|
||||
|
||||
|
||||
async def fetch_weather(api_url, timeout=10, max_retries=3):
|
||||
"""Получить данные о погоде с retry."""
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = await asyncio.to_thread(_session.get, api_url, timeout=timeout)
|
||||
response = await asyncio.to_thread(requests.get, api_url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except (SSLError, ConnectionError, Timeout):
|
||||
@ -37,7 +35,7 @@ async def fetch_open_meteo(lat=53.4069, lon=58.9797, timeout=10, max_retries=3):
|
||||
)
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = await asyncio.to_thread(_session.get, url, timeout=timeout)
|
||||
response = await asyncio.to_thread(requests.get, url, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
current = data.get("current", {})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user