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:
Riken Shah 2021-06-22 16:42:31 +00:00 committed by Tim Abbott
parent ed01ffadba
commit 9fadd43830
10 changed files with 330 additions and 48 deletions

View File

@ -11,6 +11,15 @@ below features are supported.
## 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**
* [`POST /register`](/api/register-queue), `PATCH /realm`: Replaced `add_emoji_by_admins_only`

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.3"
# Changes should be accompanied by documentation explaining what the
# new level means in templates/zerver/api/changelog.md, as well as
# "**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
# only when going from an old version of the code to a newer version. Bump

View File

@ -5401,7 +5401,13 @@ def update_user_presence(
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:
if away is None:
status = None
@ -5417,6 +5423,9 @@ def do_update_user_status(
status=status,
status_text=status_text,
client_id=client_id,
emoji_name=emoji_name,
emoji_code=emoji_code,
reaction_type=reaction_type,
)
event = dict(
@ -5430,6 +5439,10 @@ def do_update_user_status(
if status_text is not None:
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))

View File

@ -1653,6 +1653,9 @@ user_status_event = event_dict_type(
# force vertical
("away", bool),
("status_text", str),
("emoji_name", str),
("emoji_code", str),
("reaction_type", str),
],
)
_check_user_status = make_checker(user_status_event)

View File

