email_notifications: Soft reactivate mentioned users.

Signed-off-by: Zixuan James Li <359101898@qq.com>
This commit is contained in:
Zixuan James Li
2022-04-14 22:51:41 -04:00
committed by Tim Abbott
parent d8101de34d
commit a8fd9eb701
5 changed files with 116 additions and 0 deletions

View File

@@ -25,6 +25,7 @@ from zerver.lib.message import bulk_access_messages
from zerver.lib.notification_data import get_mentioned_user_group_name
from zerver.lib.queue import queue_json_publish
from zerver.lib.send_email import FromAddress, send_future_email
from zerver.lib.soft_deactivation import soft_reactivate_if_personal_notification
from zerver.lib.types import DisplayRecipientT
from zerver.lib.url_encoding import (
huddle_narrow_url,
@@ -488,6 +489,11 @@ def do_send_missedmessage_events_reply_in_zulip(
show_message_content=True,
)
# Soft reactivate the long_term_idle user personally mentioned
soft_reactivate_if_personal_notification(
user_profile, unique_triggers, mentioned_user_group_name
)
with override_language(user_profile.default_language):
from_name: str = _("Zulip notifications")
from_address = FromAddress.NOREPLY

View File

@@ -10,6 +10,7 @@ from django.utils.timezone import now as timezone_now
from sentry_sdk import capture_exception
from zerver.lib.logging_util import log_to_file
from zerver.lib.queue import queue_json_publish
from zerver.lib.utils import assert_is_not_none
from zerver.models import (
Message,
@@ -374,3 +375,33 @@ def get_soft_deactivated_users_for_catch_up(filter_kwargs: Any) -> List[UserProf
**filter_kwargs,
)
return users_to_catch_up
def soft_reactivate_if_personal_notification(
user_profile: UserProfile, unique_triggers: Set[str], mentioned_user_group_name: Optional[str]
) -> None:
"""When we're about to send an email/push notification to a
long_term_idle user, it's very likely that the user will try to
return to Zulip. As a result, it makes sense to optimistically
soft-reactivate that user, to give them a good return experience.
It's important that we do nothing for wildcard or group mentions,
because soft-reactivating an entire realm would be very expensive
(and we can't easily check the group's size). The caller is
responsible for passing a mentioned_user_group_name that is None
for messages that contain both a personal mention and a group
mention.
"""
if not user_profile.long_term_idle:
return
private_message = "private_message" in unique_triggers
personal_mention = "mentioned" in unique_triggers and mentioned_user_group_name is None
if not private_message and not personal_mention:
return
event = {
"type": "soft_reactivate",
"user_profile_id": user_profile.id,
}
queue_json_publish("deferred_work", event)

View File

@@ -56,6 +56,7 @@ from zerver.lib.initial_password import initial_password
from zerver.lib.notification_data import UserMessageNotificationsData
from zerver.lib.rate_limiter import bounce_redis_key_prefix_for_testing
from zerver.lib.sessions import get_session_dict_user
from zerver.lib.soft_deactivation import do_soft_deactivate_users
from zerver.lib.stream_subscription import get_stream_subscriptions_for_user
from zerver.lib.streams import (
create_stream_if_needed,
@@ -1524,6 +1525,24 @@ Output:
UserGroupMembership.objects.filter(user_profile=user, user_group=user_group).exists()
)
@contextmanager
def soft_deactivate_and_check_long_term_idle(
self, user: UserProfile, expected: bool
) -> Iterator[None]:
"""
Ensure that the user is soft deactivated (long term idle), and check if the user
has been reactivated when exiting the context with an assertion
"""
if not user.long_term_idle:
do_soft_deactivate_users([user])
self.assertTrue(user.long_term_idle)
try:
yield
finally:
# Prevent from using the old user object
user.refresh_from_db()
self.assertEqual(user.long_term_idle, expected)
class WebhookTestCase(ZulipTestCase):
"""Shared test class for all incoming webhooks tests.

View File

@@ -1449,6 +1449,62 @@ class TestMissedMessages(ZulipTestCase):
msg_id, verify_body_include, email_subject, send_as_user=False, verify_html_body=True
)
def test_long_term_idle_user_missed_message(self) -> None:
hamlet = self.example_user("hamlet")
othello = self.example_user("othello")
cordelia = self.example_user("cordelia")
large_user_group = create_user_group(
"large_user_group", [hamlet, othello, cordelia], get_realm("zulip")
)
# Do note that the event dicts for the missed messages are constructed by hand
# The part of testing the consumption of missed messages by the worker is left to
# test_queue_worker.test_missed_message_worker
# Personal mention in a stream message should soft reactivate the user
with self.soft_deactivate_and_check_long_term_idle(hamlet, expected=False):
mention = f"@**{hamlet.full_name}**"
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
handle_missedmessage_emails(
hamlet.id,
[{"message_id": stream_mentioned_message_id, "trigger": "mentioned"}],
)
# Private message should soft reactivate the user
with self.soft_deactivate_and_check_long_term_idle(hamlet, expected=False):
# Soft reactivate the user by sending a personal message
personal_message_id = self.send_personal_message(othello, hamlet, "Message")
handle_missedmessage_emails(
hamlet.id,
[{"message_id": personal_message_id, "trigger": "private_message"}],
)
# Wild card mention should NOT soft reactivate the user
with self.soft_deactivate_and_check_long_term_idle(hamlet, expected=True):
# Soft reactivate the user by sending a personal message
mention = "@**all**"
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
handle_missedmessage_emails(
hamlet.id,
[{"message_id": stream_mentioned_message_id, "trigger": "wildcard_mentioned"}],
)
# Group mention should NOT soft reactivate the user
with self.soft_deactivate_and_check_long_term_idle(hamlet, expected=True):
# Soft reactivate the user by sending a personal message
mention = "@*large_user_group*"
stream_mentioned_message_id = self.send_stream_message(othello, "Denmark", mention)
handle_missedmessage_emails(
hamlet.id,
[
{
"message_id": stream_mentioned_message_id,
"trigger": "mentioned",
"mentioned_user_group_id": large_user_group.id,
}
],
)
class TestFollowupEmailDelay(ZulipTestCase):
def test_followup_day2_email_delay(self) -> None:

View File

@@ -83,6 +83,7 @@ from zerver.lib.send_email import (
send_email,
send_future_email,
)
from zerver.lib.soft_deactivation import reactivate_user_if_soft_deactivated
from zerver.lib.timestamp import timestamp_to_datetime
from zerver.lib.upload import handle_reupload_emojis_event
from zerver.lib.url_preview import preview as url_preview
@@ -1083,6 +1084,9 @@ class DeferredWorker(QueueProcessingWorker):
realm = Realm.objects.get(id=event["realm_id"])
logger.info("Processing reupload_realm_emoji event for realm %s", realm.id)
handle_reupload_emojis_event(realm, logger)
elif event["type"] == "soft_reactivate":
user_profile = get_user_profile_by_id(event["user_profile_id"])
reactivate_user_if_soft_deactivated(user_profile)
end = time.time()
logger.info("deferred_work processed %s event (%dms)", event["type"], (end - start) * 1000)