leandrotoledo/python-telegram-bot

View on GitHub
telegram/helpers.py

Summary

Maintainability
A
1 hr
Test Coverage
#!/usr/bin/env python
#
# A library that provides a Python interface to the Telegram Bot API
# Copyright (C) 2015-2024
# Leandro Toledo de Souza <devs@python-telegram-bot.org>
#
# 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 convenience helper functions.

.. versionchanged:: 20.0
   Previously, the contents of this module were available through the (no longer existing)
   module ``telegram.utils.helpers``.
"""

__all__ = (
    "create_deep_linked_url",
    "effective_message_type",
    "escape_markdown",
    "mention_html",
    "mention_markdown",
)

import re
from html import escape
from typing import TYPE_CHECKING, Optional, Union

from telegram._utils.types import MarkdownVersion
from telegram.constants import MessageType

if TYPE_CHECKING:
    from telegram import Message, Update


def escape_markdown(
    text: str, version: MarkdownVersion = 1, entity_type: Optional[str] = None
) -> str:
    """Helper function to escape telegram markup symbols.

    .. versionchanged:: 20.3
        Custom emoji entity escaping is now supported.

    Args:
        text (:obj:`str`): The text.
        version (:obj:`int` | :obj:`str`): Use to specify the version of telegrams Markdown.
            Either ``1`` or ``2``. Defaults to ``1``.
        entity_type (:obj:`str`, optional): For the entity types
            :tg-const:`telegram.MessageEntity.PRE`, :tg-const:`telegram.MessageEntity.CODE` and
            the link part of :tg-const:`telegram.MessageEntity.TEXT_LINK` and
            :tg-const:`telegram.MessageEntity.CUSTOM_EMOJI`, only certain characters need to be
            escaped in :tg-const:`telegram.constants.ParseMode.MARKDOWN_V2`. See the `official API
            documentation <https://core.telegram.org/bots/api#formatting-options>`_ for details.
            Only valid in combination with ``version=2``, will be ignored else.
    """
    if int(version) == 1:
        escape_chars = r"_*`["
    elif int(version) == 2:
        if entity_type in ["pre", "code"]:
            escape_chars = r"\`"
        elif entity_type in ["text_link", "custom_emoji"]:
            escape_chars = r"\)"
        else:
            escape_chars = r"\_*[]()~`>#+-=|{}.!"
    else:
        raise ValueError("Markdown version must be either 1 or 2!")

    return re.sub(f"([{re.escape(escape_chars)}])", r"\\\1", text)


def mention_html(user_id: Union[int, str], name: str) -> str:
    """
    Helper function to create a user mention as HTML tag.

    Args:
        user_id (:obj:`int`): The user's id which you want to mention.
        name (:obj:`str`): The name the mention is showing.

    Returns:
        :obj:`str`: The inline mention for the user as HTML.
    """
    return f'<a href="tg://user?id={user_id}">{escape(name)}</a>'


def mention_markdown(user_id: Union[int, str], name: str, version: MarkdownVersion = 1) -> str:
    """
    Helper function to create a user mention in Markdown syntax.

    Args:
        user_id (:obj:`int`): The user's id which you want to mention.
        name (:obj:`str`): The name the mention is showing.
        version (:obj:`int` | :obj:`str`): Use to specify the version of Telegram's Markdown.
            Either ``1`` or ``2``. Defaults to ``1``.

    Returns:
        :obj:`str`: The inline mention for the user as Markdown.
    """
    tg_link = f"tg://user?id={user_id}"
    if version == 1:
        return f"[{name}]({tg_link})"
    return f"[{escape_markdown(name, version=version)}]({tg_link})"


def effective_message_type(entity: Union["Message", "Update"]) -> Optional[str]:
    """
    Extracts the type of message as a string identifier from a :class:`telegram.Message` or a
    :class:`telegram.Update`.

    Args:
        entity (:class:`telegram.Update` | :class:`telegram.Message`): The ``update`` or
            ``message`` to extract from.

    Returns:
        :obj:`str` | :obj:`None`: One of :class:`telegram.constants.MessageType` if the entity
        contains a message that matches one of those types. :obj:`None` otherwise.

    """
    # Importing on file-level yields cyclic Import Errors
    from telegram import Message, Update  # pylint: disable=import-outside-toplevel

    if isinstance(entity, Message):
        message = entity
    elif isinstance(entity, Update):
        if not entity.effective_message:
            return None
        message = entity.effective_message
    else:
        raise TypeError(f"The entity is neither Message nor Update (got: {type(entity)})")

    for message_type in MessageType:
        if message[message_type]:
            return message_type

    return None


def create_deep_linked_url(
    bot_username: str, payload: Optional[str] = None, group: bool = False
) -> str:
    """
    Creates a deep-linked URL for this :paramref:`~create_deep_linked_url.bot_username` with the
    specified :paramref:`~create_deep_linked_url.payload`. See
    https://core.telegram.org/bots/features#deep-linking to learn more.

    The :paramref:`~create_deep_linked_url.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")``
        * :any:`Deep Linking <examples.deeplinking>`

    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 :obj:`True` the user is prompted to select a group to
            add the bot to. If :obj:`False`, opens a one-on-one conversation with the bot.
            Defaults to :obj:`False`.

    Returns:
        :obj:`str`: An URL to start the bot with specific parameters.

    Raises:
        :exc:`ValueError`: If the length of the :paramref:`payload` exceeds 64 characters,
            contains invalid characters, or if the :paramref:`bot_username` is less than 4
            characters.
    """
    if bot_username is None or len(bot_username) <= 3:
        raise ValueError("You must provide a valid bot_username.")

    base_url = f"https://t.me/{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 -"
        )

    key = "startgroup" if group else "start"

    return f"{base_url}?{key}={payload}"