diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index f1f51db6387b87..93ec2d7919f2dc 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -82,7 +82,7 @@ get_realm_email_validator, validate_email_is_valid, ) -from zerver.lib.emoji import get_emoji_file_name +from zerver.lib.emoji import check_emoji_request, emoji_name_to_emoji_code, get_emoji_file_name from zerver.lib.exceptions import ( ErrorCode, JsonableError, @@ -2197,6 +2197,76 @@ def notify_reaction_update( send_event(user_profile.realm, event, list(user_ids)) +def check_add_reaction( + user_profile: UserProfile, + message_id: int, + emoji_name: str, + emoji_code: Optional[str], + reaction_type: Optional[str], +) -> None: + message, user_message = access_message(user_profile, message_id) + + 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(message.sender.realm, emoji_name)[0] + + if reaction_type is None: + reaction_type = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[1] + + if Reaction.objects.filter( + user_profile=user_profile, + message=message, + emoji_code=emoji_code, + reaction_type=reaction_type, + ).exists(): + raise JsonableError(_("Reaction already exists.")) + + query = Reaction.objects.filter( + message=message, emoji_code=emoji_code, reaction_type=reaction_type + ) + if query.exists(): + # If another user has already reacted to this message with + # same emoji code, we treat the new reaction as a vote for the + # existing reaction. So the emoji name used by that earlier + # reaction takes precedence over whatever was passed in this + # request. This is necessary to avoid a message having 2 + # "different" emoji reactions with the same emoji code (and + # thus same image) on the same message, which looks ugly. + # + # In this "voting for an existing reaction" case, we shouldn't + # check whether the emoji code and emoji name match, since + # it's possible that the (emoji_type, emoji_name, emoji_code) + # triple for this existing rection xmay not pass validation + # now (e.g. because it is for a realm emoji that has been + # since deactivated). We still want to allow users to add a + # vote any old reaction they see in the UI even if that is a + # deactivated custom emoji, so we just use the emoji name from + # the existing reaction with no further validation. + emoji_name = query.first().emoji_name + else: + # Otherwise, use the name provided in this request, but verify + # it is valid in the user's realm (e.g. not a deactivated + # realm emoji). + check_emoji_request(user_profile.realm, emoji_name, emoji_code, reaction_type) + + if user_message is None: + # Users can see and react to messages sent to streams they + # were not a subscriber to; in order to receive events for + # those, we give the user a `historical` UserMessage objects + # for the message. This is the same trick we use for starring + # messages. + UserMessage.objects.create( + user_profile=user_profile, + message=message, + flags=UserMessage.flags.historical | UserMessage.flags.read, + ) + + do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type) + + def do_add_reaction( user_profile: UserProfile, message: Message, diff --git a/zerver/views/reactions.py b/zerver/views/reactions.py index 8d64c226944f4a..e66161159aa25a 100644 --- a/zerver/views/reactions.py +++ b/zerver/views/reactions.py @@ -4,25 +4,12 @@ from django.utils.translation import ugettext as _ from zerver.decorator import REQ, has_request_variables -from zerver.lib.actions import do_add_reaction, do_remove_reaction -from zerver.lib.emoji import check_emoji_request, emoji_name_to_emoji_code +from zerver.lib.actions import check_add_reaction, do_remove_reaction +from zerver.lib.emoji import emoji_name_to_emoji_code from zerver.lib.message import access_message from zerver.lib.request import JsonableError from zerver.lib.response import json_success -from zerver.models import Message, Reaction, UserMessage, UserProfile - - -def create_historical_message(user_profile: UserProfile, message: Message) -> None: - # Users can see and react to messages sent to streams they - # were not a subscriber to; in order to receive events for - # those, we give the user a `historical` UserMessage objects - # for the message. This is the same trick we use for starring - # messages. - UserMessage.objects.create( - user_profile=user_profile, - message=message, - flags=UserMessage.flags.historical | UserMessage.flags.read, - ) +from zerver.models import Reaction, UserProfile @has_request_variables @@ -34,58 +21,7 @@ def add_reaction( emoji_code: Optional[str] = REQ(default=None), reaction_type: Optional[str] = REQ(default=None), ) -> HttpResponse: - message, user_message = access_message(user_profile, message_id) - - 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(message.sender.realm, emoji_name)[0] - - if reaction_type is None: - reaction_type = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[1] - - if Reaction.objects.filter( - user_profile=user_profile, - message=message, - emoji_code=emoji_code, - reaction_type=reaction_type, - ).exists(): - raise JsonableError(_("Reaction already exists.")) - - query = Reaction.objects.filter( - message=message, emoji_code=emoji_code, reaction_type=reaction_type - ) - if query.exists(): - # If another user has already reacted to this message with - # same emoji code, we treat the new reaction as a vote for the - # existing reaction. So the emoji name used by that earlier - # reaction takes precedence over whatever was passed in this - # request. This is necessary to avoid a message having 2 - # "different" emoji reactions with the same emoji code (and - # thus same image) on the same message, which looks ugly. - # - # In this "voting for an existing reaction" case, we shouldn't - # check whether the emoji code and emoji name match, since - # it's possible that the (emoji_type, emoji_name, emoji_code) - # triple for this existing rection xmay not pass validation - # now (e.g. because it is for a realm emoji that has been - # since deactivated). We still want to allow users to add a - # vote any old reaction they see in the UI even if that is a - # deactivated custom emoji, so we just use the emoji name from - # the existing reaction with no further validation. - emoji_name = query.first().emoji_name - else: - # Otherwise, use the name provided in this request, but verify - # it is valid in the user's realm (e.g. not a deactivated - # realm emoji). - check_emoji_request(user_profile.realm, emoji_name, emoji_code, reaction_type) - - if user_message is None: - create_historical_message(user_profile, message) - - do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type) + check_add_reaction(user_profile, message_id, emoji_name, emoji_code, reaction_type) return json_success()