mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 20:13:46 +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