import asyncio import logging import os from datetime import datetime from aiogram import Bot, Dispatcher, F, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext from aiogram.fsm.state import State, StatesGroup from aiogram.types import ReplyKeyboardMarkup, KeyboardButton from apscheduler.schedulers.asyncio import AsyncIOScheduler from dotenv import load_dotenv from weather import get_daily_weather_summary load_dotenv() BOT_TOKEN = os.getenv("BOT_TOKEN") logging.basicConfig(level=logging.INFO) bot = Bot(token=BOT_TOKEN, parse_mode="Markdown") dp = Dispatcher() scheduler = AsyncIOScheduler() # Формат: user_id: {"lat": float, "lon": float, "time": "HH:MM", "job_id": str} users_db = {} class WeatherSetup(StatesGroup): waiting_for_time = State() # --- Клавиатуры --- def get_main_keyboard(): """Главное меню бота""" return ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="🌤 Погода сейчас")], [ KeyboardButton(text="📍 Обновить локацию", request_location=True), KeyboardButton(text="⏰ Изменить время") ] ], resize_keyboard=True ) def get_location_keyboard(): """Клавиатура для первичного запроса локации""" return ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="📍 Отправить геолокацию", request_location=True)]], resize_keyboard=True ) # --- Логика планировщика --- async def send_weather_update(user_id: int): """Функция планировщика. Берет актуальные координаты из БД на момент отправки.""" user_data = users_db.get(user_id) if not user_data: return weather_text = await get_daily_weather_summary(user_data["lat"], user_data["lon"]) try: await bot.send_message(chat_id=user_id, text=weather_text) except Exception as e: logging.error(f"Ошибка отправки пользователю {user_id}: {e}") # --- Обработчики команд --- @dp.message(Command("start")) async def cmd_start(message: types.Message, state: FSMContext): await message.answer( "Привет! Я погодный бот. Отправь мне свою локацию, чтобы я знал, где смотреть погоду.", reply_markup=get_location_keyboard() ) @dp.message(F.content_type == "location") async def process_location(message: types.Message, state: FSMContext): """Срабатывает как при первой настройке, так и при нажатии 'Обновить локацию'""" lat = message.location.latitude lon = message.location.longitude user_id = message.from_user.id if user_id in users_db: # Если пользователь уже зарегистрирован, просто обновляем координаты users_db[user_id]["lat"] = lat users_db[user_id]["lon"] = lon await message.answer("📍 Локация успешно обновлена!", reply_markup=get_main_keyboard()) else: # Первичная настройка users_db[user_id] = {"lat": lat, "lon": lon, "time": None, "job_id": None} await state.set_state(WeatherSetup.waiting_for_time) await message.answer( "Отлично! Теперь напиши время, в которое хочешь получать прогноз (в формате ЧЧ:ММ):", reply_markup=types.ReplyKeyboardRemove() ) @dp.message(F.text == "⏰ Изменить время") async def cmd_change_time(message: types.Message, state: FSMContext): if message.from_user.id not in users_db: await message.answer("Сначала отправьте локацию через меню или команду /start.") return await state.set_state(WeatherSetup.waiting_for_time) await message.answer("Введите новое время для ежедневного прогноза (в формате ЧЧ:ММ):", reply_markup=types.ReplyKeyboardRemove()) @dp.message(WeatherSetup.waiting_for_time, F.text) async def process_time(message: types.Message, state: FSMContext): time_text = message.text.strip() user_id = message.from_user.id try: dt = datetime.strptime(time_text, "%H:%M") # Удаляем старую задачу, если она была old_job_id = users_db[user_id].get("job_id") if old_job_id and scheduler.get_job(old_job_id): scheduler.remove_job(old_job_id) # Создаем новую задачу (передаем только user_id) job = scheduler.add_job( send_weather_update, trigger='cron', hour=dt.hour, minute=dt.minute, args=[user_id] ) users_db[user_id]["time"] = time_text users_db[user_id]["job_id"] = job.id await message.answer( f"✅ Готово! Ежедневный прогноз установлен на {time_text}.", reply_markup=get_main_keyboard() ) await state.clear() except ValueError: await message.answer("Неверный формат времени. Пожалуйста, используй формат ЧЧ:ММ (например, 07:00).") @dp.message(F.text == "🌤 Погода сейчас") async def cmd_weather_now(message: types.Message): user_id = message.from_user.id user_data = users_db.get(user_id) if not user_data or not user_data.get("lat"): await message.answer("Сначала отправьте локацию через команду /start.") return # Отправляем "заглушку", пока идет запрос к API loading_msg = await message.answer("⏳ Узнаю погоду...") weather_text = await get_daily_weather_summary(user_data["lat"], user_data["lon"]) # Редактируем сообщение-заглушку готовым текстом await loading_msg.edit_text(weather_text) async def main(): scheduler.start() await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) if __name__ == "__main__": asyncio.run(main())