search: Add server support for has:reaction search operator.

Web app support will be a follow-up commit.
This commit is contained in:
Kenneth Rodrigues 2024-04-02 09:54:20 +05:30 committed by Tim Abbott
parent 436dab0e01
commit c3408b56f0
6 changed files with 99 additions and 7 deletions

View File

@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 9.0 ## Changes in Zulip 9.0
**Feature level 249**
* [`GET /messages`](/api/get-messages), [`GET
/messages/matches_narrow`](/api/check-messages-match-narrow): Added
new `has:reaction` search operator, matching messages with at least
one emoji reaction.
**Feature level 248** **Feature level 248**
* [`POST /typing`](/api/set-typing-status), [`POST /messages`](/api/send-message), * [`POST /typing`](/api/set-typing-status), [`POST /messages`](/api/send-message),

View File

@ -51,7 +51,11 @@ important optimization when fetching messages in certain cases (e.g.
when [adding the `read` flag to a user's personal when [adding the `read` flag to a user's personal
messages](/api/update-message-flags-for-narrow)). messages](/api/update-message-flags-for-narrow)).
**Changes**: In Zulip 7.0 (feature level 177), support was added **Changes**: In Zulip 9.0 (feature level 249), narrows gained support
for a new filter `has:reaction`. This allows clients to retrieve only
messages that have at least one reaction.
In Zulip 7.0 (feature level 177), support was added
for three filters related to direct messages: `is:dm`, `dm` and for three filters related to direct messages: `is:dm`, `dm` and
`dm-including`. The `dm` operator replaced and deprecated the `dm-including`. The `dm` operator replaced and deprecated the
`pm-with` operator. The `is:dm` filter replaced and deprecated `pm-with` operator. The `is:dm` filter replaced and deprecated

View File

@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3"
# Changes should be accompanied by documentation explaining what the # Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**" # new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`. # entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 248 API_FEATURE_LEVEL = 249
# Bump the minor PROVISION_VERSION to indicate that folks should provision # Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump # only when going from an old version of the code to a newer version. Bump

View File

@ -333,8 +333,24 @@ class NarrowBuilder:
return method(query, operand, maybe_negate) return method(query, operand, maybe_negate)
def by_has(self, query: Select, operand: str, maybe_negate: ConditionTransform) -> Select: def by_has(self, query: Select, operand: str, maybe_negate: ConditionTransform) -> Select:
if operand not in ["attachment", "image", "link"]: if operand not in ["attachment", "image", "link", "reaction"]:
raise BadNarrowOperatorError("unknown 'has' operand " + operand) raise BadNarrowOperatorError("unknown 'has' operand " + operand)
if operand == "reaction":
if self.msg_id_column.name == "message_id":
# If the initial query uses `zerver_usermessage`
check_col = literal_column("zerver_usermessage.message_id", Integer)
else:
# If the initial query doesn't use `zerver_usermessage`
check_col = literal_column("zerver_message.id", Integer)
exists_cond = (
select([1])
.select_from(table("zerver_reaction"))
.where(check_col == literal_column("zerver_reaction.message_id", Integer))
.exists()
)
return query.where(maybe_negate(exists_cond))
col_name = "has_" + operand col_name = "has_" + operand
cond = column(col_name, Boolean) cond = column(col_name, Boolean)
return query.where(maybe_negate(cond)) return query.where(maybe_negate(cond))

View File

@ -6189,7 +6189,10 @@ paths:
subscribed to appropriate streams or use a shared history subscribed to appropriate streams or use a shared history
search narrow with this endpoint. search narrow with this endpoint.
**Changes**: In Zulip 7.0 (feature level 177), narrows gained support **Changes**: In Zulip 9.0 (feature level 249), added new `has:reaction`
filter, matching messages with at least one emoji reaction.
In Zulip 7.0 (feature level 177), narrows gained support
for three new filters related to direct messages: `is:dm`, `dm` and for three new filters related to direct messages: `is:dm`, `dm` and
`dm-including`; replacing and deprecating `is:private`, `pm-with` and `dm-including`; replacing and deprecating `is:private`, `pm-with` and
`group-pm-with` respectively. `group-pm-with` respectively.
@ -7059,7 +7062,10 @@ paths:
optimization. Including that filter takes advantage of the fact that optimization. Including that filter takes advantage of the fact that
the server has a database index for unread messages. the server has a database index for unread messages.
**Changes**: In Zulip 7.0 (feature level 177), narrows gained support **Changes**: In Zulip 9.0 (feature level 249), added new `has:reaction`
filter, matching messages with at least one emoji reaction.
In Zulip 7.0 (feature level 177), narrows gained support
for three new filters related to direct messages: `is:dm`, `dm` and for three new filters related to direct messages: `is:dm`, `dm` and
`dm-including`; replacing and deprecating `is:private`, `pm-with` and `dm-including`; replacing and deprecating `is:private`, `pm-with` and
`group-pm-with` respectively. `group-pm-with` respectively.
@ -7483,7 +7489,10 @@ paths:
A structure defining the narrow to check against. See how to A structure defining the narrow to check against. See how to
[construct a narrow](/api/construct-narrow). [construct a narrow](/api/construct-narrow).
**Changes**: In Zulip 7.0 (feature level 177), narrows gained support **Changes**: In Zulip 9.0 (feature level 249), added new `has:reaction`
filter, matching messages with at least one emoji reaction.
In Zulip 7.0 (feature level 177), narrows gained support
for three new filters related to direct messages: `is:dm`, `dm` and for three new filters related to direct messages: `is:dm`, `dm` and
`dm-including`; replacing and deprecating `is:private`, `pm-with` and `dm-including`; replacing and deprecating `is:private`, `pm-with` and
`group-pm-with` respectively. `group-pm-with` respectively.
@ -21361,7 +21370,10 @@ components:
Defaults to `[]`. Defaults to `[]`.
**Changes**: In Zulip 7.0 (feature level 177), narrows gained support **Changes**: In Zulip 9.0 (feature level 249), added new `has:reaction`
filter, matching messages with at least one emoji reaction.
In Zulip 7.0 (feature level 177), narrows gained support
for three new filters related to direct messages: `is:dm`, `dm` and for three new filters related to direct messages: `is:dm`, `dm` and
`dm-including`; replacing and deprecating `is:private`, `pm-with` and `dm-including`; replacing and deprecating `is:private`, `pm-with` and
`group-pm-with` respectively. `group-pm-with` respectively.

