Как запустить планировщик aioschedule для нескольких юзеров одновременно
Пишу бот-напоминалку на aiogram2, столкнулся со следующей проблемой:
Мой бот записывает данные в две таблицы. Данные о пользователе в одну, данные о его расписании в другую. Когда пользователь завершает ввод всех необходимых данных, в хендлере должен запускаться планировщик, который отсылает ему расписание на день в нужное время, когда расписание заканчивается - планировщик должен прекращать свою работу, а пользователь заново вводить часть данных. При этом программа не должна останавливаться.
Все работает как нужно, пока пользователь один, но как только их становится 2 и больше - программа начинает вести себя неожиданным для меня образом - при выводе данных, вместо вывода расписания на один день, программа выводит столько расписаний, сколько пользователей подключаются, иногда больше.
Подозреваю, что нужно либо создать отдельный поток и запускать планировщик там, либо что то еще. В силу того, что в программировании я любитель, у меня не получается найти решение проблемы.
Вот мой код, буду признателен за помощь) :
import time
from datetime import datetime, timedelta
import pytz # таймзоны
from timezonefinder import TimezoneFinder # поиск timezone по широте и долготе
import re
from config import TOKEN
import asyncio
import aioschedule
from database_create import DataBase
from aiogram import Bot, Dispatcher # _______________________________ импортируем классы бот и диспетчер
from aiogram.utils.executor import start_polling # _______________________ импортируем бесконечный опрос
from aiogram.dispatcher.filters import Command, CommandStart, StateFilter, Text # импортируем нужные типы фильтров
from aiogram.dispatcher.filters.state import State, StatesGroup # __________ имортируем состояния и группу состояний
from aiogram.dispatcher.filters.state import default_state # __________ импортируем состояние по умолчанию
from aiogram.dispatcher.storage import FSMContext # ___________________ импортируем класс хранения контекста
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram import types # __________________________________________ импортируем типы данных
# функция перерасчета времени по часовому поясу
def determining_user_time(entered_time: str, lng: str, lat: str) -> str:
hours, minutes = entered_time.split(':')
entered_time = timedelta(hours=int(hours), minutes=int(minutes))
tz_str_user: str = TimezoneFinder().timezone_at(lng=lng, lat=lat)
timezone_server = pytz.timezone('Asia/Oral') # 'Europe/Moscow'
timezone_user = pytz.timezone(tz_str_user)
dt = datetime.utcnow()
offset_server = timezone_server.utcoffset(dt)
offset_user = timezone_user.utcoffset(dt)
if offset_server > offset_user:
diference = offset_server - offset_user
user_local_time = entered_time + diference
elif offset_server < offset_user:
deference = offset_user - offset_server
user_local_time = entered_time - deference
else:
user_local_time = entered_time
hours = user_local_time.seconds // 3600
minutes = (user_local_time.seconds // 60) % 60
return f'{hours}:{minutes}'
################################ Функция, которую запускает планировщик ########################
# функция отправки сообщения для планировщика
async def reminder(bot: Bot, chat_id, state: FSMContext):
print('reminder работаеt')
with DataBase(path_db='bot_reminder.db') as db:
reminder = db.query_data('SELECT * FROM weekday WHERE id_person = ?', (chat_id,))[0][1:]
days = [name[0] for name in db.cursor.description][1:]
days_reminder = [(k, v) for k, v in zip(days, reminder) if v]
if days_reminder:
day, text = days_reminder.pop(0)
db.execute(f'UPDATE weekday SET {day} = ? WHERE id_person = ?', (None, chat_id))
db.commit()
await bot.send_message(chat_id=chat_id, text=text)
else:
await bot.send_message(chat_id=chat_id, text='Ваше расписание на неделю закончилось.\n'
'Что бы установить новое:\n'
'Введите время ежедневного напоминания в формате чч:мм')
await state.set_state(FSMstates.fill_time.state)
aioschedule.clear()
db.execute('DELETE FROM weekday WHERE id_person = ?', (chat_id,))
db.execute('DELETEFROM person WHERE person_id = ?', (chat_id,))
db.commit()
#####################################################################################
# Создаем объекты
storage: MemoryStorage = MemoryStorage() # хранилище состояний
bot: Bot = Bot(token=TOKEN) # бот
dispatcher: Dispatcher = Dispatcher(bot=bot, storage=storage) # диспетчер
# Cоздаем класс, наследуемый от StatesGroup, для группы состояний нашей FSM
class FSMstates(StatesGroup):
fill_geolocation = State() # состояние ожидания отправки геолокации
fill_time = State() # состояние ожидания корректного времени
fill_reminder = State() # состояние ожидания заполнения расписания
@dispatcher.message_handler(CommandStart, state=default_state)
async def start_bot(message: types.Message, state: FSMContext):
print('start_bot')
with DataBase(path_db='bot_reminder.db') as db: # создаем базу данных и таблицы, если их нет
db.execute('''CREATE TABLE IF NOT EXISTS person
(person_id INTEGER PRIMARY KEY, time_reminders TEXT, latitude REAL, longitude REAL)''')
db.execute('''CREATE TABLE IF NOT EXISTS weekday(
id_person INTEGER PRIMARY KEY,
Monday TEXT,
Tuesday TEXT,
Wednesday TEXT,
Thursday TEXT,
Friday TEXT,
Saturday TEXT,
Sunday TEXT,
FOREIGN KEY(id_person) REFERENCES person(person_id))
''')
db.commit()
markup = types.ReplyKeyboardMarkup(row_width=1, resize_keyboard=True)
markup.add(types.KeyboardButton(text='Отправь геолокацию', request_location=True))
await message.answer(text='Бот начал работу. Отправь свою геолокацию, что бы бот мог определить твой часовой пояс',
reply_markup=markup)
await state.set_state(FSMstates.fill_geolocation.state)
# передаем в handler кастомный фильтр и дефолтное состояние
@dispatcher.message_handler(content_types=['location'], state=FSMstates.fill_geolocation)
# в хендлере стоит конструктор, создающий элемент состояния
async def input_location(message: types.Message, state: FSMContext):
print('input_location')
with DataBase(path_db='bot_reminder.db') as db: # записываем id, широту и долготу в базу
db.execute('INSERT OR IGNORE INTO person (person_id, latitude, longitude) VALUES(?, ?, ?)',
(message.from_user.id, message.location.latitude, message.location.longitude))
db.commit()
await message.answer(text='Введите время ежедневного напоминания в формате чч:мм',
reply_markup=types.ReplyKeyboardRemove())
await state.set_state(FSMstates.fill_time.state) # устанавливаем состояние ожидания ввода времени
# передаем в handler фильтр на любой текст и сосотояние ожидания ввода корректного времени
@dispatcher.message_handler(lambda x: True, state=FSMstates.fill_time)
async def input_time(message: types.Message, state: FSMContext):
print('input_time')
if bool(re.fullmatch(r'\d{2}:\d{2}', message.text)) != True: # улучшить проверку
await message.answer(text='То, что вы ввели, не соответсвует нужному формату')
return # прерываем функцию, сохраняя статус ожидания
else:
with DataBase(path_db='bot_reminder.db') as db: # записываем время в базу
db.execute('UPDATE person SET time_reminders = ? WHERE person_id = ?', (message.text, message.from_user.id))
db.commit()
await message.answer(f'Время вашего напоминания: {message.text}\n'
'Напишите свои задачи на неделю в порядке следования дней недели в формате:\n'
'1. задачи\n2. задачи\n3. задачи\n.....')
await state.set_state(FSMstates.fill_reminder.state) # устанавливаем состояние ожидания ввода расписания
# передаем в handler фильтр на любой текст и сосотояние ожидания ввода корректного расписания
@dispatcher.message_handler(lambda x: True, state=FSMstates.fill_reminder)
async def input_reminder(message: types.Message, state: FSMContext):
print('input_reminder')
reminder_week = tuple(map(lambda x: x[3:], [i for i in message.text.split('\n\n')]))
with DataBase(path_db='bot_reminder.db') as db: # заполнение таблицы weekday
db.execute('INSERT OR IGNORE INTO weekday VALUES(?, ?, ?, ?, ?, ?, ?, ?)',
(message.from_user.id, *reminder_week))
db.commit()
time_user, lng, lat = db.query_data('SELECT time_reminders, latitude, longitude FROM person WHERE person_id = ?',
(message.from_user.id,))[0]
user_local_time = determining_user_time(time_user, lng=lng, lat=lat)
await message.answer('Ваше расписание записано, я напомню вам о нем в заданное время')
########### Вот тут должен запускаться планировщик #########################
aioschedule.every().days.at(user_local_time).do(reminder, bot, message.chat.id, state)
while aioschedule.jobs:
await aioschedule.run_pending()
await asyncio.sleep(0.1)
###########################################################################################
if __name__ == '__main__':
try:
start_polling(dispatcher=dispatcher, skip_updates=True)
except Exception as e:
print(e)
time.sleep(5)