From 06886919748d60304950e24928f44992eaae3de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Thu, 5 Nov 2015 13:52:33 +0100 Subject: [PATCH 1/8] initial commit for BotEventHandler and Broadcaster --- telegram/boteventhandler.py | 102 ++++++++ telegram/broadcaster.py | 452 ++++++++++++++++++++++++++++++++++++ 2 files changed, 554 insertions(+) create mode 100644 telegram/boteventhandler.py create mode 100644 telegram/broadcaster.py diff --git a/telegram/boteventhandler.py b/telegram/boteventhandler.py new file mode 100644 index 000000000..eaaf32c97 --- /dev/null +++ b/telegram/boteventhandler.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +""" +This module contains the class BotEventHandler, which tries to make creating +Telegram Bots intuitive! +""" + +import sys +from threading import Thread +from telegram import (Bot, TelegramError, TelegramObject, Broadcaster) +from time import sleep + +# Adjust for differences in Python versions +if sys.version_info.major is 2: + from Queue import Queue +elif sys.version_info.major is 3: + from queue import Queue + +class BotEventHandler(TelegramObject): + """ + This class provides a frontend to telegram.Bot to the programmer, so they + can focus on coding the bot. I also runs in a separate thread, so the user + can interact with the bot, for example on the command line. It supports + Handlers for different kinds of data: Updates from Telegram, basic text + commands and even arbitrary types. + + Attributes: + + Args: + token (str): The bots token given by the @BotFather + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + base_url (Optional[str]): + """ + + def __init__(self, token, base_url=None): + self.bot = Bot(token, base_url) + self.update_queue = Queue() + self.last_update_id = 0 + self.broadcaster = Broadcaster(self.bot, self.update_queue) + + def start(self, poll_interval=1.0): + """ + Starts polling updates from Telegram. + + Args: + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + poll_interval (Optional[float]): Time to wait between polling + updates from Telegram in seconds. Default is 1.0 + + Returns: + Queue: The update queue that can be filled from the main thread + """ + + # Create Thread objects + broadcaster_thread = Thread(target=self.broadcaster.start, + name="broadcaster") + event_handler_thread = Thread(target=self.__start, name="eventhandler", + args=(poll_interval,)) + + # Set threads as daemons so they'll stop if the main thread stops + broadcaster_thread.daemon = True + event_handler_thread.daemon = True + + # Start threads + broadcaster_thread.start() + event_handler_thread.start() + + # Return the update queue so the main thread can insert updates + return self.update_queue + + def __start(self, poll_interval): + """ + Thread target of thread 'eventhandler'. Runs in background, pulls + updates from Telegram and inserts them in the update queue of the + Broadcaster. + """ + + current_interval = poll_interval + + while True: + try: + updates = self.bot.getUpdates(self.last_update_id) + for update in updates: + self.update_queue.put(update) + self.last_update_id = update.update_id + 1 + current_interval = poll_interval + sleep(current_interval) + except TelegramError as te: + # Put the error into the update queue and let the Broadcaster + # broadcast it + self.update_queue.put(te) + sleep(current_interval) + + # increase waiting times on subsequent errors up to 30secs + if current_interval < 30: + current_interval += current_interval / 2 + if current_interval > 30: + current_interval = 30 diff --git a/telegram/broadcaster.py b/telegram/broadcaster.py new file mode 100644 index 000000000..63e45dd07 --- /dev/null +++ b/telegram/broadcaster.py @@ -0,0 +1,452 @@ +#!/usr/bin/env python + +""" +This module contains the Broadcaster class. +""" + +from telegram import (TelegramError, TelegramObject, Update) + +class Broadcaster(TelegramObject): + """ + This class broadcasts all kinds of updates to its registered handlers. + + Attributes: + + Args: + bot (telegram.Bot): The bot object that should be passed to the handlers + update_queue (queue.Queue): The synchronized queue that will contain the + updates + """ + def __init__(self, bot, update_queue): + self.bot = bot + self.update_queue = update_queue + self.telegram_message_handlers = [] + self.telegram_command_handlers = {} + self.telegram_regex_handlers = {} + self.string_regex_handlers = {} + self.string_command_handlers = {} + self.type_handlers = {} + self.unknown_telegram_command_handlers = [] + self.unknown_string_command_handlers = [] + self.error_handlers = [] + + # Add Handlers + def addTelegramMessageHandler(self, handler): + """ + Registers a message handler in the Broadcaster. + + Args: + handler (function): A function that takes (Bot, Update) as + arguments. + """ + + self.telegram_message_handlers.append(handler) + + def addTelegramCommandHandler(self, command, handler): + """ + Registers a command handler in the Broadcaster. + + Args: + command (str): The command keyword that this handler should be + listening to. + handler (function): A function that takes (Bot, Update) as + arguments. + """ + + if command not in self.telegram_command_handlers: + self.telegram_command_handlers[command] = [] + + self.telegram_command_handlers[command].append(handler) + + def addTelegramRegexHandler(self, matcher, handler): + """ + Registers a regex handler in the Broadcaster. If handlers will be called + if matcher.math(update.message.text) is True. + + Args: + matcher (str): A compiled regex object that matches on messages that + handler should be listening to + handler (function): A function that takes (Bot, Update) as + arguments. + """ + + if matcher not in self.telegram_regex_handlers: + self.telegram_regex_handlers[matcher] = [] + + self.telegram_regex_handlers[matcher].append(handler) + + def addStringCommandHandler(self, command, handler): + """ + Registers a string-command handler in the Broadcaster. + + Args: + command (str): The command keyword that this handler should be + listening to. + handler (function): A function that takes (Bot, str) as arguments. + """ + + if command not in self.string_command_handlers: + self.string_command_handlers[command] = [] + + self.string_command_handlers[command].append(handler) + + def addStringRegexHandler(self, matcher, handler): + """ + Registers a regex handler in the Broadcaster. If handlers will be called + if matcher.math(string) is True. + + Args: + matcher (str): A compiled regex object that matches on the string + input that handler should be listening to + handler (function): A function that takes (Bot, Update) as + arguments. + """ + + if matcher not in self.string_regex_handlers: + self.string_regex_handlers[matcher] = [] + + self.string_regex_handlers[matcher].append(handler) + + def addUnknownTelegramCommandHandler(self, handler): + """ + Registers a command handler in the Broadcaster, that will receive all + commands that have no associated handler. + + Args: + handler (function): A function that takes (Bot, Update) as + arguments. + """ + + self.unknown_telegram_command_handlers.append(handler) + + def addUnknownStringCommandHandler(self, handler): + """ + Registers a string-command handler in the Broadcaster, that will receive + all commands that have no associated handler. + + Args: + handler (function): A function that takes (Bot, str) as arguments. + """ + + self.unknown_string_command_handlers.append(handler) + + def addErrorHandler(self, handler): + """ + Registers an error handler in the Broadcaster. + + Args: + handler (function): A function that takes (Bot, TelegramError) as + arguments. + """ + + self.error_handlers.append(handler) + + def addTypeHandler(self, the_type, handler): + """ + Registers a type handler in the Broadcaster. This allows you to send + any type of object into the update queue. + + Args: + the_type (type): The type this handler should listen to + handler (function): A function that takes (Bot, type) as arguments. + """ + + if the_type not in self.type_handlers: + self.type_handlers[the_type] = [] + + self.type_handlers[the_type].append(handler) + + # Remove Handlers + def removeTelegramMessageHandler(self, handler): + """ + De-registers a message handler. + + Args: + handler (any): + """ + + if handler in self.telegram_message_handlers: + self.telegram_message_handlers.remove(handler) + + def removeTelegramCommandHandler(self, command, handler): + """ + De-registers a command handler. + + Args: + command (str): The command + handler (any): + """ + + if command in self.telegram_command_handlers \ + and handler in self.telegram_command_handlers[command]: + self.telegram_command_handlers[command].remove(handler) + + def removeTelegramRegexHandler(self, matcher, handler): + """ + De-registers a regex handler. + + Args: + matcher (str): The regex matcher object + handler (any): + """ + + if matcher in self.telegram_regex_handlers \ + and handler in self.telegram_regex_handlers[matcher]: + self.telegram_regex_handlers[matcher].remove(handler) + + def removeStringCommandHandler(self, command, handler): + """ + De-registers a string-command handler. + + Args: + command (str): The command + handler (any): + """ + + if command in self.string_command_handlers \ + and handler in self.string_command_handlers[command]: + self.string_command_handlers[command].remove(handler) + + def removeStringRegexHandler(self, matcher, handler): + """ + De-registers a regex handler. + + Args: + matcher (str): The regex matcher object + handler (any): + """ + + if matcher in self.string_regex_handlers \ + and handler in self.string_regex_handlers[matcher]: + self.string_regex_handlers[matcher].remove(handler) + + def removeUnknownTelegramCommandHandler(self, handler): + """ + De-registers an unknown-command handler. + + Args: + handler (any): + """ + + if handler in self.unknown_telegram_command_handlers: + self.unknown_telegram_command_handlers.remove(handler) + + def removeUnknownStringCommandHandler(self, handler): + """ + De-registers an unknown-command handler. + + Args: + handler (any): + """ + + if handler in self.unknown_string_command_handlers: + self.unknown_string_command_handlers.remove(handler) + + def removeErrorHandler(self, handler): + """ + De-registers an error handler. + + Args: + handler (any): + """ + + if handler in self.error_handlers: + self.error_handlers.remove(handler) + + def removeTypeHandler(self, the_type, handler): + """ + De-registers a type handler. + + Args: + handler (any): + """ + + if the_type in self.type_handlers \ + and handler in self.type_handlers[the_type]: + self.type_handlers[the_type].remove(handler) + + def start(self): + """ + Thread target of thread 'broadcaster'. Runs in background and processes + the update queue. + """ + + while True: + try: + # Pop update from update queue. + # Blocks if no updates are available. + update = self.update_queue.get() + self.processUpdate(update) + + # Broadcast any errors + except TelegramError as te: + self.broadcastError(te) + + def processUpdate(self, update): + """ + Processes a single update. + + Args: + update (any): + """ + + handled = False + + # Custom type handlers + for t in self.type_handlers: + if isinstance(update, t): + self.broadcastType(update) + handled = True + + # string update + if type(update) is str and update.startswith('/'): + self.broadcastStringCommand(update) + handled = True + elif type(update) is str: + self.broadcastStringRegex(update) + handled = True + + # An error happened while polling + if isinstance(update, TelegramError): + self.broadcastError(update) + handled = True + + # Telegram update (regex) + if isinstance(update, Update): + self.broadcastTelegramRegex(update) + handled = True + + # Telegram update (command) + if isinstance(update, Update) \ + and update.message.text.startswith('/'): + self.broadcastTelegramCommand(update) + handled = True + + # Telegram update (message) + elif isinstance(update, Update): + self.broadcastTelegramMessage(update) + handled = True + + # Update not recognized + if not handled: + self.broadcastError(TelegramError( + "Received update of unknown type %s" % type(update))) + + def broadcastTelegramCommand(self, update): + """ + Broadcasts an update that contains a command. + + Args: + command (str): The command keyword + update (telegram.Update): The Telegram update that contains the + command + """ + + command = update.message.text.split(' ')[0][1:].split('@')[0] + + if command in self.telegram_command_handlers: + self.broadcastTo(self.telegram_command_handlers[command], update) + else: + self.broadcastTo(self.unknown_telegram_command_handlers, update) + + def broadcastTelegramRegex(self, update): + """ + Broadcasts an update to all regex handlers that match the message string. + + Args: + command (str): The command keyword + update (telegram.Update): The Telegram update that contains the + command + """ + + matching_handlers = [] + + for matcher in self.telegram_regex_handlers: + if matcher.match(update.message.text): + for handler in self.telegram_regex_handlers[matcher]: + matching_handlers.append(handler) + + self.broadcastTo(matching_handlers, update) + + def broadcastStringCommand(self, update): + """ + Broadcasts a string-update that contains a command. + + Args: + update (str): The string input + """ + + command = update.split(' ')[0][1:] + + if command in self.string_command_handlers: + self.broadcastTo(self.string_command_handlers[command], update) + else: + self.broadcastTo(self.unknown_string_command_handlers, update) + + def broadcastStringRegex(self, update): + """ + Broadcasts an update to all string regex handlers that match the string. + + Args: + command (str): The command keyword + update (telegram.Update): The Telegram update that contains the + command + """ + + matching_handlers = [] + + for matcher in self.string_regex_handlers: + if matcher.match(update): + for handler in self.string_regex_handlers[matcher]: + matching_handlers.append(handler) + + self.broadcastTo(matching_handlers, update) + + def broadcastType(self, update): + """ + Broadcasts an update of any type. + + Args: + update (any): The update + """ + + for t in self.type_handlers: + if isinstance(update, t): + self.broadcastTo(self.type_handlers[t], update) + else: + self.broadcastError(TelegramError( + "Received update of unknown type %s" % type(update))) + + def broadcastTelegramMessage(self, update): + """ + Broadcasts an update that contains a regular message. + + Args: + update (telegram.Update): The Telegram update that contains the + message. + """ + + self.broadcastTo(self.telegram_message_handlers, update) + + def broadcastError(self, error): + """ + Broadcasts an error. + + Args: + error (telegram.TelegramError): The Telegram error that was raised. + """ + + for handler in self.error_handlers: + handler(self.bot, error) + + def broadcastTo(self, handlers, update): + """ + Broadcasts an update to a list of handlers. + + Args: + handlers (list): A list of handler-functions. + update (any): The update to be broadcasted + """ + + for handler in handlers: + handler(self.bot, update) From eb2be61eac00c77981b9b87d61a266e7b90b1bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Thu, 5 Nov 2015 13:52:57 +0100 Subject: [PATCH 2/8] example bot for BotEventHandler --- examples/eventhandler_bot.py | 96 ++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 examples/eventhandler_bot.py diff --git a/examples/eventhandler_bot.py b/examples/eventhandler_bot.py new file mode 100644 index 000000000..1d0d6c2b7 --- /dev/null +++ b/examples/eventhandler_bot.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +""" +This Bot uses the BotEventHandler class to handle the bot. + +First, a few handler functions are defined. Then, those functions are passed to +the Broadcaster and registered at their respective places. +Then, the bot is started and the CLI-Loop is entered, where all text inputs are +inserted into the update queue for the bot to handle. + +""" + +import sys +from telegram.boteventhandler import BotEventHandler +import re + +global last_chat_id +last_chat_id = 0 + +def removeCommand(text): + return ' '.join(text.split(' ')[1:]) + +""" Command Handlers """ +def startCommandHandler(bot, update): + bot.sendMessage(update.message.chat_id, text='Hi!') + +def helpCommandHandler(bot, update): + bot.sendMessage(update.message.chat_id, text='Help!') + +def anyMessageHandler(bot, update): + print("chat_id: %d\nFrom: %s\nText: %s" % + (update.message.chat_id, str(update.message.from_user), + update.message.text)) + +def unknownCommandHandler(bot, update): + bot.sendMessage(update.message.chat_id, text='Command not recognized!') + +def messageHandler(bot, update): + global last_chat_id + last_chat_id = update.message.chat_id + bot.sendMessage(update.message.chat_id, text=update.message.text) + +def errorHandler(bot, error): + print(str(error)) + +def CLIReplyCommandHandler(bot, update): + if last_chat_id is not 0: + bot.sendMessage(chat_id=last_chat_id, text=removeCommand(update)) + +def unknownCLICommandHandler(bot, update): + print("Command not found: %s" % update) + +def main(): + # Create the EventHandler and pass it your bot's token + eh = BotEventHandler("TOKEN") + + # on different commands - answer in Telegram + eh.broadcaster.addTelegramCommandHandler("start", startCommandHandler) + eh.broadcaster.addTelegramCommandHandler("help", helpCommandHandler) + + # on regex match - print all messages to stdout + eh.broadcaster.addTelegramRegexHandler(re.compile('.*'), anyMessageHandler) + + # on CLI commands - type "/reply text" in terminal to reply to the last + # active chat + eh.broadcaster.addStringCommandHandler('reply', CLIReplyCommandHandler) + + # on unknown commands - answer on Telegram + eh.broadcaster.addUnknownTelegramCommandHandler(unknownCommandHandler) + + # on unknown CLI commands - notify the user + eh.broadcaster.addUnknownStringCommandHandler(unknownCLICommandHandler) + + # on noncommand i.e message - echo the message on Telegram + eh.broadcaster.addTelegramMessageHandler(messageHandler) + + # on error - print error to stdout + eh.broadcaster.addErrorHandler(errorHandler) + + # Start the Bot and store the update Queue, + # so we can insert updates ourselves + update_queue = eh.start() + + # Start CLI-Loop + while True: + if sys.version_info.major is 2: + text = raw_input() + elif sys.version_info.major is 3: + text = input() + + if len(text) > 0: + update_queue.put(text) # Put command into queue + +if __name__ == '__main__': + main() + From aabaaa9049ed4ca8390faaa45f36db58814e314e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Thu, 5 Nov 2015 14:07:28 +0100 Subject: [PATCH 3/8] Update AUTHORS.rst --- AUTHORS.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 9b6f57d20..83361aebc 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -13,8 +13,10 @@ The following wonderful people contributed directly or indirectly to this projec - `ErgoZ Riftbit Vaper `_ - `franciscod `_ - `JASON0916 `_ +- `jh0ker `_ - `JRoot3D `_ - `macrojames `_ +- `naveenvhegde `_ - `njittam `_ - `Rahiel Kasim `_ - `sooyhwang `_ From d36318503170c72b476874ad65f1de2d89fac498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Thu, 5 Nov 2015 15:22:13 +0100 Subject: [PATCH 4/8] Import BotEventHandler and Broadcaster --- telegram/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/telegram/__init__.py b/telegram/__init__.py index b3dffe976..f26b0373e 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -47,8 +47,10 @@ from .parsemode import ParseMode from .message import Message from .update import Update from .bot import Bot +from .broadcaster import Broadcaster +from .boteventhandler import BotEventHandler -__all__ = ['Bot', 'Emoji', 'TelegramError', 'InputFile', 'ReplyMarkup', +__all__ = ['Bot', 'BotEventHandler', 'Broadcaster', 'Emoji', 'TelegramError', 'InputFile', 'ReplyMarkup', 'ForceReply', 'ReplyKeyboardHide', 'ReplyKeyboardMarkup', 'UserProfilePhotos', 'ChatAction', 'Location', 'Contact', 'Video', 'Sticker', 'Document', 'File', 'Audio', 'PhotoSize', From 63a8700258ca5b32191b49a971ca0c6ab775ee4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Thu, 5 Nov 2015 16:01:08 +0100 Subject: [PATCH 5/8] add run_async decorator --- telegram/boteventhandler.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/telegram/boteventhandler.py b/telegram/boteventhandler.py index eaaf32c97..2fa200155 100644 --- a/telegram/boteventhandler.py +++ b/telegram/boteventhandler.py @@ -15,6 +15,18 @@ if sys.version_info.major is 2: from Queue import Queue elif sys.version_info.major is 3: from queue import Queue + +def run_async(func): + from threading import Thread + from functools import wraps + + @wraps(func) + def async_func(*args, **kwargs): + func_hl = Thread(target=func, args=args, kwargs=kwargs) + func_hl.start() + return func_hl + + return async_func class BotEventHandler(TelegramObject): """ @@ -88,7 +100,8 @@ class BotEventHandler(TelegramObject): self.update_queue.put(update) self.last_update_id = update.update_id + 1 current_interval = poll_interval - sleep(current_interval) + + sleep(current_interval) except TelegramError as te: # Put the error into the update queue and let the Broadcaster # broadcast it From 2ec56abe7ec27381c3f721d66eb91cdd16001826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Thu, 5 Nov 2015 16:01:25 +0100 Subject: [PATCH 6/8] use run_async decorator in example --- examples/eventhandler_bot.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/examples/eventhandler_bot.py b/examples/eventhandler_bot.py index 1d0d6c2b7..8eae61b57 100644 --- a/examples/eventhandler_bot.py +++ b/examples/eventhandler_bot.py @@ -11,7 +11,9 @@ inserted into the update queue for the bot to handle. """ import sys -from telegram.boteventhandler import BotEventHandler +from telegram import BotEventHandler +from telegram.boteventhandler import run_async +from time import sleep import re global last_chat_id @@ -35,9 +37,18 @@ def anyMessageHandler(bot, update): def unknownCommandHandler(bot, update): bot.sendMessage(update.message.chat_id, text='Command not recognized!') +@run_async def messageHandler(bot, update): + """ + Example for an asynchronous handler. It's not guaranteed that replies will + be in order when using @run_async + """ + + # Save last chat_id to use in reply handler global last_chat_id last_chat_id = update.message.chat_id + + sleep(2) # IO-heavy operation here bot.sendMessage(update.message.chat_id, text=update.message.text) def errorHandler(bot, error): From 404fdc2dd7210839789dab34d880a656d7c7ac92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Fri, 6 Nov 2015 00:23:34 +0100 Subject: [PATCH 7/8] First tests for BotEventHandler and Broadcaster --- tests/test_boteventhandler.py | 175 ++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 tests/test_boteventhandler.py diff --git a/tests/test_boteventhandler.py b/tests/test_boteventhandler.py new file mode 100644 index 000000000..d260878a9 --- /dev/null +++ b/tests/test_boteventhandler.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# encoding: utf-8 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015 Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +""" This module contains a object that represents Tests for BotEventHandler """ + +import unittest +import sys +from time import sleep + +import re + +sys.path.append('.') + +from telegram import Update, Message, TelegramError +from tests.base import BaseTest + + +class BotEventHandlerTest(BaseTest, unittest.TestCase): + """This object represents Tests for Telegram Bot.""" + + def setUp(self): + from telegram import BotEventHandler + self.beh = BotEventHandler('') + + self.received_message = None + self.message_count = 0 + + def telegramHandlerTest(self, bot, update): + self.received_message = update.message.text + self.message_count += 1 + + def stringHandlerTest(self, bot, update): + self.received_message = update + self.message_count += 1 + + def test_addTelegramMessageHandler(self): + print('Testing addTelegramMessageHandler') + self.beh.bot = MockBot('Test') + self.beh.broadcaster.addTelegramMessageHandler( + self.telegramHandlerTest) + self.beh.start(0.05) + sleep(.1) + self.assertEqual(self.received_message, 'Test') + + def test_addTelegramMessageHandlerMultipleMessages(self): + print('Testing addTelegramMessageHandler and send 100 messages...') + self.beh.bot = MockBot('Multiple', 1000) + self.beh.broadcaster.addTelegramMessageHandler( + self.telegramHandlerTest) + self.beh.start(0.0) + sleep(.1) + self.assertEqual(self.received_message, 'Multiple') + self.assertEqual(self.message_count, 1000) + + def test_addTelegramRegexHandler(self): + print('Testing addStringRegexHandler') + self.beh.bot = MockBot('Test2') + self.beh.broadcaster.addTelegramRegexHandler(re.compile('Te.*'), + self.telegramHandlerTest) + self.beh.start(0.05) + sleep(.1) + self.assertEqual(self.received_message, 'Test2') + + def test_addTelegramCommandHandler(self): + print('Testing addTelegramCommandHandler') + self.beh.bot = MockBot('/test') + self.beh.broadcaster.addTelegramCommandHandler( + 'test', self.telegramHandlerTest) + self.beh.start(0.05) + sleep(.1) + self.assertEqual(self.received_message, '/test') + + def test_addUnknownTelegramCommandHandler(self): + print('Testing addUnknownTelegramCommandHandler') + self.beh.bot = MockBot('/test2') + self.beh.broadcaster.addUnknownTelegramCommandHandler( + self.telegramHandlerTest) + self.beh.start(0.05) + sleep(.1) + self.assertEqual(self.received_message, '/test2') + + def test_addStringRegexHandler(self): + print('Testing addStringRegexHandler') + self.beh.bot = MockBot('') + self.beh.broadcaster.addStringRegexHandler(re.compile('Te.*'), + self.stringHandlerTest) + queue = self.beh.start(0.05) + queue.put('Test3') + sleep(.1) + self.assertEqual(self.received_message, 'Test3') + + def test_addStringCommandHandler(self): + print('Testing addStringCommandHandler') + self.beh.bot = MockBot('') + self.beh.broadcaster.addStringCommandHandler( + 'test3', self.stringHandlerTest) + + queue = self.beh.start(0.05) + queue.put('/test3') + sleep(.1) + self.assertEqual(self.received_message, '/test3') + + def test_addUnknownStringCommandHandler(self): + print('Testing addUnknownStringCommandHandler') + self.beh.bot = MockBot('/test') + self.beh.broadcaster.addUnknownStringCommandHandler( + self.stringHandlerTest) + queue = self.beh.start(0.05) + queue.put('/test4') + sleep(.1) + self.assertEqual(self.received_message, '/test4') + + def test_addErrorHandler(self): + print('Testing addErrorHandler') + self.beh.bot = MockBot('') + self.beh.broadcaster.addErrorHandler(self.stringHandlerTest) + queue = self.beh.start(0.05) + error = TelegramError("Unauthorized.") + queue.put(error) + sleep(.1) + self.assertEqual(self.received_message, error) + + def test_addTypeHandler(self): + print('Testing addTypeHandler') + self.beh.bot = MockBot('') + self.beh.broadcaster.addTypeHandler(dict, self.stringHandlerTest) + queue = self.beh.start(0.05) + payload = {"Test": 42} + queue.put(payload) + sleep(.1) + self.assertEqual(self.received_message, payload) + +class MockBot: + + def __init__(self, text, messages=1): + self.text = text + self.send_messages = messages + pass + + def mockUpdate(self, text): + message = Message(0, None, None, None) + message.text = text + update = Update(0) + update.message = message + return update + + def getUpdates(self, offset): + + if self.send_messages >= 2: + self.send_messages -= 2 + return self.mockUpdate(self.text), self.mockUpdate(self.text) + elif self.send_messages == 1: + self.send_messages -= 1 + return self.mockUpdate(self.text), + else: + return [] + +if __name__ == '__main__': + unittest.main() From c19a84ac055530dad47876c35802b686e5651835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jannes=20H=C3=B6ke?= Date: Fri, 6 Nov 2015 00:24:01 +0100 Subject: [PATCH 8/8] improved code quality --- examples/eventhandler_bot.py | 3 ++- telegram/boteventhandler.py | 34 +++++++++++++++++++--------------- telegram/broadcaster.py | 9 +++++---- 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/examples/eventhandler_bot.py b/examples/eventhandler_bot.py index 8eae61b57..575209c30 100644 --- a/examples/eventhandler_bot.py +++ b/examples/eventhandler_bot.py @@ -37,6 +37,7 @@ def anyMessageHandler(bot, update): def unknownCommandHandler(bot, update): bot.sendMessage(update.message.chat_id, text='Command not recognized!') + @run_async def messageHandler(bot, update): """ @@ -63,7 +64,7 @@ def unknownCLICommandHandler(bot, update): def main(): # Create the EventHandler and pass it your bot's token - eh = BotEventHandler("TOKEN") + eh = BotEventHandler("148447715:AAGG70cC6s_kwrji2pLsaMeEzKGe0llA1hY") # on different commands - answer in Telegram eh.broadcaster.addTelegramCommandHandler("start", startCommandHandler) diff --git a/telegram/boteventhandler.py b/telegram/boteventhandler.py index 2fa200155..3a6ffe79d 100644 --- a/telegram/boteventhandler.py +++ b/telegram/boteventhandler.py @@ -9,24 +9,34 @@ import sys from threading import Thread from telegram import (Bot, TelegramError, TelegramObject, Broadcaster) from time import sleep +from functools import wraps # Adjust for differences in Python versions if sys.version_info.major is 2: from Queue import Queue elif sys.version_info.major is 3: from queue import Queue - + + def run_async(func): - from threading import Thread - from functools import wraps + """ + Function decorator that will run the function in a new thread. - @wraps(func) - def async_func(*args, **kwargs): - func_hl = Thread(target=func, args=args, kwargs=kwargs) - func_hl.start() - return func_hl + Args: + func (function): The function to run in the thread. + + Returns: + function: + """ + + @wraps(func) + def async_func(*args, **kwargs): + thread = Thread(target=func, args=args, kwargs=kwargs) + thread.start() + return thread + + return async_func - return async_func class BotEventHandler(TelegramObject): """ @@ -40,9 +50,6 @@ class BotEventHandler(TelegramObject): Args: token (str): The bots token given by the @BotFather - **kwargs: Arbitrary keyword arguments. - - Keyword Args: base_url (Optional[str]): """ @@ -57,9 +64,6 @@ class BotEventHandler(TelegramObject): Starts polling updates from Telegram. Args: - **kwargs: Arbitrary keyword arguments. - - Keyword Args: poll_interval (Optional[float]): Time to wait between polling updates from Telegram in seconds. Default is 1.0 diff --git a/telegram/broadcaster.py b/telegram/broadcaster.py index 63e45dd07..4441efd56 100644 --- a/telegram/broadcaster.py +++ b/telegram/broadcaster.py @@ -6,6 +6,7 @@ This module contains the Broadcaster class. from telegram import (TelegramError, TelegramObject, Update) + class Broadcaster(TelegramObject): """ This class broadcasts all kinds of updates to its registered handlers. @@ -15,7 +16,7 @@ class Broadcaster(TelegramObject): Args: bot (telegram.Bot): The bot object that should be passed to the handlers update_queue (queue.Queue): The synchronized queue that will contain the - updates + updates. """ def __init__(self, bot, update_queue): self.bot = bot @@ -64,8 +65,8 @@ class Broadcaster(TelegramObject): if matcher.math(update.message.text) is True. Args: - matcher (str): A compiled regex object that matches on messages that - handler should be listening to + matcher (__Regex): A compiled regex object that matches on messages + that handler should be listening to handler (function): A function that takes (Bot, Update) as arguments. """ @@ -96,7 +97,7 @@ class Broadcaster(TelegramObject): if matcher.math(string) is True. Args: - matcher (str): A compiled regex object that matches on the string + matcher (__Regex): A compiled regex object that matches on the string input that handler should be listening to handler (function): A function that takes (Bot, Update) as arguments.