diff --git a/.travis.yml b/.travis.yml index f2630b00c..066b7d757 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,11 @@ python: - "2.7" - "3.3" - "3.4" + - "pypy" + - "pypy3" install: - pip install coveralls script: - coverage run telegram_test.py + coverage run tests/test_*.py after_success: coveralls diff --git a/AUTHORS.rst b/AUTHORS.rst index 55a05b75b..5ce4542de 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -14,6 +14,7 @@ The following wonderful people contributed directly or indirectly to this projec - `JASON0916 `_ - `JRoot3D `_ - `macrojames `_ +- `njittam `_ - `Rahiel Kasim `_ - `sooyhwang `_ - `wjt `_ diff --git a/CHANGES b/CHANGES.rst similarity index 100% rename from CHANGES rename to CHANGES.rst diff --git a/Makefile b/Makefile index ec8ec3b62..79d2c0ff7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,4 @@ -help: - @echo " clean remove unwanted stuff" - @echo " lint check style with flake8" - @echo " test run tests" +.PHONY: clean pep8 lint test clean: rm -fr build @@ -10,8 +7,18 @@ clean: find . -name '*.pyo' -exec rm -f {} \; find . -name '*~' -exec rm -f {} \; +pep8: + flake8 telegram + lint: - flake8 --doctests --max-complexity 10 telegram + pylint -E telegram test: - python telegram_test.py + @- $(foreach TEST, $(wildcard tests/test_*.py), python $(TEST)) + +help: + @echo "Available targets:" + @echo "- clean Clean up the source directory" + @echo "- pep8 Check style with flake8" + @echo "- lint Check style with pylint" + @echo "- test Run tests" diff --git a/docs-requirements.txt b/docs/requirements-docs.txt similarity index 100% rename from docs-requirements.txt rename to docs/requirements-docs.txt diff --git a/examples/command_handler_example.py b/examples/command_handler_example.py new file mode 100644 index 000000000..33e1bc9f8 --- /dev/null +++ b/examples/command_handler_example.py @@ -0,0 +1,89 @@ +# There could be some unused imports +from inspect import getmembers, ismethod +import threading +import logging +import telegram +import time +from telegram import CommandHandlerWithHelp, CommandHandler +class ExampleCommandHandler(CommandHandlerWithHelp): + """This is an example how to use a CommandHandlerWithHelp or just a CommandHandler. + + If You want to use a CommandHandler it is very easy. + create a class which inherits a CommandHandler. + create a method in this class which start with 'command_' and takes 1 argument: 'update' (which comes directly from + getUpdate()). + If you inherit CommandHandlerWithHelp it also creates a nice /help for you. + """ + def __init__(self, bot): # only necessary for a WithHelp + super(ExampleCommandHandler, self).__init__(bot) + self._help_title = 'Welcome this is a help file!' # optional + self._help_before_list = """ + Yeah here I explain some things about this bot. + and of course I can do this in Multiple lines. + """ # default is empty + self._help_list_title = ' These are the available commands:' # optional + self._help_after_list = ' These are some footnotes' # default is empty + self.is_reply = True # default is True + + # only necessary if you want to override to default + def _command_not_found(self, update): + """Inform the telegram user that the command was not found.""" + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = "Sorry, I don't know how to do {command}.".format(command=update.message.text.split(' ')[0]) + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + + # creates /test command. This code gets called when a telegram user enters /test + def command_test(self, update): + """ Test if the server is online. """ + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = 'Yeah, the server is online!' + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + + # creates /parrot command + def command_parrot(self, update): + """ Says back what you say after the command""" + chat_id = update.message.chat.id + reply_to = update.message.message_id + send = update.message.text.split(' ') + message = update.message.text[len(send[0]):] + if len(send) == 1: + message = '...' + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + + # creates /p command + def command_p(self, update): + """Does the same as parrot.""" + return self.command_parrot(update) + + # this doesn't create a command. + def another_test(self, update): + """ This won't be called by the CommandHandler. + + This is an example of a function that isn't a command in telegram. + Because it didn't start with 'command_'. + """ + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = 'Yeah, this is another test' + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + + +class Exampe2CommandHandler(CommandHandler): + """ + This is an example of a small working CommandHandler with only one command. + """ + def command_test(self, update): + """ Test if the server is online. """ + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = 'Yeah, the server is online!' + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + +if __name__ == '__main__': + import telegram + token = '' # use your own token here + Bot = telegram.Bot(token=token) + test_command_handler = ExampleCommandHandler(Bot) + test_command_handler.run() diff --git a/telegram/__init__.py b/telegram/__init__.py index dcc3e6c08..78e0dad8c 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -16,14 +16,13 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +"""A library that provides a Python interface to the Telegram Bot API""" __author__ = 'leandrotoledodesouza@gmail.com' __version__ = '2.7.1' from .base import TelegramObject from .user import User -from .message import Message -from .update import Update from .groupchat import GroupChat from .photosize import PhotoSize from .audio import Audio @@ -39,10 +38,12 @@ from .replymarkup import ReplyMarkup from .replykeyboardmarkup import ReplyKeyboardMarkup from .replykeyboardhide import ReplyKeyboardHide from .forcereply import ForceReply -from .inputfile import InputFile from .error import TelegramError +from .inputfile import InputFile from .nullhandler import NullHandler from .emoji import Emoji +from .message import Message +from .update import Update from .bot import Bot __all__ = ['Bot', 'Emoji', 'TelegramError', 'InputFile', 'ReplyMarkup', diff --git a/telegram/audio.py b/telegram/audio.py index 5fff1d2e7..ff54b339d 100644 --- a/telegram/audio.py +++ b/telegram/audio.py @@ -16,43 +16,57 @@ # 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 a object that represents a Telegram Audio""" from telegram import TelegramObject class Audio(TelegramObject): + """This object represents a Telegram Audio. + + Attributes: + file_id (str): + duration (int): + performer (str): + title (str): + mime_type (str): + file_size (int): + + Args: + file_id (str): + duration (int): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + performer (Optional[str]): + title (Optional[str]): + mime_type (Optional[str]): + file_size (Optional[int]): + """ + def __init__(self, file_id, duration, - performer=None, - title=None, - mime_type=None, - file_size=None): + **kwargs): + # Required self.file_id = file_id - self.duration = duration - self.performer = performer - self.title = title - self.mime_type = mime_type - self.file_size = file_size + self.duration = int(duration) + # Optionals + self.performer = kwargs.get('performer', '') + self.title = kwargs.get('title', '') + self.mime_type = kwargs.get('mime_type', '') + self.file_size = int(kwargs.get('file_size', 0)) @staticmethod def de_json(data): - return Audio(file_id=data.get('file_id', None), - duration=data.get('duration', None), - performer=data.get('performer', None), - title=data.get('title', None), - mime_type=data.get('mime_type', None), - file_size=data.get('file_size', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'file_id': self.file_id, - 'duration': self.duration} - if self.performer: - data['performer'] = self.performer - if self.title: - data['title'] = self.title - if self.mime_type: - data['mime_type'] = self.mime_type - if self.file_size: - data['file_size'] = self.file_size - return data + Returns: + telegram.Audio: + """ + if not data: + return None + + return Audio(**data) diff --git a/telegram/base.py b/telegram/base.py index 6654fd7d0..ac0deac0f 100644 --- a/telegram/base.py +++ b/telegram/base.py @@ -16,13 +16,14 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +"""Base class for Telegram Objects""" import json -from abc import ABCMeta, abstractmethod +from abc import ABCMeta class TelegramObject(object): - """Base class for most telegram object""" + """Base class for most telegram objects""" __metaclass__ = ABCMeta @@ -34,11 +35,34 @@ class TelegramObject(object): @staticmethod def de_json(data): + """ + Args: + data (str): + + Returns: + telegram.TelegramObject: + """ raise NotImplementedError def to_json(self): + """ + Returns: + str: + """ return json.dumps(self.to_dict()) - @abstractmethod def to_dict(self): - return + """ + Returns: + dict: + """ + data = dict() + + for key, value in self.__dict__.items(): + if value: + if hasattr(value, 'to_dict'): + data[key] = value.to_dict() + else: + data[key] = value + + return data diff --git a/telegram/bot.py b/telegram/bot.py index 8264651e7..c6ec61731 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# pylint: disable=E0611,E0213,E1102,C0103,E1101,W0613,R0913,R0904 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015 Leandro Toledo de Souza @@ -16,6 +17,7 @@ # 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 a object that represents a Telegram Bot""" import json try: @@ -32,11 +34,27 @@ import logging from telegram import (User, Message, Update, UserProfilePhotos, TelegramError, ReplyMarkup, InputFile, TelegramObject, NullHandler) -h = NullHandler() -logging.getLogger(__name__).addHandler(h) +H = NullHandler() +logging.getLogger(__name__).addHandler(H) class Bot(TelegramObject): + """This object represents a Telegram Bot. + + Attributes: + id (int): + first_name (str): + last_name (str): + username (str): + name (str): + + Args: + token (str): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + base_url (Optional[str]): + """ def __init__(self, token, @@ -50,11 +68,17 @@ class Bot(TelegramObject): self.bot = None - self.log = logging.getLogger(__name__) + self.logger = logging.getLogger(__name__) def info(func): + """ + Returns: + """ @functools.wraps(func) def decorator(self, *args, **kwargs): + """ + decorator + """ if not self.bot: self.getMe() @@ -65,36 +89,48 @@ class Bot(TelegramObject): @property @info def id(self): + """int: """ return self.bot.id @property @info def first_name(self): + """str: """ return self.bot.first_name @property @info def last_name(self): + """str: """ return self.bot.last_name @property @info def username(self): + """str: """ return self.bot.username @property def name(self): + """str: """ return '@%s' % self.username def log(func): + """ + Returns: + A telegram.Message instance representing the message posted. + """ logger = logging.getLogger(func.__module__) @functools.wraps(func) def decorator(self, *args, **kwargs): - logger.debug('Entering: %s' % func.__name__) + """ + decorator + """ + logger.debug('Entering: %s', func.__name__) result = func(self, *args, **kwargs) logger.debug(result) - logger.debug('Exiting: %s' % func.__name__) + logger.debug('Exiting: %s', func.__name__) return result return decorator @@ -105,8 +141,14 @@ class Bot(TelegramObject): """ @functools.wraps(func) def decorator(self, *args, **kwargs): + """ + decorator + """ url, data = func(self, *args, **kwargs) + if not kwargs.get('chat_id'): + raise TelegramError('Invalid chat_id.') + if kwargs.get('reply_to_message_id'): reply_to_message_id = kwargs.get('reply_to_message_id') data['reply_to_message_id'] = reply_to_message_id @@ -118,8 +160,8 @@ class Bot(TelegramObject): else: data['reply_markup'] = reply_markup - json_data = self._requestUrl(url, 'POST', data=data) - data = self._parseAndCheckTelegram(json_data) + json_data = Bot._requestUrl(url, 'POST', data=data) + data = Bot._parseAndCheckTelegram(json_data) if data is True: return data @@ -150,8 +192,7 @@ class Bot(TelegramObject): chat_id, text, disable_web_page_preview=None, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send text messages. Args: @@ -222,8 +263,7 @@ class Bot(TelegramObject): chat_id, photo, caption=None, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send photos. Args: @@ -265,8 +305,7 @@ class Bot(TelegramObject): duration=None, performer=None, title=None, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send audio files, if you want Telegram clients to display them in the music player. Your audio must be in an .mp3 format. On success, the sent Message is returned. Bots can currently send audio @@ -321,8 +360,7 @@ class Bot(TelegramObject): def sendDocument(self, chat_id, document, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send general files. Args: @@ -355,8 +393,7 @@ class Bot(TelegramObject): def sendSticker(self, chat_id, sticker, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send .webp stickers. Args: @@ -391,8 +428,7 @@ class Bot(TelegramObject): video, duration=None, caption=None, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as telegram.Document). @@ -437,8 +473,7 @@ class Bot(TelegramObject): chat_id, voice, duration=None, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send audio files, if you want Telegram clients to display the file as a playable voice message. For this to work, your audio must be in an .ogg file encoded with OPUS (other formats may be @@ -482,8 +517,7 @@ class Bot(TelegramObject): chat_id, latitude, longitude, - reply_to_message_id=None, - reply_markup=None): + **kwargs): """Use this method to send point on the map. Args: @@ -617,16 +651,17 @@ class Bot(TelegramObject): data = self._parseAndCheckTelegram(json_data) if data: - self.log.info( - 'Getting updates: %s' % [u['update_id'] for u in data]) + self.logger.info( + 'Getting updates: %s', [u['update_id'] for u in data]) else: - self.log.info('No new updates found.') + self.logger.info('No new updates found.') return [Update.de_json(x) for x in data] @log def setWebhook(self, - webhook_url): + webhook_url=None, + certificate=None): """Use this method to specify a url and receive incoming updates via an outgoing webhook. Whenever there is an update for the bot, we will send an HTTPS POST request to the specified url, containing a @@ -643,15 +678,19 @@ class Bot(TelegramObject): """ url = '%s/setWebhook' % self.base_url - data = {'url': webhook_url} + data = {} + if webhook_url: + data['url'] = webhook_url + if certificate: + data['certificate'] = certificate json_data = self._requestUrl(url, 'POST', data=data) data = self._parseAndCheckTelegram(json_data) - return True + return data - def _requestUrl(self, - url, + @staticmethod + def _requestUrl(url, method, data=None): """Request an URL. @@ -688,12 +727,12 @@ class Bot(TelegramObject): url, urlencode(data).encode() ).read() - except IOError as e: - raise TelegramError(str(e)) except HTTPError as e: raise TelegramError(str(e)) except URLError as e: raise TelegramError(str(e)) + except IOError as e: + raise TelegramError(str(e)) if method == 'GET': try: @@ -701,8 +740,8 @@ class Bot(TelegramObject): except URLError as e: raise TelegramError(str(e)) - def _parseAndCheckTelegram(self, - json_data): + @staticmethod + def _parseAndCheckTelegram(json_data): """Try and parse the JSON returned from Telegram and return an empty dictionary if there is any error. @@ -725,7 +764,15 @@ class Bot(TelegramObject): return data['result'] + @staticmethod + def de_json(data): + pass + def to_dict(self): + """ + Returns: + dict: + """ data = {'id': self.id, 'username': self.username, 'first_name': self.username} diff --git a/telegram/chataction.py b/telegram/chataction.py index ba7293e4c..fce7e4189 100644 --- a/telegram/chataction.py +++ b/telegram/chataction.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# pylint: disable=R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015 Leandro Toledo de Souza @@ -16,8 +17,12 @@ # 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 a object that represents a Telegram ChatAction""" + class ChatAction(object): + """This object represents a Telegram ChatAction.""" + TYPING = 'typing' UPLOAD_PHOTO = 'upload_photo' RECORD_VIDEO = 'record_video' diff --git a/telegram/command_handler.py b/telegram/command_handler.py new file mode 100644 index 000000000..86de9f9d9 --- /dev/null +++ b/telegram/command_handler.py @@ -0,0 +1,174 @@ +from inspect import getmembers, ismethod +import threading +import logging +import telegram +import time +logger = logging.getLogger(__name__) +__all__ = ['CommandHandler', 'CommandHandlerWithHelp'] +class CommandHandler(object): + """ This handles incomming commands and gives an easy way to create commands. + + How to use this: + create a new class which inherits this class or CommandHandlerWithHelp. + define new methods that start with 'command_' and then the command_name. + run run() + """ + def __init__(self, bot): + self.bot = bot # a telegram bot + self.isValidCommand = None # a function that returns a boolean and takes one agrument an update. if False is returned the the comaand is not executed. + + def _get_command_func(self, command): + if command[0] == '/': + command = command[1:] + if hasattr(self, 'command_' + command): + return self.__getattribute__('command_' + command) # a function + else: + return None + + def run(self, make_thread=True, last_update_id=None, thread_timeout=2, sleep=0.2): + """Continuously check for commands and run the according method + + Args: + make_thread: + if True make a thread for each command it found. + if False make run the code linearly + last_update: + the offset arg from getUpdates and is kept up to date within this function + thread_timeout: + The timeout on a thread. If a thread is alive after this period then try to join the thread in + the next loop. + """ + old_threads = [] + while True: + time.sleep(sleep) + threads, last_update_id = self.run_once(make_thread=make_thread, last_update_id=last_update_id) + for t in threads: + t.start() + for t in old_threads: + threads.append(t) + old_threads = [] + for t in threads: + t.join(timeout=thread_timeout) + if t.isAlive(): + old_threads.append(t) + + def run_once(self, make_thread=True, last_update_id=None): + """ Check the the messages for commands and make a Thread with the command or run the command depending on make_thread. + + Args: + make_thread: + True: the function returns a list with threads. Which didn't start yet. + False: the function just runs the command it found and returns an empty list. + last_update_id: + the offset arg from getUpdates and is kept up to date within this function + + Returns: + A tuple of two elements. The first element is a list with threads which didn't start yet or an empty list if + make_threads==False. The second element is the updated las_update_id + """ + bot_name = self.bot.getMe().username + threads = [] + try: + updates = self.bot.getUpdates(offset=last_update_id) + except: + updates = [] + for update in updates: + last_update_id = update.update_id + 1 + message = update.message + if message.text[0] == '/': + command, username = message.text.split(' ')[0], bot_name + if '@' in command: + command, username = command.split('@') + if username == bot_name: + command_func = self._get_command_func(command) + if command_func is not None: + self.bot.sendChatAction(update.message.chat.id,telegram.ChatAction.TYPING) + if self.isValidCommand is None or self.isValidCommand(update): + if make_thread: + t = threading.Thread(target=command_func, args=(update,)) + threads.append(t) + else: + command_func(update) + else: + self._command_not_found(update) # TODO this must be another function. + else: + if make_thread: + t = threading.Thread(target=self._command_not_found, args=(update,)) + threads.append(t) + else: + self._command_not_valid(update) + return threads, last_update_id + + def _command_not_valid(self, update): + """Inform the telegram user that the command was not found. + + Override this method if you want to do it another way then by sending the the text: + Sorry, I didn't understand the command: /command[@bot]. + """ + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = "Sorry, the command was not authorised or valid: {command}.".format(command=update.message.text.split(' ')[0]) + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + + def _command_not_found(self, update): + """Inform the telegram user that the command was not found. + + Override this method if you want to do it another way then by sending the the text: + Sorry, I didn't understand the command: /command[@bot]. + """ + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = "Sorry, I didn't understand the command: {command}.".format(command=update.message.text.split(' ')[0]) + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + + +class CommandHandlerWithHelp(CommandHandler): + """ This CommandHandler has a builtin /help. It grabs the text from the docstrings of command_ functions.""" + def __init__(self, bot): + super(CommandHandlerWithHelp, self).__init__(bot) + self._help_title = 'Welcome to {name}.'.format(name=self.bot.getMe().username) # the title of help + self._help_before_list = '' # text with information about the bot + self._help_after_list = '' # a footer + self._help_list_title = 'These are the commands:' # the title of the list + self.is_reply = True + self.command_start = self.command_help + + def _generate_help(self): + """ Generate a string which can be send as a help file. + + This function generates a help file from all the docstrings from the commands. + so docstrings of methods that start with command_ should explain what a command does and how a to use the + command to the telegram user. + """ + + command_functions = [attr[1] for attr in getmembers(self, predicate=ismethod) if attr[0][:8] == 'command_'] + help_message = self._help_title + '\n\n' + help_message += self._help_before_list + '\n\n' + help_message += self._help_list_title + '\n' + for command_function in command_functions: + if command_function.__doc__ is not None: + help_message += ' /' + command_function.__name__[8:] + ' - ' + command_function.__doc__ + '\n' + else: + help_message += ' /' + command_function.__name__[8:] + ' - ' + '\n' + help_message += '\n' + help_message += self._help_after_list + return help_message + + def _command_not_found(self, update): + """Inform the telegram user that the command was not found.""" + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = 'Sorry, I did not understand the command: {command}. Please see /help for all available commands' + if self.is_reply: + self.bot.sendMessage(chat_id, message.format(command=update.message.text.split(' ')[0]), + reply_to_message_id=reply_to) + else: + self.bot.sendMessage(chat_id, message.format(command=update.message.text.split(' ')[0])) + + def command_help(self, update): + """ The help file. """ + chat_id = update.message.chat.id + reply_to = update.message.message_id + message = self._generate_help() + self.bot.sendMessage(chat_id, message, reply_to_message_id=reply_to) + diff --git a/telegram/contact.py b/telegram/contact.py index 66a5e0954..e26d755c3 100644 --- a/telegram/contact.py +++ b/telegram/contact.py @@ -16,33 +16,51 @@ # 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 a object that represents a Telegram Contact""" from telegram import TelegramObject class Contact(TelegramObject): + """This object represents a Telegram Contact. + + Attributes: + phone_number (str): + first_name (str): + last_name (str): + user_id (int): + + Args: + phone_number (str): + first_name (str): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + last_name (Optional[str]): + user_id (Optional[int]): + """ + def __init__(self, phone_number, first_name, - last_name=None, - user_id=None): + **kwargs): + # Required self.phone_number = phone_number self.first_name = first_name - self.last_name = last_name - self.user_id = user_id + # Optionals + self.last_name = kwargs.get('last_name', '') + self.user_id = int(kwargs.get('user_id', 0)) @staticmethod def de_json(data): - return Contact(phone_number=data.get('phone_number', None), - first_name=data.get('first_name', None), - last_name=data.get('last_name', None), - user_id=data.get('user_id', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'phone_number': self.phone_number, - 'first_name': self.first_name} - if self.last_name: - data['last_name'] = self.last_name - if self.user_id: - data['user_id'] = self.user_id - return data + Returns: + telegram.Contact: + """ + if not data: + return None + + return Contact(**data) diff --git a/telegram/document.py b/telegram/document.py index 23921c799..1fb21a92c 100644 --- a/telegram/document.py +++ b/telegram/document.py @@ -16,45 +16,53 @@ # 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 a object that represents a Telegram Document""" -from telegram import TelegramObject +from telegram import PhotoSize, TelegramObject class Document(TelegramObject): + """This object represents a Telegram Document. + + Attributes: + file_id (str): + thumb (:class:`telegram.PhotoSize`): + file_name (str): + mime_type (str): + file_size (int): + + Args: + file_id (str): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + thumb (Optional[:class:`telegram.PhotoSize`]): + file_name (Optional[str]): + mime_type (Optional[str]): + file_size (Optional[int]): + """ + def __init__(self, file_id, - thumb=None, - file_name=None, - mime_type=None, - file_size=None): + **kwargs): + # Required self.file_id = file_id - self.thumb = thumb - self.file_name = file_name - self.mime_type = mime_type - self.file_size = file_size + # Optionals + self.thumb = kwargs.get('thumb') + self.file_name = kwargs.get('file_name', '') + self.mime_type = kwargs.get('mime_type', '') + self.file_size = int(kwargs.get('file_size', 0)) @staticmethod def de_json(data): - if 'thumb' in data: - from telegram import PhotoSize - thumb = PhotoSize.de_json(data['thumb']) - else: - thumb = None + """ + Args: + data (str): - return Document(file_id=data.get('file_id', None), - thumb=thumb, - file_name=data.get('file_name', None), - mime_type=data.get('mime_type', None), - file_size=data.get('file_size', None)) + Returns: + telegram.Document: + """ + if not data: + return None - def to_dict(self): - data = {'file_id': self.file_id} - if self.thumb: - data['thumb'] = self.thumb.to_dict() - if self.file_name: - data['file_name'] = self.file_name - if self.mime_type: - data['mime_type'] = self.mime_type - if self.file_size: - data['file_size'] = self.file_size - return data + return Document(**data) diff --git a/telegram/emoji.py b/telegram/emoji.py index 1f1fa47a4..ee10651a5 100644 --- a/telegram/emoji.py +++ b/telegram/emoji.py @@ -1,5 +1,6 @@ #!/usr/bin/env python # flake8: noqa +# pylint: disable=C0103,C0301,R0903 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015 Leandro Toledo de Souza @@ -17,8 +18,12 @@ # 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 a object that represents an Emoji""" + class Emoji(object): + """This object represents an Emoji.""" + GRINNING_FACE_WITH_SMILING_EYES = b'\xF0\x9F\x98\x81' FACE_WITH_TEARS_OF_JOY = b'\xF0\x9F\x98\x82' SMILING_FACE_WITH_OPEN_MOUTH = b'\xF0\x9F\x98\x83' @@ -155,16 +160,26 @@ class Emoji(object): SQUARED_SOS = b'\xF0\x9F\x86\x98' SQUARED_UP_WITH_EXCLAMATION_MARK = b'\xF0\x9F\x86\x99' SQUARED_VS = b'\xF0\x9F\x86\x9A' - REGIONAL_INDICATOR_SYMBOL_LETTER_D_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_E = b'\xF0\x9F\x87\xA9\xF0\x9F\x87\xAA' - REGIONAL_INDICATOR_SYMBOL_LETTER_G_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_B = b'\xF0\x9F\x87\xAC\xF0\x9F\x87\xA7' - REGIONAL_INDICATOR_SYMBOL_LETTER_C_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_N = b'\xF0\x9F\x87\xA8\xF0\x9F\x87\xB3' - REGIONAL_INDICATOR_SYMBOL_LETTER_J_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_P = b'\xF0\x9F\x87\xAF\xF0\x9F\x87\xB5' - REGIONAL_INDICATOR_SYMBOL_LETTER_K_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_R = b'\xF0\x9F\x87\xB0\xF0\x9F\x87\xB7' - REGIONAL_INDICATOR_SYMBOL_LETTER_F_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_R = b'\xF0\x9F\x87\xAB\xF0\x9F\x87\xB7' - REGIONAL_INDICATOR_SYMBOL_LETTER_E_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_S = b'\xF0\x9F\x87\xAA\xF0\x9F\x87\xB8' - REGIONAL_INDICATOR_SYMBOL_LETTER_I_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_T = b'\xF0\x9F\x87\xAE\xF0\x9F\x87\xB9' - REGIONAL_INDICATOR_SYMBOL_LETTER_U_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_S = b'\xF0\x9F\x87\xBA\xF0\x9F\x87\xB8' - REGIONAL_INDICATOR_SYMBOL_LETTER_R_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_U = b'\xF0\x9F\x87\xB7\xF0\x9F\x87\xBA' + REGIONAL_INDICATOR_SYMBOL_LETTER_D_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_E\ + = b'\xF0\x9F\x87\xA9\xF0\x9F\x87\xAA' + REGIONAL_INDICATOR_SYMBOL_LETTER_G_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_B\ + = b'\xF0\x9F\x87\xAC\xF0\x9F\x87\xA7' + REGIONAL_INDICATOR_SYMBOL_LETTER_C_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_N\ + = b'\xF0\x9F\x87\xA8\xF0\x9F\x87\xB3' + REGIONAL_INDICATOR_SYMBOL_LETTER_J_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_P\ + = b'\xF0\x9F\x87\xAF\xF0\x9F\x87\xB5' + REGIONAL_INDICATOR_SYMBOL_LETTER_K_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_R\ + = b'\xF0\x9F\x87\xB0\xF0\x9F\x87\xB7' + REGIONAL_INDICATOR_SYMBOL_LETTER_F_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_R\ + = b'\xF0\x9F\x87\xAB\xF0\x9F\x87\xB7' + REGIONAL_INDICATOR_SYMBOL_LETTER_E_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_S\ + = b'\xF0\x9F\x87\xAA\xF0\x9F\x87\xB8' + REGIONAL_INDICATOR_SYMBOL_LETTER_I_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_T\ + = b'\xF0\x9F\x87\xAE\xF0\x9F\x87\xB9' + REGIONAL_INDICATOR_SYMBOL_LETTER_U_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_S\ + = b'\xF0\x9F\x87\xBA\xF0\x9F\x87\xB8' + REGIONAL_INDICATOR_SYMBOL_LETTER_R_PLUS_REGIONAL_INDICATOR_SYMBOL_LETTER_U\ + = b'\xF0\x9F\x87\xB7\xF0\x9F\x87\xBA' SQUARED_KATAKANA_KOKO = b'\xF0\x9F\x88\x81' SQUARED_KATAKANA_SA = b'\xF0\x9F\x88\x82' SQUARED_CJK_UNIFIED_IDEOGRAPH_7121 = b'\xF0\x9F\x88\x9A' diff --git a/telegram/enchancedbot.py b/telegram/enchancedbot.py new file mode 100644 index 000000000..79606ca2c --- /dev/null +++ b/telegram/enchancedbot.py @@ -0,0 +1,83 @@ +import telegram + + +class NoSuchCommandException(BaseException): + pass + +class CommandDispatcher: + def __init__(self,): + self.commands = list() + self.default = None + + def addCommand(self, command, callback): + self.commands.append((command, callback)) + + def setDefault(self, callback): + self.default = callback + + def dispatch(self, update): + if hasattr(update.message, 'text'): + text = update.message.text + else: + text = '' + + user_id = update.message.from_user.id + com = text.split('@')[0] + for command, callback in self.commands: + if com == command: + return callback(command, user_id) + if self.default is not None: + return self.default(text, user_id) + else: + raise NoSuchCommandException() + + +class EnhancedBot(telegram.Bot): + """The Bot class with command dispatcher added. + + >>> bot = EnhancedBot(token=TOKEN) + >>> @bot.command('/start') + ... def start(command, user_id): + ... # should return a tuple: (text, reply_id, custom_keyboard) + ... return ("Hello, there! Your id is {}".format(user_id), None, None) + >>> while True: + ... bot.processUpdates() + ... time.sleep(3) + """ + def __init__(self, token): + self.dispatcher = CommandDispatcher() + telegram.Bot.__init__(self, token=token) + self.offset = 0 #id of the last processed update + + def command(self, *names, default=False): + """Decorator for adding callbacks for commands.""" + + def inner_command(callback): + for name in names: + self.dispatcher.addCommand(name, callback) + if default: + self.dispatcher.setDefault(callback) + return callback # doesn't touch the callback, so we can use it + return inner_command + + def processUpdates(self): + updates = self.getUpdates(offset=self.offset) + + for update in updates: + print('processing update: {}'.format(str(update.to_dict()))) + self.offset = update.update_id + 1 + if not hasattr(update, 'message'): + continue + + try: + answer, reply_to, reply_markup = self.dispatcher.dispatch(update) + except Exception as e: + print('error occured') # TODO logging + print(update.to_dict()) + raise e + + if answer is not None: + self.sendMessage(chat_id=update.message.chat_id, + text=answer, + reply_to_message_id=reply_to, + reply_markup=reply_markup) diff --git a/telegram/error.py b/telegram/error.py index 68754a04e..b19b3f905 100644 --- a/telegram/error.py +++ b/telegram/error.py @@ -16,11 +16,16 @@ # 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 a object that represents a Telegram Error""" + class TelegramError(Exception): - """Base class for Telegram errors.""" + """This object represents a Telegram Error.""" @property def message(self): - '''Returns the first argument used to construct this error.''' + """ + Returns: + str: + """ return self.args[0] diff --git a/telegram/forcereply.py b/telegram/forcereply.py index 8e163d6b3..b28e8f58c 100644 --- a/telegram/forcereply.py +++ b/telegram/forcereply.py @@ -16,24 +16,45 @@ # 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 a object that represents a Telegram ForceReply""" from telegram import ReplyMarkup class ForceReply(ReplyMarkup): + """This object represents a Telegram ForceReply. + + Attributes: + force_reply (bool): + selective (bool): + + Args: + force_reply (bool): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + selective (Optional[bool]): + """ + def __init__(self, force_reply=True, - selective=None): - self.force_reply = force_reply - self.selective = selective + **kwargs): + # Required + self.force_reply = bool(force_reply) + # Optionals + self.selective = bool(kwargs.get('selective', False)) @staticmethod def de_json(data): - return ForceReply(force_reply=data.get('force_reply', None), - selective=data.get('selective', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'force_reply': self.force_reply} - if self.selective: - data['selective'] = self.selective - return data + Returns: + telegram.ForceReply: + """ + if not data: + return None + + return ForceReply(**data) + diff --git a/telegram/groupchat.py b/telegram/groupchat.py index 049d16629..1b3fac8df 100644 --- a/telegram/groupchat.py +++ b/telegram/groupchat.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# pylint: disable=C0103,W0622 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015 Leandro Toledo de Souza @@ -16,23 +17,40 @@ # 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 a object that represents a Telegram GroupChat""" from telegram import TelegramObject class GroupChat(TelegramObject): + """This object represents a Telegram GroupChat. + + Attributes: + id (int): + title (str): + + Args: + id (int): + title (str): + """ + def __init__(self, id, title): - self.id = id + # Required + self.id = int(id) self.title = title @staticmethod def de_json(data): - return GroupChat(id=data.get('id', None), - title=data.get('title', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'id': self.id, - 'title': self.title} - return data + Returns: + telegram.GroupChat: + """ + if not data: + return None + + return GroupChat(**data) diff --git a/telegram/inputfile.py b/telegram/inputfile.py index 5d2d00fba..e0f234f62 100644 --- a/telegram/inputfile.py +++ b/telegram/inputfile.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# pylint: disable=W0622,E0611 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015 Leandro Toledo de Souza @@ -16,6 +17,7 @@ # 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 a object that represents a Telegram InputFile""" try: from email.generator import _make_boundary as choose_boundary @@ -29,7 +31,7 @@ import os import sys import imghdr -from .error import TelegramError +from telegram import TelegramError DEFAULT_MIME_TYPE = 'application/octet-stream' USER_AGENT = 'Python Telegram Bot' \ @@ -37,6 +39,8 @@ USER_AGENT = 'Python Telegram Bot' \ class InputFile(object): + """This object represents a Telegram InputFile.""" + def __init__(self, data): self.data = data @@ -57,6 +61,9 @@ class InputFile(object): if 'voice' in data: self.input_name = 'voice' self.input_file = data.pop('voice') + if 'certificate' in data: + self.input_name = 'certificate' + self.input_file = data.pop('certificate') if isinstance(self.input_file, file): self.input_file_content = self.input_file.read() @@ -71,14 +78,26 @@ class InputFile(object): @property def headers(self): + """ + Returns: + str: + """ return {'User-agent': USER_AGENT, 'Content-type': self.content_type} @property def content_type(self): + """ + Returns: + str: + """ return 'multipart/form-data; boundary=%s' % self.boundary def to_form(self): + """ + Returns: + str: + """ form = [] form_boundary = '--' + self.boundary @@ -105,9 +124,14 @@ class InputFile(object): form.append('--' + self.boundary + '--') form.append('') - return self._parse(form) + return InputFile._parse(form) - def _parse(self, form): + @staticmethod + def _parse(form): + """ + Returns: + str: + """ if sys.version_info > (3,): # on Python 3 form needs to be byte encoded encoded_form = [] @@ -125,11 +149,10 @@ class InputFile(object): """Check if the content file is an image by analyzing its headers. Args: - stream: - A str representing the content of a file. + stream (str): A str representing the content of a file. Returns: - The str mimetype of an image. + str: The str mimetype of an image. """ image = imghdr.what(None, stream) if image: @@ -142,8 +165,7 @@ class InputFile(object): """Check if the request is a file request. Args: - data: - A dict of (str, unicode) key/value pairs + data (str): A dict of (str, unicode) key/value pairs Returns: bool diff --git a/telegram/location.py b/telegram/location.py index 643c135d1..66c28fc59 100644 --- a/telegram/location.py +++ b/telegram/location.py @@ -16,23 +16,40 @@ # 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 a object that represents a Telegram Location""" from telegram import TelegramObject class Location(TelegramObject): + """This object represents a Telegram Sticker. + + Attributes: + longitude (float): + latitude (float): + + Args: + longitude (float): + latitude (float): + """ + def __init__(self, longitude, latitude): - self.longitude = longitude - self.latitude = latitude + # Required + self.longitude = float(longitude) + self.latitude = float(latitude) @staticmethod def de_json(data): - return Location(longitude=data.get('longitude', None), - latitude=data.get('latitude', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'longitude': self.longitude, - 'latitude': self.latitude} - return data + Returns: + telegram.Location: + """ + if not data: + return None + + return Location(**data) diff --git a/telegram/message.py b/telegram/message.py index 770ec0acf..1caf61dc7 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# pylint: disable=R0902,R0912,R0913 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015 Leandro Toledo de Souza @@ -16,251 +17,209 @@ # 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 a object that represents a Telegram Message""" -from telegram import TelegramObject from datetime import datetime from time import mktime +from telegram import (Audio, Contact, Document, GroupChat, Location, PhotoSize, + Sticker, TelegramObject, User, Video, Voice) + class Message(TelegramObject): + """This object represents a Telegram Message. + + Note: + * In Python `from` is a reserved word, use `from_user` instead. + + Attributes: + message_id (int): + from_user (:class:`telegram.User`): + date (:class:`datetime.datetime`): + forward_from (:class:`telegram.User`): + forward_date (:class:`datetime.datetime`): + reply_to_message (:class:`telegram.Message`): + text (str): + audio (:class:`telegram.Audio`): + document (:class:`telegram.Document`): + photo (List[:class:`telegram.PhotoSize`]): + sticker (:class:`telegram.Sticker`): + video (:class:`telegram.Video`): + voice (:class:`telegram.Voice`): + caption (str): + contact (:class:`telegram.Contact`): + location (:class:`telegram.Location`): + new_chat_participant (:class:`telegram.User`): + left_chat_participant (:class:`telegram.User`): + new_chat_title (str): + new_chat_photo (List[:class:`telegram.PhotoSize`]): + delete_chat_photo (bool): + group_chat_created (bool): + + Args: + message_id (int): + from_user (:class:`telegram.User`): + date (:class:`datetime.datetime`): + chat (:class:`telegram.User` or :class:`telegram.GroupChat`): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + forward_from (Optional[:class:`telegram.User`]): + forward_date (Optional[:class:`datetime.datetime`]): + reply_to_message (Optional[:class:`telegram.Message`]): + text (Optional[str]): + audio (Optional[:class:`telegram.Audio`]): + document (Optional[:class:`telegram.Document`]): + photo (Optional[List[:class:`telegram.PhotoSize`]]): + sticker (Optional[:class:`telegram.Sticker`]): + video (Optional[:class:`telegram.Video`]): + voice (Optional[:class:`telegram.Voice`]): + caption (Optional[str]): + contact (Optional[:class:`telegram.Contact`]): + location (Optional[:class:`telegram.Location`]): + new_chat_participant (Optional[:class:`telegram.User`]): + left_chat_participant (Optional[:class:`telegram.User`]): + new_chat_title (Optional[str]): + new_chat_photo (Optional[List[:class:`telegram.PhotoSize`]): + delete_chat_photo (Optional[bool]): + group_chat_created (Optional[bool]): + """ + def __init__(self, message_id, from_user, date, chat, - forward_from=None, - forward_date=None, - reply_to_message=None, - text=None, - audio=None, - document=None, - photo=None, - sticker=None, - video=None, - voice=None, - caption=None, - contact=None, - location=None, - new_chat_participant=None, - left_chat_participant=None, - new_chat_title=None, - new_chat_photo=None, - delete_chat_photo=None, - group_chat_created=None): - self.message_id = message_id + **kwargs): + # Required + self.message_id = int(message_id) self.from_user = from_user self.date = date self.chat = chat - self.forward_from = forward_from - self.forward_date = forward_date - self.reply_to_message = reply_to_message - self.text = text - self.audio = audio - self.document = document - self.photo = photo - self.sticker = sticker - self.video = video - self.voice = voice - self.caption = caption - self.contact = contact - self.location = location - self.new_chat_participant = new_chat_participant - self.left_chat_participant = left_chat_participant - self.new_chat_title = new_chat_title - self.new_chat_photo = new_chat_photo - self.delete_chat_photo = delete_chat_photo - self.group_chat_created = group_chat_created + # Optionals + self.forward_from = kwargs.get('forward_from') + self.forward_date = kwargs.get('forward_date') + self.reply_to_message = kwargs.get('reply_to_message') + self.text = kwargs.get('text', '') + self.audio = kwargs.get('audio') + self.document = kwargs.get('document') + self.photo = kwargs.get('photo') + self.sticker = kwargs.get('sticker') + self.video = kwargs.get('video') + self.voice = kwargs.get('voice') + self.caption = kwargs.get('caption', '') + self.contact = kwargs.get('contact') + self.location = kwargs.get('location') + self.new_chat_participant = kwargs.get('new_chat_participant') + self.left_chat_participant = kwargs.get('left_chat_participant') + self.new_chat_title = kwargs.get('new_chat_title', '') + self.new_chat_photo = kwargs.get('new_chat_photo') + self.delete_chat_photo = bool(kwargs.get('delete_chat_photo', False)) + self.group_chat_created = bool(kwargs.get('group_chat_created', False)) @property def chat_id(self): + """int: Short for :attr:`Message.chat.id`""" return self.chat.id @staticmethod def de_json(data): - if 'from' in data: # from is a reserved word, use from_user instead. - from telegram import User - from_user = User.de_json(data['from']) - else: - from_user = None + """ + Args: + data (str): - if 'date' in data: - date = datetime.fromtimestamp(data['date']) - else: - date = None + Returns: + telegram.Message: + """ + if not data: + return None - if 'chat' in data: - if 'first_name' in data['chat']: - from telegram import User - chat = User.de_json(data['chat']) - if 'title' in data['chat']: - from telegram import GroupChat - chat = GroupChat.de_json(data['chat']) - else: - chat = None + data['from_user'] = User.de_json(data['from']) + data['date'] = datetime.fromtimestamp(data['date']) + if 'first_name' in data.get('chat', ''): + data['chat'] = User.de_json(data.get('chat')) + elif 'title' in data.get('chat', ''): + data['chat'] = GroupChat.de_json(data.get('chat')) + data['forward_from'] = \ + User.de_json(data.get('forward_from')) + data['forward_date'] = \ + Message._fromtimestamp(data.get('forward_date')) + data['reply_to_message'] = \ + Message.de_json(data.get('reply_to_message')) + data['audio'] = \ + Audio.de_json(data.get('audio')) + data['document'] = \ + Document.de_json(data.get('document')) + data['photo'] = \ + PhotoSize.de_list(data.get('photo')) + data['sticker'] = \ + Sticker.de_json(data.get('sticker')) + data['video'] = \ + Video.de_json(data.get('video')) + data['voice'] = \ + Voice.de_json(data.get('voice')) + data['contact'] = \ + Contact.de_json(data.get('contact')) + data['location'] = \ + Location.de_json(data.get('location')) + data['new_chat_participant'] = \ + User.de_json(data.get('new_chat_participant')) + data['left_chat_participant'] = \ + User.de_json(data.get('left_chat_participant')) + data['new_chat_photo'] = \ + PhotoSize.de_list(data.get('new_chat_photo')) - if 'forward_from' in data: - from telegram import User - forward_from = User.de_json(data['forward_from']) - else: - forward_from = None - - if 'forward_date' in data: - forward_date = datetime.fromtimestamp(data['forward_date']) - else: - forward_date = None - - if 'reply_to_message' in data: - reply_to_message = Message.de_json(data['reply_to_message']) - else: - reply_to_message = None - - if 'audio' in data: - from telegram import Audio - audio = Audio.de_json(data['audio']) - else: - audio = None - - if 'document' in data: - from telegram import Document - document = Document.de_json(data['document']) - else: - document = None - - if 'photo' in data: - from telegram import PhotoSize - photo = [PhotoSize.de_json(x) for x in data['photo']] - else: - photo = None - - if 'sticker' in data: - from telegram import Sticker - sticker = Sticker.de_json(data['sticker']) - else: - sticker = None - - if 'video' in data: - from telegram import Video - video = Video.de_json(data['video']) - else: - video = None - - if 'voice' in data: - from telegram import Voice - voice = Voice.de_json(data['voice']) - else: - voice = None - - if 'contact' in data: - from telegram import Contact - contact = Contact.de_json(data['contact']) - else: - contact = None - - if 'location' in data: - from telegram import Location - location = Location.de_json(data['location']) - else: - location = None - - if 'new_chat_participant' in data: - from telegram import User - new_chat_participant = User.de_json(data['new_chat_participant']) - else: - new_chat_participant = None - - if 'left_chat_participant' in data: - from telegram import User - left_chat_participant = User.de_json(data['left_chat_participant']) - else: - left_chat_participant = None - - if 'new_chat_photo' in data: - from telegram import PhotoSize - new_chat_photo = \ - [PhotoSize.de_json(x) for x in data['new_chat_photo']] - else: - new_chat_photo = None - - return Message(message_id=data.get('message_id', None), - from_user=from_user, - date=date, - chat=chat, - forward_from=forward_from, - forward_date=forward_date, - reply_to_message=reply_to_message, - text=data.get('text', ''), - audio=audio, - document=document, - photo=photo, - sticker=sticker, - video=video, - voice=voice, - caption=data.get('caption', ''), - contact=contact, - location=location, - new_chat_participant=new_chat_participant, - left_chat_participant=left_chat_participant, - new_chat_title=data.get('new_chat_title', None), - new_chat_photo=new_chat_photo, - delete_chat_photo=data.get('delete_chat_photo', None), - group_chat_created=data.get('group_chat_created', None)) + return Message(**data) def to_dict(self): - data = {'message_id': self.message_id, - 'from': self.from_user.to_dict(), - 'chat': self.chat.to_dict()} - try: - # Python 3.3+ supports .timestamp() - data['date'] = int(self.date.timestamp()) + """ + Returns: + dict: + """ + data = super(Message, self).to_dict() - if self.forward_date: - data['forward_date'] = int(self.forward_date.timestamp()) - except AttributeError: - # _totimestamp() for Python 3 (< 3.3) and Python 2 - data['date'] = self._totimestamp(self.date) - - if self.forward_date: - data['forward_date'] = self._totimestamp(self.forward_date) - - if self.forward_from: - data['forward_from'] = self.forward_from.to_dict() - if self.reply_to_message: - data['reply_to_message'] = self.reply_to_message.to_dict() - if self.text: - data['text'] = self.text - if self.audio: - data['audio'] = self.audio.to_dict() - if self.document: - data['document'] = self.document.to_dict() + # Required + data['from'] = data.pop('from_user') + data['date'] = self._totimestamp(self.date) + # Optionals + if self.forward_date: + data['forward_date'] = self._totimestamp(self.forward_date) if self.photo: data['photo'] = [p.to_dict() for p in self.photo] - if self.sticker: - data['sticker'] = self.sticker.to_dict() - if self.video: - data['video'] = self.video.to_dict() - if self.voice: - data['voice'] = self.voice.to_dict() - if self.caption: - data['caption'] = self.caption - if self.contact: - data['contact'] = self.contact.to_dict() - if self.location: - data['location'] = self.location.to_dict() - if self.new_chat_participant: - data['new_chat_participant'] = self.new_chat_participant.to_dict() - if self.left_chat_participant: - data['left_chat_participant'] = \ - self.left_chat_participant.to_dict() - if self.new_chat_title: - data['new_chat_title'] = self.new_chat_title if self.new_chat_photo: data['new_chat_photo'] = [p.to_dict() for p in self.new_chat_photo] - if self.delete_chat_photo: - data['delete_chat_photo'] = self.delete_chat_photo - if self.group_chat_created: - data['group_chat_created'] = self.group_chat_created + return data @staticmethod - def _totimestamp(dt): - return int(mktime(dt.timetuple())) + def _fromtimestamp(unixtime): + """ + Args: + unixtime (int): + + Returns: + datetime.datetime: + """ + if not unixtime: + return None + + return datetime.fromtimestamp(unixtime) + + @staticmethod + def _totimestamp(dt_obj): + """ + Args: + dt_obj (:class:`datetime.datetime`): + + Returns: + int: + """ + if not dt_obj: + return None + + try: + # Python 3.3+ + return int(dt_obj.timestamp()) + except AttributeError: + # Python 3 (< 3.3) and Python 2 + return int(mktime(dt_obj.timetuple())) diff --git a/telegram/nullhandler.py b/telegram/nullhandler.py index a51ccf358..50bd370eb 100644 --- a/telegram/nullhandler.py +++ b/telegram/nullhandler.py @@ -16,10 +16,17 @@ # 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 a object that represents a logging NullHandler""" import logging class NullHandler(logging.Handler): + """This object represents a logging NullHandler.""" + def emit(self, record): + """ + Args: + record (str): + """ pass diff --git a/telegram/photosize.py b/telegram/photosize.py index 54ce4effd..adc47649a 100644 --- a/telegram/photosize.py +++ b/telegram/photosize.py @@ -16,32 +16,70 @@ # 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 a object that represents a Telegram PhotoSize""" from telegram import TelegramObject class PhotoSize(TelegramObject): + """This object represents a Telegram PhotoSize. + + Attributes: + file_id (str): + width (int): + height (int): + file_size (int): + + Args: + file_id (str): + width (int): + height (int): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + file_size (Optional[int]): + """ + def __init__(self, file_id, width, height, - file_size=None): + **kwargs): + # Required self.file_id = file_id - self.width = width - self.height = height - self.file_size = file_size + self.width = int(width) + self.height = int(height) + # Optionals + self.file_size = int(kwargs.get('file_size', 0)) @staticmethod def de_json(data): - return PhotoSize(file_id=data.get('file_id', None), - width=data.get('width', None), - height=data.get('height', None), - file_size=data.get('file_size', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'file_id': self.file_id, - 'width': self.width, - 'height': self.height} - if self.file_size: - data['file_size'] = self.file_size - return data + Returns: + telegram.PhotoSize: + """ + if not data: + return None + + return PhotoSize(**data) + + @staticmethod + def de_list(data): + """ + Args: + data (list): + + Returns: + List: + """ + if not data: + return [] + + photos = list() + for photo in data: + photos.append(PhotoSize.de_json(photo)) + + return photos diff --git a/telegram/replykeyboardhide.py b/telegram/replykeyboardhide.py index 084e6cd0a..f2cb80bb7 100644 --- a/telegram/replykeyboardhide.py +++ b/telegram/replykeyboardhide.py @@ -16,24 +16,45 @@ # 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 a object that represents a Telegram +ReplyKeyboardHide""" from telegram import ReplyMarkup class ReplyKeyboardHide(ReplyMarkup): + """This object represents a Telegram ReplyKeyboardHide. + + Attributes: + hide_keyboard (bool): + selective (bool): + + Args: + hide_keyboard (bool): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + selective (Optional[bool]): + """ + def __init__(self, hide_keyboard=True, - selective=None): - self.hide_keyboard = hide_keyboard - self.selective = selective + **kwargs): + # Required + self.hide_keyboard = bool(hide_keyboard) + # Optionals + self.selective = bool(kwargs.get('selective', False)) @staticmethod def de_json(data): - return ReplyKeyboardHide(hide_keyboard=data.get('hide_keyboard', None), - selective=data.get('selective', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'hide_keyboard': self.hide_keyboard} - if self.selective: - data['selective'] = self.selective - return data + Returns: + telegram.ReplyKeyboardHide: + """ + if not data: + return None + + return ReplyKeyboardHide(**data) diff --git a/telegram/replykeyboardmarkup.py b/telegram/replykeyboardmarkup.py index e9175475d..39912ea39 100644 --- a/telegram/replykeyboardmarkup.py +++ b/telegram/replykeyboardmarkup.py @@ -16,38 +16,51 @@ # 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 a object that represents a Telegram +ReplyKeyboardMarkup""" from telegram import ReplyMarkup class ReplyKeyboardMarkup(ReplyMarkup): + """This object represents a Telegram ReplyKeyboardMarkup. + + Attributes: + keyboard (List[List[str]]): + resize_keyboard (bool): + one_time_keyboard (bool): + selective (bool): + + Args: + keyboard (List[List[str]]): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + resize_keyboard (Optional[bool]): + one_time_keyboard (Optional[bool]): + selective (Optional[bool]): + """ + def __init__(self, keyboard, - resize_keyboard=None, - one_time_keyboard=None, - selective=None): + **kwargs): + # Required self.keyboard = keyboard - self.resize_keyboard = resize_keyboard - self.one_time_keyboard = one_time_keyboard - self.selective = selective + # Optionals + self.resize_keyboard = bool(kwargs.get('resize_keyboard', False)) + self.one_time_keyboard = bool(kwargs.get('one_time_keyboard', False)) + self.selective = bool(kwargs.get('selective', False)) @staticmethod def de_json(data): - return ReplyKeyboardMarkup(keyboard=data.get('keyboard', None), - resize_keyboard=data.get( - 'resize_keyboard', None - ), - one_time_keyboard=data.get( - 'one_time_keyboard', None - ), - selective=data.get('selective', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'keyboard': self.keyboard} - if self.resize_keyboard: - data['resize_keyboard'] = self.resize_keyboard - if self.one_time_keyboard: - data['one_time_keyboard'] = self.one_time_keyboard - if self.selective: - data['selective'] = self.selective - return data + Returns: + telegram.ReplyKeyboardMarkup: + """ + if not data: + return None + + return ReplyKeyboardMarkup(**data) diff --git a/telegram/replymarkup.py b/telegram/replymarkup.py index e86f0b3da..a308e9d5c 100644 --- a/telegram/replymarkup.py +++ b/telegram/replymarkup.py @@ -16,9 +16,17 @@ # You should have received a copy of the GNU Lesser Public License # along with this program. If not, see [http://www.gnu.org/licenses/]. +"""Base class for Telegram ReplyMarkup Objects""" from telegram import TelegramObject class ReplyMarkup(TelegramObject): - pass + """Base class for Telegram ReplyMarkup Objects""" + + @staticmethod + def de_json(data): + pass + + def to_dict(self): + pass diff --git a/telegram/sticker.py b/telegram/sticker.py index eae52d25f..c663a3fed 100644 --- a/telegram/sticker.py +++ b/telegram/sticker.py @@ -16,42 +16,55 @@ # 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 a object that represents a Telegram Sticker""" -from telegram import TelegramObject +from telegram import PhotoSize, TelegramObject class Sticker(TelegramObject): + """This object represents a Telegram Sticker. + + Attributes: + file_id (str): + width (int): + height (int): + thumb (:class:`telegram.PhotoSize`): + file_size (int): + + Args: + file_id (str): + width (int): + height (int): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + thumb (Optional[:class:`telegram.PhotoSize`]): + file_size (Optional[int]): + """ + def __init__(self, file_id, width, height, - thumb=None, - file_size=None): + **kwargs): + # Required self.file_id = file_id - self.width = width - self.height = height - self.thumb = thumb - self.file_size = file_size + self.width = int(width) + self.height = int(height) + # Optionals + self.thumb = kwargs.get('thumb') + self.file_size = int(kwargs.get('file_size', 0)) @staticmethod def de_json(data): - if 'thumb' in data: - from telegram import PhotoSize - thumb = PhotoSize.de_json(data['thumb']) - else: - thumb = None + """ + Args: + data (str): - return Sticker(file_id=data.get('file_id', None), - width=data.get('width', None), - height=data.get('height', None), - thumb=thumb, - file_size=data.get('file_size', None)) + Returns: + telegram.Sticker: + """ + if not data: + return None - def to_dict(self): - data = {'file_id': self.file_id, - 'width': self.width, - 'height': self.height, - 'thumb': self.thumb.to_dict()} - if self.file_size: - data['file_size'] = self.file_size - return data + return Sticker(**data) diff --git a/telegram/update.py b/telegram/update.py index 362f52f64..83e068593 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -16,30 +16,45 @@ # 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 a object that represents a Telegram Update""" -from telegram import TelegramObject +from telegram import Message, TelegramObject class Update(TelegramObject): + """This object represents a Telegram Update. + + Attributes: + update_id (int): + message (:class:`telegram.Message`): + + Args: + update_id (int): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + message (Optional[:class:`telegram.Message`]): + """ def __init__(self, update_id, - message=None): + **kwargs): + # Required self.update_id = update_id - self.message = message + # Optionals + self.message = kwargs.get('message') @staticmethod def de_json(data): - if 'message' in data: - from telegram import Message - message = Message.de_json(data['message']) - else: - message = None + """ + Args: + data (str): - return Update(update_id=data.get('update_id', None), - message=message) + Returns: + telegram.Update: + """ + if not data: + return None - def to_dict(self): - data = {'update_id': self.update_id} - if self.message: - data['message'] = self.message.to_dict() - return data + data['message'] = Message.de_json(data['message']) + + return Update(**data) diff --git a/telegram/user.py b/telegram/user.py index eaff7f2e0..1cdbe8e86 100644 --- a/telegram/user.py +++ b/telegram/user.py @@ -1,4 +1,5 @@ #!/usr/bin/env python +# pylint: disable=C0103,W0622 # # A library that provides a Python interface to the Telegram Bot API # Copyright (C) 2015 Leandro Toledo de Souza @@ -16,23 +17,44 @@ # 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 a object that represents a Telegram User""" from telegram import TelegramObject class User(TelegramObject): + """This object represents a Telegram Sticker. + + Attributes: + id (int): + first_name (str): + last_name (str): + username (str): + + Args: + id (int): + first_name (str): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + last_name (Optional[str]): + username (Optional[str]): + """ + def __init__(self, id, first_name, - last_name=None, - username=None): - self.id = id + **kwargs): + # Required + self.id = int(id) self.first_name = first_name - self.last_name = last_name - self.username = username + # Optionals + self.last_name = kwargs.get('last_name', '') + self.username = kwargs.get('username', '') @property def name(self): + """str: """ if self.username: return '@%s' % self.username if self.last_name: @@ -41,16 +63,14 @@ class User(TelegramObject): @staticmethod def de_json(data): - return User(id=data.get('id', None), - first_name=data.get('first_name', None), - last_name=data.get('last_name', None), - username=data.get('username', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'id': self.id, - 'first_name': self.first_name} - if self.last_name: - data['last_name'] = self.last_name - if self.username: - data['username'] = self.username - return data + Returns: + telegram.User: + """ + if not data: + return None + + return User(**data) diff --git a/telegram/userprofilephotos.py b/telegram/userprofilephotos.py index 6e57b495f..bce623b3b 100644 --- a/telegram/userprofilephotos.py +++ b/telegram/userprofilephotos.py @@ -16,36 +16,56 @@ # 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 a object that represents a Telegram +UserProfilePhotos""" -from telegram import TelegramObject +from telegram import PhotoSize, TelegramObject class UserProfilePhotos(TelegramObject): + """This object represents a Telegram UserProfilePhotos. + + Attributes: + total_count (int): + photos (List[List[:class:`telegram.PhotoSize`]]): + + Args: + total_count (int): + photos (List[List[:class:`telegram.PhotoSize`]]): + """ + def __init__(self, total_count, photos): - self.total_count = total_count + # Required + self.total_count = int(total_count) self.photos = photos @staticmethod def de_json(data): - if 'photos' in data: - from telegram import PhotoSize - photos = [] - for photo in data['photos']: - photos.append([PhotoSize.de_json(x) for x in photo]) - else: - photos = None + """ + Args: + data (str): - return UserProfilePhotos(total_count=data.get('total_count', None), - photos=photos) + Returns: + telegram.UserProfilePhotos: + """ + if not data: + return None + + data['photos'] = [PhotoSize.de_list(photo) for photo in data['photos']] + + return UserProfilePhotos(**data) def to_dict(self): - data = {} - if self.total_count: - data['total_count'] = self.total_count - if self.photos: - data['photos'] = [] - for photo in self.photos: - data['photos'].append([x.to_dict() for x in photo]) + """ + Returns: + dict: + """ + data = super(UserProfilePhotos, self).to_dict() + + data['photos'] = [] + for photo in self.photos: + data['photos'].append([x.to_dict() for x in photo]) + return data diff --git a/tests/__init__.py b/telegram/utils/__init__.py similarity index 100% rename from tests/__init__.py rename to telegram/utils/__init__.py diff --git a/telegram/utils/botan.py b/telegram/utils/botan.py new file mode 100644 index 000000000..ff2b6f82c --- /dev/null +++ b/telegram/utils/botan.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# +# 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 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/]. + +import json +try: + from urllib.parse import urlencode + from urllib.request import urlopen, Request + from urllib.error import HTTPError, URLError +except ImportError: + from urllib import urlencode + from urllib2 import urlopen, Request + from urllib2 import HTTPError, URLError + +DEFAULT_BASE_URL = \ + 'https://api.botan.io/track?token=%(token)&uid=%(uid)&name=%(name)' +USER_AGENT = 'Python Telegram Bot' \ + ' (https://github.com/leandrotoledo/python-telegram-bot)' +CONTENT_TYPE = 'application/json' + +class Botan(Object): + def __init__(self, + token, + base_url=None): + self.token = token + + if base_url is None: + self.base_url = DEFAULT_BASE_URL % {'token': self.token} + else: + self.base_url = base_url % {'token': self.token} + + def track(self, + uid, + text, + name = 'Message'): + + url = self.base_url % {'uid': uid, + 'message': text, + 'name': name} + + self._post(url, message) + + def _post(self, + url, + data): + headers = {'User-agent': USER_AGENT, + 'Content-type': CONTENT_TYPE} + + try: + request = Request( + url, + data=urlencode(data).encode(), + headers=headers + ) + + return urlopen(request).read() + except IOError as e: + raise TelegramError(str(e)) + except HTTPError as e: + raise TelegramError(str(e)) diff --git a/telegram/video.py b/telegram/video.py index a75c4a931..b9414835b 100644 --- a/telegram/video.py +++ b/telegram/video.py @@ -16,52 +16,62 @@ # 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 a object that represents a Telegram Video""" -from telegram import TelegramObject +from telegram import PhotoSize, TelegramObject class Video(TelegramObject): + """This object represents a Telegram Video. + + Attributes: + file_id (str): + width (int): + height (int): + duration (int): + thumb (:class:`telegram.PhotoSize`): + mime_type (str): + file_size (int): + + Args: + file_id (str): + width (int): + height (int): + duration (int): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + thumb (Optional[:class:`telegram.PhotoSize`]): + mime_type (Optional[str]): + file_size (Optional[int]): + """ + def __init__(self, file_id, width, height, duration, - thumb=None, - mime_type=None, - file_size=None): + **kwargs): + # Required self.file_id = file_id - self.width = width - self.height = height - self.duration = duration - self.thumb = thumb - self.mime_type = mime_type - self.file_size = file_size + self.width = int(width) + self.height = int(height) + self.duration = int(duration) + # Optionals + self.thumb = kwargs.get('thumb') + self.mime_type = kwargs.get('mime_type', '') + self.file_size = int(kwargs.get('file_size', 0)) @staticmethod def de_json(data): - if 'thumb' in data: - from telegram import PhotoSize - thumb = PhotoSize.de_json(data['thumb']) - else: - thumb = None + """ + Args: + data (str): - return Video(file_id=data.get('file_id', None), - width=data.get('width', None), - height=data.get('height', None), - duration=data.get('duration', None), - thumb=thumb, - mime_type=data.get('mime_type', None), - file_size=data.get('file_size', None)) + Returns: + telegram.Video: + """ + if not data: + return None - def to_dict(self): - data = {'file_id': self.file_id, - 'width': self.width, - 'height': self.height, - 'duration': self.duration} - if self.thumb: - data['thumb'] = self.thumb.to_dict() - if self.mime_type: - data['mime_type'] = self.mime_type - if self.file_size: - data['file_size'] = self.file_size - return data + return Video(**data) diff --git a/telegram/voice.py b/telegram/voice.py index a628b04e6..96f3fa5f1 100644 --- a/telegram/voice.py +++ b/telegram/voice.py @@ -16,34 +16,50 @@ # 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 a object that represents a Telegram Voice""" from telegram import TelegramObject class Voice(TelegramObject): + """This object represents a Telegram Voice. + + Attributes: + file_id (str): + duration (int): + mime_type (str): + file_size (int): + + Args: + file_id (str): + **kwargs: Arbitrary keyword arguments. + + Keyword Args: + duration (Optional[int]): + mime_type (Optional[str]): + file_size (Optional[int]): + """ + def __init__(self, file_id, - duration=None, - mime_type=None, - file_size=None): + **kwargs): + # Required self.file_id = file_id - self.duration = duration - self.mime_type = mime_type - self.file_size = file_size + # Optionals + self.duration = int(kwargs.get('duration', 0)) + self.mime_type = kwargs.get('mime_type', '') + self.file_size = int(kwargs.get('file_size', 0)) @staticmethod def de_json(data): - return Voice(file_id=data.get('file_id', None), - duration=data.get('duration', None), - mime_type=data.get('mime_type', None), - file_size=data.get('file_size', None)) + """ + Args: + data (str): - def to_dict(self): - data = {'file_id': self.file_id} - if self.duration: - data['duration'] = self.duration - if self.mime_type: - data['mime_type'] = self.mime_type - if self.file_size: - data['file_size'] = self.file_size - return data + Returns: + telegram.Voice: + """ + if not data: + return None + + return Voice(**data) diff --git a/telegram_test.py b/telegram_test.py deleted file mode 100644 index 488b915a7..000000000 --- a/telegram_test.py +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env python -# -# 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 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/]. - - -import logging -import unittest -from tests.test_bot import BotTest - -if __name__ == '__main__': - logging.basicConfig( - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - testsuite = unittest.TestLoader().loadTestsFromTestCase(BotTest) - unittest.TextTestRunner(verbosity=1).run(testsuite) diff --git a/tests/telegram.mp3 b/tests/data/telegram.mp3 similarity index 100% rename from tests/telegram.mp3 rename to tests/data/telegram.mp3 diff --git a/tests/telegram.mp4 b/tests/data/telegram.mp4 similarity index 100% rename from tests/telegram.mp4 rename to tests/data/telegram.mp4 diff --git a/tests/telegram.ogg b/tests/data/telegram.ogg similarity index 100% rename from tests/telegram.ogg rename to tests/data/telegram.ogg diff --git a/tests/telegram.png b/tests/data/telegram.png similarity index 100% rename from tests/telegram.png rename to tests/data/telegram.png diff --git a/tests/test_bot.py b/tests/test_bot.py index b44808c68..707b50ca7 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -19,6 +19,8 @@ import os +import sys +sys.path.append('.') import json import telegram import unittest @@ -36,195 +38,225 @@ class BotTest(unittest.TestCase): def setUp(self): bot = telegram.Bot(token=os.environ.get('TOKEN')) + chat_id = os.environ.get('CHAT_ID') + self._bot = bot + self._chat_id = chat_id + print('Testing the Bot API class') def testGetMe(self): '''Test the telegram.Bot getMe method''' print('Testing getMe') bot = self._bot.getMe() + self.assertTrue(self.is_json(bot.to_json())) - self.assertEqual(120405045, bot.id) - self.assertEqual('Toledo\'s Palace Bot', bot.first_name) - self.assertEqual(None, bot.last_name) - self.assertEqual('ToledosPalaceBot', bot.username) + self.assertEqual(bot.id, 120405045) + self.assertEqual(bot.first_name, 'Toledo\'s Palace Bot') + self.assertEqual(bot.last_name, '') + self.assertEqual(bot.username, 'ToledosPalaceBot') + self.assertEqual(bot.name, '@ToledosPalaceBot') def testSendMessage(self): '''Test the telegram.Bot sendMessage method''' print('Testing sendMessage') - message = self._bot.sendMessage(chat_id=12173560, + message = self._bot.sendMessage(chat_id=self._chat_id, text='Моё судно на воздушной подушке полно угрей') + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(u'Моё судно на воздушной подушке полно угрей', message.text) - self.assertIsInstance(message.date, datetime) + self.assertEqual(message.text, u'Моё судно на воздушной подушке полно угрей') + self.assertTrue(isinstance(message.date, datetime)) def testGetUpdates(self): '''Test the telegram.Bot getUpdates method''' print('Testing getUpdates') updates = self._bot.getUpdates() - self.assertTrue(self.is_json(updates[0].to_json())) - self.assertIsInstance(updates[0], telegram.Update) + + if updates: + self.assertTrue(self.is_json(updates[0].to_json())) + self.assertTrue(isinstance(updates[0], telegram.Update)) def testForwardMessage(self): '''Test the telegram.Bot forwardMessage method''' print('Testing forwardMessage') - message = self._bot.forwardMessage(chat_id=12173560, - from_chat_id=12173560, + message = self._bot.forwardMessage(chat_id=self._chat_id, + from_chat_id=self._chat_id, message_id=138) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual('Oi', message.text) - self.assertEqual('leandrotoledo', message.forward_from.username) - self.assertIsInstance(message.forward_date, datetime) + self.assertEqual(message.text, 'Oi') + self.assertEqual(message.forward_from.username, 'leandrotoledo') + self.assertTrue(isinstance(message.forward_date, datetime)) def testSendPhoto(self): '''Test the telegram.Bot sendPhoto method''' print('Testing sendPhoto - File') - message = self._bot.sendPhoto(photo=open('tests/telegram.png', 'rb'), + message = self._bot.sendPhoto(photo=open('tests/data/telegram.png', 'rb'), caption='testSendPhoto', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(1451, message.photo[0].file_size) - self.assertEqual('testSendPhoto', message.caption) + self.assertEqual(message.photo[0].file_size, 1451) + self.assertEqual(message.caption, 'testSendPhoto') def testResendPhoto(self): '''Test the telegram.Bot sendPhoto method''' print('Testing sendPhoto - Resend') message = self._bot.sendPhoto(photo='AgADAQADr6cxGzU8LQe6q0dMJD2rHYkP2ykABFymiQqJgjxRGGMAAgI', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual('AgADAQADr6cxGzU8LQe6q0dMJD2rHYkP2ykABFymiQqJgjxRGGMAAgI', message.photo[0].file_id) + self.assertEqual(message.photo[0].file_id, 'AgADAQADr6cxGzU8LQe6q0dMJD2rHYkP2ykABFymiQqJgjxRGGMAAgI') def testSendJPGURLPhoto(self): '''Test the telegram.Bot sendPhoto method''' print('Testing testSendJPGURLPhoto - URL') message = self._bot.sendPhoto(photo='http://dummyimage.com/600x400/000/fff.jpg&text=telegram', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(822, message.photo[0].file_size) + self.assertEqual(message.photo[0].file_size, 822) def testSendPNGURLPhoto(self): '''Test the telegram.Bot sendPhoto method''' print('Testing testSendPNGURLPhoto - URL') message = self._bot.sendPhoto(photo='http://dummyimage.com/600x400/000/fff.png&text=telegram', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(684, message.photo[0].file_size) + self.assertEqual(message.photo[0].file_size, 684) def testSendGIFURLPhoto(self): '''Test the telegram.Bot sendPhoto method''' print('Testing testSendGIFURLPhoto - URL') message = self._bot.sendPhoto(photo='http://dummyimage.com/600x400/000/fff.gif&text=telegram', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(684, message.photo[0].file_size) + self.assertEqual(message.photo[0].file_size, 684) def testSendAudio(self): '''Test the telegram.Bot sendAudio method''' print('Testing sendAudio - File') - message = self._bot.sendAudio(audio=open('tests/telegram.mp3', 'rb'), - chat_id=12173560, + message = self._bot.sendAudio(audio=open('tests/data/telegram.mp3', 'rb'), + chat_id=self._chat_id, performer='Leandro Toledo', title='Teste') + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(28232, message.audio.file_size) - self.assertEqual('Leandro Toledo', message.audio.performer) - self.assertEqual('Teste', message.audio.title) + self.assertEqual(message.audio.file_size, 28232) + self.assertEqual(message.audio.performer, 'Leandro Toledo') + self.assertEqual(message.audio.title, 'Teste') def testResendAudio(self): '''Test the telegram.Bot sendAudio method''' print('Testing sendAudio - Resend') message = self._bot.sendAudio(audio='BQADAQADwwcAAjU8LQdBRsl3_qD2TAI', - chat_id=12173560, + chat_id=self._chat_id, performer='Leandro Toledo', # Bug #39 title='Teste') # Bug #39 + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual('BQADAQADwwcAAjU8LQdBRsl3_qD2TAI', message.audio.file_id) + self.assertEqual(message.audio.file_id, 'BQADAQADwwcAAjU8LQdBRsl3_qD2TAI') def testSendVoice(self): '''Test the telegram.Bot sendVoice method''' print('Testing sendVoice - File') - message = self._bot.sendVoice(voice=open('tests/telegram.ogg', 'rb'), - chat_id=12173560) + message = self._bot.sendVoice(voice=open('tests/data/telegram.ogg', 'rb'), + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(9199, message.voice.file_size) + self.assertEqual(message.voice.file_size, 9199) def testResendVoice(self): '''Test the telegram.Bot sendVoice method''' print('Testing sendVoice - Resend') message = self._bot.sendVoice(voice='AwADAQADIQEAAvjAuQABSAXg_GhkhZcC', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual('AwADAQADIQEAAvjAuQABSAXg_GhkhZcC', message.voice.file_id) + self.assertEqual(message.voice.file_id, 'AwADAQADIQEAAvjAuQABSAXg_GhkhZcC') def testSendDocument(self): '''Test the telegram.Bot sendDocument method''' print('Testing sendDocument - File') - message = self._bot.sendDocument(document=open('tests/telegram.png', 'rb'), - chat_id=12173560) + message = self._bot.sendDocument(document=open('tests/data/telegram.png', 'rb'), + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(12948, message.document.file_size) + self.assertEqual(message.document.file_size, 12948) def testSendGIFURLDocument(self): '''Test the telegram.Bot sendDocument method''' print('Testing sendDocument - File') message = self._bot.sendPhoto(photo='http://dummyimage.com/600x400/000/fff.gif&text=telegram', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(684, message.photo[0].file_size) + self.assertEqual(message.photo[0].file_size, 684) def testResendDocument(self): '''Test the telegram.Bot sendDocument method''' print('Testing sendDocument - Resend') message = self._bot.sendDocument(document='BQADAQADHAADNTwtBxZxUGKyxYbYAg', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual('BQADAQADHAADNTwtBxZxUGKyxYbYAg', message.document.file_id) + self.assertEqual(message.document.file_id, 'BQADAQADHAADNTwtBxZxUGKyxYbYAg') def testSendVideo(self): '''Test the telegram.Bot sendVideo method''' print('Testing sendVideo - File') - message = self._bot.sendVideo(video=open('tests/telegram.mp4', 'rb'), + message = self._bot.sendVideo(video=open('tests/data/telegram.mp4', 'rb'), caption='testSendVideo', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(326534, message.video.file_size) - self.assertEqual('testSendVideo', message.caption) + self.assertEqual(message.video.file_size, 326534) + self.assertEqual(message.caption, 'testSendVideo') def testResendVideo(self): '''Test the telegram.Bot sendVideo method''' print('Testing sendVideo - Resend') message = self._bot.sendVideo(video='BAADAQADIgEAAvjAuQABOuTB937fPTgC', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(4, message.video.duration) + self.assertEqual(message.video.duration, 4) def testResendSticker(self): '''Test the telegram.Bot sendSticker method''' print('Testing sendSticker - Resend') message = self._bot.sendSticker(sticker='BQADAQADHAADyIsGAAFZfq1bphjqlgI', - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(39518, message.sticker.file_size) + self.assertEqual(message.sticker.file_size, 39518) def testSendLocation(self): '''Test the telegram.Bot sendLocation method''' print('Testing sendLocation') message = self._bot.sendLocation(latitude=-23.558873, longitude=-46.659732, - chat_id=12173560) + chat_id=self._chat_id) + self.assertTrue(self.is_json(message.to_json())) - self.assertEqual(-23.558873, message.location.latitude) - self.assertEqual(-46.659732, message.location.longitude) + self.assertEqual(message.location.latitude, -23.558873) + self.assertEqual(message.location.longitude, -46.659732) def testSendChatAction(self): '''Test the telegram.Bot sendChatAction method''' print('Testing sendChatAction - ChatAction.TYPING') + self._bot.sendChatAction(action=telegram.ChatAction.TYPING, - chat_id=12173560) + chat_id=self._chat_id) def testGetUserProfilePhotos(self): '''Test the telegram.Bot getUserProfilePhotos method''' print('Testing getUserProfilePhotos') - upf = self._bot.getUserProfilePhotos(user_id=12173560) + upf = self._bot.getUserProfilePhotos(user_id=self._chat_id) + self.assertTrue(self.is_json(upf.to_json())) - self.assertEqual(6547, upf.photos[0][0].file_size) + self.assertEqual(upf.photos[0][0].file_size, 6547) + +unittest.main() diff --git a/tests/test_command_handler.py b/tests/test_command_handler.py new file mode 100644 index 000000000..171b24ef4 --- /dev/null +++ b/tests/test_command_handler.py @@ -0,0 +1,75 @@ +import sys +sys.path.append('.') +import telegram +import unittest +import unittest.mock +from telegram.command_handler import CommandHandler, CommandHandlerWithHelp + + +class CommandHandlerTmp(CommandHandler): + def __init__(self, *args, **kwargs): + super(CommandHandlerTmp, self).__init__(*args, **kwargs) + self.output = None + + def command_test(self, update): + self.output = 1 + + +class CommandHandlerTmp2(CommandHandlerWithHelp): + def __init__(self, *args, **kwargs): + super(CommandHandlerTmp2, self).__init__(*args, **kwargs) + self.output_test = None + + def command_test(self, update): + self.output_test = 1 + + +def fake_getUpdates(*args, **kwargs): + from_user = telegram.User(id=42, first_name='hello') + message = telegram.Message(message_id=42, from_user=from_user, date=None, chat=from_user, text='/test') + update = telegram.Update(update_id=42, message=message) + return [update] + +output_fsm = None + + +def fake_sendMessage(chat_id, message, *args, **kwargs): + global output_fsm + output_fsm = (chat_id, message) + return telegram.Message(43, 123, 000000, telegram.User(chat_id, 'test'), text=message) + + +class CommandHandlerTest(unittest.TestCase): + def setUp(self): + self.bot = unittest.mock.MagicMock() + self.bot.getUpdates = fake_getUpdates + self.bot.sendMessage = fake_sendMessage + + def test_get_command_func(self): + CH = CommandHandlerTmp(self.bot) + self.assertEqual(CH.command_test, CH._get_command_func('test')) + self.assertEqual(CH.command_test, CH._get_command_func('/test')) + self.assertEqual(None, CH._get_command_func('this function does not exsist')) + + def test_run_once(self): + CH = CommandHandlerTmp(self.bot) + self.assertEqual(CH.output, None) + threads, last_update = CH.run_once(make_thread=True) + for t in threads: + t.start() + for t in threads: + t.join() + self.assertEqual(CH.output, 1) + + def test_run(self): + pass # TODO implement test + + def test__command_not_found(self): + CH = CommandHandlerTmp(self.bot) + CH._command_not_found(self.bot.getUpdates()[0]) + self.assertEqual(output_fsm, (42, "Sorry, I didn't understand the command: /test.")) + + +if __name__ == '__main__': + import sys + unittest.main(sys.argv)