mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 12:03:46 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			370 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			370 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| import math
 | |
| from collections.abc import Collection
 | |
| from dataclasses import dataclass
 | |
| from typing import Any
 | |
| 
 | |
| from zerver.lib.mention import MentionData
 | |
| from zerver.lib.user_groups import get_user_group_member_ids
 | |
| from zerver.models import NamedUserGroup, UserProfile, UserTopic
 | |
| from zerver.models.scheduled_jobs import NotificationTriggers
 | |
| 
 | |
| 
 | |
| @dataclass
 | |
| class UserMessageNotificationsData:
 | |
|     user_id: int
 | |
|     online_push_enabled: bool
 | |
|     dm_email_notify: bool
 | |
|     dm_push_notify: bool
 | |
|     mention_email_notify: bool
 | |
|     mention_push_notify: bool
 | |
|     topic_wildcard_mention_email_notify: bool
 | |
|     topic_wildcard_mention_push_notify: bool
 | |
|     stream_wildcard_mention_email_notify: bool
 | |
|     stream_wildcard_mention_push_notify: bool
 | |
|     stream_push_notify: bool
 | |
|     stream_email_notify: bool
 | |
|     followed_topic_push_notify: bool
 | |
|     followed_topic_email_notify: bool
 | |
|     topic_wildcard_mention_in_followed_topic_push_notify: bool
 | |
|     topic_wildcard_mention_in_followed_topic_email_notify: bool
 | |
|     stream_wildcard_mention_in_followed_topic_push_notify: bool
 | |
|     stream_wildcard_mention_in_followed_topic_email_notify: bool
 | |
|     sender_is_muted: bool
 | |
|     disable_external_notifications: bool
 | |
| 
 | |
|     def __post_init__(self) -> None:
 | |
|         # Check that there's no dubious data.
 | |
|         if self.dm_email_notify or self.dm_push_notify:
 | |
|             assert not (
 | |
|                 self.stream_email_notify
 | |
|                 or self.stream_push_notify
 | |
|                 or self.followed_topic_email_notify
 | |
|                 or self.followed_topic_push_notify
 | |
|             )
 | |
| 
 | |
|         if (
 | |
|             self.stream_email_notify
 | |
|             or self.stream_push_notify
 | |
|             or self.followed_topic_email_notify
 | |
|             or self.followed_topic_push_notify
 | |
|         ):
 | |
|             assert not (self.dm_email_notify or self.dm_push_notify)
 | |
| 
 | |
|     @classmethod
 | |
|     def from_user_id_sets(
 | |
|         cls,
 | |
|         *,
 | |
|         user_id: int,
 | |
|         flags: Collection[str],
 | |
|         private_message: bool,
 | |
|         disable_external_notifications: bool,
 | |
|         online_push_user_ids: set[int],
 | |
|         dm_mention_push_disabled_user_ids: set[int],
 | |
|         dm_mention_email_disabled_user_ids: set[int],
 | |
|         stream_push_user_ids: set[int],
 | |
|         stream_email_user_ids: set[int],
 | |
|         topic_wildcard_mention_user_ids: set[int],
 | |
|         stream_wildcard_mention_user_ids: set[int],
 | |
|         followed_topic_push_user_ids: set[int],
 | |
|         followed_topic_email_user_ids: set[int],
 | |
|         topic_wildcard_mention_in_followed_topic_user_ids: set[int],
 | |
|         stream_wildcard_mention_in_followed_topic_user_ids: set[int],
 | |
|         muted_sender_user_ids: set[int],
 | |
|         all_bot_user_ids: set[int],
 | |
|     ) -> "UserMessageNotificationsData":
 | |
|         if user_id in all_bot_user_ids:
 | |
|             # Don't send any notifications to bots
 | |
