mirror of
https://github.com/zulip/zulip.git
synced 2025-10-23 04:52:12 +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.
209 lines
8.0 KiB
Python
209 lines
8.0 KiB
Python
from datetime import timedelta
|
|
from typing import Annotated, Any
|
|
|
|
from django.conf import settings
|
|
from django.http import HttpRequest, HttpResponse
|
|
from django.utils.timezone import now as timezone_now
|
|
from django.utils.translation import gettext as _
|
|
from pydantic import Json, StringConstraints
|
|
|
|
from zerver.actions.presence import update_user_presence
|
|
from zerver.actions.user_status import do_update_user_status
|
|
from zerver.decorator import human_users_only
|
|
from zerver.lib.emoji import check_emoji_request, get_emoji_data
|
|
from zerver.lib.exceptions import JsonableError
|
|
from zerver.lib.presence import get_presence_for_user, get_presence_response
|
|
from zerver.lib.request import RequestNotes
|
|
from zerver.lib.response import json_success
|
|
from zerver.lib.timestamp import datetime_to_timestamp
|
|
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
|
|
from zerver.lib.user_status import get_user_status
|
|
from zerver.lib.users import access_user_by_id, check_can_access_user
|
|
from zerver.models import UserActivity, UserPresence, UserProfile, UserStatus
|
|
from zerver.models.users import get_active_user, get_active_user_profile_by_id_in_realm
|
|
|
|
|
|
def get_presence_backend(
|
|
request: HttpRequest, user_profile: UserProfile, user_id_or_email: str
|
|
) -> HttpResponse:
|
|
# This isn't used by the web app; it's available for API use by
|
|
# bots and other clients. We may want to add slim_presence
|
|
# support for it (or just migrate its API wholesale) later.
|
|
|
|
try:
|
|
try:
|
|
user_id = int(user_id_or_email)
|
|
target = get_active_user_profile_by_id_in_realm(user_id, user_profile.realm)
|
|
except ValueError:
|
|
email = user_id_or_email
|
|
target = get_active_user(email, user_profile.realm)
|
|
except UserProfile.DoesNotExist:
|
|
raise JsonableError(_("No such user"))
|
|
|
|
if target.is_bot:
|
|
raise JsonableError(_("Presence is not supported for bot users."))
|
|
|
|
if settings.CAN_ACCESS_ALL_USERS_GROUP_LIMITS_PRESENCE and not check_can_access_user(
|
|
target, user_profile
|
|
):
|
|
raise JsonableError(_("Insufficient permission"))
|
|
|
|
presence_dict = get_presence_for_user(target.id)
|
|
if len(presence_dict) == 0:
|
|
raise JsonableError(
|
|
_("No presence data for {user_id_or_email}").format(user_id_or_email=user_id_or_email)
|
|
)
|
|
|
|
# For initial version, we just include the status and timestamp keys
|
|
result = dict(presence=presence_dict[target.email])
|
|
aggregated_info = result["presence"]["aggregated"]
|
|
aggr_status_duration = datetime_to_timestamp(timezone_now()) - aggregated_info["timestamp"]
|
|
if aggr_status_duration > settings.OFFLINE_THRESHOLD_SECS:
|
|
aggregated_info["status"] = "offline"
|
|
for val in result["presence"].values():
|
|
val.pop("client", None)
|
|
val.pop("pushable", None)
|
|
return json_success(request, data=result)
|
|
|
|
|
|
def get_status_backend(
|
|
request: HttpRequest, user_profile: UserProfile, user_id: int
|
|
) -> HttpResponse:
|
|
target_user = access_user_by_id(user_profile, user_id, for_admin=False)
|
|
return json_success(request, data={"status": get_user_status(target_user)})
|
|
|
|
|
|
@human_users_only
|
|
@typed_endpoint
|
|
def update_user_status_backend(
|
|
request: HttpRequest,
|
|
user_profile: UserProfile,
|
|
*,
|
|
away: Json[bool] | None = None,
|
|
status_text: Annotated[
|
|
str | None, StringConstraints(strip_whitespace=True, max_length=60)
|
|
] = None,
|
|
emoji_name: str | None = None,
|
|
emoji_code: str | None = None,
|
|
# TODO: emoji_type is the more appropriate name for this parameter, but changing
|
|
# that requires nontrivial work on the API documentation, since it's not clear
|
|
# that the reactions endpoint would prefer such a change.
|
|
emoji_type: Annotated[str | None, ApiParamConfig("reaction_type")] = None,
|
|
) -> HttpResponse:
|
|
if status_text is not None:
|
|
status_text = status_text.strip()
|
|
|
|
if (away is None) and (status_text is None) and (emoji_name is None):
|
|
raise JsonableError(_("Client did not pass any new values."))
|
|
|
|
if emoji_name == "":
|
|
# Reset the emoji_code and reaction_type if emoji_name is empty.
|
|
# This should clear the user's configured emoji.
|
|
emoji_code = ""
|
|
emoji_type = UserStatus.UNICODE_EMOJI
|
|
|
|
elif emoji_name is not None:
|
|
if emoji_code is None or emoji_type is None:
|
|
emoji_data = get_emoji_data(user_profile.realm_id, emoji_name)
|
|
if emoji_code is None:
|
|
# The emoji_code argument is only required for rare corner
|
|
# cases discussed in the long block comment below. For simple
|
|
# API clients, we allow specifying just the name, and just
|
|
# look up the code using the current name->code mapping.
|
|
emoji_code = emoji_data.emoji_code
|
|
|
|
if emoji_type is None:
|
|
emoji_type = emoji_data.reaction_type
|
|
|
|
elif emoji_type or emoji_code:
|
|
raise JsonableError(
|
|
_("Client must pass emoji_name if they pass either emoji_code or reaction_type.")
|
|
)
|
|
|
|
# If we're asking to set an emoji (not clear it ("") or not adjust
|
|
# it (None)), we need to verify the emoji is valid.
|
|
if emoji_name not in ["", None]:
|
|
assert emoji_name is not None
|
|
assert emoji_code is not None
|
|
assert emoji_type is not None
|
|
check_emoji_request(user_profile.realm, emoji_name, emoji_code, emoji_type)
|
|
|
|
client = RequestNotes.get_notes(request).client
|
|
assert client is not None
|
|
do_update_user_status(
|
|
user_profile=user_profile,
|
|
away=away,
|
|
status_text=status_text,
|
|
client_id=client.id,
|
|
emoji_name=emoji_name,
|
|
emoji_code=emoji_code,
|
|
reaction_type=emoji_type,
|
|
)
|
|
|
|
return json_success(request)
|
|
|
|
|
|
@human_users_only
|
|
@typed_endpoint
|
|
def update_active_status_backend(
|
|
request: HttpRequest,
|
|
user_profile: UserProfile,
|
|
*,
|
|
status: str,
|
|
ping_only: Json[bool] = False,
|
|
new_user_input: Json[bool] = False,
|
|
slim_presence: Json[bool] = False,
|
|
last_update_id: Json[int] | None = None,
|
|
history_limit_days: Json[int] | None = None,
|
|
) -> HttpResponse:
|
|
if last_update_id is not None:
|
|
# This param being submitted by the client, means they want to use
|
|
# the modern API.
|
|
slim_presence = True
|
|
|
|
status_val = UserPresence.status_from_string(status)
|
|
if status_val is None:
|
|
raise JsonableError(_("Invalid status: {status}").format(status=status))
|
|
elif user_profile.presence_enabled:
|
|
client = RequestNotes.get_notes(request).client
|
|
assert client is not None
|
|
update_user_presence(user_profile, client, timezone_now(), status_val, new_user_input)
|
|
|
|
if ping_only:
|
|
ret: dict[str, Any] = {}
|
|
else:
|
|
ret = get_presence_response(
|
|
user_profile,
|
|
slim_presence,
|
|
last_update_id_fetched_by_client=last_update_id,
|
|
history_limit_days=history_limit_days,
|
|
)
|
|
|
|
if user_profile.realm.is_zephyr_mirror_realm:
|
|
# In zephyr mirroring realms, users can't see the presence of other
|
|
# users, but each user **is** interested in whether their mirror bot
|
|
# (running as their user) has been active.
|
|
try:
|
|
activity = UserActivity.objects.get(
|
|
user_profile=user_profile, query="get_events", client__name="zephyr_mirror"
|
|
)
|
|
|
|
ret["zephyr_mirror_active"] = activity.last_visit > timezone_now() - timedelta(
|
|
minutes=5
|
|
)
|
|
except UserActivity.DoesNotExist:
|
|
ret["zephyr_mirror_active"] = False
|
|
|
|
return json_success(request, data=ret)
|
|
|
|
|
|
def get_statuses_for_realm(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
|
# This isn't used by the web app; it's available for API use by
|
|
# bots and other clients. We may want to add slim_presence
|
|
# support for it (or just migrate its API wholesale) later.
|
|
data = get_presence_response(user_profile, slim_presence=False)
|
|
|
|
# We're not interested in the last_update_id field in this context.
|
|
data.pop("presence_last_update_id", None)
|
|
return json_success(request, data=data)
|