diff --git a/api_docs/changelog.md b/api_docs/changelog.md index 35135474df..15f23340c9 100644 --- a/api_docs/changelog.md +++ b/api_docs/changelog.md @@ -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/`](/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): diff --git a/version.py b/version.py index 5bc98652ce..360c76ae0c 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 = 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 diff --git a/zerver/models/messages.py b/zerver/models/messages.py index d2054458bc..46547e77f2 100644 --- a/zerver/models/messages.py +++ b/zerver/models/messages.py @@ -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) diff --git a/zerver/openapi/zulip.yaml b/zerver/openapi/zulip.yaml index abe256194d..07f5aa7b04 100644 --- a/zerver/openapi/zulip.yaml +++ b/zerver/openapi/zulip.yaml @@ -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: diff --git a/zerver/tests/test_message_send.py b/zerver/tests/test_message_send.py index 1d170792b0..242fc61ee8 100644 --- a/zerver/tests/test_message_send.py +++ b/zerver/tests/test_message_send.py @@ -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": """&<"'>""", "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", diff --git a/zerver/tests/test_scheduled_messages.py b/zerver/tests/test_scheduled_messages.py index 5804cd17ed..8e492a52c5 100644 --- a/zerver/tests/test_scheduled_messages.py +++ b/zerver/tests/test_scheduled_messages.py @@ -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)), diff --git a/zerver/tests/test_typing.py b/zerver/tests/test_typing.py index 4400ba1f8b..71a1d8df1e 100644 --- a/zerver/tests/test_typing.py +++ b/zerver/tests/test_typing.py @@ -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] diff --git a/zerver/views/message_send.py b/zerver/views/message_send.py index 307156c08b..6e3fce7619 100644 --- a/zerver/views/message_send.py +++ b/zerver/views/message_send.py @@ -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. diff --git a/zerver/views/scheduled_messages.py b/zerver/views/scheduled_messages.py index e4d1a669a1..ec1412291d 100644 --- a/zerver/views/scheduled_messages.py +++ b/zerver/views/scheduled_messages.py @@ -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(): diff --git a/zerver/views/typing.py b/zerver/views/typing.py index 428f0a4ae8..028d5db667 100644 --- a/zerver/views/typing.py +++ b/zerver/views/typing.py @@ -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"))