api: Add new endpoint for reactions.

This endpoint will allow us to add/delete emoji reactions whose emoji
got renamed during various emoji infra changes. This was also a
required change for realm emoji migration.

This commit was tweaked significantly by tabbott for greater clarity
(with no changes to the actual logic).
This commit is contained in:
Harshit Bansal 2017-10-08 07:34:59 +00:00 committed by Tim Abbott
parent 40fca20c51
commit d9c2f613e3
6 changed files with 672 additions and 5 deletions

View File

@ -1380,6 +1380,23 @@ def do_remove_reaction_legacy(user_profile, message, emoji_name):
reaction.delete()
notify_reaction_update(user_profile, message, reaction, "remove")
def do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type):
# type: (UserProfile, Message, Text, Text, Text) -> None
reaction = Reaction(user_profile=user_profile, message=message,
emoji_name=emoji_name, emoji_code=emoji_code,
reaction_type=reaction_type)
reaction.save()
notify_reaction_update(user_profile, message, reaction, "add")
def do_remove_reaction(user_profile, message, emoji_code, reaction_type):
# type: (UserProfile, Message, Text, Text) -> None
reaction = Reaction.objects.filter(user_profile=user_profile,
message=message,
emoji_code=emoji_code,
reaction_type=reaction_type).get()
reaction.delete()
notify_reaction_update(user_profile, message, reaction, "remove")
def do_send_typing_notification(notification):
# type: (Dict[str, Any]) -> None
recipient_user_profiles = get_typing_user_profiles(notification['recipient'],

View File

@ -12,9 +12,14 @@ from zerver.lib.upload import upload_backend
from zerver.models import Reaction, Realm, RealmEmoji, UserProfile
NAME_TO_CODEPOINT_PATH = os.path.join(settings.STATIC_ROOT, "generated", "emoji", "name_to_codepoint.json")
CODEPOINT_TO_NAME_PATH = os.path.join(settings.STATIC_ROOT, "generated", "emoji", "codepoint_to_name.json")
with open(NAME_TO_CODEPOINT_PATH) as fp:
name_to_codepoint = ujson.load(fp)
with open(CODEPOINT_TO_NAME_PATH) as fp:
codepoint_to_name = ujson.load(fp)
def emoji_name_to_emoji_code(realm, emoji_name):
# type: (Realm, Text) -> Tuple[Text, Text]
realm_emojis = realm.get_emoji()
@ -30,6 +35,45 @@ def check_valid_emoji(realm, emoji_name):
# type: (Realm, Text) -> None
emoji_name_to_emoji_code(realm, emoji_name)
def emoji_code_is_valid(realm: Realm, emoji_code: str, emoji_type: str) -> bool:
# For a given realm and emoji type, determines whether an emoji
# code is valid for new reactions, or not.
if emoji_type == "realm_emoji":
realm_emojis = realm.get_emoji()
if emoji_code not in realm_emojis:
return False
if realm_emojis[emoji_code]["deactivated"]:
return False
return True
elif emoji_type == "zulip_extra_emoji":
return emoji_code == "zulip"
elif emoji_type == "unicode_emoji":
return emoji_code in codepoint_to_name
# The above are the only valid emoji types
return False
def emoji_name_is_valid(emoji_name: str, emoji_code: str, emoji_type: str) -> bool:
# Given a realm, emoji code and emoji type, determines whether the
# passed emoji name is a valid name for that emoji. It is assumed
# here that the emoji code has been checked for validity before
# calling this function.
if emoji_type == "realm_emoji":
return emoji_code == emoji_name
elif emoji_type == "zulip_extra_emoji":
return emoji_name == "zulip"
elif emoji_type == "unicode_emoji":
return name_to_codepoint.get(emoji_name) == emoji_code
raise AssertionError("Emoji type should have been checked previously")
def check_emoji_name_consistency(emoji_name: str, emoji_code: str, emoji_type: str) -> None:
if not emoji_name_is_valid(emoji_name, emoji_code, emoji_type):
raise JsonableError(_("Invalid emoji name."))
def check_emoji_code_consistency(realm: Realm, emoji_code: str, emoji_type: str) -> None:
if not emoji_code_is_valid(realm, emoji_code, emoji_type):
raise JsonableError(_("Emoji for this emoji code not found."))
def check_emoji_admin(user_profile, emoji_name=None):
# type: (UserProfile, Optional[Text]) -> None
"""Raises an exception if the user cannot administer the target realm

View File

@ -25,6 +25,7 @@ from zerver.lib.actions import (
check_send_typing_notification,
do_add_alert_words,
do_add_default_stream,
do_add_reaction,
do_add_reaction_legacy,
do_add_realm_domain,
do_add_realm_filter,
@ -55,6 +56,7 @@ from zerver.lib.actions import (
do_remove_alert_words,
do_remove_default_stream,
do_remove_default_stream_group,
do_remove_reaction,
do_remove_reaction_legacy,
do_remove_realm_domain,
do_remove_realm_emoji,
@ -861,6 +863,59 @@ class EventsRegisterTest(ZulipTestCase):
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_add_reaction(self):
# type: () -> None
schema_checker = self.check_events_dict([
('type', equals('reaction')),
('op', equals('add')),
('message_id', check_int),
('emoji_name', check_string),
('emoji_code', check_string),
('reaction_type', check_string),
('user', check_dict_only([
('email', check_string),
('full_name', check_string),
('user_id', check_int)
])),
])
message_id = self.send_stream_message(self.example_email("hamlet"), "Verona", "hello")
message = Message.objects.get(id=message_id)
events = self.do_test(
lambda: do_add_reaction(
self.user_profile, message, "tada", "1f389", "unicode_emoji"),
state_change_expected=False,
)
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_remove_reaction(self):
# type: () -> None
schema_checker = self.check_events_dict([
('type', equals('reaction')),
('op', equals('remove')),
('message_id', check_int),
('emoji_name', check_string),
('emoji_code', check_string),
('reaction_type', check_string),
('user', check_dict_only([
('email', check_string),
('full_name', check_string),
('user_id', check_int)
])),
])
message_id = self.send_stream_message(self.example_email("hamlet"), "Verona", "hello")
message = Message.objects.get(id=message_id)
do_add_reaction(self.user_profile, message, "tada", "1f389", "unicode_emoji")
events = self.do_test(
lambda: do_remove_reaction(
self.user_profile, message, "1f389", "unicode_emoji"),
state_change_expected=False,
)
error = schema_checker('events[0]', events[0])
self.assert_on_error(error)
def test_typing_events(self):
# type: () -> None
schema_checker = self.check_events_dict([

View File

@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
import ujson
from typing import Any, Mapping, List
from django.http import HttpResponse
from typing import Any, Dict, List, Mapping, Text
from unittest import mock
from zerver.lib.emoji import emoji_name_to_emoji_code
@ -9,7 +10,7 @@ from zerver.lib.request import JsonableError
from zerver.lib.test_helpers import tornado_redirected_to_list, get_display_recipient, \
get_test_image_file
from zerver.lib.test_classes import ZulipTestCase
from zerver.models import get_realm, RealmEmoji, Recipient, UserMessage
from zerver.models import get_realm, Message, Reaction, RealmEmoji, Recipient, UserMessage
class ReactionEmojiTest(ZulipTestCase):
def test_missing_emoji(self):
@ -367,3 +368,473 @@ class ReactionEventTest(ZulipTestCase):
self.assertEqual(event['op'], 'remove')
self.assertEqual(event['emoji_name'], 'smile')
self.assertEqual(event['message_id'], pm_id)
class DefaultEmojiReactionTests(ZulipTestCase):
def post_reaction(self, reaction_info, message_id=1, sender='hamlet'):
# type: (Dict[str, str], int, str) -> HttpResponse
reaction_info['reaction_type'] = 'unicode_emoji'
sender = self.example_email(sender)
result = self.client_post('/api/v1/messages/%s/reactions' % (message_id,),
reaction_info,
**self.api_auth(sender))
return result
def delete_reaction(self, reaction_info, message_id=1, sender='hamlet'):
# type: (Dict[str, str], int, str) -> HttpResponse
reaction_info['reaction_type'] = 'unicode_emoji'
sender = self.example_email(sender)
result = self.client_delete('/api/v1/messages/%s/reactions' % (message_id,),
reaction_info,
**self.api_auth(sender))
return result
def get_message_reactions(self, message_id, emoji_code, reaction_type):
# type: (int, Text, Text) -> List[Reaction]
message = Message.objects.get(id=message_id)
reactions = Reaction.objects.filter(message=message,
emoji_code=emoji_code,
reaction_type=reaction_type)
return list(reactions)
def setUp(self):
# type: () -> None
reaction_info = {
'emoji_name': 'hamburger',
'emoji_code': '1f354',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
def test_add_default_emoji_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'thumbs_up',
'emoji_code': '1f44d',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
def test_add_default_emoji_invalid_code(self):
# type: () -> None
reaction_info = {
'emoji_name': 'hamburger',
'emoji_code': 'TBD',
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, 'Emoji for this emoji code not found.')
def test_add_default_emoji_invalid_name(self):
# type: () -> None
reaction_info = {
'emoji_name': 'non-existent',
'emoji_code': '1f44d',
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, 'Invalid emoji name.')
def test_add_to_existing_renamed_default_emoji_reaction(self):
# type: () -> None
hamlet = self.example_user('hamlet')
message = Message.objects.get(id=1)
reaction = Reaction.objects.create(user_profile=hamlet,
message=message,
emoji_name='old_name',
emoji_code='1f603',
reaction_type='unicode_emoji',
)
reaction_info = {
'emoji_name': 'smiley',
'emoji_code': '1f603',
}
result = self.post_reaction(reaction_info, sender='AARON')
self.assert_json_success(result)
reactions = self.get_message_reactions(1, '1f603', 'unicode_emoji')
for reaction in reactions:
self.assertEqual(reaction.emoji_name, 'old_name')
def test_add_duplicate_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'non-existent',
'emoji_code': '1f354',
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, 'Reaction already exists.')
def test_preserve_non_canonical_name(self):
# type: () -> None
reaction_info = {
'emoji_name': '+1',
'emoji_code': '1f44d',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
reactions = self.get_message_reactions(1, '1f44d', 'unicode_emoji')
for reaction in reactions:
self.assertEqual(reaction.emoji_name, '+1')
def test_reaction_name_collapse(self):
# type: () -> None
reaction_info = {
'emoji_name': '+1',
'emoji_code': '1f44d',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
reaction_info['emoji_name'] = 'thumbs_up'
result = self.post_reaction(reaction_info, sender='AARON')
self.assert_json_success(result)
reactions = self.get_message_reactions(1, '1f44d', 'unicode_emoji')
for reaction in reactions:
self.assertEqual(reaction.emoji_name, '+1')
def test_delete_default_emoji_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'hamburger',
'emoji_code': '1f354',
}
result = self.delete_reaction(reaction_info)
self.assert_json_success(result)
def test_delete_non_existing_emoji_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'thumbs_up',
'emoji_code': '1f44d',
}
result = self.delete_reaction(reaction_info)
self.assert_json_error(result, "Reaction doesn't exist.")
def test_delete_renamed_default_emoji(self):
# type: () -> None
hamlet = self.example_user('hamlet')
message = Message.objects.get(id=1)
Reaction.objects.create(user_profile=hamlet,
message=message,
emoji_name='old_name',
emoji_code='1f44f',
reaction_type='unicode_emoji',
)
reaction_info = {
'emoji_name': 'new_name',
'emoji_code': '1f44f',
}
result = self.delete_reaction(reaction_info)
self.assert_json_success(result)
def test_react_historical(self):
# type: () -> None
"""
Reacting with valid emoji on a historical message succeeds.
"""
stream_name = "Saxony"
self.subscribe(self.example_user("cordelia"), stream_name)
message_id = self.send_stream_message(self.example_email("cordelia"), stream_name)
user_profile = self.example_user('hamlet')
# Verify that hamlet did not receive the message.
self.assertFalse(UserMessage.objects.filter(user_profile=user_profile,
message_id=message_id).exists())
# Have hamlet react to the message
reaction_info = {
'emoji_name': 'hamburger',
'emoji_code': '1f354',
}
result = self.post_reaction(reaction_info, message_id=message_id)
self.assert_json_success(result)
# Fetch the now-created UserMessage object to confirm it exists and is historical
user_message = UserMessage.objects.get(user_profile=user_profile, message_id=message_id)
self.assertTrue(user_message.flags.historical)
self.assertTrue(user_message.flags.read)
self.assertFalse(user_message.flags.starred)
class ZulipExtraEmojiReactionTest(ZulipTestCase):
def post_zulip_reaction(self, message_id=1, sender='hamlet'):
# type: (int, Text) -> HttpResponse
sender = self.example_email(sender)
reaction_info = {
'emoji_name': 'zulip',
'emoji_code': 'zulip',
'reaction_type': 'zulip_extra_emoji',
}
result = self.client_post('/api/v1/messages/%s/reactions' % (message_id,),
reaction_info,
**self.api_auth(sender))
return result
def test_add_zulip_emoji_reaction(self):
# type: () -> None
result = self.post_zulip_reaction()
self.assert_json_success(result)
def test_add_duplicate_zulip_reaction(self):
# type: () -> None
result = self.post_zulip_reaction()
self.assert_json_success(result)
result = self.post_zulip_reaction()
self.assert_json_error(result, 'Reaction already exists.')
def test_delete_zulip_emoji(self):
# type: () -> None
result = self.post_zulip_reaction()
self.assert_json_success(result)
sender = self.example_email('hamlet')
reaction_info = {
'emoji_name': 'zulip',
'emoji_code': 'zulip',
'reaction_type': 'zulip_extra_emoji',
}
result = self.client_delete('/api/v1/messages/1/reactions',
reaction_info,
**self.api_auth(sender))
self.assert_json_success(result)
def test_delete_non_existent_zulip_reaction(self):
# type: () -> None
sender = self.example_email('hamlet')
reaction_info = {
'emoji_name': 'zulip',
'emoji_code': 'zulip',
'reaction_type': 'zulip_extra_emoji',
}
result = self.client_delete('/api/v1/messages/1/reactions',
reaction_info,
**self.api_auth(sender))
self.assert_json_error(result, "Reaction doesn't exist.")
class RealmEmojiReactionTests(ZulipTestCase):
def post_reaction(self, reaction_info, message_id=1, sender='hamlet'):
# type: (Dict[str, str], int, str) -> HttpResponse
reaction_info['reaction_type'] = 'realm_emoji'
sender = self.example_email(sender)
result = self.client_post('/api/v1/messages/%s/reactions' % (message_id,),
reaction_info,
**self.api_auth(sender))
return result
def delete_reaction(self, reaction_info, message_id=1, sender='hamlet'):
# type: (Dict[str, str], int, str) -> HttpResponse
reaction_info['reaction_type'] = 'realm_emoji'
sender = self.example_email(sender)
result = self.client_delete('/api/v1/messages/%s/reactions' % (message_id,),
reaction_info,
**self.api_auth(sender))
return result
def get_message_reactions(self, message_id, emoji_code, reaction_type):
# type: (int, Text, Text) -> List[Reaction]
message = Message.objects.get(id=message_id)
reactions = Reaction.objects.filter(message=message,
emoji_code=emoji_code,
reaction_type=reaction_type)
return list(reactions)
def test_add_realm_emoji(self):
# type: () -> None
reaction_info = {
'emoji_name': 'green_tick',
'emoji_code': 'green_tick',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
def test_add_realm_emoji_invalid_code(self):
# type: () -> None
reaction_info = {
'emoji_name': 'green_tick',
'emoji_code': 'non_existent',
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, 'Emoji for this emoji code not found.')
def test_add_deactivated_realm_emoji(self):
# type: () -> None
emoji = RealmEmoji.objects.get(name="green_tick")
emoji.deactivated = True
emoji.save(update_fields=['deactivated'])
reaction_info = {
'emoji_name': 'green_tick',
'emoji_code': 'green_tick',
}
result = self.post_reaction(reaction_info)
self.assert_json_error(result, 'Emoji for this emoji code not found.')
def test_add_to_existing_deactivated_realm_emoji_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'green_tick',
'emoji_code': 'green_tick',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
emoji = RealmEmoji.objects.get(name="green_tick")
emoji.deactivated = True
emoji.save(update_fields=['deactivated'])
result = self.post_reaction(reaction_info, sender='AARON')
self.assert_json_success(result)
reactions = self.get_message_reactions(1, 'green_tick', 'realm_emoji')
self.assertEqual(len(reactions), 2)
def test_remove_realm_emoji_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'green_tick',
'emoji_code': 'green_tick',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
result = self.delete_reaction(reaction_info)
self.assert_json_success(result)
def test_remove_deactivated_realm_emoji_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'green_tick',
'emoji_code': 'green_tick',
}
result = self.post_reaction(reaction_info)
self.assert_json_success(result)
emoji = RealmEmoji.objects.get(name="green_tick")
emoji.deactivated = True
emoji.save(update_fields=['deactivated'])
result = self.delete_reaction(reaction_info)
self.assert_json_success(result)
def test_remove_non_existent_realm_emoji_reaction(self):
# type: () -> None
reaction_info = {
'emoji_name': 'non_existent',
'emoji_code': 'TBD',
}
result = self.delete_reaction(reaction_info)
self.assert_json_error(result, "Reaction doesn't exist.")
def test_invalid_reaction_type(self) -> None:
reaction_info = {
'emoji_name': 'zulip',
'emoji_code': 'zulip',
'reaction_type': 'nonexistent_emoji_type',
}
sender = self.example_email("hamlet")
message_id = 1
result = self.client_post('/api/v1/messages/%s/reactions' % (message_id,),
reaction_info,
**self.api_auth(sender))
self.assert_json_error(result, "Emoji for this emoji code not found.")
class ReactionAPIEventTest(ZulipTestCase):
def test_add_event(self):
# type: () -> None
"""
Recipients of the message receive the reaction event
and event contains relevant data
"""
pm_sender = self.example_user('hamlet')
pm_recipient = self.example_user('othello')
reaction_sender = pm_recipient
result = self.client_post("/api/v1/messages", {"type": "private",
"content": "Test message",
"to": pm_recipient.email},
**self.api_auth(pm_sender.email))
self.assert_json_success(result)
pm_id = result.json()['id']
expected_recipient_ids = set([pm_sender.id, pm_recipient.id])
reaction_info = {
'emoji_name': 'hamburger',
'emoji_code': '1f354',
'reaction_type': 'unicode_emoji',
}
events = [] # type: List[Mapping[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_post('/api/v1/messages/%s/reactions' % (pm_id,),
reaction_info,
**self.api_auth(reaction_sender.email))
self.assert_json_success(result)
self.assertEqual(len(events), 1)
event = events[0]['event']
event_user_ids = set(events[0]['users'])
self.assertEqual(expected_recipient_ids, event_user_ids)
self.assertEqual(event['user']['user_id'], reaction_sender.id)
self.assertEqual(event['user']['email'], reaction_sender.email)
self.assertEqual(event['user']['full_name'], reaction_sender.full_name)
self.assertEqual(event['type'], 'reaction')
self.assertEqual(event['op'], 'add')
self.assertEqual(event['message_id'], pm_id)
self.assertEqual(event['emoji_name'], reaction_info['emoji_name'])
self.assertEqual(event['emoji_code'], reaction_info['emoji_code'])
self.assertEqual(event['reaction_type'], reaction_info['reaction_type'])
def test_remove_event(self):
# type: () -> None
"""
Recipients of the message receive the reaction event
and event contains relevant data
"""
pm_sender = self.example_user('hamlet')
pm_recipient = self.example_user('othello')
reaction_sender = pm_recipient
result = self.client_post("/api/v1/messages", {"type": "private",
"content": "Test message",
"to": pm_recipient.email},
**self.api_auth(pm_sender.email))
self.assert_json_success(result)
content = result.json()
pm_id = content['id']
expected_recipient_ids = set([pm_sender.id, pm_recipient.id])
reaction_info = {
'emoji_name': 'hamburger',
'emoji_code': '1f354',
'reaction_type': 'unicode_emoji',
}
add = self.client_post('/api/v1/messages/%s/reactions' % (pm_id,),
reaction_info,
**self.api_auth(reaction_sender.email))
self.assert_json_success(add)
events = [] # type: List[Mapping[str, Any]]
with tornado_redirected_to_list(events):
result = self.client_delete('/api/v1/messages/%s/reactions' % (pm_id,),
reaction_info,
**self.api_auth(reaction_sender.email))
self.assert_json_success(result)
self.assertEqual(len(events), 1)
event = events[0]['event']
event_user_ids = set(events[0]['users'])
self.assertEqual(expected_recipient_ids, event_user_ids)
self.assertEqual(event['user']['user_id'], reaction_sender.id)
self.assertEqual(event['user']['email'], reaction_sender.email)
self.assertEqual(event['user']['full_name'], reaction_sender.full_name)
self.assertEqual(event['type'], 'reaction')
self.assertEqual(event['op'], 'remove')
self.assertEqual(event['message_id'], pm_id)
self.assertEqual(event['emoji_name'], reaction_info['emoji_name'])
self.assertEqual(event['emoji_code'], reaction_info['emoji_code'])
self.assertEqual(event['reaction_type'], reaction_info['reaction_type'])

View File

@ -5,9 +5,10 @@ from typing import Text
from zerver.decorator import \
has_request_variables, REQ, to_non_negative_int
from zerver.lib.actions import do_add_reaction_legacy,\
do_remove_reaction_legacy
from zerver.lib.emoji import check_valid_emoji
from zerver.lib.actions import do_add_reaction, do_add_reaction_legacy,\
do_remove_reaction, do_remove_reaction_legacy
from zerver.lib.emoji import check_emoji_code_consistency,\
check_emoji_name_consistency, check_valid_emoji
from zerver.lib.message import access_message
from zerver.lib.request import JsonableError
from zerver.lib.response import json_success
@ -66,3 +67,76 @@ def remove_reaction_backend(request, user_profile, message_id, emoji_name):
do_remove_reaction_legacy(user_profile, message, emoji_name)
return json_success()
@has_request_variables
def add_reaction(request: HttpRequest, user_profile: UserProfile, message_id: int,
emoji_name: str=REQ(),
emoji_code: str=REQ(),
reaction_type: str=REQ(default="unicode_emoji")) -> HttpResponse:
message, user_message = access_message(user_profile, message_id)
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 precendence 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_code_consistency(message.sender.realm, emoji_code, reaction_type)
check_emoji_name_consistency(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_code: str=REQ(),
reaction_type: str=REQ(default="unicode_emoji")) -> HttpResponse:
message, user_message = access_message(user_profile, message_id)
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()

View File

@ -171,6 +171,12 @@ v1_api_and_json_patterns = [
url(r'users/me/subscriptions/(?P<stream_id>\d+)$', rest_dispatch,
{'PATCH': 'zerver.views.streams.update_subscriptions_property'}),
# New endpoint for handling reactions.
url(r'^messages/(?P<message_id>[0-9]+)/reactions$',
rest_dispatch,
{'POST': 'zerver.views.reactions.add_reaction',
'DELETE': 'zerver.views.reactions.remove_reaction'}),
# reactions -> zerver.view.reactions
# PUT adds a reaction to a message
# DELETE removes a reaction from a message