160 lines
6.4 KiB
Python
160 lines
6.4 KiB
Python
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()) |