fix: исправить дублирование кода, добавить retry и fallback для погоды

- Убран дублирующийся on_command_error и импорт CommandNotFound
- stop.py: добавлены аргументы stop_event, bot + обработка ошибок
- console_commands интегрирован в console_input()
- pogoda.py: retry (3 попытки), fallback на Open-Meteo при SSL-ошибках
- pogoda.py: безопасная обработка wind_kmh и давления
- pogoda.py: сортировка translate-словаря по длине ключа
- Добавлен _wmo_to_russian() для WMO weather code
This commit is contained in:
deadzilla 2026-05-25 00:07:20 +05:00
parent e78d34e3cb
commit 486e3124ed
3 changed files with 146 additions and 46 deletions

25
bot.py
View File

@ -6,10 +6,10 @@ import threading
import discord import discord
from discord.ext import commands from discord.ext import commands
from discord.ext.commands import CommandNotFound from discord.ext.commands import CommandNotFound
from discord.ext.commands import CommandNotFound
from dotenv import load_dotenv from dotenv import load_dotenv
from commands import ALL_COMMANDS from commands import ALL_COMMANDS
from console_commands import ALL_CONSOLE_COMMANDS
load_dotenv() load_dotenv()
@ -37,13 +37,6 @@ async def on_command_error(ctx, error):
print(f"Ошибка команды: {error}") print(f"Ошибка команды: {error}")
@bot.event
async def on_command_error(ctx, error):
if isinstance(error, CommandNotFound):
return
print(f"Ошибка команды: {error}")
@bot.command(name="msg") @bot.command(name="msg")
async def msg(ctx, *, text: str): async def msg(ctx, *, text: str):
"""Повторяет текст после !msg""" """Повторяет текст после !msg"""
@ -56,14 +49,20 @@ def console_input():
cmd = input().strip().lower() cmd = input().strip().lower()
if cmd == "stop": if cmd == "stop":
print("\nОстановка бота...") print("\nОстановка бота...")
stop_event.set() if "stop" in ALL_CONSOLE_COMMANDS:
asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result() ALL_CONSOLE_COMMANDS["stop"](stop_event, bot)
break break
elif cmd: elif cmd:
print(f"Неизвестная команда: {cmd}") if cmd in ALL_CONSOLE_COMMANDS:
except EOFError: ALL_CONSOLE_COMMANDS[cmd](stop_event, bot)
else:
print(f"Неизвестная команда: {cmd}")
except (EOFError, KeyboardInterrupt):
stop_event.set() stop_event.set()
asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result() try:
asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result(timeout=5)
except Exception as e:
print(f"Ошибка при остановке бота: {e}")
break break

View File

