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

For endpoints with a type parameter to indicate whether a message is
a direct or stream message, adds support for passing "channel" as a
value for stream messages.

Part of stream to channel rename project.
This commit is contained in:
Lauryn Menard 2024-04-10 20:48:10 +02:00 committed by Tim Abbott
parent d24dadb52f
commit 01b59c5aa2
10 changed files with 188 additions and 92 deletions

View File

@ -20,6 +20,14 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 9.0
**Feature level 248**
* [`POST /typing`](/api/set-typing-status), [`POST /messages`](/api/send-message),
[`POST /scheduled_messages`](/api/create-scheduled-message),
[`PATCH /scheduled_messages/<int:scheduled_message_id>`](/api/update-scheduled-message):
Added `"channel"` as an additional value for the `type` parameter to
indicate a stream message.
**Feature level 247**
* [Markdown message formatting](/api/message-formatting#mentions):

View File

@ -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 = 247
API_FEATURE_LEVEL = 248
# 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

@ -129,7 +129,7 @@ class Message(AbstractMessage):
# 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"]
API_RECIPIENT_TYPES = ["direct", "private", "stream", "channel"]
search_tsvector = SearchVectorField(null=True)

View File

@ -5677,17 +5677,20 @@ paths:
type:
description: |
The type of scheduled message to be sent. `"direct"` for a direct
message and `"stream"` for a stream message.
message and `"stream"` or `"channel"` for a stream message.
In Zulip 7.0 (feature level 174), `"direct"` was added as the
preferred way to indicate the type of a direct message, deprecating
the original `"private"`. While `"private"` is supported for
scheduling direct messages, clients are encouraged to use to the
modern convention because support for `"private"` may eventually
be removed.
Note that, while `"private"` is supported for scheduling direct
messages, clients are encouraged to use to the modern convention of
`"direct"` to indicate this message type, because support for
`"private"` may eventually be removed.
**Changes**: In Zulip 9.0 (feature level 248), `"channel"` was added as
an additional value for this parameter to indicate the type of a stream
message.
type: string
enum:
- direct
- channel
- stream
- private
example: direct
@ -5822,21 +5825,24 @@ paths:
type:
description: |
The type of scheduled message to be sent. `"direct"` for a direct
message and `"stream"` for a stream message.
message and `"stream"` or `"channel"` for a stream message.
When updating the type of the scheduled message, the `to` parameter
is required. And, if updating the type of the scheduled message to
`"stream"`, then the `topic` parameter is also required.
`"stream"`/`"channel"`, then the `topic` parameter is also required.
In Zulip 7.0 (feature level 174), `"direct"` was added as the
preferred way to indicate the type of a direct message, deprecating
the original `"private"`. While `"private"` is supported for
scheduling direct messages, clients are encouraged to use to the
modern convention because support for `"private"` may eventually
be removed.
Note that, while `"private"` is supported for scheduling direct
messages, clients are encouraged to use to the modern convention of
`"direct"` to indicate this message type, because support for
`"private"` may eventually be removed.
**Changes**: In Zulip 9.0 (feature level 248), `"channel"` was added as
an additional value for this parameter to indicate the type of a stream
message.
type: string
enum:
- direct
- channel
- stream
- private
example: stream
@ -6452,9 +6458,13 @@ paths:
description: |
The type of message to be sent.
`"direct"` for a direct message and `"stream"` for a stream message.
`"direct"` for a direct message and `"stream"` or `"channel"` for a
stream message.
**Changes**: In Zulip 7.0 (feature level 174), `"direct"` was added as
**Changes**: In Zulip 9.0 (feature level 248), `"channel"` was added as
an additional value for this parameter to request a stream message.
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
@ -6463,6 +6473,7 @@ paths:
type: string
enum:
- direct
- channel
- stream
- private
example: direct
@ -18069,7 +18080,11 @@ paths:
description: |
Type of the message being composed.
**Changes**: In Zulip 8.0 (feature level 215), stopped supporting
**Changes**: In Zulip 9.0 (feature level 248), `"channel"` was added as
an additional value for this parameter to indicate a stream message is
being composed.
In Zulip 8.0 (feature level 215), stopped supporting
`"private"` as a valid value for this parameter.
In Zulip 7.0 (feature level 174), `"direct"` was added
@ -18083,6 +18098,7 @@ paths:
enum:
- direct
- stream
- channel
default: direct
example: direct
op:

View File

@ -84,34 +84,40 @@ class MessagePOSTTest(ZulipTestCase):
Sending a message to a stream to which you are subscribed is
successful.
"""
recipient_type_name = ["stream", "channel"]
self.login("hamlet")
result = self.client_post(
"/json/messages",
{
"type": "stream",
"to": orjson.dumps("Verona").decode(),
"content": "Test message",
"topic": "Test topic",
},
)
self.assert_json_success(result)
for recipient_type in recipient_type_name:
result = self.client_post(
"/json/messages",
{
"type": recipient_type,
"to": orjson.dumps("Verona").decode(),
"content": "Test message",
"topic": "Test topic",
},
)
self.assert_json_success(result)
def test_api_message_to_stream_by_name(self) -> None:
"""
Same as above, but for the API view
"""
recipient_type_name = ["stream", "channel"]
user = self.example_user("hamlet")
result = self.api_post(
user,
"/api/v1/messages",
{
"type": "stream",
"to": orjson.dumps("Verona").decode(),
"content": "Test message",
"topic": "Test topic",
},
)
self.assert_json_success(result)
for recipient_type in recipient_type_name:
result = self.api_post(
user,
"/api/v1/messages",
{
"type": recipient_type,
"to": orjson.dumps("Verona").decode(),
"content": "Test message",
"topic": "Test topic",
},
)
self.assert_json_success(result)
def test_message_to_stream_with_nonexistent_id(self) -> None:
cordelia = self.example_user("cordelia")
@ -123,7 +129,7 @@ class MessagePOSTTest(ZulipTestCase):
bot,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": orjson.dumps([99999]).decode(),
"content": "Stream message by ID.",
"topic": "Test topic for stream ID message",
@ -154,7 +160,7 @@ class MessagePOSTTest(ZulipTestCase):
bot,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": orjson.dumps(stream.name).decode(),
"content": "Stream message to an empty stream by name.",
"topic": "Test topic for empty stream name message",
@ -189,7 +195,7 @@ class MessagePOSTTest(ZulipTestCase):
bot,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": orjson.dumps([stream.id]).decode(),
"content": "Stream message to an empty stream by id.",
"topic": "Test topic for empty stream id message",
@ -213,21 +219,25 @@ class MessagePOSTTest(ZulipTestCase):
Sending a message to a stream (by stream ID) to which you are
subscribed is successful.
"""
recipient_type_name = ["stream", "channel"]
self.login("hamlet")
realm = get_realm("zulip")
stream = get_stream("Verona", realm)
result = self.client_post(
"/json/messages",
{
"type": "stream",
"to": orjson.dumps([stream.id]).decode(),
"content": "Stream message by ID.",
"topic": "Test topic for stream ID message",
},
)
self.assert_json_success(result)
sent_message = self.get_last_message()
self.assertEqual(sent_message.content, "Stream message by ID.")
for recipient_type in recipient_type_name:
content = f"Stream message by ID, type parameter: {recipient_type}."
result = self.client_post(
"/json/messages",
{
"type": recipient_type,
"to": orjson.dumps([stream.id]).decode(),
"content": content,
"topic": "Test topic for stream ID message",
},
)
self.assert_json_success(result)
sent_message = self.get_last_message()
self.assertEqual(sent_message.content, content)
def test_sending_message_as_stream_post_policy_admins(self) -> None:
"""
@ -522,7 +532,7 @@ class MessagePOSTTest(ZulipTestCase):
user,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"content": "Test message no to",
"topic": "Test topic",
},
@ -542,7 +552,7 @@ class MessagePOSTTest(ZulipTestCase):
result = self.client_post(
"/json/messages",
{
"type": "stream",
"type": "channel",
"to": "nonexistent_stream",
"content": "Test message",
"topic": "Test topic",
@ -559,7 +569,7 @@ class MessagePOSTTest(ZulipTestCase):
result = self.client_post(
"/json/messages",
{
"type": "stream",
"type": "channel",
"to": """&<"'><non-existent>""",
"content": "Test message",
"topic": "Test topic",
@ -585,7 +595,7 @@ class MessagePOSTTest(ZulipTestCase):
user,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": orjson.dumps("Verona").decode(),
"content": "Test message",
"topic": "Test topic",
@ -601,7 +611,7 @@ class MessagePOSTTest(ZulipTestCase):
user,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": orjson.dumps("Verona").decode(),
"content": "Another Test message",
"topic": "Test topic",
@ -882,7 +892,7 @@ class MessagePOSTTest(ZulipTestCase):
result = self.client_post(
"/json/messages",
{
"type": "stream",
"type": "channel",
"to": "Verona",
"content": "Test message",
"topic": "",
@ -897,7 +907,7 @@ class MessagePOSTTest(ZulipTestCase):
self.login("hamlet")
result = self.client_post(
"/json/messages",
{"type": "stream", "to": "Verona", "content": "Test message"},
{"type": "channel", "to": "Verona", "content": "Test message"},
)
self.assert_json_error(result, "Missing topic")
@ -910,7 +920,7 @@ class MessagePOSTTest(ZulipTestCase):
result = self.client_post(
"/json/messages",
{
"type": "stream",
"type": "channel",
"to": "Verona",
"topic": "Test\n\rTopic",
"content": "Test message",
@ -922,7 +932,7 @@ class MessagePOSTTest(ZulipTestCase):
result = self.client_post(
"/json/messages",
{
"type": "stream",
"type": "channel",
"to": "Verona",
"topic": "Test\ufffeTopic",
"content": "Test message",
@ -932,7 +942,7 @@ class MessagePOSTTest(ZulipTestCase):
def test_invalid_recipient_type(self) -> None:
"""
Messages other than the type of "direct", "private" or "stream" are invalid.
Messages other than the type of "direct", "private", "channel" or "stream" are invalid.
"""
self.login("hamlet")
result = self.client_post(
@ -1072,7 +1082,7 @@ class MessagePOSTTest(ZulipTestCase):
"""
self.login("hamlet")
post_data = {
"type": "stream",
"type": "channel",
"to": "Verona",
"content": " I like null bytes \x00 in my content",
"topic": "Test topic",
@ -1086,7 +1096,7 @@ class MessagePOSTTest(ZulipTestCase):
"""
self.login("hamlet")
post_data = {
"type": "stream",
"type": "channel",
"to": orjson.dumps("Verona").decode(),
"content": " I like whitespace at the end! \n\n \n",
"topic": "Test topic",
@ -1098,7 +1108,7 @@ class MessagePOSTTest(ZulipTestCase):
# Test if it removes the new line from the beginning of the message.
post_data = {
"type": "stream",
"type": "channel",
"to": orjson.dumps("Verona").decode(),
"content": "\nAvoid the new line at the beginning of the message.",
"topic": "Test topic",
@ -1120,7 +1130,7 @@ class MessagePOSTTest(ZulipTestCase):
MAX_MESSAGE_LENGTH = settings.MAX_MESSAGE_LENGTH
long_message = "A" * (MAX_MESSAGE_LENGTH + 1)
post_data = {
"type": "stream",
"type": "channel",
"to": orjson.dumps("Verona").decode(),
"content": long_message,
"topic": "Test topic",
@ -1141,7 +1151,7 @@ class MessagePOSTTest(ZulipTestCase):
self.login("hamlet")
long_topic_name = "A" * (MAX_TOPIC_NAME_LENGTH + 1)
post_data = {
"type": "stream",
"type": "channel",
"to": orjson.dumps("Verona").decode(),
"content": "test content",
"topic": long_topic_name,
@ -1157,7 +1167,7 @@ class MessagePOSTTest(ZulipTestCase):
result = self.client_post(
"/json/messages",
{
"type": "stream",
"type": "channel",
"to": "Verona",
"content": "Test message",
"topic": "Test topic",
@ -1185,7 +1195,7 @@ class MessagePOSTTest(ZulipTestCase):
self.mit_user("starnine"),
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"sender": self.mit_email("sipbtest"),
"content": "Test message",
"client": "zephyr_mirror",
@ -1282,7 +1292,7 @@ class MessagePOSTTest(ZulipTestCase):
user,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"forged": "true",
"time": fake_timestamp,
"sender": "irc-user@irc.zulip.com",
@ -1305,7 +1315,7 @@ class MessagePOSTTest(ZulipTestCase):
user,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"forged": "yes",
"time": fake_timestamp,
"sender": "irc-user@irc.zulip.com",
@ -1335,7 +1345,7 @@ class MessagePOSTTest(ZulipTestCase):
def test_with(sender_email: str, client: str, forged: bool) -> None:
payload = dict(
type="stream",
type="channel",
to=orjson.dumps(stream_name).decode(),
client=client,
topic="whatever",
@ -1382,7 +1392,7 @@ class MessagePOSTTest(ZulipTestCase):
self.make_stream(stream_name, invite_only=True)
payload = dict(
type="stream",
type="channel",
to=orjson.dumps(stream_name).decode(),
topic="whatever",
content="whatever",
@ -1408,7 +1418,7 @@ class MessagePOSTTest(ZulipTestCase):
notification_bot,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": orjson.dumps("notify_channel").decode(),
"content": "Test message",
"topic": "Test topic",
@ -1429,7 +1439,7 @@ class MessagePOSTTest(ZulipTestCase):
stream_name = "public stream"
self.make_stream(stream_name, invite_only=False)
payload = dict(
type="stream",
type="channel",
to=orjson.dumps(stream_name).decode(),
topic="whatever",
content="whatever",
@ -2211,7 +2221,7 @@ class StreamMessagesTest(ZulipTestCase):
user,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": orjson.dumps("Verona").decode(),
"sender": self.mit_email("sipbtest"),
"client": "zephyr_mirror",
@ -2228,7 +2238,7 @@ class StreamMessagesTest(ZulipTestCase):
user,
"/api/v1/messages",
{
"type": "stream",
"type": "channel",
"to": "Verona",
"sender": self.mit_email("sipbtest"),
"client": "zephyr_mirror",

View File

@ -40,7 +40,7 @@ class ScheduledMessageTest(ZulipTestCase):
self.login("hamlet")
topic_name = ""
if msg_type == "stream":
if msg_type in ["stream", "channel"]:
topic_name = "Test topic"
payload = {
@ -61,7 +61,7 @@ class ScheduledMessageTest(ZulipTestCase):
# Scheduling a message to a stream you are subscribed is successful.
result = self.do_schedule_message(
"stream", verona_stream_id, content + " 1", scheduled_delivery_timestamp
"channel", verona_stream_id, content + " 1", scheduled_delivery_timestamp
)
scheduled_message = self.last_scheduled_message()
self.assert_json_success(result)
@ -99,7 +99,7 @@ class ScheduledMessageTest(ZulipTestCase):
scheduled_delivery_timestamp = int(scheduled_delivery_datetime.timestamp())
verona_stream_id = self.get_stream_id("Verona")
result = self.do_schedule_message(
"stream", verona_stream_id, content + " 1", scheduled_delivery_timestamp
"channel", verona_stream_id, content + " 1", scheduled_delivery_timestamp
)
self.assert_json_success(result)
@ -420,7 +420,7 @@ class ScheduledMessageTest(ZulipTestCase):
scheduled_delivery_timestamp = int(time.time() - 86400)
result = self.do_schedule_message(
"stream", verona_stream_id, content + " 1", scheduled_delivery_timestamp
"channel", verona_stream_id, content + " 1", scheduled_delivery_timestamp
)
self.assert_json_error(result, "Scheduled delivery time must be in the future.")
@ -431,7 +431,7 @@ class ScheduledMessageTest(ZulipTestCase):
# Scheduling a message to a stream you are subscribed is successful.
result = self.do_schedule_message(
"stream", verona_stream_id, content, scheduled_delivery_timestamp
"channel", verona_stream_id, content, scheduled_delivery_timestamp
)
scheduled_message = self.last_scheduled_message()
self.assert_json_success(result)
@ -445,6 +445,26 @@ class ScheduledMessageTest(ZulipTestCase):
scheduled_message_id = scheduled_message.id
payload: Dict[str, Any]
# Edit message with other stream message type ("stream") and no other changes
# results in no changes to the scheduled message.
payload = {
"type": "stream",
"to": orjson.dumps(verona_stream_id).decode(),
"topic": "Test topic",
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
self.assert_json_success(result)
scheduled_message = self.get_scheduled_message(str(scheduled_message_id))
self.assertEqual(scheduled_message.recipient.type, Recipient.STREAM)
self.assertEqual(scheduled_message.stream_id, verona_stream_id)
self.assertEqual(scheduled_message.content, "Original test message")
self.assertEqual(scheduled_message.topic_name(), "Test topic")
self.assertEqual(
scheduled_message.scheduled_timestamp,
timestamp_to_datetime(scheduled_delivery_timestamp),
)
# Sending request with only scheduled message ID returns an error
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}")
self.assert_json_error(result, "Nothing to change")
@ -480,7 +500,7 @@ class ScheduledMessageTest(ZulipTestCase):
# Trying to edit `type` to stream message type without a `topic` returns an error
payload = {
"type": "stream",
"type": "channel",
"to": orjson.dumps(verona_stream_id).decode(),
}
result = self.client_patch(f"/json/scheduled_messages/{scheduled_message_id}", payload)
@ -490,7 +510,7 @@ class ScheduledMessageTest(ZulipTestCase):
# Edit message `type` to stream with valid `to` and `topic` succeeds
payload = {
"type": "stream",
"type": "channel",
"to": orjson.dumps(verona_stream_id).decode(),
"topic": "New test topic",
}
@ -568,7 +588,7 @@ class ScheduledMessageTest(ZulipTestCase):
verona_stream_id = self.get_stream_id("Verona")
content = "Test message"
scheduled_delivery_timestamp = int(time.time() + 86400)
self.do_schedule_message("stream", verona_stream_id, content, scheduled_delivery_timestamp)
self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)
# Single scheduled message
result = self.client_get("/json/scheduled_messages")
@ -611,7 +631,7 @@ class ScheduledMessageTest(ZulipTestCase):
verona_stream_id = self.get_stream_id("Verona")
scheduled_delivery_timestamp = int(time.time() + 86400)
self.do_schedule_message("stream", verona_stream_id, content, scheduled_delivery_timestamp)
self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)
scheduled_message = self.last_scheduled_message()
self.logout()
@ -649,7 +669,7 @@ class ScheduledMessageTest(ZulipTestCase):
scheduled_delivery_timestamp = int(time.time() + 86400)
# Test sending with attachment
self.do_schedule_message("stream", verona_stream_id, content, scheduled_delivery_timestamp)
self.do_schedule_message("channel", verona_stream_id, content, scheduled_delivery_timestamp)
scheduled_message = self.last_scheduled_message()
self.assertEqual(
list(attachment_object1.scheduled_messages.all().values_list("id", flat=True)),

View File

@ -367,6 +367,26 @@ class TypingHappyPathTestDirectMessages(ZulipTestCase):
class TypingHappyPathTestStreams(ZulipTestCase):
def test_valid_type_and_op_parameters(self) -> None:
recipient_type_name = ["channel", "stream"]
operator_type = ["start", "stop"]
sender = self.example_user("hamlet")
stream_name = self.get_streams(sender)[0]
stream_id = self.get_stream_id(stream_name)
topic_name = "Some topic"
for recipient_type in recipient_type_name:
for operator in operator_type:
params = dict(
type=recipient_type,
op=operator,
stream_id=str(stream_id),
topic=topic_name,
)
result = self.api_post(sender, "/api/v1/typing", params)
self.assert_json_success(result)
def test_start(self) -> None:
sender = self.example_user("hamlet")
stream_name = self.get_streams(sender)[0]

View File

@ -145,6 +145,11 @@ def send_message_backend(
# TODO: Use "direct" here, as well as in events and
# message (created, schdeduled, drafts) objects/dicts.
recipient_type_name = "private"
elif recipient_type_name == "channel":
# For now, use "stream" from Message.API_RECIPIENT_TYPES.
# TODO: Use "channel" here, as well as in events and
# message (created, schdeduled, drafts) objects/dicts.
recipient_type_name = "stream"
# If req_to is None, then we default to an
# empty list of recipients.

View File

@ -66,6 +66,12 @@ def update_scheduled_message_backend(
else:
recipient_type_name = req_type
if recipient_type_name is not None and recipient_type_name == "channel":
# For now, use "stream" from Message.API_RECIPIENT_TYPES.
# TODO: Use "channel" here, as well as in events and
# message (created, schdeduled, drafts) objects/dicts.
recipient_type_name = "stream"
if recipient_type_name is not None and recipient_type_name == "stream" and topic_name is None:
raise JsonableError(_("Topic required when updating scheduled message type to stream."))
@ -123,6 +129,11 @@ def create_scheduled_message_backend(
# TODO: Use "direct" here, as well as in events and
# scheduled message objects/dicts.
recipient_type_name = "private"
elif recipient_type_name == "channel":
# For now, use "stream" from Message.API_RECIPIENT_TYPES.
# TODO: Use "channel" here, as well as in events and
# message (created, schdeduled, drafts) objects/dicts.
recipient_type_name = "stream"
deliver_at = timestamp_to_datetime(scheduled_delivery_timestamp)
if deliver_at <= timezone_now():

View File

@ -12,7 +12,7 @@ from zerver.lib.validator import check_int, check_list, check_string_in
from zerver.models import UserProfile
VALID_OPERATOR_TYPES = ["start", "stop"]
VALID_RECIPIENT_TYPES = ["direct", "stream"]
VALID_RECIPIENT_TYPES = ["direct", "stream", "channel"]
@has_request_variables
@ -30,6 +30,12 @@ def send_notification_backend(
topic: Optional[str] = REQ("topic", default=None),
) -> HttpResponse:
recipient_type_name = req_type
if recipient_type_name == "channel":
# For now, use "stream" from Message.API_RECIPIENT_TYPES.
# TODO: Use "channel" here, as well as in events and
# message (created, schdeduled, drafts) objects/dicts.
recipient_type_name = "stream"
if recipient_type_name == "stream":
if stream_id is None:
raise JsonableError(_("Missing stream_id"))