message-type: Add support for "direct" as value for type parameter.

For endpoints with a `type` parameter to indicate whether the message
is a stream or direct message, `POST /typing` and `POST /messages`,
adds support for passing "direct" as the preferred value for direct
messages, group and 1-on-1.

Maintains support for "private" as a deprecated value to indicate
direct messages.

Fixes #24960.
This commit is contained in:
Lauryn Menard 2023-04-17 17:02:07 +02:00 committed by Tim Abbott
parent 42d9560413
commit 2c043c6242
11 changed files with 173 additions and 97 deletions

View File

@ -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

View File

@ -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
```

View File

@ -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

View File

@ -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:

View File

@ -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}
});

View File

@ -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"

View File

@ -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

View File

@ -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,13 +628,17 @@ 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")
recipient_type_name = ["direct", "private"]
for type in recipient_type_name:
result = self.client_post(
"/json/messages",
{
"type": "private",
"type": type,
"content": "Test message",
"to": orjson.dumps([self.example_user("othello").id]).decode(),
},
@ -647,13 +651,17 @@ class MessagePOSTTest(ZulipTestCase):
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")
recipient_type_name = ["direct", "private"]
for type in recipient_type_name:
result = self.client_post(
"/json/messages",
{
"type": "private",
"type": type,
"content": "Test message",
"to": orjson.dumps(
[self.example_user("othello").id, self.example_user("cordelia").id]
@ -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",

View File

@ -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")

View File

@ -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.

View File

@ -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"))