Files
weather_bot/bot.py
2026-03-26 10:15:41 +03:00

182 lines
7.2 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, 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())