diff --git a/telegram/__init__.py b/telegram/__init__.py index 15ff0439e..e9a3c5091 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -48,6 +48,7 @@ from .parsemode import ParseMode from .messageentity import MessageEntity from .games.game import Game from .poll import Poll, PollOption +from .loginurl import LoginUrl from .games.callbackgame import CallbackGame from .payment.shippingaddress import ShippingAddress from .payment.orderinfo import OrderInfo @@ -58,11 +59,11 @@ from .passport.passportfile import PassportFile from .passport.data import IdDocumentData, PersonalDetails, ResidentialAddress from .passport.encryptedpassportelement import EncryptedPassportElement from .passport.passportdata import PassportData +from .inline.inlinekeyboardbutton import InlineKeyboardButton +from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup from .message import Message from .callbackquery import CallbackQuery from .choseninlineresult import ChosenInlineResult -from .inline.inlinekeyboardbutton import InlineKeyboardButton -from .inline.inlinekeyboardmarkup import InlineKeyboardMarkup from .inline.inputmessagecontent import InputMessageContent from .inline.inlinequery import InlineQuery from .inline.inlinequeryresult import InlineQueryResult @@ -153,5 +154,6 @@ __all__ = [ 'PersonalDetails', 'ResidentialAddress', 'InputMediaVideo', 'InputMediaAnimation', 'InputMediaAudio', 'InputMediaDocument', 'TelegramDecryptionError', 'PassportElementErrorSelfie', 'PassportElementErrorTranslationFile', - 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll', 'PollOption' + 'PassportElementErrorTranslationFiles', 'PassportElementErrorUnspecified', 'Poll', + 'PollOption', 'LoginUrl' ] diff --git a/telegram/inline/inlinekeyboardbutton.py b/telegram/inline/inlinekeyboardbutton.py index bbf2a7f93..eedce7913 100644 --- a/telegram/inline/inlinekeyboardbutton.py +++ b/telegram/inline/inlinekeyboardbutton.py @@ -31,6 +31,8 @@ class InlineKeyboardButton(TelegramObject): Attributes: text (:obj:`str`): Label text on the button. url (:obj:`str`): Optional. HTTP url to be opened when button is pressed. + login_url (:class:`telegram.LoginUrl`) Optional. An HTTP URL used to automatically + authorize the user. callback_data (:obj:`str`): Optional. Data to be sent in a callback query to the bot when button is pressed, UTF-8 1-64 bytes. switch_inline_query (:obj:`str`): Optional. Will prompt the user to select one of their @@ -45,6 +47,8 @@ class InlineKeyboardButton(TelegramObject): Args: text (:obj:`str`): Label text on the button. url (:obj:`str`): HTTP url to be opened when button is pressed. + login_url (:class:`telegram.LoginUrl`, optional) An HTTP URL used to automatically + authorize the user. callback_data (:obj:`str`, optional): Data to be sent in a callback query to the bot when button is pressed, 1-64 UTF-8 bytes. switch_inline_query (:obj:`str`, optional): If set, pressing the button will prompt the @@ -76,14 +80,30 @@ class InlineKeyboardButton(TelegramObject): switch_inline_query_current_chat=None, callback_game=None, pay=None, + login_url=None, **kwargs): # Required self.text = text # Optionals - self.url = url - self.callback_data = callback_data - self.switch_inline_query = switch_inline_query - self.switch_inline_query_current_chat = switch_inline_query_current_chat - self.callback_game = callback_game - self.pay = pay + if url: + self.url = url + if login_url: + self.login_url = login_url + if callback_data: + self.callback_data = callback_data + if switch_inline_query: + self.switch_inline_query = switch_inline_query + if switch_inline_query_current_chat: + self.switch_inline_query_current_chat = switch_inline_query_current_chat + if callback_game: + self.callback_game = callback_game + if pay: + self.pay = pay + + @classmethod + def de_json(cls, data, bot): + if not data: + return None + + return cls(**data) diff --git a/telegram/inline/inlinekeyboardmarkup.py b/telegram/inline/inlinekeyboardmarkup.py index d212d896c..064231b78 100644 --- a/telegram/inline/inlinekeyboardmarkup.py +++ b/telegram/inline/inlinekeyboardmarkup.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram InlineKeyboardMarkup.""" -from telegram import ReplyMarkup +from telegram import ReplyMarkup, InlineKeyboardButton class InlineKeyboardMarkup(ReplyMarkup): @@ -49,6 +49,19 @@ class InlineKeyboardMarkup(ReplyMarkup): return data + @classmethod + def de_json(cls, data, bot): + if not data: + return None + keyboard = [] + for row in data['inline_keyboard']: + tmp = [] + for col in row: + tmp.append(InlineKeyboardButton.de_json(col, bot)) + keyboard.append(tmp) + + return cls(keyboard) + @classmethod def from_button(cls, button, **kwargs): """Shortcut for:: diff --git a/telegram/loginurl.py b/telegram/loginurl.py new file mode 100644 index 000000000..f5283c641 --- /dev/null +++ b/telegram/loginurl.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# pylint: disable=R0903 +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2019 +# 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 LoginUrl.""" +from telegram import TelegramObject + + +class LoginUrl(TelegramObject): + """This object represents a parameter of the inline keyboard button used to automatically + authorize a user. Serves as a great replacement for the Telegram Login Widget when the user is + coming from Telegram. All the user needs to do is tap/click a button and confirm that they want + to log in. Telegram apps support these buttons as of version 5.7. + Sample bot: @discussbot + + Attributes: + url (:obj:`str`): An HTTP URL to be opened with user authorization data. + forward_text (:obj:`str`): Optional. New text of the button in forwarded messages. + bot_username (:obj:`str`): Optional. Username of a bot, which will be used for user + authorization. + request_write_access (:obj:`bool`): Optional. Pass True to request the permission for your + bot to send messages to the user. + + Args: + url (:obj:`str`): An HTTP URL to be opened with user authorization data added to the query + string when the button is pressed. If the user refuses to provide authorization data, + the original URL without information about the user will be opened. The data added is + the same as described in Receiving authorization data. + NOTE: You must always check the hash of the received data to verify the authentication + and the integrity of the data as described in Checking authorization. + forward_text (:obj:`str`, optional): New text of the button in forwarded messages. + bot_username (:obj:`str`, optional): Username of a bot, which will be used for user + authorization. See Setting up a bot for more details. If not specified, the current + bot's username will be assumed. The url's domain must be the same as the domain linked + with the bot. See Linking your domain to the bot for more details. + request_write_access (:obj:`bool`, optional): Pass True to request the permission for your + bot to send messages to the user. + """ + + def __init__(self, url, forward_text=None, bot_username=None, request_write_access=None): + self.url = url + + if forward_text: + self.forward_text = forward_text + if bot_username: + self.bot_username = bot_username + if request_write_access: + self.request_write_access = request_write_access + + self._id_attrs = (self.url,) diff --git a/telegram/message.py b/telegram/message.py index 52bb070a3..f4ab3a6c0 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) + SuccessfulPayment, VideoNote, PassportData, Poll, InlineKeyboardMarkup) from telegram import ParseMode from telegram.utils.helpers import escape_markdown, to_timestamp, from_timestamp @@ -106,6 +106,8 @@ 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. + reply_markup (:class:`telegram.InlineKeyboardMarkup`): Optional. Inline keyboard attached + to the message. bot (:class:`telegram.Bot`): Optional. The Bot to use for instance methods. Args: @@ -210,6 +212,9 @@ 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. + reply_markup (:class:`telegram.InlineKeyboardMarkup`, optional): Inline keyboard attached + to the message. login_url buttons are represented as ordinary url buttons. + """ _effective_attachment = _UNDEFINED @@ -270,6 +275,7 @@ class Message(TelegramObject): passport_data=None, poll=None, forward_sender_name=None, + reply_markup=None, bot=None, **kwargs): # Required @@ -320,7 +326,7 @@ class Message(TelegramObject): self.animation = animation self.passport_data = passport_data self.poll = poll - + self.reply_markup = reply_markup self.bot = bot self._id_attrs = (self.message_id,) @@ -375,6 +381,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['reply_markup'] = InlineKeyboardMarkup.de_json(data.get('reply_markup'), bot) return cls(bot=bot, **data) diff --git a/tests/test_inlinekeyboardbutton.py b/tests/test_inlinekeyboardbutton.py index 8a0e9b06a..98faf1734 100644 --- a/tests/test_inlinekeyboardbutton.py +++ b/tests/test_inlinekeyboardbutton.py @@ -19,7 +19,7 @@ import pytest -from telegram import InlineKeyboardButton +from telegram import InlineKeyboardButton, LoginUrl @pytest.fixture(scope='class') @@ -31,7 +31,8 @@ def inline_keyboard_button(): switch_inline_query_current_chat=TestInlineKeyboardButton .switch_inline_query_current_chat, callback_game=TestInlineKeyboardButton.callback_game, - pay=TestInlineKeyboardButton.pay) + pay=TestInlineKeyboardButton.pay, + login_url=TestInlineKeyboardButton.login_url) class TestInlineKeyboardButton(object): @@ -42,6 +43,7 @@ class TestInlineKeyboardButton(object): switch_inline_query_current_chat = 'switch_inline_query_current_chat' callback_game = 'callback_game' pay = 'pay' + login_url = LoginUrl("http://google.com") def test_expected_values(self, inline_keyboard_button): assert inline_keyboard_button.text == self.text @@ -52,6 +54,7 @@ class TestInlineKeyboardButton(object): == self.switch_inline_query_current_chat) assert inline_keyboard_button.callback_game == self.callback_game assert inline_keyboard_button.pay == self.pay + assert inline_keyboard_button.login_url == self.login_url def test_to_dict(self, inline_keyboard_button): inline_keyboard_button_dict = inline_keyboard_button.to_dict() @@ -66,3 +69,26 @@ class TestInlineKeyboardButton(object): == inline_keyboard_button.switch_inline_query_current_chat) assert inline_keyboard_button_dict['callback_game'] == inline_keyboard_button.callback_game assert inline_keyboard_button_dict['pay'] == inline_keyboard_button.pay + assert inline_keyboard_button_dict['login_url'] == \ + inline_keyboard_button.login_url.to_dict() # NOQA: E127 + + def test_de_json(self, bot): + json_dict = { + 'text': self.text, + 'url': self.url, + 'callback_data': self.callback_data, + 'switch_inline_query': self.switch_inline_query, + 'switch_inline_query_current_chat': self.switch_inline_query_current_chat, + 'callback_game': self.callback_game, + 'pay': self.pay + } + + inline_keyboard_button = InlineKeyboardButton.de_json(json_dict, None) + assert inline_keyboard_button.text == self.text + assert inline_keyboard_button.url == self.url + assert inline_keyboard_button.callback_data == self.callback_data + assert inline_keyboard_button.switch_inline_query == self.switch_inline_query + assert (inline_keyboard_button.switch_inline_query_current_chat + == self.switch_inline_query_current_chat) + assert inline_keyboard_button.callback_game == self.callback_game + assert inline_keyboard_button.pay == self.pay diff --git a/tests/test_inlinekeyboardmarkup.py b/tests/test_inlinekeyboardmarkup.py index 68da77065..8666ff185 100644 --- a/tests/test_inlinekeyboardmarkup.py +++ b/tests/test_inlinekeyboardmarkup.py @@ -78,3 +78,34 @@ class TestInlineKeyboardMarkup(object): self.inline_keyboard[0][1].to_dict() ] ] + + def test_de_json(self): + json_dict = { + 'inline_keyboard': [[ + { + 'text': 'start', + 'url': 'http://google.com' + }, + { + 'text': 'next', + 'callback_data': 'abcd' + }], + [{ + 'text': 'Cancel', + 'callback_data': 'Cancel' + }] + ]} + inline_keyboard_markup = InlineKeyboardMarkup.de_json(json_dict, None) + + assert isinstance(inline_keyboard_markup, InlineKeyboardMarkup) + keyboard = inline_keyboard_markup.inline_keyboard + assert len(keyboard) == 2 + assert len(keyboard[0]) == 2 + assert len(keyboard[1]) == 1 + + assert isinstance(keyboard[0][0], InlineKeyboardButton) + assert isinstance(keyboard[0][1], InlineKeyboardButton) + assert isinstance(keyboard[1][0], InlineKeyboardButton) + + assert keyboard[0][0].text == 'start' + assert keyboard[0][0].url == 'http://google.com' diff --git a/tests/test_loginurl.py b/tests/test_loginurl.py new file mode 100644 index 000000000..4a41f0b88 --- /dev/null +++ b/tests/test_loginurl.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2019 +# 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 LoginUrl + + +@pytest.fixture(scope='class') +def login_url(): + return LoginUrl(url=TestLoginUrl.url, forward_text=TestLoginUrl.forward_text, + bot_username=TestLoginUrl.bot_username, + request_write_access=TestLoginUrl.request_write_access) + + +class TestLoginUrl(object): + url = "http://www.google.com" + forward_text = "Send me forward!" + bot_username = "botname" + request_write_access = True + + def test_to_dict(self, login_url): + login_url_dict = login_url.to_dict() + + assert isinstance(login_url_dict, dict) + assert login_url_dict['url'] == self.url + assert login_url_dict['forward_text'] == self.forward_text + assert login_url_dict['bot_username'] == self.bot_username + assert login_url_dict['request_write_access'] == self.request_write_access + + def test_equality(self): + a = LoginUrl(self.url, self.forward_text, self.bot_username, self.request_write_access) + b = LoginUrl(self.url, self.forward_text, self.bot_username, self.request_write_access) + c = LoginUrl(self.url) + d = LoginUrl("text.com", self.forward_text, self.bot_username, self.request_write_access) + + assert a == b + assert hash(a) == hash(b) + assert a is not b + + assert a == c + assert hash(a) == hash(c) + + assert a != d + assert hash(a) != hash(d) diff --git a/tests/test_message.py b/tests/test_message.py index f35f479ae..bc0e0138c 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -88,7 +88,11 @@ def message(bot): {'photo': [PhotoSize('photo_id', 50, 50)], 'caption': 'photo_file', 'media_group_id': 1234443322222}, - {'passport_data': PassportData.de_json(RAW_PASSPORT_DATA, None)} + {'passport_data': PassportData.de_json(RAW_PASSPORT_DATA, None)}, + {'text': 'a text message', 'reply_markup': {'inline_keyboard': [[{ + 'text': 'start', 'url': 'http://google.com'}, { + 'text': 'next', 'callback_data': 'abcd'}], + [{'text': 'Cancel', 'callback_data': 'Cancel'}]]}} ], ids=['forwarded_user', 'forwarded_channel', 'reply', 'edited', 'text', 'caption_entities', 'audio', 'document', 'animation', 'game', 'photo', @@ -97,7 +101,7 @@ def message(bot): 'group_created', 'supergroup_created', 'channel_created', 'migrated_to', 'migrated_from', 'pinned', 'invoice', 'successful_payment', 'connected_website', 'forward_signature', 'author_signature', - 'photo_from_media_group', 'passport_data']) + 'photo_from_media_group', 'passport_data', 'reply_markup']) def message_params(bot, request): return Message(message_id=TestMessage.id, from_user=TestMessage.from_user,