snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
190
zerver/actions/reactions.py
Normal file
190
zerver/actions/reactions.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from typing import Literal
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
from zerver.actions.user_topics import do_set_user_topic_visibility_policy
|
||||
from zerver.lib.emoji import check_emoji_request, get_emoji_data
|
||||
from zerver.lib.exceptions import ReactionExistsError
|
||||
from zerver.lib.message import (
|
||||
access_message_and_usermessage,
|
||||
event_recipient_ids_for_action_on_messages,
|
||||
set_visibility_policy_possible,
|
||||
should_change_visibility_policy,
|
||||
visibility_policy_for_participation,
|
||||
)
|
||||
from zerver.lib.message_cache import update_message_cache
|
||||
from zerver.lib.streams import access_stream_by_id
|
||||
from zerver.lib.user_message import create_historical_user_messages
|
||||
from zerver.models import Message, Reaction, UserProfile
|
||||
from zerver.tornado.django_api import send_event_on_commit
|
||||
|
||||
|
||||
class ReactionEvent(TypedDict):
|
||||
type: Literal["reaction"]
|
||||
op: Literal["add", "remove"]
|
||||
user_id: int
|
||||
message_id: int
|
||||
emoji_name: str
|
||||
emoji_code: str
|
||||
reaction_type: str
|
||||
|
||||
|
||||
def notify_reaction_update(
|
||||
user_profile: UserProfile,
|
||||
message: Message,
|
||||
reaction: Reaction,
|
||||
op: Literal["add", "remove"],
|
||||
) -> None:
|
||||
event: ReactionEvent = {
|
||||
"type": "reaction",
|
||||
"op": op,
|
||||
"user_id": user_profile.id,
|
||||
"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_message_cache([message])
|
||||
|
||||
user_ids = event_recipient_ids_for_action_on_messages([message.id], message.is_channel_message)
|
||||
send_event_on_commit(user_profile.realm, event, list(user_ids))
|
||||
|
||||
|
||||
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()
|
||||
|
||||
# Determine and set the visibility_policy depending on 'automatically_follow_topics_policy'
|
||||
# and 'automatically_unmute_topics_in_muted_streams_policy'.
|
||||
if set_visibility_policy_possible(
|
||||
user_profile, message
|
||||
) and UserProfile.AUTOMATICALLY_CHANGE_VISIBILITY_POLICY_ON_PARTICIPATION in [
|
||||
user_profile.automatically_follow_topics_policy,
|
||||
user_profile.automatically_unmute_topics_in_muted_streams_policy,
|
||||
]:
|
||||
stream_id = message.recipient.type_id
|
||||
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
||||
assert stream is not None
|
||||
if sub:
|
||||
new_visibility_policy = visibility_policy_for_participation(user_profile, sub.is_muted)
|
||||
if new_visibility_policy and should_change_visibility_policy(
|
||||
new_visibility_policy,
|
||||
user_profile,
|
||||
stream_id,
|
||||
topic_name=message.topic_name(),
|
||||
):
|
||||
do_set_user_topic_visibility_policy(
|
||||
user_profile=user_profile,
|
||||
stream=stream,
|
||||
topic_name=message.topic_name(),
|
||||
visibility_policy=new_visibility_policy,
|
||||
)
|
||||
|
||||
notify_reaction_update(user_profile, message, reaction, "add")
|
||||
|
||||
|
||||
def check_add_reaction(
|
||||
user_profile: UserProfile,
|
||||
message_id: int,
|
||||
emoji_name: str,
|
||||
emoji_code: str | None,
|
||||
reaction_type: str | None,
|
||||
) -> None:
|
||||
message, user_message = access_message_and_usermessage(
|
||||
user_profile, message_id, lock_message=True, is_modifying_message=True
|
||||
)
|
||||
|
||||
if emoji_code is None or reaction_type is None:
|
||||
emoji_data = get_emoji_data(message.realm_id, emoji_name)
|
||||
|
||||
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
|
||||
|
||||
if Reaction.objects.filter(
|
||||
user_profile=user_profile,
|
||||
message=message,
|
||||
emoji_code=emoji_code,
|
||||
reaction_type=reaction_type,
|
||||
).exists():
|
||||
raise ReactionExistsError
|
||||
|
||||
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")
|
||||
Reference in New Issue
Block a user