From c3408b56f0db046a95f56126815adc655bf4fe33 Mon Sep 17 00:00:00 2001 From: Kenneth Rodrigues Date: Tue, 2 Apr 2024 09:54:20 +0530 Subject: [PATCH] search: Add server support for has:reaction search operator. Web app support will be a follow-up commit. --- api_docs/changelog.md | 7 ++++ api_docs/construct-narrow.md | 6 +++- version.py | 2 +- zerver/lib/narrow.py | 18 +++++++++- zerver/openapi/zulip.yaml | 20 ++++++++--- zerver/tests/test_message_fetch.py | 53 ++++++++++++++++++++++++++++++ 6 files changed, 99 insertions(+), 7 deletions(-) diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 15f23340c9..3f7a281717 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with. ## 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** * [`POST /typing`](/api/set-typing-status), [`POST /messages`](/api/send-message), diff --git a/api_docs/construct-narrow.md b/api_docs/construct-narrow.md index 2bb4707274..b7d34d7f72 100644 --- a/api_docs/construct-narrow.md +++ b/api_docs/construct-narrow.md @@ -51,7 +51,11 @@ important optimization when fetching messages in certain cases (e.g. when [adding the `read` flag to a user's personal 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 `dm-including`. The `dm` operator replaced and deprecated the `pm-with` operator. The `is:dm` filter replaced and deprecated diff --git a/version.py b/version.py index 360c76ae0c..d1655e81c5 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.9.3" # Changes should be accompanied by documentation explaining what the # new level means in api_docs/changelog.md, as well as "**Changes**" # 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 # only when going from an old version of the code to a newer version. Bump diff --git a/zerver/lib/narrow.py b/zerver/lib/narrow.py index 15425de48b..0467d8e8c1 100644 --- a/zerver/lib/narrow.py +++ b/zerver/lib/narrow.py @@ -333,8 +333,24 @@ class NarrowBuilder: return method(query, operand, maybe_negate) 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) + + 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 cond = column(col_name, Boolean) return query.where(maybe_negate(cond)) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 07f5aa7b04..9359cd5ad3 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -6189,7 +6189,10 @@ paths: subscribed to appropriate streams or use a shared history 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 `dm-including`; replacing and deprecating `is:private`, `pm-with` and `group-pm-with` respectively. @@ -7059,7 +7062,10 @@ paths: optimization. Including that filter takes advantage of the fact that 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 `dm-including`; replacing and deprecating `is:private`, `pm-with` and `group-pm-with` respectively. @@ -7483,7 +7489,10 @@ paths: A structure defining the narrow to check against. See how to [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 `dm-including`; replacing and deprecating `is:private`, `pm-with` and `group-pm-with` respectively. @@ -21361,7 +21370,10 @@ components: 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 `dm-including`; replacing and deprecating `is:private`, `pm-with` and `group-pm-with` respectively. diff --git a/zerver/tests/test_message_fetch.py b/zerver/tests/test_message_fetch.py index 5e54cd51db..15fdd1a8f2 100644 --- a/zerver/tests/test_message_fetch.py +++ b/zerver/tests/test_message_fetch.py @@ -13,6 +13,7 @@ from typing_extensions import override from analytics.lib.counts import COUNT_STATS from analytics.models import RealmCount 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.uploads import do_claim_attachments 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) 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: term = dict(operator="has", operand="non_supported") self.assertRaises(BadNarrowOperatorError, self._build_query, term) @@ -4422,6 +4437,44 @@ class MessageHasKeywordsTest(ZulipTestCase): self.assertFalse(m.called) 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): def test_update_first_visible_message_id(self) -> None: