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 apscheduler.schedulers.asyncio import AsyncIOScheduler from dotenv import load_dotenv from weather import get_daily_weather_summary, get_coords_by_city # Загрузка переменных окружения 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() # Импровизированная БД users_db = {} # Состояния 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_data = users_db.get(user_id) if not user_data or not user_data.get("lat"): 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 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: await message.answer("Пожалуйста, отправь геопозицию или напиши название города текстом.") return # Сохраняем или обновляем пользователя в БД if user_id not in users_db: users_db[user_id] = {"lat": lat, "lon": lon, "time": None, "job_id": None} else: users_db[user_id]["lat"] = lat users_db[user_id]["lon"] = lon # Если время уже было настроено ранее if users_db[user_id].get("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): 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=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) 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 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())