|             return cls(
 | |
|                 user_id=user_id,
 | |
|                 dm_email_notify=False,
 | |
|                 mention_email_notify=False,
 | |
|                 topic_wildcard_mention_email_notify=False,
 | |
|                 stream_wildcard_mention_email_notify=False,
 | |
|                 dm_push_notify=False,
 | |
|                 mention_push_notify=False,
 | |
|                 topic_wildcard_mention_push_notify=False,
 | |
|                 stream_wildcard_mention_push_notify=False,
 | |
|                 online_push_enabled=False,
 | |
|                 stream_push_notify=False,
 | |
|                 stream_email_notify=False,
 | |
|                 followed_topic_push_notify=False,
 | |
|                 followed_topic_email_notify=False,
 | |
|                 topic_wildcard_mention_in_followed_topic_push_notify=False,
 | |
|                 topic_wildcard_mention_in_followed_topic_email_notify=False,
 | |
|                 stream_wildcard_mention_in_followed_topic_push_notify=False,
 | |
|                 stream_wildcard_mention_in_followed_topic_email_notify=False,
 | |
|                 sender_is_muted=False,
 | |
|                 disable_external_notifications=False,
 | |
|             )
 | |
| 
 | |
|         # `stream_wildcard_mention_user_ids`, `topic_wildcard_mention_user_ids`,
 | |
|         # `stream_wildcard_mention_in_followed_topic_user_ids` and `topic_wildcard_mention_in_followed_topic_user_ids`
 | |
|         # are those user IDs for whom stream or topic wildcard mentions should obey notification
 | |
|         # settings for personal mentions. Hence, it isn't an independent notification setting and acts as a wrapper.
 | |
|         dm_email_notify = user_id not in dm_mention_email_disabled_user_ids and private_message
 | |
|         mention_email_notify = (
 | |
|             user_id not in dm_mention_email_disabled_user_ids and "mentioned" in flags
 | |
|         )
 | |
|         topic_wildcard_mention_email_notify = (
 | |
|             user_id in topic_wildcard_mention_user_ids
 | |
|             and user_id not in dm_mention_email_disabled_user_ids
 | |
|             and "topic_wildcard_mentioned" in flags
 | |
|         )
 | |
|         stream_wildcard_mention_email_notify = (
 | |
|             user_id in stream_wildcard_mention_user_ids
 | |
|             and user_id not in dm_mention_email_disabled_user_ids
 | |
|             and "stream_wildcard_mentioned" in flags
 | |
|         )
 | |
|         topic_wildcard_mention_in_followed_topic_email_notify = (
 | |
|             user_id in topic_wildcard_mention_in_followed_topic_user_ids
 | |
|             and user_id not in dm_mention_email_disabled_user_ids
 | |
|             and "topic_wildcard_mentioned" in flags
 | |
|         )
 | |
|         stream_wildcard_mention_in_followed_topic_email_notify = (
 | |
|             user_id in stream_wildcard_mention_in_followed_topic_user_ids
 | |
|             and user_id not in dm_mention_email_disabled_user_ids
 | |
|             and "stream_wildcard_mentioned" in flags
 | |
|         )
 | |
| 
 | |
|         dm_push_notify = user_id not in dm_mention_push_disabled_user_ids and private_message
 | |
|         mention_push_notify = (
 | |
|             user_id not in dm_mention_push_disabled_user_ids and "mentioned" in flags
 | |
|         )
 | |
|         topic_wildcard_mention_push_notify = (
 | |
|             user_id in topic_wildcard_mention_user_ids
 | |
|             and user_id not in dm_mention_push_disabled_user_ids
 | |
|             and "topic_wildcard_mentioned" in flags
 | |
|         )
 | |
|         stream_wildcard_mention_push_notify = (
 | |
|             user_id in stream_wildcard_mention_user_ids
 | |
|             and user_id not in dm_mention_push_disabled_user_ids
 | |
|             and "stream_wildcard_mentioned" in flags
 | |
|         )
 | |
|         topic_wildcard_mention_in_followed_topic_push_notify = (
 | |
|             user_id in topic_wildcard_mention_in_followed_topic_user_ids
 | |
|             and user_id not in dm_mention_push_disabled_user_ids
 | |
|             and "topic_wildcard_mentioned" in flags
 | |
|         )
 | |
