mirror of https://github.com/zulip/zulip.git
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:
parent
40fca20c51
commit
d9c2f613e3
|
@ -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'],
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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'])
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue