mirror of
				https://github.com/zulip/zulip.git
				synced 2025-10-31 20:13:46 +00:00 
			
		
		
		
	user_status: Add backend changes to support status emoji.
In this commit: * We update the `UserStatus` model to accept `AbstractReaction` as a base class so, we can get all the fields related to store status emoji. * We update the user status endpoint (`users/me/status`) to accept status emoji fields. * We update the user status event to add status emoji fields. Co-authored-by: Yash Rathore <33805964+YashRE42@users.noreply.github.com>
This commit is contained in:
		| @@ -11,6 +11,15 @@ below features are supported. | |||||||
|  |  | ||||||
| ## Changes in Zulip 5.0 | ## Changes in Zulip 5.0 | ||||||
|  |  | ||||||
|  | **Feature level 86** | ||||||
|  |  | ||||||
|  | * [`GET /events`](/api/get-events): Added `emoji_name`, | ||||||
|  |   `emoji_code`, and `reaction_type` fields to `user_status` objects. | ||||||
|  | * [`POST /register`](/api/register-queue): Added `emoji_name`, | ||||||
|  |   `emoji_code`, and `reaction_type` fields to `user_status` objects. | ||||||
|  | * `POST /users/me/status`: Added support for new `emoji_name`, | ||||||
|  |   `emoji_code`, and `reaction_type` parameters. | ||||||
|  |  | ||||||
| **Feature level 85** | **Feature level 85** | ||||||
|  |  | ||||||
| * [`POST /register`](/api/register-queue), `PATCH /realm`: Replaced `add_emoji_by_admins_only` | * [`POST /register`](/api/register-queue), `PATCH /realm`: Replaced `add_emoji_by_admins_only` | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3" | |||||||
| # 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, as well as | # new level means in templates/zerver/api/changelog.md, as well as | ||||||
| # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. | # "**Changes**" entries in the endpoint's documentation in `zulip.yaml`. | ||||||
| API_FEATURE_LEVEL = 85 | API_FEATURE_LEVEL = 86 | ||||||
|  |  | ||||||
| # 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 | ||||||
|   | |||||||
| @@ -5401,7 +5401,13 @@ def update_user_presence( | |||||||
|  |  | ||||||
|  |  | ||||||
| def do_update_user_status( | def do_update_user_status( | ||||||
|     user_profile: UserProfile, away: Optional[bool], status_text: Optional[str], client_id: int |     user_profile: UserProfile, | ||||||
|  |     away: Optional[bool], | ||||||
|  |     status_text: Optional[str], | ||||||
|  |     client_id: int, | ||||||
|  |     emoji_name: Optional[str], | ||||||
|  |     emoji_code: Optional[str], | ||||||
|  |     reaction_type: Optional[str], | ||||||
| ) -> None: | ) -> None: | ||||||
|     if away is None: |     if away is None: | ||||||
|         status = None |         status = None | ||||||
| @@ -5417,6 +5423,9 @@ def do_update_user_status( | |||||||
|         status=status, |         status=status, | ||||||
|         status_text=status_text, |         status_text=status_text, | ||||||
|         client_id=client_id, |         client_id=client_id, | ||||||
|  |         emoji_name=emoji_name, | ||||||
|  |         emoji_code=emoji_code, | ||||||
|  |         reaction_type=reaction_type, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     event = dict( |     event = dict( | ||||||
| @@ -5430,6 +5439,10 @@ def do_update_user_status( | |||||||
|     if status_text is not None: |     if status_text is not None: | ||||||
|         event["status_text"] = status_text |         event["status_text"] = status_text | ||||||
|  |  | ||||||
|  |     if emoji_name is not None: | ||||||
|  |         event["emoji_name"] = emoji_name | ||||||
|  |         event["emoji_code"] = emoji_code | ||||||
|  |         event["reaction_type"] = reaction_type | ||||||
|     send_event(realm, event, active_user_ids(realm.id)) |     send_event(realm, event, active_user_ids(realm.id)) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1653,6 +1653,9 @@ user_status_event = event_dict_type( | |||||||
|         # force vertical |         # force vertical | ||||||
|         ("away", bool), |         ("away", bool), | ||||||
|         ("status_text", str), |         ("status_text", str), | ||||||
|  |         ("emoji_name", str), | ||||||
|  |         ("emoji_code", str), | ||||||
|  |         ("reaction_type", str), | ||||||
|     ], |     ], | ||||||
| ) | ) | ||||||
| _check_user_status = make_checker(user_status_event) | _check_user_status = make_checker(user_status_event) | ||||||
|   | |||||||
| @@ -59,6 +59,7 @@ from zerver.models import ( | |||||||
|     Stream, |     Stream, | ||||||
|     UserMessage, |     UserMessage, | ||||||
|     UserProfile, |     UserProfile, | ||||||
|  |     UserStatus, | ||||||
|     custom_profile_fields_for_realm, |     custom_profile_fields_for_realm, | ||||||
|     get_default_stream_groups, |     get_default_stream_groups, | ||||||
|     get_realm_domains, |     get_realm_domains, | ||||||
| @@ -1092,6 +1093,9 @@ def apply_event( | |||||||
|         user_status = state["user_status"] |         user_status = state["user_status"] | ||||||
|         away = event.get("away") |         away = event.get("away") | ||||||
|         status_text = event.get("status_text") |         status_text = event.get("status_text") | ||||||
|  |         emoji_name = event.get("emoji_name") | ||||||
|  |         emoji_code = event.get("emoji_code") | ||||||
|  |         reaction_type = event.get("reaction_type") | ||||||
|  |  | ||||||
|         if user_id_str not in user_status: |         if user_id_str not in user_status: | ||||||
|             user_status[user_id_str] = {} |             user_status[user_id_str] = {} | ||||||
| @@ -1108,6 +1112,24 @@ def apply_event( | |||||||
|             else: |             else: | ||||||
|                 user_status[user_id_str]["status_text"] = status_text |                 user_status[user_id_str]["status_text"] = status_text | ||||||
|  |  | ||||||
|  |             if emoji_name is not None: | ||||||
|  |                 if emoji_name == "": | ||||||
|  |                     user_status[user_id_str].pop("emoji_name", None) | ||||||
|  |                 else: | ||||||
|  |                     user_status[user_id_str]["emoji_name"] = emoji_name | ||||||
|  |  | ||||||
|  |                 if emoji_code is not None: | ||||||
|  |                     if emoji_code == "": | ||||||
|  |                         user_status[user_id_str].pop("emoji_code", None) | ||||||
|  |                     else: | ||||||
|  |                         user_status[user_id_str]["emoji_code"] = emoji_code | ||||||
|  |  | ||||||
|  |                 if reaction_type is not None: | ||||||
|  |                     if reaction_type == UserStatus.UNICODE_EMOJI and emoji_name == "": | ||||||
|  |                         user_status[user_id_str].pop("reaction_type", None) | ||||||
|  |                     else: | ||||||
|  |                         user_status[user_id_str]["reaction_type"] = reaction_type | ||||||
|  |  | ||||||
|         if not user_status[user_id_str]: |         if not user_status[user_id_str]: | ||||||
|             user_status.pop(user_id_str, None) |             user_status.pop(user_id_str, None) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -13,12 +13,19 @@ def get_user_info_dict(realm_id: int) -> Dict[str, Dict[str, Any]]: | |||||||
|             user_profile__is_active=True, |             user_profile__is_active=True, | ||||||
|         ) |         ) | ||||||
|         .exclude( |         .exclude( | ||||||
|             Q(status=UserStatus.NORMAL) & Q(status_text=""), |             Q(status=UserStatus.NORMAL) | ||||||
|  |             & Q(status_text="") | ||||||
|  |             & Q(emoji_name="") | ||||||
|  |             & Q(emoji_code="") | ||||||
|  |             & Q(reaction_type=UserStatus.UNICODE_EMOJI), | ||||||
|         ) |         ) | ||||||
|         .values( |         .values( | ||||||
|             "user_profile_id", |             "user_profile_id", | ||||||
|             "status", |             "status", | ||||||
|             "status_text", |             "status_text", | ||||||
|  |             "emoji_name", | ||||||
|  |             "emoji_code", | ||||||
|  |             "reaction_type", | ||||||
|         ) |         ) | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| @@ -27,12 +34,19 @@ def get_user_info_dict(realm_id: int) -> Dict[str, Dict[str, Any]]: | |||||||
|         away = row["status"] == UserStatus.AWAY |         away = row["status"] == UserStatus.AWAY | ||||||
|         status_text = row["status_text"] |         status_text = row["status_text"] | ||||||
|         user_id = row["user_profile_id"] |         user_id = row["user_profile_id"] | ||||||
|  |         emoji_name = row["emoji_name"] | ||||||
|  |         emoji_code = row["emoji_code"] | ||||||
|  |         reaction_type = row["reaction_type"] | ||||||
|  |  | ||||||
|         dct = {} |         dct = {} | ||||||
|         if away: |         if away: | ||||||
|             dct["away"] = away |             dct["away"] = away | ||||||
|         if status_text: |         if status_text: | ||||||
|             dct["status_text"] = status_text |             dct["status_text"] = status_text | ||||||
|  |         if emoji_name: | ||||||
|  |             dct["emoji_name"] = emoji_name | ||||||
|  |             dct["emoji_code"] = emoji_code | ||||||
|  |             dct["reaction_type"] = reaction_type | ||||||
|  |  | ||||||
|         user_dict[str(user_id)] = dct |         user_dict[str(user_id)] = dct | ||||||
|  |  | ||||||
| @@ -40,7 +54,13 @@ def get_user_info_dict(realm_id: int) -> Dict[str, Dict[str, Any]]: | |||||||
|  |  | ||||||
|  |  | ||||||
| def update_user_status( | def update_user_status( | ||||||
|     user_profile_id: int, status: Optional[int], status_text: Optional[str], client_id: int |     user_profile_id: int, | ||||||
|  |     status: Optional[int], | ||||||
|  |     status_text: Optional[str], | ||||||
|  |     client_id: int, | ||||||
|  |     emoji_name: Optional[str], | ||||||
|  |     emoji_code: Optional[str], | ||||||
|  |     reaction_type: Optional[str], | ||||||
| ) -> None: | ) -> None: | ||||||
|  |  | ||||||
|     timestamp = timezone_now() |     timestamp = timezone_now() | ||||||
| @@ -56,6 +76,15 @@ def update_user_status( | |||||||
|     if status_text is not None: |     if status_text is not None: | ||||||
|         defaults["status_text"] = status_text |         defaults["status_text"] = status_text | ||||||
|  |  | ||||||
|  |     if emoji_name is not None: | ||||||
|  |         defaults["emoji_name"] = emoji_name | ||||||
|  |  | ||||||
|  |         if emoji_code is not None: | ||||||
|  |             defaults["emoji_code"] = emoji_code | ||||||
|  |  | ||||||
|  |         if reaction_type is not None: | ||||||
|  |             defaults["reaction_type"] = reaction_type | ||||||
|  |  | ||||||
|     UserStatus.objects.update_or_create( |     UserStatus.objects.update_or_create( | ||||||
|         user_profile_id=user_profile_id, |         user_profile_id=user_profile_id, | ||||||
|         defaults=defaults, |         defaults=defaults, | ||||||
|   | |||||||
| @@ -1430,6 +1430,24 @@ paths: | |||||||
|                                   type: string |                                   type: string | ||||||
|                                   description: | |                                   description: | | ||||||
|                                     The text content of the status message. |                                     The text content of the status message. | ||||||
|  |                                 emoji_name: | ||||||
|  |                                   type: string | ||||||
|  |                                   description: | | ||||||
|  |                                     The [emoji name](/api/add-reaction#parameters) for the emoji associated with the new status. | ||||||
|  |  | ||||||
|  |                                     **Changes**; New in Zulip 5.0 (feature level 86). | ||||||
|  |                                 emoji_code: | ||||||
|  |                                   type: string | ||||||
|  |                                   description: | | ||||||
|  |                                     The [emoji code](/api/add-reaction#parameters) for the emoji associated with the new status. | ||||||
|  |  | ||||||
|  |                                     **Changes**; New in Zulip 5.0 (feature level 86). | ||||||
|  |                                 reaction_type: | ||||||
|  |                                   type: string | ||||||
|  |                                   description: | | ||||||
|  |                                     The [emoji type](/api/add-reaction#parameters) for the emoji associated with the new status. | ||||||
|  |  | ||||||
|  |                                     **Changes**; New in Zulip 5.0 (feature level 86). | ||||||
|                                 user_id: |                                 user_id: | ||||||
|                                   type: integer |                                   type: integer | ||||||
|                                   description: | |                                   description: | | ||||||
| @@ -1440,6 +1458,9 @@ paths: | |||||||
|                                   "user_id": 10, |                                   "user_id": 10, | ||||||
|                                   "away": true, |                                   "away": true, | ||||||
|                                   "status_text": "out to lunch", |                                   "status_text": "out to lunch", | ||||||
|  |                                   "emoji_name": "car", | ||||||
|  |                                   "emoji_code": "1f697", | ||||||
|  |                                   "reaction_type": "unicode_emoji", | ||||||
|                                   "id": 0, |                                   "id": 0, | ||||||
|                                 } |                                 } | ||||||
|                             - type: object |                             - type: object | ||||||
| @@ -8360,12 +8381,19 @@ paths: | |||||||
|                           error messages when a search returns limited results because |                           error messages when a search returns limited results because | ||||||
|                           a stop word in the query was ignored. |                           a stop word in the query was ignored. | ||||||
|                       user_status: |                       user_status: | ||||||
|                         type: object |                         allOf: | ||||||
|  |                           - $ref: "#/components/schemas/EmojiBase" | ||||||
|                         description: | |                         description: | | ||||||
|                           Present if `user_status` is present in `fetch_event_types`. |                           Present if `user_status` is present in `fetch_event_types`. | ||||||
|  |  | ||||||
|                           A dictionary which contains the [status](/help/status-and-availability) |                           A dictionary which contains the [status](/help/status-and-availability) | ||||||
|                           of all users in the Zulip organization who have set a status. |                           of all users in the Zulip organization who have set a status. | ||||||
|  |  | ||||||
|  |                           **Changes**: The emoji parameters are new in Zulip 5.0 (feature level 86). | ||||||
|  |                           Previously, Zulip did not support emoji associated with statuses. | ||||||
|  |  | ||||||
|  |                           A status that does not have an emoji associated with it is encoded | ||||||
|  |                           with `emoji_name=""`. | ||||||
|                         additionalProperties: |                         additionalProperties: | ||||||
|                           description: | |                           description: | | ||||||
|                             `{user_id}`: Object containing the status details of a user |                             `{user_id}`: Object containing the status details of a user | ||||||
| @@ -12241,7 +12269,7 @@ components: | |||||||
|             reaction_type: {} |             reaction_type: {} | ||||||
|             user_id: {} |             user_id: {} | ||||||
|             user: {} |             user: {} | ||||||
|     EmojiReactionBase: |     EmojiBase: | ||||||
|       type: object |       type: object | ||||||
|       properties: |       properties: | ||||||
|         emoji_code: |         emoji_code: | ||||||
| @@ -12267,42 +12295,47 @@ components: | |||||||
|               (`emoji_code` will be its ID). |               (`emoji_code` will be its ID). | ||||||
|             * `zulip_extra_emoji`: Special emoji included with Zulip.  Exists to |             * `zulip_extra_emoji`: Special emoji included with Zulip.  Exists to | ||||||
|               namespace the `zulip` emoji. |               namespace the `zulip` emoji. | ||||||
|         user_id: |     EmojiReactionBase: | ||||||
|           type: integer |       allOf: | ||||||
|           description: | |         - $ref: "#/components/schemas/EmojiBase" | ||||||
|             The ID of the user who added the reaction. |         - properties: | ||||||
|  |             user_id: | ||||||
|             **Changes**: New in Zulip 3.0 (feature level 2). The `user` |  | ||||||
|             object is deprecated and will be removed in the future. |  | ||||||
|         user: |  | ||||||
|           type: object |  | ||||||
|           additionalProperties: false |  | ||||||
|           deprecated: true |  | ||||||
|           description: | |  | ||||||
|             Dictionary with data on the user who added the reaction, including |  | ||||||
|             the user ID as the `id` field.  **Note**: In the [events |  | ||||||
|             API](/api/get-events), this `user` dictionary |  | ||||||
|             confusing had the user ID in a field called `user_id` |  | ||||||
|             instead.  We recommend ignoring fields other than the user |  | ||||||
|             ID.  **Deprecated** and to be removed in a future release |  | ||||||
|             once core clients have migrated to use the `user_id` field. |  | ||||||
|           properties: |  | ||||||
|             id: |  | ||||||
|               type: integer |               type: integer | ||||||
|               description: | |               description: | | ||||||
|                 ID of the user. |                 The ID of the user who added the reaction. | ||||||
|             email: |  | ||||||
|               type: string |                 **Changes**: New in Zulip 3.0 (feature level 2). The `user` | ||||||
|               description: | |                 object is deprecated and will be removed in the future. | ||||||
|                 Email of the user. |             user: | ||||||
|             full_name: |               type: object | ||||||
|               type: string |               additionalProperties: false | ||||||
|               description: | |               deprecated: true | ||||||
|                 Full name of the user. |  | ||||||
|             is_mirror_dummy: |  | ||||||
|               type: boolean |  | ||||||
|               description: | |               description: | | ||||||
|                 Whether the user is a mirror dummy. |                 Whether the user is a mirror dummy. | ||||||
|  |                 Dictionary with data on the user who added the reaction, including | ||||||
|  |                 the user ID as the `id` field.  **Note**: In the [events | ||||||
|  |                 API](/api/get-events), this `user` dictionary | ||||||
|  |                 confusing had the user ID in a field called `user_id` | ||||||
|  |                 instead.  We recommend ignoring fields other than the user | ||||||
|  |                 ID.  **Deprecated** and to be removed in a future release | ||||||
|  |                 once core clients have migrated to use the `user_id` field. | ||||||
|  |               properties: | ||||||
|  |                 id: | ||||||
|  |                   type: integer | ||||||
|  |                   description: | | ||||||
|  |                     ID of the user. | ||||||
|  |                 email: | ||||||
|  |                   type: string | ||||||
|  |                   description: | | ||||||
|  |                     Email of the user. | ||||||
|  |                 full_name: | ||||||
|  |                   type: string | ||||||
|  |                   description: | | ||||||
|  |                     Full name of the user. | ||||||
|  |                 is_mirror_dummy: | ||||||
|  |                   type: boolean | ||||||
|  |                   description: | | ||||||
|  |                     Whether the user is a mirror dummy. | ||||||
|     Messages: |     Messages: | ||||||
|       allOf: |       allOf: | ||||||
|         - $ref: "#/components/schemas/MessagesBase" |         - $ref: "#/components/schemas/MessagesBase" | ||||||
|   | |||||||
| @@ -196,6 +196,7 @@ from zerver.models import ( | |||||||
|     UserMessage, |     UserMessage, | ||||||
|     UserPresence, |     UserPresence, | ||||||
|     UserProfile, |     UserProfile, | ||||||
|  |     UserStatus, | ||||||
|     get_client, |     get_client, | ||||||
|     get_stream, |     get_stream, | ||||||
|     get_user_by_delivery_email, |     get_user_by_delivery_email, | ||||||
| @@ -954,23 +955,45 @@ class NormalActionsTest(BaseAction): | |||||||
|                 user_profile=self.user_profile, |                 user_profile=self.user_profile, | ||||||
|                 away=True, |                 away=True, | ||||||
|                 status_text="out to lunch", |                 status_text="out to lunch", | ||||||
|  |                 emoji_name="car", | ||||||
|  |                 emoji_code="1f697", | ||||||
|  |                 reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|                 client_id=client.id, |                 client_id=client.id, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         check_user_status("events[0]", events[0], {"away", "status_text"}) |         check_user_status( | ||||||
|  |             "events[0]", | ||||||
|  |             events[0], | ||||||
|  |             {"away", "status_text", "emoji_name", "emoji_code", "reaction_type"}, | ||||||
|  |         ) | ||||||
|         events = self.verify_action( |         events = self.verify_action( | ||||||
|             lambda: do_update_user_status( |             lambda: do_update_user_status( | ||||||
|                 user_profile=self.user_profile, away=False, status_text="", client_id=client.id |                 user_profile=self.user_profile, | ||||||
|  |                 away=False, | ||||||
|  |                 status_text="", | ||||||
|  |                 emoji_name="", | ||||||
|  |                 emoji_code="", | ||||||
|  |                 reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|  |                 client_id=client.id, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         check_user_status("events[0]", events[0], {"away", "status_text"}) |         check_user_status( | ||||||
|  |             "events[0]", | ||||||
|  |             events[0], | ||||||
|  |             {"away", "status_text", "emoji_name", "emoji_code", "reaction_type"}, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|         events = self.verify_action( |         events = self.verify_action( | ||||||
|             lambda: do_update_user_status( |             lambda: do_update_user_status( | ||||||
|                 user_profile=self.user_profile, away=True, status_text=None, client_id=client.id |                 user_profile=self.user_profile, | ||||||
|  |                 away=True, | ||||||
|  |                 status_text=None, | ||||||
|  |                 emoji_name=None, | ||||||
|  |                 emoji_code=None, | ||||||
|  |                 reaction_type=None, | ||||||
|  |                 client_id=client.id, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -981,6 +1004,9 @@ class NormalActionsTest(BaseAction): | |||||||
|                 user_profile=self.user_profile, |                 user_profile=self.user_profile, | ||||||
|                 away=None, |                 away=None, | ||||||
|                 status_text="at the beach", |                 status_text="at the beach", | ||||||
|  |                 emoji_name=None, | ||||||
|  |                 emoji_code=None, | ||||||
|  |                 reaction_type=None, | ||||||
|                 client_id=client.id, |                 client_id=client.id, | ||||||
|             ) |             ) | ||||||
|         ) |         ) | ||||||
|   | |||||||
| @@ -36,6 +36,9 @@ class UserStatusTest(ZulipTestCase): | |||||||
|             user_profile_id=hamlet.id, |             user_profile_id=hamlet.id, | ||||||
|             status=UserStatus.AWAY, |             status=UserStatus.AWAY, | ||||||
|             status_text=None, |             status_text=None, | ||||||
|  |             emoji_name=None, | ||||||
|  |             emoji_code=None, | ||||||
|  |             reaction_type=None, | ||||||
|             client_id=client1.id, |             client_id=client1.id, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -52,12 +55,21 @@ class UserStatusTest(ZulipTestCase): | |||||||
|             user_profile_id=hamlet.id, |             user_profile_id=hamlet.id, | ||||||
|             status=UserStatus.AWAY, |             status=UserStatus.AWAY, | ||||||
|             status_text="out to lunch", |             status_text="out to lunch", | ||||||
|  |             emoji_name="car", | ||||||
|  |             emoji_code="1f697", | ||||||
|  |             reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|             client_id=client2.id, |             client_id=client2.id, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             user_info(hamlet), |             user_info(hamlet), | ||||||
|             dict(away=True, status_text="out to lunch"), |             dict( | ||||||
|  |                 away=True, | ||||||
|  |                 status_text="out to lunch", | ||||||
|  |                 emoji_name="car", | ||||||
|  |                 emoji_code="1f697", | ||||||
|  |                 reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|  |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         away_user_ids = get_away_user_ids(realm_id=realm_id) |         away_user_ids = get_away_user_ids(realm_id=realm_id) | ||||||
| @@ -66,24 +78,35 @@ class UserStatusTest(ZulipTestCase): | |||||||
|         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) | ||||||
|  |  | ||||||
|         # Setting status_text to None causes it be ignored. |         # Setting status_text and emoji_info to None causes it be ignored. | ||||||
|         update_user_status( |         update_user_status( | ||||||
|             user_profile_id=hamlet.id, |             user_profile_id=hamlet.id, | ||||||
|             status=UserStatus.NORMAL, |             status=UserStatus.NORMAL, | ||||||
|             status_text=None, |             status_text=None, | ||||||
|  |             emoji_name=None, | ||||||
|  |             emoji_code=None, | ||||||
|  |             reaction_type=None, | ||||||
|             client_id=client2.id, |             client_id=client2.id, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         self.assertEqual( |         self.assertEqual( | ||||||
|             user_info(hamlet), |             user_info(hamlet), | ||||||
|             dict(status_text="out to lunch"), |             dict( | ||||||
|  |                 status_text="out to lunch", | ||||||
|  |                 emoji_name="car", | ||||||
|  |                 emoji_code="1f697", | ||||||
|  |                 reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|  |             ), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|         # Clear the status_text 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, | ||||||
|             status=None, |             status=None, | ||||||
|             status_text="", |             status_text="", | ||||||
|  |             emoji_name="", | ||||||
|  |             emoji_code="", | ||||||
|  |             reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|             client_id=client2.id, |             client_id=client2.id, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -101,18 +124,27 @@ class UserStatusTest(ZulipTestCase): | |||||||
|             user_profile_id=hamlet.id, |             user_profile_id=hamlet.id, | ||||||
|             status=UserStatus.AWAY, |             status=UserStatus.AWAY, | ||||||
|             status_text=None, |             status_text=None, | ||||||
|  |             emoji_name=None, | ||||||
|  |             emoji_code=None, | ||||||
|  |             reaction_type=None, | ||||||
|             client_id=client1.id, |             client_id=client1.id, | ||||||
|         ) |         ) | ||||||
|         update_user_status( |         update_user_status( | ||||||
|             user_profile_id=cordelia.id, |             user_profile_id=cordelia.id, | ||||||
|             status=UserStatus.AWAY, |             status=UserStatus.AWAY, | ||||||
|             status_text=None, |             status_text=None, | ||||||
|  |             emoji_name=None, | ||||||
|  |             emoji_code=None, | ||||||
|  |             reaction_type=None, | ||||||
|             client_id=client2.id, |             client_id=client2.id, | ||||||
|         ) |         ) | ||||||
|         update_user_status( |         update_user_status( | ||||||
|             user_profile_id=king_lear.id, |             user_profile_id=king_lear.id, | ||||||
|             status=UserStatus.AWAY, |             status=UserStatus.AWAY, | ||||||
|             status_text=None, |             status_text=None, | ||||||
|  |             emoji_name=None, | ||||||
|  |             emoji_code=None, | ||||||
|  |             reaction_type=None, | ||||||
|             client_id=client2.id, |             client_id=client2.id, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -127,6 +159,9 @@ class UserStatusTest(ZulipTestCase): | |||||||
|             user_profile_id=hamlet.id, |             user_profile_id=hamlet.id, | ||||||
|             status=UserStatus.NORMAL, |             status=UserStatus.NORMAL, | ||||||
|             status_text="in a meeting", |             status_text="in a meeting", | ||||||
|  |             emoji_name=None, | ||||||
|  |             emoji_code=None, | ||||||
|  |             reaction_type=None, | ||||||
|             client_id=client2.id, |             client_id=client2.id, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
| @@ -158,6 +193,31 @@ class UserStatusTest(ZulipTestCase): | |||||||
|         result = self.client_post("/json/users/me/status", payload) |         result = self.client_post("/json/users/me/status", payload) | ||||||
|         self.assert_json_error(result, "Client did not pass any new values.") |         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. | ||||||
|  |         payload = {"status_text": "In a meeting", "emoji_code": "1f4bb"} | ||||||
|  |         result = self.client_post("/json/users/me/status", payload) | ||||||
|  |         self.assert_json_error( | ||||||
|  |             result, "Client must pass emoji_name if they pass either emoji_code or reaction_type." | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Invalid emoji requests fail | ||||||
|  |         payload = {"status_text": "In a meeting", "emoji_code": "1f4bb", "emoji_name": "invalid"} | ||||||
|  |         result = self.client_post("/json/users/me/status", payload) | ||||||
|  |         self.assert_json_error(result, "Emoji 'invalid' does not exist") | ||||||
|  |  | ||||||
|  |         payload = {"status_text": "In a meeting", "emoji_code": "1f4bb", "emoji_name": "car"} | ||||||
|  |         result = self.client_post("/json/users/me/status", payload) | ||||||
|  |         self.assert_json_error(result, "Invalid emoji name.") | ||||||
|  |  | ||||||
|  |         payload = { | ||||||
|  |             "status_text": "In a meeting", | ||||||
|  |             "emoji_code": "1f4bb", | ||||||
|  |             "emoji_name": "car", | ||||||
|  |             "reaction_type": "realm_emoji", | ||||||
|  |         } | ||||||
|  |         result = self.client_post("/json/users/me/status", payload) | ||||||
|  |         self.assert_json_error(result, "Invalid custom emoji.") | ||||||
|  |  | ||||||
|         # Try a long message. |         # Try a long message. | ||||||
|         long_text = "x" * 61 |         long_text = "x" * 61 | ||||||
|         payload = dict(status_text=long_text) |         payload = dict(status_text=long_text) | ||||||
| @@ -178,6 +238,52 @@ class UserStatusTest(ZulipTestCase): | |||||||
|             dict(away=True, status_text="on vacation"), |             dict(away=True, status_text="on vacation"), | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         # Server should fill emoji_code and reaction_type by emoji_name. | ||||||
|  |         self.update_status_and_assert_event( | ||||||
|  |             payload=dict( | ||||||
|  |                 away=orjson.dumps(True).decode(), | ||||||
|  |                 emoji_name="car", | ||||||
|  |             ), | ||||||
|  |             expected_event=dict( | ||||||
|  |                 type="user_status", | ||||||
|  |                 user_id=hamlet.id, | ||||||
|  |                 away=True, | ||||||
|  |                 emoji_name="car", | ||||||
|  |                 emoji_code="1f697", | ||||||
|  |                 reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             user_info(hamlet), | ||||||
|  |             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. | ||||||
|  |         self.update_status_and_assert_event( | ||||||
|  |             payload=dict( | ||||||
|  |                 away=orjson.dumps(True).decode(), | ||||||
|  |                 emoji_name="", | ||||||
|  |             ), | ||||||
|  |             expected_event=dict( | ||||||
|  |                 type="user_status", | ||||||
|  |                 user_id=hamlet.id, | ||||||
|  |                 away=True, | ||||||
|  |                 emoji_name="", | ||||||
|  |                 emoji_code="", | ||||||
|  |                 reaction_type=UserStatus.UNICODE_EMOJI, | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |         self.assertEqual( | ||||||
|  |             user_info(hamlet), | ||||||
|  |             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()), | ||||||
|   | |||||||
| @@ -8,6 +8,7 @@ from django.utils.translation import gettext as _ | |||||||
|  |  | ||||||
| from zerver.decorator import human_users_only | from zerver.decorator import human_users_only | ||||||
| from zerver.lib.actions import do_update_user_status, update_user_presence | from zerver.lib.actions import do_update_user_status, update_user_presence | ||||||
|  | from zerver.lib.emoji import check_emoji_request, emoji_name_to_emoji_code | ||||||
| from zerver.lib.exceptions import JsonableError | from zerver.lib.exceptions import JsonableError | ||||||
| from zerver.lib.presence import get_presence_for_user, get_presence_response | from zerver.lib.presence import get_presence_for_user, get_presence_response | ||||||
| from zerver.lib.request import REQ, get_request_notes, has_request_variables | from zerver.lib.request import REQ, get_request_notes, has_request_variables | ||||||
| @@ -18,6 +19,7 @@ from zerver.models import ( | |||||||
|     UserActivity, |     UserActivity, | ||||||
|     UserPresence, |     UserPresence, | ||||||
|     UserProfile, |     UserProfile, | ||||||
|  |     UserStatus, | ||||||
|     get_active_user, |     get_active_user, | ||||||
|     get_active_user_profile_by_id_in_realm, |     get_active_user_profile_by_id_in_realm, | ||||||
| ) | ) | ||||||
| @@ -68,14 +70,50 @@ def update_user_status_backend( | |||||||
|     user_profile: UserProfile, |     user_profile: UserProfile, | ||||||
|     away: Optional[bool] = REQ(json_validator=check_bool, default=None), |     away: Optional[bool] = REQ(json_validator=check_bool, default=None), | ||||||
|     status_text: Optional[str] = REQ(str_validator=check_capped_string(60), default=None), |     status_text: Optional[str] = REQ(str_validator=check_capped_string(60), default=None), | ||||||
|  |     emoji_name: Optional[str] = REQ(default=None), | ||||||
|  |     emoji_code: Optional[str] = REQ(default=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: Optional[str] = REQ("reaction_type", default=None), | ||||||
| ) -> HttpResponse: | ) -> HttpResponse: | ||||||
|  |  | ||||||
|     if status_text is not None: |     if status_text is not None: | ||||||
|         status_text = status_text.strip() |         status_text = status_text.strip() | ||||||
|  |  | ||||||
|     if (away is None) and (status_text is None): |     if (away is None) and (status_text is None) and (emoji_name is None): | ||||||
|         raise JsonableError(_("Client did not pass any new values.")) |         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: | ||||||
|  |             # 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_name_to_emoji_code(user_profile.realm, emoji_name)[0] | ||||||
|  |  | ||||||
|  |         if emoji_type is None: | ||||||
|  |             emoji_type = emoji_name_to_emoji_code(user_profile.realm, emoji_name)[1] | ||||||
|  |  | ||||||
|  |     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 = get_request_notes(request).client |     client = get_request_notes(request).client | ||||||
|     assert client is not None |     assert client is not None | ||||||
|     do_update_user_status( |     do_update_user_status( | ||||||
| @@ -83,6 +121,9 @@ def update_user_status_backend( | |||||||
|         away=away, |         away=away, | ||||||
|         status_text=status_text, |         status_text=status_text, | ||||||
|         client_id=client.id, |         client_id=client.id, | ||||||
|  |         emoji_name=emoji_name, | ||||||
|  |         emoji_code=emoji_code, | ||||||
|  |         reaction_type=emoji_type, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|     return json_success() |     return json_success() | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user