Ответ пользователю через определённый промежуток времени (чат-бот)

Как сделать так чтобы чат-бот отвечал пользователю через определенный промежуток времени, но так чтобы не затрагивалась работа бота в целом (то есть, time.sleep() не подходит т. к. он просто тормозит работу всего бота)

Главное условие - бот должен писать сообщение первым (по прошествии 1 минуты). Возможно стоит использовать threading или multiprocessing, но я совершенно не понимаю как это внедрить в мой код.

Важное замечание - вся информация о пользователе (в частности chat_id) хранится в словаре, который загружается при запуске и сохраняется при выключении программы, с помощью библиотеки pickle

Архитектура бота такова:

Файл bot.py:

def main():

    bot = Bot(token=TOKEN, name=NAME, version=VERSION)

    # Handler for start command
    bot.dispatcher.add_handler(StartCommandHandler(callback=start_cb))

    # Handler for simple text message without media content
    bot.dispatcher.add_handler(MessageHandler(filters=Filter.text, callback=message_cb))

    # Handler for help command
    bot.dispatcher.add_handler(CommandHandler(command="help", callback=help_cb))

    # Any other user command handler
    bot.dispatcher.add_handler(CommandHandler(command="team", callback=team_cb))
    bot.dispatcher.add_handler(CommandHandler(command="rules", callback=rules_cb))
    bot.dispatcher.add_handler(CommandHandler(command="play", callback=play_cb))
    bot.dispatcher.add_handler(CommandHandler(command="top", callback=show_leaderboard))
    bot.dispatcher.add_handler(CommandHandler(command="stop", callback=stop_playing_cb))

    # Handler for unknown commands
    bot.dispatcher.add_handler(UnknownCommandHandler(callback=unknown_command_cb))

    # Handlers for other file types
    bot.dispatcher.add_handler(MessageHandler(filters=Filter.image, callback=image_cb))

    # Handler for add user to chat
    bot.dispatcher.add_handler(NewChatMembersHandler(callback=im_new_chat_member_cb))

    bot.start_polling()

    bot.idle()

    signal.signal(signal.SIGTERM, my_handler())


if __name__ == '__main__':

    main()

Файл logic.py - это набор функций, который используются хендлерами

Не закидывайте тряпками, бот для ICQ New... Библиотека - mailru-im-bot


Ответы (3 шт):

Автор решения: Shamus Rezol

Совершенно верно на счет многопоточности. Как на счет запустить кастомный поток, в котором и будет испльзоваться задержка перед отправкой сообщения методом бота(? Не уверен в конкретной реализации этого в вашей библиотеке).

Я накидал пример кода. И помните, что это набросок, а не рабочая реализация. Все потому, что я не знаком с используемой вами библиотекой бота.

import threading
import time

class Sender(threading.Thread):
    """ Отправляет сообщение пользователю с задержкой.
    """
    def __init__(self, bot, chat_id, message, delay: int):
        #
        #   @bot, ботик /_:
        #
        #   @chat_id (или все таки chad_id?), идентификатор чата.
        #
        #   @message, сообщение для отправки.
        #
        #   @delay, int, задержка перед отправкой в секундах.
        #

        """ Инициализируем поток.
        """
        super().__init__()

        """ Запоминаем то, что нам понадобится:
        """
        self.bot        =bot
        self.chat_id    =chat_id
        self.message    =message
        self.delay      =delay

        """ ВСЕ! Ладно. Не забудь вызвать Sender().start() после создания нового экземпляра потока.
            Иначе ничего не будет :)
        """

    def run(self):
        """ Класс потока, который вызывается вместе со start().
        """

        """ Воот. Нам нужно подождать перед отправкой сообщения.
            В ЭТОМ ПОТОКЕ. То есть основной поток никак не зафризит.
        """
        time.sleep(self.delay)

        """ Привет замочкам! Хорошей манерой будет заблокировать тут замочек на тот случай,
            если несколько потоков захотят отправить сообщение с интервалом в 1 милисекунду,
            а ботик этого не любит. Потом следует сделать задержку перед разблокировкой,
            но я не буду тут все усложнять. Оставляю это опционально для вас.
        """

        """ А еще я не имею понятия,
            как отправить сообщение через вашего бота,
            поэтому то это болванка реализации кода.
            Тут ваша ставка!
        """
        # может быть код ниже?
        #self.bot.send_message_to_chat(self.chat_id, self.message)

        """ Я уделил минутку гуглу, и вроде это то:
        """
        #self.bot.send_text(chat_id=self.chat_id, text=self.message)

        """ Готово! Ты великолепен! Поток будет уничтожен примерно СЕЙЧАС.
        """

#Sender(my_bot, chat_id, message, delay_in_sec).start()

Можете протестить то, что основной поток не блокируется, а сообщение будет успешно отправлено в срок, к примеру небольшими модификациями наброска выше с использованием curses.

→ Ссылка
Автор решения: Mamba

Так или иначе здесь придется использовать threading, для того, в идеале создать особый класс который будет собирать данные и отправлять их поистечению определенного времени, но можно сделать все куда проще и реализовать "отложенную отправку".

from time import sleep
import threading
import sys
import datetime


def send(to, message):
  # Твой код обыкновенной отправки сообщения
  return f"Я отправил сообщение {to}: {message}!"

