mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 16:14:02 +00:00
This param allows clients to specify how much presence history they want to fetch. Previously, the server always returned 14 days of history. With the recent migration of the presence API to the much more efficient system relying on incremental fetches via the last_update_id param added in #29999, we can now afford to provide much more history to clients that request it - as all that historical data will only be fetched once. There are three endpoints involved: - `/register` - this is the main useful endpoint for this, used by API clients to fetch initial data and register an events queue. Clients can pass the `presence_history_limit_days` param here. - `/users/me/presence` - this endpoint is currently used by clients to update their presence status and fetch incremental data, making the new functionality not particularly useful here. However, we still add the new `history_limit_days` param here, in case in the future clients transition to using this also for the initial presence data fetch. - `/` - used when opening the webapp. Naturally, params aren't passed here, so the server just assumes a value from `settings.PRESENCE_HISTORY_LIMIT_DAYS_FOR_WEB_APP` and returns information about this default value in page_params.
273 lines
9.7 KiB
Python
273 lines
9.7 KiB
Python
import time
|
|
from collections import defaultdict
|
|
from collections.abc import Mapping, Sequence
|
|
from datetime import datetime, timedelta
|
|
from typing import Any
|
|
|
|
from django.conf import settings
|
|
from django.utils.timezone import now as timezone_now
|
|
|
|
from zerver.lib.timestamp import datetime_to_timestamp
|
|
from zerver.lib.users import check_user_can_access_all_users, get_accessible_user_ids
|
|
from zerver.models import Realm, UserPresence, UserProfile
|
|
|
|
|
|
def get_presence_dicts_for_rows(
|
|
all_rows: Sequence[Mapping[str, Any]], slim_presence: bool
|
|
) -> dict[str, dict[str, Any]]:
|
|
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"])
|
|
get_user_presence_info = get_modern_user_presence_info
|
|
else:
|
|
get_user_key = lambda row: row["user_profile__email"]
|
|
get_user_presence_info = get_legacy_user_presence_info
|
|
|
|
user_statuses: dict[str, dict[str, Any]] = {}
|
|
|
|
for presence_row in all_rows:
|
|
user_key = get_user_key(presence_row)
|
|
|
|
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"]
|
|
)
|
|
|
|
info = get_user_presence_info(
|
|
last_active_time,
|
|
last_connected_time,
|
|
)
|
|
user_statuses[user_key] = info
|
|
|
|
return user_statuses
|
|
|
|
|
|
def user_presence_datetime_with_date_joined_default(
|
|
dt: datetime | None, date_joined: datetime
|
|
) -> datetime:
|
|
"""
|
|
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
|
|
to default to a sane
|
|
|
|
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
|
|
|
|
|
|
def get_modern_user_presence_info(
|
|
last_active_time: datetime, last_connected_time: datetime
|
|
) -> dict[str, Any]:
|
|
# TODO: Do further bandwidth optimizations to this structure.
|
|
result = {}
|
|
result["active_timestamp"] = datetime_to_timestamp(last_active_time)
|
|
result["idle_timestamp"] = datetime_to_timestamp(last_connected_time)
|
|
return result
|
|
|
|
|
|
def get_legacy_user_presence_info(
|
|
last_active_time: datetime, last_connected_time: datetime
|
|
) -> dict[str, Any]:
|
|
"""
|
|
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.
|
|
"""
|
|
|
|
# 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.
|
|
most_recent_info = format_legacy_presence_dict(last_active_time, last_connected_time)
|
|
|
|
result = {}
|
|
|
|
# The word "aggregated" 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"],
|
|
)
|
|
|
|
result["website"] = most_recent_info
|
|
|
|
return result
|
|
|
|
|
|
def format_legacy_presence_dict(
|
|
last_active_time: datetime, last_connected_time: datetime
|
|
) -> dict[str, Any]:
|
|
"""
|
|
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 (
|
|
last_active_time
|
|
+ timedelta(seconds=settings.PRESENCE_LEGACY_EVENT_OFFSET_FOR_ACTIVITY_SECONDS)
|
|
>= last_connected_time
|
|
):
|
|
status = UserPresence.LEGACY_STATUS_ACTIVE
|
|
timestamp = datetime_to_timestamp(last_active_time)
|
|
else:
|
|
status = UserPresence.LEGACY_STATUS_IDLE
|
|
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
|
|
|
|
return dict(client="website", status=status, timestamp=timestamp, pushable=pushable)
|
|
|
|
|
|
def get_presence_for_user(
|
|
user_profile_id: int, slim_presence: bool = False
|
|
) -> dict[str, dict[str, Any]]:
|
|
query = UserPresence.objects.filter(user_profile_id=user_profile_id).values(
|
|
"last_active_time",
|
|
"last_connected_time",
|
|
"user_profile__email",
|
|
"user_profile_id",
|
|
"user_profile__enable_offline_push_notifications",
|
|
"user_profile__date_joined",
|
|
)
|
|
presence_rows = list(query)
|
|
|
|
return get_presence_dicts_for_rows(presence_rows, slim_presence)
|
|
|
|
|
|
def get_presence_dict_by_realm(
|
|
realm: Realm,
|
|
slim_presence: bool = False,
|
|
last_update_id_fetched_by_client: int | None = None,
|
|
history_limit_days: int | None = None,
|
|
requesting_user_profile: UserProfile | None = None,
|
|
) -> tuple[dict[str, dict[str, Any]], int]:
|
|
now = timezone_now()
|
|
if history_limit_days is not None:
|
|
fetch_since_datetime = now - timedelta(days=history_limit_days)
|
|
else:
|
|
# The original behavior for this API was to return last two weeks
|
|
# of data at most, so we preserve that when the history_limit_days
|
|
# param is not provided.
|
|
fetch_since_datetime = now - timedelta(days=14)
|
|
|
|
kwargs: dict[str, object] = dict()
|
|
if last_update_id_fetched_by_client is not None:
|
|
kwargs["last_update_id__gt"] = last_update_id_fetched_by_client
|
|
|
|
if last_update_id_fetched_by_client is None or last_update_id_fetched_by_client <= 0:
|
|
# If the client already has fetched some presence data, as indicated by
|
|
# last_update_id_fetched_by_client, then filtering by last_connected_time
|
|
# is redundant, as it shouldn't affect the results.
|
|
kwargs["last_connected_time__gte"] = fetch_since_datetime
|
|
|
|
if history_limit_days != 0:
|
|
query = UserPresence.objects.filter(
|
|
realm_id=realm.id,
|
|
user_profile__is_active=True,
|
|
user_profile__is_bot=False,
|
|
**kwargs,
|
|
)
|
|
else:
|
|
# If history_limit_days is 0, the client doesn't want any presence data.
|
|
# Explicitly return an empty QuerySet to avoid a query or races which
|
|
# might cause a UserPresence row to get fetched if it gets updated
|
|
# during the execution of this function.
|
|
query = UserPresence.objects.none()
|
|
|
|
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",
|
|
"last_update_id",
|
|
)
|
|
)
|
|
# Get max last_update_id from the list.
|
|
if presence_rows:
|
|
last_update_id_fetched_by_server: int | None = max(
|
|
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
|
|
|
|
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
|
|
|
|
|
|
def get_presences_for_realm(
|
|
realm: Realm,
|
|
slim_presence: bool,
|
|
last_update_id_fetched_by_client: int | None,
|
|
history_limit_days: int | None,
|
|
requesting_user_profile: UserProfile,
|
|
) -> tuple[dict[str, dict[str, dict[str, Any]]], int]:
|
|
if realm.presence_disabled:
|
|
# Return an empty dict if presence is disabled in this realm
|
|
return defaultdict(dict), -1
|
|
|
|
return get_presence_dict_by_realm(
|
|
realm,
|
|
slim_presence,
|
|
last_update_id_fetched_by_client,
|
|
history_limit_days,
|
|
requesting_user_profile=requesting_user_profile,
|
|
)
|
|
|
|
|
|
def get_presence_response(
|
|
requesting_user_profile: UserProfile,
|
|
slim_presence: bool,
|
|
last_update_id_fetched_by_client: int | None = None,
|
|
history_limit_days: int | None = None,
|
|
) -> dict[str, Any]:
|
|
realm = requesting_user_profile.realm
|
|
server_timestamp = time.time()
|
|
presences, last_update_id_fetched_by_server = get_presences_for_realm(
|
|
realm,
|
|
slim_presence,
|
|
last_update_id_fetched_by_client,
|
|
history_limit_days,
|
|
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
|