253 lines
9.6 KiB
Python
253 lines
9.6 KiB
Python
#!/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() |