2020-02-06 15:54:57 +01:00
|
|
|
import time
|
2020-06-11 00:54:34 +02:00
|
|
|
from collections import defaultdict
|
2024-07-12 02:30:25 +02:00
|
|
|
from collections.abc import Mapping, Sequence
|
2023-11-19 19:45:19 +01:00
|
|
|
from datetime import datetime, timedelta
|
2024-07-12 02:30:25 +02:00
|
|
|
from typing import Any
|
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
|
|
|
|
|
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
|
2024-04-29 04:03:19 +02:00
|
|
|
from zerver.models import 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(
|
2024-04-29 04:03:19 +02:00
|
|
|
all_rows: Sequence[Mapping[str, Any]], slim_presence: bool
|
2024-07-12 02:30:17 +02: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
|
|
|
|
2024-07-12 02:30:17 +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(
|
2024-07-12 02:30:23 +02:00
|
|
|
dt: datetime | None, date_joined: datetime
|
2023-11-19 19:45:19 +01:00
|
|
|
) -> 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
|
2024-07-12 02:30:17 +02: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
|
2024-07-12 02:30:17 +02: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
|
2024-07-12 02:30:17 +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
|
2024-07-12 02:30:17 +02:00
|
|
|
) -> 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)
|
|
|
|
|
2024-04-29 04:03:19 +02:00
|
|
|
return get_presence_dicts_for_rows(presence_rows, slim_presence)
|
2020-02-06 16:35:22 +01:00
|
|
|
|
|
|
|
|
2022-09-16 18:12:20 +02:00
|
|
|
def get_presence_dict_by_realm(
|
2024-06-05 21:36:22 +02:00
|
|
|
realm: Realm,
|
|
|
|
slim_presence: bool = False,
|
2024-07-12 02:30:23 +02:00
|
|
|
last_update_id_fetched_by_client: int | None = None,
|
|
|
|
requesting_user_profile: UserProfile | None = None,
|
2024-07-12 02:30:17 +02:00
|
|
|
) -> tuple[dict[str, dict[str, Any]], int]:
|
2023-11-19 19:45:19 +01:00
|
|
|
two_weeks_ago = timezone_now() - timedelta(weeks=2)
|
2024-07-12 02:30:17 +02:00
|
|
|
kwargs: dict[str, object] = dict()
|
2024-06-05 21:36:22 +02:00
|
|
|
if last_update_id_fetched_by_client is not None:
|
|
|
|
kwargs["last_update_id__gt"] = last_update_id_fetched_by_client
|
|
|
|
|
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-02-08 21:50:55 +01:00
|
|
|
user_profile__is_active=True,
|
|
|
|
user_profile__is_bot=False,
|
2024-06-05 21:36:22 +02:00
|
|
|
# We can consider tweaking this value when last_update_id is being used,
|
|
|
|
# to potentially fetch more data since such a client is expected to only
|
|
|
|
# do it once and then only do small, incremental fetches.
|
|
|
|
last_connected_time__gte=two_weeks_ago,
|
|
|
|
**kwargs,
|
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",
|
2024-06-05 21:36:22 +02:00
|
|
|
"last_update_id",
|
2023-10-17 12:56:39 +02:00
|
|
|
)
|
|
|
|
)
|
2024-06-05 21:36:22 +02:00
|
|
|
# Get max last_update_id from the list.
|
|
|
|
if presence_rows:
|
2024-07-12 02:30:23 +02:00
|
|
|
last_update_id_fetched_by_server: int | None = max(
|
2024-06-05 21:36:22 +02:00
|
|
|
row["last_update_id"] for row in presence_rows
|
|
|
|
)
|
|
|
|
elif last_update_id_fetched_by_client is not None:
|
|
|
|
# If there are no results, that means that are no new updates to presence
|
|
|
|
# since what the client has last seen. Therefore, returning the same
|
|
|
|
# last_update_id that the client provided is correct.
|
|
|
|
last_update_id_fetched_by_server = last_update_id_fetched_by_client
|
|
|
|
else:
|
|
|
|
# If the client didn't specify a last_update_id, we return -1 to indicate
|
|
|
|
# the lack of any data fetched, while sticking to the convention of
|
|
|
|
# returning an integer.
|
|
|
|
last_update_id_fetched_by_server = -1
|
2020-02-06 16:23:35 +01:00
|
|
|
|
2024-06-05 21:36:22 +02:00
|
|
|
assert last_update_id_fetched_by_server is not None
|
|
|
|
return get_presence_dicts_for_rows(
|
|
|
|
presence_rows, slim_presence
|
|
|
|
), last_update_id_fetched_by_server
|
2020-02-06 15:54:57 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def get_presences_for_realm(
|
2024-06-05 21:36:22 +02:00
|
|
|
realm: Realm,
|
|
|
|
slim_presence: bool,
|
2024-07-12 02:30:23 +02:00
|
|
|
last_update_id_fetched_by_client: int | None,
|
2024-06-05 21:36:22 +02:00
|
|
|
requesting_user_profile: UserProfile,
|
2024-07-12 02:30:17 +02:00
|
|
|
) -> tuple[dict[str, dict[str, dict[str, Any]]], int]:
|
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
|
2024-06-07 18:44:20 +02:00
|
|
|
return defaultdict(dict), -1
|
2020-02-06 15:54:57 +01:00
|
|
|
|
2024-06-05 21:36:22 +02:00
|
|
|
return get_presence_dict_by_realm(
|
|
|
|
realm,
|
|
|
|
slim_presence,
|
|
|
|
last_update_id_fetched_by_client,
|
|
|
|
requesting_user_profile=requesting_user_profile,
|
|
|
|
)
|
2020-02-06 15:54:57 +01:00
|
|
|
|
2021-02-12 08:19:30 +01:00
|
|
|
|
|
|
|
def get_presence_response(
|
2024-06-05 21:36:22 +02:00
|
|
|
requesting_user_profile: UserProfile,
|
|
|
|
slim_presence: bool,
|
2024-07-12 02:30:23 +02:00
|
|
|
last_update_id_fetched_by_client: int | None = None,
|
2024-07-12 02:30:17 +02:00
|
|
|
) -> 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()
|
2024-06-05 21:36:22 +02:00
|
|
|
presences, last_update_id_fetched_by_server = get_presences_for_realm(
|
|
|
|
realm,
|
|
|
|
slim_presence,
|
|
|
|
last_update_id_fetched_by_client,
|
|
|
|
requesting_user_profile=requesting_user_profile,
|
|
|
|
)
|
|
|
|
|
|
|
|
response_dict = dict(
|
|
|
|
presences=presences,
|
|
|
|
server_timestamp=server_timestamp,
|
|
|
|
presence_last_update_id=last_update_id_fetched_by_server,
|
|
|
|
)
|
|
|
|
|
|
|
|
return response_dict
|