diff --git a/compiler/docs/compiler.py b/compiler/docs/compiler.py index 9916f9c368..920e665bc9 100644 --- a/compiler/docs/compiler.py +++ b/compiler/docs/compiler.py @@ -393,6 +393,8 @@ def get_title_list(s: str) -> list: refund_star_payment get_business_connection get_collectible_item_info + get_payment_form + send_payment_form """, advanced=""" Advanced @@ -651,6 +653,7 @@ def get_title_list(s: str) -> list: ShippingAddress OrderInfo ShippingOption + PaymentForm SuccessfulPayment RefundedPayment ShippingQuery @@ -726,6 +729,7 @@ def get_title_list(s: str) -> list: Message.read Message.view Message.translate + Message.pay """, chat=""" Chat diff --git a/compiler/errors/source/400_BAD_REQUEST.tsv b/compiler/errors/source/400_BAD_REQUEST.tsv index cd6403c7ae..b3b8b4e33e 100644 --- a/compiler/errors/source/400_BAD_REQUEST.tsv +++ b/compiler/errors/source/400_BAD_REQUEST.tsv @@ -21,6 +21,7 @@ AUTH_TOKEN_EXPIRED The authorization token has expired. AUTH_TOKEN_INVALID The specified auth token is invalid. AUTH_TOKEN_INVALIDX The specified auth token is invalid. AUTOARCHIVE_NOT_AVAILABLE The autoarchive setting is not available at this time: please check the value of the [autoarchive_setting_available field in client config](https://core.telegram.org/api/config#client-configuration) before calling this method. +BALANCE_TOO_LOW Your stars balance too low BANK_CARD_NUMBER_INVALID The specified card number is invalid. BANNED_RIGHTS_INVALID You provided some invalid flags in the banned rights. BASE_PORT_LOC_INVALID The base port location is invalid diff --git a/compiler/errors/source/406_NOT_ACCEPTABLE.tsv b/compiler/errors/source/406_NOT_ACCEPTABLE.tsv index e535e57716..b6d483a2a3 100644 --- a/compiler/errors/source/406_NOT_ACCEPTABLE.tsv +++ b/compiler/errors/source/406_NOT_ACCEPTABLE.tsv @@ -1,6 +1,7 @@ id message AUTH_KEY_DUPLICATED The same authorization key (session file) was used in more than one place simultaneously. You must delete your session file and log in again with your phone number or bot token BANNED_RIGHTS_INVALID You provided some invalid flags in the banned rights. +BOT_PRECHECKOUT_FAILED Bot precheckout failed. CALL_PROTOCOL_COMPAT_LAYER_INVALID The other side of the call does not support any of the VoIP protocols supported by the local client, as specified by the `protocol.layer` and `protocol.library_versions` fields. CHANNEL_PRIVATE You haven't joined this channel/supergroup. CHANNEL_TOO_LARGE Channel is too large to be deleted; this error is issued when trying to delete channels with more than 1000 members (subject to change). @@ -22,4 +23,4 @@ TOPIC_CLOSED This topic was closed, you can't send messages to it anymore. TOPIC_DELETED The specified topic was deleted. USERPIC_PRIVACY_REQUIRED You need to disable privacy settings for your profile picture in order to make your geolocation public. USERPIC_UPLOAD_REQUIRED You must have a profile picture to publish your geolocation. -USER_RESTRICTED You're spamreported, you can't create channels or chats. \ No newline at end of file +USER_RESTRICTED You're spamreported, you can't create channels or chats. diff --git a/pyrogram/client.py b/pyrogram/client.py index 6f77e6b26a..1b775977b5 100644 --- a/pyrogram/client.py +++ b/pyrogram/client.py @@ -237,6 +237,7 @@ class Client(Methods): INVITE_LINK_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/(?:joinchat/|\+))([\w-]+)$") TME_PUBLIC_LINK_RE = re.compile(r"^(?:https?://)?(?:www|([\w-]+)\.)?(?:t(?:elegram)?\.(?:org|me|dog))/?([\w-]+)?$") + INVOICE_LINK_RE = re.compile(r"^(?:https?://)?(?:www\.)?(?:t(?:elegram)?\.(?:org|me|dog)/\$)([\w-]+)$") WORKERS = min(32, (os.cpu_count() or 0) + 4) # os.cpu_count() can be None WORKDIR = PARENT_DIR diff --git a/pyrogram/methods/business/__init__.py b/pyrogram/methods/business/__init__.py index 287b8588e4..88111fbc51 100644 --- a/pyrogram/methods/business/__init__.py +++ b/pyrogram/methods/business/__init__.py @@ -23,6 +23,8 @@ from .get_collectible_item_info import GetCollectibleItemInfo from .refund_star_payment import RefundStarPayment from .send_invoice import SendInvoice +from .get_payment_from import GetPaymentForm +from .send_payment_from import SendPaymentForm class TelegramBusiness( @@ -33,5 +35,7 @@ class TelegramBusiness( GetCollectibleItemInfo, RefundStarPayment, SendInvoice, + GetPaymentForm, + SendPaymentForm, ): pass diff --git a/pyrogram/methods/business/get_payment_from.py b/pyrogram/methods/business/get_payment_from.py new file mode 100644 index 0000000000..ca1771e3a8 --- /dev/null +++ b/pyrogram/methods/business/get_payment_from.py @@ -0,0 +1,87 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union + +import pyrogram +from pyrogram import raw, types + + +class GetPaymentForm: + async def get_payment_form( + self: "pyrogram.Client", *, + chat_id: Union[int, str] = None, + message_id: int = None, + invoice_link: str = None + ) -> "types.PaymentForm": + """Get information about a invoice or paid media. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + of the target channel/supergroup (in the format @username). + + message_id (``int``): + Pass a message identifier or to get the invoice from message. + + invoice_link (``str``): + Pass a invoice link in form of a *t.me/$...* link or slug itself to get the payment form from link. + + Returns: + :obj:`~pyrogram.types.PaymentForm`: On success, a payment form is returned. + + Example: + .. code-block:: python + + # get payment form from message + app.get_payment_form(chat_id=chat_id, message_id=123) + + # get payment form from link + app.get_payment_form(invoice_link="https://t.me/$xvbzUtt5sUlJCAAATqZrWRy9Yzk") + """ + if not any((all((chat_id, message_id)), invoice_link)): + raise ValueError("You should pass at least one parameter to this method.") + + invoice = None + + if message_id: + invoice = raw.types.InputInvoiceMessage( + peer=await self.resolve_peer(chat_id), + msg_id=message_id + ) + elif invoice_link: + match = self.INVOICE_LINK_RE.match(invoice_link) + + if match: + slug = match.group(1) + else: + slug = invoice_link + + invoice = raw.types.InputInvoiceSlug( + slug=slug + ) + + r = await self.invoke( + raw.functions.payments.GetPaymentForm( + invoice=invoice + ) + ) + + return types.PaymentForm._parse(self, r) diff --git a/pyrogram/methods/business/send_payment_from.py b/pyrogram/methods/business/send_payment_from.py new file mode 100644 index 0000000000..6a59bccc99 --- /dev/null +++ b/pyrogram/methods/business/send_payment_from.py @@ -0,0 +1,104 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Union + +import pyrogram +from pyrogram import raw + + +class SendPaymentForm: + async def send_payment_form( + self: "pyrogram.Client", *, + chat_id: Union[int, str] = None, + message_id: int = None, + invoice_link: str = None + ) -> bool: + """Pay an invoice. + + .. note:: + + For now only stars invoices are supported. + + .. include:: /_includes/usable-by/users.rst + + Parameters: + chat_id (``int`` | ``str``): + Unique identifier (int) or username (str) of the target chat. + of the target channel/supergroup (in the format @username). + + message_id (``int``): + Pass a message identifier or to get the invoice from message. + + invoice_link (``str``): + Pass a invoice link in form of a *t.me/$...* link or slug itself to pay this invoice. + + Returns: + ``bool``: On success, True is returned. + + Example: + .. code-block:: python + + # Pay invoice from message + app.send_payment_form(chat_id=chat_id, message_id=123) + + # Pay invoice form from link + app.send_payment_form(invoice_link="https://t.me/$xvbzUtt5sUlJCAAATqZrWRy9Yzk") + """ + if not any((all((chat_id, message_id)), invoice_link)): + raise ValueError("You should pass at least one parameter to this method.") + + form = None + invoice = None + + if message_id: + invoice = raw.types.InputInvoiceMessage( + peer=await self.resolve_peer(chat_id), + msg_id=message_id + ) + elif invoice_link: + match = self.INVOICE_LINK_RE.match(invoice_link) + if match: + slug = match.group(1) + else: + slug = invoice_link + + invoice = raw.types.InputInvoiceSlug( + slug=slug + ) + + form = await self.get_payment_form(chat_id=chat_id, message_id=message_id, invoice_link=invoice_link) + + # if form.invoice.currency == "XTR": + await self.invoke( + raw.functions.payments.SendStarsForm( + form_id=form.id, + invoice=invoice + ) + ) + # TODO: Add support for regular invoices (credentials) + # else: + # r = await self.invoke( + # raw.functions.payments.SendPaymentForm( + # form_id=form.id, + # invoice=invoice, + # credentials=raw.types.InputPaymentCredentials(data=raw.types.DataJSON(data={})) + # ) + # ) + + return True diff --git a/pyrogram/types/business/invoice.py b/pyrogram/types/business/invoice.py index baf7ddc4d1..cb9d3ff79b 100644 --- a/pyrogram/types/business/invoice.py +++ b/pyrogram/types/business/invoice.py @@ -16,6 +16,8 @@ # You should have received a copy of the GNU Lesser General Public License # along with Pyrogram. If not, see . +from typing import Optional, List, Union + import pyrogram from pyrogram import raw, types from ..object import Object @@ -25,49 +27,136 @@ class Invoice(Object): """This object contains basic information about an invoice. Parameters: - title (``str``): + currency (``str``): + Three-letter ISO 4217 `currency `_ code. + + is_test (``bool``): + True, if the invoice is a test invoice. + + title (``str``, *optional*): Product name. - description (``str``): + description (``str``, *optional*): Product description. - start_parameter (``str``): + total_amount (``int``, *optional*): + Total price in the smallest units of the currency (integer, **not** float/double). For example, for a price of ``US$ 1.45`` pass ``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). + + start_parameter (``str``, *optional*): Unique bot deep-linking parameter that can be used to generate this invoice. - currency (``str``): - Three-letter ISO 4217 `currency `_ code. + prices (List of :obj:`~pyrogram.types.LabeledPrice`, *optional*): + Price breakdown, a list of components (e.g. product price, tax, discount, delivery cost, delivery tax, bonus, etc.). - total_amount (``int``): - Total price in the smallest units of the currency (integer, **not** float/double). For example, for a price of ``US$ 1.45`` pass ``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). + is_name_requested (``bool``, *optional*): + True, if the name should be specified. + + is_phone_requested (``bool``, *optional*): + True, if the phone should be specified. + + is_email_requested (``bool``, *optional*): + True, if the email address should be specified. + is_shipping_address_requested (``bool``, *optional*): + True, if the shipping address should be specified. + + is_flexible (``bool``, *optional*): + True, if the final price depends on the shipping method. + + is_phone_to_provider (``bool``, *optional*): + True, if user's phone should be sent to provider. + + is_email_to_provider (``bool``, *optional*): + True, if user's email address should be sent to provider. + + is_recurring (``bool``, *optional*): + Whether this is a recurring payment. + + max_tip_amount (``int``, *optional*): + The maximum accepted amount for tips in the smallest units of the currency (integer, not float/double). + For example, for a price of US$ 1.45 pass 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). + + suggested_tip_amounts (List of ``int``, *optional*): + A vector 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. + + terms_url (``str``, *optional*): + Terms of service URL. + + _raw (:obj:`~raw.base.payments.MessageMediaInvoice` | :obj:`~raw.base.Invoice`, *optional*): + The raw object, as received from the Telegram API. """ def __init__( - self, - *, - client: "pyrogram.Client" = None, - title: str, - description: str, - start_parameter: str, - currency: str, - total_amount: int + self, + *, + client: "pyrogram.Client" = None, + currency: str, + is_test: bool, + title: Optional[str] = None, + description: Optional[str] = None, + total_amount: Optional[int] = None, + start_parameter: Optional[str] = None, + prices: Optional[List["types.LabeledPrice"]] = None, + is_name_requested: Optional[bool] = None, + is_phone_requested: Optional[bool] = None, + is_email_requested: Optional[bool] = None, + is_shipping_address_requested: Optional[bool] = None, + is_flexible: Optional[bool] = None, + is_phone_to_provider: Optional[bool] = None, + is_email_to_provider: Optional[bool] = None, + is_recurring: Optional[bool] = None, + max_tip_amount: Optional[int] = None, + suggested_tip_amounts: Optional[List[int]] = None, + terms_url: Optional[str] = None, + _raw: Union["raw.types.MessageMediaInvoice", "raw.types.Invoice"] = None ): super().__init__(client) + self.currency = currency + self.is_test = is_test self.title = title self.description = description - self.start_parameter = start_parameter - self.currency = currency self.total_amount = total_amount + self.start_parameter = start_parameter + self.prices = prices + self.is_name_requested = is_name_requested + self.is_phone_requested = is_phone_requested + self.is_email_requested = is_email_requested + self.is_shipping_address_requested = is_shipping_address_requested + self.is_flexible = is_flexible + self.is_phone_to_provider = is_phone_to_provider + self.is_email_to_provider = is_email_to_provider + self.is_recurring = is_recurring + self.max_tip_amount = max_tip_amount + self.suggested_tip_amounts = suggested_tip_amounts + self.terms_url = terms_url + self._raw = _raw @staticmethod - def _parse(client, invoice: "raw.types.MessageMediaInvoice") -> "Invoice": + def _parse(client, invoice: Union["raw.types.MessageMediaInvoice", "raw.types.Invoice"]) -> "Invoice": return Invoice( - title=invoice.title, - description=invoice.description, - start_parameter=invoice.start_param, currency=invoice.currency, - total_amount=invoice.total_amount, - # TODO + is_test=invoice.test, + title=getattr(invoice, "title", None), + description=getattr(invoice, "description", None), + total_amount=getattr(invoice, "total_amount", None), + start_parameter=getattr(invoice, "start_param", None) or None, + prices=types.List(types.LabeledPrice._parse(lp) for lp in invoice.prices) if getattr(invoice, "prices", None) else None, + is_name_requested=getattr(invoice, "name_requested", None), + is_phone_requested=getattr(invoice, "phone_requested", None), + is_email_requested=getattr(invoice, "email_requested", None), + is_shipping_address_requested=getattr(invoice, "shipping_address_requested", None), + is_flexible=getattr(invoice, "flexible", None), + is_phone_to_provider=getattr(invoice, "phone_to_provider", None), + is_email_to_provider=getattr(invoice, "email_to_provider", None), + is_recurring=getattr(invoice, "recurring", None), + max_tip_amount=getattr(invoice, "max_tip_amount", None), + suggested_tip_amounts=getattr(invoice, "suggested_tip_amounts", None) or None, + terms_url=getattr(invoice, "terms_url", None), + _raw=invoice, client=client + # TODO: Add photo and extended media ) diff --git a/pyrogram/types/business/shipping_query.py b/pyrogram/types/business/shipping_query.py index 753bac4a96..89114491a4 100644 --- a/pyrogram/types/business/shipping_query.py +++ b/pyrogram/types/business/shipping_query.py @@ -78,7 +78,7 @@ async def _parse( invoice_payload=payload, shipping_address=types.ShippingAddress( country_code=shipping_query.shipping_address.country_iso2, - state=pshipping_query.shipping_address.state, + state=shipping_query.shipping_address.state, city=shipping_query.shipping_address.city, street_line1=shipping_query.shipping_address.street_line1, street_line2=shipping_query.shipping_address.street_line2, diff --git a/pyrogram/types/messages_and_media/__init__.py b/pyrogram/types/messages_and_media/__init__.py index c2f2275944..2b113f652c 100644 --- a/pyrogram/types/messages_and_media/__init__.py +++ b/pyrogram/types/messages_and_media/__init__.py @@ -52,6 +52,7 @@ from .message_reaction_count_updated import MessageReactionCountUpdated from .chat_boost_added import ChatBoostAdded from .story import Story +from .payment_from import PaymentForm from .giveaway import Giveaway from .giveaway_completed import GiveawayCompleted from .giveaway_winners import GiveawayWinners @@ -72,6 +73,7 @@ "Dice", "Document", "Game", + "PaymentForm", "GiftCode", "GiftedPremium", "GiftedStars", diff --git a/pyrogram/types/messages_and_media/message.py b/pyrogram/types/messages_and_media/message.py index 8ff08903be..f772107319 100644 --- a/pyrogram/types/messages_and_media/message.py +++ b/pyrogram/types/messages_and_media/message.py @@ -5233,3 +5233,29 @@ async def translate( message_ids=self.id, to_language_code=to_language_code ) + + + async def pay(self) -> bool: + """Bound method *pay* of :obj:`~pyrogram.types.Message`. + + Use as a shortcut for: + + .. code-block:: python + + await client.send_payment_form( + chat_id=message.chat.id, + message_id=message_id + ) + + Example: + .. code-block:: python + + await message.pay() + + Returns: + True on success. + """ + return await self._client.send_payment_form( + chat_id=self.chat.id, + message_id=self.id + ) diff --git a/pyrogram/types/messages_and_media/payment_from.py b/pyrogram/types/messages_and_media/payment_from.py new file mode 100644 index 0000000000..d63ac5a373 --- /dev/null +++ b/pyrogram/types/messages_and_media/payment_from.py @@ -0,0 +1,117 @@ +# Pyrogram - Telegram MTProto API Client Library for Python +# Copyright (C) 2017-present Dan +# +# This file is part of Pyrogram. +# +# Pyrogram is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Pyrogram 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 General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with Pyrogram. If not, see . + +from typing import Optional + +import pyrogram +from pyrogram import types, raw +from ..object import Object + + +class PaymentForm(Object): + """This object contains basic information about an payment form. + + Parameters: + id (``int``): + Form id. + + bot (``types.User``): + Bot. + + title (``str``): + Form title. + + description (``str``): + Form description. + + invoice (``types.Invoice``): + Invoice. + + provider (``types.User``, *optional*): + Payment provider. + + url (``str``, *optional*): + Payment form URL. + + can_save_credentials (``str``, *optional*): + Whether the user can choose to save credentials. + + is_password_missing (``str``, *optional*): + Indicates that the user can save payment credentials, + but only after setting up a 2FA password + (currently the account doesn't have a 2FA password). + + native_provider (``str``, *optional*): + Payment provider name. + + _raw (:obj:`~raw.base.payments.PaymentForm`, *optional*): + The raw object, as received from the Telegram API. + """ + + def __init__( + self, + *, + client: "pyrogram.Client" = None, + id: int, + bot: "types.User", + title: str, + description: str, + invoice: "types.Invoice", + provider: Optional["types.User"] = None, + url: Optional[str] = None, + can_save_credentials: Optional[bool] = None, + is_password_missing: Optional[bool] = None, + native_provider: Optional[str] = None, + _raw: "raw.base.payments.PaymentForm" = None, + # TODO: Add support for other params: + # native_params + # additional_params + # saved_info + # saved_credentials + ): + super().__init__(client) + + self.id = id + self.bot = bot + self.title = title + self.description = description + self.invoice = invoice + self.provider = provider + self.url = url + self.can_save_credentials = can_save_credentials + self.is_password_missing = is_password_missing + self.native_provider = native_provider + self._raw = _raw + + @staticmethod + def _parse(client, payment_form: "raw.base.payments.PaymentForm") -> "PaymentForm": + users = {i.id: i for i in payment_form.users} + + return PaymentForm( + id=payment_form.form_id, + bot=types.User._parse(client, users.get(payment_form.bot_id)), + title=payment_form.title, + description=payment_form.description, + invoice=types.Invoice._parse(client, payment_form.invoice), + provider=types.User._parse(client, users.get(getattr(payment_form, "provider_id", None))), + url=getattr(payment_form, "url", None), + can_save_credentials=getattr(payment_form, "can_save_credentials", None), + is_password_missing=getattr(payment_form, "password_missing", None), + native_provider=getattr(payment_form, "native_provider", None), + _raw=payment_form + )