From 9a49271a0dfd3686e0554cd45ed915ac969022f7 Mon Sep 17 00:00:00 2001 From: cisterna Date: Mon, 30 Mar 2026 21:23:44 +0300 Subject: [PATCH] tommorow and week --- .gitignore | 3 +- Dockerfile | 10 +++- bot.py | 40 +++++++++++++++- database.py | 8 +++- docker-compose.yml | 11 ++--- weather.py | 113 ++++++++++++++++++++++++++++++++------------- 6 files changed, 142 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 2eea525..c06317a 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.env \ No newline at end of file +.env +/data \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 1e3d427..2c6602a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,9 +2,17 @@ FROM python:3.11-slim WORKDIR /app +# Устанавливаем переменные окружения для Python +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# Копируем файлы зависимостей COPY requirements.txt . + +# Устанавливаем зависимости RUN pip install --no-cache-dir -r requirements.txt -COPY *.py ./ +# Копируем исходный код +COPY . . CMD ["python", "bot.py"] \ No newline at end of file diff --git a/bot.py b/bot.py index 56e5e1f..6db5c2d 100644 --- a/bot.py +++ b/bot.py @@ -3,6 +3,7 @@ import logging import os from datetime import datetime +from aiogram.exceptions import TelegramForbiddenError from aiogram import Bot, Dispatcher, F, types from aiogram.filters import Command from aiogram.fsm.context import FSMContext @@ -12,7 +13,12 @@ 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 +from weather import ( + get_daily_weather_summary, + get_coords_by_city, + get_tomorrow_weather_summary, + get_weekly_weather_summary +) import database as db # Импортируем нашу базу данных load_dotenv() @@ -32,6 +38,10 @@ def get_main_keyboard(): return ReplyKeyboardMarkup( keyboard=[ [KeyboardButton(text="🌤 Погода сейчас")], + [ + KeyboardButton(text="📅 Погода на завтра"), + KeyboardButton(text="📆 Прогноз на неделю") + ], [ KeyboardButton(text="📍 Изменить город"), KeyboardButton(text="⏰ Изменить время") @@ -56,6 +66,10 @@ async def send_weather_update(user_id: int): weather_text = await get_daily_weather_summary(user["lat"], user["lon"]) try: await bot.send_message(chat_id=user_id, text=weather_text) + except TelegramForbiddenError: + logging.info(f"Пользователь {user_id} заблокировал бота. Удаляем из рассылки.") + scheduler.remove_job(str(user_id)) + await db.delete_user(user_id) except Exception as e: logging.error(f"Ошибка отправки пользователю {user_id}: {e}") @@ -155,6 +169,30 @@ async def cmd_weather_now(message: types.Message): weather_text = await get_daily_weather_summary(user["lat"], user["lon"]) await loading_msg.edit_text(weather_text) +@dp.message(F.text == "📅 Погода на завтра") +async def cmd_weather_tomorrow(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_tomorrow_weather_summary(user["lat"], user["lon"]) + await loading_msg.edit_text(weather_text) + +@dp.message(F.text == "📆 Прогноз на неделю") +async def cmd_weather_week(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("⏳ Собираю прогноз на 7 дней...") + weather_text = await get_weekly_weather_summary(user["lat"], user["lon"]) + await loading_msg.edit_text(weather_text) + # --- ВОССТАНОВЛЕНИЕ ПЛАНИРОВЩИКА --- async def restore_jobs(): """Загружает задачи из БД в планировщик после перезапуска бота""" diff --git a/database.py b/database.py index 7547c1c..a61e82b 100644 --- a/database.py +++ b/database.py @@ -48,4 +48,10 @@ async def get_all_users_with_time(): async with aiosqlite.connect(DB_PATH) as db: db.row_factory = aiosqlite.Row async with db.execute("SELECT * FROM users WHERE time IS NOT NULL") as cursor: - return await cursor.fetchall() \ No newline at end of file + return await cursor.fetchall() + +async def delete_user(user_id: int): + """Удаляет пользователя из БД (например, если он заблокировал бота).""" + async with aiosqlite.connect(DB_PATH) as db: + await db.execute("DELETE FROM users WHERE user_id = ?", (user_id,)) + await db.commit() \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index c16617c..0af3cbb 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,11 @@ -version: '3.8' - services: - weather_bot: + bot: build: . - container_name: tg_weather_bot + container_name: weather_bot restart: unless-stopped - network_mode: "host" - environment: - - TZ=Europe/Moscow env_file: - .env volumes: + # Пробрасываем директорию с БД наружу, чтобы данные сохранялись + # при пересоздании контейнера - ./data:/app/data \ No newline at end of file diff --git a/weather.py b/weather.py index 1abbfac..51a19c2 100644 --- a/weather.py +++ b/weather.py @@ -1,4 +1,5 @@ import aiohttp +from datetime import datetime async def get_coords_by_city(city_name: str): """Ищет координаты по названию города.""" @@ -6,7 +7,6 @@ async def get_coords_by_city(city_name: str): async with aiohttp.ClientSession() as session: try: - # Добавили таймаут 5 секунд async with session.get(url, timeout=5) as response: if response.status != 200: return None, None, None @@ -22,44 +22,93 @@ async def get_coords_by_city(city_name: str): except Exception: return None, None, None -async def get_daily_weather_summary(lat: float, lon: float) -> str: - """Получает текущую погоду и сводку на день по координатам.""" +async def fetch_weather_data(lat: float, lon: float, days: int): + """Базовая функция для запроса данных о погоде на N дней.""" url = ( f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}" f"¤t=temperature_2m" f"&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max" - f"&timezone=auto&forecast_days=1" + f"&timezone=auto&forecast_days={days}" ) - async with aiohttp.ClientSession() as session: try: async with session.get(url, timeout=5) as response: if response.status != 200: - return "Не удалось получить данные о погоде от сервера." - - data = await response.json() - - current = data.get("current", {}) - daily = data.get("daily", {}) - - if not daily or not current: - return "Ошибка парсинга данных о погоде." + return None + return await response.json() + except Exception: + return None - current_temp = current.get("temperature_2m", "N/A") - temp_max = daily["temperature_2m_max"][0] - temp_min = daily["temperature_2m_min"][0] - precip_prob = daily["precipitation_probability_max"][0] - - temp_diff = round(temp_max - temp_min, 1) - - summary = ( - f"🌤 **Погода на сегодня:**\n\n" - f"🌡 **Сейчас:** {current_temp}°C\n" - f"📊 **Дневная норма:** от {temp_min}°C до {temp_max}°C\n" - f"📉 **Перепад температур за день:** {temp_diff}°C\n" - f"🌧 **Макс. вероятность осадков:** {precip_prob}%\n\n" - f"Одевайтесь по погоде и хорошего дня!" - ) - return summary - except Exception as e: - return "Служба погоды временно недоступна." \ No newline at end of file +async def get_daily_weather_summary(lat: float, lon: float) -> str: + """Прогноз на сегодня.""" + data = await fetch_weather_data(lat, lon, 1) + if not data or "daily" not in data or "current" not in data: + return "Не удалось получить данные о погоде от сервера." + + current_temp = data["current"].get("temperature_2m", "N/A") + temp_max = data["daily"]["temperature_2m_max"][0] + temp_min = data["daily"]["temperature_2m_min"][0] + precip_prob = data["daily"]["precipitation_probability_max"][0] + + temp_diff = round(temp_max - temp_min, 1) + + return ( + f"🌤 **Погода на сегодня:**\n\n" + f"🌡 **Сейчас:** {current_temp}°C\n" + f"📊 **Дневная норма:** от {temp_min}°C до {temp_max}°C\n" + f"📉 **Перепад температур за день:** {temp_diff}°C\n" + f"🌧 **Макс. вероятность осадков:** {precip_prob}%\n\n" + f"Одевайтесь по погоде и хорошего дня!" + ) + +async def get_tomorrow_weather_summary(lat: float, lon: float) -> str: + """Прогноз на завтра (берем индекс [1] из массивов).""" + data = await fetch_weather_data(lat, lon, 2) + if not data or "daily" not in data: + return "Не удалось получить данные о погоде от сервера." + + temp_max = data["daily"]["temperature_2m_max"][1] + temp_min = data["daily"]["temperature_2m_min"][1] + precip_prob = data["daily"]["precipitation_probability_max"][1] + + temp_diff = round(temp_max - temp_min, 1) + + return ( + f"📅 **Погода на завтра:**\n\n" + f"📊 **Дневная норма:** от {temp_min}°C до {temp_max}°C\n" + f"📉 **Перепад температур:** {temp_diff}°C\n" + f"🌧 **Макс. вероятность осадков:** {precip_prob}%\n\n" + f"Запланируйте свой день с учетом прогноза!" + ) + +async def get_weekly_weather_summary(lat: float, lon: float) -> str: + """Прогноз на 7 дней.""" + data = await fetch_weather_data(lat, lon, 7) + if not data or "daily" not in data: + return "Не удалось получить данные о погоде от сервера." + + dates = data["daily"]["time"] + max_temps = data["daily"]["temperature_2m_max"] + min_temps = data["daily"]["temperature_2m_min"] + precips = data["daily"]["precipitation_probability_max"] + + week_days = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"] + + summary = "📆 **Прогноз на неделю:**\n\n" + + for i in range(7): + # Преобразуем дату "YYYY-MM-DD" в красивый формат "DD.MM (День недели)" + date_obj = datetime.strptime(dates[i], "%Y-%m-%d") + day_name = week_days[date_obj.weekday()] + formatted_date = date_obj.strftime("%d.%m") + + t_min = min_temps[i] + t_max = max_temps[i] + prob = precips[i] + + # Эмодзи для осадков, чтобы легче читалось + precip_emoji = "☔️" if prob > 40 else ("☁️" if prob > 10 else "☀️") + + summary += f"🔹 **{formatted_date} ({day_name}):** от {t_min}°C до {t_max}°C | {precip_emoji} {prob}%\n" + + return summary \ No newline at end of file