api: Add "users/<int:user_id>/status" endpoint.

The documentation Creates a shared UserStatus schema that's used for
the return value of this new endpoint and for the existing user_status
objects returned by the register queue endpoint.

Co-authored-by: Suyash Vardhan Mathur <suyash.mathur@research.iiit.ac.in>

Fixes #19079.
This commit is contained in:
Vector73 2024-05-16 19:29:27 +05:30 committed by Tim Abbott
parent d6672c57ff
commit 62dfd93a83
9 changed files with 276 additions and 60 deletions

View File

@ -20,6 +20,12 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 9.0 ## Changes in Zulip 9.0
**Feature level 262**:
* [`GET /users/{user_id}/status`](/api/get-user-status): Added a new
endpoint to fetch an individual user's currently set
[status](/help/status-and-availability).
**Feature level 261** **Feature level 261**
* [`POST /invites`](/api/send-invites), * [`POST /invites`](/api/send-invites),

View File

@ -69,6 +69,7 @@
* [Deactivate own user](/api/deactivate-own-user) * [Deactivate own user](/api/deactivate-own-user)
* [Set "typing" status](/api/set-typing-status) * [Set "typing" status](/api/set-typing-status)
* [Get user presence](/api/get-user-presence) * [Get user presence](/api/get-user-presence)
* [Get a user's status](/api/get-user-status)
* [Get presence of all users](/api/get-presence) * [Get presence of all users](/api/get-presence)
* [Get attachments](/api/get-attachments) * [Get attachments](/api/get-attachments)
* [Delete an attachment](/api/remove-attachment) * [Delete an attachment](/api/remove-attachment)

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 261 API_FEATURE_LEVEL = 262
# 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

View File

@ -113,3 +113,22 @@ def update_user_status(
user_profile_id=user_profile_id, user_profile_id=user_profile_id,
defaults=defaults, defaults=defaults,
) )
def get_user_status(user_profile: UserProfile) -> UserInfoDict:
status_set_by_user = (
UserStatus.objects.filter(user_profile=user_profile)
.values(
"user_profile_id",
"user_profile__presence_enabled",
"status_text",
"emoji_name",
"emoji_code",
"reaction_type",
)
.first()
)
if not status_set_by_user:
return {}
return format_user_status(status_set_by_user)

View File