@ -1,6 +1,7 @@
import discord import discord
from discord.ext import commands from discord.ext import commands
import requests import requests
from requests.exceptions import ConnectionError, Timeout, SSLError
class Pogoda(commands.Cog): class Pogoda(commands.Cog):
@ -11,12 +12,8 @@ class Pogoda(commands.Cog):
@commands.command(name="pogoda") @commands.command(name="pogoda")
async def pogoda(self, ctx): async def pogoda(self, ctx):
try: data = await self._fetch_weather(ctx)
response = requests.get(self.api_url, timeout=10) if data is None:
response.raise_for_status()
data = response.json()
except requests.RequestException as e:
await ctx.send(f"Ошибка при получении данных: {e}")
return return
current = data.get("current_condition", [{}])[0] current = data.get("current_condition", [{}])[0]
@ -29,7 +26,10 @@ class Pogoda(commands.Cog):
description = self._translate_weather(current.get("weatherDesc", [{}])[0].get("value", "")) description = self._translate_weather(current.get("weatherDesc", [{}])[0].get("value", ""))
humidity = current.get("humidity", "") humidity = current.get("humidity", "")
wind_kmh = current.get("windspeedKmph", "") wind_kmh = current.get("windspeedKmph", "")
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "" else "" try:
wind = round(int(wind_kmh) / 3.6, 1) if wind_kmh != "" else ""
except (ValueError, TypeError):
wind = ""
pressure_mb = current.get("pressure", "") pressure_mb = current.get("pressure", "")
pressure_mm = self._pressure_to_mmhg(pressure_mb) pressure_mm = self._pressure_to_mmhg(pressure_mb)
@ -42,32 +42,123 @@ class Pogoda(commands.Cog):
f"[PRESS] Давление: {pressure_mm} мм рт. ст." f"[PRESS] Давление: {pressure_mm} мм рт. ст."
) )
def _translate_weather(self, en): async def _fetch_weather(self, ctx):
"""Получить данные о погоде с retry и fallback."""
# Пробуем wttr.in с retry
for attempt in range(3):
try:
response = requests.get(self.api_url, timeout=10)
response.raise_for_status()
return response.json()
except (SSLError, ConnectionError, Timeout):
if attempt < 2:
continue
break
except requests.RequestException as e:
await ctx.send(f"Ошибка при получении данных: {e}")
return None
# Fallback: Open-Meteo API (без ключа, HTTPS)
return await self._fetch_open_meteo(ctx)
async def _fetch_open_meteo(self, ctx):
"""Fallback на Open-Meteo API."""
url = (
"https://api.open-meteo.com/v1/forecast?"
"latitude=53.4069&longitude=58.9797&current=temperature,"
"apparent_temperature,weather_code,wind_speed_10m,"
"relative_humidity_2m,pressure_msl&timezone=Asia/Chelyabinsk"
)
for attempt in range(3):
try:
response = requests.get(url, timeout=10)
response.raise_for_status()
data = response.json()
current = data.get("current", {})
# weather_code WMO код → перевод (https://open-meteo.com/en/docs)
weather_code = current.get("weather_code", None)
desc = self._wmo_to_russian(weather_code)
return {
"current_condition": [{
"temp_C": current.get("temperature", ""),
"FeelsLikeC": current.get("apparent_temperature", ""),
"weatherDesc": [{"value": desc}],
"humidity": current.get("relative_humidity_2m", ""),
"windspeedKmph": current.get("wind_speed_10m", ""),
"pressure": current.get("pressure_msl", ""),
}]
}
except (SSLError, ConnectionError, Timeout):
if attempt < 2:
continue
break
except requests.RequestException as e:
await ctx.send(f"Ошибка при получении данных: {e}")
return None
await ctx.send("Не удалось получить данные о погоде. Попробуйте позже.")
return None
def _wmo_to_russian(self, code):
"""Перевод WMO weather code в русский."""
mapping = {
0: "Ясно",
1: "Ясно", 2: "Переменная облачность",
3: "Пасмурно",
45: "Туман", 48: "Туман",
51: "Лёгкая морось", 53: "Морось", 55: "Сильная морось",
56: "Ледяная морось", 57: "Сильная ледяная морось",
61: "Небольшой дождь", 63: "Дождь", 65: "Сильный дождь",
66: "Ледяной дождь", 67: "Сильный ледяной дождь",
71: "Небольшой снег", 73: "Снег", 75: "Сильный снег",
77: "Снежная крупа",
80: "Небольшой ливень", 81: "Ливень", 82: "Сильный ливень",
85: "Снежный ливень", 86: "Сильный снежный ливень",
95: "Гроза", 96: "Гроза с градом", 99: "Сильная гроза с градом",
}
return mapping.get(code, "Неизвестно")
def _translate_weather(self, en):
if not en:
return ""
mapping = { mapping = {
"Sunny": "Ясно",
"Clear": "Ясно",
"Partly cloudy": "Переменная облачность",
"Cloudy": "Облачно",
"Overcast": "Пасмурно",
"Mist": "Туман",
"Fog": "Туман",
"Patchy rain nearby": "Местами дождь",
"Light rain": "Небольшой дождь",
"Moderate rain": "Умеренный дождь",
"Heavy rain": "Сильный дождь",
"Patchy snow nearby": "Местами снег",
"Light snow": "Небольшой снег",
"Heavy snow": "Сильный снег",
"Patchy sleet nearby": "Местами слякоть",
"Blowing snow": "Метель",
"Thundery outbreaks in nearby": "Гроза вблизи",
"Moderate or heavy snow in area": "Снег",
"Moderate or heavy rain in area": "Дождь",
"Moderate or heavy freezing rain in area": "Ледяной дождь", "Moderate or heavy freezing rain in area": "Ледяной дождь",
"Moderate or heavy sleet in area": "Слякоть", "Moderate or heavy sleet in area": "Слякоть",
"Ice pellets": "Ледяные кристаллы", "Moderate or heavy snow in area": "Снег",
"Haze": "Дымка", "Moderate or heavy rain in area": "Дождь",
"Thundery outbreaks in nearby": "Гроза вблизи",
"Patchy rain nearby": "Местами дождь",
"Patchy snow nearby": "Местами снег",
"Patchy sleet nearby": "Местами слякоть",
"Heavy freezing rain": "Сильный ледяной дождь",
"Heavy snow": "Сильный снег",
"Heavy rain": "Сильный дождь",
"Moderate or heavy rain at times": "Дождь",
"Moderate or heavy snow at times": "Снег",
"Blowing snow": "Метель",
"Patchy light drizzle": "Местами лёгкая морось",
"Moderate or heavy freezing rain at a distance": "Ледяной дождь",
"Moderate or heavy sleet at a distance": "Слякоть",
"Light rain shower": "Небольшой дождь",
"Heavy rain shower": "Сильный дождь",
"Moderate rain": "Умеренный дождь",
"Light rain": "Небольшой дождь",
"Moderate rain at times": "Умеренный дождь",
"Heavy rain at times": "Сильный дождь",
"Light snow": "Небольшой снег",
"Moderate snow": "Умеренный снег",
"Patchy light snow": "Местами лёгкий снег",
"Partly cloudy": "Переменная облачность",
"Moderate or light sleet": "Слякоть",
"Light freezing rain": "Лёгкий ледяной дождь",
"Foggy": "Туманно", "Foggy": "Туманно",
"Fog": "Туман",
"Mist": "Туман",
"Haze": "Дымка",
"Overcast": "Пасмурно",
"Cloudy": "Облачно",
"Clear": "Ясно",
"Sunny": "Ясно",
} }
for key, value in mapping.items(): for key, value in mapping.items():
if key.lower() in en.lower(): if key.lower() in en.lower():
@ -75,6 +166,9 @@ class Pogoda(commands.Cog):
return en return en
def _pressure_to_mmhg(self, mb): def _pressure_to_mmhg(self, mb):
if mb == "": if mb == "" or not mb:
return ""
try:
return round(float(mb) * 0.750062, 1)
except (ValueError, TypeError):
return "" return ""
return round(int(mb) * 0.750062, 1)

View File

@ -1,3 +1,10 @@
def stop(stop_event): import asyncio
def stop(stop_event, bot):
"""Остановка бота""" """Остановка бота"""
pass stop_event.set()
try:
asyncio.run_coroutine_threadsafe(bot.close(), bot.loop).result(timeout=5)
except Exception as e:
print(f"Ошибка при остановке бота: {e}")