From 3b66a2b2e0a90ffc80582be3d1ef0e7045ce51fa Mon Sep 17 00:00:00 2001 From: Alex Root Junior Date: Wed, 22 Jun 2022 00:59:49 +0300 Subject: [PATCH] Added full support of Telegram Bot API 6.1; Ported web-app utils; Deprecated emoji module; --- aiogram/bot/api.py | 1 + aiogram/bot/bot.py | 68 ++++++++++++++++++++++++++++++++++++++++ aiogram/types/chat.py | 2 ++ aiogram/types/sticker.py | 3 +- aiogram/types/user.py | 2 ++ aiogram/utils/emoji.py | 8 +++++ aiogram/utils/web_app.py | 67 +++++++++++++++++++++++++++++++++++++++ message.py | 17 ++++++++++ 8 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 aiogram/utils/web_app.py create mode 100644 message.py diff --git a/aiogram/bot/api.py b/aiogram/bot/api.py index f2cc7234..509e6c71 100644 --- a/aiogram/bot/api.py +++ b/aiogram/bot/api.py @@ -287,6 +287,7 @@ class Methods(Helper): # Payments SEND_INVOICE = Item() # sendInvoice + CREATE_INVOICE_LINK = Item() # createInvoiceLink ANSWER_SHIPPING_QUERY = Item() # answerShippingQuery ANSWER_PRE_CHECKOUT_QUERY = Item() # answerPreCheckoutQuery diff --git a/aiogram/bot/bot.py b/aiogram/bot/bot.py index 461f68b2..c883409f 100644 --- a/aiogram/bot/bot.py +++ b/aiogram/bot/bot.py @@ -117,6 +117,7 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): max_connections: typing.Optional[base.Integer] = None, allowed_updates: typing.Optional[typing.List[base.String]] = None, drop_pending_updates: typing.Optional[base.Boolean] = None, + secret_token: typing.Optional[str] = None, ) -> base.Boolean: """ Use this method to specify a url and receive incoming updates via an outgoing @@ -165,6 +166,10 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): :param drop_pending_updates: Pass True to drop all pending updates :type drop_pending_updates: :obj:`typing.Optional[base.Boolean]` + :param secret_token: A secret token to be sent in a header “X-Telegram-Bot-Api-Secret-Token” + in every webhook request, 1-256 characters. Only characters A-Z, a-z, 0-9, _ and - are allowed. + The header is useful to ensure that the request comes from a webhook set by you. + :type secret_token: :obj:`typing.Optional[str]` :return: Returns true :rtype: :obj:`base.Boolean` """ @@ -3397,6 +3402,69 @@ class Bot(BaseBot, DataMixin, ContextInstanceMixin): result = await self.request(api.Methods.SEND_INVOICE, payload_) return types.Message(**result) + async def create_invoice_link(self, + title: base.String, + description: base.String, + payload: base.String, + provider_token: base.String, + currency: base.String, + prices: typing.List[types.LabeledPrice], + max_tip_amount: typing.Optional[int] = None, + suggested_tip_amounts: typing.Optional[typing.List[int]] = None, + provider_data: typing.Optional[base.String] = None, + photo_url: typing.Optional[str] = None, + photo_size: typing.Optional[int] = None, + photo_width: typing.Optional[int] = None, + photo_height: typing.Optional[int] = None, + need_name: typing.Optional[bool] = None, + need_phone_number: typing.Optional[bool] = None, + need_email: typing.Optional[bool] = None, + need_shipping_address: typing.Optional[bool] = None, + send_phone_number_to_provider: typing.Optional[bool] = None, + send_email_to_provider: typing.Optional[bool] = None, + is_flexible: typing.Optional[bool] = None, + ) -> str: + """ + Use this method to create a link for an invoice. On success, the created link is returned. + + Source: https://core.telegram.org/bots/api#createinvoicelink + + :param title: Product name, 1-32 characters + :param description: Product description, 1-255 characters + :param payload: Bot-defined invoice payload, 1-128 bytes. This will not be displayed to the user, use for your internal processes. + :param provider_token: Payment provider token, obtained via BotFather + :param currency: Three-letter ISO 4217 currency code, see more on currencies + :param prices: Price breakdown, a JSON-serialized list of components + (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.) + :param max_tip_amount: The maximum accepted amount for tips in the smallest units of the currency + (integer, not float/double). For example, for a maximum tip of US$ 1.45 pass max_tip_amount = 145. + See the exp parameter in currencies.json, it shows the number of digits past the decimal point for + each currency (2 for the majority of currencies). Defaults to 0 + :param suggested_tip_amounts: A JSON-serialized array of suggested amounts of tips in the smallest units + of the currency (integer, not float/double). At most 4 suggested tip amounts can be specified. + The suggested tip amounts must be positive, passed in a strictly increased order and must not + exceed max_tip_amount. + :param provider_data: JSON-serialized data about the invoice, which will be shared with the payment provider. + A detailed description of required fields should be provided by the payment provider. + :param photo_url: URL of the product photo for the invoice. + Can be a photo of the goods or a marketing image for a service. + :param photo_size: Photo size in bytes + :param photo_width: Photo width + :param photo_height: Photo height + :param need_name: Pass True, if you require the user's full name to complete the order + :param need_phone_number: Pass True, if you require the user's phone number to complete the order + :param need_email: Pass True, if you require the user's email address to complete the order + :param need_shipping_address: Pass True, if you require the user's shipping address to complete the order + :param send_phone_number_to_provider: Pass True, if the user's phone number should be sent to the provider + :param send_email_to_provider: Pass True, if the user's email address should be sent to the provider + :param is_flexible: Pass True, if the final price depends on the shipping method + :return: + """ + prices = prepare_arg([price.to_python() if hasattr(price, 'to_python') else price for price in prices]) + payload = generate_payload(**locals()) + + return await self.request(api.Methods.CREATE_INVOICE_LINK, payload) + async def answer_shipping_query(self, shipping_query_id: base.String, ok: base.Boolean, shipping_options: typing.Union[typing.List[types.ShippingOption], None] = None, error_message: typing.Optional[base.String] = None) -> base.Boolean: diff --git a/aiogram/types/chat.py b/aiogram/types/chat.py index c18ad88b..dd11100a 100644 --- a/aiogram/types/chat.py +++ b/aiogram/types/chat.py @@ -31,6 +31,8 @@ class Chat(base.TelegramObject): photo: ChatPhoto = fields.Field(base=ChatPhoto) bio: base.String = fields.Field() has_private_forwards: base.Boolean = fields.Field() + join_to_send_messages: base.Boolean = fields.Field() + join_by_request: base.Boolean = fields.Field() description: base.String = fields.Field() invite_link: base.String = fields.Field() pinned_message: 'Message' = fields.Field(base='Message') diff --git a/aiogram/types/sticker.py b/aiogram/types/sticker.py index 20c162e8..f1c0f527 100644 --- a/aiogram/types/sticker.py +++ b/aiogram/types/sticker.py @@ -3,7 +3,7 @@ from . import fields from . import mixins from .mask_position import MaskPosition from .photo_size import PhotoSize - +from .file import File class Sticker(base.TelegramObject, mixins.Downloadable): """ @@ -20,6 +20,7 @@ class Sticker(base.TelegramObject, mixins.Downloadable): thumb: PhotoSize = fields.Field(base=PhotoSize) emoji: base.String = fields.Field() set_name: base.String = fields.Field() + premium_animation: File = fields.Field(base=File) mask_position: MaskPosition = fields.Field(base=MaskPosition) file_size: base.Integer = fields.Field() diff --git a/aiogram/types/user.py b/aiogram/types/user.py index 6bde2dcd..3d594ecc 100644 --- a/aiogram/types/user.py +++ b/aiogram/types/user.py @@ -22,6 +22,8 @@ class User(base.TelegramObject): last_name: base.String = fields.Field() username: base.String = fields.Field() language_code: base.String = fields.Field() + is_premium: base.Boolean = fields.Field() + added_to_attachment_menu: base.Boolean = fields.Field() can_join_groups: base.Boolean = fields.Field() can_read_all_group_messages: base.Boolean = fields.Field() supports_inline_queries: base.Boolean = fields.Field() diff --git a/aiogram/utils/emoji.py b/aiogram/utils/emoji.py index 07faff56..92252076 100644 --- a/aiogram/utils/emoji.py +++ b/aiogram/utils/emoji.py @@ -1,3 +1,5 @@ +import warnings + try: import emoji except ImportError: @@ -5,8 +7,14 @@ except ImportError: def emojize(text): + warnings.warn(message="'aiogram.utils.emoji' module deprecated, use emoji symbols directly instead," + "this function will be removed in aiogram v2.22", + category=DeprecationWarning, stacklevel=2) return emoji.emojize(text, use_aliases=True) def demojize(text): + warnings.warn(message="'aiogram.utils.emoji' module deprecated, use emoji symbols directly instead," + "this function will be removed in aiogram v2.22", + category=DeprecationWarning, stacklevel=2) return emoji.demojize(text) diff --git a/aiogram/utils/web_app.py b/aiogram/utils/web_app.py new file mode 100644 index 00000000..1ba43658 --- /dev/null +++ b/aiogram/utils/web_app.py @@ -0,0 +1,67 @@ +import hashlib +import hmac +from operator import itemgetter +from typing import Callable, Any, Dict +from urllib.parse import parse_qsl + + +def check_webapp_signature(token: str, init_data: str) -> bool: + """ + Check incoming WebApp init data signature + + Source: https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app + + :param token: + :param init_data: + :return: + """ + try: + parsed_data = dict(parse_qsl(init_data)) + except ValueError: + # Init data is not a valid query string + return False + if "hash" not in parsed_data: + # Hash is not present in init data + return False + + hash_ = parsed_data.pop('hash') + data_check_string = "\n".join( + f"{k}={v}" for k, v in sorted(parsed_data.items(), key=itemgetter(0)) + ) + secret_key = hmac.new( + key=b"WebAppData", msg=token.encode(), digestmod=hashlib.sha256 + ) + calculated_hash = hmac.new( + key=secret_key.digest(), msg=data_check_string.encode(), digestmod=hashlib.sha256 + ).hexdigest() + return calculated_hash == hash_ + + +def parse_init_data(init_data: str, _loads: Callable[..., Any]) -> Dict[str, Any]: + """ + Parse WebApp init data and return it as dict + + :param init_data: + :param _loads: + :return: + """ + result = {} + for key, value in parse_qsl(init_data): + if (value.startswith('[') and value.endswith(']')) or (value.startswith('{') and value.endswith('}')): + value = _loads(value) + result[key] = value + return result + + +def safe_parse_webapp_init_data(token: str, init_data: str, _loads: Callable[..., Any]) -> Dict[str, Any]: + """ + Validate WebApp init data and return it as dict + + :param token: + :param init_data: + :param _loads: + :return: + """ + if check_webapp_signature(token, init_data): + return parse_init_data(init_data, _loads) + raise ValueError("Invalid init data signature") diff --git a/message.py b/message.py new file mode 100644 index 00000000..ef790190 --- /dev/null +++ b/message.py @@ -0,0 +1,17 @@ +import asyncio +import secrets + +from aiogram import Bot + + +async def main(): + bot = Bot(token="5115369270:AAFlipWd1qbhc7cIe0nRM-SyGLkTC_9Ulgg") + index = 0 + while True: + index += 1 + print(index) + await bot.send_message(chat_id=879238251, text=secrets.token_urlsafe(24)) + await asyncio.sleep(.2) + + +asyncio.run(main())