mirror of https://github.com/zulip/zulip.git
api: Support user_id in get_user_presence_endpoint.
This is part of our general process of replacing emails, which are not static with time, with user_ids when referring to users in the API. We still keep the `email` reference option, since it can be useful for linking third-party applications to Zulip on an intranet that might have a user's corporate email handy and not want to do the extra round trip to lookup the user. The name of the parameter, user_id_or_email, was chosen to to make it clear that the default/preferred option is user_id. Fixes #14304.
This commit is contained in:
parent
dc67870e0c
commit
55de66f944
|
@ -10,6 +10,11 @@ below features are supported.
|
||||||
|
|
||||||
## Changes in Zulip 4.0
|
## Changes in Zulip 4.0
|
||||||
|
|
||||||
|
**Feature level 43**
|
||||||
|
|
||||||
|
* [`GET /users/{user_id_or_email}/presence`]: Added support for
|
||||||
|
passing the `user_id` to identify the target user.
|
||||||
|
|
||||||
**Feature level 42**
|
**Feature level 42**
|
||||||
|
|
||||||
* `PATCH /settings/display`: Added a new `default_view` setting allowing
|
* `PATCH /settings/display`: Added a new `default_view` setting allowing
|
||||||
|
|
|
@ -1,32 +1,32 @@
|
||||||
# Get user presence
|
# Get user presence
|
||||||
|
|
||||||
{generate_api_description(/users/{email}/presence:get)}
|
{generate_api_description(/users/{user_id_or_email}/presence:get)}
|
||||||
|
|
||||||
## Usage examples
|
## Usage examples
|
||||||
|
|
||||||
{start_tabs}
|
{start_tabs}
|
||||||
{tab|python}
|
{tab|python}
|
||||||
|
|
||||||
{generate_code_example(python)|/users/{email}/presence:get|example}
|
{generate_code_example(python)|/users/{user_id_or_email}/presence:get|example}
|
||||||
|
|
||||||
{tab|curl}
|
{tab|curl}
|
||||||
|
|
||||||
{generate_code_example(curl)|/users/{email}/presence:get|example}
|
{generate_code_example(curl)|/users/{user_id_or_email}/presence:get|example}
|
||||||
|
|
||||||
{end_tabs}
|
{end_tabs}
|
||||||
|
|
||||||
## Parameters
|
## Parameters
|
||||||
|
|
||||||
{generate_api_arguments_table|zulip.yaml|/users/{email}/presence:get}
|
{generate_api_arguments_table|zulip.yaml|/users/{user_id_or_email}/presence:get}
|
||||||
|
|
||||||
## Response
|
## Response
|
||||||
|
|
||||||
#### Return values
|
#### Return values
|
||||||
|
|
||||||
{generate_return_values_table|zulip.yaml|/users/{email}/presence:get}
|
{generate_return_values_table|zulip.yaml|/users/{user_id_or_email}/presence:get}
|
||||||
|
|
||||||
#### Example response
|
#### Example response
|
||||||
|
|
||||||
A typical successful JSON response may look like:
|
A typical successful JSON response may look like:
|
||||||
|
|
||||||
{generate_code_example|/users/{email}/presence:get|fixture(200)}
|
{generate_code_example|/users/{user_id_or_email}/presence:get|fixture(200)}
|
||||||
|
|
|
@ -30,7 +30,7 @@ DESKTOP_WARNING_VERSION = "5.2.0"
|
||||||
#
|
#
|
||||||
# Changes should be accompanied by documentation explaining what the
|
# Changes should be accompanied by documentation explaining what the
|
||||||
# new level means in templates/zerver/api/changelog.md.
|
# new level means in templates/zerver/api/changelog.md.
|
||||||
API_FEATURE_LEVEL = 42
|
API_FEATURE_LEVEL = 43
|
||||||
|
|
||||||
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
# Bump the minor PROVISION_VERSION to indicate that folks should provision
|
||||||
# only when going from an old version of the code to a newer version. Bump
|
# only when going from an old version of the code to a newer version. Bump
|
||||||
|
|
|
@ -219,7 +219,7 @@ def delete_event_queue() -> Dict[str, object]:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@openapi_param_value_generator(["/users/{email}/presence:get"])
|
@openapi_param_value_generator(["/users/{user_id_or_email}/presence:get"])
|
||||||
def get_user_presence() -> Dict[str, object]:
|
def get_user_presence() -> Dict[str, object]:
|
||||||
iago = helpers.example_user("iago")
|
iago = helpers.example_user("iago")
|
||||||
client = Client.objects.create(name="curl-test-client-3")
|
client = Client.objects.create(name="curl-test-client-3")
|
||||||
|
|
|
@ -140,7 +140,7 @@ def test_authorization_errors_fatal(client: Client, nonadmin_client: Client) ->
|
||||||
validate_against_openapi_schema(result, "/users/me/subscriptions", "post", "400_1")
|
validate_against_openapi_schema(result, "/users/me/subscriptions", "post", "400_1")
|
||||||
|
|
||||||
|
|
||||||
@openapi_test_function("/users/{email}/presence:get")
|
@openapi_test_function("/users/{user_id_or_email}/presence:get")
|
||||||
def get_user_presence(client: Client) -> None:
|
def get_user_presence(client: Client) -> None:
|
||||||
|
|
||||||
# {code_example|start}
|
# {code_example|start}
|
||||||
|
@ -148,7 +148,7 @@ def get_user_presence(client: Client) -> None:
|
||||||
result = client.get_user_presence("iago@zulip.com")
|
result = client.get_user_presence("iago@zulip.com")
|
||||||
# {code_example|end}
|
# {code_example|end}
|
||||||
|
|
||||||
validate_against_openapi_schema(result, "/users/{email}/presence", "get", "200")
|
validate_against_openapi_schema(result, "/users/{user_id_or_email}/presence", "get", "200")
|
||||||
|
|
||||||
|
|
||||||
@openapi_test_function("/users/me/presence:post")
|
@openapi_test_function("/users/me/presence:post")
|
||||||
|
|
|
@ -4260,7 +4260,7 @@ paths:
|
||||||
- $ref: "#/components/schemas/JsonSuccess"
|
- $ref: "#/components/schemas/JsonSuccess"
|
||||||
- example: {"msg": "", "result": "success"}
|
- example: {"msg": "", "result": "success"}
|
||||||
|
|
||||||
/users/{email}/presence:
|
/users/{user_id_or_email}/presence:
|
||||||
get:
|
get:
|
||||||
operationId: get_user_presence
|
operationId: get_user_presence
|
||||||
tags: ["users"]
|
tags: ["users"]
|
||||||
|
@ -4273,16 +4273,19 @@ paths:
|
||||||
presence endpoint, which returns data for all active users in the
|
presence endpoint, which returns data for all active users in the
|
||||||
organization, instead.
|
organization, instead.
|
||||||
|
|
||||||
`GET {{ api_url }}/v1/users/{email}/presence`
|
`GET {{ api_url }}/v1/users/{user_id_or_email}/presence`
|
||||||
|
|
||||||
See
|
See
|
||||||
[Zulip's developer documentation](https://zulip.readthedocs.io/en/latest/subsystems/presence.html)
|
[Zulip's developer documentation](https://zulip.readthedocs.io/en/latest/subsystems/presence.html)
|
||||||
for details on the data model for presence in Zulip.
|
for details on the data model for presence in Zulip.
|
||||||
parameters:
|
parameters:
|
||||||
- name: email
|
- name: user_id_or_email
|
||||||
in: path
|
in: path
|
||||||
description: |
|
description: |
|
||||||
The email address of the user whose presence you want to fetch.
|
The user_id or Zulip display email address of the user whose presence you want to fetch.
|
||||||
|
|
||||||
|
**Changes**: New in Zulip 4.0 (feature level 43). Previous versions only supported
|
||||||
|
identifying the user by Zulip display email.
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
example: iago@zulip.com
|
example: iago@zulip.com
|
||||||
|
|
|
@ -1063,7 +1063,10 @@ class OpenAPIRegexTest(ZulipTestCase):
|
||||||
find_openapi_endpoint("/users/23/subscriptions/21")
|
find_openapi_endpoint("/users/23/subscriptions/21")
|
||||||
== "/users/{user_id}/subscriptions/{stream_id}"
|
== "/users/{user_id}/subscriptions/{stream_id}"
|
||||||
)
|
)
|
||||||
assert find_openapi_endpoint("/users/iago@zulip.com/presence") == "/users/{email}/presence"
|
assert (
|
||||||
|
find_openapi_endpoint("/users/iago@zulip.com/presence")
|
||||||
|
== "/users/{user_id_or_email}/presence"
|
||||||
|
)
|
||||||
assert find_openapi_endpoint("/users/iago@zulip.com") == "/users/{email}"
|
assert find_openapi_endpoint("/users/iago@zulip.com") == "/users/{email}"
|
||||||
assert find_openapi_endpoint("/messages/23") == "/messages/{message_id}"
|
assert find_openapi_endpoint("/messages/23") == "/messages/{message_id}"
|
||||||
assert find_openapi_endpoint("/realm/emoji/realm_emoji_1") == "/realm/emoji/{emoji_name}"
|
assert find_openapi_endpoint("/realm/emoji/realm_emoji_1") == "/realm/emoji/{emoji_name}"
|
||||||
|
|
|
@ -464,10 +464,17 @@ class SingleUserPresenceTests(ZulipTestCase):
|
||||||
result = self.client_get("/json/users/cordelia@zulip.com/presence")
|
result = self.client_get("/json/users/cordelia@zulip.com/presence")
|
||||||
self.assert_json_error(result, "No presence data for cordelia@zulip.com")
|
self.assert_json_error(result, "No presence data for cordelia@zulip.com")
|
||||||
|
|
||||||
|
cordelia = self.example_user("cordelia")
|
||||||
|
result = self.client_get(f"/json/users/{cordelia.id}/presence")
|
||||||
|
self.assert_json_error(result, f"No presence data for {cordelia.id}")
|
||||||
|
|
||||||
do_deactivate_user(self.example_user("cordelia"))
|
do_deactivate_user(self.example_user("cordelia"))
|
||||||
result = self.client_get("/json/users/cordelia@zulip.com/presence")
|
result = self.client_get("/json/users/cordelia@zulip.com/presence")
|
||||||
self.assert_json_error(result, "No such user")
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
|
result = self.client_get(f"/json/users/{cordelia.id}/presence")
|
||||||
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
result = self.client_get("/json/users/default-bot@zulip.com/presence")
|
result = self.client_get("/json/users/default-bot@zulip.com/presence")
|
||||||
self.assert_json_error(result, "Presence is not supported for bot users.")
|
self.assert_json_error(result, "Presence is not supported for bot users.")
|
||||||
|
|
||||||
|
@ -476,6 +483,10 @@ class SingleUserPresenceTests(ZulipTestCase):
|
||||||
result = self.client_get("/json/users/othello@zulip.com/presence", subdomain="zephyr")
|
result = self.client_get("/json/users/othello@zulip.com/presence", subdomain="zephyr")
|
||||||
self.assert_json_error(result, "No such user")
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
|
othello = self.example_user("othello")
|
||||||
|
result = self.client_get(f"/json/users/{othello.id}/presence", subdomain="zephyr")
|
||||||
|
self.assert_json_error(result, "No such user")
|
||||||
|
|
||||||
# Then, we check everything works
|
# Then, we check everything works
|
||||||
self.login("hamlet")
|
self.login("hamlet")
|
||||||
result = self.client_get("/json/users/othello@zulip.com/presence")
|
result = self.client_get("/json/users/othello@zulip.com/presence")
|
||||||
|
@ -485,6 +496,13 @@ class SingleUserPresenceTests(ZulipTestCase):
|
||||||
)
|
)
|
||||||
self.assertEqual(set(result_dict["presence"]["website"].keys()), {"status", "timestamp"})
|
self.assertEqual(set(result_dict["presence"]["website"].keys()), {"status", "timestamp"})
|
||||||
|
|
||||||
|
result = self.client_get(f"/json/users/{othello.id}/presence")
|
||||||
|
result_dict = result.json()
|
||||||
|
self.assertEqual(
|
||||||
|
set(result_dict["presence"].keys()), {"ZulipAndroid", "website", "aggregated"}
|
||||||
|
)
|
||||||
|
self.assertEqual(set(result_dict["presence"]["website"].keys()), {"status", "timestamp"})
|
||||||
|
|
||||||
def test_ping_only(self) -> None:
|
def test_ping_only(self) -> None:
|
||||||
|
|
||||||
self.login("othello")
|
self.login("othello")
|
||||||
|
|
|
@ -13,25 +13,40 @@ from zerver.lib.request import REQ, JsonableError, has_request_variables
|
||||||
from zerver.lib.response import json_error, json_success
|
from zerver.lib.response import json_error, json_success
|
||||||
from zerver.lib.timestamp import datetime_to_timestamp
|
from zerver.lib.timestamp import datetime_to_timestamp
|
||||||
from zerver.lib.validator import check_bool, check_capped_string
|
from zerver.lib.validator import check_bool, check_capped_string
|
||||||
from zerver.models import UserActivity, UserPresence, UserProfile, get_active_user
|
from zerver.models import (
|
||||||
|
UserActivity,
|
||||||
|
UserPresence,
|
||||||
|
UserProfile,
|
||||||
|
get_active_user,
|
||||||
|
get_active_user_profile_by_id_in_realm,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_presence_backend(
|
def get_presence_backend(
|
||||||
request: HttpRequest, user_profile: UserProfile, email: str
|
request: HttpRequest, user_profile: UserProfile, user_id_or_email: str
|
||||||
) -> HttpResponse:
|
) -> HttpResponse:
|
||||||
# This isn't used by the webapp; it's available for API use by
|
# This isn't used by the webapp; it's available for API use by
|
||||||
# bots and other clients. We may want to add slim_presence
|
# bots and other clients. We may want to add slim_presence
|
||||||
# support for it (or just migrate its API wholesale) later.
|
# support for it (or just migrate its API wholesale) later.
|
||||||
|
|
||||||
try:
|
try:
|
||||||
target = get_active_user(email, user_profile.realm)
|
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:
|
except UserProfile.DoesNotExist:
|
||||||
return json_error(_("No such user"))
|
return json_error(_("No such user"))
|
||||||
|
|
||||||
if target.is_bot:
|
if target.is_bot:
|
||||||
return json_error(_("Presence is not supported for bot users."))
|
return json_error(_("Presence is not supported for bot users."))
|
||||||
|
|
||||||
presence_dict = get_presence_for_user(target.id)
|
presence_dict = get_presence_for_user(target.id)
|
||||||
if len(presence_dict) == 0:
|
if len(presence_dict) == 0:
|
||||||
return json_error(_("No presence data for {email}").format(email=email))
|
return json_error(
|
||||||
|
_("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
|
# For initial version, we just include the status and timestamp keys
|
||||||
result = dict(presence=presence_dict[target.email])
|
result = dict(presence=presence_dict[target.email])
|
||||||
|
|
|
@ -371,7 +371,7 @@ v1_api_and_json_patterns = [
|
||||||
# It's important that this sit after users/me/presence so that
|
# It's important that this sit after users/me/presence so that
|
||||||
# Django's URL resolution order doesn't break the
|
# Django's URL resolution order doesn't break the
|
||||||
# /users/me/presence endpoint.
|
# /users/me/presence endpoint.
|
||||||
rest_path("users/<email>/presence", GET=get_presence_backend),
|
rest_path("users/<user_id_or_email>/presence", GET=get_presence_backend),
|
||||||
rest_path("realm/presence", GET=get_statuses_for_realm),
|
rest_path("realm/presence", GET=get_statuses_for_realm),
|
||||||
rest_path("users/me/status", POST=update_user_status_backend),
|
rest_path("users/me/status", POST=update_user_status_backend),
|
||||||
# user_groups -> zerver.views.user_groups
|
# user_groups -> zerver.views.user_groups
|
||||||
|
|
Loading…
Reference in New Issue