From d979c105c6ca8bb4b48270a7e84d37c1f7cf2ef3 Mon Sep 17 00:00:00 2001 From: yogurtmenn Date: Tue, 25 Mar 2025 10:18:53 +0000 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20bot-tg.py?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot-tg.py | 271 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 169 insertions(+), 102 deletions(-) diff --git a/bot-tg.py b/bot-tg.py index a83e76f..a958d4b 100644 --- a/bot-tg.py +++ b/bot-tg.py @@ -1,122 +1,189 @@ import logging -from logging.handlers import RotatingFileHandler -from aiogram import Bot, Dispatcher, types +from aiogram import Bot, Dispatcher, types, F from aiogram.filters import Command from aiogram.utils.keyboard import ReplyKeyboardBuilder import requests import asyncio +from config import BOT_TOKEN, WEATHER_API_KEY -# настройка логов -logger = logging.getLogger() -logger.setLevel(logging.DEBUG) - -formatter = logging.Formatter( - '%(asctime)s - %(name)s - %(levelname)s - %(message)s' +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(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) dp = Dispatcher() -# клавиатура -builder = ReplyKeyboardBuilder() -builder.button(text="Узнать погоду") -builder.button(text="Помощь") -keyboard = builder.as_markup(resize_keyboard=True) +# Клавиатура +def get_keyboard(): + builder = ReplyKeyboardBuilder() + builder.button(text="🌤️ Узнать погоду") + builder.button(text="📍 Мое местоположение", request_location=True) + return builder.as_markup(resize_keyboard=True) -# обработка команд -@dp.message(Command("start", "help")) -async def cmd_start(message: types.Message): - logging.info(f"Новый пользователь: {message.from_user.id}") - await message.answer( - "Бот Погоды\n\n" - "Отправьте мне название города, и я пришлю текущую погоду.\n" - "Или нажмите кнопку ниже:", - reply_markup=keyboard - ) - -@dp.message(lambda message: message.text in ["Узнать погоду", "Помощь"]) -async def button_handler(message: types.Message): - if message.text == "Узнать погоду": - await message.answer("Введите название города:") - else: - await cmd_start(message) - -@dp.message() -async def get_weather(message: types.Message): - city = message.text.strip() - if not city: - return - - logging.info(f"Запрос погоды для: {city} (от {message.from_user.id})") +async def get_coordinates(city: str): + """Получение координат города через Nominatim""" + url = f"https://nominatim.openstreetmap.org/search?city={city}&format=json" + headers = {"User-Agent": "CityPhotoBot/1.0"} 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) - - logging.debug(f"API Response: {response.status_code} {response.text[:200]}...") - - if response.status_code != 200: - error_msg = response.json().get('message', 'Unknown error') - logging.error(f"API Error for {city}: {response.status_code} - {error_msg}") - await message.answer(f"Ошибка: {error_msg.capitalize()}") - return - - data = response.json() - - # формирование ответа - weather_report = ( - f"Город: {data['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']}%" - ) - - logging.info(f"Успешный ответ для {city}") - await message.answer(weather_report) - - except requests.exceptions.Timeout: - logging.error(f"Таймаут запроса для {city}") - await message.answer("Сервер погоды не отвечает. Попробуйте позже.") + response = requests.get(url, headers=headers, timeout=10) + if response.status_code == 200: + data = response.json() + if data: + for item in data: + if item.get('type') in ['city', 'town']: + return float(item['lon']), float(item['lat']) + return float(data[0]['lon']), float(data[0]['lat']) except Exception as e: - logging.exception(f"Ошибка для {city}:") - await message.answer("⚠️ Произошла внутренняя ошибка. Попробуйте другой город.") + logger.error(f"Geocoding error: {e}") + return None + +async def get_weather(lat: float, lon: float, city_name: str) -> str: + """Получение данных о погоде""" + url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lon}&appid={WEATHER_API_KEY}&units=metric&lang=ru" + + try: + 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 "Не удалось получить данные о погоде" + +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}") + + # 2. Попробуем Wikimedia Commons + wikimedia_url = f"https://commons.wikimedia.org/w/api.php?action=query&generator=images&titles={city}&prop=imageinfo&iiprop=url&iiurlwidth=800&format=json" + try: + 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 + + lon, lat = coords + weather_data = await get_weather(lat, lon, city) + + # Пробуем получить фото с 3 попытками + for attempt in range(3): + photo_url, source = await get_city_photo(city, 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) + return + await asyncio.sleep(1) + + await message.answer(f"{weather_data}\n\n(Не удалось найти фото города)") + + except Exception as e: + logger.error(f"City handler error: {e}") + await message.answer("Произошла ошибка при обработке запроса") -# запуск бота async def main(): - logging.info("Starting bot...") await dp.start_polling(bot) if __name__ == "__main__": - try: - asyncio.run(main()) - except KeyboardInterrupt: - logging.info("Bot stopped by user") - except Exception as e: - logging.critical(f"Fatal error: {e}") + asyncio.run(main()) \ No newline at end of file