diff --git a/AUTHORS.rst b/AUTHORS.rst index cc898d1fc..4ba404d5f 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -16,10 +16,12 @@ The following wonderful people contributed directly or indirectly to this projec - `JASON0916 `_ - `jh0ker `_ - `JRoot3D `_ +- `jlmadurga `_ - `macrojames `_ - `naveenvhegde `_ - `njittam `_ - `Noam Meltzer `_ +- `Oleg Shlyazhko `_ - `Rahiel Kasim `_ - `sooyhwang `_ - `wjt `_ diff --git a/telegram/__init__.py b/telegram/__init__.py index 7a303f35d..42a2a985c 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -51,8 +51,10 @@ from .update import Update from .bot import Bot from .dispatcher import Dispatcher from .jobqueue import JobQueue +from .updatequeue import UpdateQueue from .updater import Updater + __author__ = 'devs@python-telegram-bot.org' __version__ = '3.3b1' __all__ = ('Bot', 'Updater', 'Dispatcher', 'Emoji', 'TelegramError', @@ -63,4 +65,5 @@ __all__ = ('Bot', 'Updater', 'Dispatcher', 'Emoji', 'TelegramError', 'User', 'TelegramObject', 'NullHandler', 'Voice', 'JobQueue', 'InlineQuery', 'ChosenInlineResult', 'InlineQueryResultArticle', 'InlineQueryResultGif', 'InlineQueryResultPhoto', - 'InlineQueryResultMpeg4Gif', 'InlineQueryResultVideo') + 'InlineQueryResultMpeg4Gif', 'InlineQueryResultVideo', + 'UpdateQueue') diff --git a/telegram/dispatcher.py b/telegram/dispatcher.py index 99388ba9c..4af60f1c5 100644 --- a/telegram/dispatcher.py +++ b/telegram/dispatcher.py @@ -110,6 +110,13 @@ class Dispatcher: whole query split on spaces. For other updates, args will be None + In some cases handlers may need some context data to process the update. To + procedure just queue in update_queue.put(update, context=context) or + processUpdate(update,context=context). + + context: + Extra data for handling updates. + For regex-based handlers, you can also request information about the match. For all other handlers, these will be None @@ -123,7 +130,7 @@ class Dispatcher: Args: bot (telegram.Bot): The bot object that should be passed to the handlers - update_queue (queue.Queue): The synchronized queue that will + update_queue (UpdateQueue): The synchronized queue that will contain the updates. """ def __init__(self, bot, update_queue, workers=4): @@ -174,14 +181,15 @@ class Dispatcher: try: # Pop update from update queue. # Blocks if no updates are available. - update = self.update_queue.get() + update, context = self.update_queue.get(context=True) if type(update) is self._Stop: self.running = False break - self.processUpdate(update) - self.logger.debug('Processed Update: %s' % update) + self.processUpdate(update, context) + self.logger.debug('Processed Update: %s with context %s' + % (update, context)) # Dispatch any errors except TelegramError as te: @@ -207,7 +215,7 @@ class Dispatcher: while self.running: sleep(0.1) - def processUpdate(self, update): + def processUpdate(self, update, context=None): """ Processes a single update. @@ -220,15 +228,15 @@ class Dispatcher: # Custom type handlers for t in self.type_handlers: if isinstance(update, t): - self.dispatchType(update) + self.dispatchType(update, context) handled = True # string update if type(update) is str and update.startswith('/'): - self.dispatchStringCommand(update) + self.dispatchStringCommand(update, context) handled = True elif type(update) is str: - self.dispatchRegex(update) + self.dispatchRegex(update, context) handled = True # An error happened while polling @@ -238,21 +246,21 @@ class Dispatcher: # Telegram update (regex) if isinstance(update, Update) and update.message is not None: - self.dispatchRegex(update) + self.dispatchRegex(update, context) handled = True # Telegram update (command) if update.message.text.startswith('/'): - self.dispatchTelegramCommand(update) + self.dispatchTelegramCommand(update, context) # Telegram update (message) else: - self.dispatchTelegramMessage(update) + self.dispatchTelegramMessage(update, context) handled = True elif isinstance(update, Update) and \ (update.inline_query is not None or update.chosen_inline_result is not None): - self.dispatchTelegramInline(update) + self.dispatchTelegramInline(update, context) handled = True # Update not recognized if not handled: @@ -519,7 +527,7 @@ class Dispatcher: and handler in self.type_handlers[the_type]: self.type_handlers[the_type].remove(handler) - def dispatchTelegramCommand(self, update): + def dispatchTelegramCommand(self, update, context=None): """ Dispatches an update that contains a command. @@ -532,11 +540,13 @@ class Dispatcher: command = update.message.text.split(' ')[0][1:].split('@')[0] if command in self.telegram_command_handlers: - self.dispatchTo(self.telegram_command_handlers[command], update) + self.dispatchTo(self.telegram_command_handlers[command], update, + context=context) else: - self.dispatchTo(self.unknown_telegram_command_handlers, update) + self.dispatchTo(self.unknown_telegram_command_handlers, update, + context=context) - def dispatchRegex(self, update): + def dispatchRegex(self, update, context=None): """ Dispatches an update to all string or telegram regex handlers that match the string/message content. @@ -559,9 +569,10 @@ class Dispatcher: self.call_handler(handler, update, groups=m.groups(), - groupdict=m.groupdict()) + groupdict=m.groupdict(), + context=context) - def dispatchStringCommand(self, update): + def dispatchStringCommand(self, update, context=None): """ Dispatches a string-update that contains a command. @@ -572,11 +583,13 @@ class Dispatcher: command = update.split(' ')[0][1:] if command in self.string_command_handlers: - self.dispatchTo(self.string_command_handlers[command], update) + self.dispatchTo(self.string_command_handlers[command], update, + context=context) else: - self.dispatchTo(self.unknown_string_command_handlers, update) + self.dispatchTo(self.unknown_string_command_handlers, update, + context=context) - def dispatchType(self, update): + def dispatchType(self, update, context=None): """ Dispatches an update of any type. @@ -586,9 +599,9 @@ class Dispatcher: for t in self.type_handlers: if isinstance(update, t): - self.dispatchTo(self.type_handlers[t], update) + self.dispatchTo(self.type_handlers[t], update, context=context) - def dispatchTelegramMessage(self, update): + def dispatchTelegramMessage(self, update, context=None): """ Dispatches an update that contains a regular message. @@ -597,9 +610,10 @@ class Dispatcher: message. """ - self.dispatchTo(self.telegram_message_handlers, update) + self.dispatchTo(self.telegram_message_handlers, update, + context=context) - def dispatchTelegramInline(self, update): + def dispatchTelegramInline(self, update, context=None): """ Dispatches an update that contains an inline update. @@ -608,7 +622,7 @@ class Dispatcher: message. """ - self.dispatchTo(self.telegram_inline_handlers, update) + self.dispatchTo(self.telegram_inline_handlers, update, context=None) def dispatchError(self, update, error): """ @@ -675,4 +689,7 @@ class Dispatcher: if is_async or 'groupdict' in fargs: target_kwargs['groupdict'] = kwargs.get('groupdict', None) + if is_async or 'context' in fargs: + target_kwargs['context'] = kwargs.get('context', None) + handler(self.bot, update, **target_kwargs) diff --git a/telegram/updatequeue.py b/telegram/updatequeue.py new file mode 100644 index 000000000..21032a4da --- /dev/null +++ b/telegram/updatequeue.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2016 +# Leandro Toledo de Souza +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser 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 Lesser Public License for more details. +# +# You should have received a copy of the GNU Lesser Public License +# along with this program. If not, see [http://www.gnu.org/licenses/]. + +"""This module contains the class UpdateQueue to override standard +Queue.""" + + +# Adjust for differences in Python versions +try: + from queue import Queue +except ImportError: + from Queue import Queue + + +class UpdateQueue(Queue): + """ + This class overrides standard Queues. Allows you to de/queue context + data apart from the handled `update` + """ + + def put(self, item, block=True, timeout=None, context=None): + """ + Put an item into the queue with context data if provided as a + tuple (item, context). Overrides standard Queue.put method. + + Args: + update (any): handled by the dispatcher + context (any): extra data to use in handlers + """ + Queue.put(self, (item, context), block, timeout) + + def get(self, block=True, timeout=None, context=False): + """ + Remove and return an item from the queue. A tuple of + (update, context) if requested. Overrides standard Queue.get + method. + + Args: + context (boolean): set true to get (update, context) + """ + if not context: + return Queue.get(self, block, timeout)[0] + return Queue.get(self, block, timeout) diff --git a/telegram/updater.py b/telegram/updater.py index 5903b63ec..cd9d3f3f5 100644 --- a/telegram/updater.py +++ b/telegram/updater.py @@ -29,15 +29,9 @@ from time import sleep import subprocess from signal import signal, SIGINT, SIGTERM, SIGABRT from telegram import (Bot, TelegramError, dispatcher, Dispatcher, - NullHandler, JobQueue) + NullHandler, JobQueue, UpdateQueue) from telegram.utils.webhookhandler import (WebhookServer, WebhookHandler) -# Adjust for differences in Python versions -try: - from Queue import Queue -except ImportError: - from queue import Queue - try: from urllib2 import URLError except ImportError: @@ -89,7 +83,7 @@ class Updater: self.bot = bot else: self.bot = Bot(token, base_url) - self.update_queue = Queue() + self.update_queue = UpdateQueue() self.job_queue = JobQueue(self.bot, job_queue_tick_interval) self.dispatcher = Dispatcher(self.bot, self.update_queue, workers=workers) diff --git a/telegram/utils/botan.py b/telegram/utils/botan.py new file mode 100644 index 000000000..1c6b774c3 --- /dev/null +++ b/telegram/utils/botan.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +import json +import logging +from telegram import NullHandler + +try: + from urllib.request import urlopen, Request + from urllib.parse import quote + from urllib.error import URLError, HTTPError +except ImportError: + from urllib2 import urlopen, Request + from urllib import quote + from urllib2 import URLError, HTTPError + +H = NullHandler() +logging.getLogger(__name__).addHandler(H) + + +class Botan(object): + """This class helps to send incoming events in your botan analytics account. + See more: https://github.com/botanio/sdk#botan-sdk""" + + token = '' + url_template = 'https://api.botan.io/track?token={token}' \ + '&uid={uid}&name={name}&src=python-telegram-bot' + + def __init__(self, token): + self.token = token + self.logger = logging.getLogger(__name__) + + def track(self, message, event_name='event'): + try: + uid = message.chat_id + except AttributeError: + self.logger.warn('No chat_id in message') + return False + data = json.dumps(message.__dict__) + try: + url = self.url_template.format(token=str(self.token), + uid=str(uid), + name=quote(event_name)) + request = Request(url, + data=data.encode(), + headers={'Content-Type': 'application/json'}) + urlopen(request) + return True + except HTTPError as error: + self.logger.warn('Botan track error ' + + str(error.code) + + ':' + error.read().decode('utf-8')) + return False + except URLError as error: + self.logger.warn('Botan track error ' + str(error.reason)) + return False diff --git a/telegram/utils/request.py b/telegram/utils/request.py index 63059a704..e5493f00b 100644 --- a/telegram/utils/request.py +++ b/telegram/utils/request.py @@ -25,6 +25,13 @@ import json import socket from ssl import SSLError +try: + # python2 + from httplib import HTTPException +except ImportError: + # python3 + from http.client import HTTPException + try: from urllib.request import urlopen, urlretrieve, Request from urllib.error import HTTPError @@ -82,6 +89,8 @@ def _try_except_req(func): raise TelegramError("Timed out") raise TelegramError(str(error)) + except HTTPException as error: + raise TelegramError('HTTPException: {0!r}'.format(error)) return decorator diff --git a/tests/test_botan.py b/tests/test_botan.py new file mode 100644 index 000000000..1206b8bab --- /dev/null +++ b/tests/test_botan.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python + +"""This module contains a object that represents Tests for Botan analytics integration""" + +import os +import unittest +import sys +sys.path.append('.') + +from telegram.utils.botan import Botan +from tests.base import BaseTest + + +class MessageMock(object): + chat_id = None + + def __init__(self, chat_id): + self.chat_id = chat_id + + +class BotanTest(BaseTest, unittest.TestCase): + """This object represents Tests for Botan analytics integration.""" + + token = os.environ.get('BOTAN_TOKEN') + + def test_track(self): + """Test sending event to botan""" + print('Test sending event to botan') + botan = Botan(self.token) + message = MessageMock(self._chat_id) + result = botan.track(message, 'named event') + self.assertTrue(result) + + def test_track_fail(self): + """Test fail when sending event to botan""" + print('Test fail when sending event to botan') + botan = Botan(self.token) + botan.url_template = 'https://api.botan.io/traccc?token={token}&uid={uid}&name={name}' + message = MessageMock(self._chat_id) + result = botan.track(message, 'named event') + self.assertFalse(result) + + def test_wrong_message(self): + """Test sending wrong message""" + print('Test sending wrong message') + botan = Botan(self.token) + message = MessageMock(self._chat_id) + message = delattr(message, 'chat_id') + result = botan.track(message, 'named event') + self.assertFalse(result) + + def test_wrong_endpoint(self): + """Test wrong endpoint""" + print('Test wrong endpoint') + botan = Botan(self.token) + botan.url_template = 'https://api.botaaaaan.io/traccc?token={token}&uid={uid}&name={name}' + message = MessageMock(self._chat_id) + result = botan.track(message, 'named event') + self.assertFalse(result) + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_updater.py b/tests/test_updater.py index 7e42da625..3c1472a43 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -109,6 +109,11 @@ class UpdaterTest(BaseTest, unittest.TestCase): update_queue.put('/test5 noresend') elif args[0] == 'noresend': pass + + def contextTest(self, bot, update, context): + self.received_message = update + self.message_count += 1 + self.context = context @run_async def asyncAdditionalHandlerTest(self, bot, update, update_queue=None, @@ -348,6 +353,20 @@ class UpdaterTest(BaseTest, unittest.TestCase): sleep(.1) self.assertEqual(self.received_message, '/test5 noresend') self.assertEqual(self.message_count, 2) + + def test_context(self): + print('Testing context for handlers') + context = "context_data" + self._setup_updater('', messages=0) + self.updater.dispatcher.addStringCommandHandler( + 'test_context', self.contextTest) + + queue = self.updater.start_polling(0.01) + queue.put('/test_context', context=context) + sleep(.5) + self.assertEqual(self.received_message, '/test_context') + self.assertEqual(self.message_count, 1) + self.assertEqual(self.context, context) def test_regexGroupHandler(self): print('Testing optional groups and groupdict parameters')