995 lines
44 KiB
Python
995 lines
44 KiB
Python
import sqlite3
|
||
import asyncio
|
||
import logging
|
||
import os
|
||
import sys
|
||
from datetime import datetime, date, timedelta
|
||
from typing import List, Optional
|
||
from zoneinfo import ZoneInfo, available_timezones
|
||
from dotenv import load_dotenv
|
||
|
||
from telegram import (
|
||
Update,
|
||
ReplyKeyboardMarkup,
|
||
ReplyKeyboardRemove,
|
||
InlineKeyboardButton,
|
||
InlineKeyboardMarkup,
|
||
)
|
||
from telegram.ext import (
|
||
Application,
|
||
CommandHandler,
|
||
MessageHandler,
|
||
filters,
|
||
ContextTypes,
|
||
ConversationHandler,
|
||
CallbackQueryHandler,
|
||
)
|
||
|
||
# ----------------- LOAD ENV -----------------
|
||
load_dotenv()
|
||
|
||
# ----------------- VALIDATE CONFIG -----------------
|
||
TOKEN = os.getenv("BOT_TOKEN")
|
||
if not TOKEN or TOKEN == "ваш_токен_бота":
|
||
print("ERROR: BOT_TOKEN not set or is default value!")
|
||
print("Please set BOT_TOKEN in .env file")
|
||
sys.exit(1)
|
||
|
||
DB_FILE = os.getenv("DB_FILE", "pills.db")
|
||
LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
|
||
DEBUG = os.getenv("DEBUG", "False").lower() == "true"
|
||
|
||
# ----------------- LOGGING -----------------
|
||
# Сначала настраиваем минимальное логирование для проверки конфигурации
|
||
logging.basicConfig(level=logging.INFO)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# Затем настраиваем нормальное логирование
|
||
log_level = getattr(logging, LOG_LEVEL.upper(), logging.INFO)
|
||
logger.setLevel(log_level)
|
||
|
||
if DEBUG:
|
||
handler = logging.StreamHandler()
|
||
else:
|
||
handler = logging.FileHandler("bot.log", encoding="utf-8")
|
||
|
||
handler.setFormatter(logging.Formatter(
|
||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||
))
|
||
logger.addHandler(handler)
|
||
|
||
# Выводим информацию о конфигурации
|
||
logger.info(f"Starting Pill Reminder Bot")
|
||
logger.info(f"Database: {DB_FILE}")
|
||
logger.info(f"Log level: {LOG_LEVEL}")
|
||
|
||
# UI константы (не из .env, т.к. это статические строки интерфейса)
|
||
BTN_ADD = "➕ Добавить лекарство"
|
||
BTN_LIST = "📋 Мои лекарства"
|
||
BTN_EDIT = "✏️ Редактировать"
|
||
BTN_DELETE = "🗑 Удалить"
|
||
BTN_HISTORY = "📅 История (7 дней)"
|
||
BTN_TZ = "🌍 Установить часовой пояс"
|
||
BTN_CANCEL = "❌ Отмена"
|
||
|
||
FREQ_EVERY_DAY = "Каждый день"
|
||
FREQ_EVERY_N = "Каждые N дней"
|
||
FREQ_3 = "Каждые 3 дня"
|
||
FREQ_7 = "Каждые 7 дней"
|
||
FREQ_WEEKDAYS = "По дням недели"
|
||
|
||
TZ_SUGGESTIONS = [
|
||
"Europe/Moscow",
|
||
"Asia/Yekaterinburg",
|
||
"Asia/Krasnoyarsk",
|
||
"Asia/Irkutsk",
|
||
"Asia/Vladivostok",
|
||
"Europe/Kaliningrad",
|
||
"Asia/Novosibirsk"
|
||
]
|
||
|
||
# Conversation states
|
||
(
|
||
S_TZ_SELECT,
|
||
S_TZ_MANUAL,
|
||
S_GENDER,
|
||
S_NAME,
|
||
S_ADD_NAME,
|
||
S_ADD_DOSE,
|
||
S_ADD_FREQ_TYPE,
|
||
S_ADD_FREQ_N,
|
||
S_ADD_WEEKDAYS,
|
||
S_ADD_TIMES,
|
||
S_ADD_COURSE,
|
||
S_EDIT_SELECT,
|
||
S_EDIT_FIELD,
|
||
S_EDIT_VALUE,
|
||
S_DELETE_SELECT,
|
||
) = range(15)
|
||
|
||
# ----------------- LOGGING -----------------
|
||
logging.basicConfig(format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", level=LOG_LEVEL)
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# ----------------- DB -----------------
|
||
conn = sqlite3.connect(DB_FILE, check_same_thread=False)
|
||
cursor = conn.cursor()
|
||
|
||
def init_db():
|
||
# create tables if not exists
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INTEGER PRIMARY KEY,
|
||
tz TEXT, -- IANA timezone name
|
||
gender TEXT,
|
||
name TEXT
|
||
)''')
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS pills (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id INTEGER,
|
||
name TEXT,
|
||
dose TEXT,
|
||
interval_days INTEGER DEFAULT 1,
|
||
weekdays TEXT DEFAULT NULL, -- csv of 0..6 Monday..Sunday
|
||
course_days INTEGER DEFAULT 0,
|
||
start_date TEXT -- YYYY-MM-DD
|
||
)''')
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS pill_times (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
pill_id INTEGER,
|
||
time TEXT, -- HH:MM in user's local tz
|
||
last_sent TEXT DEFAULT NULL, -- UTC YYYY-MM-DD HH:MM
|
||
last_confirmed TEXT DEFAULT NULL -- UTC YYYY-MM-DD HH:MM
|
||
)''')
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS messages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
gender TEXT,
|
||
text_message TEXT,
|
||
sticker_id TEXT
|
||
)''')
|
||
cursor.execute('''
|
||
CREATE TABLE IF NOT EXISTS history (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
pill_id INTEGER,
|
||
user_id INTEGER,
|
||
ts TEXT, -- UTC timestamp YYYY-MM-DD HH:MM:SS
|
||
taken INTEGER -- 1 or 0
|
||
)''')
|
||
conn.commit()
|
||
|
||
# populate default messages if empty
|
||
cursor.execute("SELECT COUNT(*) FROM messages")
|
||
if cursor.fetchone()[0] == 0:
|
||
msg_rows = [
|
||
("Я мужчина", "Пора принять {pill} ({dose}).", None),
|
||
("Я мужчина", "Напоминание: {pill} — {dose}.", None),
|
||
("Я женщина", "Пора принять {pill} ({dose}).", None),
|
||
("Я женщина", "Напоминание: {pill} — {dose}.", None),
|
||
]
|
||
cursor.executemany("INSERT INTO messages (gender, text_message, sticker_id) VALUES (?, ?, ?)", msg_rows)
|
||
conn.commit()
|
||
|
||
init_db()
|
||
|
||
# ----------------- HELPERS -----------------
|
||
def main_menu_kb():
|
||
kb = [
|
||
[BTN_ADD, BTN_LIST],
|
||
[BTN_EDIT, BTN_DELETE],
|
||
[BTN_HISTORY, BTN_TZ],
|
||
[BTN_CANCEL]
|
||
]
|
||
return ReplyKeyboardMarkup(kb, resize_keyboard=True, one_time_keyboard=False)
|
||
|
||
def parse_hhmm(s: str) -> Optional[str]:
|
||
s = s.strip()
|
||
try:
|
||
dt = datetime.strptime(s, "%H:%M")
|
||
return dt.strftime("%H:%M")
|
||
except:
|
||
return None
|
||
|
||
def weekdays_list_to_csv(lst: List[int]) -> str:
|
||
return ",".join(str(i) for i in sorted(set(lst))) if lst else ""
|
||
|
||
def weekdays_csv_to_list(csv: Optional[str]) -> List[int]:
|
||
if not csv:
|
||
return []
|
||
return [int(x) for x in csv.split(",") if x!='']
|
||
|
||
def now_utc() -> datetime:
|
||
return datetime.utcnow()
|
||
|
||
def utc_str_min(dt: datetime) -> str:
|
||
return dt.strftime("%Y-%m-%d %H:%M")
|
||
|
||
def utc_str_full(dt: datetime) -> str:
|
||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
# Return user's localized now (datetime) given user's tz name (IANA)
|
||
def localized_now_for_tz(tzname: str) -> datetime:
|
||
try:
|
||
tz = ZoneInfo(tzname)
|
||
except Exception:
|
||
tz = ZoneInfo("UTC")
|
||
return datetime.now(tz)
|
||
|
||
# Convert UTC now to user's local time and return HH:MM
|
||
def local_time_hhmm_for_user(tzname: str, when_utc: datetime) -> str:
|
||
try:
|
||
tz = ZoneInfo(tzname)
|
||
except Exception:
|
||
tz = ZoneInfo("UTC")
|
||
local = when_utc.astimezone(tz)
|
||
return local.strftime("%H:%M")
|
||
|
||
# Convert user's local date/time (today + hh:mm) to UTC datetime (for comparison and storing last_sent)
|
||
def user_local_hhmm_to_utc_dt(tzname: str, local_date: date, hhmm: str) -> datetime:
|
||
hour, minute = map(int, hhmm.split(":"))
|
||
local_dt = datetime(local_date.year, local_date.month, local_date.day, hour, minute, tzinfo=ZoneInfo(tzname))
|
||
return local_dt.astimezone(ZoneInfo("UTC")).replace(tzinfo=None)
|
||
|
||
# ----------------- UI: Timezone selection -----------------
|
||
TZ_SUGGESTIONS = [
|
||
"Europe/Moscow",
|
||
"Asia/Yekaterinburg",
|
||
"Asia/Krasnoyarsk",
|
||
"Asia/Irkutsk",
|
||
"Asia/Vladivostok",
|
||
"Europe/Kaliningrad",
|
||
"Asia/Novosibirsk"
|
||
]
|
||
|
||
def tz_keyboard():
|
||
kb = [[tz] for tz in TZ_SUGGESTIONS]
|
||
kb.append(["Ввести вручную"])
|
||
kb.append([BTN_CANCEL])
|
||
return ReplyKeyboardMarkup(kb, one_time_keyboard=True, resize_keyboard=True)
|
||
|
||
# ----------------- HANDLERS -----------------
|
||
async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user = update.effective_user
|
||
user_id = user.id
|
||
# check user in DB
|
||
cursor.execute("SELECT tz, gender, name FROM users WHERE id=?", (user_id,))
|
||
row = cursor.fetchone()
|
||
if row and row[0] and row[1] and row[2]: # все поля заполнены
|
||
tzname, gender, name = row
|
||
await update.message.reply_text(f"Привет, {name}! Меню:", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
# Not set -> ask tz and name and gender
|
||
await update.message.reply_text("Добро пожаловать! Сначала выберите ваш часовой пояс:", reply_markup=tz_keyboard())
|
||
return S_TZ_SELECT
|
||
|
||
# Отдельный обработчик для кнопки "Установить часовой пояс"
|
||
async def tz_reconfigure(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text("Выберите ваш часовой пояс:", reply_markup=tz_keyboard())
|
||
return S_TZ_SELECT
|
||
|
||
async def tz_select_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
text = update.message.text.strip()
|
||
user_id = update.effective_user.id
|
||
|
||
if text == "Ввести вручную":
|
||
await update.message.reply_text("Введите IANA имя часового пояса (например, Asia/Krasnoyarsk):", reply_markup=ReplyKeyboardRemove())
|
||
return S_TZ_MANUAL
|
||
|
||
if text == BTN_CANCEL:
|
||
await update.message.reply_text("Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
|
||
# validate
|
||
try:
|
||
_ = ZoneInfo(text)
|
||
except Exception:
|
||
await update.message.reply_text("Неверный timezone. Попробуйте ещё раз или выберите 'Ввести вручную'.", reply_markup=tz_keyboard())
|
||
return S_TZ_SELECT
|
||
|
||
# store tz, ask gender
|
||
cursor.execute("INSERT OR REPLACE INTO users (id, tz) VALUES (?, ?)", (user_id, text))
|
||
conn.commit()
|
||
await update.message.reply_text("Выберите пол:", reply_markup=ReplyKeyboardMarkup([["Я мужчина","Я женщина"]], one_time_keyboard=True, resize_keyboard=True))
|
||
return S_GENDER
|
||
|
||
async def tz_manual_input_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
tzname = update.message.text.strip()
|
||
user_id = update.effective_user.id
|
||
try:
|
||
_ = ZoneInfo(tzname)
|
||
except Exception:
|
||
await update.message.reply_text("Неправильный IANA timezone. Попробуйте снова или отмените.", reply_markup=tz_keyboard())
|
||
return S_TZ_MANUAL
|
||
|
||
cursor.execute("INSERT OR REPLACE INTO users (id, tz) VALUES (?, ?)", (user_id, tzname))
|
||
conn.commit()
|
||
await update.message.reply_text("Выберите пол:", reply_markup=ReplyKeyboardMarkup([["Я мужчина","Я женщина"]], one_time_keyboard=True, resize_keyboard=True))
|
||
return S_GENDER
|
||
|
||
async def gender_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user_id = update.effective_user.id
|
||
text = update.message.text.strip()
|
||
|
||
if text not in ("Я мужчина", "Я женщина"):
|
||
await update.message.reply_text("Пожалуйста, выберите вариант на клавиатуре.")
|
||
return S_GENDER
|
||
|
||
cursor.execute("UPDATE users SET gender=? WHERE id=?", (text, user_id))
|
||
conn.commit()
|
||
await update.message.reply_text("Теперь введите ваше имя:", reply_markup=ReplyKeyboardRemove())
|
||
return S_NAME
|
||
|
||
async def name_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user_id = update.effective_user.id
|
||
name = update.message.text.strip()
|
||
cursor.execute("UPDATE users SET name=? WHERE id=?", (name, user_id))
|
||
conn.commit()
|
||
await update.message.reply_text(f"Регистрация завершена. Меню:", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
|
||
# ----------------- Add pill flow -----------------
|
||
async def start_add(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text("Введите название лекарства (например, Витамин D3):", reply_markup=ReplyKeyboardRemove())
|
||
return S_ADD_NAME
|
||
|
||
async def add_name(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
context.user_data['add_name'] = update.message.text.strip()
|
||
await update.message.reply_text("Введите дозу (например, 1 капсула / 1000 IU):")
|
||
return S_ADD_DOSE
|
||
|
||
async def add_dose(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
context.user_data['add_dose'] = update.message.text.strip()
|
||
# frequency choice
|
||
kb = [
|
||
[FREQ_EVERY_DAY, FREQ_EVERY_N],
|
||
[FREQ_3, FREQ_7],
|
||
[FREQ_WEEKDAYS, BTN_CANCEL]
|
||
]
|
||
await update.message.reply_text("Выберите режим приёма:", reply_markup=ReplyKeyboardMarkup(kb, one_time_keyboard=True))
|
||
return S_ADD_FREQ_TYPE
|
||
|
||
async def add_freq_type(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
choice = update.message.text.strip()
|
||
if choice == BTN_CANCEL:
|
||
await update.message.reply_text("Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
if choice == FREQ_EVERY_DAY:
|
||
context.user_data['interval_days'] = 1
|
||
context.user_data['weekdays'] = None
|
||
await update.message.reply_text("Введите одно или несколько времён приёма через запятую (например: 08:00,21:00):", reply_markup=ReplyKeyboardRemove())
|
||
return S_ADD_TIMES
|
||
if choice == FREQ_EVERY_N:
|
||
await update.message.reply_text("Введите N (через сколько дней принимать, например 2 = через день):", reply_markup=ReplyKeyboardRemove())
|
||
return S_ADD_FREQ_N
|
||
if choice == FREQ_3:
|
||
context.user_data['interval_days'] = 3
|
||
context.user_data['weekdays'] = None
|
||
await update.message.reply_text("Введите времена через запятую (например: 08:00,20:00):", reply_markup=ReplyKeyboardRemove())
|
||
return S_ADD_TIMES
|
||
if choice == FREQ_7:
|
||
context.user_data['interval_days'] = 7
|
||
context.user_data['weekdays'] = None
|
||
await update.message.reply_text("Введите времена через запятую (например: 09:00):", reply_markup=ReplyKeyboardRemove())
|
||
return S_ADD_TIMES
|
||
if choice == FREQ_WEEKDAYS:
|
||
# show inline weekday selector
|
||
# We'll manage selection in user_data['wd_sel'] as set
|
||
context.user_data['wd_sel'] = set()
|
||
await send_weekday_selector(update, context)
|
||
return S_ADD_WEEKDAYS
|
||
|
||
async def add_freq_n(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
try:
|
||
n = int(update.message.text.strip())
|
||
if n < 1:
|
||
raise ValueError
|
||
except:
|
||
await update.message.reply_text("Некорректное число. Введите целое положительное N:")
|
||
return S_ADD_FREQ_N
|
||
context.user_data['interval_days'] = n
|
||
context.user_data['weekdays'] = None
|
||
await update.message.reply_text("Введите времена приёма через запятую (пример: 08:00,20:00):")
|
||
return S_ADD_TIMES
|
||
|
||
# Weekday inline keyboard builder and handler
|
||
def build_wd_keyboard(wd_sel: set):
|
||
labels = ["Пн","Вт","Ср","Чт","Пт","Сб","Вс"]
|
||
kb = []
|
||
row = []
|
||
for i, lab in enumerate(labels):
|
||
prefix = "✅" if i in wd_sel else "◻️"
|
||
row.append(InlineKeyboardButton(f"{prefix} {lab}", callback_data=f"wd_toggle:{i}"))
|
||
if (i+1)%3 == 0:
|
||
kb.append(row); row=[]
|
||
if row:
|
||
kb.append(row)
|
||
kb.append([InlineKeyboardButton("Готово", callback_data="wd_done"), InlineKeyboardButton("Отмена", callback_data="wd_cancel")])
|
||
return InlineKeyboardMarkup(kb)
|
||
|
||
async def send_weekday_selector(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
# if original is message, edit or send new
|
||
if update.callback_query:
|
||
await update.callback_query.answer()
|
||
await update.callback_query.edit_message_text("Выберите дни недели:", reply_markup=build_wd_keyboard(context.user_data.get('wd_sel', set())))
|
||
else:
|
||
await update.message.reply_text("Выберите дни недели:", reply_markup=build_wd_keyboard(context.user_data.get('wd_sel', set())))
|
||
|
||
async def weekday_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
query = update.callback_query
|
||
await query.answer()
|
||
data = query.data
|
||
if data.startswith("wd_toggle:"):
|
||
idx = int(data.split(":")[1])
|
||
sel = context.user_data.get('wd_sel', set())
|
||
if idx in sel:
|
||
sel.remove(idx)
|
||
else:
|
||
sel.add(idx)
|
||
context.user_data['wd_sel'] = sel
|
||
await query.edit_message_reply_markup(reply_markup=build_wd_keyboard(sel))
|
||
return
|
||
if data == "wd_done":
|
||
sel = context.user_data.get('wd_sel', set())
|
||
if not sel:
|
||
await query.edit_message_text("Нужно выбрать хотя бы один день или нажмите Отмена.")
|
||
return
|
||
csv = weekdays_list_to_csv(list(sel))
|
||
context.user_data['weekdays'] = csv
|
||
context.user_data['interval_days'] = None
|
||
await query.edit_message_text("Выбраны дни: " + ", ".join(str(i) for i in sorted(sel)))
|
||
await query.message.reply_text("Введите времена приёма через запятую (например: 08:00,20:00):")
|
||
return S_ADD_TIMES
|
||
if data == "wd_cancel":
|
||
await query.edit_message_text("Отменено.")
|
||
await query.message.reply_text("Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
|
||
async def add_times_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
text = update.message.text.strip()
|
||
parts = [p.strip() for p in text.split(",") if p.strip()!='']
|
||
times = []
|
||
for p in parts:
|
||
hh = parse_hhmm(p)
|
||
if not hh:
|
||
await update.message.reply_text(f"Неверное время: {p}. Попробуйте снова ввод всех времён.")
|
||
return S_ADD_TIMES
|
||
times.append(hh)
|
||
context.user_data['times'] = times
|
||
await update.message.reply_text("На сколько дней курс? (0 = бессрочно):")
|
||
return S_ADD_COURSE
|
||
|
||
async def add_course_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
try:
|
||
days = int(update.message.text.strip())
|
||
if days < 0:
|
||
raise ValueError
|
||
except:
|
||
await update.message.reply_text("Введите целое неотрицательное число дней (0 = бессрочно):")
|
||
return S_ADD_COURSE
|
||
context.user_data['course_days'] = days
|
||
# All collected -> store pill and times
|
||
user_id = update.effective_user.id
|
||
name = context.user_data.get('add_name')
|
||
dose = context.user_data.get('add_dose')
|
||
interval_days = context.user_data.get('interval_days') or 1
|
||
weekdays = context.user_data.get('weekdays') # csv or None
|
||
course_days = context.user_data.get('course_days') or 0
|
||
start_date = datetime.now().strftime("%Y-%m-%d")
|
||
cursor.execute('INSERT INTO pills (user_id, name, dose, interval_days, weekdays, course_days, start_date) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
||
(user_id, name, dose, interval_days, weekdays, course_days, start_date))
|
||
pid = cursor.lastrowid
|
||
times = context.user_data.get('times', [])
|
||
for t in times:
|
||
cursor.execute('INSERT INTO pill_times (pill_id, time) VALUES (?, ?)', (pid, t))
|
||
conn.commit()
|
||
await update.message.reply_text(f"Добавлено: {name} — {dose}. Напоминания: {', '.join(times)}", reply_markup=main_menu_kb())
|
||
context.user_data.pop('wd_sel', None)
|
||
return ConversationHandler.END
|
||
|
||
# ----------------- List pills -----------------
|
||
async def list_pills(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user_id = update.effective_user.id
|
||
cursor.execute('SELECT id, name, dose, interval_days, weekdays, course_days, start_date FROM pills WHERE user_id=?', (user_id,))
|
||
rows = cursor.fetchall()
|
||
if not rows:
|
||
await update.message.reply_text("У вас нет добавленных лекарств.", reply_markup=main_menu_kb())
|
||
return
|
||
lines = []
|
||
for r in rows:
|
||
pid, name, dose, interval_days, weekdays, course_days, start_date = r
|
||
times = []
|
||
cursor.execute('SELECT time FROM pill_times WHERE pill_id=?', (pid,))
|
||
for trow in cursor.fetchall():
|
||
times.append(trow[0])
|
||
freq = ""
|
||
if weekdays:
|
||
freq = f"по дням ({weekdays})"
|
||
else:
|
||
freq = f"каждые {interval_days} дн." if interval_days and interval_days>1 else "каждый день"
|
||
lines.append(f"ID {pid}: {name} — {dose} | {', '.join(times)} | {freq} | курс {course_days} (старт {start_date})")
|
||
await update.message.reply_text("\n".join(lines), reply_markup=main_menu_kb())
|
||
|
||
# ----------------- Delete -----------------
|
||
async def start_delete(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text("Введите ID таблетки для удаления (см. список):", reply_markup=ReplyKeyboardRemove())
|
||
return S_DELETE_SELECT
|
||
|
||
async def delete_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
txt = update.message.text.strip()
|
||
try:
|
||
pid = int(txt)
|
||
except:
|
||
await update.message.reply_text("ID должен быть числом. Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
user_id = update.effective_user.id
|
||
cursor.execute('DELETE FROM pill_times WHERE pill_id=?', (pid,))
|
||
cursor.execute('DELETE FROM pills WHERE id=? AND user_id=?', (pid, user_id))
|
||
conn.commit()
|
||
await update.message.reply_text(f"Таблетка {pid} удалена (если была).", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
|
||
# ----------------- Edit flow (simple fields) -----------------
|
||
async def start_edit(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
await update.message.reply_text("Введите ID таблетки для редактирования:", reply_markup=ReplyKeyboardRemove())
|
||
return S_EDIT_SELECT
|
||
|
||
async def edit_select_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
txt = update.message.text.strip()
|
||
try:
|
||
pid = int(txt)
|
||
except:
|
||
await update.message.reply_text("ID должен быть числом. Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
context.user_data['edit_id'] = pid
|
||
kb = [
|
||
['Название', 'Доза'],
|
||
['Время(ы)', 'Частота'],
|
||
['Длительность (дни)', BTN_CANCEL]
|
||
]
|
||
await update.message.reply_text("Выберите что редактировать:", reply_markup=ReplyKeyboardMarkup(kb, one_time_keyboard=True))
|
||
return S_EDIT_FIELD
|
||
|
||
async def edit_field_choice(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
choice = update.message.text.strip().lower()
|
||
pid = context.user_data.get('edit_id')
|
||
if choice == 'название':
|
||
context.user_data['edit_field'] = 'name'
|
||
await update.message.reply_text("Введите новое название:", reply_markup=ReplyKeyboardRemove())
|
||
return S_EDIT_VALUE
|
||
if choice == 'доза':
|
||
context.user_data['edit_field'] = 'dose'
|
||
await update.message.reply_text("Введите новую дозу:", reply_markup=ReplyKeyboardRemove())
|
||
return S_EDIT_VALUE
|
||
if choice == 'время(ы)':
|
||
# instruct to input comma-separated times; will replace existing times
|
||
context.user_data['edit_field'] = 'times'
|
||
await update.message.reply_text("Введите новые времена через запятую (пример: 08:00,20:00):", reply_markup=ReplyKeyboardRemove())
|
||
return S_EDIT_VALUE
|
||
if choice == 'частота':
|
||
context.user_data['edit_field'] = 'freq'
|
||
kb = [
|
||
[FREQ_EVERY_DAY, FREQ_EVERY_N],
|
||
[FREQ_3, FREQ_7],
|
||
[FREQ_WEEKDAYS, BTN_CANCEL]
|
||
]
|
||
await update.message.reply_text("Выберите новую частоту:", reply_markup=ReplyKeyboardMarkup(kb, one_time_keyboard=True))
|
||
return S_EDIT_VALUE
|
||
if choice == 'длительность (дни)':
|
||
context.user_data['edit_field'] = 'course'
|
||
await update.message.reply_text("Введите число дней курса (0 = бессрочно):", reply_markup=ReplyKeyboardRemove())
|
||
return S_EDIT_VALUE
|
||
await update.message.reply_text("Неизвестный выбор.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
|
||
async def edit_value_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
field = context.user_data.get('edit_field')
|
||
pid = context.user_data.get('edit_id')
|
||
user_id = update.effective_user.id
|
||
text = update.message.text.strip()
|
||
|
||
if field == 'name':
|
||
cursor.execute('UPDATE pills SET name=? WHERE id=? AND user_id=?', (text, pid, user_id))
|
||
conn.commit()
|
||
await update.message.reply_text("Название обновлено.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
if field == 'dose':
|
||
cursor.execute('UPDATE pills SET dose=? WHERE id=? AND user_id=?', (text, pid, user_id))
|
||
conn.commit()
|
||
await update.message.reply_text("Доза обновлена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
if field == 'times':
|
||
parts = [p.strip() for p in text.split(",") if p.strip()!='']
|
||
times = []
|
||
for p in parts:
|
||
hh = parse_hhmm(p)
|
||
if not hh:
|
||
await update.message.reply_text(f"Неверное время: {p}. Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
times.append(hh)
|
||
cursor.execute('DELETE FROM pill_times WHERE pill_id=?', (pid,))
|
||
for t in times:
|
||
cursor.execute('INSERT INTO pill_times (pill_id, time) VALUES (?, ?)', (pid, t))
|
||
conn.commit()
|
||
await update.message.reply_text("Времена обновлены.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
if field == 'freq':
|
||
choice = text
|
||
if choice == FREQ_EVERY_DAY:
|
||
cursor.execute('UPDATE pills SET interval_days=1, weekdays=NULL WHERE id=? AND user_id=?', (pid, user_id))
|
||
elif choice == FREQ_3:
|
||
cursor.execute('UPDATE pills SET interval_days=3, weekdays=NULL WHERE id=? AND user_id=?', (pid, user_id))
|
||
elif choice == FREQ_7:
|
||
cursor.execute('UPDATE pills SET interval_days=7, weekdays=NULL WHERE id=? AND user_id=?', (pid, user_id))
|
||
elif choice == FREQ_EVERY_N:
|
||
await update.message.reply_text("Введите N (целое число дней):")
|
||
context.user_data['expect_n_edit'] = True
|
||
return S_EDIT_VALUE
|
||
elif choice == FREQ_WEEKDAYS:
|
||
# launch inline selector similar to add flow
|
||
context.user_data['wd_sel'] = set()
|
||
await send_weekday_selector(update, context)
|
||
return S_EDIT_VALUE
|
||
else:
|
||
# fallback for N or week days text
|
||
if context.user_data.pop('expect_n_edit', False):
|
||
try:
|
||
n = int(text)
|
||
cursor.execute('UPDATE pills SET interval_days=?, weekdays=NULL WHERE id=? AND user_id=?', (n, pid, user_id))
|
||
conn.commit()
|
||
await update.message.reply_text("Частота обновлена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
except:
|
||
await update.message.reply_text("Неверное число. Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
conn.commit()
|
||
await update.message.reply_text("Частота обновлена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
if field == 'course':
|
||
try:
|
||
days = int(text)
|
||
cursor.execute('UPDATE pills SET course_days=? WHERE id=? AND user_id=?', (days, pid, user_id))
|
||
conn.commit()
|
||
await update.message.reply_text("Длительность курса обновлена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
except:
|
||
await update.message.reply_text("Неверный ввод. Отмена.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
await update.message.reply_text("Неизвестная операция.", reply_markup=main_menu_kb())
|
||
return ConversationHandler.END
|
||
|
||
# ----------------- History (7 days) -----------------
|
||
async def history_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
user_id = update.effective_user.id
|
||
|
||
conn = get_db_connection()
|
||
cursor = conn.cursor()
|
||
|
||
try:
|
||
# Получаем сегодняшнюю дату в UTC
|
||
today = datetime.utcnow().date()
|
||
|
||
# Получаем все лекарства пользователя
|
||
cursor.execute(
|
||
'SELECT id, name FROM pills WHERE user_id=? ORDER BY name',
|
||
(user_id,)
|
||
)
|
||
pills = cursor.fetchall()
|
||
|
||
if not pills:
|
||
await update.message.reply_text(
|
||
"💊 У вас пока нет добавленных лекарств.\n"
|
||
"Нажмите '➕ Добавить лекарство', чтобы начать.",
|
||
reply_markup=main_menu_kb()
|
||
)
|
||
conn.close()
|
||
return
|
||
|
||
# Формируем заголовок с датами
|
||
date_range = []
|
||
for d in range(6, -1, -1):
|
||
day = today - timedelta(days=d)
|
||
date_range.append(day)
|
||
|
||
date_header = " | ".join([d.strftime("%d.%m") for d in date_range])
|
||
lines = [f"📅 История за 7 дней:\n📅 {date_header}"]
|
||
|
||
total_pills_with_history = 0
|
||
|
||
for pill in pills:
|
||
pid = pill["id"]
|
||
name = pill["name"]
|
||
|
||
# Проверяем историю для каждого дня
|
||
day_marks = []
|
||
pill_has_history = False
|
||
|
||
for day in date_range:
|
||
day_str = day.strftime("%Y-%m-%d")
|
||
cursor.execute(
|
||
'''SELECT COUNT(*) as cnt FROM history
|
||
WHERE pill_id=? AND user_id=? AND date(ts)=? AND taken=1''',
|
||
(pid, user_id, day_str)
|
||
)
|
||
cnt = cursor.fetchone()["cnt"]
|
||
|
||
if cnt > 0:
|
||
day_marks.append("✅")
|
||
pill_has_history = True
|
||
else:
|
||
day_marks.append("❌")
|
||
|
||
# Добавляем только если есть хотя бы одна отметка
|
||
if pill_has_history:
|
||
total_pills_with_history += 1
|
||
marks_str = " | ".join(day_marks)
|
||
# Обрезаем длинное название лекарства
|
||
display_name = name[:20] + "..." if len(name) > 20 else name
|
||
lines.append(f"💊 {display_name:<23} {marks_str}")
|
||
|
||
# Добавляем статистику
|
||
lines.append("")
|
||
lines.append(f"📊 Статистика: {total_pills_with_history}/{len(pills)} лекарств с историей")
|
||
|
||
if total_pills_with_history == 0:
|
||
await update.message.reply_text(
|
||
"📭 История приёмов пуста.\n\n"
|
||
"Как использовать:\n"
|
||
"1. Бот пришлёт напоминание в указанное время\n"
|
||
"2. Нажмите кнопку '✅ Выпил(-а)' в напоминании\n"
|
||
"3. Здесь появится история ваших приёмов\n\n"
|
||
"Ждём первого подтверждения приёма! 💊",
|
||
reply_markup=main_menu_kb()
|
||
)
|
||
else:
|
||
response = "\n".join(lines)
|
||
# Добавляем легенду
|
||
response += "\n\n✅ - принято\n❌ - не принято"
|
||
|
||
await update.message.reply_text(
|
||
response,
|
||
reply_markup=main_menu_kb()
|
||
)
|
||
|
||
except Exception as e:
|
||
logger.error(f"Error getting history: {e}")
|
||
await update.message.reply_text(
|
||
"❌ Произошла ошибка при получении истории.\n"
|
||
"Попробуйте позже или обратитесь к администратору.",
|
||
reply_markup=main_menu_kb()
|
||
)
|
||
finally:
|
||
conn.close()
|
||
# ----------------- Callback: taken -----------------
|
||
async def callback_taken(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
query = update.callback_query
|
||
await query.answer()
|
||
data = query.data # expected like "take:ptid" where ptid = pill_times.id
|
||
if not data or not data.startswith("take:"):
|
||
await query.edit_message_text("Неизвестная команда.")
|
||
return
|
||
ptid = int(data.split(":",1)[1])
|
||
# find pill_time and pill and user
|
||
cursor.execute('SELECT pill_id FROM pill_times WHERE id=?', (ptid,))
|
||
r = cursor.fetchone()
|
||
if not r:
|
||
await query.edit_message_text("Запись не найдена.")
|
||
return
|
||
pid = r[0]
|
||
cursor.execute('SELECT user_id FROM pills WHERE id=?', (pid,))
|
||
r2 = cursor.fetchone()
|
||
if not r2:
|
||
await query.edit_message_text("Пользователь не найден.")
|
||
return
|
||
user_id = r2[0]
|
||
nowutc = datetime.utcnow()
|
||
now_str = utc_str_min(nowutc)
|
||
# update pill_times.last_confirmed and insert history
|
||
cursor.execute('UPDATE pill_times SET last_confirmed=? WHERE id=?', (now_str, ptid))
|
||
cursor.execute('INSERT INTO history (pill_id, user_id, ts, taken) VALUES (?, ?, ?, 1)', (pid, user_id, utc_str_full(nowutc)))
|
||
conn.commit()
|
||
await query.edit_message_reply_markup(reply_markup=None)
|
||
await query.message.reply_text("✅ Спасибо, что заботишься о своём здоровье 💖", reply_markup=main_menu_kb())
|
||
|
||
# ----------------- Background loop -----------------
|
||
async def background_loop():
|
||
await asyncio.sleep(1)
|
||
logger.info("Background reminder loop started")
|
||
while True:
|
||
try:
|
||
now_utc_dt = datetime.utcnow()
|
||
# fetch all pill_times with linked pill
|
||
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
|
||
''')
|
||
rows = cursor.fetchall()
|
||
for row in rows:
|
||
(ptid, pid, time_hhmm, last_sent, last_confirmed,
|
||
user_id, pill_name, pill_dose, interval_days, weekdays_csv, course_days, start_date, user_tz) = row
|
||
|
||
if not user_tz:
|
||
user_tz = "UTC"
|
||
# User's local current datetime
|
||
try:
|
||
user_local_now = datetime.now(ZoneInfo(user_tz))
|
||
except Exception:
|
||
user_local_now = datetime.now(ZoneInfo("UTC"))
|
||
user_local_hhmm = user_local_now.strftime("%H:%M")
|
||
# If times equal -> candidate for sending
|
||
if user_local_hhmm != time_hhmm:
|
||
# Not this minute
|
||
# But also check for resend logic if last_sent exists and older than 60 minutes and not confirmed
|
||
if last_sent and (not last_confirmed or last_confirmed < last_sent):
|
||
try:
|
||
last_sent_dt = datetime.strptime(last_sent, "%Y-%m-%d %H:%M")
|
||
if (now_utc_dt - last_sent_dt) >= timedelta(minutes=60):
|
||
# resend (one resend)
|
||
cursor.execute('SELECT start_date FROM pills WHERE id=?', (pid,))
|
||
pstart = cursor.fetchone()
|
||
# check course end
|
||
if pstart and pstart[0] and course_days and course_days>0:
|
||
try:
|
||
sd = datetime.strptime(pstart[0], "%Y-%m-%d").date()
|
||
if (datetime.utcnow().date() - sd).days >= course_days:
|
||
continue
|
||
except:
|
||
pass
|
||
# send repeat
|
||
kb = InlineKeyboardMarkup([[InlineKeyboardButton("Выпил(-а)", callback_data=f"take:{ptid}")]])
|
||
text = f"⏰ Напоминание: всё ещё пора принять {pill_name} ({pill_dose})."
|
||
try:
|
||
await app.bot.send_message(chat_id=user_id, text=text, reply_markup=kb)
|
||
now_min = utc_str_min(now_utc_dt)
|
||
cursor.execute('UPDATE pill_times SET last_sent=? WHERE id=?', (now_min, ptid))
|
||
conn.commit()
|
||
except Exception as e:
|
||
logger.exception("Resend failed: %s", e)
|
||
except Exception:
|
||
pass
|
||
continue
|
||
|
||
# If time matches, check schedule constraints: weekdays or interval
|
||
send_flag = True
|
||
# check course end
|
||
if start_date and course_days and course_days>0:
|
||
try:
|
||
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||
if (user_local_now.date() - sd).days >= course_days:
|
||
send_flag = False
|
||
except:
|
||
pass
|
||
if not send_flag:
|
||
continue
|
||
|
||
# weekdays priority
|
||
if weekdays_csv:
|
||
wlist = weekdays_csv_to_list(weekdays_csv)
|
||
if user_local_now.weekday() not in wlist:
|
||
continue
|
||
else:
|
||
# interval_days logic: compute days since start_date
|
||
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:
|
||
pass
|
||
|
||
# Avoid sending multiple times same minute: compare last_sent (UTC minute)
|
||
now_min_utc = utc_str_min(now_utc_dt)
|
||
if last_sent == now_min_utc:
|
||
continue
|
||
|
||
# also if last_confirmed equals last_sent (i.e., already confirmed) skip
|
||
if last_confirmed and last_sent and last_confirmed >= last_sent:
|
||
continue
|
||
|
||
# send reminder
|
||
kb = InlineKeyboardMarkup([[InlineKeyboardButton("Выпил(-а)", callback_data=f"take:{ptid}")]])
|
||
text = f"💊 Пора принять {pill_name} ({pill_dose})."
|
||
try:
|
||
await app.bot.send_message(chat_id=user_id, text=text, reply_markup=kb)
|
||
# update last_sent to UTC minute
|
||
cursor.execute('UPDATE pill_times SET last_sent=? WHERE id=?', (now_min_utc, ptid))
|
||
conn.commit()
|
||
except Exception as e:
|
||
logger.exception("Send failed: %s", e)
|
||
|
||
# sleep until next minute boundary
|
||
except Exception as e:
|
||
logger.exception("Error in background loop: %s", e)
|
||
# wait to next minute
|
||
now2 = datetime.utcnow()
|
||
wait = 60 - now2.second
|
||
await asyncio.sleep(wait)
|
||
|
||
# ----------------- Register handlers -----------------
|
||
app = Application.builder().token(TOKEN).build()
|
||
|
||
# Главный ConversationHandler для настройки timezone/пола/имени
|
||
conv_tz = ConversationHandler(
|
||
entry_points=[
|
||
CommandHandler("start", start_handler),
|
||
MessageHandler(filters.Regex(f'^{BTN_TZ}$'), tz_reconfigure),
|
||
],
|
||
states={
|
||
S_TZ_SELECT: [
|
||
MessageHandler(filters.Regex("^(" + "|".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)],
|
||
},
|
||
fallbacks=[MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'), lambda u,c: (u.message.reply_text("Отмена.", reply_markup=main_menu_kb()), ConversationHandler.END)[1])]
|
||
)
|
||
app.add_handler(conv_tz)
|
||
|
||
# Обработчики кнопок главного меню
|
||
app.add_handler(MessageHandler(filters.Regex(f'^{BTN_LIST}$'), list_pills))
|
||
app.add_handler(MessageHandler(filters.Regex(f'^{BTN_HISTORY}$'), history_handler))
|
||
|
||
# add conv
|
||
conv_add = ConversationHandler(
|
||
entry_points=[MessageHandler(filters.Regex(f'^{BTN_ADD}$'), start_add)],
|
||
states={
|
||
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)],
|
||
},
|
||
fallbacks=[MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'), lambda u,c: (u.message.reply_text("Отмена.", reply_markup=main_menu_kb()), ConversationHandler.END)[1])]
|
||
)
|
||
app.add_handler(conv_add)
|
||
|
||
# edit conv
|
||
conv_edit = ConversationHandler(
|
||
entry_points=[MessageHandler(filters.Regex(f'^{BTN_EDIT}$'), start_edit)],
|
||
states={
|
||
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)$")],
|
||
},
|
||
fallbacks=[MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'), lambda u,c: (u.message.reply_text("Отмена.", reply_markup=main_menu_kb()), ConversationHandler.END)[1])]
|
||
)
|
||
app.add_handler(conv_edit)
|
||
|
||
# delete conv
|
||
conv_del = ConversationHandler(
|
||
entry_points=[MessageHandler(filters.Regex(f'^{BTN_DELETE}$'), start_delete)],
|
||
states={S_DELETE_SELECT: [MessageHandler(filters.TEXT & ~filters.COMMAND, delete_confirm)]},
|
||
fallbacks=[MessageHandler(filters.Regex(f'^{BTN_CANCEL}$'), lambda u,c: (u.message.reply_text("Отмена.", reply_markup=main_menu_kb()), ConversationHandler.END)[1])]
|
||
)
|
||
app.add_handler(conv_del)
|
||
|
||
# callback taken handler
|
||
app.add_handler(CallbackQueryHandler(callback_taken, pattern=r"^take:\d+$"))
|
||
|
||
# Общий обработчик текста (должен быть самым последним!)
|
||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, lambda u,c: u.message.reply_text("Нажмите кнопку в меню.", reply_markup=main_menu_kb())))
|
||
|
||
# ----------------- START BOT -----------------
|
||
def main():
|
||
logger.info("Starting bot...")
|
||
# create background task
|
||
try:
|
||
loop = asyncio.get_event_loop()
|
||
loop.create_task(background_loop())
|
||
except RuntimeError:
|
||
# will be created by run_polling
|
||
pass
|
||
app.run_polling()
|
||
|
||
if __name__ == "__main__":
|
||
main() |