Как запустить планировщик aioschedule для нескольких юзеров одновременно

Рейтинг: -1Ответов: 1Опубликовано: 04.05.2023

Пишу бот-напоминалку на 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)

Ответы

▲ 0Принят

Предполагаю, что проблема в функций input_reminder:

async def input_reminder(message: types.Message, state: FSMContext):
    ...
    while aioschedule.jobs:
            await aioschedule.run_pending()
            await asyncio.sleep(0.1)

Это участок кода запускать один раз, а не для квждого пользователя, например при запуске бота:

...
async def reminder(chat_id):
    ...

async def input_reminder(message: types.Message, state: FSMContext):
    ...
    aioschedule.every().days.at(user_local_time).do(reminder, message.chat.id)

async def on_startup(_):
    asyncio.create_task(run_scheduler())

async def run_scheduler():
    while True:
        await schedule.run_pending()
        await asyncio.sleep(1)

executor.start_polling(dp, on_startup=on_startup)

Нет сброса состояние FSM при завершений диалога с пользователем, aioschedule.clear() удвлить все задвчи из планировщика. В функций reminder можно передовать один параметр chat_id async def reminder(chat_id)