v0.0.5
This commit is contained in:
253
app/bot.py
Normal file
253
app/bot.py
Normal 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()
|
||||
Reference in New Issue
Block a user