diff --git a/telegram/__init__.py b/telegram/__init__.py index 6f918547a..d0f133035 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -23,6 +23,7 @@ from sys import version_info from .base import TelegramObject from .user import User from .chat import Chat +from .chatmember import ChatMember from .photosize import PhotoSize from .audio import Audio from .voice import Voice @@ -82,24 +83,25 @@ from .bot import Bot __author__ = 'devs@python-telegram-bot.org' __version__ = '4.1.2' -__all__ = ['Audio', 'Bot', 'Chat', 'ChatAction', 'ChosenInlineResult', 'CallbackQuery', 'Contact', - 'Document', 'Emoji', 'File', 'ForceReply', 'InlineKeyboardButton', - 'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', 'InlineQueryResult', - 'InlineQueryResultArticle', 'InlineQueryResultAudio', 'InlineQueryResultCachedAudio', - 'InlineQueryResultCachedDocument', 'InlineQueryResultCachedGif', - 'InlineQueryResultCachedMpeg4Gif', 'InlineQueryResultCachedPhoto', - 'InlineQueryResultCachedSticker', 'InlineQueryResultCachedVideo', - 'InlineQueryResultCachedVoice', 'InlineQueryResultContact', 'InlineQueryResultDocument', - 'InlineQueryResultGif', 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif', - 'InlineQueryResultPhoto', 'InlineQueryResultVenue', 'InlineQueryResultVideo', - 'InlineQueryResultVoice', 'InputContactMessageContent', 'InputFile', - 'InputLocationMessageContent', 'InputMessageContent', 'InputTextMessageContent', - 'InputVenueMessageContent', 'KeyboardButton', 'Location', 'Message', 'MessageEntity', - 'NullHandler', 'ParseMode', 'PhotoSize', 'ReplyKeyboardHide', 'ReplyKeyboardMarkup', - 'ReplyMarkup', 'Sticker', 'TelegramError', 'TelegramObject', 'Update', 'User', - 'UserProfilePhotos', 'Venue', 'Video', 'Voice'] +__all__ = ['Audio', 'Bot', 'Chat', 'ChatMember', 'ChatAction', 'ChosenInlineResult', + 'CallbackQuery', 'Contact', 'Document', 'Emoji', 'File', 'ForceReply', + 'InlineKeyboardButton', 'InlineKeyboardMarkup', 'InlineQuery', 'InlineQueryResult', + 'InlineQueryResult', 'InlineQueryResultArticle', 'InlineQueryResultAudio', + 'InlineQueryResultCachedAudio', 'InlineQueryResultCachedDocument', + 'InlineQueryResultCachedGif', 'InlineQueryResultCachedMpeg4Gif', + 'InlineQueryResultCachedPhoto', 'InlineQueryResultCachedSticker', + 'InlineQueryResultCachedVideo', 'InlineQueryResultCachedVoice', + 'InlineQueryResultContact', 'InlineQueryResultDocument', 'InlineQueryResultGif', + 'InlineQueryResultLocation', 'InlineQueryResultMpeg4Gif', 'InlineQueryResultPhoto', + 'InlineQueryResultVenue', 'InlineQueryResultVideo', 'InlineQueryResultVoice', + 'InputContactMessageContent', 'InputFile', 'InputLocationMessageContent', + 'InputMessageContent', 'InputTextMessageContent', 'InputVenueMessageContent', + 'KeyboardButton', 'Location', 'Message', 'MessageEntity', 'NullHandler', 'ParseMode', + 'PhotoSize', 'ReplyKeyboardHide', 'ReplyKeyboardMarkup', 'ReplyMarkup', 'Sticker', + 'TelegramError', 'TelegramObject', 'Update', 'User', 'UserProfilePhotos', 'Venue', + 'Video', 'Voice'] if version_info < (2, 7): from warnings import warn warn("python-telegram-bot will stop supporting Python 2.6 in a future release. " - "Please upgrade your Python!") + "Please upgrade your Python version to at least Python 2.7!") diff --git a/telegram/bot.py b/telegram/bot.py index fe55a45c9..d16d18926 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -19,11 +19,11 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains a object that represents a Telegram Bot.""" -import logging import functools +import logging -from telegram import (User, Message, Update, UserProfilePhotos, File, ReplyMarkup, TelegramObject, - NullHandler) +from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File, + ReplyMarkup, TelegramObject, NullHandler) from telegram.error import InvalidToken from telegram.utils import request @@ -154,7 +154,7 @@ class Bot(TelegramObject): return decorator @log - def getMe(self): + def getMe(self, **kwargs): """A simple method for testing your bot's auth token. Returns: @@ -1182,7 +1182,7 @@ class Bot(TelegramObject): return url, data @log - def getUpdates(self, offset=None, limit=100, timeout=0, network_delay=.2): + def getUpdates(self, offset=None, limit=100, timeout=0, network_delay=.2, **kwargs): """Use this method to receive incoming updates using long polling. Args: @@ -1271,6 +1271,166 @@ class Bot(TelegramObject): return result + @log + def leaveChat(self, chat_id, **kwargs): + """Use this method for your bot to leave a group, supergroup or + channel. + + Args: + chat_id: + Unique identifier for the target chat or username of the target + channel (in the format @channelusername). + + Keyword Args: + timeout (Optional[float]): If this value is specified, use it as + the definitive timeout (in seconds) for urlopen() operations. + + Returns: + bool: On success, `True` is returned. + + Raises: + :class:`telegram.TelegramError` + + """ + + url = '{0}/leaveChat'.format(self.base_url) + + data = {'chat_id': chat_id} + + result = request.post(url, data, timeout=kwargs.get('timeout')) + + return result + + @log + def getChat(self, chat_id, **kwargs): + """Use this method to get up to date information about the chat + (current name of the user for one-on-one conversations, current + username of a user, group or channel, etc.). + + Args: + chat_id: + Unique identifier for the target chat or username of the target + channel (in the format @channelusername). + + Keyword Args: + timeout (Optional[float]): If this value is specified, use it as + the definitive timeout (in seconds) for urlopen() operations. + + Returns: + :class:`telegram.Chat`: On success, :class:`telegram.Chat` is + returned. + + Raises: + :class:`telegram.TelegramError` + + """ + + url = '{0}/getChat'.format(self.base_url) + + data = {'chat_id': chat_id} + + result = request.post(url, data, timeout=kwargs.get('timeout')) + + return Chat.de_json(result) + + @log + def getChatAdministrators(self, chat_id, **kwargs): + """Use this method to get a list of administrators in a chat. On + success, returns an Array of ChatMember objects that contains + information about all chat administrators except other bots. If the + chat is a group or a supergroup and no administrators were appointed, + only the creator will be returned. + + Args: + chat_id: + Unique identifier for the target chat or username of the target + channel (in the format @channelusername). + + + Keyword Args: + timeout (Optional[float]): If this value is specified, use it as + the definitive timeout (in seconds) for urlopen() operations. + + Returns: + list[:class:`telegram.ChatMember`]: On success, a list of + :class:`telegram.ChatMember` objects are returned. + + Raises: + :class:`telegram.TelegramError` + + """ + + url = '{0}/getChatAdministrators'.format(self.base_url) + + data = {'chat_id': chat_id} + + result = request.post(url, data, timeout=kwargs.get('timeout')) + + return [ChatMember.de_json(x) for x in result] + + @log + def getChatMembersCount(self, chat_id, **kwargs): + """Use this method to get the number of members in a chat. + + Args: + chat_id: + Unique identifier for the target chat or username of the target + channel (in the format @channelusername). + + + Keyword Args: + timeout (Optional[float]): If this value is specified, use it as + the definitive timeout (in seconds) for urlopen() operations. + + Returns: + int: On success, an `int` is returned. + + Raises: + :class:`telegram.TelegramError` + + """ + + url = '{0}/getChatMembersCount'.format(self.base_url) + + data = {'chat_id': chat_id} + + result = request.post(url, data, timeout=kwargs.get('timeout')) + + return result + + @log + def getChatMember(self, chat_id, user_id, **kwargs): + """Use this method to get information about a member of a chat. + + Args: + chat_id: + Unique identifier for the target chat or username of the target + channel (in the format @channelusername). + user_id: + Unique identifier of the target user. + + + Keyword Args: + timeout (Optional[float]): If this value is specified, use it as + the definitive timeout (in seconds) for urlopen() operations. + + Returns: + :class:`telegram.ChatMember`: On success, + :class:`telegram.ChatMember` is returned. + + Raises: + :class:`telegram.TelegramError` + + """ + + url = '{0}/getChatMember'.format(self.base_url) + + data = {'chat_id': chat_id, 'user_id': user_id} + + result = request.post(url, data, timeout=kwargs.get('timeout')) + + return ChatMember.de_json(result) + @staticmethod def de_json(data): data = super(Bot, Bot).de_json(data) @@ -1314,3 +1474,8 @@ class Bot(TelegramObject): edit_message_reply_markup = editMessageReplyMarkup get_updates = getUpdates set_webhook = setWebhook + leave_chat = leaveChat + get_chat = getChat + get_chat_administrators = getChatAdministrators + get_chat_member = getChatMember + get_chat_members_count = getChatMembersCount diff --git a/telegram/chat.py b/telegram/chat.py index 55b3c2c0a..962d60041 100644 --- a/telegram/chat.py +++ b/telegram/chat.py @@ -42,6 +42,11 @@ class Chat(TelegramObject): type (Optional[str]): """ + PRIVATE = 'private' + GROUP = 'group' + SUPERGROUP = 'supergroup' + CHANNEL = 'channel' + def __init__(self, id, type, **kwargs): # Required self.id = int(id) diff --git a/telegram/chatmember.py b/telegram/chatmember.py new file mode 100644 index 000000000..22b7d306a --- /dev/null +++ b/telegram/chatmember.py @@ -0,0 +1,62 @@ +#!/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 a object that represents a Telegram ChatMember.""" + +from telegram import User, TelegramObject + + +class ChatMember(TelegramObject): + """This object represents a Telegram ChatMember. + + Attributes: + user (:class:`telegram.User`): Information about the user. + status (str): The member's status in the chat. Can be 'creator', 'administrator', 'member', + 'left' or 'kicked'. + + Args: + user (:class:`telegram.User`): + status (str): + """ + + CREATOR = 'creator' + ADMINISTRATOR = 'administrator' + MEMBER = 'member' + LEFT = 'left' + KICKED = 'kicked' + + def __init__(self, user, status, **kwargs): + # Required + self.user = user + self.status = status + + @staticmethod + def de_json(data): + """ + Args: + data (dict): + + Returns: + telegram.ChatMember: + """ + if not data: + return None + + data['user'] = User.de_json(data.get('user')) + + return ChatMember(**data) diff --git a/telegram/choseninlineresult.py b/telegram/choseninlineresult.py index 2de914be7..7eed66519 100644 --- a/telegram/choseninlineresult.py +++ b/telegram/choseninlineresult.py @@ -41,7 +41,13 @@ class ChosenInlineResult(TelegramObject): """ - def __init__(self, result_id, from_user, query, location=None, inline_message_id=None): + def __init__(self, + result_id, + from_user, + query, + location=None, + inline_message_id=None, + **kwargs): # Required self.result_id = result_id self.from_user = from_user diff --git a/telegram/error.py b/telegram/error.py index 0f9005b40..201f2c778 100644 --- a/telegram/error.py +++ b/telegram/error.py @@ -51,6 +51,7 @@ class TelegramError(Exception): msg = _lstrip_str(message, 'Error: ') msg = _lstrip_str(msg, '[Error]: ') + msg = _lstrip_str(msg, 'Bad Request: ') if msg != message: # api_error - capitalize the msg... msg = msg.capitalize() @@ -76,6 +77,10 @@ class NetworkError(TelegramError): pass +class BadRequest(NetworkError): + pass + + class TimedOut(NetworkError): def __init__(self): diff --git a/telegram/ext/commandhandler.py b/telegram/ext/commandhandler.py index 2d22f9221..4c2a98c56 100644 --- a/telegram/ext/commandhandler.py +++ b/telegram/ext/commandhandler.py @@ -34,6 +34,8 @@ class CommandHandler(Handler): callback (function): A function that takes ``bot, update`` as positional arguments. It will be called when the ``check_update`` has determined that an update should be processed by this handler. + allow_edited (Optional[bool]): If the handler should also accept edited messages. + Default is ``False`` pass_args (optional[bool]): If the handler should be passed the arguments passed to the command as a keyword argument called ` ``args``. It will contain a list of strings, which is the text @@ -43,21 +45,35 @@ class CommandHandler(Handler): be used to insert updates. Default is ``False`` """ - def __init__(self, command, callback, pass_args=False, pass_update_queue=False): + def __init__(self, + command, + callback, + allow_edited=False, + pass_args=False, + pass_update_queue=False): super(CommandHandler, self).__init__(callback, pass_update_queue) self.command = command + self.allow_edited = allow_edited self.pass_args = pass_args def check_update(self, update): - return (isinstance(update, Update) and update.message and update.message.text - and update.message.text.startswith('/') - and update.message.text[1:].split(' ')[0].split('@')[0] == self.command) + if (isinstance(update, Update) + and (update.message or update.edited_message and self.allow_edited)): + message = update.message or update.edited_message + + return (message.text and message.text.startswith('/') + and message.text[1:].split(' ')[0].split('@')[0] == self.command) + + else: + return False def handle_update(self, update, dispatcher): optional_args = self.collect_optional_args(dispatcher) + message = update.message or update.edited_message + if self.pass_args: - optional_args['args'] = update.message.text.split(' ')[1:] + optional_args['args'] = message.text.split(' ')[1:] self.callback(dispatcher.bot, update, **optional_args) diff --git a/telegram/ext/messagehandler.py b/telegram/ext/messagehandler.py index fd3248762..ddef6bdb1 100644 --- a/telegram/ext/messagehandler.py +++ b/telegram/ext/messagehandler.py @@ -30,57 +30,56 @@ class Filters(object): """ @staticmethod - def text(update): - return update.message.text and not update.message.text.startswith('/') + def text(message): + return message.text and not message.text.startswith('/') @staticmethod - def command(update): - return update.message.text and update.message.text.startswith('/') + def command(message): + return message.text and message.text.startswith('/') @staticmethod - def audio(update): - return bool(update.message.audio) + def audio(message): + return bool(message.audio) @staticmethod - def document(update): - return bool(update.message.document) + def document(message): + return bool(message.document) @staticmethod - def photo(update): - return bool(update.message.photo) + def photo(message): + return bool(message.photo) @staticmethod - def sticker(update): - return bool(update.message.sticker) + def sticker(message): + return bool(message.sticker) @staticmethod - def video(update): - return bool(update.message.video) + def video(message): + return bool(message.video) @staticmethod - def voice(update): - return bool(update.message.voice) + def voice(message): + return bool(message.voice) @staticmethod - def contact(update): - return bool(update.message.contact) + def contact(message): + return bool(message.contact) @staticmethod - def location(update): - return bool(update.message.location) + def location(message): + return bool(message.location) @staticmethod - def venue(update): - return bool(update.message.venue) + def venue(message): + return bool(message.venue) @staticmethod - def status_update(update): - return bool(update.message.new_chat_member or update.message.left_chat_member - or update.message.new_chat_title or update.message.new_chat_photo - or update.message.delete_chat_photo or update.message.group_chat_created - or update.message.supergroup_chat_created - or update.message.channel_chat_created or update.message.migrate_to_chat_id - or update.message.migrate_from_chat_id or update.message.pinned_message) + def status_update(message): + return bool(message.new_chat_member or message.left_chat_member or message.new_chat_title + or message.new_chat_photo or message.delete_chat_photo + or message.group_chat_created or message.supergroup_chat_created + or message.channel_chat_created or message.migrate_to_chat_id + or message.migrate_from_chat_id or message.pinned_message) class MessageHandler(Handler): @@ -99,23 +98,32 @@ class MessageHandler(Handler): callback (function): A function that takes ``bot, update`` as positional arguments. It will be called when the ``check_update`` has determined that an update should be processed by this handler. + allow_edited (Optional[bool]): If the handler should also accept edited messages. + Default is ``False`` pass_update_queue (optional[bool]): If the handler should be passed the update queue as a keyword argument called ``update_queue``. It can be used to insert updates. Default is ``False`` """ - def __init__(self, filters, callback, pass_update_queue=False): + def __init__(self, filters, callback, allow_edited=False, pass_update_queue=False): super(MessageHandler, self).__init__(callback, pass_update_queue) self.filters = filters + self.allow_edited = allow_edited def check_update(self, update): - if isinstance(update, Update) and update.message: + if (isinstance(update, Update) + and (update.message or update.edited_message and self.allow_edited)): + if not self.filters: res = True + else: - res = any(func(update) for func in self.filters) + message = update.message or update.edited_message + res = any(func(message) for func in self.filters) + else: res = False + return res def handle_update(self, update, dispatcher): diff --git a/telegram/inlinekeyboardmarkup.py b/telegram/inlinekeyboardmarkup.py index fadef209d..139a3e2fc 100644 --- a/telegram/inlinekeyboardmarkup.py +++ b/telegram/inlinekeyboardmarkup.py @@ -33,7 +33,7 @@ class InlineKeyboardMarkup(ReplyMarkup): """ - def __init__(self, inline_keyboard): + def __init__(self, inline_keyboard, **kwargs): # Required self.inline_keyboard = inline_keyboard diff --git a/telegram/inputcontactmessagecontent.py b/telegram/inputcontactmessagecontent.py index 2dd41c4cf..4a0964813 100644 --- a/telegram/inputcontactmessagecontent.py +++ b/telegram/inputcontactmessagecontent.py @@ -25,7 +25,7 @@ from telegram import InputMessageContent class InputContactMessageContent(InputMessageContent): """Base class for Telegram InputContactMessageContent Objects""" - def __init__(self, phone_number, first_name, last_name=None): + def __init__(self, phone_number, first_name, last_name=None, **kwargs): # Required self.phone_number = phone_number self.first_name = first_name diff --git a/telegram/inputlocationmessagecontent.py b/telegram/inputlocationmessagecontent.py index b975cd118..05a7e35f6 100644 --- a/telegram/inputlocationmessagecontent.py +++ b/telegram/inputlocationmessagecontent.py @@ -25,7 +25,7 @@ from telegram import InputMessageContent class InputLocationMessageContent(InputMessageContent): """Base class for Telegram InputLocationMessageContent Objects""" - def __init__(self, latitude, longitude): + def __init__(self, latitude, longitude, **kwargs): # Required self.latitude = latitude self.longitude = longitude diff --git a/telegram/inputmessagecontent.py b/telegram/inputmessagecontent.py index 56072681d..0663ecbcd 100644 --- a/telegram/inputmessagecontent.py +++ b/telegram/inputmessagecontent.py @@ -39,14 +39,14 @@ class InputMessageContent(TelegramObject): pass try: - from telegram import InputLocationMessageContent - return InputLocationMessageContent.de_json(data) + from telegram import InputVenueMessageContent + return InputVenueMessageContent.de_json(data) except TypeError: pass try: - from telegram import InputVenueMessageContent - return InputVenueMessageContent.de_json(data) + from telegram import InputLocationMessageContent + return InputLocationMessageContent.de_json(data) except TypeError: pass diff --git a/telegram/inputtextmessagecontent.py b/telegram/inputtextmessagecontent.py index d78eb9bc7..999d47b0a 100644 --- a/telegram/inputtextmessagecontent.py +++ b/telegram/inputtextmessagecontent.py @@ -25,7 +25,7 @@ from telegram import InputMessageContent class InputTextMessageContent(InputMessageContent): """Base class for Telegram InputTextMessageContent Objects""" - def __init__(self, message_text, parse_mode=None, disable_web_page_preview=None): + def __init__(self, message_text, parse_mode=None, disable_web_page_preview=None, **kwargs): # Required self.message_text = message_text # Optionals diff --git a/telegram/inputvenuemessagecontent.py b/telegram/inputvenuemessagecontent.py index d34d48a50..23b342f9c 100644 --- a/telegram/inputvenuemessagecontent.py +++ b/telegram/inputvenuemessagecontent.py @@ -25,7 +25,7 @@ from telegram import InputMessageContent class InputVenueMessageContent(InputMessageContent): """Base class for Telegram InputVenueMessageContent Objects""" - def __init__(self, latitude, longitude, title, address, foursquare_id=None): + def __init__(self, latitude, longitude, title, address, foursquare_id=None, **kwargs): # Required self.latitude = latitude self.longitude = longitude diff --git a/telegram/keyboardbutton.py b/telegram/keyboardbutton.py index 904c0631c..c6c0731d0 100644 --- a/telegram/keyboardbutton.py +++ b/telegram/keyboardbutton.py @@ -33,7 +33,7 @@ class KeyboardButton(TelegramObject): request_contact (Optional[bool]): """ - def __init__(self, text, request_contact=None, request_location=None): + def __init__(self, text, request_contact=None, request_location=None, **kwargs): # Required self.text = text # Optionals diff --git a/telegram/location.py b/telegram/location.py index 3b84b7ad0..cc28dde10 100644 --- a/telegram/location.py +++ b/telegram/location.py @@ -33,7 +33,7 @@ class Location(TelegramObject): latitude (float): """ - def __init__(self, longitude, latitude): + def __init__(self, longitude, latitude, **kwargs): # Required self.longitude = float(longitude) self.latitude = float(latitude) diff --git a/telegram/message.py b/telegram/message.py index 2e2471ee3..009f0b89d 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -40,6 +40,7 @@ class Message(TelegramObject): forward_from_chat (:class:`telegram.Chat`): forward_date (:class:`datetime.datetime`): reply_to_message (:class:`telegram.Message`): + edit_date (:class:`datetime.datetime`): text (str): audio (:class:`telegram.Audio`): document (:class:`telegram.Document`): @@ -80,6 +81,7 @@ class Message(TelegramObject): forward_from_chat (:class:`telegram.Chat`): forward_date (Optional[:class:`datetime.datetime`]): reply_to_message (Optional[:class:`telegram.Message`]): + edit_date (Optional[:class:`datetime.datetime`]): text (Optional[str]): audio (Optional[:class:`telegram.Audio`]): document (Optional[:class:`telegram.Document`]): @@ -113,6 +115,7 @@ class Message(TelegramObject): self.forward_from_chat = kwargs.get('forward_from_chat') self.forward_date = kwargs.get('forward_date') self.reply_to_message = kwargs.get('reply_to_message') + self.edit_date = kwargs.get('edit_date') self.text = kwargs.get('text', '') self.entities = kwargs.get('entities', list()) self.audio = kwargs.get('audio') @@ -162,6 +165,7 @@ class Message(TelegramObject): data['forward_from_chat'] = Chat.de_json(data.get('forward_from_chat')) data['forward_date'] = Message._fromtimestamp(data.get('forward_date')) data['reply_to_message'] = Message.de_json(data.get('reply_to_message')) + data['edit_date'] = Message._fromtimestamp(data.get('edit_date')) data['audio'] = Audio.de_json(data.get('audio')) data['document'] = Document.de_json(data.get('document')) data['photo'] = PhotoSize.de_list(data.get('photo')) @@ -197,6 +201,8 @@ class Message(TelegramObject): # Optionals if self.forward_date: data['forward_date'] = self._totimestamp(self.forward_date) + if self.edit_date: + data['edit_date'] = self._totimestamp(self.edit_date) if self.photo: data['photo'] = [p.to_dict() for p in self.photo] if self.entities: diff --git a/telegram/messageentity.py b/telegram/messageentity.py index cfa309a18..1eb87180b 100644 --- a/telegram/messageentity.py +++ b/telegram/messageentity.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains a object that represents a Telegram MessageEntity.""" -from telegram import TelegramObject +from telegram import User, TelegramObject class MessageEntity(TelegramObject): @@ -31,6 +31,7 @@ class MessageEntity(TelegramObject): offset (int): length (int): url (Optional[str]): + user (Optional[:class:`telegram.User`]): """ def __init__(self, type, offset, length, **kwargs): @@ -40,11 +41,14 @@ class MessageEntity(TelegramObject): self.length = length # Optionals self.url = kwargs.get('url') + self.user = kwargs.get('user') @staticmethod def de_json(data): data = super(MessageEntity, MessageEntity).de_json(data) + data['user'] = User.de_json(data.get('user')) + return MessageEntity(**data) @staticmethod diff --git a/telegram/update.py b/telegram/update.py index 17cc16603..c20c61d3c 100644 --- a/telegram/update.py +++ b/telegram/update.py @@ -27,6 +27,7 @@ class Update(TelegramObject): Attributes: update_id (int): message (:class:`telegram.Message`): + edited_message (:class:`telegram.Message`): inline_query (:class:`telegram.InlineQuery`): chosen_inline_result (:class:`telegram.ChosenInlineResult`): callback_query (:class:`telegram.CallbackQuery`): @@ -37,6 +38,7 @@ class Update(TelegramObject): Keyword Args: message (Optional[:class:`telegram.Message`]): + edited_message (Optional[:class:`telegram.Message`]): inline_query (Optional[:class:`telegram.InlineQuery`]): chosen_inline_result (Optional[:class:`telegram.ChosenInlineResult`]) callback_query (Optional[:class:`telegram.CallbackQuery`]): @@ -47,6 +49,7 @@ class Update(TelegramObject): self.update_id = int(update_id) # Optionals self.message = kwargs.get('message') + self.edited_message = kwargs.get('edited_message') self.inline_query = kwargs.get('inline_query') self.chosen_inline_result = kwargs.get('chosen_inline_result') self.callback_query = kwargs.get('callback_query') @@ -64,6 +67,7 @@ class Update(TelegramObject): return None data['message'] = Message.de_json(data.get('message')) + data['edited_message'] = Message.de_json(data.get('edited_message')) data['inline_query'] = InlineQuery.de_json(data.get('inline_query')) data['chosen_inline_result'] = ChosenInlineResult.de_json(data.get('chosen_inline_result')) data['callback_query'] = CallbackQuery.de_json(data.get('callback_query')) diff --git a/telegram/userprofilephotos.py b/telegram/userprofilephotos.py index 806270c6a..91ded979a 100644 --- a/telegram/userprofilephotos.py +++ b/telegram/userprofilephotos.py @@ -34,7 +34,7 @@ class UserProfilePhotos(TelegramObject): photos (List[List[:class:`telegram.PhotoSize`]]): """ - def __init__(self, total_count, photos): + def __init__(self, total_count, photos, **kwargs): # Required self.total_count = int(total_count) self.photos = photos diff --git a/telegram/utils/request.py b/telegram/utils/request.py index b326d6ccf..52fe434d7 100644 --- a/telegram/utils/request.py +++ b/telegram/utils/request.py @@ -28,7 +28,7 @@ from future.moves.urllib.error import HTTPError, URLError from future.moves.urllib.request import urlopen, urlretrieve, Request from telegram import (InputFile, TelegramError) -from telegram.error import Unauthorized, NetworkError, TimedOut +from telegram.error import Unauthorized, NetworkError, TimedOut, BadRequest def _parse(json_data): @@ -67,13 +67,15 @@ def _try_except_req(func): # come first. errcode = error.getcode() - if errcode in (401, 403): - raise Unauthorized() - elif errcode == 502: - raise NetworkError('Bad Gateway') - try: message = _parse(error.read()) + + if errcode in (401, 403): + raise Unauthorized() + elif errcode == 400: + raise BadRequest(message) + elif errcode == 502: + raise NetworkError('Bad Gateway') except ValueError: message = 'Unknown HTTPError {0}'.format(error.getcode()) diff --git a/telegram/venue.py b/telegram/venue.py index 12550ce8e..d059cb8f0 100644 --- a/telegram/venue.py +++ b/telegram/venue.py @@ -32,7 +32,7 @@ class Venue(TelegramObject): foursquare_id (Optional[str]): """ - def __init__(self, location, title, address, foursquare_id=None): + def __init__(self, location, title, address, foursquare_id=None, **kwargs): # Required self.location = location self.title = title diff --git a/tests/base.py b/tests/base.py index 6b9824df5..4d8bbb4ae 100644 --- a/tests/base.py +++ b/tests/base.py @@ -40,6 +40,8 @@ class BaseTest(object): '133505823:AAHZFMHno3mzVLErU5b5jJvaeG--qUyLyG0')) chat_id = os.environ.get('CHAT_ID', '12173560') + self._group_id = os.environ.get('GROUP_ID', '-49740850') + self._channel_id = os.environ.get('CHANNEL_ID', '@pythontelegrambottests') self._bot = bot self._chat_id = chat_id diff --git a/tests/test_bot.py b/tests/test_bot.py index aac104d94..db26e330f 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -45,11 +45,7 @@ class BotTest(BaseTest, unittest.TestCase): bot = self._bot.getMe() self.assertTrue(self.is_json(bot.to_json())) - self.assertEqual(bot.id, 133505823) - self.assertEqual(bot.first_name, 'PythonTelegramBot') - self.assertEqual(bot.last_name, '') - self.assertEqual(bot.username, 'PythonTelegramBot') - self.assertEqual(bot.name, '@PythonTelegramBot') + self._testUserEqualsBot(bot) @flaky(3, 1) @timeout(10) @@ -75,7 +71,7 @@ class BotTest(BaseTest, unittest.TestCase): @flaky(3, 1) @timeout(10) def testGetUpdates(self): - updates = self._bot.getUpdates() + updates = self._bot.getUpdates(timeout=1) if updates: self.assertTrue(self.is_json(updates[0].to_json())) @@ -212,6 +208,63 @@ class BotTest(BaseTest, unittest.TestCase): bot.getMe() + @flaky(3, 1) + @timeout(10) + def testLeaveChat(self): + with self.assertRaisesRegexp(telegram.error.BadRequest, 'Chat not found'): + chat = self._bot.leaveChat(-123456) + + with self.assertRaisesRegexp(telegram.error.NetworkError, 'Chat not found'): + chat = self._bot.leaveChat(-123456) + + @flaky(3, 1) + @timeout(10) + def testGetChat(self): + chat = self._bot.getChat(self._group_id) + + self.assertTrue(self.is_json(chat.to_json())) + self.assertEqual(chat.type, "group") + self.assertEqual(chat.title, ">>> telegram.Bot() - Developers") + self.assertEqual(chat.id, int(self._group_id)) + + @flaky(3, 1) + @timeout(10) + def testGetChatAdministrators(self): + admins = self._bot.getChatAdministrators(self._channel_id) + self.assertTrue(isinstance(admins, list)) + self.assertTrue(self.is_json(admins[0].to_json())) + + for a in admins: + self.assertTrue(a.status in ("administrator", "creator")) + + bot = [a.user for a in admins if a.user.id == 133505823][0] + self._testUserEqualsBot(bot) + + @flaky(3, 1) + @timeout(10) + def testGetChatMembersCount(self): + count = self._bot.getChatMembersCount(self._channel_id) + self.assertTrue(isinstance(count, int)) + self.assertTrue(count > 3) + + @flaky(3, 1) + @timeout(10) + def testGetChatMember(self): + chat_member = self._bot.getChatMember(self._channel_id, 133505823) + bot = chat_member.user + + self.assertTrue(self.is_json(chat_member.to_json())) + self.assertEqual(chat_member.status, "administrator") + self._testUserEqualsBot(bot) + + def _testUserEqualsBot(self, user): + """Tests if user is our trusty @PythonTelegramBot.""" + self.assertEqual(user.id, 133505823) + self.assertEqual(user.first_name, 'PythonTelegramBot') + self.assertEqual(user.last_name, '') + self.assertEqual(user.username, 'PythonTelegramBot') + self.assertEqual(user.name, '@PythonTelegramBot') + if __name__ == '__main__': unittest.main() diff --git a/tests/test_filters.py b/tests/test_filters.py index 58127fc3b..b023700af 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -26,7 +26,7 @@ from datetime import datetime sys.path.append('.') -from telegram import Update, Message, User, Chat +from telegram import Message, User, Chat from telegram.ext import Filters from tests.base import BaseTest @@ -36,119 +36,118 @@ class FiltersTest(BaseTest, unittest.TestCase): def setUp(self): self.message = Message(0, User(0, "Testuser"), datetime.now(), Chat(0, 'private')) - self.update = Update(0, message=self.message) def test_filters_text(self): self.message.text = 'test' - self.assertTrue(Filters.text(self.update)) + self.assertTrue(Filters.text(self.message)) self.message.text = '/test' - self.assertFalse(Filters.text(self.update)) + self.assertFalse(Filters.text(self.message)) def test_filters_command(self): self.message.text = 'test' - self.assertFalse(Filters.command(self.update)) + self.assertFalse(Filters.command(self.message)) self.message.text = '/test' - self.assertTrue(Filters.command(self.update)) + self.assertTrue(Filters.command(self.message)) def test_filters_audio(self): self.message.audio = 'test' - self.assertTrue(Filters.audio(self.update)) + self.assertTrue(Filters.audio(self.message)) self.message.audio = None - self.assertFalse(Filters.audio(self.update)) + self.assertFalse(Filters.audio(self.message)) def test_filters_document(self): self.message.document = 'test' - self.assertTrue(Filters.document(self.update)) + self.assertTrue(Filters.document(self.message)) self.message.document = None - self.assertFalse(Filters.document(self.update)) + self.assertFalse(Filters.document(self.message)) def test_filters_photo(self): self.message.photo = 'test' - self.assertTrue(Filters.photo(self.update)) + self.assertTrue(Filters.photo(self.message)) self.message.photo = None - self.assertFalse(Filters.photo(self.update)) + self.assertFalse(Filters.photo(self.message)) def test_filters_sticker(self): self.message.sticker = 'test' - self.assertTrue(Filters.sticker(self.update)) + self.assertTrue(Filters.sticker(self.message)) self.message.sticker = None - self.assertFalse(Filters.sticker(self.update)) + self.assertFalse(Filters.sticker(self.message)) def test_filters_video(self): self.message.video = 'test' - self.assertTrue(Filters.video(self.update)) + self.assertTrue(Filters.video(self.message)) self.message.video = None - self.assertFalse(Filters.video(self.update)) + self.assertFalse(Filters.video(self.message)) def test_filters_voice(self): self.message.voice = 'test' - self.assertTrue(Filters.voice(self.update)) + self.assertTrue(Filters.voice(self.message)) self.message.voice = None - self.assertFalse(Filters.voice(self.update)) + self.assertFalse(Filters.voice(self.message)) def test_filters_contact(self): self.message.contact = 'test' - self.assertTrue(Filters.contact(self.update)) + self.assertTrue(Filters.contact(self.message)) self.message.contact = None - self.assertFalse(Filters.contact(self.update)) + self.assertFalse(Filters.contact(self.message)) def test_filters_location(self): self.message.location = 'test' - self.assertTrue(Filters.location(self.update)) + self.assertTrue(Filters.location(self.message)) self.message.location = None - self.assertFalse(Filters.location(self.update)) + self.assertFalse(Filters.location(self.message)) def test_filters_venue(self): self.message.venue = 'test' - self.assertTrue(Filters.venue(self.update)) + self.assertTrue(Filters.venue(self.message)) self.message.venue = None - self.assertFalse(Filters.venue(self.update)) + self.assertFalse(Filters.venue(self.message)) def test_filters_status_update(self): - self.assertFalse(Filters.status_update(self.update)) + self.assertFalse(Filters.status_update(self.message)) self.message.new_chat_member = 'test' - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.new_chat_member = None self.message.left_chat_member = 'test' - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.left_chat_member = None self.message.new_chat_title = 'test' - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.new_chat_title = '' self.message.new_chat_photo = 'test' - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.new_chat_photo = None self.message.delete_chat_photo = True - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.delete_chat_photo = False self.message.group_chat_created = True - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.group_chat_created = False self.message.supergroup_chat_created = True - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.supergroup_chat_created = False self.message.migrate_to_chat_id = 100 - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.migrate_to_chat_id = 0 self.message.migrate_from_chat_id = 100 - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.migrate_from_chat_id = 0 self.message.channel_chat_created = True - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.channel_chat_created = False self.message.pinned_message = 'test' - self.assertTrue(Filters.status_update(self.update)) + self.assertTrue(Filters.status_update(self.message)) self.message.pinned_message = None diff --git a/tests/test_updater.py b/tests/test_updater.py index ab12d9bda..192416ba1 100644 --- a/tests/test_updater.py +++ b/tests/test_updater.py @@ -93,6 +93,10 @@ class UpdaterTest(BaseTest, unittest.TestCase): self.received_message = update.message.text self.message_count += 1 + def telegramHandlerEditedTest(self, bot, update): + self.received_message = update.edited_message.text + self.message_count += 1 + def telegramInlineHandlerTest(self, bot, update): self.received_message = (update.inline_query, update.chosen_inline_result) self.message_count += 1 @@ -157,6 +161,28 @@ class UpdaterTest(BaseTest, unittest.TestCase): sleep(.1) self.assertTrue(None is self.received_message) + def test_editedMessageHandler(self): + self._setup_updater('Test', edited=True) + d = self.updater.dispatcher + from telegram.ext import Filters + handler = MessageHandler([Filters.text], self.telegramHandlerEditedTest, allow_edited=True) + d.addHandler(handler) + self.updater.start_polling(0.01) + sleep(.1) + self.assertEqual(self.received_message, 'Test') + + # Remove handler + d.removeHandler(handler) + handler = MessageHandler([Filters.text], + self.telegramHandlerEditedTest, + allow_edited=False) + d.addHandler(handler) + self.reset() + + self.updater.bot.send_messages = 1 + sleep(.1) + self.assertTrue(None is self.received_message) + def test_addTelegramMessageHandlerMultipleMessages(self): self._setup_updater('Multiple', 100) self.updater.dispatcher.addHandler(MessageHandler([], self.telegramHandlerTest)) @@ -200,6 +226,25 @@ class UpdaterTest(BaseTest, unittest.TestCase): sleep(.1) self.assertTrue(None is self.received_message) + def test_editedCommandHandler(self): + self._setup_updater('/test', edited=True) + d = self.updater.dispatcher + handler = CommandHandler('test', self.telegramHandlerEditedTest, allow_edited=True) + d.addHandler(handler) + self.updater.start_polling(0.01) + sleep(.1) + self.assertEqual(self.received_message, '/test') + + # Remove handler + d.removeHandler(handler) + handler = CommandHandler('test', self.telegramHandlerEditedTest, allow_edited=False) + d.addHandler(handler) + self.reset() + + self.updater.bot.send_messages = 1 + sleep(.1) + self.assertTrue(None is self.received_message) + def test_addRemoveStringRegexHandler(self): self._setup_updater('', messages=0) d = self.updater.dispatcher @@ -612,7 +657,8 @@ class MockBot(object): messages=1, raise_error=False, bootstrap_retries=None, - bootstrap_err=TelegramError('test')): + bootstrap_err=TelegramError('test'), + edited=False): self.text = text self.send_messages = messages self.raise_error = raise_error @@ -620,13 +666,18 @@ class MockBot(object): self.bootstrap_retries = bootstrap_retries self.bootstrap_attempts = 0 self.bootstrap_err = bootstrap_err + self.edited = edited - @staticmethod - def mockUpdate(text): + def mockUpdate(self, text): message = Message(0, None, None, None) message.text = text update = Update(0) - update.message = message + + if self.edited: + update.edited_message = message + else: + update.message = message + return update def setWebhook(self, webhook_url=None, certificate=None):