mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +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
 |