diff --git a/README.rst b/README.rst index e8aecc8df93..d58e814c391 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ :target: https://pypi.org/project/python-telegram-bot/ :alt: Supported Python versions -.. image:: https://img.shields.io/badge/Bot%20API-7.6-blue?logo=telegram +.. image:: https://img.shields.io/badge/Bot%20API-7.7-blue?logo=telegram :target: https://core.telegram.org/bots/api-changelog :alt: Supported Bot API version @@ -79,7 +79,7 @@ make the development of bots easy and straightforward. These classes are contain Telegram API support ==================== -All types and methods of the Telegram Bot API **7.6** are supported. +All types and methods of the Telegram Bot API **7.7** are supported. Installing ========== diff --git a/docs/source/telegram.payments-tree.rst b/docs/source/telegram.payments-tree.rst index 2a09c5415ac..0db0ba21959 100644 --- a/docs/source/telegram.payments-tree.rst +++ b/docs/source/telegram.payments-tree.rst @@ -8,6 +8,7 @@ Payments telegram.labeledprice telegram.orderinfo telegram.precheckoutquery + telegram.refundedpayment telegram.revenuewithdrawalstate telegram.revenuewithdrawalstatefailed telegram.revenuewithdrawalstatepending diff --git a/docs/source/telegram.refundedpayment.rst b/docs/source/telegram.refundedpayment.rst new file mode 100644 index 00000000000..f99349c859c --- /dev/null +++ b/docs/source/telegram.refundedpayment.rst @@ -0,0 +1,6 @@ +RefundedPayment +=============== + +.. autoclass:: telegram.RefundedPayment + :members: + :show-inheritance: diff --git a/telegram/__init__.py b/telegram/__init__.py index af2336a4ac9..5b52bf85c40 100644 --- a/telegram/__init__.py +++ b/telegram/__init__.py @@ -204,6 +204,7 @@ "ReactionType", "ReactionTypeCustomEmoji", "ReactionTypeEmoji", + "RefundedPayment", "ReplyKeyboardMarkup", "ReplyKeyboardRemove", "ReplyParameters", @@ -446,6 +447,7 @@ from ._payment.labeledprice import LabeledPrice from ._payment.orderinfo import OrderInfo from ._payment.precheckoutquery import PreCheckoutQuery +from ._payment.refundedpayment import RefundedPayment from ._payment.shippingaddress import ShippingAddress from ._payment.shippingoption import ShippingOption from ._payment.shippingquery import ShippingQuery diff --git a/telegram/_message.py b/telegram/_message.py index b52b2bc9b48..5c45b9582a4 100644 --- a/telegram/_message.py +++ b/telegram/_message.py @@ -55,6 +55,7 @@ from telegram._paidmedia import PaidMediaInfo from telegram._passport.passportdata import PassportData from telegram._payment.invoice import Invoice +from telegram._payment.refundedpayment import RefundedPayment from telegram._payment.successfulpayment import SuccessfulPayment from telegram._poll import Poll from telegram._proximityalerttriggered import ProximityAlertTriggered @@ -576,6 +577,10 @@ class Message(MaybeInaccessibleMessage): paid_media (:obj:`telegram.PaidMediaInfo`, optional): Message contains paid media; information about the paid media. + .. versionadded:: NEXT.VERSION + refunded_payment (:obj:`telegram.RefundedPayment`, optional): Message is a service message + about a refunded payment, information about the payment. + .. versionadded:: NEXT.VERSION Attributes: @@ -894,6 +899,10 @@ class Message(MaybeInaccessibleMessage): paid_media (:obj:`telegram.PaidMediaInfo`): Optional. Message contains paid media; information about the paid media. + .. versionadded:: NEXT.VERSION + refunded_payment (:obj:`telegram.RefundedPayment`): Optional. Message is a service message + about a refunded payment, information about the payment. + .. versionadded:: NEXT.VERSION .. |custom_emoji_no_md1_support| replace:: Since custom emoji entities are not supported by @@ -968,6 +977,7 @@ class Message(MaybeInaccessibleMessage): "poll", "proximity_alert_triggered", "quote", + "refunded_payment", "reply_markup", "reply_to_message", "reply_to_story", @@ -1080,6 +1090,7 @@ def __init__( effect_id: Optional[str] = None, show_caption_above_media: Optional[bool] = None, paid_media: Optional[PaidMediaInfo] = None, + refunded_payment: Optional[RefundedPayment] = None, *, api_kwargs: Optional[JSONDict] = None, ): @@ -1182,6 +1193,7 @@ def __init__( self.effect_id: Optional[str] = effect_id self.show_caption_above_media: Optional[bool] = show_caption_above_media self.paid_media: Optional[PaidMediaInfo] = paid_media + self.refunded_payment: Optional[RefundedPayment] = refunded_payment self._effective_attachment = DEFAULT_NONE @@ -1298,6 +1310,7 @@ def de_json(cls, data: Optional[JSONDict], bot: Optional["Bot"] = None) -> Optio data["chat_shared"] = ChatShared.de_json(data.get("chat_shared"), bot) data["chat_background_set"] = ChatBackground.de_json(data.get("chat_background_set"), bot) data["paid_media"] = PaidMediaInfo.de_json(data.get("paid_media"), bot) + data["refunded_payment"] = RefundedPayment.de_json(data.get("refunded_payment"), bot) # Unfortunately, this needs to be here due to cyclic imports from telegram._giveaway import ( # pylint: disable=import-outside-toplevel diff --git a/telegram/_payment/refundedpayment.py b/telegram/_payment/refundedpayment.py new file mode 100644 index 00000000000..19bdfe84649 --- /dev/null +++ b/telegram/_payment/refundedpayment.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 RefundedPayment.""" + +from typing import Optional + +from telegram._telegramobject import TelegramObject +from telegram._utils.types import JSONDict + + +class RefundedPayment(TelegramObject): + """This object contains basic information about a refunded payment. + + Objects of this class are comparable in terms of equality. Two objects of this class are + considered equal, if their :attr:`telegram_payment_charge_id` is equal. + + Args: + currency (:obj:`str`): Three-letter ISO 4217 `currency + `_ code, or ``XTR`` for + payments in |tg_stars|. Currently, always ``XTR``. + total_amount (:obj:`int`): Total refunded price in the *smallest units* of the currency + (integer, **not** float/double). For example, for a price of ``US$ 1.45``, + ``total_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). + invoice_payload (:obj:`str`): Bot-specified invoice payload. + telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. + provider_payment_charge_id (:obj:`str`, optional): Provider payment identifier. + + Attributes: + currency (:obj:`str`): Three-letter ISO 4217 `currency + `_ code, or ``XTR`` for + payments in |tg_stars|. Currently, always ``XTR``. + total_amount (:obj:`int`): Total refunded price in the *smallest units* of the currency + (integer, **not** float/double). For example, for a price of ``US$ 1.45``, + ``total_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). + invoice_payload (:obj:`str`): Bot-specified invoice payload. + telegram_payment_charge_id (:obj:`str`): Telegram payment identifier. + provider_payment_charge_id (:obj:`str`): Optional. Provider payment identifier. + + """ + + __slots__ = ( + "currency", + "invoice_payload", + "provider_payment_charge_id", + "telegram_payment_charge_id", + "total_amount", + ) + + def __init__( + self, + currency: str, + total_amount: int, + invoice_payload: str, + telegram_payment_charge_id: str, + provider_payment_charge_id: Optional[str] = None, + *, + api_kwargs: Optional[JSONDict] = None, + ): + super().__init__(api_kwargs=api_kwargs) + self.currency: str = currency + self.total_amount: int = total_amount + self.invoice_payload: str = invoice_payload + self.telegram_payment_charge_id: str = telegram_payment_charge_id + # Optional + self.provider_payment_charge_id: Optional[str] = provider_payment_charge_id + + self._id_attrs = (self.telegram_payment_charge_id,) + + self._freeze() diff --git a/telegram/constants.py b/telegram/constants.py index d9b26b417c6..9cd41b5f3a2 100644 --- a/telegram/constants.py +++ b/telegram/constants.py @@ -151,7 +151,7 @@ class _AccentColor(NamedTuple): #: :data:`telegram.__bot_api_version_info__`. #: #: .. versionadded:: 20.0 -BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=6) +BOT_API_VERSION_INFO: Final[_BotAPIVersion] = _BotAPIVersion(major=7, minor=7) #: :obj:`str`: Telegram Bot API #: version supported by this version of `python-telegram-bot`. Also available as #: :data:`telegram.__bot_api_version__`. @@ -1920,6 +1920,11 @@ class MessageType(StringEnum): """:obj:`str`: Messages with :attr:`telegram.Message.poll`.""" PROXIMITY_ALERT_TRIGGERED = "proximity_alert_triggered" """:obj:`str`: Messages with :attr:`telegram.Message.proximity_alert_triggered`.""" + REFUNDED_PAYMENT = "refunded_payment" + """:obj:`str`: Messages with :attr:`telegram.Message.refunded_payment`. + + .. versionadded:: NEXT.VERSION + """ REPLY_TO_STORY = "reply_to_story" """:obj:`str`: Messages with :attr:`telegram.Message.reply_to_story`. diff --git a/telegram/ext/filters.py b/telegram/ext/filters.py index 5147574e07a..fa260ad7476 100644 --- a/telegram/ext/filters.py +++ b/telegram/ext/filters.py @@ -1944,6 +1944,7 @@ def filter(self, update: Update) -> bool: or StatusUpdate.NEW_CHAT_TITLE.check_update(update) or StatusUpdate.PINNED_MESSAGE.check_update(update) or StatusUpdate.PROXIMITY_ALERT_TRIGGERED.check_update(update) + or StatusUpdate.REFUNDED_PAYMENT.check_update(update) or StatusUpdate.USERS_SHARED.check_update(update) or StatusUpdate.USER_SHARED.check_update(update) or StatusUpdate.VIDEO_CHAT_ENDED.check_update(update) @@ -2190,6 +2191,17 @@ def filter(self, message: Message) -> bool: ) """Messages that contain :attr:`telegram.Message.proximity_alert_triggered`.""" + class _RefundedPayment(MessageFilter): + __slots__ = () + + def filter(self, message: Message) -> bool: + return bool(message.refunded_payment) + + REFUNDED_PAYMENT = _RefundedPayment("filters.StatusUpdate.REFUNDED_PAYMENT") + """Messages that contain :attr:`telegram.Message.refunded_payment`. + .. versionadded:: NEXT.VERSION + """ + class _UserShared(MessageFilter): __slots__ = () diff --git a/tests/_payment/test_refundedpayment.py b/tests/_payment/test_refundedpayment.py new file mode 100644 index 00000000000..75e252660da --- /dev/null +++ b/tests/_payment/test_refundedpayment.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# +# A library that provides a Python interface to the Telegram Bot API +# Copyright (C) 2015-2024 +# 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 RefundedPayment +from tests.auxil.slots import mro_slots + + +@pytest.fixture(scope="module") +def refunded_payment(): + return RefundedPayment( + TestRefundedPaymentBase.currency, + TestRefundedPaymentBase.total_amount, + TestRefundedPaymentBase.invoice_payload, + TestRefundedPaymentBase.telegram_payment_charge_id, + TestRefundedPaymentBase.provider_payment_charge_id, + ) + + +class TestRefundedPaymentBase: + invoice_payload = "invoice_payload" + currency = "EUR" + total_amount = 100 + telegram_payment_charge_id = "telegram_payment_charge_id" + provider_payment_charge_id = "provider_payment_charge_id" + + +class TestRefundedPaymentWithoutRequest(TestRefundedPaymentBase): + def test_slot_behaviour(self, refunded_payment): + inst = refunded_payment + for attr in inst.__slots__: + assert getattr(inst, attr, "err") != "err", f"got extra slot '{attr}'" + assert len(mro_slots(inst)) == len(set(mro_slots(inst))), "duplicate slot" + + def test_de_json(self, bot): + json_dict = { + "invoice_payload": self.invoice_payload, + "currency": self.currency, + "total_amount": self.total_amount, + "telegram_payment_charge_id": self.telegram_payment_charge_id, + "provider_payment_charge_id": self.provider_payment_charge_id, + } + refunded_payment = RefundedPayment.de_json(json_dict, bot) + assert refunded_payment.api_kwargs == {} + + assert refunded_payment.invoice_payload == self.invoice_payload + assert refunded_payment.currency == self.currency + assert refunded_payment.total_amount == self.total_amount + assert refunded_payment.telegram_payment_charge_id == self.telegram_payment_charge_id + assert refunded_payment.provider_payment_charge_id == self.provider_payment_charge_id + + def test_to_dict(self, refunded_payment): + refunded_payment_dict = refunded_payment.to_dict() + + assert isinstance(refunded_payment_dict, dict) + assert refunded_payment_dict["invoice_payload"] == refunded_payment.invoice_payload + assert refunded_payment_dict["currency"] == refunded_payment.currency + assert refunded_payment_dict["total_amount"] == refunded_payment.total_amount + assert ( + refunded_payment_dict["telegram_payment_charge_id"] + == refunded_payment.telegram_payment_charge_id + ) + assert ( + refunded_payment_dict["provider_payment_charge_id"] + == refunded_payment.provider_payment_charge_id + ) + + def test_equality(self): + a = RefundedPayment( + self.currency, + self.total_amount, + self.invoice_payload, + self.telegram_payment_charge_id, + self.provider_payment_charge_id, + ) + b = RefundedPayment( + self.currency, + self.total_amount, + self.invoice_payload, + self.telegram_payment_charge_id, + self.provider_payment_charge_id, + ) + c = RefundedPayment("", 0, "", self.telegram_payment_charge_id) + d = RefundedPayment( + self.currency, + self.total_amount, + self.invoice_payload, + "", + ) + + 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/ext/test_filters.py b/tests/ext/test_filters.py index 97d17e2ebaf..0ea2b34b0d1 100644 --- a/tests/ext/test_filters.py +++ b/tests/ext/test_filters.py @@ -1095,6 +1095,11 @@ def test_filters_status_update(self, update): assert filters.StatusUpdate.CHAT_BACKGROUND_SET.check_update(update) update.message.chat_background_set = None + update.message.refunded_payment = "refunded_payment" + assert filters.StatusUpdate.ALL.check_update(update) + assert filters.StatusUpdate.REFUNDED_PAYMENT.check_update(update) + update.message.refunded_payment = None + def test_filters_forwarded(self, update, message_origin_user): assert filters.FORWARDED.check_update(update) update.message.forward_origin = MessageOriginHiddenUser(datetime.datetime.utcnow(), 1) diff --git a/tests/test_message.py b/tests/test_message.py index 5596710396d..6352845ffa2 100644 --- a/tests/test_message.py +++ b/tests/test_message.py @@ -53,6 +53,7 @@ Poll, PollOption, ProximityAlertTriggered, + RefundedPayment, ReplyParameters, SharedUser, Sticker, @@ -278,6 +279,7 @@ def message(bot): {"effect_id": "123456789"}, {"show_caption_above_media": True}, {"paid_media": PaidMediaInfo(5, [PaidMediaPreview(10, 10, 10)])}, + {"refunded_payment": RefundedPayment("EUR", 243, "payload", "charge_id", "provider_id")}, ], ids=[ "reply", @@ -350,6 +352,7 @@ def message(bot): "effect_id", "show_caption_above_media", "paid_media", + "refunded_payment", ], ) def message_params(bot, request):