Files
botPillbox/app/bot.py
2026-01-06 13:39:22 +07:00

253 lines
9.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 app.handlers import help_handler, unknown_command, callback_taken
from conversations import *
from app.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()