Files
weather_bot/bot.py
2026-03-26 09:37:50 +03:00

160 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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())