|         stream_wildcard_mention_in_followed_topic_push_notify = (
 | |
|             user_id in stream_wildcard_mention_in_followed_topic_user_ids
 | |
|             and user_id not in dm_mention_push_disabled_user_ids
 | |
|             and "stream_wildcard_mentioned" in flags
 | |
|         )
 | |
|         return cls(
 | |
|             user_id=user_id,
 | |
|             dm_email_notify=dm_email_notify,
 | |
|             mention_email_notify=mention_email_notify,
 | |
|             topic_wildcard_mention_email_notify=topic_wildcard_mention_email_notify,
 | |
|             stream_wildcard_mention_email_notify=stream_wildcard_mention_email_notify,
 | |
|             dm_push_notify=dm_push_notify,
 | |
|             mention_push_notify=mention_push_notify,
 | |
|             topic_wildcard_mention_push_notify=topic_wildcard_mention_push_notify,
 | |
|             stream_wildcard_mention_push_notify=stream_wildcard_mention_push_notify,
 | |
|             online_push_enabled=user_id in online_push_user_ids,
 | |
|             stream_push_notify=user_id in stream_push_user_ids,
 | |
|             stream_email_notify=user_id in stream_email_user_ids,
 | |
|             followed_topic_push_notify=user_id in followed_topic_push_user_ids,
 | |
|             followed_topic_email_notify=user_id in followed_topic_email_user_ids,
 | |
|             topic_wildcard_mention_in_followed_topic_push_notify=topic_wildcard_mention_in_followed_topic_push_notify,
 | |
|             topic_wildcard_mention_in_followed_topic_email_notify=topic_wildcard_mention_in_followed_topic_email_notify,
 | |
|             stream_wildcard_mention_in_followed_topic_push_notify=stream_wildcard_mention_in_followed_topic_push_notify,
 | |
|             stream_wildcard_mention_in_followed_topic_email_notify=stream_wildcard_mention_in_followed_topic_email_notify,
 | |
|             sender_is_muted=user_id in muted_sender_user_ids,
 | |
|             disable_external_notifications=disable_external_notifications,
 | |
|         )
 | |
| 
 | |
|     # For these functions, acting_user_id is the user sent a message
 | |
|     # (or edited a message) triggering the event for which we need to
 | |
|     # determine notifiability.
 | |
|     def trivially_should_not_notify(self, acting_user_id: int) -> bool:
 | |
|         """Common check for reasons not to trigger a notification that arex
 | |
|         independent of users' notification settings and thus don't
 | |
|         depend on what type of notification (email/push) it is.
 | |
|         """
 | |
|         if self.user_id == acting_user_id:
 | |
|             return True
 | |
| 
 | |
|         if self.sender_is_muted:
 | |
|             return True
 | |
| 
 | |
|         if self.disable_external_notifications:
 | |
|             return True
 | |
| 
 | |
|         return False
 | |
| 
 | |
|     def is_notifiable(self, acting_user_id: int, idle: bool) -> bool:
 | |
|         return self.is_email_notifiable(acting_user_id, idle) or self.is_push_notifiable(
 | |
|             acting_user_id, idle
 | |
|         )
 | |
| 
 | |
|     def is_push_notifiable(self, acting_user_id: int, idle: bool) -> bool:
 | |
|         return self.get_push_notification_trigger(acting_user_id, idle) is not None
 | |
| 
 | |
|     def get_push_notification_trigger(self, acting_user_id: int, idle: bool) -> str | None:
 | |
|         if not idle and not self.online_push_enabled:
 | |
|             return None
 | |
| 
 | |
|         if self.trivially_should_not_notify(acting_user_id):
 | |
|             return None
 | |
| 
 | |
|         # The order here is important. If, for example, both
 | |
|         # `mention_push_notify` and `stream_push_notify` are True, we
 | |
|         # want to classify it as a mention, since that's more salient.
 | |
