topic_mentions: Fix restriction rule for @-topic mentions.

Now, the topic wildcard mention follows the following
rules:
* If the topic has less than 15 participants , anyone
can use @ topic mentions.
* For more than 15, the org setting 'wildcard_mention_policy'
determines who can use @ topic mentions.

Earlier, topic wildcard mentions followed the same restriction
as stream wildcard mentions, which was incorrect.

Fixes part of #27700.
This commit is contained in:
Prakhar Pratyush 2023-11-21 15:09:13 +05:30 committed by Tim Abbott
parent 31a731469d
commit 49388d5d3d
18 changed files with 282 additions and 68 deletions

View File

@ -20,6 +20,16 @@ format used by the Zulip server that they are interacting with.
## Changes in Zulip 8.0
**Feature level 229**
* [`PATCH /messages/{message_id}`](/api/update-message), [`POST
/messages`](/api/send-message): Topic wildcard mentions involving
large numbers of participants are now restricted by
`wildcard_mention_policy`. The server now uses the
`STREAM_WILDCARD_MENTION_NOT_ALLOWED` and
`TOPIC_WILDCARD_MENTION_NOT_ALLOWED` error codes when a message is
rejected because of `wildcard_mention_policy`.
**Feature level 228**
* [`GET /events`](/api/get-events): `realm_user` events with `op: "update"`

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 = 228
API_FEATURE_LEVEL = 229
# 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

