update docker
This commit is contained in:
915
main.py
915
main.py
@@ -0,0 +1,915 @@
|
||||
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
|
||||
cursor.execute('SELECT id, name FROM pills WHERE user_id=?', (user_id,))
|
||||
pills = cursor.fetchall()
|
||||
if not pills:
|
||||
await update.message.reply_text("Нет данных о лекарствах.", reply_markup=main_menu_kb())
|
||||
return
|
||||
now = datetime.utcnow().date()
|
||||
lines = []
|
||||
for pid, name in pills:
|
||||
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(*) FROM history WHERE pill_id=? AND user_id=? AND date(ts)=?', (pid, user_id, day_str))
|
||||
cnt = cursor.fetchone()[0]
|
||||
mark = "✅" if cnt>0 else "❌"
|
||||
lines.append(f"{day.strftime('%d.%m')}: {mark}")
|
||||
lines.append("")
|
||||
await update.message.reply_text("\n".join(lines), reply_markup=main_menu_kb())
|
||||
|
||||
# ----------------- 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()
|
||||
Reference in New Issue
Block a user