zulip/zerver/views/streams.py

768 lines
32 KiB
Python

from collections import defaultdict
from typing import (
Any,
Callable,
Dict,
Iterable,
List,
Mapping,
Optional,
Sequence,
Set,
Tuple,
Union,
)
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 override as override_language
from django.utils.translation import ugettext as _
from zerver.context_processors import get_valid_realm_from_request
from zerver.decorator import (
authenticated_json_view,
require_non_guest_user,
require_post,
require_realm_admin,
)
from zerver.lib.actions import (
bulk_add_subscriptions,
bulk_remove_subscriptions,
do_add_default_stream,
do_add_streams_to_default_stream_group,
do_change_default_stream_group_description,
do_change_default_stream_group_name,
do_change_stream_description,
do_change_stream_invite_only,
do_change_stream_message_retention_days,
do_change_stream_post_policy,
do_change_subscription_property,
do_create_default_stream_group,
do_deactivate_stream,
do_delete_messages,
do_get_streams,
do_remove_default_stream,
do_remove_default_stream_group,
do_remove_streams_from_default_stream_group,
do_rename_stream,
do_send_messages,
gather_subscriptions,
get_subscriber_emails,
internal_prep_private_message,
internal_prep_stream_message,
)
from zerver.lib.exceptions import ErrorCode, JsonableError, OrganizationOwnerRequired
from zerver.lib.request import REQ, has_request_variables
from zerver.lib.response import json_error, json_success
from zerver.lib.retention import parse_message_retention_days
from zerver.lib.streams import (
access_default_stream_group_by_id,
access_stream_by_id,
access_stream_by_name,
access_stream_for_delete_or_update,
access_web_public_stream,
check_stream_name,
check_stream_name_available,
filter_stream_authorization,
list_to_streams,
)
from zerver.lib.topic import (
get_topic_history_for_public_stream,
get_topic_history_for_stream,
messages_for_topic,
)
from zerver.lib.types import Validator
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 (
Realm,
Stream,
UserMessage,
UserProfile,
get_active_user,
get_active_user_profile_by_id_in_realm,
get_system_bot,
)
class PrincipalError(JsonableError):
code = ErrorCode.UNAUTHORIZED_PRINCIPAL
data_fields = ['principal']
http_status_code = 403
def __init__(self, principal: Union[int, str]) -> None:
self.principal: Union[int, str] = principal
@staticmethod
def msg_format() -> str:
return _("User not authorized to execute queries on behalf of '{principal}'")
def principal_to_user_profile(agent: UserProfile, principal: Union[str, int]) -> UserProfile:
try:
if isinstance(principal, str):
return get_active_user(principal, agent.realm)
else:
return get_active_user_profile_by_id_in_realm(principal, agent.realm)
except UserProfile.DoesNotExist:
# We have to make sure we don't leak information about which users
# are registered for Zulip in a different realm. We could do
# something a little more clever and check the domain part of the
# principal to maybe give a better error message
raise PrincipalError(principal)
def check_if_removing_someone_else(user_profile: UserProfile,
principals: Optional[Union[List[str], List[int]]]) -> bool:
if principals is None or len(principals) == 0:
return False
if len(principals) > 1:
return True
if isinstance(principals[0], int):
return principals[0] != user_profile.id
else:
return principals[0] != user_profile.email
def deactivate_stream_backend(request: HttpRequest,
user_profile: UserProfile,
stream_id: int) -> HttpResponse:
stream = access_stream_for_delete_or_update(user_profile, stream_id)
do_deactivate_stream(stream, acting_user=user_profile)
return json_success()
@require_realm_admin
@has_request_variables
def add_default_stream(request: HttpRequest,
user_profile: UserProfile,
stream_id: int=REQ(validator=check_int)) -> HttpResponse:
(stream, recipient, sub) = access_stream_by_id(user_profile, stream_id)
do_add_default_stream(stream)
return json_success()
@require_realm_admin
@has_request_variables
def create_default_stream_group(request: HttpRequest, user_profile: UserProfile,
group_name: str=REQ(), description: str=REQ(),
stream_names: List[str]=REQ(validator=check_list(check_string))) -> None:
streams = []
for stream_name in stream_names:
(stream, recipient, sub) = access_stream_by_name(user_profile, stream_name)
streams.append(stream)
do_create_default_stream_group(user_profile.realm, group_name, description, streams)
return json_success()
@require_realm_admin
@has_request_variables
def update_default_stream_group_info(request: HttpRequest, user_profile: UserProfile, group_id: int,
new_group_name: Optional[str]=REQ(validator=check_string, default=None),
new_description: Optional[str]=REQ(validator=check_string,
default=None)) -> None:
if not new_group_name and not new_description:
return json_error(_('You must pass "new_description" or "new_group_name".'))
group = access_default_stream_group_by_id(user_profile.realm, group_id)
if new_group_name is not None:
do_change_default_stream_group_name(user_profile.realm, group, new_group_name)
if new_description is not None:
do_change_default_stream_group_description(user_profile.realm, group, new_description)
return json_success()
@require_realm_admin
@has_request_variables
def update_default_stream_group_streams(request: HttpRequest, user_profile: UserProfile,
group_id: int, op: str=REQ(),
stream_names: List[str]=REQ(
validator=check_list(check_string))) -> None:
group = access_default_stream_group_by_id(user_profile.realm, group_id)
streams = []
for stream_name in stream_names:
(stream, recipient, sub) = access_stream_by_name(user_profile, stream_name)
streams.append(stream)
if op == 'add':
do_add_streams_to_default_stream_group(user_profile.realm, group, streams)
elif op == 'remove':
do_remove_streams_from_default_stream_group(user_profile.realm, group, streams)
else:
return json_error(_('Invalid value for "op". Specify one of "add" or "remove".'))
return json_success()
@require_realm_admin
@has_request_variables
def remove_default_stream_group(request: HttpRequest, user_profile: UserProfile,
group_id: int) -> None:
group = access_default_stream_group_by_id(user_profile.realm, group_id)
do_remove_default_stream_group(user_profile.realm, group)
return json_success()
@require_realm_admin
@has_request_variables
def remove_default_stream(request: HttpRequest,
user_profile: UserProfile,
stream_id: int=REQ(validator=check_int)) -> HttpResponse:
(stream, recipient, sub) = access_stream_by_id(
user_profile,
stream_id,
allow_realm_admin=True,
)
do_remove_default_stream(stream)
return json_success()
@has_request_variables
def update_stream_backend(
request: HttpRequest, user_profile: UserProfile,
stream_id: int,
description: Optional[str]=REQ(validator=check_capped_string(
Stream.MAX_DESCRIPTION_LENGTH), default=None),
is_private: Optional[bool]=REQ(validator=check_bool, default=None),
is_announcement_only: Optional[bool]=REQ(validator=check_bool, default=None),
stream_post_policy: Optional[int]=REQ(validator=check_int_in(
Stream.STREAM_POST_POLICY_TYPES), default=None),
history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None),
new_name: Optional[str]=REQ(validator=check_string, default=None),
message_retention_days: Optional[Union[int, str]]=REQ(validator=check_string_or_int, default=None),
) -> HttpResponse:
# We allow realm administrators to to update the stream name and
# description even for private streams.
stream = access_stream_for_delete_or_update(user_profile, stream_id)
if message_retention_days is not None:
if not user_profile.is_realm_owner:
raise OrganizationOwnerRequired()
user_profile.realm.ensure_not_on_limited_plan()
message_retention_days_value = parse_message_retention_days(
message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP)
do_change_stream_message_retention_days(stream, message_retention_days_value)
if description is not None:
if '\n' in description:
# We don't allow newline characters in stream descriptions.
description = description.replace("\n", " ")
do_change_stream_description(stream, description)
if new_name is not None:
new_name = new_name.strip()
if stream.name == new_name:
return json_error(_("Stream already has that name!"))
if stream.name.lower() != new_name.lower():
# Check that the stream name is available (unless we are
# are only changing the casing of the stream name).
check_stream_name_available(user_profile.realm, new_name)
do_rename_stream(stream, new_name, user_profile)
if is_announcement_only is not None:
# is_announcement_only is a legacy way to specify
# stream_post_policy. We can probably just delete this code,
# since we're not aware of clients that used it, but we're
# keeping it for backwards-compatibility for now.
stream_post_policy = Stream.STREAM_POST_POLICY_EVERYONE
if is_announcement_only:
stream_post_policy = Stream.STREAM_POST_POLICY_ADMINS
if stream_post_policy is not None:
do_change_stream_post_policy(stream, stream_post_policy)
# But we require even realm administrators to be actually
# subscribed to make a private stream public.
if is_private is not None:
(stream, recipient, sub) = access_stream_by_id(user_profile, stream_id)
do_change_stream_invite_only(stream, is_private, history_public_to_subscribers)
return json_success()
@has_request_variables
def list_subscriptions_backend(
request: HttpRequest,
user_profile: UserProfile,
include_subscribers: bool=REQ(validator=check_bool, default=False),
) -> HttpResponse:
subscribed, _ = gather_subscriptions(
user_profile, include_subscribers=include_subscribers,
)
return json_success({"subscriptions": subscribed})
FuncKwargPair = Tuple[Callable[..., HttpResponse], Dict[str, Union[int, Iterable[Any]]]]
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)),
],
),
)
remove_subscriptions_schema = check_list(check_string)
@has_request_variables
def update_subscriptions_backend(
request: HttpRequest, user_profile: UserProfile,
delete: Iterable[str]=REQ(validator=remove_subscriptions_schema, default=[]),
add: Iterable[Mapping[str, Any]]=REQ(validator=add_subscriptions_schema, default=[]),
) -> HttpResponse:
if not add and not delete:
return json_error(_('Nothing to do. Specify at least one of "add" or "delete".'))
method_kwarg_pairs: List[FuncKwargPair] = [
(add_subscriptions_backend, dict(streams_raw=add)),
(remove_subscriptions_backend, dict(streams_raw=delete)),
]
return compose_views(request, user_profile, method_kwarg_pairs)
def compose_views(
request: HttpRequest,
user_profile: UserProfile,
method_kwarg_pairs: "List[FuncKwargPair]",
) -> HttpResponse:
'''
This takes a series of view methods from method_kwarg_pairs and calls
them in sequence, and it smushes all the json results into a single
response when everything goes right. (This helps clients avoid extra
latency hops.) It rolls back the transaction when things go wrong in
any one of the composed methods.
TODO: Move this a utils-like module if we end up using it more widely.
'''
json_dict: Dict[str, Any] = {}
with transaction.atomic():
for method, kwargs in method_kwarg_pairs:
response = method(request, user_profile, **kwargs)
if response.status_code != 200:
raise JsonableError(response.content)
json_dict.update(orjson.loads(response.content))
return json_success(json_dict)
check_principals: Validator[Union[List[str], List[int]]] = check_union(
[check_list(check_string), check_list(check_int)],
)
@has_request_variables
def remove_subscriptions_backend(
request: HttpRequest, user_profile: UserProfile,
streams_raw: Iterable[str]=REQ("subscriptions", validator=remove_subscriptions_schema),
principals: Optional[Union[List[str], List[int]]]=REQ(validator=check_principals, default=None),
) -> HttpResponse:
removing_someone_else = check_if_removing_someone_else(user_profile, principals)
streams_as_dict = []
for stream_name in streams_raw:
streams_as_dict.append({"name": stream_name.strip()})
streams, __ = list_to_streams(streams_as_dict, user_profile,
admin_access_required=removing_someone_else)
if principals:
people_to_unsub = {principal_to_user_profile(
user_profile, principal) for principal in principals}
else:
people_to_unsub = {user_profile}
result: Dict[str, List[str]] = dict(removed=[], not_removed=[])
(removed, not_subscribed) = bulk_remove_subscriptions(people_to_unsub, streams,
request.client,
acting_user=user_profile)
for (subscriber, removed_stream) in removed:
result["removed"].append(removed_stream.name)
for (subscriber, not_subscribed_stream) in not_subscribed:
result["not_removed"].append(not_subscribed_stream.name)
return json_success(result)
def you_were_just_subscribed_message(acting_user: UserProfile,
recipient_user: UserProfile,
stream_names: Set[str]) -> str:
subscriptions = sorted(stream_names)
if len(subscriptions) == 1:
with override_language(recipient_user.default_language):
return _("{user_full_name} subscribed you to the stream {stream_name}.").format(
user_full_name=f"@**{acting_user.full_name}**",
stream_name=f"#**{subscriptions[0]}**",
)
with override_language(recipient_user.default_language):
message = _("{user_full_name} subscribed you to the following streams:").format(
user_full_name=f"@**{acting_user.full_name}**",
)
message += "\n\n"
for stream_name in subscriptions:
message += f"* #**{stream_name}**\n"
return message
RETENTION_DEFAULT: Union[str, int] = "realm_default"
EMPTY_PRINCIPALS: Union[Sequence[str], Sequence[int]] = []
@require_non_guest_user
@has_request_variables
def add_subscriptions_backend(
request: HttpRequest, user_profile: UserProfile,
streams_raw: Iterable[Dict[str, str]]=REQ("subscriptions", validator=add_subscriptions_schema),
invite_only: bool=REQ(validator=check_bool, default=False),
stream_post_policy: int=REQ(validator=check_int_in(
Stream.STREAM_POST_POLICY_TYPES), default=Stream.STREAM_POST_POLICY_EVERYONE),
history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None),
message_retention_days: Union[str, int]=REQ(validator=check_string_or_int,
default=RETENTION_DEFAULT),
announce: bool=REQ(validator=check_bool, default=False),
principals: Union[Sequence[str], Sequence[int]]=REQ(
validator=check_principals, default=EMPTY_PRINCIPALS,
),
authorization_errors_fatal: bool=REQ(validator=check_bool, default=True),
) -> HttpResponse:
stream_dicts = []
color_map = {}
for stream_dict 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 'description' in stream_dict:
# We don't allow newline characters in stream descriptions.
stream_dict['description'] = stream_dict['description'].replace("\n", " ")
stream_dict_copy: Dict[str, Any] = {}
for field in stream_dict:
stream_dict_copy[field] = stream_dict[field]
# Strip the stream name here.
stream_dict_copy['name'] = stream_dict_copy['name'].strip()
stream_dict_copy["invite_only"] = invite_only
stream_dict_copy["stream_post_policy"] = stream_post_policy
stream_dict_copy["history_public_to_subscribers"] = history_public_to_subscribers
stream_dict_copy["message_retention_days"] = parse_message_retention_days(
message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP)
stream_dicts.append(stream_dict_copy)
# Validation of the streams arguments, including enforcement of
# can_create_streams policy and check_stream_name policy is inside
# list_to_streams.
existing_streams, created_streams = \
list_to_streams(stream_dicts, user_profile, autocreate=True)
authorized_streams, unauthorized_streams = \
filter_stream_authorization(user_profile, existing_streams)
if len(unauthorized_streams) > 0 and authorization_errors_fatal:
return json_error(_("Unable to access stream ({stream_name}).").format(
stream_name=unauthorized_streams[0].name,
))
# Newly created streams are also authorized for the creator
streams = authorized_streams + created_streams
if len(principals) > 0:
if user_profile.realm.is_zephyr_mirror_realm and not all(stream.invite_only for stream in streams):
return json_error(_("You can only invite other Zephyr mirroring users to private streams."))
if not user_profile.can_subscribe_other_users():
if user_profile.realm.invite_to_stream_policy == Realm.POLICY_ADMINS_ONLY:
return json_error(_("Only administrators can modify other users' subscriptions."))
# Realm.POLICY_MEMBERS_ONLY only fails if the
# user is a guest, which happens in the decorator above.
assert user_profile.realm.invite_to_stream_policy == \
Realm.POLICY_FULL_MEMBERS_ONLY
return json_error(_("Your account is too new to modify other users' subscriptions."))
subscribers = {principal_to_user_profile(user_profile, principal) for principal in principals}
else:
subscribers = {user_profile}
(subscribed, already_subscribed) = bulk_add_subscriptions(streams, subscribers,
acting_user=user_profile, color_map=color_map)
# We can assume unique emails here for now, but we should eventually
# convert this function to be more id-centric.
email_to_user_profile: Dict[str, UserProfile] = {}
result: Dict[str, Any] = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list))
for (subscriber, stream) in subscribed:
result["subscribed"][subscriber.email].append(stream.name)
email_to_user_profile[subscriber.email] = subscriber
for (subscriber, stream) in already_subscribed:
result["already_subscribed"][subscriber.email].append(stream.name)
bots = {subscriber.email: subscriber.is_bot for subscriber in subscribers}
newly_created_stream_names = {s.name for s in created_streams}
# Inform the user if someone else subscribed them to stuff,
# or if a new stream was created with the "announce" option.
notifications = []
if len(principals) > 0 and result["subscribed"]:
for email, subscribed_stream_names in result["subscribed"].items():
if email == user_profile.email:
# Don't send a Zulip if you invited yourself.
continue
if bots[email]:
# Don't send invitation Zulips to bots
continue
# For each user, we notify them about newly subscribed streams, except for
# streams that were newly created.
notify_stream_names = set(subscribed_stream_names) - newly_created_stream_names
if not notify_stream_names:
continue
sender = get_system_bot(settings.NOTIFICATION_BOT)
recipient_user = email_to_user_profile[email]
msg = you_were_just_subscribed_message(
acting_user=user_profile,
recipient_user=recipient_user,
stream_names=notify_stream_names,
)
notifications.append(
internal_prep_private_message(
realm=user_profile.realm,
sender=sender,
recipient_user=recipient_user,
content=msg))
if announce and len(created_streams) > 0:
notifications_stream = user_profile.realm.get_notifications_stream()
if notifications_stream is not None:
with override_language(notifications_stream.realm.default_language):
if len(created_streams) > 1:
content = _("{user_name} created the following streams: {stream_str}.")
else:
content = _("{user_name} created a new stream {stream_str}.")
topic = _('new streams')
content = content.format(
user_name=f"@_**{user_profile.full_name}|{user_profile.id}**",
stream_str=", ".join(f'#**{s.name}**' for s in created_streams)
)
sender = get_system_bot(settings.NOTIFICATION_BOT)
notifications.append(
internal_prep_stream_message(
realm=user_profile.realm,
sender=sender,
stream=notifications_stream,
topic=topic,
content=content,
),
)
if not user_profile.realm.is_zephyr_mirror_realm and len(created_streams) > 0:
sender = get_system_bot(settings.NOTIFICATION_BOT)
for stream in created_streams:
with override_language(stream.realm.default_language):
notifications.append(
internal_prep_stream_message(
realm=user_profile.realm,
sender=sender,
stream=stream,
topic=Realm.STREAM_EVENTS_NOTIFICATION_TOPIC,
content=_('Stream created by {user_name}.').format(
user_name=f"@_**{user_profile.full_name}|{user_profile.id}**",
),
),
)
if len(notifications) > 0:
do_send_messages(notifications, mark_as_read=[user_profile.id])
result["subscribed"] = dict(result["subscribed"])
result["already_subscribed"] = dict(result["already_subscribed"])
if not authorization_errors_fatal:
result["unauthorized"] = [s.name for s in unauthorized_streams]
return json_success(result)
@has_request_variables
def get_subscribers_backend(request: HttpRequest, user_profile: UserProfile,
stream_id: int=REQ('stream', converter=to_non_negative_int)) -> HttpResponse:
(stream, recipient, sub) = access_stream_by_id(user_profile, stream_id,
allow_realm_admin=True)
subscribers = get_subscriber_emails(stream, user_profile)
return json_success({'subscribers': subscribers})
# 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
def get_streams_backend(
request: HttpRequest, user_profile: UserProfile,
include_public: bool=REQ(validator=check_bool, default=True),
include_web_public: bool=REQ(validator=check_bool, default=False),
include_subscribed: bool=REQ(validator=check_bool, default=True),
include_all_active: bool=REQ(validator=check_bool, default=False),
include_default: bool=REQ(validator=check_bool, default=False),
include_owner_subscribed: bool=REQ(validator=check_bool, default=False),
) -> HttpResponse:
streams = do_get_streams(user_profile, include_public=include_public,
include_web_public=include_web_public,
include_subscribed=include_subscribed,
include_all_active=include_all_active,
include_default=include_default,
include_owner_subscribed=include_owner_subscribed)
return json_success({"streams": streams})
@has_request_variables
def get_topics_backend(
request: HttpRequest, maybe_user_profile: Union[UserProfile, AnonymousUser],
stream_id: int=REQ(converter=to_non_negative_int,
path_only=True)) -> HttpResponse:
if not maybe_user_profile.is_authenticated:
is_web_public_query = True
user_profile: Optional[UserProfile] = None
else:
is_web_public_query = False
assert isinstance(maybe_user_profile, UserProfile)
user_profile = maybe_user_profile
assert user_profile is not None
if is_web_public_query:
realm = get_valid_realm_from_request(request)
stream = access_web_public_stream(stream_id, realm)
result = get_topic_history_for_public_stream(recipient=stream.recipient)
else:
assert user_profile is not None
(stream, recipient, sub) = access_stream_by_id(user_profile, stream_id)
result = get_topic_history_for_stream(
user_profile=user_profile,
recipient=recipient,
public_history=stream.is_history_public_to_subscribers(),
)
return json_success(dict(topics=result))
@require_realm_admin
@has_request_variables
def delete_in_topic(request: HttpRequest, user_profile: UserProfile,
stream_id: int=REQ(converter=to_non_negative_int),
topic_name: str=REQ("topic_name")) -> HttpResponse:
(stream, recipient, sub) = access_stream_by_id(user_profile, stream_id)
messages = messages_for_topic(stream.recipient_id, topic_name)
if not stream.is_history_public_to_subscribers():
# Don't allow the user to delete messages that they don't have access to.
deletable_message_ids = UserMessage.objects.filter(
user_profile=user_profile, message_id__in=messages).values_list("message_id", flat=True)
messages = [message for message in messages if message.id in
deletable_message_ids]
do_delete_messages(user_profile.realm, messages)
return json_success()
@require_post
@authenticated_json_view
@has_request_variables
def json_stream_exists(request: HttpRequest, user_profile: UserProfile, stream_name: str=REQ("stream"),
autosubscribe: bool=REQ(validator=check_bool, default=False)) -> HttpResponse:
check_stream_name(stream_name)
try:
(stream, recipient, sub) = access_stream_by_name(user_profile, stream_name)
except JsonableError as e:
return json_error(e.msg, status=404)
# access_stream functions return a subscription if and only if we
# are already subscribed.
result = {"subscribed": sub is not None}
# If we got here, we're either subscribed or the stream is public.
# So if we're not yet subscribed and autosubscribe is enabled, we
# should join.
if sub is None and autosubscribe:
bulk_add_subscriptions([stream], [user_profile], acting_user=user_profile)
result["subscribed"] = True
return json_success(result) # results are ignored for HEAD requests
@has_request_variables
def json_get_stream_id(request: HttpRequest,
user_profile: UserProfile,
stream_name: str=REQ('stream')) -> HttpResponse:
(stream, recipient, sub) = access_stream_by_name(user_profile, stream_name)
return json_success({'stream_id': stream.id})
@has_request_variables
def update_subscriptions_property(request: HttpRequest,
user_profile: UserProfile,
stream_id: int=REQ(validator=check_int),
property: str=REQ(),
value: str=REQ()) -> HttpResponse:
subscription_data = [{"property": property,
"stream_id": stream_id,
"value": value}]
return update_subscription_properties_backend(request, user_profile,
subscription_data=subscription_data)
@has_request_variables
def update_subscription_properties_backend(
request: HttpRequest, user_profile: UserProfile,
subscription_data: List[Dict[str, Any]]=REQ(
validator=check_list(
check_dict([("stream_id", check_int),
("property", check_string),
("value", check_union([check_string, check_bool]))]),
),
),
) -> HttpResponse:
"""
This is the entry point to changing subscription properties. This
is a bulk endpoint: requestors always provide a subscription_data
list containing dictionaries for each stream of interest.
Requests are of the form:
[{"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}
response_data = []
for change in subscription_data:
stream_id = change["stream_id"]
property = change["property"]
value = change["value"]
if property not in property_converters:
return json_error(_("Unknown subscription property: {}").format(property))
(stream, recipient, sub) = access_stream_by_id(user_profile, stream_id)
if sub is None:
return json_error(_("Not subscribed to stream id {}").format(stream_id))
try:
value = property_converters[property](property, value)
except ValidationError as error:
return json_error(error.message)
do_change_subscription_property(user_profile, sub, stream,
property, value, acting_user=user_profile)
response_data.append({'stream_id': stream_id,
'property': property,
'value': value})
return json_success({"subscription_data": response_data})