tommorow and week
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
.env
|
.env
|
||||||
|
/data
|
||||||
10
Dockerfile
10
Dockerfile
@@ -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
40
bot.py
@@ -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():
|
||||||
"""Загружает задачи из БД в планировщик после перезапуска бота"""
|
"""Загружает задачи из БД в планировщик после перезапуска бота"""
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
|
||||||
113
weather.py
113
weather.py
@@ -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"¤t=temperature_2m"
|
f"¤t=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
|
||||||
Reference in New Issue
Block a user