@ -195,6 +195,20 @@ def get_user_presence(client: Client) -> None:
validate_against_openapi_schema(result, "/users/{user_id_or_email}/presence", "get", "200") validate_against_openapi_schema(result, "/users/{user_id_or_email}/presence", "get", "200")
@openapi_test_function("/users/{user_id}/status:get")
def get_user_status(client: Client) -> None:
# {code_example|start}
# Get the status currently set by a user.
user_id = 11
result = client.call_endpoint(
url=f"/users/{user_id}/status",
method="GET",
)
# {code_example|end}
validate_against_openapi_schema(result, "/users/{user_id}/status", "get", "200")
@openapi_test_function("/users/me/presence:post") @openapi_test_function("/users/me/presence:post")
def update_presence(client: Client) -> None: def update_presence(client: Client) -> None:
request = { request = {
@ -1701,6 +1715,7 @@ def test_users(client: Client, owner_client: Client) -> None:
reactivate_user(client) reactivate_user(client)
update_user(client) update_user(client)
update_status(client) update_status(client)
get_user_status(client)
get_user_by_email(client) get_user_by_email(client)
get_subscription_status(client) get_subscription_status(client)
get_profile(client) get_profile(client)

View File

@ -8416,6 +8416,68 @@ paths:
"200": "200":
$ref: "#/components/responses/SimpleSuccess" $ref: "#/components/responses/SimpleSuccess"
/users/{user_id}/status:
get:
operationId: get-user-status
summary: Get a user's status
tags: ["users"]
description: |
Get the [status](/help/status-and-availability) currently set by a
user in the organization.
**Changes**: New in Zulip 9.0 (feature level 262). Previously,
user statuses could only be fetched via the [`POST
/register`](/api/register-queue) endpoint.
parameters:
- $ref: "#/components/parameters/UserId"
responses:
"200":
description: Success.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/JsonSuccessBase"
- additionalProperties: false
properties:
result: {}
msg: {}
ignored_parameters_unsupported: {}
status:
allOf:
- description: |
The status set by the user. Note that, if the user doesn't have a status
currently set, then the returned dictionary will be empty as none of the
keys listed below will be present.
- $ref: "#/components/schemas/UserStatus"
example:
{
"result": "success",
"msg": "",
"status":
{
"status_text": "on vacation",
"emoji_name": "car",
"emoji_code": "1f697",
"reaction_type": "unicode_emoji",
},
}
"400":
description: Success.
content:
application/json:
schema:
allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"result": "error",
"msg": "No such user",
"code": "BAD_REQUEST",
}
description: |
An example JSON error response when the user does not exist:
/users/{user_id_or_email}/presence: /users/{user_id_or_email}/presence:
get: get:
operationId: get-user-presence operationId: get-user-presence
@ -13652,63 +13714,11 @@ paths:
**Changes**: The emoji parameters are new in Zulip 5.0 (feature level 86). **Changes**: The emoji parameters are new in Zulip 5.0 (feature level 86).
Previously, Zulip did not support emoji associated with statuses. Previously, Zulip did not support emoji associated with statuses.
additionalProperties: additionalProperties:
description: | allOf:
`{user_id}`: Object containing the status details of a user - description: |
with the key of the object being the ID of the user. `{user_id}`: Object containing the status details of a user
type: object with the key of the object being the ID of the user.
additionalProperties: false - $ref: "#/components/schemas/UserStatus"
properties:
away:
type: boolean
deprecated: true
description: |
If present, the user has marked themself "away".
**Changes**: Deprecated in Zulip 6.0 (feature level 148);
starting with that feature level, `away` is a legacy way to
access the user's `presence_enabled` setting, with
`away = !presence_enabled`. To be removed in a future release.
status_text:
type: string
description: |
If present, the text content of the user's status message.
emoji_name:
type: string
description: |
If present, the name for the emoji to associate with the user's status.
**Changes**: New in Zulip 5.0 (feature level 86).
emoji_code:
type: string
description: |
If present, a unique identifier, defining the specific emoji codepoint
requested, within the namespace of the `reaction_type`.
**Changes**: New in Zulip 5.0 (feature level 86).
reaction_type:
type: string
enum:
- unicode_emoji
- realm_emoji
- zulip_extra_emoji
description: |
If present, a string indicating the type of emoji. Each emoji
`reaction_type` has an independent namespace for values of `emoji_code`.
Must be one of the following values:
- `unicode_emoji` : In this namespace, `emoji_code` will be a
dash-separated hex encoding of the sequence of Unicode codepoints
that define this emoji in the Unicode specification.
- `realm_emoji` : In this namespace, `emoji_code` will be the ID of
the uploaded [custom emoji](/help/custom-emoji).
- `zulip_extra_emoji` : These are special emoji included with Zulip.
In this namespace, `emoji_code` will be the name of the emoji (e.g.
"zulip").
**Changes**: New in Zulip 5.0 (feature level 86).
user_settings: user_settings:
type: object type: object
description: | description: |
@ -21100,6 +21110,61 @@ components:
**Changes**: Starting with Zulip 7.0 (feature level 178), always **Changes**: Starting with Zulip 7.0 (feature level 178), always
`false` when present as the server no longer stores which client `false` when present as the server no longer stores which client
submitted presence data. submitted presence data.
UserStatus:
type: object
additionalProperties: false
properties:
away:
type: boolean
deprecated: true
description: |
If present, the user has marked themself "away".
**Changes**: Deprecated in Zulip 6.0 (feature level 148);
starting with that feature level, `away` is a legacy way to
access the user's `presence_enabled` setting, with
`away = !presence_enabled`. To be removed in a future release.
status_text:
type: string
description: |
If present, the text content of the user's status message.
emoji_name:
type: string
description: |
If present, the name for the emoji to associate with the user's status.
**Changes**: New in Zulip 5.0 (feature level 86).
emoji_code:
type: string
description: |
If present, a unique identifier, defining the specific emoji codepoint
requested, within the namespace of the `reaction_type`.
**Changes**: New in Zulip 5.0 (feature level 86).
reaction_type:
type: string
enum:
- unicode_emoji
- realm_emoji
- zulip_extra_emoji
description: |
If present, a string indicating the type of emoji. Each emoji
`reaction_type` has an independent namespace for values of `emoji_code`.
Must be one of the following values:
- `unicode_emoji` : In this namespace, `emoji_code` will be a
dash-separated hex encoding of the sequence of Unicode codepoints
that define this emoji in the Unicode specification.
- `realm_emoji` : In this namespace, `emoji_code` will be the ID of
the uploaded [custom emoji](/help/custom-emoji).
- `zulip_extra_emoji` : These are special emoji included with Zulip.
In this namespace, `emoji_code` will be the name of the emoji (e.g.
"zulip").
**Changes**: New in Zulip 5.0 (feature level 86).
Draft: Draft:
type: object type: object
description: | description: |

View File

@ -3,7 +3,12 @@ from typing import Any, Dict, Optional
import orjson import orjson
from zerver.lib.test_classes import ZulipTestCase from zerver.lib.test_classes import ZulipTestCase
from zerver.lib.user_status import UserInfoDict, get_all_users_status_dict, update_user_status from zerver.lib.user_status import (
UserInfoDict,
get_all_users_status_dict,
get_user_status,
update_user_status,
)
from zerver.models import UserProfile, UserStatus from zerver.models import UserProfile, UserStatus
from zerver.models.clients import get_client from zerver.models.clients import get_client
@ -65,6 +70,17 @@ class UserStatusTest(ZulipTestCase):
), ),
) )
fetched_status = get_user_status(hamlet)
self.assertEqual(
fetched_status,
dict(
status_text="out to lunch",
emoji_name="car",
emoji_code="1f697",
reaction_type=UserStatus.UNICODE_EMOJI,
),
)
rec_count = UserStatus.objects.filter(user_profile_id=hamlet.id).count() rec_count = UserStatus.objects.filter(user_profile_id=hamlet.id).count()
self.assertEqual(rec_count, 1) self.assertEqual(rec_count, 1)
@ -88,6 +104,17 @@ class UserStatusTest(ZulipTestCase):
), ),
) )
fetched_status = get_user_status(hamlet)
self.assertEqual(
fetched_status,
dict(
status_text="out to lunch",
emoji_name="car",
emoji_code="1f697",
reaction_type=UserStatus.UNICODE_EMOJI,
),
)
# Clear the status_text and emoji_info now. # Clear the status_text and emoji_info now.
update_user_status( update_user_status(
user_profile_id=hamlet.id, user_profile_id=hamlet.id,
@ -103,6 +130,12 @@ class UserStatusTest(ZulipTestCase):
{}, {},
) )
fetched_status = get_user_status(hamlet)
self.assertEqual(
fetched_status,
{},
)
# Set Hamlet to in a meeting. # Set Hamlet to in a meeting.
update_user_status( update_user_status(
user_profile_id=hamlet.id, user_profile_id=hamlet.id,
@ -118,6 +151,12 @@ class UserStatusTest(ZulipTestCase):
dict(status_text="in a meeting"), dict(status_text="in a meeting"),
) )
fetched_status = get_user_status(hamlet)
self.assertEqual(
fetched_status,
dict(status_text="in a meeting"),
)
# Test user status for inaccessible users. # Test user status for inaccessible users.
self.set_up_db_for_testing_user_access() self.set_up_db_for_testing_user_access()
cordelia = self.example_user("cordelia") cordelia = self.example_user("cordelia")
@ -204,6 +243,13 @@ class UserStatusTest(ZulipTestCase):
dict(away=True, status_text="on vacation"), dict(away=True, status_text="on vacation"),
) )
result = self.client_get(f"/json/users/{hamlet.id}/status")
result_dict = self.assert_json_success(result)
self.assertEqual(
result_dict["status"],
dict(away=True, status_text="on vacation"),
)
# Setting away is a deprecated way of accessing a user's presence_enabled # Setting away is a deprecated way of accessing a user's presence_enabled
# setting. Can be removed when clients migrate "away" (also referred to as # setting. Can be removed when clients migrate "away" (also referred to as
# "unavailable") feature to directly use the presence_enabled setting. # "unavailable") feature to directly use the presence_enabled setting.
@ -234,6 +280,19 @@ class UserStatusTest(ZulipTestCase):
), ),
) )
result = self.client_get(f"/json/users/{hamlet.id}/status")
result_dict = self.assert_json_success(result)
self.assertEqual(
result_dict["status"],
dict(
away=True,
status_text="on vacation",
emoji_name="car",
emoji_code="1f697",
reaction_type=UserStatus.UNICODE_EMOJI,
),
)
# Server should remove emoji_code and reaction_type if emoji_name is empty. # Server should remove emoji_code and reaction_type if emoji_name is empty.
self.update_status_and_assert_event( self.update_status_and_assert_event(
payload=dict( payload=dict(
@ -252,6 +311,13 @@ class UserStatusTest(ZulipTestCase):
dict(away=True, status_text="on vacation"), dict(away=True, status_text="on vacation"),
) )
result = self.client_get(f"/json/users/{hamlet.id}/status")
result_dict = self.assert_json_success(result)
self.assertEqual(
result_dict["status"],
dict(away=True, status_text="on vacation"),
)
# Now revoke "away" status. # Now revoke "away" status.
self.update_status_and_assert_event( self.update_status_and_assert_event(
payload=dict(away=orjson.dumps(False).decode()), payload=dict(away=orjson.dumps(False).decode()),
@ -280,6 +346,13 @@ class UserStatusTest(ZulipTestCase):
dict(status_text="in office"), dict(status_text="in office"),
) )
result = self.client_get(f"/json/users/{hamlet.id}/status")
result_dict = self.assert_json_success(result)
self.assertEqual(
result_dict["status"],
dict(status_text="in office"),
)
# And finally clear your info. # And finally clear your info.
self.update_status_and_assert_event( self.update_status_and_assert_event(
payload=dict(status_text=""), payload=dict(status_text=""),
@ -290,6 +363,13 @@ class UserStatusTest(ZulipTestCase):
{}, {},
) )
result = self.client_get(f"/json/users/{hamlet.id}/status")
result_dict = self.assert_json_success(result)
self.assertEqual(
result_dict["status"],
{},
)
# Turn on "away" status again. # Turn on "away" status again.
self.update_status_and_assert_event( self.update_status_and_assert_event(
payload=dict(away=orjson.dumps(True).decode()), payload=dict(away=orjson.dumps(True).decode()),
@ -312,3 +392,23 @@ class UserStatusTest(ZulipTestCase):
user_status_info(hamlet), user_status_info(hamlet),
dict(status_text="at the beach", away=True), dict(status_text="at the beach", away=True),
) )
result = self.client_get(f"/json/users/{hamlet.id}/status")
result_dict = self.assert_json_success(result)
self.assertEqual(
result_dict["status"],
dict(status_text="at the beach", away=True),
)
# Invalid user ID should fail
result = self.client_get("/json/users/12345/status")
self.assert_json_error(result, "No such user")
# Test status if the status has not been set
iago = self.example_user("iago")
result = self.client_get(f"/json/users/{iago.id}/status")
result_dict = self.assert_json_success(result)
self.assertEqual(
result_dict["status"],
{},
)

View File

@ -18,7 +18,8 @@ from zerver.lib.request import RequestNotes
from zerver.lib.response import json_success from zerver.lib.response import json_success
from zerver.lib.timestamp import datetime_to_timestamp from zerver.lib.timestamp import datetime_to_timestamp
from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint from zerver.lib.typed_endpoint import ApiParamConfig, typed_endpoint
from zerver.lib.users import check_can_access_user 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 import UserActivity, UserPresence, UserProfile, UserStatus
from zerver.models.users import get_active_user, get_active_user_profile_by_id_in_realm from zerver.models.users import get_active_user, get_active_user_profile_by_id_in_realm
@ -66,6 +67,13 @@ def get_presence_backend(
return json_success(request, data=result) 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 @human_users_only
@typed_endpoint @typed_endpoint
def update_user_status_backend( def update_user_status_backend(

View File

@ -89,6 +89,7 @@ from zerver.views.muted_users import mute_user, unmute_user
from zerver.views.onboarding_steps import mark_onboarding_step_as_read from zerver.views.onboarding_steps import mark_onboarding_step_as_read
from zerver.views.presence import ( from zerver.views.presence import (
get_presence_backend, get_presence_backend,
get_status_backend,
get_statuses_for_realm, get_statuses_for_realm,
update_active_status_backend, update_active_status_backend,
update_user_status_backend, update_user_status_backend,
@ -396,6 +397,7 @@ v1_api_and_json_patterns = [
rest_path("users/<user_id_or_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),
rest_path("users/<int:user_id>/status", GET=get_status_backend),
# user_groups -> zerver.views.user_groups # user_groups -> zerver.views.user_groups
rest_path("user_groups", GET=get_user_group), rest_path("user_groups", GET=get_user_group),
rest_path("user_groups/create", POST=add_user_group), rest_path("user_groups/create", POST=add_user_group),