Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Comment thread
Bibo-Joshi marked this conversation as resolved.

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},
Comment thread
Bibo-Joshi marked this conversation as resolved.
{"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