def get_subscribers_backend(request, user_profile, stream_id=REQ('stream', converter=to_non_negative_int)): # type: (HttpRequest, UserProfile, int) -> HttpResponse (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) subscribers = get_subscriber_emails(stream, user_profile) return json_success({'subscribers': subscribers})
def update_stream_backend( request: HttpRequest, user_profile: UserProfile, stream_id: int, description: Optional[Text]=REQ(validator=check_string, default=None), is_private: Optional[bool]=REQ(validator=check_bool, default=None), new_name: Optional[Text]=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) # 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) return json_success()
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})
def get_topics_backend(request: HttpRequest, user_profile: UserProfile, stream_id: int=REQ(converter=to_non_negative_int)) -> HttpResponse: (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) result = get_topic_history_for_stream( user_profile=user_profile, recipient=recipient, ) return json_success(dict(topics=result))
def get_topics_backend(request: HttpRequest, user_profile: UserProfile, stream_id: int=REQ(converter=to_non_negative_int)) -> HttpResponse: (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))
def consume(self, event: Mapping[str, Any]) -> None: if event['type'] == 'mark_stream_messages_as_read': user_profile = get_user_profile_by_id(event['user_profile_id']) for stream_id in event['stream_ids']: # Since the user just unsubscribed, we don't require # an active Subscription object (otherwise, private # streams would never be accessible) (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id, require_active=False) do_mark_stream_messages_as_read(user_profile, stream)
def get_topics_backend(request, user_profile, stream_id=REQ(converter=to_non_negative_int)): # type: (HttpRequest, UserProfile, int) -> HttpResponse (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) result = get_topic_history_for_stream( user_profile=user_profile, recipient=recipient, ) return json_success(dict(topics=result))
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})
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_variable_type([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} 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: %s") % (property,)) (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) if sub is None: return json_error(_("Not subscribed to stream id %d") % (stream_id,)) property_conversion = property_converters[property](property, value) if property_conversion: return json_error(property_conversion) do_change_subscription_property(user_profile, sub, stream, property, value) response_data.append({'stream_id': stream_id, 'property': property, 'value': value}) return json_success({"subscription_data": response_data})
def mark_stream_as_read( request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(json_validator=check_int) ) -> HttpResponse: stream, sub = access_stream_by_id(user_profile, stream_id) count = do_mark_stream_messages_as_read(user_profile, stream.recipient_id) log_data_str = f"[{count} updated]" request._log_data["extra"] = log_data_str return json_success({"result": "success", "msg": ""})
def mark_stream_as_read( request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(json_validator=check_int) ) -> HttpResponse: stream, sub = access_stream_by_id(user_profile, stream_id) count = do_mark_stream_messages_as_read(user_profile, stream.recipient_id) log_data_str = f"[{count} updated]" log_data = RequestNotes.get_notes(request).log_data assert log_data is not None log_data["extra"] = log_data_str return json_success(request)
def remove_default_stream( request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(json_validator=check_int) ) -> HttpResponse: (stream, sub) = access_stream_by_id( user_profile, stream_id, allow_realm_admin=True, ) do_remove_default_stream(stream) 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), 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), ) -> 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: 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 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_variable_type([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": "in_home_view", "value": False}, {"stream_id": "1", "property": "color", "value": "#c2c2c2"}] """ property_converters = {"color": check_color, "in_home_view": check_bool, "desktop_notifications": check_bool, "audible_notifications": check_bool, "push_notifications": check_bool, "email_notifications": check_bool, "pin_to_top": 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: %s") % (property,)) (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) if sub is None: return json_error(_("Not subscribed to stream id %d") % (stream_id,)) property_conversion = property_converters[property](property, value) if property_conversion: return json_error(property_conversion) do_change_subscription_property(user_profile, sub, stream, property, value) response_data.append({'stream_id': stream_id, 'property': property, 'value': value}) return json_success({"subscription_data": response_data})
def invite_users_backend( request: HttpRequest, user_profile: UserProfile, invitee_emails_raw: str = REQ("invitee_emails"), invite_expires_in_minutes: Optional[int] = REQ( json_validator=check_none_or(check_int), default=settings.INVITATION_LINK_VALIDITY_MINUTES ), invite_as: int = REQ(json_validator=check_int, default=PreregistrationUser.INVITE_AS["MEMBER"]), stream_ids: List[int] = REQ(json_validator=check_list(check_int)), ) -> HttpResponse: if not user_profile.can_invite_others_to_realm(): # Guest users case will not be handled here as it will # be handled by the decorator above. raise JsonableError(_("Insufficient permission")) if invite_as not in PreregistrationUser.INVITE_AS.values(): raise JsonableError(_("Must be invited as an valid type of user")) check_if_owner_required(invite_as, user_profile) if ( invite_as in [ PreregistrationUser.INVITE_AS["REALM_ADMIN"], PreregistrationUser.INVITE_AS["MODERATOR"], ] and not user_profile.is_realm_admin ): raise JsonableError(_("Must be an organization administrator")) if not invitee_emails_raw: raise JsonableError(_("You must specify at least one email address.")) if not stream_ids: raise JsonableError(_("You must specify at least one stream for invitees to join.")) invitee_emails = get_invitee_emails_set(invitee_emails_raw) streams: List[Stream] = [] for stream_id in stream_ids: try: (stream, sub) = access_stream_by_id(user_profile, stream_id) except JsonableError: raise JsonableError( _("Stream does not exist with id: {}. No invites were sent.").format(stream_id) ) streams.append(stream) do_invite_users( user_profile, invitee_emails, streams, invite_expires_in_minutes=invite_expires_in_minutes, invite_as=invite_as, ) return json_success(request)
def generate_multiuse_invite_backend(request: HttpRequest, user_profile: UserProfile, stream_ids: List[int]=REQ(validator=check_list(check_int), default=[])) -> HttpResponse: streams = [] for stream_id in stream_ids: try: (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) except JsonableError: return json_error(_("Invalid stream id {}. No invites were sent.".format(stream_id))) streams.append(stream) invite_link = do_create_multiuse_invite_link(user_profile, streams) return json_success({'invite_link': invite_link})
def mark_stream_as_read( request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ(validator=check_int) ) -> HttpResponse: stream, recipient, sub = access_stream_by_id(user_profile, stream_id) count = do_mark_stream_messages_as_read(user_profile, request.client, stream) log_data_str = f"[{count} updated]" request._log_data["extra"] = log_data_str return json_success({'result': 'success', 'msg': ''})
def get_subscribers_backend( request: HttpRequest, user_profile: UserProfile, stream_id: int = REQ("stream", converter=to_non_negative_int, path_only=True), ) -> HttpResponse: (stream, sub) = access_stream_by_id( user_profile, stream_id, allow_realm_admin=True, ) subscribers = get_subscriber_ids(stream, user_profile) return json_success({"subscribers": list(subscribers)})
def consume(self, event: Mapping[str, Any]) -> None: if event['type'] == 'mark_stream_messages_as_read': user_profile = get_user_profile_by_id(event['user_profile_id']) client = Client.objects.get(id=event['client_id']) for stream_id in event['stream_ids']: # Since the user just unsubscribed, we don't require # an active Subscription object (otherwise, private # streams would never be accessible) (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id, require_active=False) do_mark_stream_messages_as_read(user_profile, client, stream) elif event['type'] == 'clear_push_device_tokens': clear_push_device_tokens(event["user_profile_id"]) elif event['type'] == 'realm_export': start = time.time() realm = Realm.objects.get(id=event['realm_id']) output_dir = tempfile.mkdtemp(prefix="zulip-export-") public_url = export_realm_wrapper(realm=realm, output_dir=output_dir, threads=6, upload=True, public_only=True, delete_after_upload=True) assert public_url is not None # Update the extra_data field now that the export is complete. export_event = RealmAuditLog.objects.get(id=event['id']) export_event.extra_data = ujson.dumps( dict(export_path=urllib.parse.urlparse(public_url).path, )) export_event.save(update_fields=['extra_data']) # Send a private message notification letting the user who # triggered the export know the export finished. user_profile = get_user_profile_by_id(event['user_profile_id']) content = "Your data export is complete and has been uploaded here:\n\n%s" % ( public_url, ) internal_send_private_message(realm=user_profile.realm, sender=get_system_bot( settings.NOTIFICATION_BOT), recipient_user=user_profile, content=content) # For future frontend use, also notify administrator # clients that the export happened. notify_realm_export(user_profile) logging.info("Completed data export for %s in %s" % (user_profile.realm.string_id, time.time() - start))
def get_topics_backend(request, user_profile, stream_id=REQ(converter=to_non_negative_int)): # type: (HttpRequest, UserProfile, int) -> HttpResponse (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) result = get_topic_history_for_stream( user_profile=user_profile, recipient=recipient, ) # Our data structure here is a list of tuples of # (topic name, unread count), and it's reverse chronological, # so the most recent topic is the first element of the list. return json_success(dict(topics=result))
def mute_topic(user_profile: UserProfile, stream_id: Optional[int], stream_name: Optional[str], topic_name: str) -> HttpResponse: if stream_name is not None: (stream, recipient, sub) = access_stream_by_name(user_profile, stream_name) else: assert stream_id is not None (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) if topic_is_muted(user_profile, stream.id, topic_name): return json_error(_("Topic already muted")) do_mute_topic(user_profile, stream, recipient, topic_name) return json_success()
def get_subscription_backend( request: HttpRequest, user_profile: UserProfile, user_id: int = REQ(json_validator=check_int, path_only=True), stream_id: int = REQ(json_validator=check_int, path_only=True), ) -> HttpResponse: target_user = access_user_by_id(user_profile, user_id, for_admin=False) (stream, sub) = access_stream_by_id(user_profile, stream_id) subscription_status = { "is_subscribed": subscribed_to_stream(target_user, stream_id) } return json_success(subscription_status)
def invite_users_backend( request: HttpRequest, user_profile: UserProfile, invitee_emails_raw: str = REQ("invitee_emails"), invite_as: int = REQ(validator=check_int, default=PreregistrationUser.INVITE_AS["MEMBER"]), stream_ids: List[int] = REQ(validator=check_list(check_int)), ) -> HttpResponse: if not user_profile.can_invite_others_to_realm(): if user_profile.realm.invite_to_realm_policy == Realm.POLICY_ADMINS_ONLY: return json_error( _("Only administrators can invite others to this organization." )) if user_profile.realm.invite_to_realm_policy == Realm.POLICY_MODERATORS_ONLY: return json_error( _("Only administrators and moderators can invite others to this organization." )) if user_profile.realm.invite_to_realm_policy == Realm.POLICY_FULL_MEMBERS_ONLY: return json_error( _("Your account is too new to invite others to this organization." )) # Guest case will be handled by require_member_or_admin decorator. raise AssertionError("Unexpected policy validation failure") if invite_as not in PreregistrationUser.INVITE_AS.values(): return json_error(_("Must be invited as an valid type of user")) check_if_owner_required(invite_as, user_profile) if (invite_as == PreregistrationUser.INVITE_AS["REALM_ADMIN"] and not user_profile.is_realm_admin): return json_error(_("Must be an organization administrator")) if not invitee_emails_raw: return json_error(_("You must specify at least one email address.")) if not stream_ids: return json_error( _("You must specify at least one stream for invitees to join.")) invitee_emails = get_invitee_emails_set(invitee_emails_raw) streams: List[Stream] = [] for stream_id in stream_ids: try: (stream, sub) = access_stream_by_id(user_profile, stream_id) except JsonableError: return json_error( _("Stream does not exist with id: {}. No invites were sent."). format(stream_id)) streams.append(stream) do_invite_users(user_profile, invitee_emails, streams, invite_as) return json_success()
def send_notification_backend( request: HttpRequest, user_profile: UserProfile, message_type: str = REQ( "type", str_validator=check_string_in(VALID_MESSAGE_TYPES), default="private"), operator: str = REQ( "op", str_validator=check_string_in(VALID_OPERATOR_TYPES)), notification_to: List[int] = REQ("to", json_validator=check_list(check_int)), topic: Optional[str] = REQ("topic", default=None), ) -> HttpResponse: to_length = len(notification_to) if to_length == 0: raise JsonableError(_("Empty 'to' list")) if message_type == "stream": if to_length > 1: raise JsonableError(_("Cannot send to multiple streams")) if topic is None: raise JsonableError(_("Missing topic")) if not user_profile.send_stream_typing_notifications: raise JsonableError( _("User has disabled typing notifications for stream messages") ) stream_id = notification_to[0] # Verify that the user has access to the stream and has # permission to send messages to it. stream = access_stream_by_id(user_profile, stream_id)[0] access_stream_for_send_message(user_profile, stream, forwarder_user_profile=None) do_send_stream_typing_notification(user_profile, operator, stream, topic) else: if not user_profile.send_private_typing_notifications: raise JsonableError( _("User has disabled typing notifications for private messages" )) user_ids = notification_to check_send_typing_notification(user_profile, user_ids, operator) return json_success()
def generate_multiuse_invite_backend( request: HttpRequest, user_profile: UserProfile, invite_as: int=REQ(validator=check_int, default=PreregistrationUser.INVITE_AS['MEMBER']), stream_ids: Sequence[int]=REQ(validator=check_list(check_int), default=[])) -> HttpResponse: check_if_owner_required(invite_as, user_profile) streams = [] for stream_id in stream_ids: try: (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) except JsonableError: return json_error(_("Invalid stream id {}. No invites were sent.").format(stream_id)) streams.append(stream) invite_link = do_create_multiuse_invite_link(user_profile, invite_as, streams) return json_success({'invite_link': invite_link})
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.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, messages) return json_success()
def consume(self, event: Mapping[str, Any]) -> None: if event['type'] == 'mark_stream_messages_as_read': user_profile = get_user_profile_by_id(event['user_profile_id']) client = Client.objects.get(id=event['client_id']) for stream_id in event['stream_ids']: # Since the user just unsubscribed, we don't require # an active Subscription object (otherwise, private # streams would never be accessible) (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id, require_active=False) do_mark_stream_messages_as_read(user_profile, client, stream) elif event['type'] == 'realm_exported': realm = Realm.objects.get(id=event['realm_id']) output_dir = tempfile.mkdtemp(prefix="zulip-export-") public_url = export_realm_wrapper(realm=realm, output_dir=output_dir, threads=6, upload=True, public_only=True, delete_after_upload=True) assert public_url is not None # TODO: This enables support for delete after access, and needs to be tested. export_event = RealmAuditLog.objects.get(id=event['id']) export_event.extra_data = public_url export_event.save(update_fields=['extra_data']) # Send a private message notification letting the user who # triggered the export know the export finished. user_profile = get_user_profile_by_id(event['user_profile_id']) content = "Your data export is complete and has been uploaded here:\n\n%s" % ( public_url, ) internal_send_private_message(realm=user_profile.realm, sender=get_system_bot( settings.NOTIFICATION_BOT), recipient_user=user_profile, content=content) # For future frontend use, also notify administrator # clients that the export happened, including sending the # url. notify_export_completed(user_profile, public_url)
def further_validated_draft_dict(draft_dict: Dict[str, Any], user_profile: UserProfile) -> Dict[str, Any]: """ Take a draft_dict that was already validated by draft_dict_validator then further sanitize, validate, and transform it. Ultimately return this "further validated" draft dict. It will have a slightly different set of keys the values for which can be used to directly create a Draft object. """ content = truncate_body(draft_dict["content"]) if "\x00" in content: raise JsonableError(_("Content must not contain null bytes")) timestamp = draft_dict.get("timestamp", time.time()) timestamp = round(timestamp, 6) if timestamp < 0: # While it's not exactly an invalid timestamp, it's not something # we want to allow either. raise JsonableError(_("Timestamp must not be negative.")) last_edit_time = timestamp_to_datetime(timestamp) topic = "" recipient = None to = draft_dict["to"] if draft_dict["type"] == "stream": topic = truncate_topic(draft_dict["topic"]) if "\x00" in topic: raise JsonableError(_("Topic must not contain null bytes")) if len(to) != 1: raise JsonableError( _("Must specify exactly 1 stream ID for stream messages")) stream, sub = access_stream_by_id(user_profile, to[0]) recipient = stream.recipient elif draft_dict["type"] == "private" and len(to) != 0: to_users = get_user_profiles_by_ids(set(to), user_profile.realm) try: recipient = recipient_for_user_profiles(to_users, False, None, user_profile) except ValidationError as e: # nocoverage raise JsonableError(e.messages[0]) return { "recipient": recipient, "topic": topic, "content": content, "last_edit_time": last_edit_time, }
def mute_topic( user_profile: UserProfile, stream_id: Optional[int], stream_name: Optional[str], topic_name: str, date_muted: datetime.datetime, ) -> HttpResponse: if stream_name is not None: (stream, sub) = access_stream_by_name(user_profile, stream_name) else: assert stream_id is not None (stream, sub) = access_stream_by_id(user_profile, stream_id) if topic_is_muted(user_profile, stream.id, topic_name): raise JsonableError(_("Topic already muted")) do_mute_topic(user_profile, stream, topic_name, date_muted) return json_success()
def invite_users_backend( request: HttpRequest, user_profile: UserProfile, invitee_emails_raw: str = REQ("invitee_emails"), invite_as: Optional[int] = REQ( validator=check_int, default=PreregistrationUser.INVITE_AS['MEMBER']), stream_ids: List[int] = REQ(validator=check_list(check_int)), ) -> HttpResponse: if user_profile.realm.invite_by_admins_only and not user_profile.is_realm_admin: return json_error(_("Must be an organization administrator")) if invite_as not in PreregistrationUser.INVITE_AS.values(): return json_error(_("Must be invited as an valid type of user")) if invite_as == PreregistrationUser.INVITE_AS[ 'REALM_ADMIN'] and not user_profile.is_realm_admin: return json_error(_("Must be an organization administrator")) if not invitee_emails_raw: return json_error(_("You must specify at least one email address.")) if not stream_ids: return json_error( _("You must specify at least one stream for invitees to join.")) invitee_emails = get_invitee_emails_set(invitee_emails_raw) # We unconditionally sub you to the notifications stream if it # exists and is public. notifications_stream = user_profile.realm.notifications_stream # type: Optional[Stream] if notifications_stream and not notifications_stream.invite_only: stream_ids.append(notifications_stream.id) streams = [] # type: List[Stream] for stream_id in stream_ids: try: (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) except JsonableError: return json_error( _("Stream does not exist with id: {}. No invites were sent."). format(stream_id)) streams.append(stream) do_invite_users(user_profile, invitee_emails, streams, invite_as) return json_success()
def update_stream_backend(request, user_profile, stream_id, description=REQ(validator=check_string, default=None), is_private=REQ(validator=check_bool, default=None), new_name=REQ(validator=check_string, default=None)): # type: (HttpRequest, UserProfile, int, Optional[Text], Optional[bool], Optional[Text]) -> HttpResponse (stream, recipient, sub) = access_stream_by_id(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() # Will raise if the new name has invalid characters. if stream.name.lower() == new_name.lower(): return json_error(_("Stream already has that name!")) check_stream_name_available(user_profile.realm, new_name) do_rename_stream(stream, new_name) if is_private is not None: do_change_stream_invite_only(stream, is_private) return json_success()
def mute_topic( user_profile: UserProfile, stream_id: Optional[int], stream_name: Optional[str], topic_name: str, date_muted: datetime.datetime, ) -> None: if stream_name is not None: (stream, sub) = access_stream_by_name(user_profile, stream_name) else: assert stream_id is not None (stream, sub) = access_stream_by_id(user_profile, stream_id) if topic_is_muted(user_profile, stream.id, topic_name): raise JsonableError(_("Topic already muted")) try: do_mute_topic(user_profile, stream, topic_name, date_muted) except IntegrityError: raise JsonableError(_("Topic already muted"))
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: 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: 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 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"), ) -> HttpResponse: (stream, sub) = access_stream_by_id(user_profile, stream_id) messages = messages_for_topic(assert_is_not_none(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 = messages.filter(id__in=deletable_message_ids) messages = messages.select_for_update(of=("self",)) do_delete_messages(user_profile.realm, messages) return json_success()
def update_stream_backend(request, user_profile, stream_id, description=REQ(validator=check_string, default=None), is_private=REQ(validator=check_bool, default=None), new_name=REQ(validator=check_string, default=None)): # type: (HttpRequest, UserProfile, int, Optional[Text], Optional[bool], Optional[Text]) -> HttpResponse (stream, recipient, sub) = access_stream_by_id(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_private is not None: do_change_stream_invite_only(stream, is_private) return json_success()
def generate_multiuse_invite_backend( request: HttpRequest, user_profile: UserProfile, invite_expires_in_minutes: Optional[int] = REQ( json_validator=check_none_or(check_int), default=settings.INVITATION_LINK_VALIDITY_MINUTES ), invite_as: int = REQ(json_validator=check_int, default=PreregistrationUser.INVITE_AS["MEMBER"]), stream_ids: Sequence[int] = REQ(json_validator=check_list(check_int), default=[]), ) -> HttpResponse: check_if_owner_required(invite_as, user_profile) streams = [] for stream_id in stream_ids: try: (stream, sub) = access_stream_by_id(user_profile, stream_id) except JsonableError: raise JsonableError(_("Invalid stream ID {}. No invites were sent.").format(stream_id)) streams.append(stream) invite_link = do_create_multiuse_invite_link( user_profile, invite_as, invite_expires_in_minutes, streams ) return json_success(request, data={"invite_link": invite_link})
def invite_users_backend(request: HttpRequest, user_profile: UserProfile, invitee_emails_raw: str=REQ("invitee_emails"), invite_as: Optional[int]=REQ( validator=check_int, default=PreregistrationUser.INVITE_AS['MEMBER']), stream_ids: List[int]=REQ(validator=check_list(check_int)), ) -> HttpResponse: if user_profile.realm.invite_by_admins_only and not user_profile.is_realm_admin: return json_error(_("Must be an organization administrator")) if invite_as not in PreregistrationUser.INVITE_AS.values(): return json_error(_("Must be invited as an valid type of user")) if invite_as == PreregistrationUser.INVITE_AS['REALM_ADMIN'] and not user_profile.is_realm_admin: return json_error(_("Must be an organization administrator")) if not invitee_emails_raw: return json_error(_("You must specify at least one email address.")) if not stream_ids: return json_error(_("You must specify at least one stream for invitees to join.")) invitee_emails = get_invitee_emails_set(invitee_emails_raw) # We unconditionally sub you to the notifications stream if it # exists and is public. notifications_stream = user_profile.realm.notifications_stream # type: Optional[Stream] if notifications_stream and not notifications_stream.invite_only: stream_ids.append(notifications_stream.id) streams = [] # type: List[Stream] for stream_id in stream_ids: try: (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) except JsonableError: return json_error( _("Stream does not exist with id: {}. No invites were sent.".format(stream_id))) streams.append(stream) do_invite_users(user_profile, invitee_emails, streams, invite_as) 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: 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: 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_realm( request: HttpRequest, user_profile: UserProfile, name: Optional[str]=REQ(validator=check_string, default=None), description: Optional[str]=REQ(validator=check_string, default=None), emails_restricted_to_domains: Optional[bool]=REQ(validator=check_bool, default=None), disallow_disposable_email_addresses: Optional[bool]=REQ(validator=check_bool, default=None), invite_required: Optional[bool]=REQ(validator=check_bool, default=None), invite_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None), name_changes_disabled: Optional[bool]=REQ(validator=check_bool, default=None), email_changes_disabled: Optional[bool]=REQ(validator=check_bool, default=None), inline_image_preview: Optional[bool]=REQ(validator=check_bool, default=None), inline_url_embed_preview: Optional[bool]=REQ(validator=check_bool, default=None), create_stream_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None), add_emoji_by_admins_only: Optional[bool]=REQ(validator=check_bool, default=None), allow_message_deleting: Optional[bool]=REQ(validator=check_bool, default=None), message_content_delete_limit_seconds: Optional[int]=REQ(converter=to_non_negative_int, default=None), allow_message_editing: Optional[bool]=REQ(validator=check_bool, default=None), allow_community_topic_editing: Optional[bool]=REQ(validator=check_bool, default=None), mandatory_topics: Optional[bool]=REQ(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(validator=check_bool, default=None), default_language: Optional[str]=REQ(validator=check_string, default=None), waiting_period_threshold: Optional[int]=REQ(converter=to_non_negative_int, default=None), authentication_methods: Optional[Dict[Any, Any]]=REQ(validator=check_dict([]), default=None), notifications_stream_id: Optional[int]=REQ(validator=check_int, default=None), signup_notifications_stream_id: Optional[int]=REQ(validator=check_int, default=None), message_retention_days: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None), send_welcome_emails: Optional[bool]=REQ(validator=check_bool, default=None), message_content_allowed_in_email_notifications: Optional[bool]=REQ(validator=check_bool, default=None), bot_creation_policy: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None), email_address_visibility: Optional[int]=REQ(converter=to_not_negative_int_or_none, default=None), default_twenty_four_hour_time: Optional[bool]=REQ(validator=check_bool, default=None), video_chat_provider: Optional[str]=REQ(validator=check_string, default=None), google_hangouts_domain: Optional[str]=REQ(validator=check_string, default=None), zoom_user_id: Optional[str]=REQ(validator=check_string, default=None), zoom_api_key: Optional[str]=REQ(validator=check_string, default=None), zoom_api_secret: Optional[str]=REQ(validator=check_string, 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 '%s'" % (default_language,))) if description is not None and len(description) > 1000: return json_error(_("Organization description is too long.")) if name is not None and len(name) > Realm.MAX_REALM_NAME_LENGTH: return json_error(_("Organization name is too long.")) if authentication_methods is not None and True not in list(authentication_methods.values()): return json_error(_("At least one authentication method must be enabled.")) if video_chat_provider == "Google Hangouts": try: validate_domain(google_hangouts_domain) except ValidationError as e: return json_error(_('Invalid domain: {}').format(e.messages[0])) if video_chat_provider == "Zoom": if not zoom_user_id: return json_error(_('Invalid user ID: user ID cannot be empty')) if not zoom_api_key: return json_error(_('Invalid API key: API key cannot be empty')) if not zoom_api_secret: return json_error(_('Invalid API secret: API secret cannot be empty')) # Technically, we could call some other API endpoint that # doesn't create a video call link, but this is a nicer # end-to-end test, since it verifies that the Zoom API user's # scopes includes the ability to create video calls, which is # the only capabiility we use. if not request_zoom_video_call_url(zoom_user_id, zoom_api_key, zoom_api_secret): return json_error(_('Invalid credentials for the %(third_party_service)s API.') % dict( third_party_service="Zoom")) # Additional validation of enum-style values if bot_creation_policy is not None and bot_creation_policy not in Realm.BOT_CREATION_POLICY_TYPES: return json_error(_("Invalid bot creation policy")) if email_address_visibility is not None and \ email_address_visibility not in Realm.EMAIL_ADDRESS_VISIBILITY_TYPES: return json_error(_("Invalid email address visibility policy")) # 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} data = {} # type: Dict[str, Any] 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) 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) 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 (allow_community_topic_editing is not None and realm.allow_community_topic_editing != allow_community_topic_editing)): 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 allow_community_topic_editing is None: allow_community_topic_editing = realm.allow_community_topic_editing do_set_realm_message_editing(realm, allow_message_editing, message_content_edit_limit_seconds, allow_community_topic_editing) data['allow_message_editing'] = allow_message_editing data['message_content_edit_limit_seconds'] = message_content_edit_limit_seconds data['allow_community_topic_editing'] = allow_community_topic_editing if (message_content_delete_limit_seconds is not None and realm.message_content_delete_limit_seconds != message_content_delete_limit_seconds): do_set_realm_message_deleting(realm, message_content_delete_limit_seconds) data['message_content_delete_limit_seconds'] = message_content_delete_limit_seconds # 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, recipient, sub) = access_stream_by_id( user_profile, notifications_stream_id) do_set_realm_notifications_stream(realm, new_notifications_stream, notifications_stream_id) 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, recipient, 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) data['signup_notifications_stream_id'] = signup_notifications_stream_id return json_success(data)
def update_realm(request, user_profile, name=REQ(validator=check_string, default=None), description=REQ(validator=check_string, default=None), restricted_to_domain=REQ(validator=check_bool, default=None), invite_required=REQ(validator=check_bool, default=None), invite_by_admins_only=REQ(validator=check_bool, default=None), name_changes_disabled=REQ(validator=check_bool, default=None), email_changes_disabled=REQ(validator=check_bool, default=None), inline_image_preview=REQ(validator=check_bool, default=None), inline_url_embed_preview=REQ(validator=check_bool, default=None), create_stream_by_admins_only=REQ(validator=check_bool, default=None), add_emoji_by_admins_only=REQ(validator=check_bool, default=None), allow_message_editing=REQ(validator=check_bool, default=None), mandatory_topics=REQ(validator=check_bool, default=None), message_content_edit_limit_seconds=REQ(converter=to_non_negative_int, default=None), allow_edit_history=REQ(validator=check_bool, default=None), default_language=REQ(validator=check_string, default=None), waiting_period_threshold=REQ(converter=to_non_negative_int, default=None), authentication_methods=REQ(validator=check_dict([]), default=None), notifications_stream_id=REQ(validator=check_int, default=None), message_retention_days=REQ(converter=to_not_negative_int_or_none, default=None)): # type: (HttpRequest, UserProfile, Optional[str], Optional[str], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[bool], Optional[int], Optional[bool], Optional[str], Optional[int], Optional[dict], Optional[int], Optional[int]) -> 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 '%s'" % (default_language,))) if description is not None and len(description) > 1000: return json_error(_("Realm description is too long.")) if authentication_methods is not None and True not in list(authentication_methods.values()): return json_error(_("At least one authentication method must be enabled.")) # 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} data = {} # type: Dict[str, Any] 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) if isinstance(v, Text): 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) 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): 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 do_set_realm_message_editing(realm, allow_message_editing, message_content_edit_limit_seconds) data['allow_message_editing'] = allow_message_editing data['message_content_edit_limit_seconds'] = message_content_edit_limit_seconds # Realm.notifications_stream is not a boolean, Text 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, recipient, sub) = access_stream_by_id(user_profile, notifications_stream_id) do_set_realm_notifications_stream(realm, new_notifications_stream, notifications_stream_id) data['notifications_stream_id'] = notifications_stream_id return json_success(data)
def deactivate_stream_backend(request, user_profile, stream_id): # type: (HttpRequest, UserProfile, int) -> HttpResponse (stream, recipient, sub) = access_stream_by_id(user_profile, stream_id) do_deactivate_stream(stream) return json_success()