mirror of https://github.com/zulip/zulip.git
topic_mentions: Fetch users to be notified of @topic mentions.
This commit adds the 'topic_wildcard_mention_user_ids' and 'topic_wildcard_mention_in_followed_topic_user_ids' attributes to the 'RecipientInfoResult' dataclass. Only topic participants are notified of @topic mentions. Topic participants are anyone who sent a message to a topic or reacted to a message on the topic. 'topic_wildcard_mention_in_followed_topic_user_ids' stores the ids of the topic participants who follow the topic and have enabled the wildcard mention notifications for followed topics. 'topic_wildcard_mention_user_ids' stores the ids of the topic participants for whom 'user_allows_notifications_in_StreamTopic' with setting 'wildcard_mentions_notify' returns True.
This commit is contained in:
parent
1df63ed448
commit
c0c30bc5f7
|
@ -466,6 +466,7 @@ def do_update_message(
|
||||||
recipient=target_message.recipient,
|
recipient=target_message.recipient,
|
||||||
sender_id=target_message.sender_id,
|
sender_id=target_message.sender_id,
|
||||||
stream_topic=stream_topic,
|
stream_topic=stream_topic,
|
||||||
|
possible_topic_wildcard_mention=mention_data.message_has_topic_wildcards(),
|
||||||
possible_stream_wildcard_mention=mention_data.message_has_stream_wildcards(),
|
possible_stream_wildcard_mention=mention_data.message_has_stream_wildcards(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -69,7 +69,10 @@ from zerver.lib.stream_topic import StreamTopicTarget
|
||||||
from zerver.lib.streams import access_stream_for_send_message, ensure_stream
|
from zerver.lib.streams import access_stream_for_send_message, ensure_stream
|
||||||
from zerver.lib.string_validation import check_stream_name
|
from zerver.lib.string_validation import check_stream_name
|
||||||
from zerver.lib.timestamp import timestamp_to_datetime
|
from zerver.lib.timestamp import timestamp_to_datetime
|
||||||
from zerver.lib.topic import filter_by_exact_message_topic
|
from zerver.lib.topic import (
|
||||||
|
filter_by_exact_message_topic,
|
||||||
|
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.validator import check_widget_content
|
from zerver.lib.validator import check_widget_content
|
||||||
|
@ -166,9 +169,11 @@ class RecipientInfoResult:
|
||||||
pm_mention_push_disabled_user_ids: Set[int]
|
pm_mention_push_disabled_user_ids: Set[int]
|
||||||
stream_email_user_ids: Set[int]
|
stream_email_user_ids: Set[int]
|
||||||
stream_push_user_ids: Set[int]
|
stream_push_user_ids: Set[int]
|
||||||
|
topic_wildcard_mention_user_ids: Set[int]
|
||||||
stream_wildcard_mention_user_ids: Set[int]
|
stream_wildcard_mention_user_ids: Set[int]
|
||||||
followed_topic_email_user_ids: Set[int]
|
followed_topic_email_user_ids: Set[int]
|
||||||
followed_topic_push_user_ids: Set[int]
|
followed_topic_push_user_ids: Set[int]
|
||||||
|
topic_wildcard_mention_in_followed_topic_user_ids: Set[int]
|
||||||
stream_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]
|
muted_sender_user_ids: Set[int]
|
||||||
um_eligible_user_ids: Set[int]
|
um_eligible_user_ids: Set[int]
|
||||||
|
@ -195,13 +200,16 @@ def get_recipient_info(
|
||||||
sender_id: int,
|
sender_id: int,
|
||||||
stream_topic: Optional[StreamTopicTarget],
|
stream_topic: Optional[StreamTopicTarget],
|
||||||
possibly_mentioned_user_ids: AbstractSet[int] = set(),
|
possibly_mentioned_user_ids: AbstractSet[int] = set(),
|
||||||
|
possible_topic_wildcard_mention: bool = True,
|
||||||
possible_stream_wildcard_mention: bool = True,
|
possible_stream_wildcard_mention: bool = True,
|
||||||
) -> RecipientInfoResult:
|
) -> RecipientInfoResult:
|
||||||
stream_push_user_ids: Set[int] = set()
|
stream_push_user_ids: Set[int] = set()
|
||||||
stream_email_user_ids: Set[int] = set()
|
stream_email_user_ids: Set[int] = set()
|
||||||
|
topic_wildcard_mention_user_ids: Set[int] = set()
|
||||||
stream_wildcard_mention_user_ids: Set[int] = set()
|
stream_wildcard_mention_user_ids: Set[int] = set()
|
||||||
followed_topic_push_user_ids: Set[int] = set()
|
followed_topic_push_user_ids: Set[int] = set()
|
||||||
followed_topic_email_user_ids: Set[int] = set()
|
followed_topic_email_user_ids: Set[int] = set()
|
||||||
|
topic_wildcard_mention_in_followed_topic_user_ids: Set[int] = set()
|
||||||
stream_wildcard_mention_in_followed_topic_user_ids: Set[int] = set()
|
stream_wildcard_mention_in_followed_topic_user_ids: Set[int] = set()
|
||||||
muted_sender_user_ids: Set[int] = get_muting_users(sender_id)
|
muted_sender_user_ids: Set[int] = get_muting_users(sender_id)
|
||||||
|
|
||||||
|
@ -217,12 +225,23 @@ def get_recipient_info(
|
||||||
# of this function for different message types.
|
# of this function for different message types.
|
||||||
assert stream_topic is not None
|
assert stream_topic is not None
|
||||||
|
|
||||||
|
topic_participant_user_ids: Set[int] = set()
|
||||||
|
if possible_topic_wildcard_mention:
|
||||||
|
# A topic participant is anyone who either sent or reacted to messages in the topic.
|
||||||
|
# It is expensive to call `participants_for_topic` if the topic has a large number
|
||||||
|
# of messages. But it is fine to call it here, as this gets called only if the message
|
||||||
|
# has syntax that might be a @topic mention without having confirmed the syntax isn't, say,
|
||||||
|
# in a code block.
|
||||||
|
topic_participant_user_ids = participants_for_topic(
|
||||||
|
recipient.id, stream_topic.topic_name
|
||||||
|
)
|
||||||
subscription_rows = (
|
subscription_rows = (
|
||||||
get_subscriptions_for_send_message(
|
get_subscriptions_for_send_message(
|
||||||
realm_id=realm_id,
|
realm_id=realm_id,
|
||||||
stream_id=stream_topic.stream_id,
|
stream_id=stream_topic.stream_id,
|
||||||
topic_name=stream_topic.topic_name,
|
topic_name=stream_topic.topic_name,
|
||||||
possible_stream_wildcard_mention=possible_stream_wildcard_mention,
|
possible_stream_wildcard_mention=possible_stream_wildcard_mention,
|
||||||
|
topic_participant_user_ids=topic_participant_user_ids,
|
||||||
possibly_mentioned_user_ids=possibly_mentioned_user_ids,
|
possibly_mentioned_user_ids=possibly_mentioned_user_ids,
|
||||||
)
|
)
|
||||||
.annotate(
|
.annotate(
|
||||||
|
@ -293,16 +312,33 @@ def get_recipient_info(
|
||||||
)
|
)
|
||||||
followed_topic_push_user_ids = followed_topic_notification_recipients("push_notifications")
|
followed_topic_push_user_ids = followed_topic_notification_recipients("push_notifications")
|
||||||
|
|
||||||
if possible_stream_wildcard_mention:
|
if possible_stream_wildcard_mention or possible_topic_wildcard_mention:
|
||||||
# We calculate `stream_wildcard_mention_user_ids` and `followed_topic_wildcard_mention_user_ids`
|
# We calculate `wildcard_mentions_notify_user_ids` and `followed_topic_wildcard_mentions_notify_user_ids`
|
||||||
# only if there's a possible stream wildcard mention in the message. This is important so as
|
# only if there's a possible stream or topic wildcard mention in the message.
|
||||||
# to avoid unnecessarily sending huge user ID lists with thousands of elements to the
|
# This is important so as to avoid unnecessarily sending huge user ID lists with
|
||||||
# event queue (which can happen because these settings are `True` by default for new users.)
|
# thousands of elements to the event queue (which can happen because these settings
|
||||||
stream_wildcard_mention_user_ids = notification_recipients("wildcard_mentions_notify")
|
# are `True` by default for new users.)
|
||||||
stream_wildcard_mention_in_followed_topic_user_ids = (
|
wildcard_mentions_notify_user_ids = notification_recipients("wildcard_mentions_notify")
|
||||||
|
followed_topic_wildcard_mentions_notify_user_ids = (
|
||||||
followed_topic_notification_recipients("wildcard_mentions_notify")
|
followed_topic_notification_recipients("wildcard_mentions_notify")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if possible_stream_wildcard_mention:
|
||||||
|
stream_wildcard_mention_user_ids = wildcard_mentions_notify_user_ids
|
||||||
|
stream_wildcard_mention_in_followed_topic_user_ids = (
|
||||||
|
followed_topic_wildcard_mentions_notify_user_ids
|
||||||
|
)
|
||||||
|
|
||||||
|
if possible_topic_wildcard_mention:
|
||||||
|
topic_wildcard_mention_user_ids = topic_participant_user_ids.intersection(
|
||||||
|
wildcard_mentions_notify_user_ids
|
||||||
|
)
|
||||||
|
topic_wildcard_mention_in_followed_topic_user_ids = (
|
||||||
|
topic_participant_user_ids.intersection(
|
||||||
|
followed_topic_wildcard_mentions_notify_user_ids
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
elif recipient.type == Recipient.HUDDLE:
|
elif recipient.type == Recipient.HUDDLE:
|
||||||
message_to_user_ids = get_huddle_user_ids(recipient)
|
message_to_user_ids = get_huddle_user_ids(recipient)
|
||||||
|
|
||||||
|
@ -418,9 +454,11 @@ def get_recipient_info(
|
||||||
pm_mention_push_disabled_user_ids=pm_mention_push_disabled_user_ids,
|
pm_mention_push_disabled_user_ids=pm_mention_push_disabled_user_ids,
|
||||||
stream_push_user_ids=stream_push_user_ids,
|
stream_push_user_ids=stream_push_user_ids,
|
||||||
stream_email_user_ids=stream_email_user_ids,
|
stream_email_user_ids=stream_email_user_ids,
|
||||||
|
topic_wildcard_mention_user_ids=topic_wildcard_mention_user_ids,
|
||||||
stream_wildcard_mention_user_ids=stream_wildcard_mention_user_ids,
|
stream_wildcard_mention_user_ids=stream_wildcard_mention_user_ids,
|
||||||
followed_topic_push_user_ids=followed_topic_push_user_ids,
|
followed_topic_push_user_ids=followed_topic_push_user_ids,
|
||||||
followed_topic_email_user_ids=followed_topic_email_user_ids,
|
followed_topic_email_user_ids=followed_topic_email_user_ids,
|
||||||
|
topic_wildcard_mention_in_followed_topic_user_ids=topic_wildcard_mention_in_followed_topic_user_ids,
|
||||||
stream_wildcard_mention_in_followed_topic_user_ids=stream_wildcard_mention_in_followed_topic_user_ids,
|
stream_wildcard_mention_in_followed_topic_user_ids=stream_wildcard_mention_in_followed_topic_user_ids,
|
||||||
muted_sender_user_ids=muted_sender_user_ids,
|
muted_sender_user_ids=muted_sender_user_ids,
|
||||||
um_eligible_user_ids=um_eligible_user_ids,
|
um_eligible_user_ids=um_eligible_user_ids,
|
||||||
|
@ -541,6 +579,7 @@ def build_message_send_dict(
|
||||||
sender_id=message.sender_id,
|
sender_id=message.sender_id,
|
||||||
stream_topic=stream_topic,
|
stream_topic=stream_topic,
|
||||||
possibly_mentioned_user_ids=mention_data.get_user_ids(),
|
possibly_mentioned_user_ids=mention_data.get_user_ids(),
|
||||||
|
possible_topic_wildcard_mention=mention_data.message_has_topic_wildcards(),
|
||||||
possible_stream_wildcard_mention=mention_data.message_has_stream_wildcards(),
|
possible_stream_wildcard_mention=mention_data.message_has_stream_wildcards(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -307,6 +307,7 @@ def get_subscriptions_for_send_message(
|
||||||
stream_id: int,
|
stream_id: int,
|
||||||
topic_name: str,
|
topic_name: str,
|
||||||
possible_stream_wildcard_mention: bool,
|
possible_stream_wildcard_mention: bool,
|
||||||
|
topic_participant_user_ids: AbstractSet[int],
|
||||||
possibly_mentioned_user_ids: AbstractSet[int],
|
possibly_mentioned_user_ids: AbstractSet[int],
|
||||||
) -> QuerySet[Subscription]:
|
) -> QuerySet[Subscription]:
|
||||||
"""This function optimizes an important use case for large
|
"""This function optimizes an important use case for large
|
||||||
|
@ -352,6 +353,7 @@ def get_subscriptions_for_send_message(
|
||||||
| Q(email_notifications=True)
|
| Q(email_notifications=True)
|
||||||
| (Q(email_notifications=None) & Q(user_profile__enable_stream_email_notifications=True))
|
| (Q(email_notifications=None) & Q(user_profile__enable_stream_email_notifications=True))
|
||||||
| Q(user_profile_id__in=possibly_mentioned_user_ids)
|
| Q(user_profile_id__in=possibly_mentioned_user_ids)
|
||||||
|
| Q(user_profile_id__in=topic_participant_user_ids)
|
||||||
| Q(
|
| Q(
|
||||||
user_profile_id__in=AlertWord.objects.filter(realm_id=realm_id).values_list(
|
user_profile_id__in=AlertWord.objects.filter(realm_id=realm_id).values_list(
|
||||||
"user_profile_id"
|
"user_profile_id"
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Tuple
|
from typing import Any, Dict, List, Optional, Set, Tuple
|
||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
from django.db.models import Q, QuerySet
|
from django.db.models import Q, QuerySet, Subquery
|
||||||
from sqlalchemy.sql import ColumnElement, column, func, literal
|
from sqlalchemy.sql import ColumnElement, column, func, literal
|
||||||
from sqlalchemy.types import Boolean, Text
|
from sqlalchemy.types import Boolean, Text
|
||||||
|
|
||||||
from zerver.lib.request import REQ
|
from zerver.lib.request import REQ
|
||||||
from zerver.lib.types import EditHistoryEvent
|
from zerver.lib.types import EditHistoryEvent
|
||||||
from zerver.models import Message, Stream, UserMessage, UserProfile
|
from zerver.models import Message, Reaction, Stream, UserMessage, UserProfile
|
||||||
|
|
||||||
# Only use these constants for events.
|
# Only use these constants for events.
|
||||||
ORIG_TOPIC = "orig_subject"
|
ORIG_TOPIC = "orig_subject"
|
||||||
|
@ -284,3 +284,22 @@ def get_topic_resolution_and_bare_name(stored_name: str) -> Tuple[bool, str]:
|
||||||
return (True, stored_name[len(RESOLVED_TOPIC_PREFIX) :])
|
return (True, stored_name[len(RESOLVED_TOPIC_PREFIX) :])
|
||||||
|
|
||||||
return (False, stored_name)
|
return (False, stored_name)
|
||||||
|
|
||||||
|
|
||||||
|
def participants_for_topic(recipient_id: int, topic_name: str) -> Set[int]:
|
||||||
|
"""
|
||||||
|
Users who either sent or reacted to the messages in the topic.
|
||||||
|
The function is expensive for large numbers of messages in the topic.
|
||||||
|
"""
|
||||||
|
messages = Message.objects.filter(recipient_id=recipient_id, subject__iexact=topic_name)
|
||||||
|
participants = set(
|
||||||
|
UserProfile.objects.filter(
|
||||||
|
Q(id__in=Subquery(messages.values("sender_id")))
|
||||||
|
| Q(
|
||||||
|
id__in=Subquery(
|
||||||
|
Reaction.objects.filter(message__in=messages).values("user_profile_id")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).values_list("id", flat=True)
|
||||||
|
)
|
||||||
|
return participants
|
||||||
|
|
|
@ -624,6 +624,7 @@ class SoftDeactivationMessageTest(ZulipTestCase):
|
||||||
expected_count: int,
|
expected_count: int,
|
||||||
*,
|
*,
|
||||||
possible_stream_wildcard_mention: bool = False,
|
possible_stream_wildcard_mention: bool = False,
|
||||||
|
topic_participant_user_ids: AbstractSet[int] = set(),
|
||||||
possibly_mentioned_user_ids: AbstractSet[int] = set(),
|
possibly_mentioned_user_ids: AbstractSet[int] = set(),
|
||||||
) -> None:
|
) -> None:
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
@ -633,6 +634,7 @@ class SoftDeactivationMessageTest(ZulipTestCase):
|
||||||
stream_id=stream_id,
|
stream_id=stream_id,
|
||||||
topic_name=topic_name,
|
topic_name=topic_name,
|
||||||
possible_stream_wildcard_mention=possible_stream_wildcard_mention,
|
possible_stream_wildcard_mention=possible_stream_wildcard_mention,
|
||||||
|
topic_participant_user_ids=topic_participant_user_ids,
|
||||||
possibly_mentioned_user_ids=possibly_mentioned_user_ids,
|
possibly_mentioned_user_ids=possibly_mentioned_user_ids,
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
|
@ -1755,6 +1755,7 @@ class RecipientInfoTest(ZulipTestCase):
|
||||||
recipient=recipient,
|
recipient=recipient,
|
||||||
sender_id=hamlet.id,
|
sender_id=hamlet.id,
|
||||||
stream_topic=stream_topic,
|
stream_topic=stream_topic,
|
||||||
|
possible_topic_wildcard_mention=False,
|
||||||
possible_stream_wildcard_mention=False,
|
possible_stream_wildcard_mention=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1767,9 +1768,11 @@ class RecipientInfoTest(ZulipTestCase):
|
||||||
pm_mention_push_disabled_user_ids=set(),
|
pm_mention_push_disabled_user_ids=set(),
|
||||||
stream_push_user_ids=set(),
|
stream_push_user_ids=set(),
|
||||||
stream_email_user_ids=set(),
|
stream_email_user_ids=set(),
|
||||||
|
topic_wildcard_mention_user_ids=set(),
|
||||||
stream_wildcard_mention_user_ids=set(),
|
stream_wildcard_mention_user_ids=set(),
|
||||||
followed_topic_push_user_ids=set(),
|
followed_topic_push_user_ids=set(),
|
||||||
followed_topic_email_user_ids=set(),
|
followed_topic_email_user_ids=set(),
|
||||||
|
topic_wildcard_mention_in_followed_topic_user_ids=set(),
|
||||||
stream_wildcard_mention_in_followed_topic_user_ids=set(),
|
stream_wildcard_mention_in_followed_topic_user_ids=set(),
|
||||||
muted_sender_user_ids=set(),
|
muted_sender_user_ids=set(),
|
||||||
um_eligible_user_ids=all_user_ids,
|
um_eligible_user_ids=all_user_ids,
|
||||||
|
@ -1820,6 +1823,59 @@ class RecipientInfoTest(ZulipTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(info.stream_wildcard_mention_user_ids, {hamlet.id, othello.id})
|
self.assertEqual(info.stream_wildcard_mention_user_ids, {hamlet.id, othello.id})
|
||||||
|
|
||||||
|
do_change_user_setting(
|
||||||
|
hamlet,
|
||||||
|
"wildcard_mentions_notify",
|
||||||
|
True,
|
||||||
|
acting_user=None,
|
||||||
|
)
|
||||||
|
info = get_recipient_info(
|
||||||
|
realm_id=realm.id,
|
||||||
|
recipient=recipient,
|
||||||
|
sender_id=hamlet.id,
|
||||||
|
stream_topic=stream_topic,
|
||||||
|
possible_topic_wildcard_mention=True,
|
||||||
|
possible_stream_wildcard_mention=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(info.stream_wildcard_mention_user_ids, set())
|
||||||
|
self.assertEqual(info.topic_wildcard_mention_user_ids, set())
|
||||||
|
|
||||||
|
# User who sent a message to the topic, or reacted to a message on the topic
|
||||||
|
# is only considered as a possible user to be notified for topic mention.
|
||||||
|
self.send_stream_message(hamlet, stream_name, content="test message", topic_name=topic_name)
|
||||||
|
info = get_recipient_info(
|
||||||
|
realm_id=realm.id,
|
||||||
|
recipient=recipient,
|
||||||
|
sender_id=hamlet.id,
|
||||||
|
stream_topic=stream_topic,
|
||||||
|
possible_topic_wildcard_mention=True,
|
||||||
|
possible_stream_wildcard_mention=False,
|
||||||
|
)
|
||||||
|
self.assertEqual(info.stream_wildcard_mention_user_ids, set())
|
||||||
|
self.assertEqual(info.topic_wildcard_mention_user_ids, {hamlet.id})
|
||||||
|
|
||||||
|
info = get_recipient_info(
|
||||||
|
realm_id=realm.id,
|
||||||
|
recipient=recipient,
|
||||||
|
sender_id=hamlet.id,
|
||||||
|
stream_topic=stream_topic,
|
||||||
|
possible_topic_wildcard_mention=False,
|
||||||
|
possible_stream_wildcard_mention=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(info.stream_wildcard_mention_user_ids, {hamlet.id, othello.id})
|
||||||
|
self.assertEqual(info.topic_wildcard_mention_user_ids, set())
|
||||||
|
|
||||||
|
info = get_recipient_info(
|
||||||
|
realm_id=realm.id,
|
||||||
|
recipient=recipient,
|
||||||
|
sender_id=hamlet.id,
|
||||||
|
stream_topic=stream_topic,
|
||||||
|
possible_topic_wildcard_mention=True,
|
||||||
|
possible_stream_wildcard_mention=True,
|
||||||
|
)
|
||||||
|
self.assertEqual(info.stream_wildcard_mention_user_ids, {hamlet.id, othello.id})
|
||||||
|
self.assertEqual(info.topic_wildcard_mention_user_ids, {hamlet.id})
|
||||||
|
|
||||||
sub = get_subscription(stream_name, hamlet)
|
sub = get_subscription(stream_name, hamlet)
|
||||||
sub.push_notifications = False
|
sub.push_notifications = False
|
||||||
sub.save()
|
sub.save()
|
||||||
|
|
Loading…
Reference in New Issue