Обновить bot-tg.py

This commit is contained in:
yogurtmenn 2025-03-25 10:18:53 +00:00
parent d996844726
commit d979c105c6

255
bot-tg.py
View file

@ -1,122 +1,189 @@
import logging import logging
from logging.handlers import RotatingFileHandler from aiogram import Bot, Dispatcher, types, F
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.utils.keyboard import ReplyKeyboardBuilder from aiogram.utils.keyboard import ReplyKeyboardBuilder
import requests import requests
import asyncio import asyncio
from config import BOT_TOKEN, WEATHER_API_KEY
# настройка логов # Настройка логирования
logger = logging.getLogger() logging.basicConfig(
logger.setLevel(logging.DEBUG) level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
) )
logger = logging.getLogger(__name__)
# логирование в файл, максимум 5мб по 3 бэкапа # Инициализация бота
file_handler = RotatingFileHandler(
'weather_bot.log',
maxBytes=5*1024*1024,
backupCount=3,
encoding='utf-8'
)
file_handler.setFormatter(formatter)
# вывод в консоль
console_handler = logging.StreamHandler()
console_handler.setFormatter(formatter)
logger.addHandler(file_handler)
logger.addHandler(console_handler)
# конфиг
try:
from config import BOT_TOKEN, WEATHER_API_KEY
except ImportError:
logging.critical("Ошибка: Создайте файл config.py с BOT_TOKEN и WEATHER_API_KEY!")
exit(1)
# инициализация бота
bot = Bot(token=BOT_TOKEN) bot = Bot(token=BOT_TOKEN)
dp = Dispatcher() dp = Dispatcher()
# клавиатура # Клавиатура
builder = ReplyKeyboardBuilder() def get_keyboard():
builder.button(text="Узнать погоду") builder = ReplyKeyboardBuilder()
builder.button(text="Помощь") builder.button(text="🌤️ Узнать погоду")
keyboard = builder.as_markup(resize_keyboard=True) builder.button(text="📍 Мое местоположение", request_location=True)
return builder.as_markup(resize_keyboard=True)
# обработка команд async def get_coordinates(city: str):
@dp.message(Command("start", "help")) """Получение координат города через Nominatim"""
async def cmd_start(message: types.Message): url = f"https://nominatim.openstreetmap.org/search?city={city}&format=json"
logging.info(f"Новый пользователь: {message.from_user.id}") headers = {"User-Agent": "CityPhotoBot/1.0"}
await message.answer(
"Бот Погоды\n\n" try:
"Отправьте мне название города, и я пришлю текущую погоду.\n" response = requests.get(url, headers=headers, timeout=10)
"Или нажмите кнопку ниже:", if response.status_code == 200:
reply_markup=keyboard data = response.json()
) if data:
for item in data:
@dp.message(lambda message: message.text in ["Узнать погоду", "Помощь"]) if item.get('type') in ['city', 'town']:
async def button_handler(message: types.Message): return float(item['lon']), float(item['lat'])
if message.text == "Узнать погоду": return float(data[0]['lon']), float(data[0]['lat'])
await message.answer("Введите название города:") except Exception as e:
else: logger.error(f"Geocoding error: {e}")
await cmd_start(message) return None
@dp.message() async def get_weather(lat: float, lon: float, city_name: str) -> str:
async def get_weather(message: types.Message): """Получение данных о погоде"""
city = message.text.strip() url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={WEATHER_API_KEY}&units=metric&lang=ru"
if not city:
return
logging.info(f"Запрос погоды для: {city} (от {message.from_user.id})")
try: try:
# запрос к API
url = f"https://api.openweathermap.org/data/2.5/weather?q={city}&appid={WEATHER_API_KEY}&units=metric&lang=ru"
response = requests.get(url, timeout=10) response = requests.get(url, timeout=10)
if response.status_code == 200:
data = response.json()
weather_info = (
f"🌆 Город: {city_name}\n"
f"🌡 Температура: {data['main']['temp']:.1f}°C\n"
f"🧭 Ощущается как: {data['main']['feels_like']:.1f}°C\n"
f"💨 Ветер: {data['wind']['speed']} м/с\n"
f"☁️ Погода: {data['weather'][0]['description'].capitalize()}\n"
f"💧 Влажность: {data['main']['humidity']}%"
)
return weather_info
except Exception as e:
logger.error(f"Weather API error: {e}")
return "Не удалось получить данные о погоде"
logging.debug(f"API Response: {response.status_code} {response.text[:200]}...") async def get_city_photo(city: str, lat: float = None, lon: float = None):
"""Поиск фотографии города через Wikipedia и другие источники"""
# 1. Попробуем Wikipedia
wikipedia_url = f"https://ru.wikipedia.org/w/api.php?action=query&prop=pageimages&titles={city}&pithumbsize=800&format=json"
try:
response = requests.get(wikipedia_url, timeout=10)
if response.status_code == 200:
data = response.json()
pages = data.get('query', {}).get('pages', {})
for page in pages.values():
if 'thumbnail' in page:
return page['thumbnail']['source'], "Wikipedia"
except Exception as e:
logger.error(f"Wikipedia API error: {e}")
if response.status_code != 200: # 2. Попробуем Wikimedia Commons
error_msg = response.json().get('message', 'Unknown error') wikimedia_url = f"https://commons.wikimedia.org/w/api.php?action=query&generator=images&titles={city}&prop=imageinfo&iiprop=url&iiurlwidth=800&format=json"
logging.error(f"API Error for {city}: {response.status_code} - {error_msg}") try:
await message.answer(f"Ошибка: {error_msg.capitalize()}") response = requests.get(wikimedia_url, timeout=10)
if response.status_code == 200:
data = response.json()
pages = data.get('query', {}).get('pages', {})
for page in pages.values():
if 'imageinfo' in page:
return page['imageinfo'][0]['thumburl'], "Wikimedia Commons"
except Exception as e:
logger.error(f"Wikimedia API error: {e}")
# 3. Для российских городов попробуем PhotoBank RG
if "россия" in city.lower() or any(city.lower().endswith(x) for x in ["ск", "ов", "ев", "ин"]):
try:
photobank_url = f"https://rusneb.ru/api/v1/photos?query={city}&limit=1"
response = requests.get(photobank_url, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get('results'):
return data['results'][0]['image_url'], "PhotoBank RG"
except Exception as e:
logger.error(f"PhotoBank error: {e}")
return None, None
@dp.message(Command("start", "help"))
async def cmd_start(message: types.Message):
await message.answer(
"🏙️ Бот Погоды с фотографиями городов\n\n"
"Отправьте мне название города или поделитесь геопозицией, "
"и я покажу погоду и фото города:",
reply_markup=get_keyboard()
)
@dp.message(F.text == "🌤️ Узнать погоду")
async def weather_button(message: types.Message):
await message.answer("Введите точное название города:")
@dp.message(F.location)
async def handle_location(message: types.Message):
"""Обработка геолокации"""
try:
lat = message.location.latitude
lon = message.location.longitude
# Получаем название города
url = f"https://nominatim.openstreetmap.org/reverse?lat={lat}&lon={lon}&format=json"
headers = {"User-Agent": "CityPhotoBot/1.0"}
response = requests.get(url, headers=headers, timeout=10)
if response.status_code == 200:
data = response.json()
city_name = data.get('address', {}).get('city',
data.get('address', {}).get('town',
data.get('address', {}).get('village', "это место")))
weather_data = await get_weather(lat, lon, city_name)
photo_url, source = await get_city_photo(city_name, lat, lon)
if photo_url:
caption = f"{weather_data}\n\n📷 Источник: {source}" if source else weather_data
await message.answer_photo(photo_url, caption=caption)
else:
await message.answer(f"{weather_data}\n\n(Не удалось найти фото города)")
except Exception as e:
logger.error(f"Location error: {e}")
await message.answer("Ошибка обработки местоположения")
@dp.message(F.text)
async def city_handler(message: types.Message):
"""Обработка текстовых запросов"""
city = message.text.strip()
if city.startswith('/'):
return
logger.info(f"Processing city: {city}")
try:
coords = await get_coordinates(city)
if not coords:
await message.answer("Город не найден. Попробуйте уточнить название")
return return
data = response.json() lon, lat = coords
weather_data = await get_weather(lat, lon, city)
# формирование ответа # Пробуем получить фото с 3 попытками
weather_report = ( for attempt in range(3):
f"Город: {data['name']}\n" photo_url, source = await get_city_photo(city, lat, lon)
f"Температура: {data['main']['temp']:.1f}°C\n" if photo_url:
f"Ощущается как: {data['main']['feels_like']:.1f}°C\n" caption = f"{weather_data}\n\n📷 Источник: {source}" if source else weather_data
f"Ветер: {data['wind']['speed']} м/с\n" await message.answer_photo(photo_url, caption=caption)
f"Погода: {data['weather'][0]['description'].capitalize()}\n" return
f"Влажность: {data['main']['humidity']}%" await asyncio.sleep(1)
)
logging.info(f"Успешный ответ для {city}") await message.answer(f"{weather_data}\n\n(Не удалось найти фото города)")
await message.answer(weather_report)
except requests.exceptions.Timeout:
logging.error(f"Таймаут запроса для {city}")
await message.answer("Сервер погоды не отвечает. Попробуйте позже.")
except Exception as e: except Exception as e:
logging.exception(f"Ошибка для {city}:") logger.error(f"City handler error: {e}")
await message.answer("⚠️ Произошла внутренняя ошибка. Попробуйте другой город.") await message.answer("Произошла ошибка при обработке запроса")
# запуск бота
async def main(): async def main():
logging.info("Starting bot...")
await dp.start_polling(bot) await dp.start_polling(bot)
if __name__ == "__main__": if __name__ == "__main__":
try: asyncio.run(main())
asyncio.run(main())
except KeyboardInterrupt:
logging.info("Bot stopped by user")
except Exception as e:
logging.critical(f"Fatal error: {e}")