2020-02-06 15:54:57 +01:00
|
|
|
from collections import defaultdict
|
2020-02-06 16:23:35 +01:00
|
|
|
|
|
|
|
import datetime
|
2020-02-06 21:00:21 +01:00
|
|
|
import itertools
|
2020-02-06 15:54:57 +01:00
|
|
|
import time
|
2020-02-06 16:23:35 +01:00
|
|
|
|
|
|
|
from django.utils.timezone import now as timezone_now
|
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
from typing import Any, Dict, List, Set
|
2020-02-06 16:42:28 +01:00
|
|
|
|
|
|
|
from zerver.lib.timestamp import datetime_to_timestamp
|
2020-02-06 16:23:35 +01:00
|
|
|
from zerver.models import (
|
|
|
|
query_for_ids,
|
|
|
|
PushDeviceToken,
|
2020-02-06 17:41:55 +01:00
|
|
|
Realm,
|
2020-02-06 16:23:35 +01:00
|
|
|
UserPresence,
|
|
|
|
UserProfile,
|
|
|
|
)
|
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
def get_status_dicts_for_rows(all_rows: List[Dict[str, Any]],
|
2020-02-06 16:42:28 +01:00
|
|
|
mobile_user_ids: Set[int],
|
|
|
|
slim_presence: bool) -> Dict[str, Dict[str, Any]]:
|
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
# Note that datetime values have sub-second granularity, which is
|
|
|
|
# mostly important for avoiding test flakes, but it's also technically
|
|
|
|
# more precise for real users.
|
|
|
|
# We could technically do this sort with the database, but doing it
|
|
|
|
# here prevents us from having to assume the caller is playing nice.
|
|
|
|
all_rows = sorted(
|
|
|
|
all_rows,
|
|
|
|
key = lambda row: (row['user_profile__id'], row['timestamp'])
|
|
|
|
)
|
|
|
|
|
|
|
|
if slim_presence:
|
|
|
|
# Stringify user_id here, since it's gonna be turned
|
|
|
|
# into a string anyway by JSON, and it keeps mypy happy.
|
|
|
|
get_user_key = lambda row: str(row['user_profile__id'])
|
2020-02-07 14:50:30 +01:00
|
|
|
get_user_info = get_modern_user_info
|
2020-02-06 21:00:21 +01:00
|
|
|
else:
|
|
|
|
get_user_key = lambda row: row['user_profile__email']
|
2020-02-07 14:50:30 +01:00
|
|
|
get_user_info = get_legacy_user_info
|
2020-02-06 21:00:21 +01:00
|
|
|
|
|
|
|
user_statuses = dict() # type: Dict[str, Dict[str, Any]]
|
|
|
|
|
|
|
|
for user_key, presence_rows in itertools.groupby(all_rows, get_user_key):
|
2020-02-07 14:50:30 +01:00
|
|
|
info = get_user_info(
|
2020-02-06 21:00:21 +01:00
|
|
|
list(presence_rows),
|
2020-02-07 14:50:30 +01:00
|
|
|
mobile_user_ids=mobile_user_ids,
|
2020-02-06 21:00:21 +01:00
|
|
|
)
|
|
|
|
user_statuses[user_key] = info
|
|
|
|
|
|
|
|
return user_statuses
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2020-02-07 14:50:30 +01:00
|
|
|
def get_modern_user_info(presence_rows: List[Dict[str, Any]],
|
|
|
|
mobile_user_ids: Set[int]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
active_timestamp = None
|
|
|
|
for row in reversed(presence_rows):
|
|
|
|
if row['status'] == UserPresence.ACTIVE:
|
|
|
|
active_timestamp = datetime_to_timestamp(
|
|
|
|
row['timestamp'])
|
|
|
|
break
|
|
|
|
|
|
|
|
idle_timestamp = None
|
|
|
|
for row in reversed(presence_rows):
|
|
|
|
if row['status'] == UserPresence.IDLE:
|
|
|
|
idle_timestamp = datetime_to_timestamp(
|
|
|
|
row['timestamp'])
|
|
|
|
break
|
|
|
|
|
|
|
|
# Be stingy about bandwidth, and don't even include
|
|
|
|
# keys for entities that have None values. JS
|
|
|
|
# code should just do a falsy check here.
|
|
|
|
result = dict()
|
|
|
|
|
|
|
|
if active_timestamp is not None:
|
|
|
|
result['active_timestamp'] = active_timestamp
|
|
|
|
|
|
|
|
if idle_timestamp is not None:
|
|
|
|
result['idle_timestamp'] = idle_timestamp
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
def get_legacy_user_info(presence_rows: List[Dict[str, Any]],
|
|
|
|
mobile_user_ids: Set[int]) -> Dict[str, Any]:
|
|
|
|
|
|
|
|
# The format of data here is for legacy users of our API,
|
|
|
|
# including old versions of the mobile app.
|
|
|
|
info_rows = []
|
|
|
|
for row in presence_rows:
|
2020-02-06 16:42:28 +01:00
|
|
|
client_name = row['client__name']
|
|
|
|
status = UserPresence.status_to_string(row['status'])
|
|
|
|
dt = row['timestamp']
|
|
|
|
timestamp = datetime_to_timestamp(dt)
|
|
|
|
push_enabled = row['user_profile__enable_offline_push_notifications']
|
|
|
|
has_push_devices = row['user_profile__id'] in mobile_user_ids
|
|
|
|
pushable = (push_enabled and has_push_devices)
|
|
|
|
|
|
|
|
info = dict(
|
|
|
|
client=client_name,
|
|
|
|
status=status,
|
|
|
|
timestamp=timestamp,
|
|
|
|
pushable=pushable,
|
|
|
|
)
|
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
info_rows.append(info)
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
most_recent_info = info_rows[-1]
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
result = dict()
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2020-02-06 21:00:21 +01:00
|
|
|
# The word "aggegrated" here is possibly misleading.
|
|
|
|
# It's really just the most recent client's info.
|
|
|
|
result['aggregated'] = dict(
|
|
|
|
client=most_recent_info['client'],
|
|
|
|
status=most_recent_info['status'],
|
|
|
|
timestamp=most_recent_info['timestamp'],
|
|
|
|
)
|
|
|
|
|
|
|
|
# Build a dictionary of client -> info. There should
|
|
|
|
# only be one row per client, but to be on the safe side,
|
|
|
|
# we always overwrite with rows that are later in our list.
|
|
|
|
for info in info_rows:
|
|
|
|
result[info['client']] = info
|
|
|
|
|
|
|
|
return result
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2020-02-06 17:52:12 +01:00
|
|
|
def get_presence_for_user(user_profile_id: int,
|
|
|
|
slim_presence: bool=False) -> Dict[str, Dict[str, Any]]:
|
2020-02-06 16:35:22 +01:00
|
|
|
query = UserPresence.objects.filter(user_profile_id=user_profile_id).values(
|
|
|
|
'client__name',
|
|
|
|
'status',
|
|
|
|
'timestamp',
|
|
|
|
'user_profile__email',
|
|
|
|
'user_profile__id',
|
|
|
|
'user_profile__enable_offline_push_notifications',
|
|
|
|
)
|
|
|
|
presence_rows = list(query)
|
|
|
|
|
|
|
|
mobile_user_ids = set() # type: Set[int]
|
|
|
|
if PushDeviceToken.objects.filter(user_id=user_profile_id).exists(): # nocoverage
|
|
|
|
# TODO: Add a test, though this is low priority, since we don't use mobile_user_ids yet.
|
|
|
|
mobile_user_ids.add(user_profile_id)
|
|
|
|
|
2020-02-06 16:42:28 +01:00
|
|
|
return get_status_dicts_for_rows(presence_rows, mobile_user_ids, slim_presence)
|
2020-02-06 16:35:22 +01:00
|
|
|
|
|
|
|
|
2020-02-06 16:23:35 +01:00
|
|
|
def get_status_dict_by_realm(realm_id: int, slim_presence: bool = False) -> Dict[str, Dict[str, Any]]:
|
|
|
|
user_profile_ids = UserProfile.objects.filter(
|
|
|
|
realm_id=realm_id,
|
|
|
|
is_active=True,
|
|
|
|
is_bot=False
|
|
|
|
).order_by('id').values_list('id', flat=True)
|
|
|
|
|
|
|
|
user_profile_ids = list(user_profile_ids)
|
|
|
|
if not user_profile_ids: # nocoverage
|
|
|
|
# This conditional is necessary because query_for_ids
|
|
|
|
# throws an exception if passed an empty list.
|
|
|
|
#
|
|
|
|
# It's not clear this condition is actually possible,
|
|
|
|
# though, because it shouldn't be possible to end up with
|
|
|
|
# a realm with 0 active users.
|
|
|
|
return {}
|
|
|
|
|
|
|
|
two_weeks_ago = timezone_now() - datetime.timedelta(weeks=2)
|
|
|
|
query = UserPresence.objects.filter(
|
2020-02-08 21:50:55 +01:00
|
|
|
realm_id=realm_id,
|
|
|
|
timestamp__gte=two_weeks_ago,
|
|
|
|
user_profile__is_active=True,
|
|
|
|
user_profile__is_bot=False,
|
2020-02-06 16:23:35 +01:00
|
|
|
).values(
|
|
|
|
'client__name',
|
|
|
|
'status',
|
|
|
|
'timestamp',
|
|
|
|
'user_profile__email',
|
|
|
|
'user_profile__id',
|
|
|
|
'user_profile__enable_offline_push_notifications',
|
|
|
|
)
|
|
|
|
|
|
|
|
presence_rows = list(query)
|
|
|
|
|
|
|
|
mobile_query = PushDeviceToken.objects.distinct(
|
|
|
|
'user_id'
|
|
|
|
).values_list(
|
|
|
|
'user_id',
|
|
|
|
flat=True
|
|
|
|
)
|
|
|
|
|
|
|
|
mobile_query = query_for_ids(
|
|
|
|
query=mobile_query,
|
|
|
|
user_ids=user_profile_ids,
|
|
|
|
field='user_id'
|
|
|
|
)
|
|
|
|
mobile_user_ids = set(mobile_query)
|
|
|
|
|
2020-02-06 16:42:28 +01:00
|
|
|
return get_status_dicts_for_rows(presence_rows, mobile_user_ids, slim_presence)
|
2020-02-06 15:54:57 +01:00
|
|
|
|
2020-02-06 17:41:55 +01:00
|
|
|
def get_presences_for_realm(realm: Realm,
|
|
|
|
slim_presence: bool) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
2020-02-06 15:54:57 +01:00
|
|
|
|
2020-02-06 17:41:55 +01:00
|
|
|
if realm.presence_disabled:
|
2020-02-06 15:54:57 +01:00
|
|
|
# Return an empty dict if presence is disabled in this realm
|
|
|
|
return defaultdict(dict)
|
|
|
|
|
2020-02-06 17:41:55 +01:00
|
|
|
return get_status_dict_by_realm(realm.id, slim_presence)
|
2020-02-06 15:54:57 +01:00
|
|
|
|
|
|
|
def get_presence_response(requesting_user_profile: UserProfile,
|
|
|
|
slim_presence: bool) -> Dict[str, Any]:
|
2020-02-06 17:41:55 +01:00
|
|
|
realm = requesting_user_profile.realm
|
2020-02-06 15:54:57 +01:00
|
|
|
server_timestamp = time.time()
|
2020-02-06 17:41:55 +01:00
|
|
|
presences = get_presences_for_realm(realm, slim_presence)
|
2020-02-06 15:54:57 +01:00
|
|
|
return dict(presences=presences, server_timestamp=server_timestamp)
|