def add_alert_words( request: HttpRequest, user_profile: UserProfile, alert_words: List[str] = REQ(json_validator=check_list(check_capped_string(100))), ) -> HttpResponse: do_add_alert_words(user_profile, clean_alert_words(alert_words)) return json_success(request, data={"alert_words": user_alert_words(user_profile)})
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), history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None), new_name: Optional[str]=REQ(validator=check_string, 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 description is not None: 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) if is_announcement_only is not None: do_change_stream_announcement_only(stream, is_announcement_only) # 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()
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), history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None), new_name: Optional[str]=REQ(validator=check_string, 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 description is not None: 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) if is_announcement_only is not None: do_change_stream_announcement_only(stream, is_announcement_only) # 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()
def register_remote_server( request: HttpRequest, zulip_org_id: str = REQ(str_validator=check_string_fixed_length( RemoteZulipServer.UUID_LENGTH)), zulip_org_key: str = REQ(str_validator=check_string_fixed_length( RemoteZulipServer.API_KEY_LENGTH)), hostname: str = REQ(str_validator=check_capped_string( RemoteZulipServer.HOSTNAME_MAX_LENGTH)), contact_email: str = REQ(), new_org_key: Optional[str] = REQ(str_validator=check_string_fixed_length( RemoteZulipServer.API_KEY_LENGTH), default=None), ) -> HttpResponse: # REQ validated the the field lengths, but we still need to # validate the format of these fields. try: # TODO: Ideally we'd not abuse the URL validator this way url_validator = URLValidator() url_validator("http://" + hostname) except ValidationError: raise JsonableError(_("{} is not a valid hostname").format(hostname)) try: validate_email(contact_email) except ValidationError as e: raise JsonableError(e.message) try: validate_uuid(zulip_org_id) except ValidationError as e: raise JsonableError(e.message) with transaction.atomic(): remote_server, created = RemoteZulipServer.objects.get_or_create( uuid=zulip_org_id, defaults={ "hostname": hostname, "contact_email": contact_email, "api_key": zulip_org_key, }, ) if created: RemoteZulipServerAuditLog.objects.create( event_type=RemoteZulipServerAuditLog.REMOTE_SERVER_CREATED, server=remote_server, event_time=remote_server.last_updated, ) else: if remote_server.api_key != zulip_org_key: raise InvalidZulipServerKeyError(zulip_org_id) else: remote_server.hostname = hostname remote_server.contact_email = contact_email if new_org_key is not None: remote_server.api_key = new_org_key remote_server.save() return json_success(request, data={"created": created})
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()
def check_pygments_language(var_name: str, val: object) -> str: s = check_capped_string(RealmPlayground.MAX_PYGMENTS_LANGUAGE_LENGTH)(var_name, val) # We don't want to restrict the language here to be only from the list of valid # Pygments languages. Keeping it open would allow us to hook up a "playground" # for custom "languages" that aren't known to Pygments. We use a similar strategy # even in our fenced_code Markdown processor. valid_pygments_language = re.compile(r"^[ a-zA-Z0-9_+-./#]*$") matched_results = valid_pygments_language.match(s) if not matched_results: raise JsonableError(_("Invalid characters in pygments language")) return s
def register_remote_server( request: HttpRequest, zulip_org_id: str = REQ(str_validator=check_string_fixed_length( RemoteZulipServer.UUID_LENGTH)), zulip_org_key: str = REQ(str_validator=check_string_fixed_length( RemoteZulipServer.API_KEY_LENGTH)), hostname: str = REQ(str_validator=check_capped_string( RemoteZulipServer.HOSTNAME_MAX_LENGTH)), contact_email: str = REQ(str_validator=check_string), new_org_key: Optional[str] = REQ(str_validator=check_string_fixed_length( RemoteZulipServer.API_KEY_LENGTH), default=None), ) -> HttpResponse: # REQ validated the the field lengths, but we still need to # validate the format of these fields. try: # TODO: Ideally we'd not abuse the URL validator this way url_validator = URLValidator() url_validator('http://' + hostname) except ValidationError: raise JsonableError(_('%s is not a valid hostname') % (hostname, )) try: validate_email(contact_email) except ValidationError as e: raise JsonableError(e.message) remote_server, created = RemoteZulipServer.objects.get_or_create( uuid=zulip_org_id, defaults={ 'hostname': hostname, 'contact_email': contact_email, 'api_key': zulip_org_key }) if not created: if remote_server.api_key != zulip_org_key: raise InvalidZulipServerKeyError(zulip_org_id) else: remote_server.hostname = hostname remote_server.contact_email = contact_email if new_org_key is not None: remote_server.api_key = new_org_key remote_server.save() return json_success({'created': created})
def update_user_status_backend(request: HttpRequest, user_profile: UserProfile, away: Optional[bool]=REQ(validator=check_bool, default=None), status_text: Optional[str]=REQ(str_validator=check_capped_string(60), default=None), ) -> HttpResponse: if status_text is not None: status_text = status_text.strip() if (away is None) and (status_text is None): return json_error(_('Client did not pass any new values.')) do_update_user_status( user_profile=user_profile, away=away, status_text=status_text, client_id=request.client.id, ) return json_success()
def update_user_status_backend(request: HttpRequest, user_profile: UserProfile, away: Optional[bool]=REQ(validator=check_bool, default=None), status_text: Optional[str]=REQ(str_validator=check_capped_string(60), default=None), ) -> HttpResponse: if status_text is not None: status_text = status_text.strip() if (away is None) and (status_text is None): return json_error(_('Client did not pass any new values.')) do_update_user_status( user_profile=user_profile, away=away, status_text=status_text, client_id=request.client.id, ) return json_success()
def register_remote_server( request: HttpRequest, zulip_org_id: str=REQ(str_validator=check_string_fixed_length(RemoteZulipServer.UUID_LENGTH)), zulip_org_key: str=REQ(str_validator=check_string_fixed_length(RemoteZulipServer.API_KEY_LENGTH)), hostname: str=REQ(str_validator=check_capped_string(RemoteZulipServer.HOSTNAME_MAX_LENGTH)), contact_email: str=REQ(str_validator=check_string), new_org_key: Optional[str]=REQ(str_validator=check_string_fixed_length( RemoteZulipServer.API_KEY_LENGTH), default=None), ) -> HttpResponse: # REQ validated the the field lengths, but we still need to # validate the format of these fields. try: # TODO: Ideally we'd not abuse the URL validator this way url_validator = URLValidator() url_validator('http://' + hostname) except ValidationError: raise JsonableError(_('%s is not a valid hostname') % (hostname,)) try: validate_email(contact_email) except ValidationError as e: raise JsonableError(e.message) remote_server, created = RemoteZulipServer.objects.get_or_create( uuid=zulip_org_id, defaults={'hostname': hostname, 'contact_email': contact_email, 'api_key': zulip_org_key}) if not created: if remote_server.api_key != zulip_org_key: raise InvalidZulipServerKeyError(zulip_org_id) else: remote_server.hostname = hostname remote_server.contact_email = contact_email if new_org_key is not None: remote_server.api_key = new_org_key remote_server.save() return json_success({'created': created})
def update_user_status_backend( request: HttpRequest, user_profile: UserProfile, away: Optional[bool] = REQ(json_validator=check_bool, default=None), status_text: Optional[str] = REQ(str_validator=check_capped_string(60), default=None), ) -> HttpResponse: if status_text is not None: status_text = status_text.strip() if (away is None) and (status_text is None): raise JsonableError(_("Client did not pass any new values.")) client = get_request_notes(request).client assert client is not None do_update_user_status( user_profile=user_profile, away=away, status_text=status_text, client_id=client.id, ) return json_success()
check_int, check_list, check_string, check_union, validate_select_field_data, ) from zerver.models import CustomProfileField, UserProfile, custom_profile_fields_for_realm def list_realm_custom_profile_fields( request: HttpRequest, user_profile: UserProfile) -> HttpResponse: fields = custom_profile_fields_for_realm(user_profile.realm_id) return json_success({"custom_fields": [f.as_dict() for f in fields]}) hint_validator = check_capped_string(CustomProfileField.HINT_MAX_LENGTH) name_validator = check_capped_string(CustomProfileField.NAME_MAX_LENGTH) def validate_field_name_and_hint(name: str, hint: str) -> None: if not name.strip(): raise JsonableError(_("Label cannot be blank.")) try: hint_validator("hint", hint) name_validator("name", name) except ValidationError as error: raise JsonableError(error.message) def validate_custom_field_data(field_type: int,
include_subscribers: bool = REQ(json_validator=check_bool, default=False), ) -> HttpResponse: subscribed, _ = gather_subscriptions( user_profile, include_subscribers=include_subscribers, ) return json_success({"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)), ], ), ) remove_subscriptions_schema = check_list(check_string) @has_request_variables 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=[]),
def update_realm( request: HttpRequest, user_profile: UserProfile, name: Optional[str] = REQ(str_validator=check_capped_string( Realm.MAX_REALM_NAME_LENGTH), default=None), description: Optional[str] = REQ(str_validator=check_capped_string( Realm.MAX_REALM_DESCRIPTION_LENGTH), default=None), emails_restricted_to_domains: Optional[bool] = REQ( json_validator=check_bool, default=None), disallow_disposable_email_addresses: Optional[bool] = REQ( json_validator=check_bool, default=None), invite_required: Optional[bool] = REQ(json_validator=check_bool, default=None), invite_to_realm_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.INVITE_TO_REALM_POLICY_TYPES), default=None), name_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), email_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), avatar_changes_disabled: Optional[bool] = REQ(json_validator=check_bool, default=None), inline_image_preview: Optional[bool] = REQ(json_validator=check_bool, default=None), inline_url_embed_preview: Optional[bool] = REQ(json_validator=check_bool, default=None), add_custom_emoji_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.COMMON_POLICY_TYPES), default=None), delete_own_message_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.COMMON_MESSAGE_POLICY_TYPES), default=None), message_content_delete_limit_seconds_raw: Optional[Union[int, str]] = REQ( "message_content_delete_limit_seconds", json_validator=check_string_or_int, default=None), allow_message_editing: Optional[bool] = REQ(json_validator=check_bool, default=None), edit_topic_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.COMMON_MESSAGE_POLICY_TYPES), default=None), mandatory_topics: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_edit_limit_seconds: Optional[int] = REQ( converter=to_non_negative_int, default=None), allow_edit_history: Optional[bool] = REQ(json_validator=check_bool, default=None), default_language: Optional[str] = REQ(default=None), waiting_period_threshold: Optional[int] = REQ( converter=to_non_negative_int, default=None), authentication_methods: Optional[Dict[str, Any]] = REQ( json_validator=check_dict([]), default=None), notifications_stream_id: Optional[int] = REQ(json_validator=check_int, default=None), signup_notifications_stream_id: Optional[int] = REQ( json_validator=check_int, default=None), message_retention_days_raw: Optional[Union[int, str]] = REQ( "message_retention_days", json_validator=check_string_or_int, default=None), send_welcome_emails: Optional[bool] = REQ(json_validator=check_bool, default=None), digest_emails_enabled: Optional[bool] = REQ(json_validator=check_bool, default=None), message_content_allowed_in_email_notifications: Optional[bool] = REQ( json_validator=check_bool, default=None), bot_creation_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.BOT_CREATION_POLICY_TYPES), default=None), create_public_stream_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None), create_private_stream_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None), create_web_public_stream_policy: Optional[int] = REQ( json_validator=check_int_in( Realm.CREATE_WEB_PUBLIC_STREAM_POLICY_TYPES), default=None), invite_to_stream_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.COMMON_POLICY_TYPES), default=None), move_messages_between_streams_policy: Optional[int] = REQ( json_validator=check_int_in(Realm.COMMON_POLICY_TYPES), default=None), user_group_edit_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.COMMON_POLICY_TYPES), default=None), private_message_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.PRIVATE_MESSAGE_POLICY_TYPES), default=None), wildcard_mention_policy: Optional[int] = REQ(json_validator=check_int_in( Realm.WILDCARD_MENTION_POLICY_TYPES), default=None), email_address_visibility: Optional[int] = REQ(json_validator=check_int_in( Realm.EMAIL_ADDRESS_VISIBILITY_TYPES), default=None), video_chat_provider: Optional[int] = REQ(json_validator=check_int, default=None), giphy_rating: Optional[int] = REQ(json_validator=check_int, default=None), default_code_block_language: Optional[str] = REQ(default=None), digest_weekday: Optional[int] = REQ(json_validator=check_int_in( Realm.DIGEST_WEEKDAY_VALUES), default=None), string_id: Optional[str] = REQ( str_validator=check_capped_string(Realm.MAX_REALM_SUBDOMAIN_LENGTH), default=None, ), enable_spectator_access: Optional[bool] = REQ(json_validator=check_bool, default=None), ) -> HttpResponse: realm = user_profile.realm # Additional validation/error checking beyond types go here, so # the entire request can succeed or fail atomically. if default_language is not None and default_language not in get_available_language_codes( ): raise JsonableError( _("Invalid language '{}'").format(default_language)) if authentication_methods is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() if True not in list(authentication_methods.values()): raise JsonableError( _("At least one authentication method must be enabled.")) if video_chat_provider is not None and video_chat_provider not in { p["id"] for p in Realm.VIDEO_CHAT_PROVIDERS.values() }: raise JsonableError( _("Invalid video_chat_provider {}").format(video_chat_provider)) if giphy_rating is not None and giphy_rating not in { p["id"] for p in Realm.GIPHY_RATING_OPTIONS.values() }: raise JsonableError(_("Invalid giphy_rating {}").format(giphy_rating)) message_retention_days: Optional[int] = None if message_retention_days_raw is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() realm.ensure_not_on_limited_plan() message_retention_days = parse_message_retention_days( message_retention_days_raw, Realm.MESSAGE_RETENTION_SPECIAL_VALUES_MAP) if invite_to_realm_policy is not None and not user_profile.is_realm_owner: raise OrganizationOwnerRequired() data: Dict[str, Any] = {} message_content_delete_limit_seconds: Optional[int] = None if message_content_delete_limit_seconds_raw is not None: message_content_delete_limit_seconds = parse_message_content_delete_limit( message_content_delete_limit_seconds_raw, Realm.MESSAGE_CONTENT_DELETE_LIMIT_SPECIAL_VALUES_MAP, ) do_set_realm_property( realm, "message_content_delete_limit_seconds", message_content_delete_limit_seconds, acting_user=user_profile, ) data[ "message_content_delete_limit_seconds"] = message_content_delete_limit_seconds # The user of `locals()` here is a bit of a code smell, but it's # restricted to the elements present in realm.property_types. # # TODO: It should be possible to deduplicate this function up # further by some more advanced usage of the # `REQ/has_request_variables` extraction. req_vars = { k: v for k, v in list(locals().items()) if k in realm.property_types } for k, v in list(req_vars.items()): if v is not None and getattr(realm, k) != v: do_set_realm_property(realm, k, v, acting_user=user_profile) if isinstance(v, str): data[k] = "updated" else: data[k] = v # The following realm properties do not fit the pattern above # authentication_methods is not supported by the do_set_realm_property # framework because of its bitfield. if authentication_methods is not None and ( realm.authentication_methods_dict() != authentication_methods): do_set_realm_authentication_methods(realm, authentication_methods, acting_user=user_profile) data["authentication_methods"] = authentication_methods # The message_editing settings are coupled to each other, and thus don't fit # into the do_set_realm_property framework. if ((allow_message_editing is not None and realm.allow_message_editing != allow_message_editing) or (message_content_edit_limit_seconds is not None and realm.message_content_edit_limit_seconds != message_content_edit_limit_seconds) or (edit_topic_policy is not None and realm.edit_topic_policy != edit_topic_policy)): if allow_message_editing is None: allow_message_editing = realm.allow_message_editing if message_content_edit_limit_seconds is None: message_content_edit_limit_seconds = realm.message_content_edit_limit_seconds if edit_topic_policy is None: edit_topic_policy = realm.edit_topic_policy do_set_realm_message_editing( realm, allow_message_editing, message_content_edit_limit_seconds, edit_topic_policy, acting_user=user_profile, ) data["allow_message_editing"] = allow_message_editing data[ "message_content_edit_limit_seconds"] = message_content_edit_limit_seconds data["edit_topic_policy"] = edit_topic_policy # Realm.notifications_stream and Realm.signup_notifications_stream are not boolean, # str or integer field, and thus doesn't fit into the do_set_realm_property framework. if notifications_stream_id is not None: if realm.notifications_stream is None or (realm.notifications_stream.id != notifications_stream_id): new_notifications_stream = None if notifications_stream_id >= 0: (new_notifications_stream, sub) = access_stream_by_id(user_profile, notifications_stream_id) do_set_realm_notifications_stream(realm, new_notifications_stream, notifications_stream_id, acting_user=user_profile) data["notifications_stream_id"] = notifications_stream_id if signup_notifications_stream_id is not None: if realm.signup_notifications_stream is None or ( realm.signup_notifications_stream.id != signup_notifications_stream_id): new_signup_notifications_stream = None if signup_notifications_stream_id >= 0: (new_signup_notifications_stream, sub) = access_stream_by_id(user_profile, signup_notifications_stream_id) do_set_realm_signup_notifications_stream( realm, new_signup_notifications_stream, signup_notifications_stream_id, acting_user=user_profile, ) data[ "signup_notifications_stream_id"] = signup_notifications_stream_id if default_code_block_language is not None: # Migrate '', used in the API to encode the default/None behavior of this feature. if default_code_block_language == "": data["default_code_block_language"] = None else: data["default_code_block_language"] = default_code_block_language if string_id is not None: if not user_profile.is_realm_owner: raise OrganizationOwnerRequired() if realm.demo_organization_scheduled_deletion_date is None: raise JsonableError(_("Must be a demo organization.")) try: check_subdomain(string_id) except ValidationError as err: raise JsonableError(str(err.message)) do_change_realm_subdomain(realm, string_id, acting_user=user_profile) data["realm_uri"] = realm.uri return json_success(data)
def add_subscriptions_backend( request: HttpRequest, user_profile: UserProfile, streams_raw: Iterable[Dict[str, str]] = REQ( "subscriptions", validator=check_list( check_dict_only([('name', check_string)], optional_keys=[ ('color', check_color), ('description', check_capped_string( Stream.MAX_DESCRIPTION_LENGTH)), ]), )), 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="realm_default"), announce: bool = REQ(validator=check_bool, default=False), principals: Union[Sequence[str], Sequence[int]] = REQ(validator=check_union( [check_list(check_string), check_list(check_int)]), default=[]), 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_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] = dict() 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 msg = you_were_just_subscribed_message( acting_user=user_profile, stream_names=notify_stream_names, ) sender = get_system_bot(settings.NOTIFICATION_BOT) notifications.append( internal_prep_private_message( realm=user_profile.realm, sender=sender, recipient_user=email_to_user_profile[email], content=msg)) if announce and len(created_streams) > 0: notifications_stream = user_profile.realm.get_notifications_stream() if notifications_stream is not None: if len(created_streams) > 1: content = _( "@_**%(user_name)s|%(user_id)d** created the following streams: %(stream_str)s." ) else: content = _( "@_**%(user_name)s|%(user_id)d** created a new stream %(stream_str)s." ) content = content % { 'user_name': user_profile.full_name, 'user_id': user_profile.id, 'stream_str': ", ".join(f'#**{s.name}**' for s in created_streams) } sender = get_system_bot(settings.NOTIFICATION_BOT) topic = _('new streams') 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: 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}|{user_id}**.' ).format( user_name=user_profile.full_name, user_id=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)
def add_subscriptions_backend( request: HttpRequest, user_profile: UserProfile, streams_raw: Iterable[Dict[str, str]]=REQ( "subscriptions", validator=check_list(check_dict_only( [('name', check_string)], optional_keys=[ ('color', check_color), ('description', check_capped_string(Stream.MAX_DESCRIPTION_LENGTH)), ]) )), invite_only: bool=REQ(validator=check_bool, default=False), is_announcement_only: bool=REQ(validator=check_bool, default=False), history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None), announce: bool=REQ(validator=check_bool, default=False), principals: List[str]=REQ(validator=check_list(check_string), default=[]), 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 = {} # type: 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["is_announcement_only"] = is_announcement_only stream_dict_copy["history_public_to_subscribers"] = history_public_to_subscribers 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 (%s).") % 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(): return json_error(_("Your account is too new to modify other users' subscriptions.")) subscribers = set(principal_to_user_profile(user_profile, principal) for principal in principals) else: subscribers = set([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() # type: Dict[str, UserProfile] result = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list)) # type: Dict[str, Any] 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 = dict((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 msg = you_were_just_subscribed_message( acting_user=user_profile, stream_names=notify_stream_names, ) sender = get_system_bot(settings.NOTIFICATION_BOT) notifications.append( internal_prep_private_message( realm=user_profile.realm, sender=sender, recipient_user=email_to_user_profile[email], content=msg)) if announce and len(created_streams) > 0 and settings.NOTIFICATION_BOT is not None: notifications_stream = user_profile.realm.get_notifications_stream() if notifications_stream is not None: if len(created_streams) > 1: stream_strs = ", ".join('#**%s**' % s.name for s in created_streams) stream_msg = "the following streams: %s" % (stream_strs,) else: stream_msg = "a new stream #**%s**." % created_streams[0].name msg = ("@_**%s|%d** just created %s" % (user_profile.full_name, user_profile.id, stream_msg)) sender = get_system_bot(settings.NOTIFICATION_BOT) topic = 'Streams' notifications.append( internal_prep_stream_message( realm=user_profile.realm, sender=sender, stream=notifications_stream, topic=topic, content=msg, ) ) if not user_profile.realm.is_zephyr_mirror_realm: for stream in created_streams: notifications.append(prep_stream_welcome_message(stream)) if len(notifications) > 0: do_send_messages(notifications) 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)
def add_subscriptions_backend( request: HttpRequest, user_profile: UserProfile, streams_raw: Iterable[Dict[str, str]]=REQ( "subscriptions", validator=check_list(check_dict_only( [('name', check_string)], optional_keys=[ ('color', check_color), ('description', check_capped_string(Stream.MAX_DESCRIPTION_LENGTH)), ]) )), invite_only: bool=REQ(validator=check_bool, default=False), is_announcement_only: bool=REQ(validator=check_bool, default=False), history_public_to_subscribers: Optional[bool]=REQ(validator=check_bool, default=None), announce: bool=REQ(validator=check_bool, default=False), principals: List[str]=REQ(validator=check_list(check_string), default=[]), 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 = {} # type: 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["is_announcement_only"] = is_announcement_only stream_dict_copy["history_public_to_subscribers"] = history_public_to_subscribers 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 (%s).") % 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(): return json_error(_("Your account is too new to modify other users' subscriptions.")) subscribers = set(principal_to_user_profile(user_profile, principal) for principal in principals) else: subscribers = set([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() # type: Dict[str, UserProfile] result = dict(subscribed=defaultdict(list), already_subscribed=defaultdict(list)) # type: Dict[str, Any] 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 = dict((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 msg = you_were_just_subscribed_message( acting_user=user_profile, stream_names=notify_stream_names, ) sender = get_system_bot(settings.NOTIFICATION_BOT) notifications.append( internal_prep_private_message( realm=user_profile.realm, sender=sender, recipient_user=email_to_user_profile[email], content=msg)) if announce and len(created_streams) > 0 and settings.NOTIFICATION_BOT is not None: notifications_stream = user_profile.realm.get_notifications_stream() if notifications_stream is not None: if len(created_streams) > 1: stream_strs = ", ".join('#**%s**' % s.name for s in created_streams) stream_msg = "the following streams: %s" % (stream_strs,) else: stream_msg = "a new stream #**%s**." % created_streams[0].name msg = ("@_**%s|%d** just created %s" % (user_profile.full_name, user_profile.id, stream_msg)) sender = get_system_bot(settings.NOTIFICATION_BOT) topic = 'Streams' notifications.append( internal_prep_stream_message( realm=user_profile.realm, sender=sender, stream=notifications_stream, topic=topic, content=msg, ) ) if not user_profile.realm.is_zephyr_mirror_realm: for stream in created_streams: notifications.append(prep_stream_welcome_message(stream)) if len(notifications) > 0: do_send_messages(notifications) 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)
notify_user_update_custom_profile_data) from zerver.lib.response import json_success, json_error from zerver.lib.types import ProfileFieldData from zerver.lib.validator import (check_dict, check_list, check_int, validate_field_data, check_capped_string) from zerver.models import (custom_profile_fields_for_realm, UserProfile, CustomProfileFieldValue, CustomProfileField, custom_profile_fields_for_realm) from zerver.lib.exceptions import JsonableError from zerver.lib.users import validate_user_custom_profile_data def list_realm_custom_profile_fields(request: HttpRequest, user_profile: UserProfile) -> HttpResponse: fields = custom_profile_fields_for_realm(user_profile.realm_id) return json_success({'custom_fields': [f.as_dict() for f in fields]}) hint_validator = check_capped_string(CustomProfileField.HINT_MAX_LENGTH) name_validator = check_capped_string(CustomProfileField.NAME_MAX_LENGTH) def validate_field_name_and_hint(name: str, hint: str) -> None: if not name.strip(): raise JsonableError(_("Name cannot be blank.")) error = hint_validator('hint', hint) if error: raise JsonableError(error) error = name_validator('name', name) if error: raise JsonableError(error) @require_realm_admin
def update_user_status_backend( request: HttpRequest, user_profile: UserProfile, away: Optional[bool] = REQ(json_validator=check_bool, default=None), status_text: Optional[str] = REQ(str_validator=check_capped_string(60), default=None), emoji_name: Optional[str] = REQ(default=None), emoji_code: Optional[str] = REQ(default=None), # TODO: emoji_type is the more appropriate name for this parameter, but changing # that requires nontrivial work on the API documentation, since it's not clear # that the reactions endpoint would prefer such a change. emoji_type: Optional[str] = REQ("reaction_type", default=None), ) -> HttpResponse: if status_text is not None: status_text = status_text.strip() if (away is None) and (status_text is None) and (emoji_name is None): raise JsonableError(_("Client did not pass any new values.")) if emoji_name == "": # Reset the emoji_code and reaction_type if emoji_name is empty. # This should clear the user's configured emoji. emoji_code = "" emoji_type = UserStatus.UNICODE_EMOJI elif emoji_name is not None: if emoji_code is None: # The emoji_code argument is only required for rare corner # cases discussed in the long block comment below. For simple # API clients, we allow specifying just the name, and just # look up the code using the current name->code mapping. emoji_code = emoji_name_to_emoji_code(user_profile.realm, emoji_name)[0] if emoji_type is None: emoji_type = emoji_name_to_emoji_code(user_profile.realm, emoji_name)[1] elif emoji_type or emoji_code: raise JsonableError( _("Client must pass emoji_name if they pass either emoji_code or reaction_type.") ) # If we're asking to set an emoji (not clear it ("") or not adjust # it (None)), we need to verify the emoji is valid. if emoji_name not in ["", None]: assert emoji_name is not None assert emoji_code is not None assert emoji_type is not None check_emoji_request(user_profile.realm, emoji_name, emoji_code, emoji_type) client = RequestNotes.get_notes(request).client assert client is not None do_update_user_status( user_profile=user_profile, away=away, status_text=status_text, client_id=client.id, emoji_name=emoji_name, emoji_code=emoji_code, reaction_type=emoji_type, ) return json_success(request)
) -> 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".'))
def update_stream_backend( request: HttpRequest, user_profile: UserProfile, stream_id: int, description: Optional[str] = REQ(str_validator=check_capped_string( Stream.MAX_DESCRIPTION_LENGTH), default=None), is_private: Optional[bool] = REQ(json_validator=check_bool, default=None), is_announcement_only: Optional[bool] = REQ(json_validator=check_bool, default=None), stream_post_policy: Optional[int] = REQ(json_validator=check_int_in( Stream.STREAM_POST_POLICY_TYPES), default=None), history_public_to_subscribers: Optional[bool] = REQ( json_validator=check_bool, default=None), is_web_public: Optional[bool] = REQ(json_validator=check_bool, default=None), new_name: Optional[str] = REQ(default=None), message_retention_days: Optional[Union[int, str]] = REQ( json_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, sub) = 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() new_message_retention_days_value = parse_message_retention_days( message_retention_days, Stream.MESSAGE_RETENTION_SPECIAL_VALUES_MAP) do_change_stream_message_retention_days( stream, user_profile, new_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, acting_user=user_profile) if new_name is not None: new_name = new_name.strip() if stream.name == new_name: raise JsonableError(_("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, acting_user=user_profile) # But we require even realm administrators to be actually # subscribed to make a private stream public. if is_private is not None: default_stream_ids = { s.id for s in get_default_streams_for_realm(stream.realm_id) } (stream, sub) = access_stream_by_id(user_profile, stream_id) if is_private and stream.id in default_stream_ids: raise JsonableError(_("Default streams cannot be made private.")) if is_web_public: # Enforce restrictions on creating web-public streams. if not user_profile.realm.web_public_streams_enabled(): raise JsonableError(_("Web-public streams are not enabled.")) if not user_profile.can_create_web_public_streams(): raise JsonableError(_("Insufficient permission")) # Forbid parameter combinations that are inconsistent if is_private or history_public_to_subscribers is False: raise JsonableError(_("Invalid parameters")) if is_private is not None or is_web_public is not None: do_change_stream_permission( stream, invite_only=is_private, history_public_to_subscribers=history_public_to_subscribers, is_web_public=is_web_public, acting_user=user_profile, ) return json_success(request)