From b912e4dbaf190362e8236c01f58d4ffeaf1be07a Mon Sep 17 00:00:00 2001 From: bedilbek Date: Sun, 12 Apr 2020 01:41:34 +0500 Subject: [PATCH 01/13] Update with middleware handler --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 9d78f0d..22470a3 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,24 @@ In bot2.0 update. You can get `callback_query` in update object. In telebot use def test_callback(call): logger.info(call) ``` +#### Middleware Handler + +A middleware handler is a function that allows you to modify requests or the bot context as they pass through the +Telegram to the bot. You can imagine middleware as a chain of logic connection handled before any other handlers are +executed. + +```python +@bot.middleware_handler(update_types=['message']) +def modify_message(bot_instance, message): + # modifying the message before it reaches any other handler + message.another_text = message.text + ':changed' + +@bot.message_handler(commands=['start']) +def start(message): + # the message is already modified when it reaches message handler + assert message.another_text == message.text + ':changed' +``` +There are other examples using middleware handler in the [examples/middleware](examples/middleware) directory. #### TeleBot ```python From 68330c9a0708de1d07edf734db51a5f83e023ff1 Mon Sep 17 00:00:00 2001 From: bedilbek Date: Sun, 12 Apr 2020 01:44:15 +0500 Subject: [PATCH 02/13] Update contents with middleware handler --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 22470a3..7ecb129 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ * [General use of the API](#general-use-of-the-api) * [Message handlers](#message-handlers) * [Callback Query handlers](#callback-query-handler) + * [Middleware handlers](#middleware-handler) * [TeleBot](#telebot) * [Reply markup](#reply-markup) * [Inline Mode](#inline-mode) From 286188f380e98028d03a8fdb543296cca09be480 Mon Sep 17 00:00:00 2001 From: bedilbek Date: Sun, 12 Apr 2020 22:41:32 +0500 Subject: [PATCH 03/13] Add Step/Reply Handler Backend Mechanism Implement Memory, File, Redis Backends --- requirements.txt | 1 + setup.py | 1 + telebot/__init__.py | 191 ++++++++++++++---------------------- telebot/handler_backends.py | 144 +++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 120 deletions(-) create mode 100644 telebot/handler_backends.py diff --git a/requirements.txt b/requirements.txt index 6e4ca40..f58156a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ pytest==3.0.2 requests==2.20.0 six==1.9.0 wheel==0.24.0 +redis==3.4.1 diff --git a/setup.py b/setup.py index 44a5cde..7c7c856 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ setup(name='pyTelegramBotAPI', install_requires=['requests', 'six'], extras_require={ 'json': 'ujson', + 'redis': 'redis>=3.4.1' }, classifiers=[ 'Development Status :: 5 - Production/Stable', diff --git a/telebot/__init__.py b/telebot/__init__.py index 72f6c71..89b792a 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -2,8 +2,6 @@ from __future__ import print_function import logging -import os -import pickle import re import sys import threading @@ -23,6 +21,7 @@ logger.addHandler(console_output_handler) logger.setLevel(logging.ERROR) from telebot import apihelper, types, util +from telebot.handler_backends import MemoryHandlerBackend, FileHandlerBackend """ Module : telebot @@ -43,64 +42,6 @@ class Handler: return getattr(self, item) -class Saver: - """ - Class for saving (next step|reply) handlers - """ - - def __init__(self, handlers, filename, delay): - self.handlers = handlers - self.filename = filename - self.delay = delay - self.timer = threading.Timer(delay, self.save_handlers) - - def start_save_timer(self): - if not self.timer.is_alive(): - if self.delay <= 0: - self.save_handlers() - else: - self.timer = threading.Timer(self.delay, self.save_handlers) - self.timer.start() - - def save_handlers(self): - self.dump_handlers(self.handlers, self.filename) - - def load_handlers(self, filename, del_file_after_loading=True): - tmp = self.return_load_handlers(filename, del_file_after_loading=del_file_after_loading) - if tmp is not None: - self.handlers.update(tmp) - - @staticmethod - def dump_handlers(handlers, filename, file_mode="wb"): - dirs = filename.rsplit('/', maxsplit=1)[0] - os.makedirs(dirs, exist_ok=True) - - with open(filename + ".tmp", file_mode) as file: - if (apihelper.CUSTOM_SERIALIZER is None): - pickle.dump(handlers, file) - else: - apihelper.CUSTOM_SERIALIZER.dump(handlers, file) - - if os.path.isfile(filename): - os.remove(filename) - - os.rename(filename + ".tmp", filename) - - @staticmethod - def return_load_handlers(filename, del_file_after_loading=True): - if os.path.isfile(filename) and os.path.getsize(filename) > 0: - with open(filename, "rb") as file: - if (apihelper.CUSTOM_SERIALIZER is None): - handlers = pickle.load(file) - else: - handlers = apihelper.CUSTOM_SERIALIZER.load(file) - - if del_file_after_loading: - os.remove(filename) - - return handlers - - class TeleBot: """ This is TeleBot Class Methods: @@ -141,7 +82,10 @@ class TeleBot: answerInlineQuery """ - def __init__(self, token, threaded=True, skip_pending=False, num_threads=2): + def __init__( + self, token, threaded=True, skip_pending=False, num_threads=2, + next_step_backend=None, reply_backend=None + ): """ :param token: bot API token :return: Telebot object. @@ -155,14 +99,13 @@ class TeleBot: self.last_update_id = 0 self.exc_info = None - # key: message_id, value: handler list - self.reply_handlers = {} + self.next_step_backend = next_step_backend + if not self.next_step_backend: + self.next_step_backend = MemoryHandlerBackend() - # key: chat_id, value: handler list - self.next_step_handlers = {} - - self.next_step_saver = None - self.reply_saver = None + self.reply_backend = reply_backend + if not self.reply_backend: + self.reply_backend = MemoryHandlerBackend() self.message_handlers = [] self.edited_message_handlers = [] @@ -196,51 +139,89 @@ class TeleBot: def enable_save_next_step_handlers(self, delay=120, filename="./.handler-saves/step.save"): """ - Enable saving next step handlers (by default saving disable) + Enable saving next step handlers (by default saving disabled) + + This function explicitly assigns FileHandlerBackend (instead of Saver) just to keep backward + compatibility whose purpose was to enable file saving capability for handlers. And the same + implementation is now available with FileHandlerBackend + + Most probably this function should be deprecated in future major releases :param delay: Delay between changes in handlers and saving :param filename: Filename of save file """ - self.next_step_saver = Saver(self.next_step_handlers, filename, delay) + self.next_step_backend = FileHandlerBackend(self.next_step_backend.handlers, filename, delay) def enable_save_reply_handlers(self, delay=120, filename="./.handler-saves/reply.save"): """ Enable saving reply handlers (by default saving disable) + This function explicitly assigns FileHandlerBackend (instead of Saver) just to keep backward + compatibility whose purpose was to enable file saving capability for handlers. And the same + implementation is now available with FileHandlerBackend + + Most probably this function should be deprecated in future major releases + :param delay: Delay between changes in handlers and saving :param filename: Filename of save file """ - self.reply_saver = Saver(self.reply_handlers, filename, delay) + self.reply_backend = FileHandlerBackend(self.reply_backend.handlers, filename, delay) def disable_save_next_step_handlers(self): """ Disable saving next step handlers (by default saving disable) + + This function is left to keep backward compatibility whose purpose was to disable file saving capability + for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new next_step_backend backend + instead of FileHandlerBackend. + + Most probably this function should be deprecated in future major releases """ - self.next_step_saver = None + self.next_step_backend = MemoryHandlerBackend(self.next_step_backend.handlers) def disable_save_reply_handlers(self): """ Disable saving next step handlers (by default saving disable) + + This function is left to keep backward compatibility whose purpose was to disable file saving capability + for handlers. For the same purpose, MemoryHandlerBackend is reassigned as a new reply_backend backend + instead of FileHandlerBackend. + + Most probably this function should be deprecated in future major releases """ - self.reply_saver = None + self.reply_backend = MemoryHandlerBackend(self.reply_backend.handlers) def load_next_step_handlers(self, filename="./.handler-saves/step.save", del_file_after_loading=True): """ Load next step handlers from save file + This function is left to keep backward compatibility whose purpose was to load handlers from file with the + help of FileHandlerBackend and is only recommended to use if next_step_backend was assigned as + FileHandlerBackend before entering this function + + Most probably this function should be deprecated in future major releases + :param filename: Filename of the file where handlers was saved :param del_file_after_loading: Is passed True, after loading save file will be deleted """ - self.next_step_saver.load_handlers(filename, del_file_after_loading) + self.next_step_backend: FileHandlerBackend + self.next_step_backend.load_handlers(filename, del_file_after_loading) def load_reply_handlers(self, filename="./.handler-saves/reply.save", del_file_after_loading=True): """ Load reply handlers from save file + This function is left to keep backward compatibility whose purpose was to load handlers from file with the + help of FileHandlerBackend and is only recommended to use if reply_backend was assigned as + FileHandlerBackend before entering this function + + Most probably this function should be deprecated in future major releases + :param filename: Filename of the file where handlers was saved :param del_file_after_loading: Is passed True, after loading save file will be deleted """ - self.reply_saver.load_handlers(filename) + self.reply_backend: FileHandlerBackend + self.reply_backend.load_handlers(filename, del_file_after_loading) def set_webhook(self, url=None, certificate=None, max_connections=None, allowed_updates=None): return apihelper.set_webhook(self.token, url, certificate, max_connections, allowed_updates) @@ -1399,12 +1380,7 @@ class TeleBot: :param callback: The callback function to be called when a reply arrives. Must accept one `message` parameter, which will contain the replied message. """ - if message_id in self.reply_handlers.keys(): - self.reply_handlers[message_id].append(Handler(callback, *args, **kwargs)) - else: - self.reply_handlers[message_id] = [Handler(callback, *args, **kwargs)] - if self.reply_saver is not None: - self.reply_saver.start_save_timer() + self.reply_backend.register_handler(message_id, Handler(callback, *args, **kwargs)) def _notify_reply_handlers(self, new_messages): """ @@ -1414,14 +1390,9 @@ class TeleBot: """ for message in new_messages: if hasattr(message, "reply_to_message") and message.reply_to_message is not None: - reply_msg_id = message.reply_to_message.message_id - if reply_msg_id in self.reply_handlers.keys(): - handlers = self.reply_handlers[reply_msg_id] - for handler in handlers: - self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) - self.reply_handlers.pop(reply_msg_id) - if self.reply_saver is not None: - self.reply_saver.start_save_timer() + handlers = self.reply_backend.get_handlers(message.reply_to_message.message_id) + for handler in handlers: + self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) def register_next_step_handler(self, message, callback, *args, **kwargs): """ @@ -1448,13 +1419,7 @@ class TeleBot: :param args: Args to pass in callback func :param kwargs: Args to pass in callback func """ - if chat_id in self.next_step_handlers.keys(): - self.next_step_handlers[chat_id].append(Handler(callback, *args, **kwargs)) - else: - self.next_step_handlers[chat_id] = [Handler(callback, *args, **kwargs)] - - if self.next_step_saver is not None: - self.next_step_saver.start_save_timer() + self.next_step_backend.register_handler(chat_id, Handler(callback, *args, **kwargs)) def clear_step_handler(self, message): """ @@ -1471,10 +1436,7 @@ class TeleBot: :param chat_id: The chat for which we want to clear next step handlers """ - self.next_step_handlers[chat_id] = [] - - if self.next_step_saver is not None: - self.next_step_saver.start_save_timer() + self.next_step_backend.clear_handlers(chat_id) def clear_reply_handlers(self, message): """ @@ -1491,10 +1453,7 @@ class TeleBot: :param message_id: The message id for which we want to clear reply handlers """ - self.reply_handlers[message_id] = [] - - if self.reply_saver is not None: - self.reply_saver.start_save_timer() + self.reply_backend.clear_handlers(message_id) def _notify_next_handlers(self, new_messages): """ @@ -1502,22 +1461,14 @@ class TeleBot: :param new_messages: :return: """ - i = 0 - while i < len(new_messages): - message = new_messages[i] - chat_id = message.chat.id - was_poped = False - if chat_id in self.next_step_handlers.keys(): - handlers = self.next_step_handlers.pop(chat_id, None) - if handlers: - for handler in handlers: - self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) - new_messages.pop(i) # removing message that detects with next_step_handler - was_poped = True - if self.next_step_saver is not None: - self.next_step_saver.start_save_timer() - if not was_poped: - i += 1 + for i, message in enumerate(new_messages): + need_pop = False + handlers = self.next_step_backend.get_handlers(message.chat.id) + for handler in handlers: + need_pop = True + self._exec_task(handler["callback"], message, *handler["args"], **handler["kwargs"]) + if need_pop: + new_messages.pop(i) # removing message that detects with next_step_handler @staticmethod def _build_handler_dict(handler, **filters): diff --git a/telebot/handler_backends.py b/telebot/handler_backends.py new file mode 100644 index 0000000..97dd44f --- /dev/null +++ b/telebot/handler_backends.py @@ -0,0 +1,144 @@ +import os +import pickle +import threading + +from telebot import apihelper + + +class HandlerBackend: + """ + Class for saving (next step|reply) handlers + """ + handlers = {} + + def __init__(self, handlers=None): + if handlers: + self.handlers = handlers + + def register_handler(self, handler_group_id, handler): + raise NotImplementedError() + + def clear_handlers(self, handler_group_id): + raise NotImplementedError() + + def get_handlers(self, handler_group_id): + raise NotImplementedError() + + +class MemoryHandlerBackend(HandlerBackend): + def register_handler(self, handler_group_id, handler): + if handler_group_id in self.handlers: + self.handlers[handler_group_id].append(handler) + else: + self.handlers[handler_group_id] = [handler] + + def clear_handlers(self, handler_group_id): + self.handlers.pop(handler_group_id, []) + + def get_handlers(self, handler_group_id): + return self.handlers.pop(handler_group_id, []) + + +class FileHandlerBackend(HandlerBackend): + def __init__(self, handlers=None, filename='./.handler-saves/handlers.save', delay=120): + super().__init__(handlers) + self.filename = filename + self.delay = delay + self.timer = threading.Timer(delay, self.save_handlers) + + def register_handler(self, handler_group_id, handler): + if handler_group_id in self.handlers: + self.handlers[handler_group_id].append(handler) + else: + self.handlers[handler_group_id] = [handler] + + self.start_save_timer() + + def clear_handlers(self, handler_group_id): + self.handlers.pop(handler_group_id, []) + + self.start_save_timer() + + def get_handlers(self, handler_group_id): + handlers = self.handlers.pop(handler_group_id, []) + + self.start_save_timer() + + return handlers + + def start_save_timer(self): + if not self.timer.is_alive(): + if self.delay <= 0: + self.save_handlers() + else: + self.timer = threading.Timer(self.delay, self.save_handlers) + self.timer.start() + + def save_handlers(self): + self.dump_handlers(self.handlers, self.filename) + + def load_handlers(self, filename, del_file_after_loading=True): + tmp = self.return_load_handlers(filename, del_file_after_loading=del_file_after_loading) + if tmp is not None: + self.handlers.update(tmp) + + @staticmethod + def dump_handlers(handlers, filename, file_mode="wb"): + dirs = filename.rsplit('/', maxsplit=1)[0] + os.makedirs(dirs, exist_ok=True) + + with open(filename + ".tmp", file_mode) as file: + if (apihelper.CUSTOM_SERIALIZER is None): + pickle.dump(handlers, file) + else: + apihelper.CUSTOM_SERIALIZER.dump(handlers, file) + + if os.path.isfile(filename): + os.remove(filename) + + os.rename(filename + ".tmp", filename) + + @staticmethod + def return_load_handlers(filename, del_file_after_loading=True): + if os.path.isfile(filename) and os.path.getsize(filename) > 0: + with open(filename, "rb") as file: + if (apihelper.CUSTOM_SERIALIZER is None): + handlers = pickle.load(file) + else: + handlers = apihelper.CUSTOM_SERIALIZER.load(file) + + if del_file_after_loading: + os.remove(filename) + + return handlers + + +class RedisHandlerBackend(HandlerBackend): + def __init__(self, handlers=None, host='localhost', port=6379, db=0, prefix='telebot'): + super().__init__(handlers) + from redis import Redis + self.prefix = prefix + self.redis = Redis(host, port, db) + + def _key(self, handle_group_id): + return ':'.join((self.prefix, str(handle_group_id))) + + def register_handler(self, handler_group_id, handler): + handlers = [] + value = self.redis.get(self._key(handler_group_id)) + if value: + handlers = pickle.loads(value) + handlers.append(handler) + self.redis.set(self._key(handler_group_id), pickle.dumps(handlers)) + + def clear_handlers(self, handler_group_id): + self.redis.delete(self._key(handler_group_id)) + + def get_handlers(self, handler_group_id): + handlers = [] + value = self.redis.get(self._key(handler_group_id)) + if value: + handlers = pickle.loads(value) + self.clear_handlers(handler_group_id) + + return handlers From 003c5db37f0b5930f1fe8f042c0edd123ddbc2ff Mon Sep 17 00:00:00 2001 From: bedilbek Date: Mon, 13 Apr 2020 01:45:19 +0500 Subject: [PATCH 04/13] Add filename checking --- telebot/handler_backends.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telebot/handler_backends.py b/telebot/handler_backends.py index 97dd44f..cb68594 100644 --- a/telebot/handler_backends.py +++ b/telebot/handler_backends.py @@ -77,7 +77,9 @@ class FileHandlerBackend(HandlerBackend): def save_handlers(self): self.dump_handlers(self.handlers, self.filename) - def load_handlers(self, filename, del_file_after_loading=True): + def load_handlers(self, filename=None, del_file_after_loading=True): + if not filename: + filename = self.filename tmp = self.return_load_handlers(filename, del_file_after_loading=del_file_after_loading) if tmp is not None: self.handlers.update(tmp) From e7e7c5813318a2463c4aeb94a7699fe2fac7fb9a Mon Sep 17 00:00:00 2001 From: bedilbek Date: Mon, 13 Apr 2020 01:45:52 +0500 Subject: [PATCH 05/13] Add Memory, File, Redis Backend tests --- tests/test_handler_backends.py | 261 +++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 tests/test_handler_backends.py diff --git a/tests/test_handler_backends.py b/tests/test_handler_backends.py new file mode 100644 index 0000000..9f18114 --- /dev/null +++ b/tests/test_handler_backends.py @@ -0,0 +1,261 @@ +import os +import time + +import pytest + +import telebot +from telebot import types, MemoryHandlerBackend, FileHandlerBackend +from telebot.handler_backends import RedisHandlerBackend + + +@pytest.fixture() +def telegram_bot(): + return telebot.TeleBot('', threaded=False) + + +@pytest.fixture +def private_chat(): + return types.Chat(id=11, type='private') + + +@pytest.fixture +def user(): + return types.User(id=10, is_bot=False, first_name='Some User') + + +@pytest.fixture() +def message(user, private_chat): + params = {'text': '/start'} + return types.Message( + message_id=1, from_user=user, date=None, chat=private_chat, content_type='text', options=params, json_string="" + ) + + +@pytest.fixture() +def reply_to_message(user, private_chat, message): + params = {'text': '/start'} + reply_message = types.Message( + message_id=2, from_user=user, date=None, chat=private_chat, content_type='text', options=params, json_string="" + ) + reply_message.reply_to_message = message + return reply_message + + +@pytest.fixture() +def update_type(message): + edited_message = None + channel_post = None + edited_channel_post = None + inline_query = None + chosen_inline_result = None + callback_query = None + shipping_query = None + pre_checkout_query = None + poll = None + return types.Update(1001234038283, message, edited_message, channel_post, edited_channel_post, inline_query, + chosen_inline_result, callback_query, shipping_query, pre_checkout_query, poll) + + +@pytest.fixture() +def reply_to_message_update_type(reply_to_message): + edited_message = None + channel_post = None + edited_channel_post = None + inline_query = None + chosen_inline_result = None + callback_query = None + shipping_query = None + pre_checkout_query = None + poll = None + return types.Update(1001234038284, reply_to_message, edited_message, channel_post, edited_channel_post, + inline_query, + chosen_inline_result, callback_query, shipping_query, pre_checkout_query, poll) + + +def next_handler(message): + message.text = 'entered next_handler' + + +def test_memory_handler_backend_default_backend(telegram_bot): + assert telegram_bot.reply_backend.__class__ == MemoryHandlerBackend + assert telegram_bot.next_step_backend.__class__ == MemoryHandlerBackend + + +def test_memory_handler_backend_register_next_step_handler(telegram_bot, private_chat, update_type): + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_next_step_handler_by_chat_id(message.chat.id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + assert len(telegram_bot.next_step_backend.handlers[private_chat.id]) == 1 + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered next_handler' + + assert private_chat.id not in telegram_bot.next_step_backend.handlers + + +def test_memory_handler_backend_clear_next_step_handler(telegram_bot, private_chat, update_type): + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_next_step_handler_by_chat_id(message.chat.id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + assert len(telegram_bot.next_step_backend.handlers[private_chat.id]) == 1 + + telegram_bot.clear_step_handler_by_chat_id(private_chat.id) + + assert private_chat.id not in telegram_bot.next_step_backend.handlers + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + +def test_memory_handler_backend_register_reply_handler(telegram_bot, private_chat, update_type, + reply_to_message_update_type): + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_for_reply_by_message_id(message.message_id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + assert len(telegram_bot.reply_backend.handlers[update_type.message.message_id]) == 1 + + telegram_bot.process_new_updates([reply_to_message_update_type]) + assert reply_to_message_update_type.message.text == 'entered next_handler' + + assert private_chat.id not in telegram_bot.reply_backend.handlers + + +def test_memory_handler_backend_clear_reply_handler(telegram_bot, private_chat, update_type, + reply_to_message_update_type): + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_for_reply_by_message_id(message.message_id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + assert len(telegram_bot.reply_backend.handlers[update_type.message.message_id]) == 1 + + telegram_bot.clear_reply_handlers_by_message_id(update_type.message.message_id) + + assert update_type.message.message_id not in telegram_bot.reply_backend.handlers + + telegram_bot.process_new_updates([reply_to_message_update_type]) + assert reply_to_message_update_type.message.text == 'entered start' + + +def test_file_handler_backend_register_next_step_handler(private_chat, update_type): + telegram_bot = telebot.TeleBot( + token='', + threaded=False, + next_step_backend=FileHandlerBackend(filename='./.handler-saves/step1.save', delay=1) + ) + + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_next_step_handler_by_chat_id(message.chat.id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + time.sleep(2) + + assert os.path.exists(telegram_bot.next_step_backend.filename) + + assert len(telegram_bot.next_step_backend.handlers[private_chat.id]) == 1 + + telegram_bot.next_step_backend.handlers = {} + + telegram_bot.next_step_backend.load_handlers() + + assert len(telegram_bot.next_step_backend.handlers[private_chat.id]) == 1 + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered next_handler' + + assert private_chat.id not in telegram_bot.next_step_backend.handlers + + time.sleep(2) + if os.path.exists(telegram_bot.next_step_backend.filename): + os.remove(telegram_bot.next_step_backend.filename) + + +def test_file_handler_backend_clear_next_step_handler(private_chat, update_type): + telegram_bot = telebot.TeleBot( + token='', + threaded=False, + next_step_backend=FileHandlerBackend(filename='./.handler-saves/step2.save', delay=1) + ) + + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_next_step_handler_by_chat_id(message.chat.id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + assert len(telegram_bot.next_step_backend.handlers[private_chat.id]) == 1 + + time.sleep(2) + + assert os.path.exists(telegram_bot.next_step_backend.filename) + + telegram_bot.clear_step_handler_by_chat_id(private_chat.id) + + time.sleep(2) + + telegram_bot.next_step_backend.load_handlers() + + assert private_chat.id not in telegram_bot.next_step_backend.handlers + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + time.sleep(2) + if os.path.exists(telegram_bot.next_step_backend.filename): + os.remove(telegram_bot.next_step_backend.filename) + + +def test_redis_handler_backend_register_next_step_handler(telegram_bot, private_chat, update_type): + telegram_bot.next_step_backend = RedisHandlerBackend(prefix='pyTelegramBotApi:step_backend1') + + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_next_step_handler_by_chat_id(message.chat.id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered next_handler' + + +def test_redis_handler_backend_clear_next_step_handler(telegram_bot, private_chat, update_type): + telegram_bot.next_step_backend = RedisHandlerBackend(prefix='pyTelegramBotApi:step_backend2') + + @telegram_bot.message_handler(commands=['start']) + def start(message): + message.text = 'entered start' + telegram_bot.register_next_step_handler_by_chat_id(message.chat.id, next_handler) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' + + telegram_bot.clear_step_handler_by_chat_id(private_chat.id) + + telegram_bot.process_new_updates([update_type]) + assert update_type.message.text == 'entered start' From 3aec66bc0dfbdd1aafe5616b0ba68f3a1492cd72 Mon Sep 17 00:00:00 2001 From: bedilbek Date: Mon, 13 Apr 2020 03:17:13 +0500 Subject: [PATCH 06/13] Remove class static variable --- telebot/handler_backends.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/telebot/handler_backends.py b/telebot/handler_backends.py index cb68594..cb6c688 100644 --- a/telebot/handler_backends.py +++ b/telebot/handler_backends.py @@ -9,11 +9,10 @@ class HandlerBackend: """ Class for saving (next step|reply) handlers """ - handlers = {} - def __init__(self, handlers=None): - if handlers: - self.handlers = handlers + if handlers is None: + handlers = {} + self.handlers = handlers def register_handler(self, handler_group_id, handler): raise NotImplementedError() From 0881e343819877ba79ad5dcf75452003cb6a9cda Mon Sep 17 00:00:00 2001 From: bedilbek Date: Mon, 13 Apr 2020 03:21:10 +0500 Subject: [PATCH 07/13] Refactor tests --- tests/test_handler_backends.py | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tests/test_handler_backends.py b/tests/test_handler_backends.py index 9f18114..b16642e 100644 --- a/tests/test_handler_backends.py +++ b/tests/test_handler_backends.py @@ -1,3 +1,7 @@ +import sys + +sys.path.append('../') + import os import time @@ -155,12 +159,8 @@ def test_memory_handler_backend_clear_reply_handler(telegram_bot, private_chat, assert reply_to_message_update_type.message.text == 'entered start' -def test_file_handler_backend_register_next_step_handler(private_chat, update_type): - telegram_bot = telebot.TeleBot( - token='', - threaded=False, - next_step_backend=FileHandlerBackend(filename='./.handler-saves/step1.save', delay=1) - ) +def test_file_handler_backend_register_next_step_handler(telegram_bot, private_chat, update_type): + telegram_bot.next_step_backend=FileHandlerBackend(filename='./.handler-saves/step1.save', delay=0.1) @telegram_bot.message_handler(commands=['start']) def start(message): @@ -170,7 +170,7 @@ def test_file_handler_backend_register_next_step_handler(private_chat, update_ty telegram_bot.process_new_updates([update_type]) assert update_type.message.text == 'entered start' - time.sleep(2) + time.sleep(0.2) assert os.path.exists(telegram_bot.next_step_backend.filename) @@ -187,17 +187,13 @@ def test_file_handler_backend_register_next_step_handler(private_chat, update_ty assert private_chat.id not in telegram_bot.next_step_backend.handlers - time.sleep(2) + time.sleep(0.2) if os.path.exists(telegram_bot.next_step_backend.filename): os.remove(telegram_bot.next_step_backend.filename) -def test_file_handler_backend_clear_next_step_handler(private_chat, update_type): - telegram_bot = telebot.TeleBot( - token='', - threaded=False, - next_step_backend=FileHandlerBackend(filename='./.handler-saves/step2.save', delay=1) - ) +def test_file_handler_backend_clear_next_step_handler(telegram_bot, private_chat, update_type): + telegram_bot.next_step_backend=FileHandlerBackend(filename='./.handler-saves/step2.save', delay=0.1) @telegram_bot.message_handler(commands=['start']) def start(message): @@ -209,13 +205,13 @@ def test_file_handler_backend_clear_next_step_handler(private_chat, update_type) assert len(telegram_bot.next_step_backend.handlers[private_chat.id]) == 1 - time.sleep(2) + time.sleep(0.2) assert os.path.exists(telegram_bot.next_step_backend.filename) telegram_bot.clear_step_handler_by_chat_id(private_chat.id) - time.sleep(2) + time.sleep(0.2) telegram_bot.next_step_backend.load_handlers() @@ -224,7 +220,7 @@ def test_file_handler_backend_clear_next_step_handler(private_chat, update_type) telegram_bot.process_new_updates([update_type]) assert update_type.message.text == 'entered start' - time.sleep(2) + time.sleep(0.2) if os.path.exists(telegram_bot.next_step_backend.filename): os.remove(telegram_bot.next_step_backend.filename) From 51b1fb7695c17fdd2113d8bef6c8c15eccc3c5dd Mon Sep 17 00:00:00 2001 From: dr_forse Date: Wed, 15 Apr 2020 06:10:05 +0100 Subject: [PATCH 08/13] added Dice and send_dice --- telebot/__init__.py | 18 ++++++++++++++++++ telebot/apihelper.py | 12 ++++++++++++ telebot/types.py | 22 ++++++++++++++++++++++ tests/test_telebot.py | 8 +++++++- tests/test_types.py | 8 ++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) diff --git a/telebot/__init__.py b/telebot/__init__.py index 72f6c71..a38e02f 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -117,6 +117,7 @@ class TeleBot: sendVideoNote sendLocation sendChatAction + sendDice getUserProfilePhotos getUpdates getFile @@ -665,6 +666,19 @@ class TeleBot: """ return apihelper.delete_message(self.token, chat_id, message_id) + def send_dice(self, chat_id, disable_notification=None, reply_to_message_id=None, reply_markup=None): + """ + Use this method to send dices. + :param chat_id: + :param disable_notification: + :param reply_to_message_id: + :param reply_markup: + :return: Message + """ + return types.Message.de_json( + apihelper.send_dice(self.token, chat_id, disable_notification, reply_to_message_id, reply_markup) + ) + def send_photo(self, chat_id, photo, caption=None, reply_to_message_id=None, reply_markup=None, parse_mode=None, disable_notification=None): """ @@ -1991,6 +2005,10 @@ class AsyncTeleBot(TeleBot): def send_message(self, *args, **kwargs): return TeleBot.send_message(self, *args, **kwargs) + @util.async_dec() + def send_dice(self, *args, **kwargs): + return TeleBot.send_dice(self, *args, **kwargs) + @util.async_dec() def forward_message(self, *args, **kwargs): return TeleBot.forward_message(self, *args, **kwargs) diff --git a/telebot/apihelper.py b/telebot/apihelper.py index 9db62cc..58a3e67 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -259,6 +259,18 @@ def forward_message(token, chat_id, from_chat_id, message_id, disable_notificati return _make_request(token, method_url, params=payload) +def send_dice(token, chat_id, disable_notification=None, reply_to_message_id=None, reply_markup=None): + method_url = r'sendDice' + payload = {'chat_id': chat_id} + if disable_notification: + payload['disable_notification'] = disable_notification + if reply_to_message_id: + payload['reply_to_message_id'] = reply_to_message_id + if reply_markup: + payload['reply_markup'] = _convert_markup(reply_markup) + return _make_request(token, method_url, params=payload) + + def send_photo(token, chat_id, photo, caption=None, reply_to_message_id=None, reply_markup=None, parse_mode=None, disable_notification=None): method_url = r'sendPhoto' diff --git a/telebot/types.py b/telebot/types.py index ffa1232..8929111 100644 --- a/telebot/types.py +++ b/telebot/types.py @@ -293,6 +293,9 @@ class Message(JsonDeserializable): if 'venue' in obj: opts['venue'] = Venue.de_json(obj['venue']) content_type = 'venue' + if 'dice' in obj: + opts['dice'] = Dice.de_json(obj['dice']) + content_type = 'dice' if 'new_chat_members' in obj: new_chat_members = [] for member in obj['new_chat_members']: @@ -397,6 +400,7 @@ class Message(JsonDeserializable): self.location = None self.venue = None self.animation = None + self.dice = None self.new_chat_member = None # Deprecated since Bot API 3.0. Not processed anymore self.new_chat_members = None self.left_chat_member = None @@ -511,6 +515,24 @@ class MessageEntity(JsonDeserializable): self.user = user +class Dice(JsonSerializable, Dictionaryable, JsonDeserializable): + @classmethod + def de_json(cls, json_string): + if (json_string is None): return None + obj = cls.check_json(json_string) + value = obj['value'] + return cls(value) + + def __init__(self, value): + self.value = value + + def to_json(self): + return json.dumps({'value': self.value}) + + def to_dic(self): + return {'value': self.value} + + class PhotoSize(JsonDeserializable): @classmethod def de_json(cls, json_string): diff --git a/tests/test_telebot.py b/tests/test_telebot.py index b3fbfcf..7fe4c0f 100644 --- a/tests/test_telebot.py +++ b/tests/test_telebot.py @@ -11,7 +11,7 @@ import telebot from telebot import types from telebot import util -should_skip = 'TOKEN' and 'CHAT_ID' not in os.environ +should_skip = False if not should_skip: TOKEN = os.environ['TOKEN'] @@ -241,6 +241,12 @@ class TestTeleBot: ret_msg = tb.send_message(CHAT_ID, text) assert ret_msg.message_id + def test_send_dice(self): + tb = telebot.TeleBot(TOKEN) + ret_msg = tb.send_dice(CHAT_ID) + assert ret_msg.message_id + assert ret_msg.content_type == 'dice' + def test_send_message_dis_noti(self): text = 'CI Test Message' tb = telebot.TeleBot(TOKEN) diff --git a/tests/test_types.py b/tests/test_types.py index 590b080..d3bc80d 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -17,6 +17,14 @@ def test_json_message(): assert msg.text == 'HIHI' +def test_json_message_with_dice(): + jsonstring = r'{"message_id":5560,"from":{"id":879343317,"is_bot":false,"first_name":"George","last_name":"Forse","username":"dr_fxrse","language_code":"ru"},"chat":{"id":879343317,"first_name":"George","last_name":"Forse","username":"dr_fxrse","type":"private"},"date":1586926330,"dice":{"value":4}}' + msg = types.Message.de_json(jsonstring) + assert msg.content_type == 'dice' + assert isinstance(msg.dice, types.Dice) + assert msg.dice.value == 4 + + def test_json_message_group(): json_string = r'{"message_id":10,"from":{"id":12345,"first_name":"g","last_name":"G","username":"GG","is_bot":true},"chat":{"id":-866,"type":"private","title":"\u4ea4"},"date":1435303157,"text":"HIHI"}' msg = types.Message.de_json(json_string) From 615402e4f811e87b9ef1ae616da48dffdca7974c Mon Sep 17 00:00:00 2001 From: dr_forse Date: Wed, 15 Apr 2020 06:16:07 +0100 Subject: [PATCH 09/13] return a line as it was --- tests/test_telebot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_telebot.py b/tests/test_telebot.py index 7fe4c0f..31a360f 100644 --- a/tests/test_telebot.py +++ b/tests/test_telebot.py @@ -11,7 +11,7 @@ import telebot from telebot import types from telebot import util -should_skip = False +should_skip = 'TOKEN' and 'CHAT_ID' not in os.environ if not should_skip: TOKEN = os.environ['TOKEN'] From aab560b4ee0b627ba19fb77a44f2666a5462c191 Mon Sep 17 00:00:00 2001 From: bedilbek Date: Mon, 20 Apr 2020 11:30:03 +0500 Subject: [PATCH 10/13] Fix all the time invocations on typed_middleware handlers even if update did not have that update_type message --- telebot/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/telebot/__init__.py b/telebot/__init__.py index abcc273..d514ece 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -380,7 +380,7 @@ class TeleBot: def process_middlewares(self, update): for update_type, middlewares in self.typed_middleware_handlers.items(): - if hasattr(update, update_type): + if getattr(update, update_type) is not None: for typed_middleware_handler in middlewares: typed_middleware_handler(self, getattr(update, update_type)) From da924dbaebffb48127e0a83547d9d41e12eb0df2 Mon Sep 17 00:00:00 2001 From: no_ideaw Date: Thu, 23 Apr 2020 23:59:04 +0430 Subject: [PATCH 11/13] Update __init__.py added can_invite_users parameter to restrict_chat_member function --- telebot/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telebot/__init__.py b/telebot/__init__.py index d514ece..944d9ec 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -930,7 +930,7 @@ class TeleBot: def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messages=None, can_send_media_messages=None, can_send_other_messages=None, - can_add_web_page_previews=None): + can_add_web_page_previews=None, can_invite_users=None): """ Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have @@ -949,6 +949,8 @@ class TeleBot: use inline bots, implies can_send_media_messages :param can_add_web_page_previews: Pass True, if the user may add web page previews to their messages, implies can_send_media_messages + :param can_invite_users: Pass True, if the user is allowed to invite new users to the chat, + implies can_invite_users :return: types.Message """ return apihelper.restrict_chat_member(self.token, chat_id, user_id, until_date, can_send_messages, From b1b2726ef661f6cb8a2bda7f0811aa0819e0f41a Mon Sep 17 00:00:00 2001 From: no_ideaw Date: Fri, 24 Apr 2020 00:21:05 +0430 Subject: [PATCH 12/13] Update apihelper.py added can_invite_users parameter to restrict_chat_member function --- telebot/apihelper.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/telebot/apihelper.py b/telebot/apihelper.py index 58a3e67..8eb4744 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -569,7 +569,7 @@ def unban_chat_member(token, chat_id, user_id): def restrict_chat_member(token, chat_id, user_id, until_date=None, can_send_messages=None, can_send_media_messages=None, can_send_other_messages=None, - can_add_web_page_previews=None): + can_add_web_page_previews=None, can_invite_users=None): method_url = 'restrictChatMember' payload = {'chat_id': chat_id, 'user_id': user_id} if until_date: @@ -582,7 +582,9 @@ def restrict_chat_member(token, chat_id, user_id, until_date=None, can_send_mess payload['can_send_other_messages'] = can_send_other_messages if can_add_web_page_previews: payload['can_add_web_page_previews'] = can_add_web_page_previews - + if can_invite_users: + payload['can_invite_users'] = can_invite_users + return _make_request(token, method_url, params=payload, method='post') From 8c7c7b31b21348d3660a413f53c1f01d3013c41f Mon Sep 17 00:00:00 2001 From: no_ideaw Date: Fri, 24 Apr 2020 19:38:23 +0430 Subject: [PATCH 13/13] Update __init__.py added can_invite_users parameter to restrict_chat_member function --- telebot/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/telebot/__init__.py b/telebot/__init__.py index d514ece..3d8e217 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -930,7 +930,7 @@ class TeleBot: def restrict_chat_member(self, chat_id, user_id, until_date=None, can_send_messages=None, can_send_media_messages=None, can_send_other_messages=None, - can_add_web_page_previews=None): + can_add_web_page_previews=None, can_invite_users=None): """ Use this method to restrict a user in a supergroup. The bot must be an administrator in the supergroup for this to work and must have @@ -953,7 +953,7 @@ class TeleBot: """ return apihelper.restrict_chat_member(self.token, chat_id, user_id, until_date, can_send_messages, can_send_media_messages, can_send_other_messages, - can_add_web_page_previews) + can_add_web_page_previews, can_invite_users) def promote_chat_member(self, chat_id, user_id, can_change_info=None, can_post_messages=None, can_edit_messages=None, can_delete_messages=None, can_invite_users=None,