@ -59,6 +59,7 @@ from zerver.models import (
Stream,
UserMessage,
UserProfile,
UserStatus,
custom_profile_fields_for_realm,
get_default_stream_groups,
get_realm_domains,
@ -1092,6 +1093,9 @@ def apply_event(
user_status = state["user_status"]
away = event.get("away")
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:
user_status[user_id_str] = {}
@ -1108,6 +1112,24 @@ def apply_event(
else:
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]:
user_status.pop(user_id_str, None)

View File

@ -13,12 +13,19 @@ def get_user_info_dict(realm_id: int) -> Dict[str, Dict[str, Any]]:
user_profile__is_active=True,
)
.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(
"user_profile_id",
"status",
"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
status_text = row["status_text"]
user_id = row["user_profile_id"]
emoji_name = row["emoji_name"]
emoji_code = row["emoji_code"]
reaction_type = row["reaction_type"]
dct = {}
if away:
dct["away"] = away
if 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
@ -40,7 +54,13 @@ def get_user_info_dict(realm_id: int) -> Dict[str, Dict[str, Any]]:
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:
timestamp = timezone_now()
@ -56,6 +76,15 @@ def update_user_status(
if status_text is not None:
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(
user_profile_id=user_profile_id,
defaults=defaults,

View File

@ -1430,6 +1430,24 @@ paths:
type: string
description: |
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:
type: integer
description: |
@ -1440,6 +1458,9 @@ paths:
"user_id": 10,
"away": true,
"status_text": "out to lunch",
"emoji_name": "car",
"emoji_code": "1f697",
"reaction_type": "unicode_emoji",
"id": 0,
}
- type: object
@ -8360,12 +8381,19 @@ paths:
error messages when a search returns limited results because
a stop word in the query was ignored.
user_status:
type: object
allOf:
- $ref: "#/components/schemas/EmojiBase"
description: |
Present if `user_status` is present in `fetch_event_types`.
A dictionary which contains the [status](/help/status-and-availability)
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:
description: |
`{user_id}`: Object containing the status details of a user
@ -12241,7 +12269,7 @@ components:
reaction_type: {}
user_id: {}
user: {}
EmojiReactionBase:
EmojiBase:
type: object
properties:
emoji_code:
@ -12267,42 +12295,47 @@ components:
(`emoji_code` will be its ID).
* `zulip_extra_emoji`: Special emoji included with Zulip. Exists to
namespace the `zulip` emoji.
user_id:
type: integer
description: |
The ID of the user who added the reaction.
**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:
EmojiReactionBase:
allOf:
- $ref: "#/components/schemas/EmojiBase"
- properties:
user_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
The ID of the user who added the reaction.
**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: |
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:
allOf:
- $ref: "#/components/schemas/MessagesBase"

View File

@ -196,6 +196,7 @@ from zerver.models import (
UserMessage,
UserPresence,
UserProfile,
UserStatus,
get_client,
get_stream,
get_user_by_delivery_email,
@ -954,23 +955,45 @@ class NormalActionsTest(BaseAction):
user_profile=self.user_profile,
away=True,
status_text="out to lunch",
emoji_name="car",
emoji_code="1f697",
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(
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(
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,
away=None,
status_text="at the beach",
emoji_name=None,
emoji_code=None,
reaction_type=None,
client_id=client.id,
)
)

View File

@ -36,6 +36,9 @@ class UserStatusTest(ZulipTestCase):
user_profile_id=hamlet.id,
status=UserStatus.AWAY,
status_text=None,
emoji_name=None,
emoji_code=None,
reaction_type=None,
client_id=client1.id,
)
@ -52,12 +55,21 @@ class UserStatusTest(ZulipTestCase):
user_profile_id=hamlet.id,
status=UserStatus.AWAY,
status_text="out to lunch",
emoji_name="car",
emoji_code="1f697",
reaction_type=UserStatus.UNICODE_EMOJI,
client_id=client2.id,
)
self.assertEqual(
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)
@ -66,24 +78,35 @@ class UserStatusTest(ZulipTestCase):
rec_count = UserStatus.objects.filter(user_profile_id=hamlet.id).count()
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(
user_profile_id=hamlet.id,
status=UserStatus.NORMAL,
status_text=None,
emoji_name=None,
emoji_code=None,
reaction_type=None,
client_id=client2.id,
)
self.assertEqual(
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(
user_profile_id=hamlet.id,
status=None,
status_text="",
emoji_name="",
emoji_code="",
reaction_type=UserStatus.UNICODE_EMOJI,
client_id=client2.id,
)
@ -101,18 +124,27 @@ class UserStatusTest(ZulipTestCase):
user_profile_id=hamlet.id,
status=UserStatus.AWAY,
status_text=None,
emoji_name=None,
emoji_code=None,
reaction_type=None,
client_id=client1.id,
)
update_user_status(
user_profile_id=cordelia.id,
status=UserStatus.AWAY,
status_text=None,
emoji_name=None,
emoji_code=None,
reaction_type=None,
client_id=client2.id,
)
update_user_status(
user_profile_id=king_lear.id,
status=UserStatus.AWAY,
status_text=None,
emoji_name=None,
emoji_code=None,
reaction_type=None,
client_id=client2.id,
)
@ -127,6 +159,9 @@ class UserStatusTest(ZulipTestCase):
user_profile_id=hamlet.id,
status=UserStatus.NORMAL,
status_text="in a meeting",
emoji_name=None,
emoji_code=None,
reaction_type=None,
client_id=client2.id,
)
@ -158,6 +193,31 @@ class UserStatusTest(ZulipTestCase):
result = self.client_post("/json/users/me/status", payload)
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.
long_text = "x" * 61
payload = dict(status_text=long_text)
@ -178,6 +238,52 @@ class UserStatusTest(ZulipTestCase):
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.
self.update_status_and_assert_event(
payload=dict(away=orjson.dumps(False).decode()),

View File

@ -8,6 +8,7 @@ from django.utils.translation import gettext as _
from zerver.decorator import human_users_only
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.presence import get_presence_for_user, get_presence_response
from zerver.lib.request import REQ, get_request_notes, has_request_variables
@ -18,6 +19,7 @@ from zerver.models import (
UserActivity,
UserPresence,
UserProfile,
UserStatus,
get_active_user,
get_active_user_profile_by_id_in_realm,
)
@ -68,14 +70,50 @@ def update_user_status_backend(
user_profile: UserProfile,
away: Optional[bool] = REQ(json_validator=check_bool, 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:
if status_text is not None:
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."))
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
assert client is not None
do_update_user_status(
@ -83,6 +121,9 @@ def update_user_status_backend(
away=away,
status_text=status_text,
client_id=client.id,
emoji_name=emoji_name,
emoji_code=emoji_code,
reaction_type=emoji_type,
)
return json_success()