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 .
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py weather.py ./
RUN useradd -m botuser
USER botuser
COPY *.py ./
CMD ["python", "bot.py"]

107
bot.py
View File

@@ -8,32 +8,26 @@ 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 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
import database as db # Импортируем нашу базу данных
# Загрузка переменных окружения
load_dotenv()
BOT_TOKEN = os.getenv("BOT_TOKEN")
# Настройка логирования
logging.basicConfig(level=logging.INFO)
# Инициализация
bot = Bot(token=BOT_TOKEN, parse_mode="Markdown")
bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(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=[
@@ -53,19 +47,18 @@ def get_location_keyboard():
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"):
"""Отправка погоды по расписанию"""
user = await db.get_user(user_id)
if not user or not user["lat"]:
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:
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)
@@ -77,10 +70,7 @@ async def cmd_start(message: types.Message, state: FSMContext):
@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()
)
await message.answer("Напиши название нового города или отправь геопозицию:", reply_markup=get_location_keyboard())
@dp.message(WeatherSetup.waiting_for_location)
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:
lat, lon, found_name = await get_coords_by_city(message.text)
if not lat:
await message.answer("К сожалению, я не нашел такой город 😔. Попробуй уточнить название или отправить геопозицию.")
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
# Сохраняем локацию в БД
await db.save_user_location(user_id, lat, 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 state.clear()
else:
await state.set_state(WeatherSetup.waiting_for_time)
await message.answer(
f"Отлично! Локация **{city_name_display}** найдена.\n\nТеперь напиши время, в которое хочешь получать ежедневный прогноз (в формате ЧЧ:ММ):",
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:
user = await db.get_user(message.from_user.id)
if not user:
await message.answer("Сначала отправьте локацию через команду /start.")
return
@@ -135,48 +122,68 @@ async def process_time(message: types.Message, state: FSMContext):
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)
# Сохраняем время в БД
await db.update_user_time(user_id, time_text)
job = scheduler.add_job(
# Обновляем или создаем задачу. id задачи = строковый user_id.
# replace_existing=True автоматически заменит старую задачу, если она была.
scheduler.add_job(
send_weather_update,
trigger='cron',
hour=dt.hour,
minute=dt.minute,
args=[user_id]
args=[user_id],
id=str(user_id),
replace_existing=True
)
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 message.answer(f"✅ Готово! Ежедневный прогноз установлен на {time_text}.", reply_markup=get_main_keyboard())
await state.clear()
except ValueError:
await message.answer("Неверный формат времени. Пожалуйста, используй формат ЧЧ:ММ (например, 07:00).")
await message.answer("Неверный формат времени. Пожалуйста, используй формат ЧЧ:ММ.")
@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)
user = await db.get_user(message.from_user.id)
if not user_data or not user_data.get("lat"):
if not user or not user["lat"]:
await message.answer("Сначала отправьте локацию через команду /start.")
return
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)
# --- Запуск ---
# --- ВОССТАНОВЛЕНИЕ ПЛАНИРОВЩИКА ---
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():
scheduler.start()
await db.init_db() # 1. Инициализируем БД
await restore_jobs() # 2. Восстанавливаем расписание из БД
scheduler.start() # 3. Запускаем планировщик
await bot.delete_webhook(drop_pending_updates=True)
await dp.start_polling(bot)
await dp.start_polling(bot) # 4. Запускаем бота
if __name__ == "__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: .
container_name: tg_weather_bot
restart: unless-stopped
network_mode: "host"
environment:
- TZ=Europe/Moscow # Укажи здесь нужный базовый часовой пояс
- TZ=Europe/Moscow
env_file:
- .env
volumes:
- ./data:/app/data

View File

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