def do_change_stream_post_policy( stream: Stream, stream_post_policy: int, *, acting_user: UserProfile ) -> None: old_post_policy = stream.stream_post_policy with transaction.atomic(): stream.stream_post_policy = stream_post_policy stream.save(update_fields=["stream_post_policy"]) RealmAuditLog.objects.create( realm=stream.realm, acting_user=acting_user, modified_stream=stream, event_type=RealmAuditLog.STREAM_PROPERTY_CHANGED, event_time=timezone_now(), extra_data=orjson.dumps( { RealmAuditLog.OLD_VALUE: old_post_policy, RealmAuditLog.NEW_VALUE: stream_post_policy, "property": "stream_post_policy", } ).decode(), ) event = dict( op="update", type="stream", property="stream_post_policy", value=stream_post_policy, stream_id=stream.id, name=stream.name, ) send_event(stream.realm, event, can_access_stream_user_ids(stream)) # Backwards-compatibility code: We removed the # is_announcement_only property in early 2020, but we send a # duplicate event for legacy mobile clients that might want the # data. event = dict( op="update", type="stream", property="is_announcement_only", value=stream.stream_post_policy == Stream.STREAM_POST_POLICY_ADMINS, stream_id=stream.id, name=stream.name, ) send_event(stream.realm, event, can_access_stream_user_ids(stream)) send_change_stream_post_policy_notification( stream, old_post_policy=old_post_policy, new_post_policy=stream_post_policy, acting_user=acting_user, )
def do_change_stream_description( stream: Stream, new_description: str, *, acting_user: UserProfile ) -> None: old_description = stream.description with transaction.atomic(): stream.description = new_description stream.rendered_description = render_stream_description(new_description) stream.save(update_fields=["description", "rendered_description"]) RealmAuditLog.objects.create( realm=stream.realm, acting_user=acting_user, modified_stream=stream, event_type=RealmAuditLog.STREAM_PROPERTY_CHANGED, event_time=timezone_now(), extra_data=orjson.dumps( { RealmAuditLog.OLD_VALUE: old_description, RealmAuditLog.NEW_VALUE: new_description, "property": "description", } ).decode(), ) event = dict( type="stream", op="update", property="description", name=stream.name, stream_id=stream.id, value=new_description, rendered_description=stream.rendered_description, ) send_event(stream.realm, event, can_access_stream_user_ids(stream)) send_change_stream_description_notification( stream, old_description=old_description, new_description=new_description, acting_user=acting_user, )
def do_change_stream_message_retention_days( stream: Stream, acting_user: UserProfile, message_retention_days: Optional[int] = None) -> None: old_message_retention_days_value = stream.message_retention_days with transaction.atomic(): stream.message_retention_days = message_retention_days stream.save(update_fields=["message_retention_days"]) RealmAuditLog.objects.create( realm=stream.realm, acting_user=acting_user, modified_stream=stream, event_type=RealmAuditLog.STREAM_MESSAGE_RETENTION_DAYS_CHANGED, event_time=timezone_now(), extra_data=orjson.dumps({ RealmAuditLog.OLD_VALUE: old_message_retention_days_value, RealmAuditLog.NEW_VALUE: message_retention_days, }).decode(), ) event = dict( op="update", type="stream", property="message_retention_days", value=message_retention_days, stream_id=stream.id, name=stream.name, ) send_event(stream.realm, event, can_access_stream_user_ids(stream)) send_change_stream_message_retention_days_notification( user_profile=acting_user, stream=stream, old_value=old_message_retention_days_value, new_value=message_retention_days, )
def _set_stream_message_retention_value( self, stream: Stream, retention_period: Optional[int]) -> None: stream.message_retention_days = retention_period stream.save()
def do_rename_stream(stream: Stream, new_name: str, user_profile: UserProfile) -> Dict[str, str]: old_name = stream.name stream.name = new_name stream.save(update_fields=["name"]) RealmAuditLog.objects.create( realm=stream.realm, acting_user=user_profile, modified_stream=stream, event_type=RealmAuditLog.STREAM_NAME_CHANGED, event_time=timezone_now(), extra_data=orjson.dumps( { RealmAuditLog.OLD_VALUE: old_name, RealmAuditLog.NEW_VALUE: new_name, } ).decode(), ) recipient_id = stream.recipient_id messages = Message.objects.filter(recipient_id=recipient_id).only("id") # Update the display recipient and stream, which are easy single # items to set. old_cache_key = get_stream_cache_key(old_name, stream.realm_id) new_cache_key = get_stream_cache_key(stream.name, stream.realm_id) if old_cache_key != new_cache_key: cache_delete(old_cache_key) cache_set(new_cache_key, stream) cache_set(display_recipient_cache_key(recipient_id), stream.name) # Delete cache entries for everything else, which is cheaper and # clearer than trying to set them. display_recipient is the out of # date field in all cases. cache_delete_many(to_dict_cache_key_id(message.id) for message in messages) new_email = encode_email_address(stream, show_sender=True) # We will tell our users to essentially # update stream.name = new_name where name = old_name # and update stream.email = new_email where name = old_name. # We could optimize this by trying to send one message, but the # client code really wants one property update at a time, and # updating stream names is a pretty infrequent operation. # More importantly, we want to key these updates by id, not name, # since id is the immutable primary key, and obviously name is not. data_updates = [ ["email_address", new_email], ["name", new_name], ] for property, value in data_updates: event = dict( op="update", type="stream", property=property, value=value, stream_id=stream.id, name=old_name, ) send_event(stream.realm, event, can_access_stream_user_ids(stream)) sender = get_system_bot(settings.NOTIFICATION_BOT, stream.realm_id) with override_language(stream.realm.default_language): internal_send_stream_message( sender, stream, Realm.STREAM_EVENTS_NOTIFICATION_TOPIC, _("{user_name} renamed stream {old_stream_name} to {new_stream_name}.").format( user_name=silent_mention_syntax_for_user(user_profile), old_stream_name=f"**{old_name}**", new_stream_name=f"**{new_name}**", ), ) # Even though the token doesn't change, the web client needs to update the # email forwarding address to display the correctly-escaped new name. return {"email_address": new_email}
def do_change_stream_permission( stream: Stream, *, invite_only: Optional[bool] = None, history_public_to_subscribers: Optional[bool] = None, is_web_public: Optional[bool] = None, acting_user: UserProfile, ) -> None: old_invite_only_value = stream.invite_only old_history_public_to_subscribers_value = stream.history_public_to_subscribers old_is_web_public_value = stream.is_web_public # A note on these assertions: It's possible we'd be better off # making all callers of this function pass the full set of # parameters, rather than having default values. Doing so would # allow us to remove the messy logic below, where we sometimes # ignore the passed parameters. # # But absent such a refactoring, it's important to assert that # we're not requesting an unsupported configurations. if is_web_public: assert history_public_to_subscribers is not False assert invite_only is not True stream.is_web_public = True stream.invite_only = False stream.history_public_to_subscribers = True else: assert invite_only is not None # is_web_public is falsey history_public_to_subscribers = get_default_value_for_history_public_to_subscribers( stream.realm, invite_only, history_public_to_subscribers, ) stream.invite_only = invite_only stream.history_public_to_subscribers = history_public_to_subscribers stream.is_web_public = False with transaction.atomic(): stream.save(update_fields=["invite_only", "history_public_to_subscribers", "is_web_public"]) event_time = timezone_now() if old_invite_only_value != stream.invite_only: # Reset the Attachment.is_realm_public cache for all # messages in the stream whose permissions were changed. Attachment.objects.filter(messages__recipient_id=stream.recipient_id).update( is_realm_public=None ) # We need to do the same for ArchivedAttachment to avoid # bugs if deleted attachments are later restored. ArchivedAttachment.objects.filter(messages__recipient_id=stream.recipient_id).update( is_realm_public=None ) RealmAuditLog.objects.create( realm=stream.realm, acting_user=acting_user, modified_stream=stream, event_type=RealmAuditLog.STREAM_PROPERTY_CHANGED, event_time=event_time, extra_data=orjson.dumps( { RealmAuditLog.OLD_VALUE: old_invite_only_value, RealmAuditLog.NEW_VALUE: stream.invite_only, "property": "invite_only", } ).decode(), ) if old_history_public_to_subscribers_value != stream.history_public_to_subscribers: RealmAuditLog.objects.create( realm=stream.realm, acting_user=acting_user, modified_stream=stream, event_type=RealmAuditLog.STREAM_PROPERTY_CHANGED, event_time=event_time, extra_data=orjson.dumps( { RealmAuditLog.OLD_VALUE: old_history_public_to_subscribers_value, RealmAuditLog.NEW_VALUE: stream.history_public_to_subscribers, "property": "history_public_to_subscribers", } ).decode(), ) if old_is_web_public_value != stream.is_web_public: # Reset the Attachment.is_realm_public cache for all # messages in the stream whose permissions were changed. Attachment.objects.filter(messages__recipient_id=stream.recipient_id).update( is_web_public=None ) # We need to do the same for ArchivedAttachment to avoid # bugs if deleted attachments are later restored. ArchivedAttachment.objects.filter(messages__recipient_id=stream.recipient_id).update( is_web_public=None ) RealmAuditLog.objects.create( realm=stream.realm, acting_user=acting_user, modified_stream=stream, event_type=RealmAuditLog.STREAM_PROPERTY_CHANGED, event_time=event_time, extra_data=orjson.dumps( { RealmAuditLog.OLD_VALUE: old_is_web_public_value, RealmAuditLog.NEW_VALUE: stream.is_web_public, "property": "is_web_public", } ).decode(), ) event = dict( op="update", type="stream", property="invite_only", value=stream.invite_only, history_public_to_subscribers=stream.history_public_to_subscribers, is_web_public=stream.is_web_public, stream_id=stream.id, name=stream.name, ) send_event(stream.realm, event, can_access_stream_user_ids(stream)) old_policy_name = get_stream_permission_policy_name( invite_only=old_invite_only_value, history_public_to_subscribers=old_history_public_to_subscribers_value, is_web_public=old_is_web_public_value, ) new_policy_name = get_stream_permission_policy_name( invite_only=stream.invite_only, history_public_to_subscribers=stream.history_public_to_subscribers, is_web_public=stream.is_web_public, ) send_change_stream_permission_notification( stream, old_policy_name=old_policy_name, new_policy_name=new_policy_name, acting_user=acting_user, )
def do_deactivate_stream( stream: Stream, log: bool = True, *, acting_user: Optional[UserProfile] ) -> None: # We want to mark all messages in the to-be-deactivated stream as # read for all users; otherwise they will pollute queries like # "Get the user's first unread message". Since this can be an # expensive operation, we do it via the deferred_work queue # processor. deferred_work_event = { "type": "mark_stream_messages_as_read_for_everyone", "stream_recipient_id": stream.recipient_id, } transaction.on_commit(lambda: queue_json_publish("deferred_work", deferred_work_event)) # Get the affected user ids *before* we deactivate everybody. affected_user_ids = can_access_stream_user_ids(stream) get_active_subscriptions_for_stream_id(stream.id, include_deactivated_users=True).update( active=False ) was_invite_only = stream.invite_only stream.deactivated = True stream.invite_only = True # Preserve as much as possible the original stream name while giving it a # special prefix that both indicates that the stream is deactivated and # frees up the original name for reuse. old_name = stream.name # Prepend a substring of the hashed stream ID to the new stream name streamID = str(stream.id) stream_id_hash_object = hashlib.sha512(streamID.encode()) hashed_stream_id = stream_id_hash_object.hexdigest()[0:7] new_name = (hashed_stream_id + "!DEACTIVATED:" + old_name)[: Stream.MAX_NAME_LENGTH] stream.name = new_name[: Stream.MAX_NAME_LENGTH] stream.save(update_fields=["name", "deactivated", "invite_only"]) # If this is a default stream, remove it, properly sending a # notification to browser clients. if DefaultStream.objects.filter(realm_id=stream.realm_id, stream_id=stream.id).exists(): do_remove_default_stream(stream) default_stream_groups_for_stream = DefaultStreamGroup.objects.filter(streams__id=stream.id) for group in default_stream_groups_for_stream: do_remove_streams_from_default_stream_group(stream.realm, group, [stream]) # Remove the old stream information from remote cache. old_cache_key = get_stream_cache_key(old_name, stream.realm_id) cache_delete(old_cache_key) stream_dict = stream.to_dict() stream_dict.update(dict(name=old_name, invite_only=was_invite_only)) event = dict(type="stream", op="delete", streams=[stream_dict]) transaction.on_commit(lambda: send_event(stream.realm, event, affected_user_ids)) event_time = timezone_now() RealmAuditLog.objects.create( realm=stream.realm, acting_user=acting_user, modified_stream=stream, event_type=RealmAuditLog.STREAM_DEACTIVATED, event_time=event_time, )