|         if self.dm_push_notify:
 | |
|             return NotificationTriggers.DIRECT_MESSAGE
 | |
|         elif self.mention_push_notify:
 | |
|             return NotificationTriggers.MENTION
 | |
|         elif self.topic_wildcard_mention_in_followed_topic_push_notify:
 | |
|             return NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
 | |
|         elif self.stream_wildcard_mention_in_followed_topic_push_notify:
 | |
|             return NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
 | |
|         elif self.topic_wildcard_mention_push_notify:
 | |
|             return NotificationTriggers.TOPIC_WILDCARD_MENTION
 | |
|         elif self.stream_wildcard_mention_push_notify:
 | |
|             return NotificationTriggers.STREAM_WILDCARD_MENTION
 | |
|         elif self.followed_topic_push_notify:
 | |
|             return NotificationTriggers.FOLLOWED_TOPIC_PUSH
 | |
|         elif self.stream_push_notify:
 | |
|             return NotificationTriggers.STREAM_PUSH
 | |
|         else:
 | |
|             return None
 | |
| 
 | |
|     def is_email_notifiable(self, acting_user_id: int, idle: bool) -> bool:
 | |
|         return self.get_email_notification_trigger(acting_user_id, idle) is not None
 | |
| 
 | |
|     def get_email_notification_trigger(self, acting_user_id: int, idle: bool) -> str | None:
 | |
|         if not idle:
 | |
|             return None
 | |
| 
 | |
|         if self.trivially_should_not_notify(acting_user_id):
 | |
|             return None
 | |
| 
 | |
|         # The order here is important. If, for example, both
 | |
|         # `mention_email_notify` and `stream_email_notify` are True, we
 | |
|         # want to classify it as a mention, since that's more salient.
 | |
|         if self.dm_email_notify:
 | |
|             return NotificationTriggers.DIRECT_MESSAGE
 | |
|         elif self.mention_email_notify:
 | |
|             return NotificationTriggers.MENTION
 | |
|         elif self.topic_wildcard_mention_in_followed_topic_email_notify:
 | |
|             return NotificationTriggers.TOPIC_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
 | |
|         elif self.stream_wildcard_mention_in_followed_topic_email_notify:
 | |
|             return NotificationTriggers.STREAM_WILDCARD_MENTION_IN_FOLLOWED_TOPIC
 | |
|         elif self.topic_wildcard_mention_email_notify:
 | |
|             return NotificationTriggers.TOPIC_WILDCARD_MENTION
 | |
|         elif self.stream_wildcard_mention_email_notify:
 | |
|             return NotificationTriggers.STREAM_WILDCARD_MENTION
 | |
|         elif self.followed_topic_email_notify:
 | |
|             return NotificationTriggers.FOLLOWED_TOPIC_EMAIL
 | |
|         elif self.stream_email_notify:
 | |
|             return NotificationTriggers.STREAM_EMAIL
 | |
|         else:
 | |
|             return None
 | |
| 
 | |
| 
 | |
| def user_allows_notifications_in_StreamTopic(
 | |
|     stream_is_muted: bool,
 | |
|     visibility_policy: int,
 | |
|     stream_specific_setting: bool | None,
 | |
|     global_setting: bool,
 | |
| ) -> bool:
 | |
|     """
 | |
|     Captures the hierarchy of notification settings, where visibility policy is considered first,
 | |
|     followed by stream-specific settings, and the global-setting in the UserProfile is the fallback.
 | |
|     """
 | |
|     if stream_is_muted and visibility_policy != UserTopic.VisibilityPolicy.UNMUTED:
 | |
|         return False
 | |
| 
 | |
|     if visibility_policy == UserTopic.VisibilityPolicy.MUTED:
 | |
|         return False
 | |
| 
 | |
|     if stream_specific_setting is not None:
 | |
|         return stream_specific_setting
 | |
| 
 | |
|     return global_setting
 | |
| 
 | |
| 
 | |
