diff --git a/zerver/actions/message_send.py b/zerver/actions/message_send.py index 8437da7357..b51b1ed871 100644 --- a/zerver/actions/message_send.py +++ b/zerver/actions/message_send.py @@ -75,7 +75,13 @@ from zerver.lib.timestamp import timestamp_to_datetime from zerver.lib.topic import participants_for_topic from zerver.lib.url_preview.types import UrlEmbedData 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.widget import do_widget_post_save_actions from zerver.models import ( @@ -85,6 +91,7 @@ from zerver.models import ( Realm, Recipient, Stream, + SystemGroups, UserMessage, UserPresence, UserProfile, @@ -571,6 +578,7 @@ def build_message_send_dict( mention_backend: Optional[MentionBackend] = None, limit_unread_user_ids: Optional[Set[int]] = None, disable_external_notifications: bool = False, + recipients_for_user_creation_events: Optional[Dict[UserProfile, Set[int]]] = None, ) -> SendMessageRequest: """Returns a dictionary that can be passed into do_send_messages. In 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, disable_external_notifications=disable_external_notifications, topic_participant_user_ids=topic_participant_user_ids, + recipients_for_user_creation_events=recipients_for_user_creation_events, ) return message_send_dict @@ -1057,6 +1066,15 @@ def do_send_messages( 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( type="message", 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.")) +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: # Returns message ready for sending with do_send_message on success or the error message (string) on error. def check_message( @@ -1508,6 +1579,7 @@ def check_message( if realm is None: realm = sender.realm + recipients_for_user_creation_events = None if addressee.is_stream(): topic_name = addressee.topic() topic_name = truncate_topic(topic_name) @@ -1565,6 +1637,10 @@ def check_message( 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 # forge messages sent by any user, so we disable the # `forwarded_mirror_message` security check in that case. @@ -1629,6 +1705,7 @@ def check_message( mention_backend=mention_backend, limit_unread_user_ids=limit_unread_user_ids, disable_external_notifications=disable_external_notifications, + recipients_for_user_creation_events=recipients_for_user_creation_events, ) if ( diff --git a/zerver/lib/event_schema.py b/zerver/lib/event_schema.py index bd8a99482b..1fe7f3b172 100644 --- a/zerver/lib/event_schema.py +++ b/zerver/lib/event_schema.py @@ -414,12 +414,11 @@ _check_topic_links = DictType( ] ) -message_fields = [ +basic_message_fields = [ ("avatar_url", OptionalType(str)), ("client", str), ("content", str), ("content_type", Equals("text/html")), - ("display_recipient", str), ("id", int), ("is_me_message", bool), ("reactions", ListType(dict)), @@ -428,7 +427,6 @@ message_fields = [ ("sender_email", str), ("sender_full_name", str), ("sender_id", int), - ("stream_id", int), (TOPIC_NAME, str), (TOPIC_LINKS, ListType(_check_topic_links)), ("submessages", ListType(dict)), @@ -436,6 +434,12 @@ message_fields = [ ("type", str), ] +message_fields = [ + *basic_message_fields, + ("display_recipient", str), + ("stream_id", int), +] + message_event = event_dict_type( required_keys=[ ("type", Equals("message")), @@ -445,6 +449,28 @@ message_event = event_dict_type( ) 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 # sensible data structure. presence_type = DictType( diff --git a/zerver/lib/message.py b/zerver/lib/message.py index e5cb1ec0e4..a4531d0c53 100644 --- a/zerver/lib/message.py +++ b/zerver/lib/message.py @@ -209,6 +209,7 @@ class SendMessageRequest: service_queue_events: Optional[Dict[str, List[Dict[str, Any]]]] = None disable_external_notifications: bool = False 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 diff --git a/zerver/tests/test_events.py b/zerver/tests/test_events.py index eebc554903..4af973dd43 100644 --- a/zerver/tests/test_events.py +++ b/zerver/tests/test_events.py @@ -138,6 +138,7 @@ from zerver.lib.event_schema import ( check_default_stream_groups, check_default_streams, check_delete_message, + check_direct_message, check_draft_add, check_draft_remove, check_draft_update, @@ -536,6 +537,41 @@ class NormalActionsTest(BaseAction): 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: hamlet = self.example_user("hamlet") for stream_name in ["Verona", "Denmark", "core team"]: