mirror of https://github.com/zulip/zulip.git
Send missedmessage notifications if user is idle for >1hr
(imported from commit 573f46a77497cb2f73eae3b4a648e466977e6247)
This commit is contained in:
parent
7bb96bd36b
commit
6e56342cf6
|
@ -12,7 +12,8 @@ from zerver.models import Realm, RealmEmoji, Stream, UserProfile, UserActivity,
|
|||
to_dict_cache_key, get_realm, stringify_message_dict, bulk_get_recipients, \
|
||||
email_to_domain, email_to_username, display_recipient_cache_key, \
|
||||
get_stream_cache_key, to_dict_cache_key_id, is_super_user, \
|
||||
get_active_user_profiles_by_realm, UserActivityInterval
|
||||
get_active_user_profiles_by_realm, UserActivityInterval, \
|
||||
get_status_dict_by_realm
|
||||
from django.db import transaction, IntegrityError
|
||||
from django.db.models import F, Q
|
||||
from django.core.exceptions import ValidationError
|
||||
|
@ -298,9 +299,11 @@ def do_send_messages(messages):
|
|||
message['message'].to_dict(apply_markdown=False)
|
||||
user_flags = user_message_flags.get(message['message'].id, {})
|
||||
data = dict(
|
||||
type = 'new_message',
|
||||
message = message['message'].id,
|
||||
users = [{'id': user.id, 'flags': user_flags.get(user.id, [])} for user in message['recipients']])
|
||||
type = 'new_message',
|
||||
message = message['message'].id,
|
||||
sender_realm = message['message'].sender.realm.id,
|
||||
users = [{'id': user.id, 'flags': user_flags.get(user.id, [])}
|
||||
for user in message['recipients']])
|
||||
if message['message'].recipient.type == Recipient.STREAM:
|
||||
# Note: This is where authorization for single-stream
|
||||
# get_updates happens! We only attach stream data to the
|
||||
|
@ -1345,20 +1348,13 @@ def gather_subscriptions(user_profile):
|
|||
|
||||
return (sorted(subscribed), sorted(unsubscribed))
|
||||
|
||||
@cache_with_key(status_dict_cache_key, timeout=60)
|
||||
@cache_with_key(status_dict_cache_key, timeout=3600*24*7)
|
||||
def get_status_dict(requesting_user_profile):
|
||||
user_statuses = defaultdict(dict)
|
||||
|
||||
# Return no status info for MIT
|
||||
if requesting_user_profile.realm.domain == 'mit.edu':
|
||||
return user_statuses
|
||||
return defaultdict(dict)
|
||||
|
||||
for presence in UserPresence.objects.filter(user_profile__realm=requesting_user_profile.realm,
|
||||
user_profile__is_active=True) \
|
||||
.select_related('user_profile', 'client'):
|
||||
user_statuses[presence.user_profile.email][presence.client.name] = presence.to_dict()
|
||||
|
||||
return user_statuses
|
||||
return get_status_dict_by_realm(requesting_user_profile.realm_id)
|
||||
|
||||
|
||||
def do_events_register(user_profile, user_client, apply_markdown=True,
|
||||
|
|
|
@ -250,8 +250,11 @@ def update_user_profile_cache(sender, **kwargs):
|
|||
if kwargs['update_fields'] is None or "alert_words" in kwargs['update_fields']:
|
||||
djcache.delete(KEY_PREFIX + realm_alert_words_cache_key(user_profile.realm))
|
||||
|
||||
def status_dict_cache_key_for_realm_id(realm_id):
|
||||
return "status_dict:%d" % (realm_id,)
|
||||
|
||||
def status_dict_cache_key(user_profile):
|
||||
return "status_dict:%d" % (user_profile.realm_id,)
|
||||
return status_dict_cache_key_for_realm_id(user_profile.realm_id)
|
||||
|
||||
def update_user_presence_cache(sender, **kwargs):
|
||||
user_profile = kwargs['instance'].user_profile
|
||||
|
@ -260,5 +263,8 @@ def update_user_presence_cache(sender, **kwargs):
|
|||
# entry in the UserPresence cache to avoid giving out stale state
|
||||
djcache.delete(KEY_PREFIX + status_dict_cache_key(user_profile))
|
||||
|
||||
def cache_save_status_dict(realm_id, status_dict):
|
||||
cache_set(status_dict_cache_key_for_realm_id(realm_id), status_dict, timeout=3600*24*7)
|
||||
|
||||
def realm_alert_words_cache_key(realm):
|
||||
return "realm_alert_words:%s" % (realm.domain,)
|
||||
|
|
|
@ -6,10 +6,11 @@ from __future__ import absolute_import
|
|||
from django.conf import settings
|
||||
from zerver.models import Message, UserProfile, Stream, get_stream_cache_key, \
|
||||
Recipient, get_recipient_cache_key, Client, get_client_cache_key, \
|
||||
Huddle, huddle_hash_cache_key
|
||||
Huddle, huddle_hash_cache_key, Realm, get_status_dict_by_realm
|
||||
from zerver.lib.cache import cache_with_key, cache_set, message_cache_key, \
|
||||
user_profile_by_email_cache_key, user_profile_by_id_cache_key, \
|
||||
get_memcached_time, get_memcached_requests, cache_set_many
|
||||
get_memcached_time, get_memcached_requests, cache_set_many, \
|
||||
status_dict_cache_key_for_realm_id
|
||||
from django.utils.importlib import import_module
|
||||
from django.contrib.sessions.models import Session
|
||||
import logging
|
||||
|
@ -48,6 +49,12 @@ def huddle_cache_items(items_for_memcached, huddle):
|
|||
def recipient_cache_items(items_for_memcached, recipient):
|
||||
items_for_memcached[get_recipient_cache_key(recipient.type, recipient.type_id)] = (recipient,)
|
||||
|
||||
def presence_fetch_objects():
|
||||
return [(realm.id, get_status_dict_by_realm(realm.id)) for realm in Realm.objects.all()]
|
||||
|
||||
def presence_cache_items(items_for_memcached, status_dict):
|
||||
items_for_memcached[status_dict_cache_key_for_realm_id(status_dict[0])] = (status_dict[1],)
|
||||
|
||||
session_engine = import_module(settings.SESSION_ENGINE)
|
||||
def session_cache_items(items_for_memcached, session):
|
||||
store = session_engine.SessionStore(session_key=session.session_key)
|
||||
|
@ -67,6 +74,7 @@ cache_fillers = {
|
|||
'message': (message_fetch_objects, message_cache_items, 3600 * 24, 1000),
|
||||
'huddle': (lambda: Huddle.objects.select_related().all(), huddle_cache_items, 3600*24*7, 10000),
|
||||
'session': (lambda: Session.objects.all(), session_cache_items, 3600*24*7, 10000),
|
||||
'presence': (presence_fetch_objects, presence_cache_items, 3600*24*7, 10000),
|
||||
}
|
||||
|
||||
def fill_memcached_cache(cache):
|
||||
|
|
|
@ -19,6 +19,7 @@ from django.db.models.signals import post_save, post_delete
|
|||
import zlib
|
||||
|
||||
from bitfield import BitField
|
||||
from collections import defaultdict
|
||||
import pylibmc
|
||||
import ujson
|
||||
|
||||
|
@ -701,11 +702,15 @@ class UserPresence(models.Model):
|
|||
timestamp = models.DateTimeField('presence changed')
|
||||
status = models.PositiveSmallIntegerField(default=ACTIVE)
|
||||
|
||||
@staticmethod
|
||||
def status_to_string(status):
|
||||
if status == UserPresence.ACTIVE:
|
||||
return 'active'
|
||||
elif status == UserPresence.IDLE:
|
||||
return 'idle'
|
||||
|
||||
def to_dict(self):
|
||||
if self.status == UserPresence.ACTIVE:
|
||||
presence_val = 'active'
|
||||
elif self.status == UserPresence.IDLE:
|
||||
presence_val = 'idle'
|
||||
presence_val = UserPresence.status_to_string(self.status)
|
||||
|
||||
return {'client' : self.client.name,
|
||||
'status' : presence_val,
|
||||
|
@ -729,6 +734,16 @@ class UserPresence(models.Model):
|
|||
# changes
|
||||
post_save.connect(update_user_presence_cache, sender=UserPresence)
|
||||
|
||||
def get_status_dict_by_realm(realm_id):
|
||||
user_statuses = defaultdict(dict)
|
||||
|
||||
for presence in UserPresence.objects.filter(user_profile__realm_id=realm_id,
|
||||
user_profile__is_active=True) \
|
||||
.select_related('user_profile', 'client'):
|
||||
user_statuses[presence.user_profile.email][presence.client.name] = presence.to_dict()
|
||||
|
||||
return user_statuses
|
||||
|
||||
class DefaultStream(models.Model):
|
||||
realm = models.ForeignKey(Realm)
|
||||
stream = models.ForeignKey(Stream)
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
from __future__ import absolute_import
|
||||
|
||||
from django.conf import settings
|
||||
from django.utils.timezone import now
|
||||
|
||||
from zerver.models import Message, UserProfile, UserMessage, \
|
||||
Recipient, Stream, get_stream, get_user_profile_by_id
|
||||
Recipient, Stream, get_stream, get_user_profile_by_id, \
|
||||
get_status_dict_by_realm, UserPresence
|
||||
|
||||
from zerver.decorator import JsonableError
|
||||
from zerver.lib.cache import cache_get_many, message_cache_key, \
|
||||
user_profile_by_id_cache_key, cache_save_user_profile
|
||||
user_profile_by_id_cache_key, cache_save_user_profile, \
|
||||
status_dict_cache_key_for_realm_id, cache_save_status_dict
|
||||
from zerver.lib.cache_helpers import cache_save_message
|
||||
from zerver.lib.queue import queue_json_publish
|
||||
from zerver.lib.event_queue import get_client_descriptors_for_user
|
||||
from zerver.lib.timestamp import timestamp_to_datetime
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
@ -19,8 +24,13 @@ import requests
|
|||
import ujson
|
||||
import subprocess
|
||||
import collections
|
||||
import datetime
|
||||
from django.db import connection
|
||||
|
||||
# Send email notifications to idle users
|
||||
# after they are idle for 1 hour
|
||||
NOTIFY_AFTER_IDLE_HOURS = 1
|
||||
|
||||
class Callbacks(object):
|
||||
# A user received a message. The key is user_profile.id.
|
||||
TYPE_USER_RECEIVE = 0
|
||||
|
@ -288,13 +298,17 @@ def missedmessage_hook(user_profile_id, queue, last_for_client):
|
|||
event = build_offline_notification_event(user_profile_id, msg_id)
|
||||
queue_json_publish("missedmessage_emails", event, lambda event: None)
|
||||
|
||||
def cache_load_message_data(message_id, users):
|
||||
def cache_load_message_data(message_id, users, sender_realm_id):
|
||||
# Get everything that we'll need out of memcached in one fetch, to save round-trip times:
|
||||
# * The message itself
|
||||
# * Every recipient's UserProfile
|
||||
# * The cached status dict (all UserPresences in a realm)
|
||||
user_profile_keys = [user_profile_by_id_cache_key(user_data['id']) for user_data in users]
|
||||
|
||||
cache_keys = [message_cache_key(message_id)]
|
||||
if sender_realm_id is not None:
|
||||
realm_key = status_dict_cache_key_for_realm_id(sender_realm_id)
|
||||
cache_keys.append(realm_key)
|
||||
|
||||
cache_keys.extend(user_profile_keys)
|
||||
|
||||
|
@ -304,6 +318,10 @@ def cache_load_message_data(message_id, users):
|
|||
cache_extractor = lambda result: result[0] if result is not None else None
|
||||
|
||||
message = cache_extractor(result.get(cache_keys[0], None))
|
||||
if sender_realm_id is not None:
|
||||
user_presences = cache_extractor(result.get(realm_key))
|
||||
else:
|
||||
user_presences = None
|
||||
|
||||
user_profiles = dict((user_data['id'], cache_extractor(result.get(user_profile_by_id_cache_key(user_data['id']), None)))
|
||||
for user_data in users)
|
||||
|
@ -317,6 +335,12 @@ def cache_load_message_data(message_id, users):
|
|||
|
||||
message = Message.objects.select_related().get(id=message_id)
|
||||
cache_save_message(message)
|
||||
if user_presences is None and sender_realm_id is not None:
|
||||
if not settings.TEST_SUITE:
|
||||
logging.warning("Tornado failed to load user presences from memcached when delivering message!")
|
||||
|
||||
user_presences = get_status_dict_by_realm(sender_realm_id)
|
||||
cache_save_status_dict(sender_realm_id, user_presences)
|
||||
for user_profile_id, user_profile in user_profiles.iteritems():
|
||||
if user_profile:
|
||||
continue
|
||||
|
@ -327,20 +351,41 @@ def cache_load_message_data(message_id, users):
|
|||
user_profiles[user_profile_id] = user_profile
|
||||
cache_save_user_profile(user_profile)
|
||||
|
||||
return message, user_profiles
|
||||
return message, user_profiles, user_presences
|
||||
|
||||
def receiver_is_idle(user_profile):
|
||||
def receiver_is_idle(user_profile, realm_presences):
|
||||
# If a user has no message-receiving event queues, they've got no open zulip
|
||||
# session so we notify them
|
||||
all_client_descriptors = get_client_descriptors_for_user(user_profile.id)
|
||||
message_event_queues = [client for client in all_client_descriptors if client.accepts_event_type('message')]
|
||||
off_zulip = len(message_event_queues) == 0
|
||||
|
||||
return off_zulip
|
||||
# It's possible a recipient is not in the realm of a sender. We don't have
|
||||
# presence information in this case (and it's hard to get without an additional
|
||||
# db query) so we simply don't try to guess if this cross-realm recipient
|
||||
# has been idle for too long
|
||||
if realm_presences is None or not user_profile.email in realm_presences:
|
||||
return off_zulip
|
||||
|
||||
# If the most recent online status from a user is >1hr in the past, we notify
|
||||
# them regardless of whether or not they have an open window
|
||||
user_presence = realm_presences[user_profile.email]
|
||||
idle_too_long = False
|
||||
newest = None
|
||||
for client, status in user_presence.iteritems():
|
||||
if newest is None or status['timestamp'] > newest['timestamp']:
|
||||
newest = status
|
||||
|
||||
update_time = timestamp_to_datetime(newest['timestamp'])
|
||||
if now() - update_time > datetime.timedelta(hours=NOTIFY_AFTER_IDLE_HOURS):
|
||||
idle_too_long = True
|
||||
|
||||
return off_zulip or idle_too_long
|
||||
|
||||
def process_new_message(data):
|
||||
message, user_profiles = cache_load_message_data(data['message'],
|
||||
data['users'])
|
||||
message, user_profiles, realm_presences = cache_load_message_data(data['message'],
|
||||
data['users'],
|
||||
data.get('sender_realm', None))
|
||||
|
||||
message_dict_markdown = message.to_dict(True)
|
||||
message_dict_no_markdown = message.to_dict(False)
|
||||
|
@ -380,7 +425,7 @@ def process_new_message(data):
|
|||
user_profile_id != message.sender.id
|
||||
mentioned = 'mentioned' in flags
|
||||
|
||||
idle = receiver_is_idle(user_profile)
|
||||
idle = receiver_is_idle(user_profile, realm_presences)
|
||||
|
||||
if (received_pm or mentioned) and idle:
|
||||
if receives_offline_notifications(user_profile):
|
||||
|
|
Loading…
Reference in New Issue