| def get_user_group_mentions_data(
 | |
|     mentioned_user_ids: set[int], mentioned_user_group_ids: list[int], mention_data: MentionData
 | |
| ) -> dict[int, int]:
 | |
|     # Maps user_id -> mentioned user_group_id
 | |
|     mentioned_user_groups_map: dict[int, int] = dict()
 | |
| 
 | |
|     # Add members of the mentioned user groups into `mentions_user_ids`.
 | |
|     for group_id in mentioned_user_group_ids:
 | |
|         member_ids = mention_data.get_group_members(group_id)
 | |
|         for member_id in member_ids:
 | |
|             if member_id in mentioned_user_ids:
 | |
|                 # If a user is also mentioned personally, we use that as a trigger
 | |
|                 # for notifications.
 | |
|                 continue
 | |
| 
 | |
|             if member_id in mentioned_user_groups_map:
 | |
|                 # If multiple user groups are mentioned, we prefer the
 | |
|                 # user group with the least members for email/mobile
 | |
|                 # notifications.
 | |
|                 previous_group_id = mentioned_user_groups_map[member_id]
 | |
|                 previous_group_member_ids = mention_data.get_group_members(previous_group_id)
 | |
| 
 | |
|                 if len(previous_group_member_ids) > len(member_ids):
 | |
|                     mentioned_user_groups_map[member_id] = group_id
 | |
|             else:
 | |
|                 mentioned_user_groups_map[member_id] = group_id
 | |
| 
 | |
|     return mentioned_user_groups_map
 | |
| 
 | |
| 
 | |
| @dataclass
 | |
| class MentionedUserGroup:
 | |
|     id: int
 | |
|     name: str
 | |
|     members_count: int
 | |
| 
 | |
| 
 | |
| def get_mentioned_user_group(
 | |
|     messages: list[dict[str, Any]], user_profile: UserProfile
 | |
| ) -> MentionedUserGroup | None:
 | |
|     """Returns the user group name to display in the email notification
 | |
|     if user group(s) are mentioned.
 | |
| 
 | |
|     This implements the same algorithm as get_user_group_mentions_data
 | |
|     in zerver/lib/notification_data.py, but we're passed a list of
 | |
|     messages instead.
 | |
|     """
 | |
|     for message in messages:
 | |
|         if (
 | |
|             message.get("mentioned_user_group_id") is None
 | |
|             and message["trigger"] == NotificationTriggers.MENTION
 | |
|         ):
 | |
|             # The user has also been personally mentioned, so that gets prioritized.
 | |
|             return None
 | |
| 
 | |
|     # These IDs are those of the smallest user groups mentioned in each message.
 | |
|     mentioned_user_group_ids = [
 | |
|         message["mentioned_user_group_id"]
 | |
|         for message in messages
 | |
|         if message.get("mentioned_user_group_id") is not None
 | |
|     ]
 | |
| 
 | |
|     if len(mentioned_user_group_ids) == 0:
 | |
|         return None
 | |
| 
 | |
|     # We now want to calculate the name of the smallest user group mentioned among
 | |
|     # all these messages.
 | |
|     smallest_user_group_size = math.inf
 | |
|     for user_group_id in mentioned_user_group_ids:
 | |
|         current_user_group = NamedUserGroup.objects.get(id=user_group_id, realm=user_profile.realm)
 | |
|         current_mentioned_user_group = MentionedUserGroup(
 | |
|             id=current_user_group.id,
 | |
|             name=current_user_group.name,
 | |
|             members_count=len(get_user_group_member_ids(current_user_group)),
 | |
|         )
 | |
| 
 | |
|         if current_mentioned_user_group.members_count < smallest_user_group_size:
 | |
|             # If multiple user groups are mentioned, we prefer the
 | |
|             # user group with the least members.
 | |
|             smallest_user_group_size = current_mentioned_user_group.members_count
 | |
|             smallest_mentioned_user_group = current_mentioned_user_group
 | |
| 
 | |
|     return smallest_mentioned_user_group
 |