add database

This commit is contained in:
2026-03-26 12:42:59 +03:00
parent 20531d1029
commit a6c6f061ce
6 changed files with 117 additions and 58 deletions

View File

@@ -5,9 +5,6 @@ WORKDIR /app
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 bot.py weather.py ./ COPY *.py ./
RUN useradd -m botuser
USER botuser
CMD ["python", "bot.py"] CMD ["python", "bot.py"]

109
bot.py
View File

@@ -8,32 +8,26 @@ from aiogram.filters import Command
from aiogram.fsm.context import FSMContext from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup from aiogram.fsm.state import State, StatesGroup
from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove from aiogram.types import ReplyKeyboardMarkup, KeyboardButton, ReplyKeyboardRemove
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
import database as db # Импортируем нашу базу данных
# Загрузка переменных окружения
load_dotenv() load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN") BOT_TOKEN = os.getenv("BOT_TOKEN")
# Настройка логирования
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# Инициализация bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode="Markdown"))
bot = Bot(token=BOT_TOKEN, parse_mode="Markdown")
dp = Dispatcher() dp = Dispatcher()
scheduler = AsyncIOScheduler() scheduler = AsyncIOScheduler()
# Импровизированная БД
users_db = {}
# Состояния
class WeatherSetup(StatesGroup): class WeatherSetup(StatesGroup):
waiting_for_location = State() waiting_for_location = State()
waiting_for_time = State() waiting_for_time = State()
# --- Клавиатуры ---
def get_main_keyboard(): def get_main_keyboard():
return ReplyKeyboardMarkup( return ReplyKeyboardMarkup(
keyboard=[ keyboard=[
@@ -53,19 +47,18 @@ def get_location_keyboard():
input_field_placeholder="Или напишите название города..." input_field_placeholder="Или напишите название города..."
) )
# --- Задача для планировщика ---
async def send_weather_update(user_id: int): 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"): user = await db.get_user(user_id)
if not user or not user["lat"]:
return return
weather_text = await get_daily_weather_summary(user_data["lat"], user_data["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 Exception as e: except Exception as e:
logging.error(f"Ошибка отправки пользователю {user_id}: {e}") logging.error(f"Ошибка отправки пользователю {user_id}: {e}")
# --- Обработчики ---
@dp.message(Command("start")) @dp.message(Command("start"))
async def cmd_start(message: types.Message, state: FSMContext): async def cmd_start(message: types.Message, state: FSMContext):
await state.set_state(WeatherSetup.waiting_for_location) await state.set_state(WeatherSetup.waiting_for_location)
@@ -77,10 +70,7 @@ async def cmd_start(message: types.Message, state: FSMContext):
@dp.message(F.text == "📍 Изменить город") @dp.message(F.text == "📍 Изменить город")
async def cmd_change_city(message: types.Message, state: FSMContext): async def cmd_change_city(message: types.Message, state: FSMContext):
await state.set_state(WeatherSetup.waiting_for_location) await state.set_state(WeatherSetup.waiting_for_location)
await message.answer( await message.answer("Напиши название нового города или отправь геопозицию:", reply_markup=get_location_keyboard())
"Напиши название нового города или отправь геопозицию:",
reply_markup=get_location_keyboard()
)
@dp.message(WeatherSetup.waiting_for_location) @dp.message(WeatherSetup.waiting_for_location)
async def process_location_input(message: types.Message, state: FSMContext): async def process_location_input(message: types.Message, state: FSMContext):
@@ -93,34 +83,31 @@ async def process_location_input(message: types.Message, state: FSMContext):
elif message.text: elif message.text:
lat, lon, found_name = await get_coords_by_city(message.text) lat, lon, found_name = await get_coords_by_city(message.text)
if not lat: if not lat:
await message.answer("К сожалению, я не нашел такой город 😔. Попробуй уточнить название или отправить геопозицию.") await message.answer("К сожалению, я не нашел такой город 😔. Попробуй уточнить название.")
return return
city_name_display = found_name city_name_display = found_name
else: else:
await message.answer("Пожалуйста, отправь геопозицию или напиши название города текстом.")
return return
# Сохраняем или обновляем пользователя в БД # Сохраняем локацию в БД
if user_id not in users_db: await db.save_user_location(user_id, lat, lon)
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"): user = await db.get_user(user_id)
if user and user["time"]:
await message.answer(f"📍 Локация успешно обновлена на: **{city_name_display}**!", reply_markup=get_main_keyboard()) await message.answer(f"📍 Локация успешно обновлена на: **{city_name_display}**!", reply_markup=get_main_keyboard())
await state.clear() await state.clear()
else: else:
await state.set_state(WeatherSetup.waiting_for_time) await state.set_state(WeatherSetup.waiting_for_time)
await message.answer( await message.answer(
f"Отлично! Локация **{city_name_display}** найдена.\n\nТеперь напиши время, в которое хочешь получать ежедневный прогноз (в формате ЧЧ:ММ):", f"Отлично! Локация **{city_name_display}** найдена.\n\nТеперь напиши время (в формате ЧЧ:ММ):",
reply_markup=ReplyKeyboardRemove() reply_markup=ReplyKeyboardRemove()
) )
@dp.message(F.text == "⏰ Изменить время") @dp.message(F.text == "⏰ Изменить время")
async def cmd_change_time(message: types.Message, state: FSMContext): async def cmd_change_time(message: types.Message, state: FSMContext):
if message.from_user.id not in users_db: user = await db.get_user(message.from_user.id)
if not user:
await message.answer("Сначала отправьте локацию через команду /start.") await message.answer("Сначала отправьте локацию через команду /start.")
return return
@@ -135,48 +122,68 @@ async def process_time(message: types.Message, state: FSMContext):
try: try:
dt = datetime.strptime(time_text, "%H:%M") 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): await db.update_user_time(user_id, time_text)
scheduler.remove_job(old_job_id)
# Обновляем или создаем задачу. id задачи = строковый user_id.
job = scheduler.add_job( # replace_existing=True автоматически заменит старую задачу, если она была.
scheduler.add_job(
send_weather_update, send_weather_update,
trigger='cron', trigger='cron',
hour=dt.hour, hour=dt.hour,
minute=dt.minute, minute=dt.minute,
args=[user_id] args=[user_id],
id=str(user_id),
replace_existing=True
) )
users_db[user_id]["time"] = time_text await message.answer(f"✅ Готово! Ежедневный прогноз установлен на {time_text}.", reply_markup=get_main_keyboard())
users_db[user_id]["job_id"] = job.id
await message.answer(
f"✅ Готово! Ежедневный прогноз установлен на {time_text}.",
reply_markup=get_main_keyboard()
)
await state.clear() await state.clear()
except ValueError: except ValueError:
await message.answer("Неверный формат времени. Пожалуйста, используй формат ЧЧ:ММ (например, 07:00).") await message.answer("Неверный формат времени. Пожалуйста, используй формат ЧЧ:ММ.")
@dp.message(F.text == "🌤 Погода сейчас") @dp.message(F.text == "🌤 Погода сейчас")
async def cmd_weather_now(message: types.Message): async def cmd_weather_now(message: types.Message):
user_id = message.from_user.id user = await db.get_user(message.from_user.id)
user_data = users_db.get(user_id)
if not user_data or not user_data.get("lat"): if not user or not user["lat"]:
await message.answer("Сначала отправьте локацию через команду /start.") await message.answer("Сначала отправьте локацию через команду /start.")
return return
loading_msg = await message.answer("⏳ Узнаю погоду...") loading_msg = await message.answer("⏳ Узнаю погоду...")
weather_text = await get_daily_weather_summary(user_data["lat"], user_data["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)
# --- Запуск --- # --- ВОССТАНОВЛЕНИЕ ПЛАНИРОВЩИКА ---
async def restore_jobs():
"""Загружает задачи из БД в планировщик после перезапуска бота"""
users = await db.get_all_users_with_time()
count = 0
for user in users:
try:
dt = datetime.strptime(user["time"], "%H:%M")
scheduler.add_job(
send_weather_update,
trigger='cron',
hour=dt.hour,
minute=dt.minute,
args=[user["user_id"]],
id=str(user["user_id"]),
replace_existing=True
)
count += 1
except Exception as e:
logging.error(f"Не удалось восстановить задачу для {user['user_id']}: {e}")
logging.info(f"Восстановлено задач из БД: {count}")
async def main(): async def main():
scheduler.start() await db.init_db() # 1. Инициализируем БД
await restore_jobs() # 2. Восстанавливаем расписание из БД
scheduler.start() # 3. Запускаем планировщик
await bot.delete_webhook(drop_pending_updates=True) await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot) await dp.start_polling(bot) # 4. Запускаем бота
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(main()) asyncio.run(main())

BIN
data/users.db Normal file

Binary file not shown.

51
database.py Normal file
View File

@@ -0,0 +1,51 @@
import aiosqlite
import os
# База будет храниться в папке data
DB_PATH = "data/users.db"
async def init_db():
"""Создает таблицу, если она еще не существует."""
os.makedirs("data", exist_ok=True)
async with aiosqlite.connect(DB_PATH) as db:
await db.execute('''
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY,
lat REAL,
lon REAL,
time TEXT
)
''')
await db.commit()
async def get_user(user_id: int):
"""Возвращает данные пользователя по его ID."""
async with aiosqlite.connect(DB_PATH) as db:
db.row_factory = aiosqlite.Row # Чтобы обращаться к колонкам по именам (как к словарю)
async with db.execute("SELECT * FROM users WHERE user_id = ?", (user_id,)) as cursor:
return await cursor.fetchone()
async def save_user_location(user_id: int, lat: float, lon: float):
"""Сохраняет или обновляет только координаты пользователя."""
async with aiosqlite.connect(DB_PATH) as db:
await db.execute('''
INSERT INTO users (user_id, lat, lon)
VALUES (?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET
lat=excluded.lat,
lon=excluded.lon
''', (user_id, lat, lon))
await db.commit()
async def update_user_time(user_id: int, time: str):
"""Обновляет время отправки для пользователя."""
async with aiosqlite.connect(DB_PATH) as db:
await db.execute("UPDATE users SET time = ? WHERE user_id = ?", (time, user_id))
await db.commit()
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()

View File

@@ -5,7 +5,10 @@ services:
build: . build: .
container_name: tg_weather_bot container_name: tg_weather_bot
restart: unless-stopped restart: unless-stopped
network_mode: "host"
environment: environment:
- TZ=Europe/Moscow # Укажи здесь нужный базовый часовой пояс - TZ=Europe/Moscow
env_file: env_file:
- .env - .env
volumes:
- ./data:/app/data

View File

@@ -1,4 +1,5 @@
aiogram==3.4.1 aiogram==3.4.1
APScheduler==3.10.4 APScheduler==3.10.4
aiohttp==3.9.3 aiohttp==3.9.3
python-dotenv==1.0.1 python-dotenv==1.0.1
aiosqlite==0.20.0