mirror of https://github.com/zulip/zulip.git
streams: Convert to typed_enpoint.
This commit is contained in:
parent
025fba0a11
commit
8b489f4b96
|
@ -376,7 +376,12 @@ def parse_value_for_parameter(parameter: FuncParam[T], value: object) -> T:
|
|||
elif error["type"] == "value_error":
|
||||
context["msg"] = error["msg"]
|
||||
error_template = _("Invalid {var_name}: {msg}")
|
||||
|
||||
elif error["type"] == "model_type":
|
||||
context["msg"] = error["msg"]
|
||||
error_template = _("Invalid {var_name}: {msg}")
|
||||
elif error["type"] == "missing":
|
||||
context["msg"] = error["msg"]
|
||||
error_template = _("{var_name} field is missing: {msg}")
|
||||
assert error_template is not None, MISSING_ERROR_TEMPLATE.format(
|
||||
error_type=error["type"],
|
||||
url=error.get("url", "(documentation unavailable)"),
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import re
|
||||
import zoneinfo
|
||||
from collections.abc import Collection
|
||||
|
||||
|
@ -83,3 +84,12 @@ def to_non_negative_int_or_none(s: str) -> NonNegativeInt | None:
|
|||
# integer, and we want to return None in that case.
|
||||
def non_negative_int_or_none_validator() -> BeforeValidator:
|
||||
return BeforeValidator(lambda s: to_non_negative_int_or_none(s))
|
||||
|
||||
|
||||
def check_color(var_name: str, val: object) -> str:
|
||||
s = str(val)
|
||||
valid_color_pattern = re.compile(r"^#([a-fA-F0-9]{3,6})$")
|
||||
matched_results = valid_color_pattern.match(s)
|
||||
if not matched_results:
|
||||
raise ValueError(_("{var_name} is not a valid hex color code").format(var_name=var_name))
|
||||
return s
|
||||
|
|
|
@ -3403,7 +3403,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
|||
).decode()
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, "color is not a valid hex color code")
|
||||
self.assert_json_error(
|
||||
result, "Invalid subscription_data[0]: Value error, color is not a valid hex color code"
|
||||
)
|
||||
|
||||
def test_set_color_missing_stream_id(self) -> None:
|
||||
"""
|
||||
|
@ -3420,7 +3422,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
|||
).decode()
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, "stream_id key is missing from subscription_data[0]")
|
||||
self.assert_json_error(
|
||||
result, 'subscription_data[0]["stream_id"] field is missing: Field required'
|
||||
)
|
||||
|
||||
def test_set_color_unsubscribed_stream_id(self) -> None:
|
||||
"""
|
||||
|
@ -3468,7 +3472,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
|||
).decode()
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, "value key is missing from subscription_data[0]")
|
||||
self.assert_json_error(
|
||||
result, 'subscription_data[0]["value"] field is missing: Field required'
|
||||
)
|
||||
|
||||
def test_set_stream_wildcard_mentions_notify(self) -> None:
|
||||
"""
|
||||
|
@ -3733,7 +3739,9 @@ class SubscriptionPropertiesTest(ZulipTestCase):
|
|||
).decode()
|
||||
},
|
||||
)
|
||||
self.assert_json_error(result, f"{property_name} is not a string")
|
||||
self.assert_json_error(
|
||||
result, "Invalid subscription_data[0]: Value error, color is not a valid hex color code"
|
||||
)
|
||||
|
||||
def test_json_subscription_property_invalid_stream(self) -> None:
|
||||
test_user = self.example_user("hamlet")
|
||||
|
@ -3838,7 +3846,9 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
|||
# incorrect color format
|
||||
subscriptions = [{"name": "my_test_stream_3", "color": "#0g0g0g"}]
|
||||
result = self.common_subscribe_to_streams(user, subscriptions, allow_fail=True)
|
||||
self.assert_json_error(result, 'subscriptions[0]["color"] is not a valid hex color code')
|
||||
self.assert_json_error(
|
||||
result, "Invalid subscriptions[0]: Value error, add.color is not a valid hex color code"
|
||||
)
|
||||
|
||||
def test_api_valid_property(self) -> None:
|
||||
"""
|
||||
|
@ -3881,7 +3891,7 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
|||
result = self.api_patch(
|
||||
user,
|
||||
"/api/v1/users/me/subscriptions/121",
|
||||
{"property": "is_muted", "value": "somevalue"},
|
||||
{"property": "is_muted", "value": orjson.dumps(True).decode()},
|
||||
)
|
||||
self.assert_json_error(result, "Invalid channel ID")
|
||||
|
||||
|
@ -3896,8 +3906,11 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
|||
result = self.api_patch(user, "/api/v1/users/me/subscriptions", request)
|
||||
self.assert_json_error(result, expected_message)
|
||||
|
||||
check_for_error(["foo"], "add[0] is not a dict")
|
||||
check_for_error([{"bogus": "foo"}], "name key is missing from add[0]")
|
||||
check_for_error(
|
||||
["foo"],
|
||||
"Invalid add[0]: Input should be a valid dictionary or instance of AddSubscriptionData",
|
||||
)
|
||||
check_for_error([{"bogus": "foo"}], 'add[0]["name"] field is missing: Field required')
|
||||
check_for_error([{"name": {}}], 'add[0]["name"] is not a string')
|
||||
|
||||
def test_bad_principals(self) -> None:
|
||||
|
@ -3909,7 +3922,7 @@ class SubscriptionRestApiTest(ZulipTestCase):
|
|||
"principals": orjson.dumps([{}]).decode(),
|
||||
}
|
||||
result = self.api_patch(user, "/api/v1/users/me/subscriptions", request)
|
||||
self.assert_json_error(result, "principals is not an allowed_type")
|
||||
self.assert_json_error(result, 'principals["list[str]"][0] is not a string')
|
||||
|
||||
def test_bad_delete_parameters(self) -> None:
|
||||
user = self.example_user("hamlet")
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import time
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable, Mapping, Sequence
|
||||
from typing import Any
|
||||
from collections.abc import Callable
|
||||
from typing import Annotated, Any
|
||||
|
||||
import orjson
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import override as override_language
|
||||
from pydantic import BaseModel, Field, Json, NonNegativeInt, StringConstraints, model_validator
|
||||
|
||||
from zerver.actions.default_streams import (
|
||||
do_add_default_stream,
|
||||
|
@ -48,7 +48,6 @@ from zerver.lib.email_mirror_helpers import encode_email_address
|
|||
from zerver.lib.exceptions import JsonableError, OrganizationOwnerRequiredError
|
||||
from zerver.lib.mention import MentionBackend, silent_mention_syntax_for_user
|
||||
from zerver.lib.message import bulk_access_stream_messages_query
|
||||
from zerver.lib.request import REQ, has_request_variables
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.retention import STREAM_MESSAGE_BATCH_SIZE as RETENTION_STREAM_MESSAGE_BATCH_SIZE
|
||||
from zerver.lib.retention import parse_message_retention_days
|
||||
|
@ -73,24 +72,11 @@ from zerver.lib.topic import (
|
|||
get_topic_history_for_stream,
|
||||
messages_for_topic,
|
||||
)
|
||||
from zerver.lib.types import Validator
|
||||
from zerver.lib.typed_endpoint import ApiParamConfig, PathOnly, typed_endpoint
|
||||
from zerver.lib.typed_endpoint_validators import check_color, check_int_in_validator
|
||||
from zerver.lib.user_groups import access_user_group_for_setting
|
||||
from zerver.lib.users import access_user_by_email, access_user_by_id
|
||||
from zerver.lib.utils import assert_is_not_none
|
||||
from zerver.lib.validator import (
|
||||
check_bool,
|
||||
check_capped_string,
|
||||
check_color,
|
||||
check_dict,
|
||||
check_dict_only,
|
||||
check_int,
|
||||
check_int_in,
|
||||
check_list,
|
||||
check_string,
|
||||
check_string_or_int,
|
||||
check_union,
|
||||
to_non_negative_int,
|
||||
)
|
||||
from zerver.models import NamedUserGroup, Realm, Stream, UserProfile
|
||||
from zerver.models.users import get_system_bot
|
||||
|
||||
|
@ -125,9 +111,9 @@ def deactivate_stream_backend(
|
|||
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def add_default_stream(
|
||||
request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(json_validator=check_int)
|
||||
request: HttpRequest, user_profile: UserProfile, *, stream_id: Json[int]
|
||||
) -> HttpResponse:
|
||||
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
||||
if stream.invite_only:
|
||||
|
@ -137,13 +123,14 @@ def add_default_stream(
|
|||
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def create_default_stream_group(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
group_name: str = REQ(),
|
||||
description: str = REQ(),
|
||||
stream_names: list[str] = REQ(json_validator=check_list(check_string)),
|
||||
*,
|
||||
group_name: str,
|
||||
description: str,
|
||||
stream_names: Json[list[str]],
|
||||
) -> HttpResponse:
|
||||
streams = []
|
||||
for stream_name in stream_names:
|
||||
|
@ -154,13 +141,14 @@ def create_default_stream_group(
|
|||
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def update_default_stream_group_info(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
group_id: int,
|
||||
new_group_name: str | None = REQ(default=None),
|
||||
new_description: str | None = REQ(default=None),
|
||||
*,
|
||||
group_id: PathOnly[int],
|
||||
new_group_name: str | None = None,
|
||||
new_description: str | None = None,
|
||||
) -> HttpResponse:
|
||||
if not new_group_name and not new_description:
|
||||
raise JsonableError(_('You must pass "new_description" or "new_group_name".'))
|
||||
|
@ -174,13 +162,14 @@ def update_default_stream_group_info(
|
|||
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def update_default_stream_group_streams(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
group_id: int,
|
||||
op: str = REQ(),
|
||||
stream_names: list[str] = REQ(json_validator=check_list(check_string)),
|
||||
*,
|
||||
group_id: PathOnly[int],
|
||||
op: str,
|
||||
stream_names: Json[list[str]],
|
||||
) -> HttpResponse:
|
||||
group = access_default_stream_group_by_id(user_profile.realm, group_id)
|
||||
streams = []
|
||||
|
@ -198,9 +187,9 @@ def update_default_stream_group_streams(
|
|||
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def remove_default_stream_group(
|
||||
request: HttpRequest, user_profile: UserProfile, group_id: int
|
||||
request: HttpRequest, user_profile: UserProfile, *, group_id: PathOnly[int]
|
||||
) -> HttpResponse:
|
||||
group = access_default_stream_group_by_id(user_profile.realm, group_id)
|
||||
do_remove_default_stream_group(user_profile.realm, group)
|
||||
|
@ -208,9 +197,9 @@ def remove_default_stream_group(
|
|||
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def remove_default_stream(
|
||||
request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(json_validator=check_int)
|
||||
request: HttpRequest, user_profile: UserProfile, *, stream_id: Json[int]
|
||||
) -> HttpResponse:
|
||||
(stream, sub) = access_stream_by_id(
|
||||
user_profile,
|
||||
|
@ -221,29 +210,31 @@ def remove_default_stream(
|
|||
return json_success(request)
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def update_stream_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
stream_id: int,
|
||||
description: str | None = REQ(
|
||||
str_validator=check_capped_string(Stream.MAX_DESCRIPTION_LENGTH), default=None
|
||||
),
|
||||
is_private: bool | None = REQ(json_validator=check_bool, default=None),
|
||||
is_announcement_only: bool | None = REQ(json_validator=check_bool, default=None),
|
||||
is_default_stream: bool | None = REQ(json_validator=check_bool, default=None),
|
||||
stream_post_policy: int | None = REQ(
|
||||
json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES), default=None
|
||||
),
|
||||
history_public_to_subscribers: bool | None = REQ(json_validator=check_bool, default=None),
|
||||
is_web_public: bool | None = REQ(json_validator=check_bool, default=None),
|
||||
new_name: str | None = REQ(default=None),
|
||||
message_retention_days: int | str | None = REQ(
|
||||
json_validator=check_string_or_int, default=None
|
||||
),
|
||||
can_remove_subscribers_group_id: int | None = REQ(
|
||||
"can_remove_subscribers_group", json_validator=check_int, default=None
|
||||
),
|
||||
*,
|
||||
stream_id: PathOnly[int],
|
||||
description: Annotated[str, StringConstraints(max_length=Stream.MAX_DESCRIPTION_LENGTH)]
|
||||
| None = None,
|
||||
is_private: Json[bool] | None = None,
|
||||
is_announcement_only: Json[bool] | None = None,
|
||||
is_default_stream: Json[bool] | None = None,
|
||||
stream_post_policy: Json[
|
||||
Annotated[
|
||||
int,
|
||||
check_int_in_validator(Stream.STREAM_POST_POLICY_TYPES),
|
||||
]
|
||||
]
|
||||
| None = None,
|
||||
history_public_to_subscribers: Json[bool] | None = None,
|
||||
is_web_public: Json[bool] | None = None,
|
||||
new_name: str | None = None,
|
||||
message_retention_days: Json[str] | Json[int] | None = None,
|
||||
can_remove_subscribers_group_id: Annotated[
|
||||
Json[int | None], ApiParamConfig("can_remove_subscribers_group")
|
||||
] = None,
|
||||
) -> HttpResponse:
|
||||
# We allow realm administrators to to update the stream name and
|
||||
# description even for private streams.
|
||||
|
@ -397,11 +388,12 @@ def update_stream_backend(
|
|||
return json_success(request)
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def list_subscriptions_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
include_subscribers: bool = REQ(json_validator=check_bool, default=False),
|
||||
*,
|
||||
include_subscribers: Json[bool] = False,
|
||||
) -> HttpResponse:
|
||||
subscribed, _ = gather_subscriptions(
|
||||
user_profile,
|
||||
|
@ -410,26 +402,32 @@ def list_subscriptions_backend(
|
|||
return json_success(request, data={"subscriptions": subscribed})
|
||||
|
||||
|
||||
add_subscriptions_schema = check_list(
|
||||
check_dict_only(
|
||||
required_keys=[("name", check_string)],
|
||||
optional_keys=[
|
||||
("color", check_color),
|
||||
("description", check_capped_string(Stream.MAX_DESCRIPTION_LENGTH)),
|
||||
],
|
||||
),
|
||||
)
|
||||
class AddSubscriptionData(BaseModel):
|
||||
name: str
|
||||
color: str | None = None
|
||||
description: (
|
||||
Annotated[str, StringConstraints(max_length=Stream.MAX_DESCRIPTION_LENGTH)] | None
|
||||
) = None
|
||||
|
||||
remove_subscriptions_schema = check_list(check_string)
|
||||
@model_validator(mode="after")
|
||||
def validate_terms(self) -> "AddSubscriptionData":
|
||||
if self.color is not None:
|
||||
self.color = check_color("add.color", self.color)
|
||||
return self
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def update_subscriptions_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
delete: Sequence[str] = REQ(json_validator=remove_subscriptions_schema, default=[]),
|
||||
add: Sequence[Mapping[str, str]] = REQ(json_validator=add_subscriptions_schema, default=[]),
|
||||
*,
|
||||
delete: Json[list[str]] | None = None,
|
||||
add: Json[list[AddSubscriptionData]] | None = None,
|
||||
) -> HttpResponse:
|
||||
if delete is None:
|
||||
delete = []
|
||||
if add is None:
|
||||
add = []
|
||||
if not add and not delete:
|
||||
raise JsonableError(_('Nothing to do. Specify at least one of "add" or "delete".'))
|
||||
|
||||
|
@ -459,17 +457,13 @@ def compose_views(thunks: list[Callable[[], HttpResponse]]) -> dict[str, Any]:
|
|||
return json_dict
|
||||
|
||||
|
||||
check_principals: Validator[list[str] | list[int]] = check_union(
|
||||
[check_list(check_string), check_list(check_int)],
|
||||
)
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def remove_subscriptions_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
streams_raw: Sequence[str] = REQ("subscriptions", json_validator=remove_subscriptions_schema),
|
||||
principals: list[str] | list[int] | None = REQ(json_validator=check_principals, default=None),
|
||||
*,
|
||||
streams_raw: Annotated[Json[list[str]], ApiParamConfig("subscriptions")],
|
||||
principals: Json[list[str] | list[int]] | None = None,
|
||||
) -> HttpResponse:
|
||||
realm = user_profile.realm
|
||||
|
||||
|
@ -529,42 +523,36 @@ def you_were_just_subscribed_message(
|
|||
|
||||
|
||||
RETENTION_DEFAULT: str | int = "realm_default"
|
||||
EMPTY_PRINCIPALS: Sequence[str] | Sequence[int] = []
|
||||
|
||||
|
||||
@transaction.atomic(savepoint=False)
|
||||
@require_non_guest_user
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def add_subscriptions_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
streams_raw: Sequence[Mapping[str, str]] = REQ(
|
||||
"subscriptions", json_validator=add_subscriptions_schema
|
||||
),
|
||||
invite_only: bool = REQ(json_validator=check_bool, default=False),
|
||||
is_web_public: bool = REQ(json_validator=check_bool, default=False),
|
||||
is_default_stream: bool = REQ(json_validator=check_bool, default=False),
|
||||
stream_post_policy: int = REQ(
|
||||
json_validator=check_int_in(Stream.STREAM_POST_POLICY_TYPES),
|
||||
default=Stream.STREAM_POST_POLICY_EVERYONE,
|
||||
),
|
||||
history_public_to_subscribers: bool | None = REQ(json_validator=check_bool, default=None),
|
||||
message_retention_days: str | int = REQ(
|
||||
json_validator=check_string_or_int, default=RETENTION_DEFAULT
|
||||
),
|
||||
can_remove_subscribers_group_id: int | None = REQ(
|
||||
"can_remove_subscribers_group", json_validator=check_int, default=None
|
||||
),
|
||||
announce: bool = REQ(json_validator=check_bool, default=False),
|
||||
principals: Sequence[str] | Sequence[int] = REQ(
|
||||
json_validator=check_principals,
|
||||
default=EMPTY_PRINCIPALS,
|
||||
),
|
||||
authorization_errors_fatal: bool = REQ(json_validator=check_bool, default=True),
|
||||
*,
|
||||
streams_raw: Annotated[Json[list[AddSubscriptionData]], ApiParamConfig("subscriptions")],
|
||||
invite_only: Json[bool] = False,
|
||||
is_web_public: Json[bool] = False,
|
||||
is_default_stream: Json[bool] = False,
|
||||
stream_post_policy: Json[
|
||||
Annotated[int, check_int_in_validator(Stream.STREAM_POST_POLICY_TYPES)]
|
||||
] = Stream.STREAM_POST_POLICY_EVERYONE,
|
||||
history_public_to_subscribers: Json[bool] | None = None,
|
||||
message_retention_days: Json[str] | Json[int] = RETENTION_DEFAULT,
|
||||
can_remove_subscribers_group_id: Annotated[
|
||||
Json[int | None], ApiParamConfig("can_remove_subscribers_group")
|
||||
] = None,
|
||||
announce: Json[bool] = False,
|
||||
principals: Json[list[str] | list[int]] | None = None,
|
||||
authorization_errors_fatal: Json[bool] = True,
|
||||
) -> HttpResponse:
|
||||
realm = user_profile.realm
|
||||
stream_dicts = []
|
||||
color_map = {}
|
||||
if principals is None:
|
||||
principals = []
|
||||
|
||||
if can_remove_subscribers_group_id is not None:
|
||||
permission_configuration = Stream.stream_permission_group_settings[
|
||||
|
@ -586,18 +574,18 @@ def add_subscriptions_backend(
|
|||
is_system_group=True,
|
||||
)
|
||||
|
||||
for stream_dict in streams_raw:
|
||||
for stream_obj in streams_raw:
|
||||
# 'color' field is optional
|
||||
# check for its presence in the streams_raw first
|
||||
if "color" in stream_dict:
|
||||
color_map[stream_dict["name"]] = stream_dict["color"]
|
||||
if stream_obj.color is not None:
|
||||
color_map[stream_obj.name] = stream_obj.color
|
||||
|
||||
stream_dict_copy: StreamDict = {}
|
||||
stream_dict_copy["name"] = stream_dict["name"].strip()
|
||||
stream_dict_copy["name"] = stream_obj.name.strip()
|
||||
|
||||
# We don't allow newline characters in stream descriptions.
|
||||
if "description" in stream_dict:
|
||||
stream_dict_copy["description"] = stream_dict["description"].replace("\n", " ")
|
||||
if stream_obj.description is not None:
|
||||
stream_dict_copy["description"] = stream_obj.description.replace("\n", " ")
|
||||
|
||||
stream_dict_copy["invite_only"] = invite_only
|
||||
stream_dict_copy["is_web_public"] = is_web_public
|
||||
|
@ -815,11 +803,12 @@ def send_messages_for_new_subscribers(
|
|||
do_send_messages(notifications, mark_as_read=[user_profile.id])
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def get_subscribers_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
stream_id: int = REQ("stream", converter=to_non_negative_int, path_only=True),
|
||||
*,
|
||||
stream_id: Annotated[NonNegativeInt, ApiParamConfig("stream", path_only=True)],
|
||||
) -> HttpResponse:
|
||||
(stream, sub) = access_stream_by_id(
|
||||
user_profile,
|
||||
|
@ -833,16 +822,17 @@ def get_subscribers_backend(
|
|||
|
||||
# By default, lists all streams that the user has access to --
|
||||
# i.e. public streams plus invite-only streams that the user is on
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def get_streams_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
include_public: bool = REQ(json_validator=check_bool, default=True),
|
||||
include_web_public: bool = REQ(json_validator=check_bool, default=False),
|
||||
include_subscribed: bool = REQ(json_validator=check_bool, default=True),
|
||||
include_all_active: bool = REQ(json_validator=check_bool, default=False),
|
||||
include_default: bool = REQ(json_validator=check_bool, default=False),
|
||||
include_owner_subscribed: bool = REQ(json_validator=check_bool, default=False),
|
||||
*,
|
||||
include_public: Json[bool] = True,
|
||||
include_web_public: Json[bool] = False,
|
||||
include_subscribed: Json[bool] = True,
|
||||
include_all_active: Json[bool] = False,
|
||||
include_default: Json[bool] = False,
|
||||
include_owner_subscribed: Json[bool] = False,
|
||||
) -> HttpResponse:
|
||||
streams = do_get_streams(
|
||||
user_profile,
|
||||
|
@ -856,11 +846,12 @@ def get_streams_backend(
|
|||
return json_success(request, data={"streams": streams})
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def get_stream_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
stream_id: int,
|
||||
*,
|
||||
stream_id: PathOnly[int],
|
||||
) -> HttpResponse:
|
||||
(stream, sub) = access_stream_by_id(user_profile, stream_id, allow_realm_admin=True)
|
||||
|
||||
|
@ -868,11 +859,12 @@ def get_stream_backend(
|
|||
return json_success(request, data={"stream": stream_to_dict(stream, recent_traffic)})
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def get_topics_backend(
|
||||
request: HttpRequest,
|
||||
maybe_user_profile: UserProfile | AnonymousUser,
|
||||
stream_id: int = REQ(converter=to_non_negative_int, path_only=True),
|
||||
*,
|
||||
stream_id: PathOnly[NonNegativeInt],
|
||||
) -> HttpResponse:
|
||||
if not maybe_user_profile.is_authenticated:
|
||||
is_web_public_query = True
|
||||
|
@ -906,12 +898,13 @@ def get_topics_backend(
|
|||
|
||||
|
||||
@require_realm_admin
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def delete_in_topic(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
stream_id: int = REQ(converter=to_non_negative_int, path_only=True),
|
||||
topic_name: str = REQ("topic_name"),
|
||||
*,
|
||||
stream_id: PathOnly[NonNegativeInt],
|
||||
topic_name: str,
|
||||
) -> HttpResponse:
|
||||
stream, ignored_sub = access_stream_by_id(user_profile, stream_id)
|
||||
|
||||
|
@ -943,43 +936,70 @@ def delete_in_topic(
|
|||
return json_success(request, data={"complete": True})
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def json_get_stream_id(
|
||||
request: HttpRequest, user_profile: UserProfile, stream_name: str = REQ("stream")
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
*,
|
||||
stream_name: Annotated[str, ApiParamConfig("stream")],
|
||||
) -> HttpResponse:
|
||||
(stream, sub) = access_stream_by_name(user_profile, stream_name)
|
||||
return json_success(request, data={"stream_id": stream.id})
|
||||
|
||||
|
||||
@has_request_variables
|
||||
class SubscriptionPropertyChangeRequest(BaseModel):
|
||||
stream_id: int
|
||||
property: str
|
||||
value: bool | str
|
||||
|
||||
@model_validator(mode="after")
|
||||
def validate_terms(self) -> "SubscriptionPropertyChangeRequest":
|
||||
boolean_properties = {
|
||||
"in_home_view",
|
||||
"is_muted",
|
||||
"desktop_notifications",
|
||||
"audible_notifications",
|
||||
"push_notifications",
|
||||
"email_notifications",
|
||||
"pin_to_top",
|
||||
"wildcard_mentions_notify",
|
||||
}
|
||||
|
||||
if self.property == "color":
|
||||
self.value = check_color("color", self.value)
|
||||
elif self.property in boolean_properties:
|
||||
if not isinstance(self.value, bool):
|
||||
raise JsonableError(_("{property} is not a boolean").format(property=self.property))
|
||||
else:
|
||||
raise JsonableError(
|
||||
_("Unknown subscription property: {property}").format(property=self.property)
|
||||
)
|
||||
return self
|
||||
|
||||
|
||||
@typed_endpoint
|
||||
def update_subscriptions_property(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
stream_id: int = REQ(json_validator=check_int),
|
||||
property: str = REQ(),
|
||||
value: str = REQ(),
|
||||
*,
|
||||
stream_id: PathOnly[Json[int]],
|
||||
property: str,
|
||||
value: Annotated[Json[bool] | str, Field(union_mode="left_to_right")],
|
||||
) -> HttpResponse:
|
||||
subscription_data = [{"property": property, "stream_id": stream_id, "value": value}]
|
||||
change_request = SubscriptionPropertyChangeRequest(
|
||||
stream_id=stream_id, property=property, value=value
|
||||
)
|
||||
return update_subscription_properties_backend(
|
||||
request, user_profile, subscription_data=subscription_data
|
||||
request, user_profile, subscription_data=[change_request]
|
||||
)
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def update_subscription_properties_backend(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
subscription_data: list[dict[str, Any]] = REQ(
|
||||
json_validator=check_list(
|
||||
check_dict(
|
||||
[
|
||||
("stream_id", check_int),
|
||||
("property", check_string),
|
||||
("value", check_union([check_string, check_bool])),
|
||||
]
|
||||
),
|
||||
),
|
||||
),
|
||||
*,
|
||||
subscription_data: Json[list[SubscriptionPropertyChangeRequest]],
|
||||
) -> HttpResponse:
|
||||
"""
|
||||
This is the entry point to changing subscription properties. This
|
||||
|
@ -991,27 +1011,11 @@ def update_subscription_properties_backend(
|
|||
[{"stream_id": "1", "property": "is_muted", "value": False},
|
||||
{"stream_id": "1", "property": "color", "value": "#c2c2c2"}]
|
||||
"""
|
||||
property_converters = {
|
||||
"color": check_color,
|
||||
"in_home_view": check_bool,
|
||||
"is_muted": check_bool,
|
||||
"desktop_notifications": check_bool,
|
||||
"audible_notifications": check_bool,
|
||||
"push_notifications": check_bool,
|
||||
"email_notifications": check_bool,
|
||||
"pin_to_top": check_bool,
|
||||
"wildcard_mentions_notify": check_bool,
|
||||
}
|
||||
|
||||
for change in subscription_data:
|
||||
stream_id = change["stream_id"]
|
||||
property = change["property"]
|
||||
value = change["value"]
|
||||
|
||||
if property not in property_converters:
|
||||
raise JsonableError(
|
||||
_("Unknown subscription property: {property}").format(property=property)
|
||||
)
|
||||
stream_id = change.stream_id
|
||||
property = change.property
|
||||
value = change.value
|
||||
|
||||
(stream, sub) = access_stream_by_id(user_profile, stream_id)
|
||||
if sub is None:
|
||||
|
@ -1019,11 +1023,6 @@ def update_subscription_properties_backend(
|
|||
_("Not subscribed to channel ID {channel_id}").format(channel_id=stream_id)
|
||||
)
|
||||
|
||||
try:
|
||||
value = property_converters[property](property, value)
|
||||
except ValidationError as error:
|
||||
raise JsonableError(error.message)
|
||||
|
||||
do_change_subscription_property(
|
||||
user_profile, sub, stream, property, value, acting_user=user_profile
|
||||
)
|
||||
|
@ -1031,11 +1030,12 @@ def update_subscription_properties_backend(
|
|||
return json_success(request)
|
||||
|
||||
|
||||
@has_request_variables
|
||||
@typed_endpoint
|
||||
def get_stream_email_address(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
stream_id: int = REQ("stream", converter=to_non_negative_int, path_only=True),
|
||||
*,
|
||||
stream_id: Annotated[NonNegativeInt, ApiParamConfig("stream", path_only=True)],
|
||||
) -> HttpResponse:
|
||||
(stream, sub) = access_stream_by_id(
|
||||
user_profile,
|
||||
|
|
Loading…
Reference in New Issue