mirror of
https://github.com/zulip/zulip.git
synced 2025-10-22 20:42:14 +00:00
api: Add administrator endpoint for updating user status.
Fixes #33139.
This commit is contained in:
@@ -90,6 +90,7 @@
|
||||
* [Reactivate a user](/api/reactivate-user)
|
||||
* [Get a user's status](/api/get-user-status)
|
||||
* [Update your status](/api/update-status)
|
||||
* [Update user status](/api/update-status-for-user)
|
||||
* [Set "typing" status](/api/set-typing-status)
|
||||
* [Set "typing" status for message editing](/api/set-typing-status-for-message-edit)
|
||||
* [Get a user's presence](/api/get-user-presence)
|
||||
|
@@ -10476,6 +10476,152 @@ paths:
|
||||
$ref: "#/components/responses/SimpleSuccess"
|
||||
|
||||
/users/{user_id}/status:
|
||||
post:
|
||||
operationId: update-status-for-user
|
||||
summary: Update user status
|
||||
tags: ["users"]
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/UserId"
|
||||
description: |
|
||||
Administrator endpoint for changing the [status](/help/status-and-availability) of
|
||||
another user.
|
||||
x-requires-administrator: true
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
status_text:
|
||||
type: string
|
||||
description: |
|
||||
The text content of the status message. Sending the empty string
|
||||
will clear the user's status.
|
||||
|
||||
**Note**: The limit on the size of the message is 60 characters.
|
||||
example: on vacation
|
||||
emoji_name:
|
||||
type: string
|
||||
description: |
|
||||
The name for the emoji to associate with this status.
|
||||
|
||||
**Changes**: New in Zulip 5.0 (feature level 86).
|
||||
example: car
|
||||
emoji_code:
|
||||
type: string
|
||||
description: |
|
||||
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).
|
||||
example: 1f697
|
||||
reaction_type:
|
||||
type: string
|
||||
description: |
|
||||
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).
|
||||
example: unicode_emoji
|
||||
encoding:
|
||||
away:
|
||||
contentType: application/json
|
||||
responses:
|
||||
"200":
|
||||
$ref: "#/components/responses/SimpleSuccess"
|
||||
"400":
|
||||
description: Bad request.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
oneOf:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "Insufficient permission",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
An example JSON error response user making request does not have permission to update other user's status.:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "Client did not pass any new values.",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
An example JSON error response when no changes were requested:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "status_text is too long (limit: 60 characters)",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
An example JSON error response when the
|
||||
`status_text` message exceeds the limit of
|
||||
60 characters:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "Client must pass emoji_name if they pass either emoji_code or reaction_type.",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
An example JSON error response when `emoji_name` is not specified
|
||||
but `emoji_code` or `reaction_type` is specified:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "Emoji 'invalid' does not exist",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
An example JSON error response when the emoji name does not exist:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "Invalid emoji name.",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
An example JSON error response when the emoji name is invalid:
|
||||
- allOf:
|
||||
- $ref: "#/components/schemas/CodedError"
|
||||
- example:
|
||||
{
|
||||
"result": "error",
|
||||
"msg": "Invalid custom emoji.",
|
||||
"code": "BAD_REQUEST",
|
||||
}
|
||||
description: |
|
||||
An example JSON error response when the custom emoji is invalid:
|
||||
get:
|
||||
operationId: get-user-status
|
||||
summary: Get a user's status
|
||||
|
@@ -181,10 +181,15 @@ class UserStatusTest(ZulipTestCase):
|
||||
)
|
||||
|
||||
def update_status_and_assert_event(
|
||||
self, payload: dict[str, Any], expected_event: dict[str, Any], num_events: int = 1
|
||||
self,
|
||||
*,
|
||||
payload: dict[str, Any],
|
||||
expected_event: dict[str, Any],
|
||||
url: str = "/json/users/me/status",
|
||||
num_events: int = 1,
|
||||
) -> None:
|
||||
with self.capture_send_event_calls(expected_num_events=num_events) as events:
|
||||
result = self.client_post("/json/users/me/status", payload)
|
||||
result = self.client_post(url, payload)
|
||||
self.assert_json_success(result)
|
||||
if num_events == 1:
|
||||
self.assertEqual(events[0]["event"], expected_event)
|
||||
@@ -193,6 +198,7 @@ class UserStatusTest(ZulipTestCase):
|
||||
|
||||
def test_endpoints(self) -> None:
|
||||
hamlet = self.example_user("hamlet")
|
||||
iago = self.example_user("iago")
|
||||
realm = hamlet.realm
|
||||
now = timezone_now()
|
||||
|
||||
@@ -260,7 +266,6 @@ class UserStatusTest(ZulipTestCase):
|
||||
user_status_info(hamlet),
|
||||
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(
|
||||
@@ -430,3 +435,144 @@ class UserStatusTest(ZulipTestCase):
|
||||
result_dict["status"],
|
||||
{},
|
||||
)
|
||||
|
||||
# No such user
|
||||
result = self.client_post("/json/users/12345/status")
|
||||
self.assert_json_error(result, "No such user")
|
||||
payload = {
|
||||
"status_text": "In a meeting",
|
||||
"emoji_code": "1f4bb",
|
||||
"emoji_name": "car",
|
||||
"reaction_type": "realm_emoji",
|
||||
}
|
||||
# User does not have permission to set status for other users
|
||||
self.login_user(hamlet)
|
||||
|
||||
result = self.client_post(f"/json/users/{iago.id}/status", payload)
|
||||
self.assert_json_error(result, "Insufficient permission")
|
||||
|
||||
update_status_url = f"/json/users/{hamlet.id}/status"
|
||||
|
||||
# Login as admin Iago
|
||||
self.login_user(iago)
|
||||
|
||||
# Server should remove emoji_code and reaction_type if emoji_name is empty.
|
||||
self.update_status_and_assert_event(
|
||||
payload=dict(
|
||||
emoji_name="",
|
||||
),
|
||||
url=update_status_url,
|
||||
expected_event=dict(
|
||||
type="user_status",
|
||||
user_id=hamlet.id,
|
||||
emoji_name="",
|
||||
emoji_code="",
|
||||
reaction_type=UserStatus.UNICODE_EMOJI,
|
||||
),
|
||||
)
|
||||
|
||||
self.update_status_and_assert_event(
|
||||
payload=dict(status_text=" at the beach "),
|
||||
url=update_status_url,
|
||||
expected_event=dict(type="user_status", user_id=hamlet.id, status_text="at the beach"),
|
||||
)
|
||||
self.assertEqual(
|
||||
user_status_info(hamlet),
|
||||
dict(status_text="at the beach", away=True),
|
||||
)
|
||||
|
||||
result = self.client_post(update_status_url, {})
|
||||
self.assert_json_error(result, "Client did not pass any new values.")
|
||||
|
||||
# Try to omit emoji_name parameter but passing emoji_code --this should be an error.
|
||||
result = self.client_post(
|
||||
update_status_url, {"status_text": "In a meeting", "emoji_code": "1f4bb"}
|
||||
)
|
||||
self.assert_json_error(
|
||||
result, "Client must pass emoji_name if they pass either emoji_code or reaction_type."
|
||||
)
|
||||
|
||||
# Invalid emoji requests fail.
|
||||
result = self.client_post(
|
||||
update_status_url,
|
||||
{"status_text": "In a meeting", "emoji_code": "1f4bb", "emoji_name": "invalid"},
|
||||
)
|
||||
self.assert_json_error(result, "Emoji 'invalid' does not exist")
|
||||
|
||||
result = self.client_post(
|
||||
update_status_url,
|
||||
{"status_text": "In a meeting", "emoji_code": "1f4bb", "emoji_name": "car"},
|
||||
)
|
||||
self.assert_json_error(result, "Invalid emoji name.")
|
||||
|
||||
result = self.client_post(
|
||||
update_status_url,
|
||||
{
|
||||
"status_text": "In a meeting",
|
||||
"emoji_code": "1f4bb",
|
||||
"emoji_name": "car",
|
||||
"reaction_type": "realm_emoji",
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, "Invalid custom emoji.")
|
||||
|
||||
# Try a long message--this should be an error.
|
||||
long_text = "x" * 61
|
||||
|
||||
result = self.client_post(update_status_url, dict(status_text=long_text))
|
||||
self.assert_json_error(result, "status_text is too long (limit: 60 characters)")
|
||||
|
||||
# Set "away" with a normal length message.
|
||||
self.update_status_and_assert_event(
|
||||
payload=dict(
|
||||
away=orjson.dumps(True).decode(),
|
||||
status_text="on vacation",
|
||||
),
|
||||
url=update_status_url,
|
||||
expected_event=dict(
|
||||
type="user_status", user_id=hamlet.id, away=True, status_text="on vacation"
|
||||
),
|
||||
num_events=3,
|
||||
)
|
||||
self.assertEqual(
|
||||
user_status_info(hamlet),
|
||||
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. Can be removed when clients migrate "away" (also referred to as
|
||||
# "unavailable") feature to directly use the presence_enabled setting.
|
||||
user = UserProfile.objects.get(id=hamlet.id)
|
||||
self.assertEqual(user.presence_enabled, False)
|
||||
|
||||
# Server should fill emoji_code and reaction_type by emoji_name.
|
||||
self.update_status_and_assert_event(
|
||||
payload=dict(
|
||||
emoji_name="car",
|
||||
),
|
||||
url=update_status_url,
|
||||
expected_event=dict(
|
||||
type="user_status",
|
||||
user_id=hamlet.id,
|
||||
emoji_name="car",
|
||||
emoji_code="1f697",
|
||||
reaction_type=UserStatus.UNICODE_EMOJI,
|
||||
),
|
||||
)
|
||||
self.assertEqual(
|
||||
user_status_info(hamlet),
|
||||
dict(
|
||||
away=True,
|
||||
status_text="on vacation",
|
||||
emoji_name="car",
|
||||
emoji_code="1f697",
|
||||
reaction_type=UserStatus.UNICODE_EMOJI,
|
||||
),
|
||||
)
|
||||
|
@@ -16,7 +16,7 @@ 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.typed_endpoint import ApiParamConfig, PathOnly, 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
|
||||
@@ -147,6 +147,31 @@ def update_user_status_backend(
|
||||
return json_success(request)
|
||||
|
||||
|
||||
@human_users_only
|
||||
@typed_endpoint
|
||||
def update_user_status_admin(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
user_id: PathOnly[Json[int]],
|
||||
status_text: Annotated[
|
||||
str | None, StringConstraints(strip_whitespace=True, max_length=60)
|
||||
] = None,
|
||||
emoji_name: str | None = None,
|
||||
emoji_code: str | None = None,
|
||||
emoji_type: Annotated[str | None, ApiParamConfig("reaction_type")] = None,
|
||||
) -> HttpResponse:
|
||||
target_user = access_user_by_id(user_profile, user_id, for_admin=True)
|
||||
return update_user_status_backend(
|
||||
request,
|
||||
user_profile=target_user,
|
||||
status_text=status_text,
|
||||
emoji_name=emoji_name,
|
||||
emoji_code=emoji_code,
|
||||
emoji_type=emoji_type,
|
||||
)
|
||||
|
||||
|
||||
@human_users_only
|
||||
@typed_endpoint
|
||||
def update_active_status_backend(
|
||||
|
@@ -104,6 +104,7 @@ from zerver.views.presence import (
|
||||
get_status_backend,
|
||||
get_statuses_for_realm,
|
||||
update_active_status_backend,
|
||||
update_user_status_admin,
|
||||
update_user_status_backend,
|
||||
)
|
||||
from zerver.views.push_notifications import (
|
||||
@@ -467,7 +468,7 @@ v1_api_and_json_patterns = [
|
||||
rest_path("users/<user_id_or_email>/presence", GET=get_presence_backend),
|
||||
rest_path("realm/presence", GET=get_statuses_for_realm),
|
||||
rest_path("users/me/status", POST=update_user_status_backend),
|
||||
rest_path("users/<int:user_id>/status", GET=get_status_backend),
|
||||
rest_path("users/<int:user_id>/status", POST=update_user_status_admin, GET=get_status_backend),
|
||||
# user_groups -> zerver.views.user_groups
|
||||
rest_path("user_groups", GET=get_user_groups),
|
||||
rest_path("user_groups/create", POST=add_user_group),
|
||||
|
Reference in New Issue
Block a user