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()
|
||||
88
app/database.py
Normal file
88
app/database.py
Normal file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env python3
|
||||
# database.py
|
||||
|
||||
import sqlite3
|
||||
from config import DB_FILE
|
||||
|
||||
def get_db_connection():
|
||||
"""Создаёт соединение с БД"""
|
||||
conn = sqlite3.connect(DB_FILE, check_same_thread=False)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def init_db():
|
||||
"""Инициализация базы данных"""
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY,
|
||||
tz TEXT,
|
||||
gender TEXT,
|
||||
name TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
)''')
|
||||
|
||||
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,
|
||||
course_days INTEGER DEFAULT 0,
|
||||
start_date TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)''')
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS pill_times (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pill_id INTEGER,
|
||||
time TEXT,
|
||||
last_sent TEXT DEFAULT NULL,
|
||||
last_confirmed TEXT DEFAULT NULL,
|
||||
FOREIGN KEY (pill_id) REFERENCES pills(id) ON DELETE CASCADE
|
||||
)''')
|
||||
|
||||
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,
|
||||
taken INTEGER,
|
||||
FOREIGN KEY (pill_id) REFERENCES pills(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)''')
|
||||
|
||||
# Индексы
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_pills_user_id ON pills(user_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_pill_times_pill_id ON pill_times(pill_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_history_user_pill ON history(user_id, pill_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_history_ts ON history(ts)')
|
||||
|
||||
# Default messages
|
||||
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()
|
||||
conn.close()
|
||||
250
app/handlers.py
Normal file
250
app/handlers.py
Normal file
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
# handlers.py
|
||||
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from telegram import Update, ReplyKeyboardRemove
|
||||
from telegram.ext import ContextTypes, ConversationHandler, CallbackQueryHandler, MessageHandler, filters
|
||||
|
||||
from config import *
|
||||
from utils import *
|
||||
from database import get_db_connection
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ----------------- BASIC HANDLERS -----------------
|
||||
async def help_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Обработчик команды /help"""
|
||||
help_text = """
|
||||
🤖 *Помощь по боту-напоминалке*
|
||||
|
||||
*Основные команды:*
|
||||
/start - Начать работу с ботом
|
||||
/help - Показать это сообщение
|
||||
|
||||
*Как пользоваться:*
|
||||
1. Сначала настройте часовой пояс
|
||||
2. Добавьте лекарства через меню
|
||||
3. Бот будет присылать напоминания
|
||||
4. Нажимайте "✅ Выпил(-а)" в напоминании
|
||||
|
||||
*Меню:*
|
||||
➕ Добавить лекарство - добавить новое лекарство
|
||||
📋 Мои лекарства - список всех лекарств
|
||||
✏️ Редактировать - изменить параметры лекарства
|
||||
🗑 Удалить - удалить лекарство
|
||||
📅 История (7 дней) - история приёмов
|
||||
🌍 Установить часовой пояс - сменить часовой пояс
|
||||
❌ Отмена - отменить текущее действие
|
||||
"""
|
||||
await update.message.reply_text(help_text, parse_mode='Markdown', reply_markup=main_menu_kb())
|
||||
return S_MAIN_MENU
|
||||
|
||||
async def unknown_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Обработчик неизвестных команд"""
|
||||
await update.message.reply_text(
|
||||
"Неизвестная команда. Используйте /help для списка команд.",
|
||||
reply_markup=main_menu_kb()
|
||||
)
|
||||
return S_MAIN_MENU
|
||||
|
||||
async def cancel_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Обработчик кнопки Отмена"""
|
||||
await update.message.reply_text("Отмена.", reply_markup=main_menu_kb())
|
||||
return S_MAIN_MENU
|
||||
|
||||
# ----------------- LIST PILLS -----------------
|
||||
async def list_pills(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user_id = update.effective_user.id
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
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()
|
||||
)
|
||||
conn.close()
|
||||
return S_MAIN_MENU
|
||||
|
||||
lines = []
|
||||
for r in rows:
|
||||
pid = r["id"]
|
||||
name = r["name"]
|
||||
dose = r["dose"]
|
||||
interval_days = r["interval_days"]
|
||||
weekdays = r["weekdays"]
|
||||
course_days = r["course_days"]
|
||||
start_date = r["start_date"]
|
||||
|
||||
cursor.execute(
|
||||
'SELECT time FROM pill_times WHERE pill_id=?',
|
||||
(pid,)
|
||||
)
|
||||
times = [t["time"] for t in cursor.fetchall()]
|
||||
|
||||
freq = ""
|
||||
if weekdays:
|
||||
day_names = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
||||
selected = [day_names[i] for i in weekdays_csv_to_list(weekdays)]
|
||||
freq = f"по дням ({', '.join(selected)})"
|
||||
else:
|
||||
if interval_days > 1:
|
||||
freq = f"каждые {interval_days} дн."
|
||||
else:
|
||||
freq = "каждый день"
|
||||
|
||||
course_info = f"курс {course_days} дн." if course_days > 0 else "бессрочно"
|
||||
|
||||
lines.append(
|
||||
f"ID {pid}: {name} — {dose}\n"
|
||||
f"⏰ Время: {', '.join(times)}\n"
|
||||
f"📅 {freq} | {course_info} (с {start_date})\n"
|
||||
)
|
||||
|
||||
conn.close()
|
||||
|
||||
await update.message.reply_text(
|
||||
"\n".join(lines),
|
||||
reply_markup=main_menu_kb()
|
||||
)
|
||||
return S_MAIN_MENU
|
||||
|
||||
# ----------------- HISTORY -----------------
|
||||
async def history_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user_id = update.effective_user.id
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Проверяем, есть ли вообще история за последние 7 дней
|
||||
seven_days_ago = (datetime.utcnow() - timedelta(days=7)).date().strftime("%Y-%m-%d")
|
||||
|
||||
cursor.execute(
|
||||
'''SELECT COUNT(*) as cnt FROM history
|
||||
WHERE user_id=? AND date(ts) >= ?''',
|
||||
(user_id, seven_days_ago)
|
||||
)
|
||||
has_history = cursor.fetchone()["cnt"] > 0
|
||||
|
||||
if not has_history:
|
||||
await update.message.reply_text(
|
||||
"📭 История приёмов за последние 7 дней пуста.\n\n"
|
||||
"Как только вы отметите приём лекарства (кнопка '✅ Выпил(-а)'), "
|
||||
"здесь появится история.",
|
||||
reply_markup=main_menu_kb()
|
||||
)
|
||||
conn.close()
|
||||
return S_MAIN_MENU
|
||||
|
||||
# Получаем все лекарства пользователя
|
||||
cursor.execute('SELECT id, name FROM pills WHERE user_id=?', (user_id,))
|
||||
pills = cursor.fetchall()
|
||||
|
||||
now = datetime.utcnow().date()
|
||||
lines = []
|
||||
|
||||
for pill in pills:
|
||||
pid = pill["id"]
|
||||
name = pill["name"]
|
||||
|
||||
lines.append(f"💊 {name}:")
|
||||
for d in range(6, -1, -1):
|
||||
day = now - timedelta(days=d)
|
||||
day_str = day.strftime("%Y-%m-%d")
|
||||
cursor.execute('SELECT COUNT(*) as cnt FROM history WHERE pill_id=? AND user_id=? AND date(ts)=?',
|
||||
(pid, user_id, day_str))
|
||||
cnt = cursor.fetchone()["cnt"]
|
||||
mark = "✅" if cnt > 0 else "❌"
|
||||
lines.append(f"{day.strftime('%d.%m')}: {mark}")
|
||||
lines.append("")
|
||||
|
||||
response = "\n".join(lines)
|
||||
await update.message.reply_text(response, reply_markup=main_menu_kb())
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in history_handler: {e}")
|
||||
await update.message.reply_text(
|
||||
"❌ Ошибка при получении истории.",
|
||||
reply_markup=main_menu_kb()
|
||||
)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
return S_MAIN_MENU
|
||||
|
||||
# ----------------- CALLBACK: TAKEN -----------------
|
||||
async def callback_taken(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
query = update.callback_query
|
||||
await query.answer()
|
||||
|
||||
if not query.data or not query.data.startswith("take:"):
|
||||
await query.edit_message_text("Неизвестная команда.")
|
||||
return
|
||||
|
||||
try:
|
||||
ptid = int(query.data.split(":", 1)[1])
|
||||
except ValueError:
|
||||
await query.edit_message_text("Ошибка в данных.")
|
||||
return
|
||||
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
cursor.execute(
|
||||
'''SELECT pt.pill_id, p.user_id, p.name
|
||||
FROM pill_times pt
|
||||
JOIN pills p ON p.id = pt.pill_id
|
||||
WHERE pt.id=?''',
|
||||
(ptid,)
|
||||
)
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
await query.edit_message_text("Запись не найдена.")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
pid = row["pill_id"]
|
||||
user_id = row["user_id"]
|
||||
pill_name = row["name"]
|
||||
|
||||
now_utc = datetime.utcnow()
|
||||
now_min_str = utc_str_min(now_utc)
|
||||
now_full_str = utc_str_full(now_utc)
|
||||
|
||||
cursor.execute(
|
||||
'UPDATE pill_times SET last_confirmed=? WHERE id=?',
|
||||
(now_min_str, ptid)
|
||||
)
|
||||
|
||||
cursor.execute(
|
||||
'''INSERT INTO history (pill_id, user_id, ts, taken)
|
||||
VALUES (?, ?, ?, 1)''',
|
||||
(pid, user_id, now_full_str)
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
|
||||
await query.edit_message_reply_markup(reply_markup=None)
|
||||
await query.message.reply_text(
|
||||
f"✅ Приём '{pill_name}' зарегистрирован!\n"
|
||||
"Спасибо, что заботишься о своём здоровье 💖",
|
||||
reply_markup=main_menu_kb()
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in callback_taken: {e}")
|
||||
await query.edit_message_text("Произошла ошибка.")
|
||||
finally:
|
||||
conn.close()
|
||||
94
app/utils.py
Normal file
94
app/utils.py
Normal file
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
# utils.py
|
||||
|
||||
from datetime import datetime, date
|
||||
from typing import List, Optional
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from telegram import ReplyKeyboardMarkup, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from config import BTN_ADD, BTN_LIST, BTN_EDIT, BTN_DELETE, BTN_HISTORY, BTN_TZ, BTN_CANCEL, TZ_SUGGESTIONS
|
||||
|
||||
# ----------------- UI HELPERS -----------------
|
||||
def main_menu_kb():
|
||||
kb = [
|
||||
[BTN_ADD, BTN_LIST],
|
||||
[BTN_EDIT, BTN_DELETE],
|
||||
[BTN_HISTORY, BTN_TZ]
|
||||
]
|
||||
return ReplyKeyboardMarkup(kb, resize_keyboard=True, one_time_keyboard=False)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
# ----------------- DATA HELPERS -----------------
|
||||
def parse_hhmm(s: str) -> Optional[str]:
|
||||
s = s.strip()
|
||||
try:
|
||||
dt = datetime.strptime(s, "%H:%M")
|
||||
return dt.strftime("%H:%M")
|
||||
except ValueError:
|
||||
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.isdigit()]
|
||||
|
||||
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")
|
||||
|
||||
def localized_now_for_tz(tzname: str) -> datetime:
|
||||
try:
|
||||
tz = ZoneInfo(tzname)
|
||||
except Exception:
|
||||
tz = ZoneInfo("UTC")
|
||||
return datetime.now(tz)
|
||||
|
||||
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")
|
||||
|
||||
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)
|
||||
try:
|
||||
tz = ZoneInfo(tzname)
|
||||
local_dt = local_dt.replace(tzinfo=tz)
|
||||
except Exception:
|
||||
tz = ZoneInfo("UTC")
|
||||
local_dt = local_dt.replace(tzinfo=tz)
|
||||
return local_dt.astimezone(ZoneInfo("UTC")).replace(tzinfo=None)
|
||||
Reference in New Issue
Block a user