Skip to content

Commit

Permalink
Add Update.effective_sender (#4168)
Browse files Browse the repository at this point in the history
  • Loading branch information
aelkheir committed Mar 30, 2024
1 parent 2d8d43f commit 23536ee
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 4 deletions.
56 changes: 55 additions & 1 deletion telegram/_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -224,6 +224,7 @@ class Update(TelegramObject):
__slots__ = (
"_effective_chat",
"_effective_message",
"_effective_sender",
"_effective_user",
"callback_query",
"channel_post",
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -444,6 +446,58 @@ 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:
* 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:`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`.
* If :attr:`channel_post` is present, this will give
:attr:`telegram.Message.sender_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 or self.channel_post or self.edited_channel_post
):
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"]:
"""
Expand Down
92 changes: 89 additions & 3 deletions tests/test_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -51,7 +52,21 @@
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, ""),
)
channel_post = Message(
1,
datetime.utcnow(),
Chat(1, ""),
text="Text",
sender_chat=Chat(1, ""),
)
chat_member_updated = ChatMemberUpdated(
Chat(1, "chat"),
User(1, "", False),
Expand Down Expand Up @@ -93,6 +108,7 @@
old_reaction=(ReactionTypeEmoji("👍"),),
new_reaction=(ReactionTypeEmoji("👍"),),
user=User(1, "name", False),
actor_chat=Chat(1, ""),
)


Expand All @@ -108,8 +124,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)},
Expand Down Expand Up @@ -261,6 +277,76 @@ def test_effective_user(self, update):
else:
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()
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.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
):
if update.channel_post or update.edited_channel_post:
assert isinstance(sender, Chat)
else:
assert isinstance(sender, User)

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
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.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
):
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

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
Expand Down

0 comments on commit 23536ee

Please sign in to comment.