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, ReplyKeyboardRemove from aiogram.client.default import DefaultBotProperties from apscheduler.schedulers.asyncio import AsyncIOScheduler from dotenv import load_dotenv from weather import get_daily_weather_summary, get_coords_by_city import database as db # Импортируем нашу базу данных load_dotenv() BOT_TOKEN = os.getenv("BOT_TOKEN") logging.basicConfig(level=logging.INFO) bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="Markdown")) dp = Dispatcher() scheduler = AsyncIOScheduler() class WeatherSetup(StatesGroup): waiting_for_location = State() waiting_for_time = State() def get_main_keyboard(): return ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="🌤 Погода сейчас")], [ KeyboardButton(text="📍 Изменить город"), KeyboardButton(text="⏰ Изменить время") ] ], resize_keyboard=True ) def get_location_keyboard(): return ReplyKeyboardMarkup( keyboard=[[KeyboardButton(text="📍 Отправить текущую геопозицию", request_location=True)]], resize_keyboard=True, input_field_placeholder="Или напишите название города..." ) async def send_weather_update(user_id: int): """Отправка погоды по расписанию""" user = await db.get_user(user_id) if not user or not user["lat"]: return weather_text = await get_daily_weather_summary(user["lat"], user["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 state.set_state(WeatherSetup.waiting_for_location) await message.answer( "Привет! Я погодный бот.\n\nОтправь мне свою геопозицию кнопкой ниже или **просто напиши название своего города**:", reply_markup=get_location_keyboard() ) @dp.message(F.text == "📍 Изменить город") async def cmd_change_city(message: types.Message, state: FSMContext): await state.set_state(WeatherSetup.waiting_for_location) await message.answer("Напиши название нового города или отправь геопозицию:", reply_markup=get_location_keyboard()) @dp.message(WeatherSetup.waiting_for_location) async def process_location_input(message: types.Message, state: FSMContext): user_id = message.from_user.id city_name_display = "Выбранная локация" if message.location: lat = message.location.latitude lon = message.location.longitude elif message.text: lat, lon, found_name = await get_coords_by_city(message.text) if not lat: await message.answer("К сожалению, я не нашел такой город 😔. Попробуй уточнить название.") return city_name_display = found_name else: return # Сохраняем локацию в БД await db.save_user_location(user_id, lat, lon) # Проверяем, есть ли уже у пользователя настроенное время user = await db.get_user(user_id) if user and user["time"]: await message.answer(f"📍 Локация успешно обновлена на: **{city_name_display}**!", reply_markup=get_main_keyboard()) await state.clear() else: await state.set_state(WeatherSetup.waiting_for_time) await message.answer( f"Отлично! Локация **{city_name_display}** найдена.\n\nТеперь напиши время (в формате ЧЧ:ММ):", reply_markup=ReplyKeyboardRemove() ) @dp.message(F.text == "⏰ Изменить время") async def cmd_change_time(message: types.Message, state: FSMContext): user = await db.get_user(message.from_user.id) if not user: await message.answer("Сначала отправьте локацию через команду /start.") return await state.set_state(WeatherSetup.waiting_for_time) await message.answer("Введите новое время для ежедневного прогноза (в формате ЧЧ:ММ):", reply_markup=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") # Сохраняем время в БД await db.update_user_time(user_id, time_text) # Обновляем или создаем задачу. id задачи = строковый user_id. # replace_existing=True автоматически заменит старую задачу, если она была. scheduler.add_job( send_weather_update, trigger='cron', hour=dt.hour, minute=dt.minute, args=[user_id], id=str(user_id), replace_existing=True ) await message.answer(f"✅ Готово! Ежедневный прогноз установлен на {time_text}.", reply_markup=get_main_keyboard()) await state.clear() except ValueError: await message.answer("Неверный формат времени. Пожалуйста, используй формат ЧЧ:ММ.") @dp.message(F.text == "🌤 Погода сейчас") async def cmd_weather_now(message: types.Message): user = await db.get_user(message.from_user.id) if not user or not user["lat"]: await message.answer("Сначала отправьте локацию через команду /start.") return loading_msg = await message.answer("⏳ Узнаю погоду...") weather_text = await get_daily_weather_summary(user["lat"], user["lon"]) await loading_msg.edit_text(weather_text) # --- ВОССТАНОВЛЕНИЕ ПЛАНИРОВЩИКА --- async def restore_jobs(): """Загружает задачи из БД в планировщик после перезапуска бота""" users = await db.get_all_users_with_time() count = 0 for user in users: try: dt = datetime.strptime(user["time"], "%H:%M") scheduler.add_job( send_weather_update, trigger='cron', hour=dt.hour, minute=dt.minute, args=[user["user_id"]], id=str(user["user_id"]), replace_existing=True ) count += 1 except Exception as e: logging.error(f"Не удалось восстановить задачу для {user['user_id']}: {e}") logging.info(f"Восстановлено задач из БД: {count}") async def main(): await db.init_db() # 1. Инициализируем БД await restore_jobs() # 2. Восстанавливаем расписание из БД scheduler.start() # 3. Запускаем планировщик await bot.delete_webhook(drop_pending_updates=True) await dp.start_polling(bot) # 4. Запускаем бота if __name__ == "__main__": asyncio.run(main())