View File

@ -13,6 +13,7 @@ from typing_extensions import override
from analytics.lib.counts import COUNT_STATS from analytics.lib.counts import COUNT_STATS
from analytics.models import RealmCount from analytics.models import RealmCount
from zerver.actions.message_edit import do_update_message from zerver.actions.message_edit import do_update_message
from zerver.actions.reactions import check_add_reaction
from zerver.actions.realm_settings import do_set_realm_property from zerver.actions.realm_settings import do_set_realm_property
from zerver.actions.uploads import do_claim_attachments from zerver.actions.uploads import do_claim_attachments
from zerver.actions.user_settings import do_change_user_setting from zerver.actions.user_settings import do_change_user_setting
@ -491,6 +492,20 @@ class NarrowBuilderTest(ZulipTestCase):
term = dict(operator="has", operand="link", negated=True) term = dict(operator="has", operand="link", negated=True)
self._do_add_term_test(term, "WHERE NOT has_link") self._do_add_term_test(term, "WHERE NOT has_link")
def test_add_term_using_has_operator_and_reaction_operand(self) -> None:
term = dict(operator="has", operand="reaction")
self._do_add_term_test(
term,
"EXISTS (SELECT 1 \nFROM zerver_reaction \nWHERE zerver_message.id = zerver_reaction.message_id)",
)
def test_add_term_using_has_operator_and_reaction_operand_and_negated(self) -> None:
term = dict(operator="has", operand="reaction", negated=True)
self._do_add_term_test(
term,
"NOT (EXISTS (SELECT 1 \nFROM zerver_reaction \nWHERE zerver_message.id = zerver_reaction.message_id))",
)
def test_add_term_using_has_operator_non_supported_operand_should_raise_error(self) -> None: def test_add_term_using_has_operator_non_supported_operand_should_raise_error(self) -> None:
term = dict(operator="has", operand="non_supported") term = dict(operator="has", operand="non_supported")
self.assertRaises(BadNarrowOperatorError, self._build_query, term) self.assertRaises(BadNarrowOperatorError, self._build_query, term)
@ -4422,6 +4437,44 @@ class MessageHasKeywordsTest(ZulipTestCase):
self.assertFalse(m.called) self.assertFalse(m.called)
m.reset_mock() m.reset_mock()
def test_has_reaction(self) -> None:
self.login("iago")
has_reaction_narrow = orjson.dumps([dict(operator="has", operand="reaction")]).decode()
msg_id = self.send_stream_message(self.example_user("hamlet"), "Denmark", content="Hey")
result = self.client_get(
"/json/messages",
dict(narrow=has_reaction_narrow, anchor=msg_id, num_before=0, num_after=0),
)
messages = self.assert_json_success(result)["messages"]
self.assert_length(messages, 0)
check_add_reaction(
self.example_user("hamlet"), msg_id, "hamburger", "1f354", "unicode_emoji"
)
result = self.client_get(
"/json/messages",
dict(narrow=has_reaction_narrow, anchor=msg_id, num_before=0, num_after=0),
)
messages = self.assert_json_success(result)["messages"]
self.assert_length(messages, 1)
msg_id = self.send_personal_message(
self.example_user("iago"), self.example_user("cordelia"), "Hello Cordelia"
)
result = self.client_get(
"/json/messages",
dict(narrow=has_reaction_narrow, anchor=msg_id, num_before=0, num_after=0),
)
messages = self.assert_json_success(result)["messages"]
self.assert_length(messages, 0)
check_add_reaction(self.example_user("iago"), msg_id, "hamburger", "1f354", "unicode_emoji")
result = self.client_get(
"/json/messages",
dict(narrow=has_reaction_narrow, anchor=msg_id, num_before=0, num_after=0),
)
messages = self.assert_json_success(result)["messages"]
self.assert_length(messages, 1)
class MessageVisibilityTest(ZulipTestCase): class MessageVisibilityTest(ZulipTestCase):
def test_update_first_visible_message_id(self) -> None: def test_update_first_visible_message_id(self) -> None: