Send missedmessage notifications if user is idle for >1hr

(imported from commit 573f46a77497cb2f73eae3b4a648e466977e6247)
This commit is contained in:
Leo Franchi 2013-09-13 17:33:11 -04:00
parent 7bb96bd36b
commit 6e56342cf6
5 changed files with 100 additions and 30 deletions

View File

@ -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,

View File

@ -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,)

View File

@ -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):

View File

@ -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)

View File

@ -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):