users: Send user creation events when sending DMs.

We now send user creation events to recipient users
when sending DMs if recipients gain access to either
sender or other pariticpating users in the DM.
This commit is contained in:
Sahil Batra 2023-10-25 17:32:18 +05:30 committed by Tim Abbott
parent e4a97dd3ac
commit 32c15d67b5
4 changed files with 144 additions and 4 deletions

View File

@ -75,7 +75,13 @@ from zerver.lib.timestamp import timestamp_to_datetime
from zerver.lib.topic import participants_for_topic from zerver.lib.topic import participants_for_topic
from zerver.lib.url_preview.types import UrlEmbedData from zerver.lib.url_preview.types import UrlEmbedData
from zerver.lib.user_message import UserMessageLite, bulk_insert_ums from zerver.lib.user_message import UserMessageLite, bulk_insert_ums
from zerver.lib.users import check_user_can_access_all_users, get_accessible_user_ids from zerver.lib.users import (
check_can_access_user,
check_user_can_access_all_users,
get_accessible_user_ids,
get_subscribers_of_target_user_subscriptions,
get_users_involved_in_dms_with_target_users,
)
from zerver.lib.validator import check_widget_content from zerver.lib.validator import check_widget_content
from zerver.lib.widget import do_widget_post_save_actions from zerver.lib.widget import do_widget_post_save_actions
from zerver.models import ( from zerver.models import (
@ -85,6 +91,7 @@ from zerver.models import (
Realm, Realm,
Recipient, Recipient,
Stream, Stream,
SystemGroups,
UserMessage, UserMessage,
UserPresence, UserPresence,
UserProfile, UserProfile,
@ -571,6 +578,7 @@ def build_message_send_dict(
mention_backend: Optional[MentionBackend] = None, mention_backend: Optional[MentionBackend] = None,
limit_unread_user_ids: Optional[Set[int]] = None, limit_unread_user_ids: Optional[Set[int]] = None,
disable_external_notifications: bool = False, disable_external_notifications: bool = False,
recipients_for_user_creation_events: Optional[Dict[UserProfile, Set[int]]] = None,
) -> SendMessageRequest: ) -> SendMessageRequest:
"""Returns a dictionary that can be passed into do_send_messages. In """Returns a dictionary that can be passed into do_send_messages. In
production, this is always called by check_message, but some production, this is always called by check_message, but some
@ -698,6 +706,7 @@ def build_message_send_dict(
limit_unread_user_ids=limit_unread_user_ids, limit_unread_user_ids=limit_unread_user_ids,
disable_external_notifications=disable_external_notifications, disable_external_notifications=disable_external_notifications,
topic_participant_user_ids=topic_participant_user_ids, topic_participant_user_ids=topic_participant_user_ids,
recipients_for_user_creation_events=recipients_for_user_creation_events,
) )
return message_send_dict return message_send_dict
@ -1057,6 +1066,15 @@ def do_send_messages(
user_notifications_data_list=user_notifications_data_list, user_notifications_data_list=user_notifications_data_list,
) )
if send_request.recipients_for_user_creation_events is not None:
from zerver.actions.create_user import notify_created_user
for (
new_accessible_user,
notify_user_ids,
) in send_request.recipients_for_user_creation_events.items():
notify_created_user(new_accessible_user, list(notify_user_ids))
event = dict( event = dict(
type="message", type="message",
message=send_request.message.id, message=send_request.message.id,
@ -1476,6 +1494,59 @@ def check_sender_can_access_recipients(
raise JsonableError(_("You do not have permission to access some of the recipients.")) raise JsonableError(_("You do not have permission to access some of the recipients."))
def get_recipients_for_user_creation_events(
realm: Realm, sender: UserProfile, user_profiles: Sequence[UserProfile]
) -> Dict[UserProfile, Set[int]]:
"""
This function returns a dictionary with data about which users would
receive stream creation events due to gaining access to a user.
The key of the dictionary is a user object and the value is a set of
user_ids that would gain access to that user.
"""
recipients_for_user_creation_events: Dict[UserProfile, Set[int]] = defaultdict(set)
# If none of the users in the direct message conversation are
# guests, then there is no possible can_access_all_users_group
# policy that would mean sending this message changes any user's
# user access to other users.
guest_recipients = [user for user in user_profiles if user.is_guest]
if len(guest_recipients) == 0:
return recipients_for_user_creation_events
if realm.can_access_all_users_group.name == SystemGroups.EVERYONE:
return recipients_for_user_creation_events
if len(user_profiles) == 1:
if not check_can_access_user(sender, user_profiles[0]):
recipients_for_user_creation_events[sender].add(user_profiles[0].id)
return recipients_for_user_creation_events
users_involved_in_dms = get_users_involved_in_dms_with_target_users(guest_recipients, realm)
subscribers_of_guest_recipient_subscriptions = get_subscribers_of_target_user_subscriptions(
guest_recipients
)
for recipient_user in guest_recipients:
for user in user_profiles:
if user.id == recipient_user.id or user.is_bot:
continue
if (
user.id not in users_involved_in_dms[recipient_user.id]
and user.id not in subscribers_of_guest_recipient_subscriptions[recipient_user.id]
):
recipients_for_user_creation_events[user].add(recipient_user.id)
if (
not sender.is_bot
and sender.id not in users_involved_in_dms[recipient_user.id]
and sender.id not in subscribers_of_guest_recipient_subscriptions[recipient_user.id]
):
recipients_for_user_creation_events[sender].add(recipient_user.id)
return recipients_for_user_creation_events
# check_message: # check_message:
# Returns message ready for sending with do_send_message on success or the error message (string) on error. # Returns message ready for sending with do_send_message on success or the error message (string) on error.
def check_message( def check_message(
@ -1508,6 +1579,7 @@ def check_message(
if realm is None: if realm is None:
realm = sender.realm realm = sender.realm
recipients_for_user_creation_events = None
if addressee.is_stream(): if addressee.is_stream():
topic_name = addressee.topic() topic_name = addressee.topic()
topic_name = truncate_topic(topic_name) topic_name = truncate_topic(topic_name)
@ -1565,6 +1637,10 @@ def check_message(
check_private_message_policy(realm, sender, user_profiles) check_private_message_policy(realm, sender, user_profiles)
recipients_for_user_creation_events = get_recipients_for_user_creation_events(
realm, sender, user_profiles
)
# API super-users who set the `forged` flag are allowed to # API super-users who set the `forged` flag are allowed to
# forge messages sent by any user, so we disable the # forge messages sent by any user, so we disable the
# `forwarded_mirror_message` security check in that case. # `forwarded_mirror_message` security check in that case.
@ -1629,6 +1705,7 @@ def check_message(
mention_backend=mention_backend, mention_backend=mention_backend,
limit_unread_user_ids=limit_unread_user_ids, limit_unread_user_ids=limit_unread_user_ids,
disable_external_notifications=disable_external_notifications, disable_external_notifications=disable_external_notifications,
recipients_for_user_creation_events=recipients_for_user_creation_events,
) )
if ( if (

View File

@ -414,12 +414,11 @@ _check_topic_links = DictType(
] ]
) )
message_fields = [ basic_message_fields = [
("avatar_url", OptionalType(str)), ("avatar_url", OptionalType(str)),
("client", str), ("client", str),
("content", str), ("content", str),
("content_type", Equals("text/html")), ("content_type", Equals("text/html")),
("display_recipient", str),
("id", int), ("id", int),
("is_me_message", bool), ("is_me_message", bool),
("reactions", ListType(dict)), ("reactions", ListType(dict)),
@ -428,7 +427,6 @@ message_fields = [
("sender_email", str), ("sender_email", str),
("sender_full_name", str), ("sender_full_name", str),
("sender_id", int), ("sender_id", int),
("stream_id", int),
(TOPIC_NAME, str), (TOPIC_NAME, str),
(TOPIC_LINKS, ListType(_check_topic_links)), (TOPIC_LINKS, ListType(_check_topic_links)),
("submessages", ListType(dict)), ("submessages", ListType(dict)),
@ -436,6 +434,12 @@ message_fields = [
("type", str), ("type", str),
] ]
message_fields = [
*basic_message_fields,
("display_recipient", str),
("stream_id", int),
]
message_event = event_dict_type( message_event = event_dict_type(
required_keys=[ required_keys=[
("type", Equals("message")), ("type", Equals("message")),
@ -445,6 +449,28 @@ message_event = event_dict_type(
) )
check_message = make_checker(message_event) check_message = make_checker(message_event)
_check_direct_message_display_recipient = DictType(
required_keys=[
("id", int),
("is_mirror_dummy", bool),
("email", str),
("full_name", str),
]
)
direct_message_fields = [
*basic_message_fields,
("display_recipient", ListType(_check_direct_message_display_recipient)),
]
direct_message_event = event_dict_type(
required_keys=[
("type", Equals("message")),
("flags", ListType(str)),
("message", DictType(direct_message_fields)),
]
)
check_direct_message = make_checker(direct_message_event)
# This legacy presence structure is intended to be replaced by a more # This legacy presence structure is intended to be replaced by a more
# sensible data structure. # sensible data structure.
presence_type = DictType( presence_type = DictType(

View File

@ -209,6 +209,7 @@ class SendMessageRequest:
service_queue_events: Optional[Dict[str, List[Dict[str, Any]]]] = None service_queue_events: Optional[Dict[str, List[Dict[str, Any]]]] = None
disable_external_notifications: bool = False disable_external_notifications: bool = False
automatic_new_visibility_policy: Optional[int] = None automatic_new_visibility_policy: Optional[int] = None
recipients_for_user_creation_events: Optional[Dict[UserProfile, Set[int]]] = None
# We won't try to fetch more unread message IDs from the database than # We won't try to fetch more unread message IDs from the database than

View File

@ -138,6 +138,7 @@ from zerver.lib.event_schema import (
check_default_stream_groups, check_default_stream_groups,
check_default_streams, check_default_streams,
check_delete_message, check_delete_message,
check_direct_message,
check_draft_add, check_draft_add,
check_draft_remove, check_draft_remove,
check_draft_update, check_draft_update,
@ -536,6 +537,41 @@ class NormalActionsTest(BaseAction):
lambda: self.send_huddle_message(self.example_user("cordelia"), huddle, "hola"), lambda: self.send_huddle_message(self.example_user("cordelia"), huddle, "hola"),
) )
def test_user_creation_events_on_sending_messages(self) -> None:
self.set_up_db_for_testing_user_access()
polonius = self.example_user("polonius")
cordelia = self.example_user("cordelia")
self.user_profile = polonius
# Test that guest will not receive creation event
# for bots as they can access all the bots.
bot = self.create_test_bot("test2", cordelia, full_name="Test bot")
events = self.verify_action(
lambda: self.send_personal_message(bot, polonius, "hola"), num_events=1
)
check_direct_message("events[0]", events[0])
events = self.verify_action(
lambda: self.send_personal_message(cordelia, polonius, "hola"), num_events=2
)
check_direct_message("events[0]", events[0])
check_realm_user_add("events[1]", events[1])
self.assertEqual(events[1]["person"]["user_id"], cordelia.id)
othello = self.example_user("othello")
desdemona = self.example_user("desdemona")
events = self.verify_action(
lambda: self.send_huddle_message(othello, [polonius, desdemona, bot], "hola"),
num_events=3,
)
check_direct_message("events[0]", events[0])
check_realm_user_add("events[1]", events[1])
check_realm_user_add("events[2]", events[2])
user_creation_user_ids = {events[1]["person"]["user_id"], events[2]["person"]["user_id"]}
self.assertEqual(user_creation_user_ids, {othello.id, desdemona.id})
def test_stream_send_message_events(self) -> None: def test_stream_send_message_events(self) -> None:
hamlet = self.example_user("hamlet") hamlet = self.example_user("hamlet")
for stream_name in ["Verona", "Denmark", "core team"]: for stream_name in ["Verona", "Denmark", "core team"]: