2020-02-06 15:54:57 +01:00
|
|
|
import time
|
2020-06-11 00:54:34 +02:00
|
|
|
from collections import defaultdict
|
2023-11-19 19:45:19 +01:00
|
|
|
from datetime import datetime, timedelta
|
2023-04-08 15:52:48 +02:00
|
|
|
from typing import Any, Dict, Mapping, Optional, Sequence, Set
|
2020-02-06 16:23:35 +01:00
|
|
|
|
2020-06-11 16:03:47 +02:00
|
|
|
from django.conf import settings
|
2020-02-06 16:23:35 +01:00
|
|
|
from django.utils.timezone import now as timezone_now
|
|
|
|
|
2023-12-15 21:10:20 +01:00
|
|
|
from zerver.lib.query_helpers import query_for_ids
|
2020-02-06 16:42:28 +01:00
|
|
|
from zerver.lib.timestamp import datetime_to_timestamp
|
2023-10-17 12:56:39 +02:00
|
|
|
from zerver.lib.users import check_user_can_access_all_users, get_accessible_user_ids
|
2023-12-15 21:10:20 +01:00
|
|
|
from zerver.models import PushDeviceToken, Realm, UserPresence, UserProfile
|
2020-06-11 00:54:34 +02:00
|
|
|
|
2020-02-06 16:23:35 +01:00
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
def get_presence_dicts_for_rows(
|
2021-08-19 10:45:20 +02:00
|
|
|
all_rows: Sequence[Mapping[str, Any]], mobile_user_ids: Set[int], slim_presence: bool
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> Dict[str, Dict[str, Any]]:
|
2020-02-06 21:00:21 +01:00
|
|
|
if slim_presence:
|
|
|
|
# Stringify user_id here, since it's gonna be turned
|
|
|
|
# into a string anyway by JSON, and it keeps mypy happy.
|
2021-04-22 16:23:09 +02:00
|
|
|
get_user_key = lambda row: str(row["user_profile_id"])
|
2022-09-16 18:12:20 +02:00
|
|
|
get_user_presence_info = get_modern_user_presence_info
|
2020-02-06 21:00:21 +01:00
|
|
|
else:
|
2021-02-12 08:20:45 +01:00
|
|
|
get_user_key = lambda row: row["user_profile__email"]
|
2022-09-16 18:12:20 +02:00
|
|
|
get_user_presence_info = get_legacy_user_presence_info
|
2020-02-06 21:00:21 +01:00
|
|
|
|
2020-09-02 08:14:51 +02:00
|
|
|
user_statuses: Dict[str, Dict[str, Any]] = {}
|
2020-02-06 21:00:21 +01:00
|
|
|
|
2020-06-11 16:03:47 +02:00
|
|
|
for presence_row in all_rows:
|
|
|
|
user_key = get_user_key(presence_row)
|
2023-04-08 15:52:48 +02:00
|
|
|
|
|
|
|
last_active_time = user_presence_datetime_with_date_joined_default(
|
|
|
|
presence_row["last_active_time"], presence_row["user_profile__date_joined"]
|
|
|
|
)
|
|
|
|
last_connected_time = user_presence_datetime_with_date_joined_default(
|
|
|
|
presence_row["last_connected_time"], presence_row["user_profile__date_joined"]
|
|
|
|
)
|
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
info = get_user_presence_info(
|
2023-04-08 15:52:48 +02:00
|
|
|
last_active_time,
|
|
|
|
last_connected_time,
|
2020-02-06 21:00:21 +01:00
|
|
|
)
|
|
|
|
user_statuses[user_key] = info
|
|
|
|
|
|
|
|
return user_statuses
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-04-08 15:52:48 +02:00
|
|
|
def user_presence_datetime_with_date_joined_default(
|
2023-11-19 19:45:19 +01:00
|
|
|
dt: Optional[datetime], date_joined: datetime
|
|
|
|
) -> datetime:
|
2023-04-08 15:52:48 +02:00
|
|
|
"""
|
|
|
|
Our data models support UserPresence objects not having None
|
|
|
|
values for last_active_time/last_connected_time. The legacy API
|
|
|
|
however has always sent timestamps, so for backward
|
|
|
|
compatibility we cannot send such values through the API and need
|
2023-11-19 19:45:19 +01:00
|
|
|
to default to a sane
|
2023-04-08 15:52:48 +02:00
|
|
|
|
|
|
|
This helper functions expects to take a last_active_time or
|
|
|
|
last_connected_time value and the date_joined of the user, which
|
|
|
|
will serve as the default value if the first argument is None.
|
|
|
|
"""
|
|
|
|
if dt is None:
|
|
|
|
return date_joined
|
|
|
|
|
|
|
|
return dt
|
|
|
|
|
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
def get_modern_user_presence_info(
|
2023-11-19 19:45:19 +01:00
|
|
|
last_active_time: datetime, last_connected_time: datetime
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> Dict[str, Any]:
|
2020-06-11 16:03:47 +02:00
|
|
|
# TODO: Do further bandwidth optimizations to this structure.
|
2020-09-02 08:14:51 +02:00
|
|
|
result = {}
|
2020-06-11 16:03:47 +02:00
|
|
|
result["active_timestamp"] = datetime_to_timestamp(last_active_time)
|
|
|
|
result["idle_timestamp"] = datetime_to_timestamp(last_connected_time)
|
2020-02-07 14:50:30 +01:00
|
|
|
return result
|
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
def get_legacy_user_presence_info(
|
2023-11-19 19:45:19 +01:00
|
|
|
last_active_time: datetime, last_connected_time: datetime
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> Dict[str, Any]:
|
2023-04-04 19:35:06 +02:00
|
|
|
"""
|
|
|
|
Reformats the modern UserPresence data structure so that legacy
|
|
|
|
API clients can still access presence data.
|
|
|
|
We expect this code to remain mostly unchanged until we can delete it.
|
|
|
|
"""
|
2020-06-11 16:03:47 +02:00
|
|
|
|
|
|
|
# Now we put things together in the legacy presence format with
|
|
|
|
# one client + an `aggregated` field.
|
|
|
|
#
|
|
|
|
# TODO: Look at whether we can drop to just the "aggregated" field
|
|
|
|
# if no clients look at the rest.
|
2023-04-04 19:35:06 +02:00
|
|
|
most_recent_info = format_legacy_presence_dict(last_active_time, last_connected_time)
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2020-09-02 08:14:51 +02:00
|
|
|
result = {}
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2022-02-08 00:13:33 +01:00
|
|
|
# The word "aggregated" here is possibly misleading.
|
2020-02-06 21:00:21 +01:00
|
|
|
# It's really just the most recent client's info.
|
2021-02-12 08:20:45 +01:00
|
|
|
result["aggregated"] = dict(
|
|
|
|
client=most_recent_info["client"],
|
|
|
|
status=most_recent_info["status"],
|
|
|
|
timestamp=most_recent_info["timestamp"],
|
2020-02-06 21:00:21 +01:00
|
|
|
)
|
|
|
|
|
2023-04-04 19:35:06 +02:00
|
|
|
result["website"] = most_recent_info
|
2020-02-06 21:00:21 +01:00
|
|
|
|
|
|
|
return result
|
2020-02-06 16:42:28 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
2023-04-04 19:35:06 +02:00
|
|
|
def format_legacy_presence_dict(
|
2023-11-19 19:45:19 +01:00
|
|
|
last_active_time: datetime, last_connected_time: datetime
|
2023-04-04 19:35:06 +02:00
|
|
|
) -> Dict[str, Any]:
|
2020-06-11 16:03:47 +02:00
|
|
|
"""
|
|
|
|
This function assumes it's being called right after the presence object was updated,
|
|
|
|
and is not meant to be used on old presence data.
|
|
|
|
"""
|
|
|
|
if (
|
2023-04-04 19:35:06 +02:00
|
|
|
last_active_time
|
2023-11-19 19:45:19 +01:00
|
|
|
+ timedelta(seconds=settings.PRESENCE_LEGACY_EVENT_OFFSET_FOR_ACTIVITY_SECONDS)
|
2023-04-04 19:35:06 +02:00
|
|
|
>= last_connected_time
|
2020-06-11 16:03:47 +02:00
|
|
|
):
|
|
|
|
status = UserPresence.LEGACY_STATUS_ACTIVE
|
2023-04-04 19:35:06 +02:00
|
|
|
timestamp = datetime_to_timestamp(last_active_time)
|
2020-06-11 16:03:47 +02:00
|
|
|
else:
|
|
|
|
status = UserPresence.LEGACY_STATUS_IDLE
|
2023-04-04 19:35:06 +02:00
|
|
|
timestamp = datetime_to_timestamp(last_connected_time)
|
|
|
|
|
|
|
|
# This field was never used by clients of the legacy API, so we
|
|
|
|
# just set it to a fixed value for API format compatibility.
|
|
|
|
pushable = False
|
2020-06-11 16:03:47 +02:00
|
|
|
|
2023-04-04 19:35:06 +02:00
|
|
|
return dict(client="website", status=status, timestamp=timestamp, pushable=pushable)
|
2020-06-11 16:03:47 +02:00
|
|
|
|
|
|
|
|
2021-02-12 08:19:30 +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(
|
2020-06-11 16:03:47 +02:00
|
|
|
"last_active_time",
|
|
|
|
"last_connected_time",
|
2021-02-12 08:20:45 +01:00
|
|
|
"user_profile__email",
|
2021-04-22 16:23:09 +02:00
|
|
|
"user_profile_id",
|
2021-02-12 08:20:45 +01:00
|
|
|
"user_profile__enable_offline_push_notifications",
|
2023-04-08 15:52:48 +02:00
|
|
|
"user_profile__date_joined",
|
2020-02-06 16:35:22 +01:00
|
|
|
)
|
|
|
|
presence_rows = list(query)
|
|
|
|
|
python: Convert assignment type annotations to Python 3.6 style.
This commit was split by tabbott; this piece covers the vast majority
of files in Zulip, but excludes scripts/, tools/, and puppet/ to help
ensure we at least show the right error messages for Xenial systems.
We can likely further refine the remaining pieces with some testing.
Generated by com2ann, with whitespace fixes and various manual fixes
for runtime issues:
- invoiced_through: Optional[LicenseLedger] = models.ForeignKey(
+ invoiced_through: Optional["LicenseLedger"] = models.ForeignKey(
-_apns_client: Optional[APNsClient] = None
+_apns_client: Optional["APNsClient"] = None
- notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- signup_notifications_stream: Optional[Stream] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
+ signup_notifications_stream: Optional["Stream"] = models.ForeignKey('Stream', related_name='+', null=True, blank=True, on_delete=CASCADE)
- author: Optional[UserProfile] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
+ author: Optional["UserProfile"] = models.ForeignKey('UserProfile', blank=True, null=True, on_delete=CASCADE)
- bot_owner: Optional[UserProfile] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
+ bot_owner: Optional["UserProfile"] = models.ForeignKey('self', null=True, on_delete=models.SET_NULL)
- default_sending_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
- default_events_register_stream: Optional[Stream] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_sending_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
+ default_events_register_stream: Optional["Stream"] = models.ForeignKey('zerver.Stream', null=True, related_name='+', on_delete=CASCADE)
-descriptors_by_handler_id: Dict[int, ClientDescriptor] = {}
+descriptors_by_handler_id: Dict[int, "ClientDescriptor"] = {}
-worker_classes: Dict[str, Type[QueueProcessingWorker]] = {}
-queues: Dict[str, Dict[str, Type[QueueProcessingWorker]]] = {}
+worker_classes: Dict[str, Type["QueueProcessingWorker"]] = {}
+queues: Dict[str, Dict[str, Type["QueueProcessingWorker"]]] = {}
-AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional[LDAPSearch] = None
+AUTH_LDAP_REVERSE_EMAIL_SEARCH: Optional["LDAPSearch"] = None
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-22 01:09:50 +02:00
|
|
|
mobile_user_ids: Set[int] = set()
|
2020-02-06 16:35:22 +01:00
|
|
|
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)
|
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
return get_presence_dicts_for_rows(presence_rows, mobile_user_ids, slim_presence)
|
2020-02-06 16:35:22 +01:00
|
|
|
|
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
def get_presence_dict_by_realm(
|
2023-10-17 12:56:39 +02:00
|
|
|
realm: Realm, slim_presence: bool = False, requesting_user_profile: Optional[UserProfile] = None
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> Dict[str, Dict[str, Any]]:
|
2023-11-19 19:45:19 +01:00
|
|
|
two_weeks_ago = timezone_now() - timedelta(weeks=2)
|
2020-02-06 16:23:35 +01:00
|
|
|
query = UserPresence.objects.filter(
|
2023-10-17 12:56:39 +02:00
|
|
|
realm_id=realm.id,
|
2020-06-11 16:03:47 +02:00
|
|
|
last_connected_time__gte=two_weeks_ago,
|
2020-02-08 21:50:55 +01:00
|
|
|
user_profile__is_active=True,
|
|
|
|
user_profile__is_bot=False,
|
2020-02-06 16:23:35 +01:00
|
|
|
)
|
|
|
|
|
2023-10-17 12:56:39 +02:00
|
|
|
if settings.CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE and not check_user_can_access_all_users(
|
|
|
|
requesting_user_profile
|
|
|
|
):
|
|
|
|
assert requesting_user_profile is not None
|
|
|
|
accessible_user_ids = get_accessible_user_ids(realm, requesting_user_profile)
|
|
|
|
query = query.filter(user_profile_id__in=accessible_user_ids)
|
|
|
|
|
|
|
|
presence_rows = list(
|
|
|
|
query.values(
|
|
|
|
"last_active_time",
|
|
|
|
"last_connected_time",
|
|
|
|
"user_profile__email",
|
|
|
|
"user_profile_id",
|
|
|
|
"user_profile__enable_offline_push_notifications",
|
|
|
|
"user_profile__date_joined",
|
|
|
|
)
|
|
|
|
)
|
2020-02-06 16:23:35 +01:00
|
|
|
|
2021-02-12 08:20:45 +01:00
|
|
|
mobile_query = PushDeviceToken.objects.distinct("user_id").values_list(
|
|
|
|
"user_id",
|
python: Use trailing commas consistently.
Automatically generated by the following script, based on the output
of lint with flake8-comma:
import re
import sys
last_filename = None
last_row = None
lines = []
for msg in sys.stdin:
m = re.match(
r"\x1b\[35mflake8 \|\x1b\[0m \x1b\[1;31m(.+):(\d+):(\d+): (\w+)", msg
)
if m:
filename, row_str, col_str, err = m.groups()
row, col = int(row_str), int(col_str)
if filename == last_filename:
assert last_row != row
else:
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
with open(filename) as f:
lines = f.readlines()
last_filename = filename
last_row = row
line = lines[row - 1]
if err in ["C812", "C815"]:
lines[row - 1] = line[: col - 1] + "," + line[col - 1 :]
elif err in ["C819"]:
assert line[col - 2] == ","
lines[row - 1] = line[: col - 2] + line[col - 1 :].lstrip(" ")
if last_filename is not None:
with open(last_filename, "w") as f:
f.writelines(lines)
Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
2020-04-10 05:23:40 +02:00
|
|
|
flat=True,
|
2020-02-06 16:23:35 +01:00
|
|
|
)
|
|
|
|
|
2021-04-22 16:23:09 +02:00
|
|
|
user_profile_ids = [presence_row["user_profile_id"] for presence_row in presence_rows]
|
2020-05-14 19:18:50 +02:00
|
|
|
if len(user_profile_ids) == 0:
|
|
|
|
# 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 {}
|
|
|
|
|
2022-06-23 20:21:54 +02:00
|
|
|
mobile_query_ids = query_for_ids(
|
2020-02-06 16:23:35 +01:00
|
|
|
query=mobile_query,
|
|
|
|
user_ids=user_profile_ids,
|
2021-02-12 08:20:45 +01:00
|
|
|
field="user_id",
|
2020-02-06 16:23:35 +01:00
|
|
|
)
|
2022-06-23 20:21:54 +02:00
|
|
|
mobile_user_ids = set(mobile_query_ids)
|
2020-02-06 16:23:35 +01:00
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
return get_presence_dicts_for_rows(presence_rows, mobile_user_ids, slim_presence)
|
2020-02-06 15:54:57 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def get_presences_for_realm(
|
2023-10-17 12:56:39 +02:00
|
|
|
realm: Realm, slim_presence: bool, requesting_user_profile: UserProfile
|
2021-02-12 08:19:30 +01:00
|
|
|
) -> Dict[str, Dict[str, Dict[str, Any]]]:
|
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)
|
|
|
|
|
2023-10-17 12:56:39 +02:00
|
|
|
return get_presence_dict_by_realm(realm, slim_presence, requesting_user_profile)
|
2020-02-06 15:54:57 +01:00
|
|
|
|
2021-02-12 08:19:30 +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()
|
2023-10-17 12:56:39 +02:00
|
|
|
presences = get_presences_for_realm(realm, slim_presence, requesting_user_profile)
|
2020-02-06 15:54:57 +01:00
|
|
|
return dict(presences=presences, server_timestamp=server_timestamp)
|