From ccf5e6c6927e03d8c8db5e25dc36a9a6b619fdb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joscha=20G=C3=B6tzer?= Date: Fri, 13 Sep 2019 21:09:05 +0200 Subject: [PATCH] Implemented Tools for deep linking (#1049) --- examples/deeplinking.py | 119 ++++++++++++++++++++++++++++++++++++ telegram/utils/helpers.py | 49 +++++++++++++++ telegram/vendor/ptb_urllib3 | 2 +- tests/test_helpers.py | 31 ++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 examples/deeplinking.py diff --git a/examples/deeplinking.py b/examples/deeplinking.py new file mode 100644 index 000000000..05e0a82d9 --- /dev/null +++ b/examples/deeplinking.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"""Bot that explains Telegram's "Deep Linking Parameters" functionality. + +This program is dedicated to the public domain under the CC0 license. + +This Bot uses the Updater class to handle the bot. + +First, a few handler functions are defined. Then, those functions are passed to +the Dispatcher and registered at their respective places. +Then, the bot is started and runs until we press Ctrl-C on the command line. + +Usage: +Deep Linking example. Send /start to get the link. +Press Ctrl-C on the command line or send a signal to the process to stop the +bot. +""" + +import logging + +from telegram import ParseMode, InlineKeyboardMarkup, InlineKeyboardButton +from telegram.ext import Updater, CommandHandler, Filters + +# Enable logging +from telegram.utils import helpers + +logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + level=logging.INFO) + +logger = logging.getLogger(__name__) + +# Define constants the will allow us to reuse the deep-linking parameters. +CHECK_THIS_OUT = 'check-this-out' +USING_ENTITIES = 'using-entities-here' +SO_COOL = 'so-cool' + + +def start(update, context): + """Send a deep-linked URL when the command /start is issued.""" + bot = context.bot + url = helpers.create_deep_linked_url(bot.get_me().username, CHECK_THIS_OUT, group=True) + text = "Feel free to tell your friends about it:\n\n" + url + update.message.reply_text(text) + + +def deep_linked_level_1(update, context): + """Reached through the CHECK_THIS_OUT payload""" + bot = context.bot + url = helpers.create_deep_linked_url(bot.get_me().username, SO_COOL) + text = "Awesome, you just accessed hidden functionality! " \ + " Now let's get back to the private chat." + keyboard = InlineKeyboardMarkup.from_button( + InlineKeyboardButton(text='Continue here!', url=url) + ) + update.message.reply_text(text, reply_markup=keyboard) + + +def deep_linked_level_2(update, context): + """Reached through the SO_COOL payload""" + bot = context.bot + url = helpers.create_deep_linked_url(bot.get_me().username, USING_ENTITIES) + text = "You can also mask the deep-linked URLs as links: " \ + "[▶️ CLICK HERE]({0}).".format(url) + update.message.reply_text(text, parse_mode=ParseMode.MARKDOWN, disable_web_page_preview=True) + + +def deep_linked_level_3(update, context): + """Reached through the USING_ENTITIES payload""" + payload = context.args + update.message.reply_text("Congratulations! This is as deep as it gets 👏🏻\n\n" + "The payload was: {0}".format(payload)) + + +def error(update, context): + """Log Errors caused by Updates.""" + logger.warning('Update "%s" caused error "%s"', update, context.error) + + +def main(): + """Start the bot.""" + # Create the Updater and pass it your bot's token. + updater = Updater("TOKEN", use_context=True) + + # Get the dispatcher to register handlers + dp = updater.dispatcher + + # More info on what deep linking actually is (read this first if it's unclear to you): + # https://core.telegram.org/bots#deep-linking + + # Register a deep-linking handler + dp.add_handler(CommandHandler("start", deep_linked_level_1, Filters.regex(CHECK_THIS_OUT))) + + # This one works with a textual link instead of an URL + dp.add_handler(CommandHandler("start", deep_linked_level_2, Filters.regex(SO_COOL))) + + # We can also pass on the deep-linking payload + dp.add_handler(CommandHandler("start", + deep_linked_level_3, + Filters.regex(USING_ENTITIES), + pass_args=True)) + + # Make sure the deep-linking handlers occur *before* the normal /start handler. + dp.add_handler(CommandHandler("start", start)) + + # log all errors + dp.add_error_handler(error) + + # Start the Bot + updater.start_polling() + + # Run the bot until you press Ctrl-C or the process receives SIGINT, + # SIGTERM or SIGABRT. This should be used most of the time, since + # start_polling() is non-blocking and will stop the bot gracefully. + updater.idle() + + +if __name__ == '__main__': + main() diff --git a/telegram/utils/helpers.py b/telegram/utils/helpers.py index c7697db42..ca149e55f 100644 --- a/telegram/utils/helpers.py +++ b/telegram/utils/helpers.py @@ -147,6 +147,55 @@ def effective_message_type(entity): return None +def create_deep_linked_url(bot_username, payload=None, group=False): + """ + Creates a deep-linked URL for this ``bot_username`` with the specified ``payload``. + See https://core.telegram.org/bots#deep-linking to learn more. + + The ``payload`` may consist of the following characters: ``A-Z, a-z, 0-9, _, -`` + + Note: + Works well in conjunction with + ``CommandHandler("start", callback, filters = Filters.regex('payload'))`` + + Examples: + ``create_deep_linked_url(bot.get_me().username, "some-params")`` + + Args: + bot_username (:obj:`str`): The username to link to + payload (:obj:`str`, optional): Parameters to encode in the created URL + group (:obj:`bool`, optional): If `True` the user is prompted to select a group to add the + bot to. If `False`, opens a one-on-one conversation with the bot. Defaults to `False`. + + Returns: + :obj:`str`: An URL to start the bot with specific parameters + """ + if bot_username is None or len(bot_username) <= 3: + raise ValueError("You must provide a valid bot_username.") + + base_url = 'https://t.me/{}'.format(bot_username) + if not payload: + return base_url + + if len(payload) > 64: + raise ValueError("The deep-linking payload must not exceed 64 characters.") + + if not re.match(r'^[A-Za-z0-9_-]+$', payload): + raise ValueError("Only the following characters are allowed for deep-linked " + "URLs: A-Z, a-z, 0-9, _ and -") + + if group: + key = 'startgroup' + else: + key = 'start' + + return '{0}?{1}={2}'.format( + base_url, + key, + payload + ) + + def enocde_conversations_to_json(conversations): """Helper method to encode a conversations dict (that uses tuples as keys) to a JSON-serializable way. Use :attr:`_decode_conversations_from_json` to decode. diff --git a/telegram/vendor/ptb_urllib3 b/telegram/vendor/ptb_urllib3 index d2403a79f..06d04e451 160000 --- a/telegram/vendor/ptb_urllib3 +++ b/telegram/vendor/ptb_urllib3 @@ -1 +1 @@ -Subproject commit d2403a79fc38afbdd9aba8a05d274a83dc8bb412 +Subproject commit 06d04e451f6beb5562057bf793218c4e363d8bc0 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8e60d886e..132692927 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -16,6 +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/]. +import pytest from telegram import Sticker from telegram import Update @@ -31,6 +32,36 @@ class TestHelpers(object): assert expected_str == helpers.escape_markdown(test_str) + def test_create_deep_linked_url(self): + username = 'JamesTheMock' + + payload = "hello" + expected = "https://t.me/{}?start={}".format(username, payload) + actual = helpers.create_deep_linked_url(username, payload) + assert expected == actual + + expected = "https://t.me/{}?startgroup={}".format(username, payload) + actual = helpers.create_deep_linked_url(username, payload, group=True) + assert expected == actual + + payload = "" + expected = "https://t.me/{}".format(username) + assert expected == helpers.create_deep_linked_url(username) + assert expected == helpers.create_deep_linked_url(username, payload) + payload = None + assert expected == helpers.create_deep_linked_url(username, payload) + + with pytest.raises(ValueError): + helpers.create_deep_linked_url(username, 'text with spaces') + + with pytest.raises(ValueError): + helpers.create_deep_linked_url(username, '0' * 65) + + with pytest.raises(ValueError): + helpers.create_deep_linked_url(None, None) + with pytest.raises(ValueError): # too short username (4 is minimum) + helpers.create_deep_linked_url("abc", None) + def test_effective_message_type(self): def build_test_message(**kwargs):