tommorow and week

This commit is contained in:
2026-03-30 21:23:44 +03:00
parent a6c6f061ce
commit 9a49271a0d
6 changed files with 142 additions and 43 deletions

3
.gitignore vendored
View File

@@ -1 +1,2 @@
.env .env
/data

View File

@@ -2,9 +2,17 @@ FROM python:3.11-slim
WORKDIR /app WORKDIR /app
# Устанавливаем переменные окружения для Python
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# Копируем файлы зависимостей
COPY requirements.txt . COPY requirements.txt .
# Устанавливаем зависимости
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
COPY *.py ./ # Копируем исходный код
COPY . .
CMD ["python", "bot.py"] CMD ["python", "bot.py"]

40
bot.py
View File

@@ -3,6 +3,7 @@ import logging
import os import os
from datetime import datetime from datetime import datetime
from aiogram.exceptions import TelegramForbiddenError
from aiogram import Bot, Dispatcher, F, types from aiogram import Bot, Dispatcher, F, types
from aiogram.filters import Command from aiogram.filters import Command
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
@@ -12,7 +13,12 @@ from aiogram.client.default import DefaultBotProperties
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from dotenv import load_dotenv 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 # Импортируем нашу базу данных import database as db # Импортируем нашу базу данных
load_dotenv() load_dotenv()
@@ -32,6 +38,10 @@ def get_main_keyboard():
return ReplyKeyboardMarkup( return ReplyKeyboardMarkup(
keyboard=[ keyboard=[
[KeyboardButton(text="🌤 Погода сейчас")], [KeyboardButton(text="🌤 Погода сейчас")],
[
KeyboardButton(text="📅 Погода на завтра"),
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"]) weather_text = await get_daily_weather_summary(user["lat"], user["lon"])
try: try:
await bot.send_message(chat_id=user_id, text=weather_text) 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: except Exception as e:
logging.error(f"Ошибка отправки пользователю {user_id}: {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"]) weather_text = await get_daily_weather_summary(user["lat"], user["lon"])
await loading_msg.edit_text(weather_text) 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(): async def restore_jobs():
"""Загружает задачи из БД в планировщик после перезапуска бота""" """Загружает задачи из БД в планировщик после перезапуска бота"""

View File

@@ -48,4 +48,10 @@ async def get_all_users_with_time():
async with aiosqlite.connect(DB_PATH) as db: async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row db.row_factory = aiosqlite.Row
async with db.execute("SELECT * FROM users WHERE time IS NOT NULL") as cursor: async with db.execute("SELECT * FROM users WHERE time IS NOT NULL") as cursor:
return await cursor.fetchall() 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()

View File

@@ -1,14 +1,11 @@
version: '3.8'
services: services:
weather_bot: bot:
build: . build: .
container_name: tg_weather_bot container_name: weather_bot
restart: unless-stopped restart: unless-stopped
network_mode: "host"
environment:
- TZ=Europe/Moscow
env_file: env_file:
- .env - .env
volumes: volumes:
# Пробрасываем директорию с БД наружу, чтобы данные сохранялись
# при пересоздании контейнера
- ./data:/app/data - ./data:/app/data

View File

@@ -1,4 +1,5 @@
import aiohttp import aiohttp
from datetime import datetime
async def get_coords_by_city(city_name: str): 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: async with aiohttp.ClientSession() as session:
try: try:
# Добавили таймаут 5 секунд
async with session.get(url, timeout=5) as response: async with session.get(url, timeout=5) as response:
if response.status != 200: if response.status != 200:
return None, None, None return None, None, None
@@ -22,44 +22,93 @@ async def get_coords_by_city(city_name: str):
except Exception: except Exception:
return None, None, None 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 = ( url = (
f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}" f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}"
f"&current=temperature_2m" f"&current=temperature_2m"
f"&daily=temperature_2m_max,temperature_2m_min,precipitation_probability_max" 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: async with aiohttp.ClientSession() as session:
try: try:
async with session.get(url, timeout=5) as response: async with session.get(url, timeout=5) as response:
if response.status != 200: if response.status != 200:
return "Не удалось получить данные о погоде от сервера." return None
return await response.json()
data = await response.json() except Exception:
return None
current = data.get("current", {})
daily = data.get("daily", {})
if not daily or not current:
return "Ошибка парсинга данных о погоде."
current_temp = current.get("temperature_2m", "N/A") async def get_daily_weather_summary(lat: float, lon: float) -> str:
temp_max = daily["temperature_2m_max"][0] """Прогноз на сегодня."""
temp_min = daily["temperature_2m_min"][0] data = await fetch_weather_data(lat, lon, 1)
precip_prob = daily["precipitation_probability_max"][0] if not data or "daily" not in data or "current" not in data:
return "Не удалось получить данные о погоде от сервера."
temp_diff = round(temp_max - temp_min, 1)
current_temp = data["current"].get("temperature_2m", "N/A")
summary = ( temp_max = data["daily"]["temperature_2m_max"][0]
f"🌤 **Погода на сегодня:**\n\n" temp_min = data["daily"]["temperature_2m_min"][0]
f"🌡 **Сейчас:** {current_temp}°C\n" precip_prob = data["daily"]["precipitation_probability_max"][0]
f"📊 **Дневная норма:** от {temp_min}°C до {temp_max}°C\n"
f"📉 **Перепад температур за день:** {temp_diff}°C\n" temp_diff = round(temp_max - temp_min, 1)
f"🌧 **Макс. вероятность осадков:** {precip_prob}%\n\n"
f"Одевайтесь по погоде и хорошего дня!" return (
) f"🌤 **Погода на сегодня:**\n\n"
return summary f"🌡 **Сейчас:** {current_temp}°C\n"
except Exception as e: f"📊 **Дневная норма:** от {temp_min}°C до {temp_max}°C\n"
return "Служба погоды временно недоступна." 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