add database
This commit is contained in:
@@ -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"]
|
||||
109
bot.py
109
bot.py
@@ -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)
|
||||
|
||||
job = scheduler.add_job(
|
||||
# Сохраняем время в БД
|
||||
await db.update_user_time(user_id, time_text)
|
||||
|
||||
# Обновляем или создаем задачу. 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
BIN
data/users.db
Normal file
Binary file not shown.
51
database.py
Normal file
51
database.py
Normal 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()
|
||||
@@ -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
|
||||
- .env
|
||||
volumes:
|
||||
- ./data:/app/data
|
||||
@@ -1,4 +1,5 @@
|
||||
aiogram==3.4.1
|
||||
APScheduler==3.10.4
|
||||
aiohttp==3.9.3
|
||||
python-dotenv==1.0.1
|
||||
python-dotenv==1.0.1
|
||||
aiosqlite==0.20.0
|
||||
Reference in New Issue
Block a user