diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 5fdf340a35..0ab9db84df 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -20,6 +20,15 @@ format used by the Zulip server that they are interacting with. ## Changes in Zulip 7.0 +**Feature level 174**: + +* [`POST /typing`](/api/set-typing-status), [`POST /messages`](/api/send-message): + Added `"direct"` as the preferred way to indicate a direct message for the + `type` parameter, deprecating the original `"private"`. While `"private"` + is still supported for direct messages, clients are encouraged to use to + the modern convention with servers that support it, because support for + `"private"` will eventually be removed. + **Feature level 173**: * [`GET /scheduled_messages`](/api/get-scheduled-messages), [`DELETE diff --git a/api_docs/send-message.md b/api_docs/send-message.md index c71e6211f3..c256b86380 100644 --- a/api_docs/send-message.md +++ b/api_docs/send-message.md @@ -19,10 +19,10 @@ curl -X POST {{ api_url }}/v1/messages \ --data-urlencode topic=Castle \ --data-urlencode 'content=I come not, friends, to steal away your hearts.' -# For private messages +# For direct messages curl -X POST {{ api_url }}/v1/messages \ -u BOT_EMAIL_ADDRESS:BOT_API_KEY \ - --data-urlencode type=private \ + --data-urlencode type=direct \ --data-urlencode 'to=[9]' \ --data-urlencode 'content=With mirth and laughter let old wrinkles come.' ``` @@ -38,7 +38,7 @@ the command-line, providing the message content via STDIN. zulip-send --stream Denmark --subject Castle \ --user othello-bot@example.com --api-key a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 -# For private messages +# For direct messages zulip-send hamlet@example.com \ --user othello-bot@example.com --api-key a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 ``` diff --git a/version.py b/version.py index b016fcb542..c6ffa74973 100644 --- a/version.py +++ b/version.py @@ -33,7 +33,7 @@ DESKTOP_WARNING_VERSION = "5.4.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 = 173 +API_FEATURE_LEVEL = 174 # 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/models.py b/zerver/models.py index 8a0678ff6c..1a89e9288a 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -3023,6 +3023,16 @@ class ArchivedMessage(AbstractMessage): class Message(AbstractMessage): + # Recipient types used when a Message object is provided to + # Zulip clients via the API. + # + # A detail worth noting: + # * "direct" was introduced in 2023 with the goal of + # deprecating the original "private" and becoming the + # preferred way to indicate a personal or huddle + # Recipient type via the API. + API_RECIPIENT_TYPES = ["direct", "private", "stream"] + search_tsvector = SearchVectorField(null=True) def topic_name(self) -> str: diff --git a/zerver/openapi/javascript_examples.js b/zerver/openapi/javascript_examples.js index 5db423b9e0..f371e3ab4c 100644 --- a/zerver/openapi/javascript_examples.js +++ b/zerver/openapi/javascript_examples.js @@ -113,11 +113,11 @@ add_example("send_message", "/messages:post", 200, async (client, console) => { }; console.log(await client.messages.send(params)); - // Send a private message + // Send a direct message const user_id = 9; params = { to: [user_id], - type: "private", + type: "direct", content: "With mirth and laughter let old wrinkles come.", }; console.log(await client.messages.send(params)); @@ -252,7 +252,8 @@ add_example("set_typing_status", "/typing:post", 200, async (client, console) => to: [user_id1, user_id2], }; - // The user has started to type in the group PM with Iago and Polonius + // The user has started typing in the group direct message + // with Iago and Polonius console.log(await client.typing.send(typingParams)); // {code_example|end} }); diff --git a/zerver/openapi/python_examples.py b/zerver/openapi/python_examples.py index 03f2b8453d..e6533a2c77 100644 --- a/zerver/openapi/python_examples.py +++ b/zerver/openapi/python_examples.py @@ -954,6 +954,7 @@ def send_message(client: Client) -> int: "content": "I come not, friends, to steal away your hearts.", } result = client.send_message(request) + # {code_example|end} validate_against_openapi_schema(result, "/messages", "post", "200") @@ -971,7 +972,7 @@ def send_message(client: Client) -> int: ensure_users([10], ["hamlet"]) # {code_example|start} - # Send a private message + # Send a direct message user_id = 10 request = { "type": "private", @@ -979,6 +980,7 @@ def send_message(client: Client) -> int: "content": "With mirth and laughter let old wrinkles come.", } result = client.send_message(request) + # {code_example|end} validate_against_openapi_schema(result, "/messages", "post", "200") @@ -1294,7 +1296,8 @@ def set_typing_status(client: Client) -> None: ensure_users([10, 11], ["hamlet", "iago"]) # {code_example|start} - # The user has started to type in the group PM with Iago and Polonius + # The user has started typing in the group direct message + # with Iago and Polonius user_id1 = 10 user_id2 = 11 @@ -1309,7 +1312,8 @@ def set_typing_status(client: Client) -> None: validate_against_openapi_schema(result, "/typing", "post", "200") # {code_example|start} - # The user has finished typing in the group PM with Iago and Polonius + # The user has finished typing in the group direct message + # with Iago and Polonius user_id1 = 10 user_id2 = 11 @@ -1324,7 +1328,8 @@ def set_typing_status(client: Client) -> None: validate_against_openapi_schema(result, "/typing", "post", "200") # {code_example|start} - # The user has started to type in topic "typing status" of stream "Denmark" + # The user has started to type in topic "typing status" + # of stream "Denmark" stream_id = client.get_stream_id("Denmark")["stream_id"] topic = "typing status" @@ -1341,7 +1346,8 @@ def set_typing_status(client: Client) -> None: validate_against_openapi_schema(result, "/typing", "post", "200") # {code_example|start} - # The user has finished typing in topic "typing status" of stream "Denmark" + # The user has finished typing in topic "typing status" + # of stream "Denmark" stream_id = client.get_stream_id("Denmark")["stream_id"] topic = "typing status" diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index 359165169c..631aa56276 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -5502,25 +5502,35 @@ paths: summary: Send a message tags: ["messages"] description: | - Send a stream or a private message. + Send a [stream message](/help/streams-and-topics) or a + [direct message](/help/direct-messages). parameters: - name: type in: query description: | - The type of message to be sent. `private` for a private message and - `stream` for a stream message. + The type of message to be sent. + + `"direct"` for a direct message and `"stream"` for a stream message. + + **Changes**: In Zulip 7.0 (feature level 174), `"direct"` was added as + the preferred way to request a direct message, deprecating the original + `"private"`. While `"private"` is still supported for requesting direct + messages, clients are encouraged to use to the modern convention with + servers that support it, because support for `"private"` will eventually + be removed. schema: type: string enum: - - private + - direct - stream - example: private + - private + example: direct required: true - name: to in: query description: | For stream messages, either the name or integer ID of the stream. For - private messages, either a list containing integer user IDs or a list + direct messages, either a list containing integer user IDs or a list containing string email addresses. **Changes**: Support for using user/stream IDs was added in Zulip 2.0.0. @@ -5624,7 +5634,7 @@ paths: "result": "error", } description: | - A typical failed JSON response for when a private message is sent to a user + A typical failed JSON response for when a direct message is sent to a user that does not exist: /messages/{message_id}/history: get: @@ -15056,7 +15066,7 @@ paths: summary: Set "typing" status tags: ["users"] description: | - Notify other users whether the current user is typing a message. + Notify other users whether the current user is [typing a message](/help/typing-notifications). Clients implementing Zulip's typing notifications protocol should work as follows: @@ -15100,15 +15110,21 @@ paths: description: | Type of the message being composed. - **Changes**: New in Zulip 4.0 (feature level 58). Previously, typing - notifications were only for private messages. + **Changes**: In Zulip 7.0 (feature level 174), `"direct"` was added + as the preferred way to indicate a direct message is being composed, + becoming the default value for this parameter and deprecating the + original `"private"`. + + New in Zulip 4.0 (feature level 58). Previously, typing notifications + were only for direct messages. schema: type: string enum: - - private + - direct - stream - default: private - example: private + - private + default: direct + example: direct - name: op in: query description: | @@ -15123,16 +15139,16 @@ paths: - name: to in: query description: | - For `"private"` type it is the user_ids of the recipients of the message being typed. - Send a JSON-encoded list of user_ids. (Use a list even if there is only one - recipient.) + For `"direct"` type it is the user IDs of the recipients of the message + being typed. Send a JSON-encoded list of user IDs. (Use a list even if + there is only one recipient.) For `"stream"` type it is a single element list containing ID of stream in which the message is being typed. **Changes**: Support for typing notifications for stream messages is new in Zulip 4.0 (feature level 58). Previously, typing - notifications were only for private messages. + notifications were only for direct messages. Before Zulip 2.0.0, this parameter accepted only a JSON-encoded list of email addresses. Support for the email address-based format was @@ -15149,10 +15165,10 @@ paths: in: query description: | Topic to which message is being typed. Required for the `"stream"` type. - Ignored in the case of `"private"` type. + Ignored in the case of `"direct"` type. **Changes**: New in Zulip 4.0 (feature level 58). Previously, typing - notifications were only for private messages. + notifications were only for direct messages. schema: type: string example: typing notifications diff --git a/zerver/tests/test_message_send.py b/zerver/tests/test_message_send.py index 78810d2e0b..1469ce8abe 100644 --- a/zerver/tests/test_message_send.py +++ b/zerver/tests/test_message_send.py @@ -586,7 +586,7 @@ class MessagePOSTTest(ZulipTestCase): result = self.client_post( "/json/messages", { - "type": "private", + "type": "direct", "content": "Test message", "to": orjson.dumps([othello.email]).decode(), }, @@ -605,7 +605,7 @@ class MessagePOSTTest(ZulipTestCase): result = self.client_post( "/json/messages", { - "type": "private", + "type": "direct", "content": "Test message", "to": orjson.dumps([user_profile.email]).decode(), }, @@ -628,52 +628,60 @@ class MessagePOSTTest(ZulipTestCase): def test_personal_message_by_id(self) -> None: """ - Sending a personal message to a valid user ID is successful. + Sending a personal message to a valid user ID is successful + for both valid strings for `type` parameter. """ self.login("hamlet") - result = self.client_post( - "/json/messages", - { - "type": "private", - "content": "Test message", - "to": orjson.dumps([self.example_user("othello").id]).decode(), - }, - ) - self.assert_json_success(result) + recipient_type_name = ["direct", "private"] - msg = self.get_last_message() - self.assertEqual("Test message", msg.content) - self.assertEqual(msg.recipient_id, self.example_user("othello").recipient_id) + for type in recipient_type_name: + result = self.client_post( + "/json/messages", + { + "type": type, + "content": "Test message", + "to": orjson.dumps([self.example_user("othello").id]).decode(), + }, + ) + self.assert_json_success(result) + + msg = self.get_last_message() + self.assertEqual("Test message", msg.content) + self.assertEqual(msg.recipient_id, self.example_user("othello").recipient_id) def test_group_personal_message_by_id(self) -> None: """ - Sending a personal message to a valid user ID is successful. + Sending a personal message to a valid user ID is successful + for both valid strings for `type` parameter. """ self.login("hamlet") - result = self.client_post( - "/json/messages", - { - "type": "private", - "content": "Test message", - "to": orjson.dumps( - [self.example_user("othello").id, self.example_user("cordelia").id] - ).decode(), - }, - ) - self.assert_json_success(result) + recipient_type_name = ["direct", "private"] - msg = self.get_last_message() - self.assertEqual("Test message", msg.content) - self.assertEqual( - msg.recipient_id, - get_huddle_recipient( + for type in recipient_type_name: + result = self.client_post( + "/json/messages", { - self.example_user("hamlet").id, - self.example_user("othello").id, - self.example_user("cordelia").id, - } - ).id, - ) + "type": type, + "content": "Test message", + "to": orjson.dumps( + [self.example_user("othello").id, self.example_user("cordelia").id] + ).decode(), + }, + ) + self.assert_json_success(result) + + msg = self.get_last_message() + self.assertEqual("Test message", msg.content) + self.assertEqual( + msg.recipient_id, + get_huddle_recipient( + { + self.example_user("hamlet").id, + self.example_user("othello").id, + self.example_user("cordelia").id, + } + ).id, + ) def test_personal_message_copying_self(self) -> None: """ @@ -686,7 +694,7 @@ class MessagePOSTTest(ZulipTestCase): result = self.client_post( "/json/messages", { - "type": "private", + "type": "direct", "content": "Test message", "to": orjson.dumps([hamlet.id, othello.id]).decode(), }, @@ -704,7 +712,7 @@ class MessagePOSTTest(ZulipTestCase): result = self.client_post( "/json/messages", { - "type": "private", + "type": "direct", "content": "Test message", "to": "nonexistent", }, @@ -723,7 +731,7 @@ class MessagePOSTTest(ZulipTestCase): result = self.client_post( "/json/messages", { - "type": "private", + "type": "direct", "content": "Test message", "to": orjson.dumps([othello.id]).decode(), }, @@ -733,7 +741,7 @@ class MessagePOSTTest(ZulipTestCase): result = self.client_post( "/json/messages", { - "type": "private", + "type": "direct", "content": "Test message", "to": orjson.dumps([othello.id, cordelia.id]).decode(), }, @@ -754,7 +762,7 @@ class MessagePOSTTest(ZulipTestCase): "to": othello.email, }, ) - self.assert_json_error(result, "Invalid message type") + self.assert_json_error(result, "Invalid type") def test_empty_message(self) -> None: """ @@ -764,7 +772,7 @@ class MessagePOSTTest(ZulipTestCase): othello = self.example_user("othello") result = self.client_post( "/json/messages", - {"type": "private", "content": " ", "to": othello.email}, + {"type": "direct", "content": " ", "to": othello.email}, ) self.assert_json_error(result, "Message must not be empty") @@ -824,9 +832,9 @@ class MessagePOSTTest(ZulipTestCase): ) self.assert_json_error(result, "Invalid character in topic, at position 5!") - def test_invalid_message_type(self) -> None: + def test_invalid_recipient_type(self) -> None: """ - Messages other than the type of "private" or "stream" are considered as invalid + Messages other than the type of "direct", "private" or "stream" are invalid. """ self.login("hamlet") result = self.client_post( @@ -838,7 +846,7 @@ class MessagePOSTTest(ZulipTestCase): "topic": "Test topic", }, ) - self.assert_json_error(result, "Invalid message type") + self.assert_json_error(result, "Invalid type") def test_private_message_without_recipients(self) -> None: """ @@ -847,7 +855,7 @@ class MessagePOSTTest(ZulipTestCase): self.login("hamlet") result = self.client_post( "/json/messages", - {"type": "private", "content": "Test content", "to": ""}, + {"type": "direct", "content": "Test content", "to": ""}, ) self.assert_json_error(result, "Message must have recipients") @@ -859,7 +867,7 @@ class MessagePOSTTest(ZulipTestCase): self.mit_user("starnine"), "/api/v1/messages", { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -879,7 +887,7 @@ class MessagePOSTTest(ZulipTestCase): self.mit_user("starnine"), "/api/v1/messages", { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -898,7 +906,7 @@ class MessagePOSTTest(ZulipTestCase): result = self.client_post( "/json/messages", { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -916,7 +924,7 @@ class MessagePOSTTest(ZulipTestCase): self.mit_user("starnine"), "/api/v1/messages", { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -931,7 +939,7 @@ class MessagePOSTTest(ZulipTestCase): Sending two mirrored huddles in the row return the same ID """ msg = { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -1065,7 +1073,7 @@ class MessagePOSTTest(ZulipTestCase): self.mit_user("starnine"), "/api/v1/messages", { - "type": "private", + "type": "direct", "content": "Test message", "client": "zephyr_mirror", "to": self.mit_email("starnine"), @@ -1079,7 +1087,7 @@ class MessagePOSTTest(ZulipTestCase): self.mit_user("starnine"), "/api/v1/messages", { - "type": "not-private", + "type": "stream", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -1098,7 +1106,7 @@ class MessagePOSTTest(ZulipTestCase): self.mit_user("starnine"), "/api/v1/messages", { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -1120,7 +1128,7 @@ class MessagePOSTTest(ZulipTestCase): user, "/api/v1/messages", { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", @@ -1141,7 +1149,7 @@ class MessagePOSTTest(ZulipTestCase): user, "/api/v1/messages", { - "type": "private", + "type": "direct", "sender": self.mit_email("sipbtest"), "content": "Test message", "client": "zephyr_mirror", diff --git a/zerver/tests/test_typing.py b/zerver/tests/test_typing.py index b78e0c672a..1cf1f51fe4 100644 --- a/zerver/tests/test_typing.py +++ b/zerver/tests/test_typing.py @@ -132,6 +132,23 @@ class TypingValidateToArgumentsTest(ZulipTestCase): class TypingHappyPathTestPMs(ZulipTestCase): + def test_valid_type_and_op_parameters(self) -> None: + recipient_type_name = ["direct", "private"] + operator_type = ["start", "stop"] + sender = self.example_user("hamlet") + recipient_user = self.example_user("othello") + + for type in recipient_type_name: + for operator in operator_type: + params = dict( + to=orjson.dumps([recipient_user.id]).decode(), + op=operator, + type=type, + ) + + result = self.api_post(sender, "/api/v1/typing", params) + self.assert_json_success(result) + def test_start_to_single_recipient(self) -> None: sender = self.example_user("hamlet") recipient_user = self.example_user("othello") diff --git a/zerver/views/message_send.py b/zerver/views/message_send.py index fabcee0601..07fcf2f704 100644 --- a/zerver/views/message_send.py +++ b/zerver/views/message_send.py @@ -24,7 +24,7 @@ from zerver.lib.request import REQ, RequestNotes, has_request_variables from zerver.lib.response import json_success from zerver.lib.timestamp import convert_to_UTC from zerver.lib.topic import REQ_topic -from zerver.lib.validator import check_int, to_float +from zerver.lib.validator import check_int, check_string_in, to_float from zerver.lib.zcommand import process_zcommands from zerver.lib.zephyr import compute_mit_user_fullname from zerver.models import ( @@ -193,7 +193,7 @@ def handle_deferred_message( def send_message_backend( request: HttpRequest, user_profile: UserProfile, - message_type_name: str = REQ("type"), + req_type: str = REQ("type", str_validator=check_string_in(Message.API_RECIPIENT_TYPES)), req_to: Optional[str] = REQ("to", default=None), req_sender: Optional[str] = REQ("sender", default=None, documentation_pending=True), forged_str: Optional[str] = REQ("forged", default=None, documentation_pending=True), @@ -210,7 +210,12 @@ def send_message_backend( tz_guess: Optional[str] = REQ("tz_guess", default=None, documentation_pending=True), time: Optional[float] = REQ(default=None, converter=to_float, documentation_pending=True), ) -> HttpResponse: - recipient_type_name = message_type_name + recipient_type_name = req_type + if recipient_type_name == "direct": + # For now, use "private" from Message.API_RECIPIENT_TYPES. + # TODO: Use "direct" here, as well as in events and + # message (created, schdeduled, drafts) objects/dicts. + recipient_type_name = "private" # If req_to is None, then we default to an # empty list of recipients. diff --git a/zerver/views/typing.py b/zerver/views/typing.py index 7c991dc52c..bc1ca7912f 100644 --- a/zerver/views/typing.py +++ b/zerver/views/typing.py @@ -9,18 +9,17 @@ from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.streams import access_stream_by_id, access_stream_for_send_message from zerver.lib.validator import check_int, check_list, check_string_in -from zerver.models import UserProfile +from zerver.models import Message, UserProfile VALID_OPERATOR_TYPES = ["start", "stop"] -VALID_MESSAGE_TYPES = ["private", "stream"] @has_request_variables def send_notification_backend( request: HttpRequest, user_profile: UserProfile, - message_type: str = REQ( - "type", str_validator=check_string_in(VALID_MESSAGE_TYPES), default="private" + req_type: str = REQ( + "type", str_validator=check_string_in(Message.API_RECIPIENT_TYPES), default="direct" ), operator: str = REQ("op", str_validator=check_string_in(VALID_OPERATOR_TYPES)), notification_to: List[int] = REQ("to", json_validator=check_list(check_int)), @@ -31,7 +30,12 @@ def send_notification_backend( if to_length == 0: raise JsonableError(_("Empty 'to' list")) - if message_type == "stream": + recipient_type_name = req_type + if recipient_type_name == "private": + # TODO: Use "direct" in typing notification events. + recipient_type_name = "direct" + + if recipient_type_name == "stream": if to_length > 1: raise JsonableError(_("Cannot send to multiple streams"))