mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-04 05:53:43 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			140 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			140 lines
		
	
	
		
			5.8 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
from typing import Optional
 | 
						|
 | 
						|
from django.http import HttpRequest, HttpResponse
 | 
						|
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.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,
 | 
						|
    )
 | 
						|
 | 
						|
 | 
						|
@has_request_variables
 | 
						|
def add_reaction(
 | 
						|
    request: HttpRequest,
 | 
						|
    user_profile: UserProfile,
 | 
						|
    message_id: int,
 | 
						|
    emoji_name: str = REQ(),
 | 
						|
    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)
 | 
						|
 | 
						|
    return json_success()
 | 
						|
 | 
						|
 | 
						|
@has_request_variables
 | 
						|
def remove_reaction(
 | 
						|
    request: HttpRequest,
 | 
						|
    user_profile: UserProfile,
 | 
						|
    message_id: int,
 | 
						|
    emoji_name: Optional[str] = REQ(default=None),
 | 
						|
    emoji_code: Optional[str] = REQ(default=None),
 | 
						|
    reaction_type: str = REQ(default="unicode_emoji"),
 | 
						|
) -> HttpResponse:
 | 
						|
    message, user_message = access_message(user_profile, message_id)
 | 
						|
 | 
						|
    if emoji_code is None:
 | 
						|
        if emoji_name is None:
 | 
						|
            raise JsonableError(
 | 
						|
                _(
 | 
						|
                    "At least one of the following arguments "
 | 
						|
                    "must be present: emoji_name, emoji_code"
 | 
						|
                )
 | 
						|
            )
 | 
						|
        # A correct full Zulip client implementation should always
 | 
						|
        # pass an emoji_code, because of the corner cases discussed in
 | 
						|
        # the long block comments elsewhere in this file.  However, to
 | 
						|
        # make it easy for simple API clients to use the reactions API
 | 
						|
        # without needing the mapping between emoji names and codes,
 | 
						|
        # we allow instead passing the emoji_name and looking up the
 | 
						|
        # corresponding code using the current data.
 | 
						|
        emoji_code = emoji_name_to_emoji_code(message.sender.realm, emoji_name)[0]
 | 
						|
 | 
						|
    if not Reaction.objects.filter(
 | 
						|
        user_profile=user_profile,
 | 
						|
        message=message,
 | 
						|
        emoji_code=emoji_code,
 | 
						|
        reaction_type=reaction_type,
 | 
						|
    ).exists():
 | 
						|
        raise JsonableError(_("Reaction doesn't exist."))
 | 
						|
 | 
						|
    # Unlike adding reactions, while deleting a reaction, we don't
 | 
						|
    # check whether the provided (emoji_type, emoji_code) pair is
 | 
						|
    # valid in this realm.  Since there's a row in the database, we
 | 
						|
    # know it was valid when the user added their reaction in the
 | 
						|
    # first place, so it is safe to just remove the reaction if it
 | 
						|
    # exists.  And the (reaction_type, emoji_code) pair may no longer be
 | 
						|
    # valid in legitimate situations (e.g. if a realm emoji was
 | 
						|
    # deactivated by an administrator in the meantime).
 | 
						|
    do_remove_reaction(user_profile, message, emoji_code, reaction_type)
 | 
						|
 | 
						|
    return json_success()
 |