@ -5,6 +5,7 @@ import $ from "jquery";
import _ from "lodash";
import render_success_message_scheduled_banner from "../templates/compose_banner/success_message_scheduled_banner.hbs";
import render_wildcard_mention_not_allowed_error from "../templates/compose_banner/wildcard_mention_not_allowed_error.hbs";
import * as channel from "./channel";
import * as compose_banner from "./compose_banner";
@ -180,16 +181,26 @@ export function send_message(request = create_message_object()) {
send_message_success(request, data);
}
function error(response) {
// If we're not local echo'ing messages, or if this message was not
// locally echoed, show error in compose box
function error(response, server_error_code) {
// Error callback for failed message send attempts.
if (!locally_echoed) {
if (server_error_code === "TOPIC_WILDCARD_MENTION_NOT_ALLOWED") {
// The topic wildcard mention permission code path has
// a special error.
const new_row = render_wildcard_mention_not_allowed_error({
banner_type: compose_banner.ERROR,
classname: compose_banner.CLASSNAMES.wildcards_not_allowed,
});
compose_banner.append_compose_banner_to_banner_list(new_row, $("#compose_banners"));
} else {
compose_banner.show_error_message(
response,
compose_banner.CLASSNAMES.generic_compose_error,
$("#compose_banners"),
$("textarea#compose-textarea"),
);
}
// For messages that were not locally echoed, we're
// responsible for hiding the compose spinner to restore
// the compose box so one can send a next message.

View File

@ -96,7 +96,7 @@ function resend_message(message, $row, {on_send_message_success, send_message})
failed_message_success(message_id);
}
function on_error(response) {
function on_error(response, _server_error_code) {
message_send_error(message.id, response);
setTimeout(() => {
hide_retry_spinner($row);

View File

@ -85,13 +85,22 @@ function contains_problematic_linkifier({content, get_linkifier_map}) {
return false;
}
function contains_topic_wildcard_mention(content) {
// If the content has topic wildcard mention (@**topic**) then don't
// render it locally. We have only server-side restriction check for
// @topic mention. This helps to show the error message (no permission)
// via the compose banner and not to local-echo then fail due to restriction.
return content.includes("@**topic**");
}
function content_contains_backend_only_syntax({content, get_linkifier_map}) {
// Try to guess whether or not a message contains syntax that only the
// backend Markdown processor can correctly handle.
// If it doesn't, we can immediately render it client-side for local echo.
return (
contains_preview_link(content) ||
contains_problematic_linkifier({content, get_linkifier_map})
contains_problematic_linkifier({content, get_linkifier_map}) ||
contains_topic_wildcard_mention(content)
);
}

View File

@ -2,6 +2,7 @@ import ClipboardJS from "clipboard";
import $ from "jquery";
import * as resolved_topic from "../shared/src/resolved_topic";
import render_wildcard_mention_not_allowed_error from "../templates/compose_banner/wildcard_mention_not_allowed_error.hbs";
import render_delete_message_modal from "../templates/confirm_dialog/confirm_delete_message.hbs";
import render_confirm_moving_messages_modal from "../templates/confirm_dialog/confirm_moving_messages.hbs";
import render_message_edit_form from "../templates/message_edit_form.hbs";
@ -1070,13 +1071,23 @@ export function save_message_row_edit($row) {
hide_message_edit_spinner($row);
if (xhr.readyState !== 0) {
const $container = compose_banner.get_compose_banner_container(
$row.find("textarea"),
);
if (xhr.responseJSON?.code === "TOPIC_WILDCARD_MENTION_NOT_ALLOWED") {
const new_row = render_wildcard_mention_not_allowed_error({
banner_type: compose_banner.ERROR,
classname: compose_banner.CLASSNAMES.wildcards_not_allowed,
});
compose_banner.append_compose_banner_to_banner_list(new_row, $container);
return;
}
const message = channel.xhr_error_message(
$t({defaultMessage: "Error editing message"}),
xhr,
);
const $container = compose_banner.get_compose_banner_container(
$row.find("textarea"),
);
compose_banner.show_error_message(
message,
compose_banner.CLASSNAMES.generic_compose_error,

View File

@ -71,7 +71,7 @@ export function send_message(request, on_success, error) {
}
const response = channel.xhr_error_message("Error sending message", xhr);
error(response);
error(response, xhr.responseJSON?.code);
},
});
} finally {
@ -97,7 +97,7 @@ export function reply_message(opts) {
// already handles things like reporting times to the server.)
}
function error() {
function error(_response, _server_error_code) {
// TODO: In our current use case, which is widgets, to meaningfully
// handle errors, we would want the widget to provide some
// kind of callback to us so it can do some appropriate UI.

View File

@ -1,5 +1,9 @@
{{#> compose_banner }}
<p class="banner_message">
{{#if stream_wildcard_mention}}
{{#tr}}You do not have permission to use <b>@{stream_wildcard_mention}</b> mentions in this stream.{{/tr}}
{{else}}
{{#tr}}You do not have permission to use <b>@topic</b> mentions in this topic.{{/tr}}
{{/if}}
</p>
{{/compose_banner}}

View File

@ -101,7 +101,7 @@
</select>
</div>
<div class="input-group">
<label for="realm_wildcard_mention_policy" class="dropdown-title">{{t "Who can use @all/@everyone mentions in large streams" }}
<label for="realm_wildcard_mention_policy" class="dropdown-title">{{t "Who can notify a large number of users with a wildcard mention" }}
{{> ../help_link_widget link="/help/restrict-wildcard-mentions" }}
</label>
<select name="realm_wildcard_mention_policy" id="id_realm_wildcard_mention_policy" class="prop-element settings_select bootstrap-focus-style" data-setting-widget-type="number">

View File

@ -100,6 +100,34 @@ run_test("transmit_message_ajax_reload_pending", () => {
assert.ok(reload_initiated);
});
run_test("topic wildcard mention not allowed", ({override}) => {
/* istanbul ignore next */
const success = () => {
throw new Error("unexpected success");
};
/* istanbul ignore next */
const error = (_response, server_error_code) => {
assert.equal(server_error_code, "TOPIC_WILDCARD_MENTION_NOT_ALLOWED");
};
override(reload_state, "is_pending", () => false);
const request = {foo: "bar"};
override(channel, "post", (opts) => {
assert.equal(opts.url, "/json/messages");
assert.equal(opts.data.foo, "bar");
const xhr = {
responseJSON: {
code: "TOPIC_WILDCARD_MENTION_NOT_ALLOWED",
},
};
opts.error(xhr, "bad request");
});
transmit.send_message(request, success, error);
});
run_test("reply_message_stream", ({override}) => {
const social_stream_id = 555;
stream_data.add_sub({

View File

@ -22,7 +22,12 @@ from zerver.actions.message_send import (
)
from zerver.actions.uploads import check_attachment_reference_change
from zerver.actions.user_topics import bulk_do_set_user_topic_visibility_policy
from zerver.lib.exceptions import JsonableError, MessageMoveError
from zerver.lib.exceptions import (
JsonableError,
MessageMoveError,
StreamWildcardMentionNotAllowedError,
TopicWildcardMentionNotAllowedError,
)
from zerver.lib.markdown import MessageRenderingResult, topic_links
from zerver.lib.markdown import version as markdown_version
from zerver.lib.mention import MentionBackend, MentionData, silent_mention_syntax_for_user
@ -31,9 +36,10 @@ from zerver.lib.message import (
bulk_access_messages,
check_user_group_mention_allowed,
normalize_body,
stream_wildcard_mention_allowed,
topic_wildcard_mention_allowed,
truncate_topic,
update_to_dict_cache,
wildcard_mention_allowed,
)
from zerver.lib.queue import queue_json_publish
from zerver.lib.stream_subscription import get_active_subscriptions_for_stream_id
@ -51,6 +57,7 @@ from zerver.lib.topic import (
TOPIC_LINKS,
TOPIC_NAME,
messages_for_topic,
participants_for_topic,
save_message_for_edit_use_case,
update_edit_history,
update_messages_for_topic_edit,
@ -1274,12 +1281,19 @@ def check_update_message(
)
links_for_embed |= rendering_result.links_for_preview
if message.is_stream_message() and rendering_result.has_wildcard_mention():
if message.is_stream_message() and rendering_result.mentions_stream_wildcard:
stream = access_stream_by_id(user_profile, message.recipient.type_id)[0]
if not wildcard_mention_allowed(message.sender, stream, message.realm):
raise JsonableError(
_("You do not have permission to use wildcard mentions in this stream.")
if not stream_wildcard_mention_allowed(message.sender, stream, message.realm):
raise StreamWildcardMentionNotAllowedError
if message.is_stream_message() and rendering_result.mentions_topic_wildcard:
topic_participant_count = len(
participants_for_topic(message.realm.id, message.recipient.id, message.topic_name())
)
if not topic_wildcard_mention_allowed(
message.sender, topic_participant_count, message.realm
):
raise TopicWildcardMentionNotAllowedError
if rendering_result.mentions_user_group_ids:
mentioned_group_ids = list(rendering_result.mentions_user_group_ids)

View File

@ -39,7 +39,9 @@ from zerver.lib.exceptions import (
JsonableError,
MarkdownRenderingError,
StreamDoesNotExistError,
StreamWildcardMentionNotAllowedError,
StreamWithIDDoesNotExistError,
TopicWildcardMentionNotAllowedError,
ZephyrMessageAlreadySentError,
)
from zerver.lib.markdown import MessageRenderingResult
@ -52,9 +54,10 @@ from zerver.lib.message import (
normalize_body,
render_markdown,
set_visibility_policy_possible,
stream_wildcard_mention_allowed,
topic_wildcard_mention_allowed,
truncate_topic,
visibility_policy_for_send_message,
wildcard_mention_allowed,
)
from zerver.lib.muted_users import get_muting_users
from zerver.lib.notification_data import (
@ -1710,12 +1713,18 @@ def check_message(
if (
stream is not None
and message_send_dict.rendering_result.has_wildcard_mention()
and not wildcard_mention_allowed(sender, stream, realm)
and message_send_dict.rendering_result.mentions_stream_wildcard
and not stream_wildcard_mention_allowed(sender, stream, realm)
):
raise JsonableError(
_("You do not have permission to use wildcard mentions in this stream.")
)
raise StreamWildcardMentionNotAllowedError
topic_participant_count = len(message_send_dict.topic_participant_user_ids)
if (
stream is not None
and message_send_dict.rendering_result.mentions_topic_wildcard
and not topic_wildcard_mention_allowed(sender, topic_participant_count, realm)
):
raise TopicWildcardMentionNotAllowedError
if message_send_dict.rendering_result.mentions_user_group_ids:
mentioned_group_ids = list(message_send_dict.rendering_result.mentions_user_group_ids)

View File

@ -47,6 +47,8 @@ class ErrorCode(Enum):
REACTION_DOES_NOT_EXIST = auto()
SERVER_NOT_READY = auto()
MISSING_REMOTE_REALM = auto()
TOPIC_WILDCARD_MENTION_NOT_ALLOWED = auto()
STREAM_WILDCARD_MENTION_NOT_ALLOWED = auto()
class JsonableError(Exception):
@ -584,3 +586,27 @@ class MissingRemoteRealmError(JsonableError): # nocoverage
@override
def msg_format() -> str:
return _("Organization not registered")
class StreamWildcardMentionNotAllowedError(JsonableError):
code: ErrorCode = ErrorCode.STREAM_WILDCARD_MENTION_NOT_ALLOWED
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("You do not have permission to use stream wildcard mentions in this stream.")
class TopicWildcardMentionNotAllowedError(JsonableError):
code: ErrorCode = ErrorCode.TOPIC_WILDCARD_MENTION_NOT_ALLOWED
def __init__(self) -> None:
pass
@staticmethod
@override
def msg_format() -> str:
return _("You do not have permission to use topic wildcard mentions in this topic.")

View File

@ -134,9 +134,6 @@ class MessageRenderingResult:
user_ids_with_alert_words: Set[int]
potential_attachment_path_ids: List[str]
def has_wildcard_mention(self) -> bool:
return self.mentions_stream_wildcard or self.mentions_topic_wildcard
@dataclass
class DbData:

View File

@ -1664,14 +1664,13 @@ def get_recent_private_conversations(user_profile: UserProfile) -> Dict[int, Dic
return recipient_map
def wildcard_mention_allowed(sender: UserProfile, stream: Stream, realm: Realm) -> bool:
# If there are fewer than Realm.WILDCARD_MENTION_THRESHOLD, we
# allow sending. In the future, we may want to make this behavior
# a default, and also just allow explicitly setting whether this
# applies to a stream as an override.
if num_subscribers_for_stream_id(stream.id) <= Realm.WILDCARD_MENTION_THRESHOLD:
return True
def wildcard_mention_policy_authorizes_user(sender: UserProfile, realm: Realm) -> bool:
"""Helper function for 'topic_wildcard_mention_allowed' and
'stream_wildcard_mention_allowed' to check if the sender is allowed to use
wildcard mentions based on the 'wildcard_mention_policy' setting of that realm.
This check is used only if the participants count in the topic or the subscribers
count in the stream is greater than 'Realm.WILDCARD_MENTION_THRESHOLD'.
"""
if realm.wildcard_mention_policy == Realm.WILDCARD_MENTION_POLICY_NOBODY:
return False
@ -1693,6 +1692,24 @@ def wildcard_mention_allowed(sender: UserProfile, stream: Stream, realm: Realm)
raise AssertionError("Invalid wildcard mention policy")
def topic_wildcard_mention_allowed(
sender: UserProfile, topic_participant_count: int, realm: Realm
) -> bool:
if topic_participant_count <= Realm.WILDCARD_MENTION_THRESHOLD:
return True
return wildcard_mention_policy_authorizes_user(sender, realm)
def stream_wildcard_mention_allowed(sender: UserProfile, stream: Stream, realm: Realm) -> bool:
# If there are fewer than Realm.WILDCARD_MENTION_THRESHOLD, we
# allow sending. In the future, we may want to make this behavior
# a default, and also just allow explicitly setting whether this
# applies to a stream as an override.
if num_subscribers_for_stream_id(stream.id) <= Realm.WILDCARD_MENTION_THRESHOLD:
return True
return wildcard_mention_policy_authorizes_user(sender, realm)
def check_user_group_mention_allowed(sender: UserProfile, user_group_ids: List[int]) -> None:
user_groups = UserGroup.objects.filter(id__in=user_group_ids).select_related(
"can_mention_group"

View File

@ -6404,6 +6404,38 @@ paths:
description: |
A typical failed JSON response for when a direct message is sent to a user
that does not exist:
- allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"result": "error",
"msg": "You do not have permission to use stream wildcard mentions in this stream.",
"code": "STREAM_WILDCARD_MENTION_NOT_ALLOWED",
}
description: |
An example JSON error response for when the message was rejected because
of the organization's `wildcard_mention_policy` and large number of
subscribers to the stream.
**Changes**: New in Zulip 8.0 (feature level 229). Previously, this
error returned the `"BAD_REQUEST"` code.
- allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"result": "error",
"msg": "You do not have permission to use topic wildcard mentions in this topic.",
"code": "TOPIC_WILDCARD_MENTION_NOT_ALLOWED",
}
description: |
An example JSON error response for when the message was rejected because
the message contains a topic wildcard mention, but the user doesn't have
permission to use such a mention in this topic due to the
`wildcard_mention_policy` (and large number of participants in this
specific topic).
**Changes**: New in Zulip 8.0 (feature level 229). Previously,
`wildcard_mention_policy` was not enforced for topic mentions.
/messages/{message_id}/history:
get:
operationId: get-message-history
@ -7668,6 +7700,38 @@ paths:
dialog.
**Changes**: New in Zulip 7.0 (feature level 172).
- allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"result": "error",
"msg": "You do not have permission to use stream wildcard mentions in this stream.",
"code": "STREAM_WILDCARD_MENTION_NOT_ALLOWED",
}
description: |
An example JSON error response for when the message was rejected because
of the organization's `wildcard_mention_policy` and large number of
subscribers to the stream.
**Changes**: New in Zulip 8.0 (feature level 229). Previously, this
error returned the `"BAD_REQUEST"` code.
- allOf:
- $ref: "#/components/schemas/CodedError"
- example:
{
"result": "error",
"msg": "You do not have permission to use topic wildcard mentions in this topic.",
"code": "TOPIC_WILDCARD_MENTION_NOT_ALLOWED",
}
description: |
An example JSON error response for when the message was rejected because
the message contains a topic wildcard mention, but the user doesn't have
permission to use such a mention in this topic due to the
`wildcard_mention_policy` (and large number of participants in this
specific topic).
**Changes**: New in Zulip 8.0 (feature level 229). Previously,
`wildcard_mention_policy` was not enforced for topic mentions.
delete:
operationId: delete-message
summary: Delete a message

View File

@ -2284,18 +2284,11 @@ class EditMessageTest(EditMessageTestCase):
acting_user=None,
)
with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=17):
result = self.client_patch(
"/json/messages/" + str(message_id),
{
"content": "Hello @**topic**",
},
)
self.assert_json_error(
result, "You do not have permission to use wildcard mentions in this stream."
)
with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=14):
# Less than 'Realm.WILDCARD_MENTION_THRESHOLD' participants
participants_user_ids = set(range(1, 10))
with mock.patch(
"zerver.actions.message_edit.participants_for_topic", return_value=participants_user_ids
):
result = self.client_patch(
"/json/messages/" + str(message_id),
{
@ -2304,9 +2297,27 @@ class EditMessageTest(EditMessageTestCase):
)
self.assert_json_success(result)
# More than 'Realm.WILDCARD_MENTION_THRESHOLD' participants.
participants_user_ids = set(range(1, 20))
with mock.patch(
"zerver.actions.message_edit.participants_for_topic", return_value=participants_user_ids
):
result = self.client_patch(
"/json/messages/" + str(message_id),
{
"content": "Hello @**topic**",
},
)
self.assert_json_error(
result, "You do not have permission to use topic wildcard mentions in this topic."
)
# Shiva is moderator
self.login("shiva")
message_id = self.send_stream_message(shiva, stream_name, "Hi everyone")
with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=17):
with mock.patch(
"zerver.actions.message_edit.participants_for_topic", return_value=participants_user_ids
):
result = self.client_patch(
"/json/messages/" + str(message_id),
{
@ -2389,7 +2400,7 @@ class EditMessageTest(EditMessageTestCase):
},
)
self.assert_json_error(
result, "You do not have permission to use wildcard mentions in this stream."
result, "You do not have permission to use stream wildcard mentions in this stream."
)
with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=14):

View File

@ -1783,11 +1783,14 @@ class StreamMessagesTest(ZulipTestCase):
self.assertTrue(user_message.flags.mentioned)
def send_and_verify_topic_wildcard_mention_message(
self, sender_name: str, test_fails: bool = False, sub_count: int = 16
self, sender_name: str, test_fails: bool = False, topic_participant_count: int = 20
) -> None:
sender = self.example_user(sender_name)
content = "@**topic** test topic wildcard mention"
with mock.patch("zerver.lib.message.num_subscribers_for_stream_id", return_value=sub_count):
participants_user_ids = set(range(topic_participant_count))
with mock.patch(
"zerver.actions.message_send.participants_for_topic", return_value=participants_user_ids
):
if not test_fails:
msg_id = self.send_stream_message(sender, "test_stream", content)
result = self.api_get(sender, "/api/v1/messages/" + str(msg_id))
@ -1796,7 +1799,7 @@ class StreamMessagesTest(ZulipTestCase):
else:
with self.assertRaisesRegex(
JsonableError,
"You do not have permission to use wildcard mentions in this stream.",
"You do not have permission to use topic wildcard mentions in this topic.",
):
self.send_stream_message(sender, "test_stream", content)
@ -1828,8 +1831,8 @@ class StreamMessagesTest(ZulipTestCase):
acting_user=None,
)
self.send_and_verify_topic_wildcard_mention_message("polonius", test_fails=True)
# There is no restriction on small streams.
self.send_and_verify_topic_wildcard_mention_message("polonius", sub_count=10)
# There is no restriction on topics with less than 'Realm.WILDCARD_MENTION_THRESHOLD' participants.
self.send_and_verify_topic_wildcard_mention_message("polonius", topic_participant_count=10)
self.send_and_verify_topic_wildcard_mention_message("cordelia")
do_set_realm_property(
@ -1846,7 +1849,7 @@ class StreamMessagesTest(ZulipTestCase):
cordelia.date_joined = timezone_now()
cordelia.save()
self.send_and_verify_topic_wildcard_mention_message("cordelia", test_fails=True)
self.send_and_verify_topic_wildcard_mention_message("cordelia", sub_count=10)
self.send_and_verify_topic_wildcard_mention_message("cordelia", topic_participant_count=10)
# Administrators and moderators can use wildcard mentions even if they are new.
self.send_and_verify_topic_wildcard_mention_message("iago")
self.send_and_verify_topic_wildcard_mention_message("shiva")
@ -1862,7 +1865,7 @@ class StreamMessagesTest(ZulipTestCase):
acting_user=None,
)
self.send_and_verify_topic_wildcard_mention_message("cordelia", test_fails=True)
self.send_and_verify_topic_wildcard_mention_message("cordelia", sub_count=10)
self.send_and_verify_topic_wildcard_mention_message("cordelia", topic_participant_count=10)
self.send_and_verify_topic_wildcard_mention_message("shiva")
cordelia.date_joined = timezone_now()
@ -1871,15 +1874,15 @@ class StreamMessagesTest(ZulipTestCase):
realm, "wildcard_mention_policy", Realm.WILDCARD_MENTION_POLICY_ADMINS, acting_user=None
)
self.send_and_verify_topic_wildcard_mention_message("shiva", test_fails=True)
# There is no restriction on small streams.
self.send_and_verify_topic_wildcard_mention_message("shiva", sub_count=10)
# There is no restriction on topics with less than 'Realm.WILDCARD_MENTION_THRESHOLD' participants.
self.send_and_verify_topic_wildcard_mention_message("shiva", topic_participant_count=10)
self.send_and_verify_topic_wildcard_mention_message("iago")
do_set_realm_property(
realm, "wildcard_mention_policy", Realm.WILDCARD_MENTION_POLICY_NOBODY, acting_user=None
)
self.send_and_verify_topic_wildcard_mention_message("iago", test_fails=True)
self.send_and_verify_topic_wildcard_mention_message("iago", sub_count=10)
self.send_and_verify_topic_wildcard_mention_message("iago", topic_participant_count=10)
def send_and_verify_stream_wildcard_mention_message(
self, sender_name: str, test_fails: bool = False, sub_count: int = 16
@ -1895,7 +1898,7 @@ class StreamMessagesTest(ZulipTestCase):
else:
with self.assertRaisesRegex(
JsonableError,
"You do not have permission to use wildcard mentions in this stream.",
"You do not have permission to use stream wildcard mentions in this stream.",
):
self.send_stream_message(sender, "test_stream", content)