From 9c3e0158a304f734c8254715cb0ec888cd5b11c4 Mon Sep 17 00:00:00 2001 From: aelkheir Date: Sat, 23 Mar 2024 13:58:03 +0300 Subject: [PATCH 1/6] Add property `Update.effective_sender`. --- telegram/_update.py | 54 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/telegram/_update.py b/telegram/_update.py index 566ca9cfd3f..151bde24161 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -18,7 +18,7 @@ # along with this program. If not, see [http://www.gnu.org/licenses/]. """This module contains an object that represents a Telegram Update.""" -from typing import TYPE_CHECKING, Final, List, Optional +from typing import TYPE_CHECKING, Final, List, Optional, Union from telegram import constants from telegram._callbackquery import CallbackQuery @@ -224,6 +224,7 @@ class Update(TelegramObject): __slots__ = ( "_effective_chat", "_effective_message", + "_effective_sender", "_effective_user", "callback_query", "channel_post", @@ -371,6 +372,7 @@ def __init__( self.message_reaction_count: Optional[MessageReactionCountUpdated] = message_reaction_count self._effective_user: Optional[User] = None + self._effective_sender: Optional[Union["User", "Chat"]] = None self._effective_chat: Optional[Chat] = None self._effective_message: Optional[Message] = None @@ -444,6 +446,56 @@ def effective_user(self) -> Optional["User"]: self._effective_user = user return user + @property + def effective_sender(self) -> Optional[Union["User", "Chat"]]: + """ + :class:`telegram.User` or :class:`telegram.Chat`: The user or chat that sent this update, + no matter what kind of update this is. + + Note: + * When the user is anonymous this is either :class:`telegram.Chat` or :obj:`None`. + * If the user is not anonymous it's either :class:`telegram.User` or :obj:`None`. + + If no user whatsoever is associated with this update, this gives :obj:`None`. This + is the case if any of + + * :attr:`channel_post` + * :attr:`edited_channel_post` + * :attr:`poll` + * :attr:`chat_boost` + * :attr:`removed_chat_boost` + * :attr:`message_reaction_count` + + is present. + + Example: + * If :attr:`message` is present, this will give either + :attr:`telegram.Message.from_user` or :attr:`telegram.Message.sender_chat`. + * If :attr:`poll_answer` is present, this will give either + :attr:`telegram.PollAnswer.user` or :attr:`telegram.PollAnswer.voter_chat`. + + .. versionadded:: NEXT.VERSION + """ + if self._effective_sender: + return self._effective_sender + + sender: Optional[Union["User", "Chat"]] = None + + if message := (self.message or self.edited_message): + sender = message.sender_chat + + elif self.poll_answer: + sender = self.poll_answer.voter_chat + + elif self.message_reaction: + sender = self.message_reaction.actor_chat + + if sender is None: + sender = self.effective_user + + self._effective_sender = sender + return sender + @property def effective_chat(self) -> Optional["Chat"]: """ From 96ca73d6ca42389dc7f2e7196fa507367b477bd4 Mon Sep 17 00:00:00 2001 From: aelkheir Date: Sat, 23 Mar 2024 14:00:15 +0300 Subject: [PATCH 2/6] Add tests. --- tests/test_update.py | 51 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/test_update.py b/tests/test_update.py index 7c7a35a4c02..139192b346e 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -51,7 +51,14 @@ from telegram.warnings import PTBUserWarning from tests.auxil.slots import mro_slots -message = Message(1, datetime.utcnow(), Chat(1, ""), from_user=User(1, "", False), text="Text") +message = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + from_user=User(1, "", False), + text="Text", + sender_chat=Chat(1, ""), +) chat_member_updated = ChatMemberUpdated( Chat(1, "chat"), User(1, "", False), @@ -93,6 +100,7 @@ old_reaction=(ReactionTypeEmoji("👍"),), new_reaction=(ReactionTypeEmoji("👍"),), user=User(1, "name", False), + actor_chat=Chat(1, ""), ) @@ -261,6 +269,47 @@ def test_effective_user(self, update): else: assert user is None + def test_effective_sender(self, update): + # Test that it's sometimes None per docstring + sender = update.effective_sender + if not ( + update.channel_post is not None + or update.edited_channel_post is not None + or update.poll is not None + or update.chat_boost is not None + or update.removed_chat_boost is not None + or update.message_reaction_count is not None + ): + assert sender.id == 1 + else: + assert sender is None + + def test_effective_sender_anonymous(self, update): + # remove none anonymous user + if message := (update.message or update.edited_message): + message._unfreeze() + message.from_user = None + elif reaction := (update.message_reaction): + reaction._unfreeze() + reaction.user = None + elif answer := (update.poll_answer): + answer._unfreeze() + answer.user = None + + # Test that it's sometimes None per docstring + sender = update.effective_sender + if not ( + update.channel_post is not None + or update.edited_channel_post is not None + or update.poll is not None + or update.chat_boost is not None + or update.removed_chat_boost is not None + or update.message_reaction_count is not None + ): + assert sender.id == 1 + else: + assert sender is None + def test_effective_message(self, update): # Test that it's sometimes None per docstring eff_message = update.effective_message From 8ca3df3e0d57011bbe1e75b0c761a3851519a559 Mon Sep 17 00:00:00 2001 From: aelkheir Date: Mon, 25 Mar 2024 09:16:23 +0300 Subject: [PATCH 3/6] Include `channel_post` and `edited_channel_post`. --- telegram/_update.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/telegram/_update.py b/telegram/_update.py index 151bde24161..a597f7792f8 100644 --- a/telegram/_update.py +++ b/telegram/_update.py @@ -453,14 +453,12 @@ def effective_sender(self) -> Optional[Union["User", "Chat"]]: no matter what kind of update this is. Note: - * When the user is anonymous this is either :class:`telegram.Chat` or :obj:`None`. - * If the user is not anonymous it's either :class:`telegram.User` or :obj:`None`. + * Depending on the type of update and the user's 'Remain anonymous' setting, this + could either be :class:`telegram.User`, :class:`telegram.Chat` or :obj:`None`. If no user whatsoever is associated with this update, this gives :obj:`None`. This is the case if any of - * :attr:`channel_post` - * :attr:`edited_channel_post` * :attr:`poll` * :attr:`chat_boost` * :attr:`removed_chat_boost` @@ -473,6 +471,8 @@ def effective_sender(self) -> Optional[Union["User", "Chat"]]: :attr:`telegram.Message.from_user` or :attr:`telegram.Message.sender_chat`. * If :attr:`poll_answer` is present, this will give either :attr:`telegram.PollAnswer.user` or :attr:`telegram.PollAnswer.voter_chat`. + * If :attr:`channel_post` is present, this will give + :attr:`telegram.Message.sender_chat`. .. versionadded:: NEXT.VERSION """ @@ -481,7 +481,9 @@ def effective_sender(self) -> Optional[Union["User", "Chat"]]: sender: Optional[Union["User", "Chat"]] = None - if message := (self.message or self.edited_message): + if message := ( + self.message or self.edited_message or self.channel_post or self.edited_channel_post + ): sender = message.sender_chat elif self.poll_answer: From 62b99a685c8fcf4ca95e6c2b4d1253e21060ca10 Mon Sep 17 00:00:00 2001 From: aelkheir Date: Mon, 25 Mar 2024 09:19:46 +0300 Subject: [PATCH 4/6] Refine tests. --- tests/test_update.py | 52 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/tests/test_update.py b/tests/test_update.py index 139192b346e..0eb08ea007b 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -59,6 +59,13 @@ text="Text", sender_chat=Chat(1, ""), ) +channel_post = Message( + 1, + datetime.utcnow(), + Chat(1, ""), + text="Text", + sender_chat=Chat(1, ""), +) chat_member_updated = ChatMemberUpdated( Chat(1, "chat"), User(1, "", False), @@ -116,8 +123,8 @@ {"message": message}, {"edited_message": message}, {"callback_query": CallbackQuery(1, User(1, "", False), "chat", message=message)}, - {"channel_post": message}, - {"edited_channel_post": message}, + {"channel_post": channel_post}, + {"edited_channel_post": channel_post}, {"inline_query": InlineQuery(1, User(1, "", False), "", "")}, {"chosen_inline_result": ChosenInlineResult("id", User(1, "", False), "")}, {"shipping_query": ShippingQuery("id", User(1, "", False), "", None)}, @@ -269,23 +276,36 @@ def test_effective_user(self, update): else: assert user is None - def test_effective_sender(self, update): + def test_effective_sender_non_anonymous(self, update): + # Simulate 'Remain anonymous' being turned off + if message := (update.message or update.edited_message): + message._unfreeze() + message.sender_chat = None + elif reaction := (update.message_reaction): + reaction._unfreeze() + reaction.actor_chat = None + elif answer := (update.poll_answer): + answer._unfreeze() + answer.voter_chat = None + # Test that it's sometimes None per docstring sender = update.effective_sender if not ( - update.channel_post is not None - or update.edited_channel_post is not None - or update.poll is not None + update.poll is not None or update.chat_boost is not None or update.removed_chat_boost is not None or update.message_reaction_count is not None ): - assert sender.id == 1 + if update.channel_post or update.edited_channel_post: + assert isinstance(sender, Chat) + else: + assert isinstance(sender, User) + else: assert sender is None def test_effective_sender_anonymous(self, update): - # remove none anonymous user + # Simulate 'Remain anonymous' being turned on if message := (update.message or update.edited_message): message._unfreeze() message.from_user = None @@ -299,14 +319,22 @@ def test_effective_sender_anonymous(self, update): # Test that it's sometimes None per docstring sender = update.effective_sender if not ( - update.channel_post is not None - or update.edited_channel_post is not None - or update.poll is not None + update.poll is not None or update.chat_boost is not None or update.removed_chat_boost is not None or update.message_reaction_count is not None ): - assert sender.id == 1 + if ( + update.message + or update.edited_message + or update.channel_post + or update.edited_channel_post + or update.message_reaction + or update.poll_answer + ): + assert isinstance(sender, Chat) + else: + assert isinstance(sender, User) else: assert sender is None From 25ee2253f9ddcb9aff176654d1af1d6d6e84a1c6 Mon Sep 17 00:00:00 2001 From: aelkheir Date: Thu, 28 Mar 2024 07:09:35 +0300 Subject: [PATCH 5/6] Fix tests. --- tests/test_update.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_update.py b/tests/test_update.py index 0eb08ea007b..a9d0f524812 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -17,6 +17,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 time +from copy import deepcopy from datetime import datetime import pytest @@ -277,6 +278,7 @@ def test_effective_user(self, update): assert user is None def test_effective_sender_non_anonymous(self, update): + update = deepcopy(update) # Simulate 'Remain anonymous' being turned off if message := (update.message or update.edited_message): message._unfreeze() @@ -305,6 +307,7 @@ def test_effective_sender_non_anonymous(self, update): assert sender is None def test_effective_sender_anonymous(self, update): + update = deepcopy(update) # Simulate 'Remain anonymous' being turned on if message := (update.message or update.edited_message): message._unfreeze() From 485ac0ecfd91837c924a34891a5f1234f4374324 Mon Sep 17 00:00:00 2001 From: aelkheir Date: Sat, 30 Mar 2024 10:17:18 +0200 Subject: [PATCH 6/6] Test property caching. --- tests/test_update.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test_update.py b/tests/test_update.py index a9d0f524812..e46608f8a46 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -306,6 +306,9 @@ def test_effective_sender_non_anonymous(self, update): else: assert sender is None + cached = update.effective_sender + assert cached is sender + def test_effective_sender_anonymous(self, update): update = deepcopy(update) # Simulate 'Remain anonymous' being turned on @@ -341,6 +344,9 @@ def test_effective_sender_anonymous(self, update): else: assert sender is None + cached = update.effective_sender + assert cached is sender + def test_effective_message(self, update): # Test that it's sometimes None per docstring eff_message = update.effective_message