# Для наглядности, принтуем вместе с текущим временем
def myprint(message):
    d = datetime.datetime.strftime(datetime.datetime.now(), "%H:%M:%S")
    sys.stdout.write("{} - [{}] - {}\n".format(d, threading.current_thread().getName(), message))
    sys.stdout.flush()

def deffered(time, callback, target, *args, **kwargs):
  def _deffered(*args, **kwargs):
    sleep(time)
    response = target(*args, **kwargs)
    callback(response)

  threading.Thread(target=_deffered, args=args, kwargs=kwargs).start()


if __name__ == '__main__':
    # Обыкновенная отправка:
    myprint("До отправки")
    msg = send("Алёша", "Привет")
    myprint(msg)
    myprint("Код работает дальше")


    # Отложенная отправка:
    myprint("До отправки")
    def _result(msg):
        myprint(msg)

    # 1       - Время в секундах, через которое мы хотим вызвать функцию
    # _result - Наша приватная функция, которая в итоге получит то, что вернет send
    # send    - Функция, которую нужно вызвать
    # ...     - Дальше идут любые другие аргументы в соответствии с вызываемой функцией/методом
    deffered(2, _result, send, "Миша", "Здаров")
    myprint("Код работает дальше")

output (Обыкновенный вызов send):

19:24:23 - [MainThread] - До отправки
19:24:23 - [MainThread] - Я отправил сообщение Алёша: Привет!
19:24:23 - [MainThread] - Код работает дальше

output (Использование deffered):

19:24:23 - [MainThread] - До отправки
19:24:23 - [MainThread] - Код работает дальше
19:24:24 - [Thread-1] - Я отправил сообщение Миша: Здаров!

В любом случае вы должны позаботиться о том, чтобы данные, которые используються разными потоками либо не пересекались(Вы не должны использовать одни и те же данные в разных потоках), либо защищать их с помощью локов. В пакете threading есть класс Lock, который вы должны использовать в таких случаях. Работает это примерно таким образом:

import threading
import datetime
import sys

# Для наглядности, принтуем вместе с текущим временем
def myprint(message):
    d = datetime.datetime.strftime(datetime.datetime.now(), "%H:%M:%S")
    sys.stdout.write("{} - [{}] - {}\n".format(d, threading.current_thread().getName(), message))
    sys.stdout.flush()


# Данные для общего доступа между потоками
data = 0
lock = threading.Lock()

class MyThread(threading.Thread):
    def run(self):
        while True:
            global lock
            global data

            '''
            acquire блокирует текущий lock, 
            таким образом когда следующий поток вызовет данную функцию
            он остановится и будет ждать, пока lock будет разблокирован.
            '''
            lock.acquire()
            if self.is_finish():
                myprint("finish")
                # Перед выходом из функции разблокируем лок!
                lock.release()
                return

            data += 1

            if self.is_finish():
                myprint("Win > " + str(data) + ", " + hex(data))
                # Перед выходом из функции разблокируем лок!
                lock.release()
                return

            '''
            Вы должны разблокировать Lock в любом случае, 
            иначе никто больше не сможет добраться до data.
            Если у вас несколько выходов из функции, вы должны сделать это перед каждым из них
            Таким образом мы сообщаем, что мы закончили работу с данными и любой другой может сделать тоже самое
            '''
            lock.release()

    def is_finish(self):
        global data
        return data >= 2 ** 20


for _ in range(10):
    MyThread().start()

Данная программа создаст 10 потоков, которые будут вместе увеличивать значение data. В данном контексте мы будем, образно, ждать победивший поток, который первый доберется до числа 0x100000, победитель будет только один, вывод:

19:44:23 - [Thread-6] - Win > 1048576, 0x100000
19:44:23 - [Thread-8] - finish
19:44:23 - [Thread-7] - finish
19:44:23 - [Thread-1] - finish
19:44:23 - [Thread-10] - finish
19:44:23 - [Thread-2] - finish
19:44:23 - [Thread-5] - finish
19:44:23 - [Thread-9] - finish
19:44:23 - [Thread-3] - finish
19:44:23 - [Thread-4] - finish

В данном примере победил поток №6.

Если бы данные были не защищенны мы бы увидели примерно такой ответ

19:45:20 - [Thread-6] - Win > 1048576, 0x100000
19:45:20 - [Thread-8] - Win > 1048577, 0x100001
19:45:20 - [Thread-5] - Win > 1048577, 0x100001
19:45:20 - [Thread-7] - finish
19:45:20 - [Thread-9] - Win > 1048577, 0x100001
19:45:20 - [Thread-4] - Win > 1048578, 0x100002
19:45:20 - [Thread-10] - finish
19:45:20 - [Thread-1] - Win > 1048578, 0x100002
19:45:20 - [Thread-2] - finish
19:45:20 - [Thread-3] - Win > 1048576, 0x100000

Это происходило бы из-за того, что каждый поток нападал бы на data практически одновременно и в один и тот же момент они одновременно делают 0xfffff + 1 и каждый считает, что он победил

→ Ссылка
Автор решения: Nixs Code

Это можно реализовать модулями threading и модулем time:

import time
from threading import Thread # Импортируем модуль для создания потока
def main():
    time.sleep(time) # Задержка в милисекундах
    bot.send_message(message.from_user.id, text) # Ответ
thr = Thread(target = main) # Создаем поток
thr.start() Запускаем поток
→ Ссылка