From f9cbcf9b650aa0331935d6a2c1602f58e5c027a8 Mon Sep 17 00:00:00 2001 From: Pieter van den Ham Date: Tue, 8 Sep 2015 10:44:31 +0200 Subject: [PATCH 1/6] Redesigned message handlers --- README.md | 22 ++++++++++++++++++++-- telebot/__init__.py | 37 ++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a18c5b7..57ba753 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,25 @@ All [API methods](https://core.telegram.org/bots/api#available-methods) are loca Outlined below are some general use cases of the API. #### Message handlers -A message handler is a function which is decorated with the `message_handler` decorator of a TeleBot instance. The following examples illustrate the possibilities of message handlers: +A message handler is a function that is decorated with the `message_handler` decorator of a TeleBot instance. The following examples illustrate the possibilities of message handlers. Message handlers consists of one or multiple filters. +A message handler is declared in the following way (provided `bot` is an instance of TeleBot): +```python +@bot.message_handler(filters) +def function_name(message): + bot.reply_to(message, "This is a message handler") +``` +`function_name` is not bound to any restrictions. Any function name is permitted with message handlers. The function must accept at most one argument, which will be the message that the function must handle. +`filters` is a list of keyword arguments. Each filter must return True for a certain message in order for the message handler to become eligible to handle that message. +TeleBot supports the following filters: +|name|argument(s)|Condition| +|:---:|---| ---| +|content_types|list of strings (default `['text']`)|`True` if message.content_type is in the list of strings.| +|regexp|a regular expression as a string|`True` if `re.search(regexp_arg)` returns `True` and `message.content_type == 'text'`| +|commands|list of strings|`True` if `message.content_type == 'text'` and `message.text` starts with a command that is in the list of strings.| +|func|a function (lambda or function reference)|True if the function or lambda reference returns True + +A filter is declared in the following manner: `name=argument`. +Here are some examples of using the filters and message handlers: ```python import telebot bot = telebot.TeleBot("TOKEN") @@ -163,7 +181,7 @@ def test_message(message): def handle_text_doc(message) pass ``` -*Note: all handlers are tested in the order in which they were declared* +**Important: all handlers are tested in the order in which they were declared** #### TeleBot ```python import telebot diff --git a/telebot/__init__.py b/telebot/__init__.py index 5af3100..cbe5a51 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -371,31 +371,38 @@ class TeleBot: :param func: Optional lambda function. The lambda receives the message to test as the first parameter. It must return True if the command should handle the message. :param content_types: This commands' supported content types. Must be a list. Defaults to ['text']. """ - def decorator(fn): - func_dict = {'function': fn, 'content_types': content_types} + handler_dict = {'function': fn} + filters = {'content_types': content_types} if regexp: - func_dict['regexp'] = regexp if 'text' in content_types else None + filters['regexp'] = regexp if func: - func_dict['lambda'] = func + filters['lambda'] = func if commands: - func_dict['commands'] = commands if 'text' in content_types else None - self.message_handlers.append(func_dict) + filters['commands'] = commands + handler_dict['filters'] = filters + self.message_handlers.append(handler_dict) return fn return decorator @staticmethod def _test_message_handler(message_handler, message): - if message.content_type not in message_handler['content_types']: - return False - if 'commands' in message_handler and message.content_type == 'text': - return util.extract_command(message.text) in message_handler['commands'] - if 'regexp' in message_handler and message.content_type == 'text' and re.search(message_handler['regexp'], - message.text): - return True - if 'lambda' in message_handler: - return message_handler['lambda'](message) + for filter, filter_value in message_handler['filters'].iteritems(): + if not TeleBot._test_filter(filter, filter_value, message): + return False + return True + + @staticmethod + def _test_filter(filter, filter_value, message): + if filter == 'content_types': + return message.content_type in filter_value + if filter == 'regexp': + return message.content_type == 'text' and re.search(filter_value, message.text) + if filter == 'commands': + return message.content_type == 'text' and util.extract_command(message.text) in filter_value + if filter == 'func': + return filter_value(message) return False def _notify_command_handlers(self, new_messages): From edf169460608de8dada5e69d58ec84cf976a9b8c Mon Sep 17 00:00:00 2001 From: Pieter van den Ham Date: Tue, 8 Sep 2015 11:29:35 +0200 Subject: [PATCH 2/6] Fix README message handler table --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 57ba753..cf303f6 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ def function_name(message): `function_name` is not bound to any restrictions. Any function name is permitted with message handlers. The function must accept at most one argument, which will be the message that the function must handle. `filters` is a list of keyword arguments. Each filter must return True for a certain message in order for the message handler to become eligible to handle that message. TeleBot supports the following filters: + |name|argument(s)|Condition| |:---:|---| ---| |content_types|list of strings (default `['text']`)|`True` if message.content_type is in the list of strings.| From cc7ab58ed8851a341e900b7d3b738fa6558af33f Mon Sep 17 00:00:00 2001 From: Pieter van den Ham Date: Tue, 8 Sep 2015 17:38:44 +0200 Subject: [PATCH 3/6] Fixed some typos in the README Fixed a bug where TeleBot would ignore KeyboardInterrupt events --- README.md | 10 +++++----- telebot/__init__.py | 16 +++++++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index cf303f6..28bb895 100644 --- a/README.md +++ b/README.md @@ -130,15 +130,16 @@ All [API methods](https://core.telegram.org/bots/api#available-methods) are loca Outlined below are some general use cases of the API. #### Message handlers -A message handler is a function that is decorated with the `message_handler` decorator of a TeleBot instance. The following examples illustrate the possibilities of message handlers. Message handlers consists of one or multiple filters. -A message handler is declared in the following way (provided `bot` is an instance of TeleBot): +A message handler is a function that is decorated with the `message_handler` decorator of a TeleBot instance. Message handlers consist of one or multiple filters. +Each filter much return True for a certain message in order for a message handler to become eligible to handle that message. A message handler is declared in the following way (provided `bot` is an instance of TeleBot): ```python @bot.message_handler(filters) def function_name(message): bot.reply_to(message, "This is a message handler") ``` `function_name` is not bound to any restrictions. Any function name is permitted with message handlers. The function must accept at most one argument, which will be the message that the function must handle. -`filters` is a list of keyword arguments. Each filter must return True for a certain message in order for the message handler to become eligible to handle that message. +`filters` is a list of keyword arguments. +A filter is declared in the following manner: `name=argument`. One handler may have multiple filters. TeleBot supports the following filters: |name|argument(s)|Condition| @@ -146,9 +147,8 @@ TeleBot supports the following filters: |content_types|list of strings (default `['text']`)|`True` if message.content_type is in the list of strings.| |regexp|a regular expression as a string|`True` if `re.search(regexp_arg)` returns `True` and `message.content_type == 'text'`| |commands|list of strings|`True` if `message.content_type == 'text'` and `message.text` starts with a command that is in the list of strings.| -|func|a function (lambda or function reference)|True if the function or lambda reference returns True +|func|a function (lambda or function reference)|`True` if the lambda or function reference returns `True` -A filter is declared in the following manner: `name=argument`. Here are some examples of using the filters and message handlers: ```python import telebot diff --git a/telebot/__init__.py b/telebot/__init__.py index cbe5a51..53f29c4 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -3,13 +3,12 @@ from __future__ import print_function import threading import time +import re +from telebot import apihelper, types, util import logging logging.basicConfig() logger = logging.getLogger('Telebot') -import re - -from telebot import apihelper, types, util """ Module : telebot @@ -65,7 +64,7 @@ class TeleBot: Registered listeners and applicable message handlers will be notified when a new message arrives. :raises ApiException when a call has failed. """ - updates = apihelper.get_updates(self.token, offset=(self.last_update_id + 1), timeout=20) + updates = apihelper.get_updates(self.token, offset=(self.last_update_id + 1), timeout=3) new_messages = [] for update in updates: if update['update_id'] > self.last_update_id: @@ -109,7 +108,14 @@ class TeleBot: self.polling_thread.start() if block: - self.__stop_polling.wait() + while self.polling_thread.is_alive: + try: + time.sleep(.1) + except KeyboardInterrupt: + logger.info("TeleBot: Received KeyboardInterrupt: Stopping") + self.stop_polling() + self.polling_thread.join() + break def __polling(self, none_stop, interval): logger.info('TeleBot: Started polling.') From 9f04f0ece23957b9e98e1a2834e2f3e77dc7a6bd Mon Sep 17 00:00:00 2001 From: pieter Date: Tue, 8 Sep 2015 18:53:36 +0200 Subject: [PATCH 4/6] Fix "module has no attribute 'logger'" --- telebot/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/telebot/__init__.py b/telebot/__init__.py index 53f29c4..435f1c8 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -4,12 +4,13 @@ from __future__ import print_function import threading import time import re -from telebot import apihelper, types, util import logging logging.basicConfig() logger = logging.getLogger('Telebot') +from telebot import apihelper, types, util + """ Module : telebot """ From 710fc273d6b6303a3d119f8023eb81fef0b7ddd3 Mon Sep 17 00:00:00 2001 From: pieter Date: Tue, 8 Sep 2015 19:47:55 +0200 Subject: [PATCH 5/6] Better log messages --- README.md | 2 +- telebot/__init__.py | 21 ++++++++++++++------- telebot/apihelper.py | 3 ++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 28bb895..54c4485 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ TeleBot supports the following filters: |name|argument(s)|Condition| |:---:|---| ---| |content_types|list of strings (default `['text']`)|`True` if message.content_type is in the list of strings.| -|regexp|a regular expression as a string|`True` if `re.search(regexp_arg)` returns `True` and `message.content_type == 'text'`| +|regexp|a regular expression as a string|`True` if `re.search(regexp_arg)` returns `True` and `message.content_type == 'text'` (See [Python Regular Expressions](https://docs.python.org/2/library/re.html)| |commands|list of strings|`True` if `message.content_type == 'text'` and `message.text` starts with a command that is in the list of strings.| |func|a function (lambda or function reference)|`True` if the lambda or function reference returns `True` diff --git a/telebot/__init__.py b/telebot/__init__.py index 435f1c8..6e9af53 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -4,10 +4,17 @@ from __future__ import print_function import threading import time import re +import sys import logging -logging.basicConfig() -logger = logging.getLogger('Telebot') +logger = logging.getLogger('TeleBot') +formatter = logging.Formatter('%(asctime)s (%(filename)s:%(lineno)d) %(levelname)s - %(name)s: "%(message)s"') + +ch = logging.StreamHandler(sys.stderr) +ch.setFormatter(formatter) +logger.addHandler(ch) + +logger.setLevel(logging.ERROR) from telebot import apihelper, types, util @@ -72,7 +79,7 @@ class TeleBot: self.last_update_id = update['update_id'] msg = types.Message.de_json(update['message']) new_messages.append(msg) - logger.debug('GET %d new messages' % len(new_messages)) + logger.debug('Received {} new messages'.format(len(new_messages))) if len(new_messages) > 0: self.process_new_messages(new_messages) @@ -113,13 +120,13 @@ class TeleBot: try: time.sleep(.1) except KeyboardInterrupt: - logger.info("TeleBot: Received KeyboardInterrupt: Stopping") + logger.info("Received KeyboardInterrupt. Stopping.") self.stop_polling() self.polling_thread.join() break def __polling(self, none_stop, interval): - logger.info('TeleBot: Started polling.') + logger.info('Started polling.') error_interval = .25 while not self.__stop_polling.wait(interval): @@ -129,13 +136,13 @@ class TeleBot: except apihelper.ApiException as e: if not none_stop: self.__stop_polling.set() - logger.info("TeleBot: Exception occurred. Stopping.") + logger.info("Exception occurred. Stopping.") else: time.sleep(error_interval) error_interval *= 2 logger.error(e) - logger.info('TeleBot: Stopped polling.') + logger.info('Stopped polling.') def stop_polling(self): self.__stop_polling.set() diff --git a/telebot/apihelper.py b/telebot/apihelper.py index 1c4625c..62ccfad 100644 --- a/telebot/apihelper.py +++ b/telebot/apihelper.py @@ -19,8 +19,9 @@ def _make_request(token, method_name, method='get', params=None, files=None): :return: The result parsed to a JSON dictionary. """ request_url = telebot.API_URL + 'bot' + token + '/' + method_name + logger.debug("Request: method={} url={} params={} files={}".format(method, request_url, params, files)) result = requests.request(method, request_url, params=params, files=files) - logger.debug(result.text) + logger.debug("The server returned: '{}'".format(result.text)) return _check_result(method_name, result)['result'] From 88bd6dcbb0544387a4dbdef93638309ffebe57bd Mon Sep 17 00:00:00 2001 From: pieter Date: Tue, 8 Sep 2015 19:56:05 +0200 Subject: [PATCH 6/6] Updated Logging section in README --- README.md | 10 ++++------ telebot/__init__.py | 6 +++--- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 54c4485..13fe05a 100644 --- a/README.md +++ b/README.md @@ -353,15 +353,13 @@ If you prefer using web hooks to the getUpdates method, you can use the `process ### Logging You can use the Telebot module logger to log debug info about Telebot. Use `telebot.logger` to get the logger of the TeleBot module. +It is possible to add custom logging Handlers to the logger. Refer to the [Python logging module page](https://docs.python.org/2/library/logging.html) for more info. ```python +import logging + logger = telebot.logger -formatter = logging.Formatter('[%(asctime)s] %(thread)d {%(pathname)s:%(lineno)d} %(levelname)s - %(message)s', - '%m-%d %H:%M:%S') -ch = logging.StreamHandler(sys.stdout) -logger.addHandler(ch) -logger.setLevel(logging.DEBUG) # or use logging.INFO -ch.setFormatter(formatter) +telebot.logger.setLevel(logging.DEBUG) # Outputs debug messages to console. ``` ## F.A.Q. diff --git a/telebot/__init__.py b/telebot/__init__.py index 6e9af53..d99db79 100644 --- a/telebot/__init__.py +++ b/telebot/__init__.py @@ -10,9 +10,9 @@ import logging logger = logging.getLogger('TeleBot') formatter = logging.Formatter('%(asctime)s (%(filename)s:%(lineno)d) %(levelname)s - %(name)s: "%(message)s"') -ch = logging.StreamHandler(sys.stderr) -ch.setFormatter(formatter) -logger.addHandler(ch) +console_output_handler = logging.StreamHandler(sys.stderr) +console_output_handler.setFormatter(formatter) +logger.addHandler(console_output_handler) logger.setLevel(logging.ERROR)