scheduled_messages: Set read_by_sender for self-DMs using DM group.

When using direct message group as the recipient for 1:1 or self DMs,
ensure read_by_sender is set correctly when scheduling a message.
This commit is contained in:
Mohammad Reza Kianifar
2025-06-08 23:05:50 +00:00
committed by Tim Abbott
parent bde295806c
commit 9c036024bd
4 changed files with 78 additions and 65 deletions

View File

@@ -64,8 +64,8 @@ def check_schedule_message(
# Legacy default: a scheduled message you sent from a non-API client is
# automatically marked as read for yourself, unless it was sent to
# yourself only.
read_by_sender = (
client.default_read_by_sender() and send_request.message.recipient != sender.recipient
read_by_sender = client.default_read_by_sender() and not addressee.is_message_to_self(
sender
)
return do_schedule_messages(

View File

@@ -97,6 +97,13 @@ class Addressee:
assert self._topic_name is not None
return self._topic_name
def is_message_to_self(self, sender: UserProfile) -> bool:
return (
self.is_private()
and len(self.user_profiles()) == 1
and self.user_profiles()[0].id == sender.id
)
@staticmethod
def legacy_build(
sender: UserProfile,

View File

@@ -15,7 +15,7 @@ from psycopg2.sql import SQL
from analytics.lib.counts import COUNT_STATS
from analytics.models import RealmCount
from zerver.lib.cache import generic_bulk_cached_fetch, to_dict_cache_key_id
from zerver.lib.display_recipient import get_display_recipient_by_id
from zerver.lib.display_recipient import get_display_recipient, get_display_recipient_by_id
from zerver.lib.exceptions import JsonableError, MissingAuthenticationError
from zerver.lib.markdown import MessageRenderingResult
from zerver.lib.mention import MentionData, sender_can_mention_group
@@ -1745,3 +1745,14 @@ def is_1_to_1_message(message: Message) -> bool:
return True
return False
def is_message_to_self(message: Message) -> bool:
if message.recipient.type == Recipient.DIRECT_MESSAGE_GROUP:
group_members = get_display_recipient(message.recipient)
return len(group_members) == 1 and group_members[0]["id"] == message.sender.id
if message.recipient.type == Recipient.PERSONAL:
return message.recipient == message.sender.recipient
return False

View File

@@ -14,10 +14,12 @@ from zerver.actions.scheduled_messages import (
try_deliver_one_scheduled_message,
)
from zerver.actions.users import change_user_is_active
from zerver.lib.message import is_message_to_self
from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.test_helpers import most_recent_message
from zerver.lib.timestamp import timestamp_to_datetime
from zerver.models import Attachment, Message, Recipient, ScheduledMessage, UserMessage
from zerver.models.recipients import get_or_create_direct_message_group
if TYPE_CHECKING:
from django.test.client import _MonkeyPatchedWSGIResponse as TestHttpResponse
@@ -104,13 +106,10 @@ class ScheduledMessageTest(ZulipTestCase):
)
self.assert_json_success(result)
def test_successful_deliver_stream_scheduled_message(self) -> None:
# No scheduled message
result = try_deliver_one_scheduled_message()
self.assertFalse(result)
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
def assert_scheduled_message_delivered(
self, scheduled_message: ScheduledMessage, recipient: Recipient | None
) -> None:
sender = self.example_user("hamlet")
# mock current time to be greater than the scheduled time, so that the `scheduled_message` can be sent.
more_than_scheduled_delivery_datetime = scheduled_message.scheduled_timestamp + timedelta(
@@ -130,23 +129,47 @@ class ScheduledMessageTest(ZulipTestCase):
],
)
scheduled_message.refresh_from_db()
assert isinstance(scheduled_message.delivered_message_id, int)
self.assertEqual(scheduled_message.delivered, True)
self.assertEqual(scheduled_message.failed, False)
self.assertEqual(scheduled_message.recipient, recipient)
self.assertEqual(scheduled_message.sender, sender)
delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
self.assertEqual(delivered_message.content, scheduled_message.content)
self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
self.assertEqual(delivered_message.topic_name(), scheduled_message.topic_name())
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
self.assertEqual(delivered_message.recipient, scheduled_message.recipient)
self.assertEqual(delivered_message.sender, scheduled_message.sender)
def test_successful_deliver_direct_scheduled_message(self) -> None:
sender_user_message = UserMessage.objects.get(
message_id=scheduled_message.delivered_message_id, user_profile_id=sender.id
)
self.assertEqual(
sender_user_message.flags.read, not is_message_to_self(delivered_message)
)
def test_successful_deliver_stream_scheduled_message(self) -> None:
# No scheduled message
result = try_deliver_one_scheduled_message()
self.assertFalse(result)
self.create_scheduled_message()
scheduled_message = self.last_scheduled_message()
stream_id = self.get_stream_id("Verona")
recipient = Recipient.objects.get(type=Recipient.STREAM, type_id=stream_id)
self.assert_scheduled_message_delivered(scheduled_message, recipient)
def test_successful_deliver_direct_scheduled_message_to_other(self) -> None:
# No scheduled message
self.assertFalse(try_deliver_one_scheduled_message())
content = "Test message"
scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
sender = self.example_user("hamlet")
othello = self.example_user("othello")
response = self.do_schedule_message(
"direct", [othello.id], f"{content} 3", scheduled_delivery_timestamp
@@ -154,33 +177,7 @@ class ScheduledMessageTest(ZulipTestCase):
self.assert_json_success(response)
scheduled_message = self.last_scheduled_message()
# mock current time to be greater than the scheduled time.
more_than_scheduled_delivery_datetime = scheduled_delivery_datetime + timedelta(minutes=1)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})"
],
)
scheduled_message.refresh_from_db()
assert isinstance(scheduled_message.delivered_message_id, int)
self.assertEqual(scheduled_message.delivered, True)
self.assertEqual(scheduled_message.failed, False)
delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
self.assertEqual(delivered_message.content, scheduled_message.content)
self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
sender_user_message = UserMessage.objects.get(
message_id=scheduled_message.delivered_message_id, user_profile_id=sender.id
)
self.assertTrue(sender_user_message.flags.read)
self.assert_scheduled_message_delivered(scheduled_message, recipient=othello.recipient)
# Check error is sent if an edit happens after the scheduled
# message is successfully sent.
@@ -210,33 +207,31 @@ class ScheduledMessageTest(ZulipTestCase):
self.assert_json_success(response)
scheduled_message = self.last_scheduled_message()
# mock current time to be greater than the scheduled time.
more_than_scheduled_delivery_datetime = scheduled_delivery_datetime + timedelta(minutes=1)
self.assert_scheduled_message_delivered(scheduled_message, recipient=sender.recipient)
with (
time_machine.travel(more_than_scheduled_delivery_datetime, tick=False),
self.assertLogs(level="INFO") as logs,
):
result = try_deliver_one_scheduled_message()
self.assertTrue(result)
self.assertEqual(
logs.output,
[
f"INFO:root:Sending scheduled message {scheduled_message.id} with date {scheduled_message.scheduled_timestamp} (sender: {scheduled_message.sender_id})"
],
)
scheduled_message.refresh_from_db()
assert isinstance(scheduled_message.delivered_message_id, int)
self.assertEqual(scheduled_message.delivered, True)
self.assertEqual(scheduled_message.failed, False)
delivered_message = Message.objects.get(id=scheduled_message.delivered_message_id)
self.assertEqual(delivered_message.content, scheduled_message.content)
self.assertEqual(delivered_message.rendered_content, scheduled_message.rendered_content)
self.assertEqual(delivered_message.date_sent, more_than_scheduled_delivery_datetime)
sender_user_message = UserMessage.objects.get(
message_id=scheduled_message.delivered_message_id, user_profile_id=sender.id
)
self.assertFalse(sender_user_message.flags.read)
def test_successful_deliver_direct_scheduled_message_to_self_using_direct_message_group(
self,
) -> None:
# No scheduled message
self.assertFalse(try_deliver_one_scheduled_message())
content = "Test message to self"
scheduled_delivery_datetime = timezone_now() + timedelta(minutes=5)
scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
sender = self.example_user("hamlet")
# Create a direct message group for the sender.
direct_message_group = get_or_create_direct_message_group(id_list=[sender.id])
response = self.do_schedule_message(
"direct", [sender.id], content, scheduled_delivery_timestamp
)
self.assert_json_success(response)
scheduled_message = self.last_scheduled_message()
self.assert_scheduled_message_delivered(
scheduled_message, recipient=direct_message_group.recipient
)
def verify_deliver_scheduled_message_failure(
self, scheduled_message: ScheduledMessage, expected_failure_message: str