From 5a37af6f897c2f7464685df9d124fa1cd06a15ab Mon Sep 17 00:00:00 2001 From: Jacob Bom Date: Sat, 22 Jul 2017 13:34:51 +0200 Subject: [PATCH] Bot api 3.2 (#732) NOTE: Currently not testing StickerSet in terms of bot methods (interaction with telegrams servers) as there's no delete operations. --- docs/source/telegram.sticker.rst | 3 +- telegram/__init__.py | 5 +- telegram/bot.py | 220 ++++++++++++++++++++++++++++++- telegram/files/inputfile.py | 2 +- telegram/files/sticker.py | 160 +++++++++++++++++++--- tests/test_sticker.py | 90 +++++++++++++ 6 files changed, 455 insertions(+), 25 deletions(-) diff --git a/docs/source/telegram.sticker.rst b/docs/source/telegram.sticker.rst index 57cc94e13..2d4b7d44c 100644 --- a/docs/source/telegram.sticker.rst +++ b/docs/source/telegram.sticker.rst @@ -1,7 +1,6 @@ telegram.sticker module ======================= -.. automodule:: telegram.sticker +.. automodule:: telegram.files.sticker :members: - :undoc-members: :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index 6941124ff..d02e9f22a 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -27,7 +27,7 @@ from .files.photosize import PhotoSize from .files.audio import Audio from .files.voice import Voice from .files.document import Document -from .files.sticker import Sticker +from .files.sticker import Sticker, StickerSet, MaskPosition from .files.video import Video from .files.contact import Contact from .files.location import Location @@ -120,5 +120,6 @@ __all__ = [ 'MAX_FILESIZE_DOWNLOAD', 'MAX_FILESIZE_UPLOAD', 'MAX_MESSAGES_PER_SECOND_PER_CHAT', 'MAX_MESSAGES_PER_SECOND', 'MAX_MESSAGES_PER_MINUTE_PER_GROUP', 'WebhookInfo', 'Animation', 'Game', 'GameHighScore', 'VideoNote', 'LabeledPrice', 'SuccessfulPayment', 'ShippingOption', - 'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto' + 'ShippingAddress', 'PreCheckoutQuery', 'OrderInfo', 'Invoice', 'ShippingQuery', 'ChatPhoto', + 'StickerSet', 'MaskPosition' ] diff --git a/telegram/bot.py b/telegram/bot.py index 9e0880026..fc69906b9 100644 --- a/telegram/bot.py +++ b/telegram/bot.py @@ -26,7 +26,7 @@ import warnings from datetime import datetime from telegram import (User, Message, Update, Chat, ChatMember, UserProfilePhotos, File, - ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore) + ReplyMarkup, TelegramObject, WebhookInfo, GameHighScore, StickerSet) from telegram.error import InvalidToken, TelegramError from telegram.utils.helpers import to_timestamp from telegram.utils.request import Request @@ -2369,6 +2369,218 @@ class Bot(TelegramObject): return result + def get_sticker_set(self, name, timeout=None, **kwargs): + """ + Use this method to get a sticker set. + + Args: + name (:obj:`str`): Short name of the sticker set that is used in t.me/addstickers/ + URLs (e.g., animals) + 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.StickerSet` + + Raises: + :class:`telegram.TelegramError` + """ + + url = '{0}/getStickerSet'.format(self.base_url) + + data = {'name': name} + + result = self._request.post(url, data, timeout=timeout) + + return StickerSet.de_json(result, self) + + def upload_sticker_file(self, user_id, png_sticker, timeout=None, **kwargs): + """ + Use this method to upload a .png file with a sticker for later use in + :attr:`create_new_sticker_set` and :attr:`add_sticker_to_set` methods (can be used multiple + times). + + Note: + The png_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 sticker file owner. + png_sticker (:obj:`str` | `filelike object`): 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. + 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.File`: The uploaded File + + Raises: + :class:`telegram.TelegramError` + """ + + url = '{0}/uploadStickerFile'.format(self.base_url) + + data = {'user_id': user_id, 'png_sticker': png_sticker} + + result = self._request.post(url, data, timeout=timeout) + + return File.de_json(result, self) + + def create_new_sticker_set(self, user_id, name, title, png_sticker, emojis, is_masks=None, + mask_position=None, timeout=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. + + Note: + The png_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`): Short name of sticker set, to be used in t.me/addstickers/ URLs + (e.g., animals). Can contain only english letters, digits and underscores. + Must begin with a letter, can't contain consecutive underscores and + 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, + 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. + emojis (:obj:`str`): One or more emoji corresponding to the sticker. + is_masks (:obj:`bool`, optional): Pass True, if a set of mask stickers should be + created. + mask_position (:class:`telegram.MaskPosition`, optional): Position where the mask + should be placed on faces. + 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 = '{0}/createNewStickerSet'.format(self.base_url) + + data = {'user_id': user_id, 'name': name, 'title': title, 'png_sticker': png_sticker, + 'emojis': emojis} + + if is_masks is not None: + data['is_masks'] = is_masks + if mask_position is not None: + data['mask_position'] = mask_position + + result = self._request.post(url, data, timeout=timeout) + + return result + + def add_sticker_to_set(self, user_id, name, png_sticker, emojis, mask_position=None, + timeout=None, **kwargs): + """ + Use this method to add a new sticker to a set created by the bot. + + Note: + The png_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, + 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. + 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. + 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 = '{0}/addStickerToSet'.format(self.base_url) + + data = {'user_id': user_id, 'name': name, 'png_sticker': png_sticker, 'emojis': emojis} + + if mask_position is not None: + data['mask_position'] = mask_position + + result = self._request.post(url, data, timeout=timeout) + + return result + + def set_sticker_position_in_set(self, sticker, position, timeout=None, **kwargs): + """ + Use this method to move a sticker in a set created by the bot to a specific position. + + Args: + sticker (:obj:`str`): File identifier of the sticker. + position (:obj:`int`): New sticker position in the set, zero-based. + 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 = '{0}/setStickerPositionInSet'.format(self.base_url) + + data = {'sticker': sticker, 'position': position} + + result = self._request.post(url, data, timeout=timeout) + + return result + + def delete_sticker_from_set(self, sticker, timeout=None, **kwargs): + """ + Use this method to delete a sticker from a set created by the bot. + + Args: + sticker (:obj:`str`): File identifier of the sticker. + 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 = '{0}/deleteStickerFromSet'.format(self.base_url) + + data = {'sticker': sticker} + + result = self._request.post(url, data, timeout=timeout) + + return result + @staticmethod def de_json(data, bot): data = super(Bot, Bot).de_json(data, bot) @@ -2436,3 +2648,9 @@ class Bot(TelegramObject): setChatDescription = set_chat_description pinChatMessage = pin_chat_message unpinChatMessage = unpin_chat_message + getStickerSet = get_sticker_set + uploadStickerFile = upload_sticker_file + createNewStickerSet = create_new_sticker_set + addStickerToSet = add_sticker_to_set + setStickerPositionInSet = set_sticker_position_in_set + deleteStickerFromSet = delete_sticker_from_set diff --git a/telegram/files/inputfile.py b/telegram/files/inputfile.py index 0a46c21be..86d728917 100644 --- a/telegram/files/inputfile.py +++ b/telegram/files/inputfile.py @@ -36,7 +36,7 @@ from telegram import TelegramError DEFAULT_MIME_TYPE = 'application/octet-stream' USER_AGENT = 'Python Telegram Bot (https://github.com/python-telegram-bot/python-telegram-bot)' FILE_TYPES = ('audio', 'document', 'photo', 'sticker', 'video', 'voice', 'certificate', - 'video_note') + 'video_note', 'png_sticker') class InputFile(object): diff --git a/telegram/files/sticker.py b/telegram/files/sticker.py index 3ad17d906..daeaf78c0 100644 --- a/telegram/files/sticker.py +++ b/telegram/files/sticker.py @@ -16,7 +16,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 an object that represents a Telegram Sticker.""" +"""This module contains objects that represents stickers.""" from telegram import PhotoSize, TelegramObject @@ -25,26 +25,42 @@ class Sticker(TelegramObject): """This object represents a Telegram Sticker. Attributes: - file_id (str): - width (int): - height (int): - thumb (:class:`telegram.PhotoSize`): - emoji (str): - file_size (int): + file_id (:obj:`str`): Unique identifier for this file. + width (:obj:`int`): Sticker width. + height (:obj:`int`): Sticker height. + thumb (:class:`telegram.PhotoSize`): Optional. Sticker thumbnail in the .webp or .jpg + format. + emoji (:obj:`str`): Optional. Emoji associated with the sticker. + set_name (:obj:`str`): Optional. Name of the sticker set to which the sticker belongs. + mask_position (:class:`telegram.MaskPosition`): Optional. For mask stickers, the position + where the mask should be placed. + file_size (:obj:`int`): Optional. File size. Args: - file_id (str): - width (int): - height (int): - **kwargs: Arbitrary keyword arguments. - - Keyword Args: - thumb (Optional[:class:`telegram.PhotoSize`]): - emoji (Optional[str]): - file_size (Optional[int]): + file_id (:obj:`str`): Unique identifier for this file. + width (:obj:`int`): Sticker width. + height (:obj:`int`): Sticker height. + thumb (:class:`telegram.PhotoSize`, optional): Sticker thumbnail in the .webp or .jpg + format. + emoji (:obj:`str`, optional): Emoji associated with the sticker + set_name (:obj:`str`, optional): Name of the sticker set to which the sticker + belongs. + mask_position (:class:`telegram.MaskPosition`, optional): For mask stickers, the + position where the mask should be placed. + file_size (:obj:`int`, optional): File size. + **kwargs (obj:`dict`): Arbitrary keyword arguments. """ - def __init__(self, file_id, width, height, thumb=None, emoji=None, file_size=None, **kwargs): + def __init__(self, + file_id, + width, + height, + thumb=None, + emoji=None, + file_size=None, + set_name=None, + mask_position=None, + **kwargs): # Required self.file_id = str(file_id) self.width = int(width) @@ -53,6 +69,8 @@ class Sticker(TelegramObject): self.thumb = thumb self.emoji = emoji self.file_size = file_size + self.set_name = set_name + self.mask_position = mask_position self._id_attrs = (self.file_id,) @@ -60,11 +78,11 @@ class Sticker(TelegramObject): def de_json(data, bot): """ Args: - data (dict): + data (:obj:`dict`): bot (telegram.Bot): Returns: - telegram.Sticker: + :obj:`telegram.Sticker` """ if not data: return None @@ -72,5 +90,109 @@ class Sticker(TelegramObject): data = super(Sticker, Sticker).de_json(data, bot) data['thumb'] = PhotoSize.de_json(data.get('thumb'), bot) + data['mask_position'] = MaskPosition.de_json(data.get('mask_position'), bot) return Sticker(**data) + + @staticmethod + def de_list(data, bot): + if not data: + return list() + + return [Sticker.de_json(d, bot) for d in data] + + +class StickerSet(TelegramObject): + """ + This object represents a sticker set. + + Attributes: + name (:obj:`str`): Sticker set name. + title (:obj:`str`): Sticker set title. + is_masks (:obj:`bool`): True, if the sticker set contains masks. + stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + + Args: + name (:obj:`str`): Sticker set name. + title (:obj:`str`): Sticker set title. + is_masks (:obj:`bool`): True, if the sticker set contains masks. + stickers (List[:class:`telegram.Sticker`]): List of all set stickers. + """ + + def __init__(self, name, title, contains_masks, stickers, bot=None, **kwargs): + # TODO: telegrams docs claim contains_masks is called is_masks + # remove these lines or change once we get answer from support + self.name = name + self.title = title + self.contains_masks = contains_masks + self.stickers = stickers + + self._id_attrs = (self.name,) + + @staticmethod + def de_json(data, bot): + if not data: + return None + + data = super(StickerSet, StickerSet).de_json(data, bot) + + data['stickers'] = Sticker.de_list(data.get('stickers'), bot) + + return StickerSet(bot=bot, **data) + + def to_dict(self): + data = super(StickerSet, self).to_dict() + + data['stickers'] = [s.to_dict() for s in data.get('stickers')] + + return data + + +class MaskPosition(TelegramObject): + """ + This object describes the position on faces where a mask should be placed by default. + + Attributes: + point (:obj:`str`): The part of the face relative to which the mask should be placed. + x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face + size, from left to right. + y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face + size, from top to bottom. + zoom (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size. + + Notes: + :attr:`type` should be one of the following: `forehead`, `eyes`, `mouth` or `chin`. You can + use the classconstants for those. + + Args: + point (:obj:`str`): The part of the face relative to which the mask should be placed. + x_shift (:obj:`float`): Shift by X-axis measured in widths of the mask scaled to the face + size, from left to right. For example, choosing -1.0 will place mask just to the left + of the default mask position. + y_shift (:obj:`float`): Shift by Y-axis measured in heights of the mask scaled to the face + size, from top to bottom. For example, 1.0 will place the mask just below the default + mask position. + zoom (:obj:`float`): Mask scaling coefficient. For example, 2.0 means double size. + """ + + FOREHEAD = 'forehead' + """:obj:`str`: 'forehead'""" + EYES = 'eyes' + """:obj:`str`: 'eyes'""" + MOUTH = 'mouth' + """:obj:`str`: 'mouth'""" + CHIN = 'chin' + """:obj:`str`: 'chin'""" + + def __init__(self, point, x_shift, y_shift, zoom, **kwargs): + self.point = point + self.x_shift = x_shift + self.y_shift = y_shift + self.zoom = zoom + + @staticmethod + def de_json(data, bot): + if data is None: + return None + + return MaskPosition(**data) diff --git a/tests/test_sticker.py b/tests/test_sticker.py index f4c7832f7..3032289b7 100644 --- a/tests/test_sticker.py +++ b/tests/test_sticker.py @@ -226,5 +226,95 @@ class StickerTest(BaseTest, unittest.TestCase): self.assertNotEqual(hash(a), hash(e)) +class TestStickerSet(BaseTest, unittest.TestCase): + # TODO: Implement bot tests for StickerSet + # It's hard to test creation when we can't delete sticker sets + def setUp(self): + self.name = 'test_by_{0}'.format(self._bot.username) + self.title = 'Test stickers' + self.contains_masks = False + self.stickers = [telegram.Sticker('file_id', 512, 512)] + + self.json_dict = { + 'name': self.name, + 'title': self.title, + 'contains_masks': self.contains_masks, + 'stickers': [x.to_dict() for x in self.stickers] + } + + def test_sticker_set_de_json(self): + sticker_set = telegram.StickerSet.de_json(self.json_dict, self._bot) + + self.assertEqual(sticker_set.name, self.name) + self.assertEqual(sticker_set.title, self.title) + self.assertEqual(sticker_set.contains_masks, self.contains_masks) + self.assertEqual(sticker_set.stickers, self.stickers) + + def test_sticker_set_to_json(self): + sticker_set = telegram.StickerSet.de_json(self.json_dict, self._bot) + + self.assertTrue(self.is_json(sticker_set.to_json())) + + def test_sticker_set_to_dict(self): + sticker_set = telegram.StickerSet.de_json(self.json_dict, self._bot).to_dict() + + self.assertTrue(self.is_dict(sticker_set)) + self.assertDictEqual(self.json_dict, sticker_set) + + def test_equality(self): + a = telegram.StickerSet(self.name, self.title, self.contains_masks, self.stickers) + b = telegram.StickerSet(self.name, self.title, self.contains_masks, self.stickers) + c = telegram.StickerSet(self.name, None, None, None) + d = telegram.StickerSet('blah', self.title, self.contains_masks, self.stickers) + e = telegram.Audio(self.name, 0, None, None) + + self.assertEqual(a, b) + self.assertEqual(hash(a), hash(b)) + self.assertIsNot(a, b) + + self.assertEqual(a, c) + self.assertEqual(hash(a), hash(c)) + + self.assertNotEqual(a, d) + self.assertNotEqual(hash(a), hash(d)) + + self.assertNotEqual(a, e) + self.assertNotEqual(hash(a), hash(e)) + + +class TestMaskPosition(BaseTest, unittest.TestCase): + def setUp(self): + self.point = telegram.MaskPosition.EYES + self.x_shift = -1 + self.y_shift = 1 + self.zoom = 2 + + self.json_dict = { + 'point': self.point, + 'x_shift': self.x_shift, + 'y_shift': self.y_shift, + 'zoom': self.zoom + } + + def test_mask_position_de_json(self): + mask_position = telegram.MaskPosition.de_json(self.json_dict, self._bot) + + self.assertEqual(mask_position.point, self.point) + self.assertEqual(mask_position.x_shift, self.x_shift) + self.assertEqual(mask_position.y_shift, self.y_shift) + self.assertEqual(mask_position.zoom, self.zoom) + + def test_mask_positiont_to_json(self): + mask_position = telegram.MaskPosition.de_json(self.json_dict, self._bot) + + self.assertTrue(self.is_json(mask_position.to_json())) + + def test_mask_position_to_dict(self): + mask_position = telegram.MaskPosition.de_json(self.json_dict, self._bot).to_dict() + + self.assertTrue(self.is_dict(mask_position)) + self.assertDictEqual(self.json_dict, mask_position) + + if __name__ == '__main__': unittest.main()