This commit is contained in:
2026-01-06 13:32:39 +07:00
parent 64ba266a20
commit 4708029378
6 changed files with 6 additions and 9 deletions

253
app/bot.py Normal file
View File

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
# bot.py
import asyncio
import sys
import warnings
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from telegram import InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import Application, ContextTypes
# Импорты из наших модулей
from config import *
from database import init_db
from handlers import help_handler, unknown_command, callback_taken
from conversations import *
from utils import main_menu_kb
# Отключаем предупреждения PTB
warnings.filterwarnings("ignore", category=UserWarning)
# Настройка логирования
logger = setup_logging()
# ----------------- BACKGROUND JOB -----------------
async def background_job(context: ContextTypes.DEFAULT_TYPE):
"""Задача для отправки напоминаний через JobQueue"""
try:
now_utc_dt = datetime.utcnow()
now_min_utc = utc_str_min(now_utc_dt)
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute('''
SELECT pt.id, pt.pill_id, pt.time, pt.last_sent, pt.last_confirmed,
p.user_id, p.name, p.dose, p.interval_days, p.weekdays,
p.course_days, p.start_date, u.tz
FROM pill_times pt
JOIN pills p ON p.id = pt.pill_id
JOIN users u ON u.id = p.user_id
WHERE p.course_days = 0 OR
(julianday(?) - julianday(p.start_date)) < p.course_days
''', (now_utc_dt.strftime("%Y-%m-%d"),))
rows = cursor.fetchall()
for row in rows:
ptid = row["id"]
pid = row["pill_id"]
time_hhmm = row["time"]
last_sent = row["last_sent"]
last_confirmed = row["last_confirmed"]
user_id = row["user_id"]
pill_name = row["name"]
pill_dose = row["dose"]
interval_days = row["interval_days"]
weekdays_csv = row["weekdays"]
course_days = row["course_days"]
start_date = row["start_date"]
user_tz = row["tz"] or "UTC"
try:
tz = ZoneInfo(user_tz)
except Exception:
tz = ZoneInfo("UTC")
user_local_now = datetime.now(tz)
user_local_hhmm = user_local_now.strftime("%H:%M")
if user_local_hhmm != time_hhmm:
continue
if weekdays_csv:
wlist = weekdays_csv_to_list(weekdays_csv)
if user_local_now.weekday() not in wlist:
continue
else:
if interval_days and interval_days > 1 and start_date:
try:
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
diff = (user_local_now.date() - sd).days
if diff % interval_days != 0:
continue
except Exception:
pass
if last_sent == now_min_utc:
continue
if last_confirmed and last_sent and last_confirmed >= last_sent:
continue
kb = InlineKeyboardMarkup([[
InlineKeyboardButton("✅ Выпил(-а)", callback_data=f"take:{ptid}")
]])
try:
await context.bot.send_message(
chat_id=user_id,
text=f"💊 Пора принять {pill_name} ({pill_dose})",
reply_markup=kb
)
cursor.execute(
'UPDATE pill_times SET last_sent=? WHERE id=?',
(now_min_utc, ptid)
)
conn.commit()
logger.info(f"Sent reminder to user {user_id} for pill {pill_name}")
except Exception as e:
logger.error(f"Failed to send reminder to user {user_id}: {e}")
conn.close()
except Exception as e:
logger.exception(f"Error in background job: {e}")
# ----------------- SETUP HANDLERS -----------------
def setup_handlers(application: Application):
"""Настройка всех обработчиков"""
# Главный ConversationHandler
main_conv = ConversationHandler(
entry_points=[
CommandHandler("start", start_handler),
CommandHandler("help", help_handler),
MessageHandler(filters.Regex(f'^{BTN_LIST}$'), main_menu_handler),
MessageHandler(filters.Regex(f'^{BTN_HISTORY}$'), main_menu_handler),
MessageHandler(filters.Regex(f'^{BTN_ADD}$'), main_menu_handler),
MessageHandler(filters.Regex(f'^{BTN_EDIT}$'), main_menu_handler),
MessageHandler(filters.Regex(f'^{BTN_DELETE}$'), main_menu_handler),
MessageHandler(filters.Regex(f'^{BTN_TZ}$'), main_menu_handler),
MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'),
lambda u,c: u.message.reply_text("Отмена.", reply_markup=main_menu_kb())),
],
states={
S_MAIN_MENU: [
MessageHandler(filters.TEXT & ~filters.COMMAND, main_menu_handler)
],
S_TZ_SELECT: [
MessageHandler(
filters.Regex(f'^({"|".join(TZ_SUGGESTIONS)})$'),
tz_select_handler
),
MessageHandler(
filters.Regex("^Ввести вручную$"),
tz_select_handler
),
],
S_TZ_MANUAL: [
MessageHandler(filters.TEXT & ~filters.COMMAND, tz_manual_input_handler)
],
S_GENDER: [
MessageHandler(filters.Regex("^(Я мужчина|Я женщина)$"), gender_handler)
],
S_NAME: [
MessageHandler(filters.TEXT & ~filters.COMMAND, name_handler)
],
S_ADD_NAME: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_name)
],
S_ADD_DOSE: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_dose)
],
S_ADD_FREQ_TYPE: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_freq_type)
],
S_ADD_FREQ_N: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_freq_n)
],
S_ADD_WEEKDAYS: [
CallbackQueryHandler(weekday_callback, pattern="^(wd_toggle:|wd_done|wd_cancel)$")
],
S_ADD_TIMES: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_times_handler)
],
S_ADD_COURSE: [
MessageHandler(filters.TEXT & ~filters.COMMAND, add_course_handler)
],
S_EDIT_SELECT: [
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_select_handler)
],
S_EDIT_FIELD: [
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_field_choice)
],
S_EDIT_VALUE: [
MessageHandler(filters.TEXT & ~filters.COMMAND, edit_value_handler),
CallbackQueryHandler(weekday_callback, pattern="^(wd_toggle:|wd_done|wd_cancel)$")
],
S_DELETE_SELECT: [
MessageHandler(filters.TEXT & ~filters.COMMAND, delete_confirm)
],
},
fallbacks=[
CommandHandler("start", start_handler),
CommandHandler("help", help_handler),
MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'),
lambda u,c: u.message.reply_text("Отмена.", reply_markup=main_menu_kb())),
],
per_message=True
)
application.add_handler(main_conv)
# Обработчик callback для принятия лекарства
application.add_handler(CallbackQueryHandler(callback_taken, pattern=r"^take:\d+$"))
# Обработчик неизвестных команд
application.add_handler(MessageHandler(filters.COMMAND, unknown_command))
# Общий обработчик текста для необработанных сообщений
application.add_handler(MessageHandler(
filters.TEXT & ~filters.COMMAND,
lambda u,c: u.message.reply_text("Пожалуйста, используйте кнопки меню.", reply_markup=main_menu_kb())
))
# ----------------- MAIN FUNCTION -----------------
def main():
"""Основная функция запуска бота"""
logger.info(f"Starting Pill Reminder Bot")
logger.info(f"Database: {DB_FILE}")
logger.info(f"Log level: {LOG_LEVEL}")
# Инициализация БД
init_db()
# Создаем приложение
application = Application.builder().token(TOKEN).build()
# Настраиваем обработчики
setup_handlers(application)
# Настраиваем фоновую задачу через JobQueue
job_queue = application.job_queue
if job_queue:
# Запускаем задачу каждую минуту
job_queue.run_repeating(
background_job,
interval=60, # 60 секунд
first=10 # Запустить через 10 секунд после старта
)
logger.info("Background job scheduled via JobQueue")
# Запускаем бота
application.run_polling()
if __name__ == "__main__":
main()