From d63e710784bc3eb654525335e3f750cc32df5cef Mon Sep 17 00:00:00 2001 From: Bibo-Joshi Date: Fri, 10 Apr 2020 19:22:45 +0200 Subject: [PATCH] API 4.7 (#1858) * Pure API changes * Address review * set Bot.commands on successfull call of set_my_commands * Get started on tests * More tests! * More Coverage! * Reset changes in utils.request * Filters.dice, Filters.dice.text * more coverage * Address review * Address review * Test stop_poll with reply_markup * Test stop_poll also without reply_markup * Rephrase note on 'dice' * Fix grammar in note on Filters.dice * update api version readme * address review --- README.rst | 2 +- docs/source/telegram.botcommand.rst | 6 + docs/source/telegram.dice.rst | 6 + docs/source/telegram.rst | 2 + telegram/__init__.py | 5 +- telegram/bot.py | 247 +++++++++++++++++++++-- telegram/botcommand.py | 46 +++++ telegram/dice.py | 43 ++++ telegram/ext/filters.py | 48 ++++- telegram/files/sticker.py | 10 +- telegram/message.py | 28 ++- tests/data/sticker_set_thumb.png | Bin 0 -> 1789 bytes tests/data/telegram_animated_sticker.tgs | Bin 0 -> 19802 bytes tests/test_bot.py | 62 +++++- tests/test_botcommand.py | 48 +++++ tests/test_dice.py | 44 ++++ tests/test_filters.py | 18 +- tests/test_message.py | 27 ++- tests/test_sticker.py | 91 ++++++++- 19 files changed, 686 insertions(+), 47 deletions(-) create mode 100644 docs/source/telegram.botcommand.rst create mode 100644 docs/source/telegram.dice.rst create mode 100644 telegram/botcommand.py create mode 100644 telegram/dice.py create mode 100644 tests/data/sticker_set_thumb.png create mode 100644 tests/data/telegram_animated_sticker.tgs create mode 100644 tests/test_botcommand.py create mode 100644 tests/test_dice.py diff --git a/README.rst b/README.rst index dc4fcab7d..27f2113e9 100644 --- a/README.rst +++ b/README.rst @@ -93,7 +93,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **4.6** are supported. +All types and methods of the Telegram Bot API **4.7** are supported. ========== Installing diff --git a/docs/source/telegram.botcommand.rst b/docs/source/telegram.botcommand.rst new file mode 100644 index 000000000..9ceb402c5 --- /dev/null +++ b/docs/source/telegram.botcommand.rst @@ -0,0 +1,6 @@ +telegram.BotCommand +=================== + +.. autoclass:: telegram.BotCommand + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.dice.rst b/docs/source/telegram.dice.rst new file mode 100644 index 000000000..6e80f6d48 --- /dev/null +++ b/docs/source/telegram.dice.rst @@ -0,0 +1,6 @@ +telegram.Dice +============= + +.. autoclass:: telegram.Dice + :members: + :show-inheritance: \ No newline at end of file diff --git a/docs/source/telegram.rst b/docs/source/telegram.rst index 718308b9e..ddb87d773 100644 --- a/docs/source/telegram.rst +++ b/docs/source/telegram.rst @@ -9,6 +9,7 @@ telegram package telegram.animation telegram.audio telegram.bot + telegram.botcommand telegram.callbackquery telegram.chat telegram.chataction @@ -17,6 +18,7 @@ telegram package telegram.chatphoto telegram.constants telegram.contact + telegram.dice telegram.document telegram.error telegram.file diff --git a/telegram/__init__.py b/telegram/__init__.py index bd22c4804..50ea1027e 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -19,6 +19,7 @@ """A library that provides a Python interface to the Telegram Bot API""" from .base import TelegramObject +from .botcommand import BotCommand from .user import User from .files.chatphoto import ChatPhoto from .chat import Chat @@ -36,6 +37,7 @@ from .files.location import Location from .files.venue import Venue from .files.videonote import VideoNote from .chataction import ChatAction +from .dice import Dice from .userprofilephotos import UserProfilePhotos from .keyboardbutton import KeyboardButton from .keyboardbuttonpolltype import KeyboardButtonPollType @@ -157,5 +159,6 @@ __all__ = [ 'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError', 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile', 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll', - 'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType', + 'PollOption', 'PollAnswer', 'LoginUrl', 'KeyboardButton', 'KeyboardButtonPollType', 'Dice', + 'BotCommand' ] diff --git a/telegram/bot.py b/telegram/bot.py index 1589b9419..4c6cde5ee 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -39,7 +39,7 @@ from future.utils import string_types from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File, ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet, PhotoSize, Audio, Document, Sticker, Video, Animation, Voice, VideoNote, - Location, Venue, Contact, InputFile, Poll) + Location, Venue, Contact, InputFile, Poll, BotCommand) from telegram.error import InvalidToken, TelegramError from telegram.utils.helpers import to_timestamp, DEFAULT_NONE from telegram.utils.request import Request @@ -53,6 +53,9 @@ def info(func): if not self.bot: self.get_me() + if self._commands is None: + self.get_my_commands() + result = func(self, *args, **kwargs) return result @@ -141,6 +144,7 @@ class Bot(TelegramObject): self.base_url = str(base_url) + str(self.token) self.base_file_url = str(base_file_url) + str(self.token) self.bot = None + self._commands = None self._request = request or Request() self.logger = logging.getLogger(__name__) @@ -159,7 +163,7 @@ class Bot(TelegramObject): if reply_markup is not None: if isinstance(reply_markup, ReplyMarkup): - data['reply_markup'] = reply_markup.to_json() + data['reply_markup'] = reply_markup.to_dict() else: data['reply_markup'] = reply_markup @@ -251,6 +255,13 @@ class Bot(TelegramObject): return self.bot.supports_inline_queries + @property + @info + def commands(self): + """List[:class:`BotCommand`]: Bot's commands.""" + + return self._commands + @property def name(self): """:obj:`str`: Bot's @username.""" @@ -344,6 +355,8 @@ class Bot(TelegramObject): limitations: - A message can only be deleted if it was sent less than 48 hours ago. + - A dice message in a private chat can only be deleted if it was sent more than 24 + hours ago. - Bots can delete outgoing messages in private chats, groups, and supergroups. - Bots can delete incoming messages in private chats. - Bots granted can_post_messages permissions can delete outgoing messages in channels. @@ -3308,15 +3321,23 @@ class Bot(TelegramObject): return File.de_json(result, self) @log - def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis, - contains_masks=None, mask_position=None, timeout=20, **kwargs): + def create_new_sticker_set(self, user_id, name, title, emojis, png_sticker=None, + contains_masks=None, mask_position=None, timeout=20, + tgs_sticker=None, **kwargs): """Use this method to create new sticker set owned by a user. The bot will be able to edit the created sticker set. + You must use exactly one of the fields :attr:`png_sticker` or :attr:`tgs_sticker`. + + Warning: + As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the + arguments had to be changed. Use keyword arguments to make sure that the arguments are + passed correctly. + Note: - The png_sticker argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` + The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from + disk ``open(filename, 'rb')`` Args: user_id (:obj:`int`): User identifier of created sticker set owner. @@ -3326,12 +3347,16 @@ class Bot(TelegramObject): must end in "_by_". is case insensitive. 1-64 characters. title (:obj:`str`): Sticker set title, 1-64 characters. - png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up - to 512 kilobytes in size, dimensions must not exceed 512px, + png_sticker (:obj:`str` | `filelike object`, optional): Png image with the sticker, + must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + tgs_sticker (:obj:`str` | `filelike object`, optional): TGS animation with the sticker, + uploaded using multipart/form-data. See + https://core.telegram.org/animated_stickers#technical-requirements for technical + requirements emojis (:obj:`str`): One or more emoji corresponding to the sticker. contains_masks (:obj:`bool`, optional): Pass True, if a set of mask stickers should be created. @@ -3354,13 +3379,19 @@ class Bot(TelegramObject): if InputFile.is_file(png_sticker): png_sticker = InputFile(png_sticker) - data = {'user_id': user_id, 'name': name, 'title': title, 'png_sticker': png_sticker, - 'emojis': emojis} + if InputFile.is_file(tgs_sticker): + tgs_sticker = InputFile(tgs_sticker) + data = {'user_id': user_id, 'name': name, 'title': title, 'emojis': emojis} + + if png_sticker is not None: + data['png_sticker'] = png_sticker + if tgs_sticker is not None: + data['tgs_sticker'] = tgs_sticker if contains_masks is not None: data['contains_masks'] = contains_masks if mask_position is not None: - data['mask_position'] = mask_position + data['mask_position'] = mask_position.to_json() data.update(kwargs) result = self._request.post(url, data, timeout=timeout) @@ -3368,23 +3399,35 @@ class Bot(TelegramObject): return result @log - def add_sticker_to_set(self, user_id, name, png_sticker, emojis, mask_position=None, - timeout=20, **kwargs): - """Use this method to add a new sticker to a set created by the bot. + def add_sticker_to_set(self, user_id, name, emojis, png_sticker=None, mask_position=None, + timeout=20, tgs_sticker=None, **kwargs): + """Use this method to add a new sticker to a set created by the bot. You must use exactly + one of the fields png_sticker or tgs_sticker. Animated stickers can be added to animated + sticker sets and only to them. Animated sticker sets can have up to 50 stickers. Static + sticker sets can have up to 120 stickers. + + Warning: + As of API 4.7 ``png_sticker`` is an optional argument and therefore the order of the + arguments had to be changed. Use keyword arguments to make sure that the arguments are + passed correctly. Note: - The png_sticker argument can be either a file_id, an URL or a file from disk - ``open(filename, 'rb')`` + The png_sticker and tgs_sticker argument can be either a file_id, an URL or a file from + disk ``open(filename, 'rb')`` Args: user_id (:obj:`int`): User identifier of created sticker set owner. name (:obj:`str`): Sticker set name. - png_sticker (:obj:`str` | `filelike object`): Png image with the sticker, must be up - to 512 kilobytes in size, dimensions must not exceed 512px, + png_sticker (:obj:`str` | `filelike object`, optional): Png image with the sticker, + must be up to 512 kilobytes in size, dimensions must not exceed 512px, and either width or height must be exactly 512px. Pass a file_id as a String to send a file that already exists on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from the Internet, or upload a new one using multipart/form-data. + tgs_sticker (:obj:`str` | `filelike object`, optional): TGS animation with the sticker, + uploaded using multipart/form-data. See + https://core.telegram.org/animated_stickers#technical-requirements for technical + requirements emojis (:obj:`str`): One or more emoji corresponding to the sticker. mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask should beplaced on faces. @@ -3405,10 +3448,17 @@ class Bot(TelegramObject): if InputFile.is_file(png_sticker): png_sticker = InputFile(png_sticker) - data = {'user_id': user_id, 'name': name, 'png_sticker': png_sticker, 'emojis': emojis} + if InputFile.is_file(tgs_sticker): + tgs_sticker = InputFile(tgs_sticker) + data = {'user_id': user_id, 'name': name, 'emojis': emojis} + + if png_sticker is not None: + data['png_sticker'] = png_sticker + if tgs_sticker is not None: + data['tgs_sticker'] = tgs_sticker if mask_position is not None: - data['mask_position'] = mask_position + data['mask_position'] = mask_position.to_json() data.update(kwargs) result = self._request.post(url, data, timeout=timeout) @@ -3470,6 +3520,48 @@ class Bot(TelegramObject): return result + @log + def set_sticker_set_thumb(self, name, user_id, thumb=None, timeout=None, **kwargs): + """Use this method to set the thumbnail of a sticker set. Animated thumbnails can be set + for animated sticker sets only. + + Note: + The thumb can be either a file_id, an URL or a file from disk ``open(filename, 'rb')`` + + Args: + name (:obj:`str`): Sticker set name + user_id (:obj:`int`): User identifier of created sticker set owner. + thumb (:obj:`str` | `filelike object`, optional): A PNG image with the thumbnail, must + be up to 128 kilobytes in size and have width and height exactly 100px, or a TGS + animation with the thumbnail up to 32 kilobytes in size; see + https://core.telegram.org/animated_stickers#technical-requirements for animated sticker + technical requirements. Pass a file_id as a String to send a file that already exists + on the Telegram servers, pass an HTTP URL as a String for Telegram to get a file from + the Internet, or upload a new one using multipart/form-data. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during + creation of the connection pool). + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Returns: + :obj:`bool`: On success, ``True`` is returned. + + Raises: + :class:`telegram.TelegramError` + + """ + url = '{}/setStickerSetThumb'.format(self.base_url) + + if InputFile.is_file(thumb): + thumb = InputFile(thumb) + + data = {'name': name, 'user_id': user_id, 'thumb': thumb} + data.update(kwargs) + + result = self._request.post(url, data, timeout=timeout) + + return result + @log def set_passport_data_errors(self, user_id, errors, timeout=None, **kwargs): """ @@ -3621,7 +3713,7 @@ class Bot(TelegramObject): if reply_markup: if isinstance(reply_markup, ReplyMarkup): - data['reply_markup'] = reply_markup.to_json() + data['reply_markup'] = reply_markup.to_dict() else: data['reply_markup'] = reply_markup @@ -3629,6 +3721,111 @@ class Bot(TelegramObject): return Poll.de_json(result, self) + @log + def send_dice(self, + chat_id, + disable_notification=None, + reply_to_message_id=None, + reply_markup=None, + timeout=None, + **kwargs): + """ + Use this method to send a dice, which will have a random value from 1 to 6. On success, the + sent Message is returned. + + Args: + chat_id (:obj:`int` | :obj:`str`): Unique identifier for the target private chat. + disable_notification (:obj:`bool`, optional): Sends the message silently. Users will + receive a notification with no sound. + reply_to_message_id (:obj:`int`, optional): If the message is a reply, ID of the + original message. + reply_markup (:class:`telegram.ReplyMarkup`, optional): Additional interface options. A + JSON-serialized object for an inline keyboard, custom reply keyboard, instructions + to remove reply keyboard or to force a reply from the user. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Returns: + :class:`telegram.Message`: On success, the sent Message is returned. + + Raises: + :class:`telegram.TelegramError` + + """ + url = '{0}/sendDice'.format(self.base_url) + + data = { + 'chat_id': chat_id, + } + + return self._message(url, data, timeout=timeout, disable_notification=disable_notification, + reply_to_message_id=reply_to_message_id, reply_markup=reply_markup, + **kwargs) + + @log + def get_my_commands(self, timeout=None, **kwargs): + """ + Use this method to get the current list of the bot's commands. + + Args: + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Returns: + List[:class:`telegram.BotCommand]`: On success, the commands set for the bot + + Raises: + :class:`telegram.TelegramError` + + """ + url = '{0}/getMyCommands'.format(self.base_url) + + result = self._request.get(url, timeout=timeout) + + self._commands = [BotCommand.de_json(c, self) for c in result] + + return self._commands + + @log + def set_my_commands(self, commands, timeout=None, **kwargs): + """ + Use this method to change the list of the bot's commands. + + Args: + commands (List[:class:`BotCommand` | (:obj:`str`, :obj:`str`)]): A JSON-serialized list + of bot commands to be set as the list of the bot's commands. At most 100 commands + can be specified. + timeout (:obj:`int` | :obj:`float`, optional): If this value is specified, use it as + the read timeout from the server (instead of the one specified during creation of + the connection pool). + **kwargs (:obj:`dict`): Arbitrary keyword arguments. + + Returns: + :obj:`True`: On success + + Raises: + :class:`telegram.TelegramError` + + """ + url = '{0}/setMyCommands'.format(self.base_url) + + cmds = [c if isinstance(c, BotCommand) else BotCommand(c[0], c[1]) for c in commands] + + data = {'commands': [c.to_dict() for c in cmds]} + data.update(kwargs) + + result = self._request.post(url, data, timeout=timeout) + + # Set commands. No need to check for outcome. + # If request failed, we won't come this far + self._commands = commands + + return result + def to_dict(self): data = {'id': self.id, 'username': self.username, 'first_name': self.first_name} @@ -3768,9 +3965,17 @@ class Bot(TelegramObject): """Alias for :attr:`set_sticker_position_in_set`""" deleteStickerFromSet = delete_sticker_from_set """Alias for :attr:`delete_sticker_from_set`""" + setStickerSetThumb = set_sticker_set_thumb + """Alias for :attr:`set_sticker_set_thumb`""" setPassportDataErrors = set_passport_data_errors """Alias for :attr:`set_passport_data_errors`""" sendPoll = send_poll """Alias for :attr:`send_poll`""" stopPoll = stop_poll """Alias for :attr:`stop_poll`""" + sendDice = send_dice + """Alias for :attr:`send_dice`""" + getMyCommands = get_my_commands + """Alias for :attr:`get_my_commands`""" + setMyCommands = set_my_commands + """Alias for :attr:`set_my_commands`""" diff --git a/telegram/botcommand.py b/telegram/botcommand.py new file mode 100644 index 000000000..293a5035c --- /dev/null +++ b/telegram/botcommand.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# 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 an object that represents a Telegram Bot Command.""" +from telegram import TelegramObject + + +class BotCommand(TelegramObject): + """ + This object represents a bot command. + + Attributes: + command (:obj:`str`): Text of the command. + description (:obj:`str`): Description of the command. + + Args: + command (:obj:`str`): Text of the command, 1-32 characters. Can contain only lowercase + English letters, digits and underscores. + description (:obj:`str`): Description of the command, 3-256 characters. + """ + def __init__(self, command, description, **kwargs): + self.command = command + self.description = description + + @classmethod + def de_json(cls, data, bot): + if not data: + return None + + return cls(**data) diff --git a/telegram/dice.py b/telegram/dice.py new file mode 100644 index 000000000..b90aeb363 --- /dev/null +++ b/telegram/dice.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# 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 an object that represents a Telegram Dice.""" +from telegram import TelegramObject + + +class Dice(TelegramObject): + """ + This object represents a dice with random value from 1 to 6. (The singular form of "dice" is + "die". However, PTB mimics the Telegram API, which uses the term "dice".) + + Attributes: + value (:obj:`int`): Value of the dice. + + Args: + value (:obj:`int`): Value of the dice, 1-6. + """ + def __init__(self, value, **kwargs): + self.value = value + + @classmethod + def de_json(cls, data, bot): + if not data: + return None + + return cls(**data) diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index c02e88822..1b464eb39 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -272,6 +272,10 @@ class Filters(object): ... MessageHandler(Filters.text(buttons), callback_method) + Note: + Dice messages don't have text. If you want to filter either text or dice messages, use + ``Filters.text | Filters.dice``. + Args: update (List[:obj:`str`] | Tuple[:obj:`str`], optional): Which messages to allow. Only exact matches are allowed. If not specified, will allow any text message. @@ -427,7 +431,7 @@ class Filters(object): send media with wrong types that don't fit to this handler. Example: - Filters.documents.category('audio/') returnes `True` for all types + Filters.documents.category('audio/') returns `True` for all types of audio sent as file, for example 'audio/mpeg' or 'audio/x-wav' """ @@ -957,6 +961,48 @@ officedocument.wordprocessingml.document")``- poll = _Poll() """Messages that contain a :class:`telegram.Poll`.""" + class _Dice(BaseFilter): + name = 'Filters.dice' + + class _DiceValues(BaseFilter): + + def __init__(self, values): + self.values = [values] if isinstance(values, int) else values + self.name = 'Filters.dice({})'.format(values) + + def filter(self, message): + return bool(message.dice and message.dice.value in self.values) + + def __call__(self, update): + if isinstance(update, Update): + return self.filter(update.effective_message) + else: + return self._DiceValues(update) + + def filter(self, message): + return bool(message.dice) + + dice = _Dice() + """Dice Messages. If an integer or a list of integers is passed, it filters messages to only + allow those whose dice value is appearing in the given list. + + Examples: + To allow any dice message, simply use + ``MessageHandler(Filters.dice, callback_method)``. + To allow only dice with value 6, use + ``MessageHandler(Filters.dice(6), callback_method)``. + To allow only dice with value 5 `or` 6, use + ``MessageHandler(Filters.dice([5, 6]), callback_method)``. + + Args: + update (:obj:`int` | List[:obj:`int`], optional): Which values to allow. If not + specified, will allow any dice message. + + Note: + Dice messages don't have text. If you want to filter either text or dice messages, use + ``Filters.text | Filters.dice``. + """ + class language(BaseFilter): """Filters messages to only allow those which are from users with a certain language code. diff --git a/telegram/files/sticker.py b/telegram/files/sticker.py index 16e08de7b..8249cbcbe 100644 --- a/telegram/files/sticker.py +++ b/telegram/files/sticker.py @@ -138,6 +138,8 @@ class StickerSet(TelegramObject): is_animated (:obj:`bool`): True, if the sticker set contains animated stickers. contains_masks (:obj:`bool`): True, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + thumb (:class:`telegram.PhotoSize`): Optional. Sticker set thumbnail in the .WEBP or .TGS + format Args: name (:obj:`str`): Sticker set name. @@ -145,15 +147,20 @@ class StickerSet(TelegramObject): is_animated (:obj:`bool`): True, if the sticker set contains animated stickers. contains_masks (:obj:`bool`): True, if the sticker set contains masks. stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + thumb (:class:`telegram.PhotoSize`, optional): Sticker set thumbnail in the .WEBP or .TGS + format """ - def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, **kwargs): + def __init__(self, name, title, is_animated, contains_masks, stickers, bot=None, thumb=None, + **kwargs): self.name = name self.title = title self.is_animated = is_animated self.contains_masks = contains_masks self.stickers = stickers + # Optionals + self.thumb = thumb self._id_attrs = (self.name,) @@ -164,6 +171,7 @@ class StickerSet(TelegramObject): data = super(StickerSet, StickerSet).de_json(data, bot) + data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) data['stickers'] = Sticker.de_list(data.get('stickers'), bot) return StickerSet(bot=bot, **data) diff --git a/telegram/message.py b/telegram/message.py index 76c698e90..399d8e8b3 100644 --- a/telegram/message.py +++ b/telegram/message.py @@ -23,7 +23,7 @@ from html import escape from telegram import (Animation, Audio, Contact, Document, Chat, Location, PhotoSize, Sticker, TelegramObject, User, Video, Voice, Venue, MessageEntity, Game, Invoice, - SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup) + SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup, Dice) from telegram import ParseMode from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp @@ -106,6 +106,7 @@ class Message(TelegramObject): passport_data (:class:`telegram.PassportData`): Optional. Telegram Passport data. poll (:class:`telegram.Poll`): Optional. Message is a native poll, information about the poll. + dice (:class:`telegram.Dice`): Optional. Message is a dice. reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached to the message. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. @@ -199,7 +200,7 @@ class Message(TelegramObject): smaller than 52 bits, so a signed 64 bit integer or double-precision float type are safe for storing this identifier. pinned_message (:class:`telegram.message`, optional): Specified message was pinned. Note - that the Message object in this field will not contain further attr:`reply_to_message` + that the Message object in this field will not contain further :attr:`reply_to_message` fields even if it is itself a reply. invoice (:class:`telegram.Invoice`, optional): Message is an invoice for a payment, information about the invoice. @@ -214,6 +215,7 @@ class Message(TelegramObject): passport_data (:class:`telegram.PassportData`, optional): Telegram Passport data. poll (:class:`telegram.Poll`, optional): Message is a native poll, information about the poll. + dice (:class:`telegram.Dice`, optional): Message is a dice with random value from 1 to 6. reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached to the message. login_url buttons are represented as ordinary url buttons. default_quote (:obj:`bool`, optional): Default setting for the `quote` parameter of the @@ -229,7 +231,7 @@ class Message(TelegramObject): MESSAGE_TYPES = ['text', 'new_chat_members', 'left_chat_member', 'new_chat_title', 'new_chat_photo', 'delete_chat_photo', 'group_chat_created', 'supergroup_chat_created', 'channel_chat_created', 'migrate_to_chat_id', - 'migrate_from_chat_id', 'pinned_message', + 'migrate_from_chat_id', 'pinned_message', 'poll', 'dice', 'passport_data'] + ATTACHMENT_TYPES def __init__(self, @@ -282,6 +284,7 @@ class Message(TelegramObject): reply_markup=None, bot=None, default_quote=None, + dice=None, **kwargs): # Required self.message_id = int(message_id) @@ -331,6 +334,7 @@ class Message(TelegramObject): self.animation = animation self.passport_data = passport_data self.poll = poll + self.dice = dice self.reply_markup = reply_markup self.bot = bot self.default_quote = default_quote @@ -404,6 +408,7 @@ class Message(TelegramObject): data['successful_payment'] = SuccessfulPayment.de_json(data.get('successful_payment'), bot) data['passport_data'] = PassportData.de_json(data.get('passport_data'), bot) data['poll'] = Poll.de_json(data.get('poll'), bot) + data['dice'] = Dice.de_json(data.get('dice'), bot) data['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot) return cls(bot=bot, **data) @@ -808,6 +813,23 @@ class Message(TelegramObject): self._quote(kwargs) return self.bot.send_poll(self.chat_id, *args, **kwargs) + def reply_dice(self, *args, **kwargs): + """Shortcut for:: + + bot.send_dice(update.message.chat_id, *args, **kwargs) + + Keyword Args: + quote (:obj:`bool`, optional): If set to ``True``, the dice is sent as an actual reply + to this message. If ``reply_to_message_id`` is passed in ``kwargs``, this parameter + will be ignored. Default: ``True`` in group chats and ``False`` in private chats. + + Returns: + :class:`telegram.Message`: On success, instance representing the message posted. + + """ + self._quote(kwargs) + return self.bot.send_dice(self.chat_id, *args, **kwargs) + def forward(self, chat_id, *args, **kwargs): """Shortcut for:: diff --git a/tests/data/sticker_set_thumb.png b/tests/data/sticker_set_thumb.png new file mode 100644 index 0000000000000000000000000000000000000000..063e8cc0ad39a1d60eacd71b991ded217c1ad0ae GIT binary patch literal 1789 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 literal 0 HcmV?d00001 diff --git a/tests/test_bot.py b/tests/test_bot.py index 78f166ece..ee81ed35c 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -26,7 +26,7 @@ from future.utils import string_types from telegram import (Bot, Update, ChatAction, TelegramError, User, InlineKeyboardMarkup, InlineKeyboardButton, InlineQueryResultArticle, InputTextMessageContent, - ShippingOption, LabeledPrice, ChatPermissions, Poll, + ShippingOption, LabeledPrice, ChatPermissions, Poll, BotCommand, InlineQueryResultDocument) from telegram.error import BadRequest, InvalidToken, NetworkError, RetryAfter from telegram.utils.helpers import from_timestamp, escape_markdown @@ -80,6 +80,7 @@ class TestBot(object): @pytest.mark.timeout(10) def test_get_me_and_properties(self, bot): get_me_bot = bot.get_me() + commands = bot.get_my_commands() assert isinstance(get_me_bot, User) assert get_me_bot.id == bot.id @@ -91,6 +92,7 @@ class TestBot(object): assert get_me_bot.can_read_all_group_messages == bot.can_read_all_group_messages assert get_me_bot.supports_inline_queries == bot.supports_inline_queries assert 'https://t.me/{}'.format(get_me_bot.username) == bot.link + assert commands == bot.commands @flaky(3, 1) @pytest.mark.timeout(10) @@ -174,7 +176,13 @@ class TestBot(object): @flaky(3, 1) @pytest.mark.timeout(10) - def test_send_and_stop_poll(self, bot, super_group_id): + @pytest.mark.parametrize('reply_markup', [ + None, + InlineKeyboardMarkup.from_button(InlineKeyboardButton(text='text', callback_data='data')), + InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text='text', callback_data='data')).to_dict() + ]) + def test_send_and_stop_poll(self, bot, super_group_id, reply_markup): question = 'Is this a test?' answers = ['Yes', 'No', 'Maybe'] message = bot.send_poll(chat_id=super_group_id, question=question, options=answers, @@ -190,7 +198,10 @@ class TestBot(object): assert not message.poll.is_closed assert message.poll.type == Poll.REGULAR - poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id, timeout=60) + # Since only the poll and not the complete message is returned, we can't check that the + # reply_markup is correct. So we just test that sending doesn't give an error. + poll = bot.stop_poll(chat_id=super_group_id, message_id=message.message_id, + reply_markup=reply_markup, timeout=60) assert isinstance(poll, Poll) assert poll.is_closed assert poll.options[0].text == answers[0] @@ -208,6 +219,13 @@ class TestBot(object): assert message_quiz.poll.type == Poll.QUIZ assert message_quiz.poll.is_closed + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_send_dice(self, bot, chat_id): + message = bot.send_dice(chat_id) + + assert message.dice + @flaky(3, 1) @pytest.mark.timeout(10) def test_send_chat_action(self, bot, chat_id): @@ -904,3 +922,41 @@ class TestBot(object): def test_send_message_default_quote(self, default_bot, chat_id): message = default_bot.send_message(chat_id, 'test') assert message.default_quote is True + + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_set_and_get_my_commands(self, bot): + commands = [ + BotCommand('cmd1', 'descr1'), + BotCommand('cmd2', 'descr2'), + ] + bot.set_my_commands([]) + assert bot.get_my_commands() == [] + assert bot.commands == [] + assert bot.set_my_commands(commands) + + for bc in [bot.get_my_commands(), bot.commands]: + assert len(bc) == 2 + assert bc[0].command == 'cmd1' + assert bc[0].description == 'descr1' + assert bc[1].command == 'cmd2' + assert bc[1].description == 'descr2' + + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_set_and_get_my_commands_strings(self, bot): + commands = [ + ['cmd1', 'descr1'], + ['cmd2', 'descr2'], + ] + bot.set_my_commands([]) + assert bot.get_my_commands() == [] + assert bot.commands == [] + assert bot.set_my_commands(commands) + + for bc in [bot.get_my_commands(), bot.commands]: + assert len(bc) == 2 + assert bc[0].command == 'cmd1' + assert bc[0].description == 'descr1' + assert bc[1].command == 'cmd2' + assert bc[1].description == 'descr2' diff --git a/tests/test_botcommand.py b/tests/test_botcommand.py new file mode 100644 index 000000000..9b339276d --- /dev/null +++ b/tests/test_botcommand.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# 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 pytest + +from telegram import BotCommand + + +@pytest.fixture(scope="class") +def bot_command(): + return BotCommand(command='start', description='A command') + + +class TestBotCommand(object): + command = 'start' + description = 'A command' + + def test_de_json(self, bot): + json_dict = {'command': self.command, 'description': self.description} + bot_command = BotCommand.de_json(json_dict, bot) + + assert bot_command.command == self.command + assert bot_command.description == self.description + + assert BotCommand.de_json(None, bot) is None + + def test_to_dict(self, bot_command): + bot_command_dict = bot_command.to_dict() + + assert isinstance(bot_command_dict, dict) + assert bot_command_dict['command'] == bot_command.command + assert bot_command_dict['description'] == bot_command.description diff --git a/tests/test_dice.py b/tests/test_dice.py new file mode 100644 index 000000000..89805f45f --- /dev/null +++ b/tests/test_dice.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2020 +# 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 pytest + +from telegram import Dice + + +@pytest.fixture(scope="class") +def dice(): + return Dice(value=5) + + +class TestDice(object): + value = 4 + + def test_de_json(self, bot): + json_dict = {'value': self.value} + dice = Dice.de_json(json_dict, bot) + + assert dice.value == self.value + assert Dice.de_json(None, bot) is None + + def test_to_dict(self, dice): + dice_dict = dice.to_dict() + + assert isinstance(dice_dict, dict) + assert dice_dict['value'] == dice.value diff --git a/tests/test_filters.py b/tests/test_filters.py index f081fed08..c89615899 100644 --- a/tests/test_filters.py +++ b/tests/test_filters.py @@ -20,7 +20,7 @@ import datetime import pytest -from telegram import Message, User, Chat, MessageEntity, Document, Update +from telegram import Message, User, Chat, MessageEntity, Document, Update, Dice from telegram.ext import Filters, BaseFilter import re @@ -622,6 +622,22 @@ class TestFilters(object): update.message.poll = 'test' assert Filters.poll(update) + def test_filters_dice(self, update): + update.message.dice = Dice(4) + assert Filters.dice(update) + update.message.dice = None + assert not Filters.dice(update) + + def test_filters_dice_iterable(self, update): + update.message.dice = None + assert not Filters.dice(5)(update) + + update.message.dice = Dice(5) + assert Filters.dice(5)(update) + assert Filters.dice({5, 6})(update) + assert not Filters.dice(1)(update) + assert not Filters.dice([2, 3])(update) + def test_language_filter_single(self, update): update.message.from_user.language_code = 'en_US' assert (Filters.language('en_US'))(update) diff --git a/tests/test_message.py b/tests/test_message.py index 3900e9768..c37f2ad81 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -22,7 +22,7 @@ import pytest from telegram import (Update, Message, User, MessageEntity, Chat, Audio, Document, Animation, Game, PhotoSize, Sticker, Video, Voice, VideoNote, Contact, Location, Venue, - Invoice, SuccessfulPayment, PassportData, ParseMode, Poll, PollOption) + Invoice, SuccessfulPayment, PassportData, ParseMode, Poll, PollOption, Dice) from tests.test_passport import RAW_PASSPORT_DATA @@ -97,7 +97,8 @@ def message(bot): 'text': 'start', 'url': 'http://google.com'}, { 'text': 'next', 'callback_data': 'abcd'}], [{'text': 'Cancel', 'callback_data': 'Cancel'}]]}}, - {'quote': True} + {'quote': True}, + {'dice': Dice(4)} ], ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'caption_entities', 'audio', 'document', 'animation', 'game', 'photo', @@ -107,7 +108,7 @@ def message(bot): 'migrated_from', 'pinned', 'invoice', 'successful_payment', 'connected_website', 'forward_signature', 'author_signature', 'photo_from_media_group', 'passport_data', 'poll', 'reply_markup', - 'default_quote']) + 'default_quote', 'dice']) def message_params(bot, request): return Message(message_id=TestMessage.id_, from_user=TestMessage.from_user, @@ -702,7 +703,7 @@ class TestMessage(object): def test_reply_poll(self, monkeypatch, message): def test(*args, **kwargs): id_ = args[0] == message.chat_id - contact = kwargs['contact'] == 'test_poll' + contact = kwargs['question'] == 'test_poll' if kwargs.get('reply_to_message_id'): reply = kwargs['reply_to_message_id'] == message.message_id else: @@ -710,8 +711,22 @@ class TestMessage(object): return id_ and contact and reply monkeypatch.setattr(message.bot, 'send_poll', test) - assert message.reply_poll(contact='test_poll') - assert message.reply_poll(contact='test_poll', quote=True) + assert message.reply_poll(question='test_poll') + assert message.reply_poll(question='test_poll', quote=True) + + def test_reply_dice(self, monkeypatch, message): + def test(*args, **kwargs): + id_ = args[0] == message.chat_id + contact = kwargs['disable_notification'] is True + if kwargs.get('reply_to_message_id'): + reply = kwargs['reply_to_message_id'] == message.message_id + else: + reply = True + return id_ and contact and reply + + monkeypatch.setattr(message.bot, 'send_dice', test) + assert message.reply_dice(disable_notification=True) + assert message.reply_dice(disable_notification=True, quote=True) def test_forward(self, monkeypatch, message): def test(*args, **kwargs): diff --git a/tests/test_sticker.py b/tests/test_sticker.py index 6bc844ffc..acd2b894a 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -40,6 +40,19 @@ def sticker(bot, chat_id): return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker +@pytest.fixture(scope='function') +def animated_sticker_file(): + f = open('tests/data/telegram_animated_sticker.tgs', 'rb') + yield f + f.close() + + +@pytest.fixture(scope='class') +def animated_sticker(bot, chat_id): + with open('tests/data/telegram_animated_sticker.tgs', 'rb') as f: + return bot.send_sticker(chat_id, sticker=f, timeout=50).sticker + + class TestSticker(object): # sticker_file_url = 'https://python-telegram-bot.org/static/testfiles/telegram.webp' # Serving sticker from gh since our server sends wrong content_type @@ -245,12 +258,27 @@ class TestSticker(object): @pytest.fixture(scope='function') def sticker_set(bot): - ss = bot.get_sticker_set('test_by_{0}'.format(bot.username)) + ss = bot.get_sticker_set('test_by_{}'.format(bot.username)) if len(ss.stickers) > 100: raise Exception('stickerset is growing too large.') return ss +@pytest.fixture(scope='function') +def animated_sticker_set(bot): + ss = bot.get_sticker_set('animated_test_by_{}'.format(bot.username)) + if len(ss.stickers) > 100: + raise Exception('stickerset is growing too large.') + return ss + + +@pytest.fixture(scope='function') +def sticker_set_thumb_file(): + f = open('tests/data/sticker_set_thumb.png', 'rb') + yield f + f.close() + + class TestStickerSet(object): title = 'Test stickers' is_animated = True @@ -258,14 +286,15 @@ class TestStickerSet(object): stickers = [Sticker('file_id', 'file_un_id', 512, 512, True)] name = 'NOTAREALNAME' - def test_de_json(self, bot): - name = 'test_by_{0}'.format(bot.username) + def test_de_json(self, bot, sticker): + name = 'test_by_{}'.format(bot.username) json_dict = { 'name': name, 'title': self.title, 'is_animated': self.is_animated, 'contains_masks': self.contains_masks, - 'stickers': [x.to_dict() for x in self.stickers] + 'stickers': [x.to_dict() for x in self.stickers], + 'thumb': sticker.thumb.to_dict() } sticker_set = StickerSet.de_json(json_dict, bot) @@ -274,15 +303,28 @@ class TestStickerSet(object): assert sticker_set.is_animated == self.is_animated assert sticker_set.contains_masks == self.contains_masks assert sticker_set.stickers == self.stickers + assert sticker_set.thumb == sticker.thumb @flaky(3, 1) @pytest.mark.timeout(10) - def test_bot_methods_1(self, bot, chat_id): + def test_bot_methods_1_png(self, bot, chat_id, sticker_file): with open('tests/data/telegram_sticker.png', 'rb') as f: file = bot.upload_sticker_file(95205500, f) assert file - assert bot.add_sticker_to_set(chat_id, 'test_by_{0}'.format(bot.username), - file.file_id, '😄') + assert bot.add_sticker_to_set(chat_id, 'test_by_{}'.format(bot.username), + png_sticker=file.file_id, emojis='😄') + # Also test with file input and mask + assert bot.add_sticker_to_set(chat_id, 'test_by_{}'.format(bot.username), + png_sticker=sticker_file, emojis='😄', + mask_position=MaskPosition(MaskPosition.EYES, -1, 1, 2)) + + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_bot_methods_1_tgs(self, bot, chat_id): + assert bot.add_sticker_to_set( + chat_id, 'animated_test_by_{}'.format(bot.username), + tgs_sticker=open('tests/data/telegram_animated_sticker.tgs', 'rb'), + emojis='😄') def test_sticker_set_to_dict(self, sticker_set): sticker_set_dict = sticker_set.to_dict() @@ -296,17 +338,48 @@ class TestStickerSet(object): @flaky(3, 1) @pytest.mark.timeout(10) - def test_bot_methods_2(self, bot, sticker_set): + def test_bot_methods_2_png(self, bot, sticker_set): file_id = sticker_set.stickers[0].file_id assert bot.set_sticker_position_in_set(file_id, 1) + @flaky(3, 1) + @pytest.mark.timeout(10) + def test_bot_methods_2_tgs(self, bot, animated_sticker_set): + file_id = animated_sticker_set.stickers[0].file_id + assert bot.set_sticker_position_in_set(file_id, 1) + @flaky(10, 1) @pytest.mark.timeout(10) - def test_bot_methods_3(self, bot, sticker_set): + def test_bot_methods_3_png(self, bot, chat_id, sticker_set_thumb_file): + sleep(1) + assert bot.set_sticker_set_thumb('test_by_{}'.format(bot.username), chat_id, + sticker_set_thumb_file) + + @flaky(10, 1) + @pytest.mark.timeout(10) + def test_bot_methods_3_tgs(self, bot, chat_id, animated_sticker_file, animated_sticker_set): + sleep(1) + assert bot.set_sticker_set_thumb('animated_test_by_{}'.format(bot.username), chat_id, + animated_sticker_file) + file_id = animated_sticker_set.stickers[-1].file_id + # also test with file input and mask + assert bot.set_sticker_set_thumb('animated_test_by_{}'.format(bot.username), chat_id, + file_id) + + @flaky(10, 1) + @pytest.mark.timeout(10) + def test_bot_methods_4_png(self, bot, sticker_set): sleep(1) file_id = sticker_set.stickers[-1].file_id assert bot.delete_sticker_from_set(file_id) + @flaky(10, 1) + @pytest.mark.timeout(10) + def test_bot_methods_4_tgs(self, bot, animated_sticker_set): + sleep(1) + file_id = animated_sticker_set.stickers[-1].file_id + assert bot.delete_sticker_from_set(file_id) + def test_get_file_instance_method(self, monkeypatch, sticker): def test(*args, **kwargs): return args[1] == sticker.file_id