2022-04-14 23:54:01 +02:00
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
|
|
|
|
from zerver.actions.create_user import create_historical_user_messages
|
2023-07-14 14:25:57 +02:00
|
|
|
from zerver.lib.emoji import check_emoji_request, get_emoji_data
|
2023-07-18 19:33:27 +02:00
|
|
|
from zerver.lib.exceptions import ReactionExistsError
|
2022-04-14 23:54:01 +02:00
|
|
|
from zerver.lib.message import access_message, update_to_dict_cache
|
|
|
|
from zerver.lib.stream_subscription import subscriber_ids_with_stream_history_access
|
|
|
|
from zerver.models import Message, Reaction, Recipient, Stream, UserMessage, UserProfile
|
django_api: Extract send_event_on_commit helper.
django-stubs 4.2.1 gives transaction.on_commit a more accurate type
annotation, but this exposed that mypy can’t handle the lambda default
parameters that we use to recapture loop variables such as
for stream_id in public_stream_ids:
peer_user_ids = …
event = …
transaction.on_commit(
lambda event=event, peer_user_ids=peer_user_ids: send_event(
realm, event, peer_user_ids
)
)
https://github.com/python/mypy/issues/15459
A workaround that mypy accepts is
transaction.on_commit(
(
lambda event, peer_user_ids: lambda: send_event(
realm, event, peer_user_ids
)
)(event, peer_user_ids)
)
But that’s kind of ugly and potentially error-prone, so let’s make a
helper function for this very common pattern.
send_event_on_commit(realm, event, peer_user_ids)
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-06-17 20:53:07 +02:00
|
|
|
from zerver.tornado.django_api import send_event_on_commit
|
2022-04-14 23:54:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
def notify_reaction_update(
|
|
|
|
user_profile: UserProfile, message: Message, reaction: Reaction, op: str
|
|
|
|
) -> None:
|
|
|
|
user_dict = {
|
|
|
|
"user_id": user_profile.id,
|
|
|
|
"email": user_profile.email,
|
|
|
|
"full_name": user_profile.full_name,
|
|
|
|
}
|
|
|
|
|
|
|
|
event: Dict[str, Any] = {
|
|
|
|
"type": "reaction",
|
|
|
|
"op": op,
|
|
|
|
"user_id": user_profile.id,
|
|
|
|
# TODO: We plan to remove this redundant user_dict object once
|
|
|
|
# clients are updated to support accessing use user_id. See
|
|
|
|
# https://github.com/zulip/zulip/pull/14711 for details.
|
|
|
|
"user": user_dict,
|
|
|
|
"message_id": message.id,
|
|
|
|
"emoji_name": reaction.emoji_name,
|
|
|
|
"emoji_code": reaction.emoji_code,
|
|
|
|
"reaction_type": reaction.reaction_type,
|
|
|
|
}
|
|
|
|
|
|
|
|
# Update the cached message since new reaction is added.
|
|
|
|
update_to_dict_cache([message])
|
|
|
|
|
|
|
|
# Recipients for message update events, including reactions, are
|
|
|
|
# everyone who got the original message, plus subscribers of
|
|
|
|
# streams with the access to stream's full history.
|
|
|
|
#
|
|
|
|
# This means reactions won't live-update in preview narrows for a
|
|
|
|
# stream the user isn't yet subscribed to; this is the right
|
|
|
|
# performance tradeoff to avoid sending every reaction to public
|
|
|
|
# stream messages to all users.
|
|
|
|
#
|
|
|
|
# To ensure that reactions do live-update for any user who has
|
|
|
|
# actually participated in reacting to a message, we add a
|
|
|
|
# "historical" UserMessage row for any user who reacts to message,
|
|
|
|
# subscribing them to future notifications, even if they are not
|
|
|
|
# subscribed to the stream.
|
|
|
|
user_ids = set(
|
|
|
|
UserMessage.objects.filter(message=message.id).values_list("user_profile_id", flat=True)
|
|
|
|
)
|
|
|
|
if message.recipient.type == Recipient.STREAM:
|
|
|
|
stream_id = message.recipient.type_id
|
|
|
|
stream = Stream.objects.get(id=stream_id)
|
|
|
|
user_ids |= subscriber_ids_with_stream_history_access(stream)
|
|
|
|
|
django_api: Extract send_event_on_commit helper.
django-stubs 4.2.1 gives transaction.on_commit a more accurate type
annotation, but this exposed that mypy can’t handle the lambda default
parameters that we use to recapture loop variables such as
for stream_id in public_stream_ids:
peer_user_ids = …
event = …
transaction.on_commit(
lambda event=event, peer_user_ids=peer_user_ids: send_event(
realm, event, peer_user_ids
)
)
https://github.com/python/mypy/issues/15459
A workaround that mypy accepts is
transaction.on_commit(
(
lambda event, peer_user_ids: lambda: send_event(
realm, event, peer_user_ids
)
)(event, peer_user_ids)
)
But that’s kind of ugly and potentially error-prone, so let’s make a
helper function for this very common pattern.
send_event_on_commit(realm, event, peer_user_ids)
Signed-off-by: Anders Kaseorg <anders@zulip.com>
2023-06-17 20:53:07 +02:00
|
|
|
send_event_on_commit(user_profile.realm, event, list(user_ids))
|
2022-04-14 23:54:01 +02:00
|
|
|
|
|
|
|
|
|
|
|
def do_add_reaction(
|
|
|
|
user_profile: UserProfile,
|
|
|
|
message: Message,
|
|
|
|
emoji_name: str,
|
|
|
|
emoji_code: str,
|
|
|
|
reaction_type: str,
|
|
|
|
) -> None:
|
|
|
|
"""Should be called while holding a SELECT FOR UPDATE lock
|
|
|
|
(e.g. via access_message(..., lock_message=True)) on the
|
|
|
|
Message row, to prevent race conditions.
|
|
|
|
"""
|
|
|
|
|
|
|
|
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 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, lock_message=True)
|
|
|
|
|
2023-07-14 14:25:57 +02:00
|
|
|
if emoji_code is None or reaction_type is None:
|
2023-08-10 05:59:25 +02:00
|
|
|
emoji_data = get_emoji_data(message.realm_id, emoji_name)
|
2022-04-14 23:54:01 +02:00
|
|
|
|
2023-07-14 14:25:57 +02:00
|
|
|
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_data.emoji_code
|
|
|
|
|
|
|
|
if reaction_type is None:
|
|
|
|
reaction_type = emoji_data.reaction_type
|
2022-04-14 23:54:01 +02:00
|
|
|
|
|
|
|
if Reaction.objects.filter(
|
|
|
|
user_profile=user_profile,
|
|
|
|
message=message,
|
|
|
|
emoji_code=emoji_code,
|
|
|
|
reaction_type=reaction_type,
|
|
|
|
).exists():
|
2023-07-18 19:33:27 +02:00
|
|
|
raise ReactionExistsError
|
2022-04-14 23:54:01 +02:00
|
|
|
|
|
|
|
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 reaction may 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.
|
|
|
|
reaction = query.first()
|
|
|
|
assert reaction is not None
|
|
|
|
emoji_name = reaction.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:
|
|
|
|
# See called function for more context.
|
|
|
|
create_historical_user_messages(user_id=user_profile.id, message_ids=[message.id])
|
|
|
|
|
|
|
|
do_add_reaction(user_profile, message, emoji_name, emoji_code, reaction_type)
|
|
|
|
|
|
|
|
|
|
|
|
def do_remove_reaction(
|
|
|
|
user_profile: UserProfile, message: Message, emoji_code: str, reaction_type: str
|
|
|
|
) -> None:
|
|
|
|
"""Should be called while holding a SELECT FOR UPDATE lock
|
|
|
|
(e.g. via access_message(..., lock_message=True)) on the
|
|
|
|
Message row, to prevent race conditions.
|
|
|
|
"""
|
|
|
|
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")
|