channel_folder: Add API to create a channel folder.

This commit also includes code to include channel_folders
data in register response.

Fixes part of #31972.
This commit is contained in:
Sahil Batra
2025-05-02 19:20:22 +05:30
committed by Tim Abbott
parent 350f6a1fa1
commit 332abd9e91
17 changed files with 471 additions and 4 deletions

View File

@@ -0,0 +1,60 @@
from typing import TypedDict
from django.utils.translation import gettext as _
from zerver.lib.exceptions import JsonableError
from zerver.lib.markdown import markdown_convert
from zerver.lib.string_validation import check_string_is_printable
from zerver.lib.timestamp import datetime_to_timestamp
from zerver.models import ChannelFolder, Realm, UserProfile
class ChannelFolderDict(TypedDict):
id: int
name: str
description: str
rendered_description: str
creator_id: int | None
date_created: int
is_archived: bool
def check_channel_folder_name(name: str, realm: Realm) -> None:
if name.strip() == "":
raise JsonableError(_("Channel folder name can't be empty."))
invalid_character_pos = check_string_is_printable(name)
if invalid_character_pos is not None:
raise JsonableError(
_("Invalid character in channel folder name, at position {position}.").format(
position=invalid_character_pos
)
)
if ChannelFolder.objects.filter(name__iexact=name, realm=realm).exists():
raise JsonableError(_("Channel folder '{name}' already exists").format(name=name))
def render_channel_folder_description(text: str, realm: Realm, *, acting_user: UserProfile) -> str:
return markdown_convert(
text, message_realm=realm, no_previews=True, acting_user=acting_user
).rendered_content
def get_channel_folder_dict(channel_folder: ChannelFolder) -> ChannelFolderDict:
date_created = datetime_to_timestamp(channel_folder.date_created)
return ChannelFolderDict(
id=channel_folder.id,
name=channel_folder.name,
description=channel_folder.description,
rendered_description=channel_folder.rendered_description,
date_created=date_created,
creator_id=channel_folder.creator_id,
is_archived=channel_folder.is_archived,
)
def get_channel_folders_in_realm(realm: Realm) -> list[ChannelFolderDict]:
folders = ChannelFolder.objects.filter(realm=realm)
channel_folders = [get_channel_folder_dict(channel_folder) for channel_folder in folders]
return sorted(channel_folders, key=lambda folder: folder["id"])

View File

@@ -23,6 +23,7 @@ from zerver.lib.event_types import (
EventAttachmentAdd,
EventAttachmentRemove,
EventAttachmentUpdate,
EventChannelFolderAdd,
EventCustomProfileFields,
EventDefaultStreamGroups,
EventDefaultStreams,
@@ -161,6 +162,7 @@ check_alert_words = make_checker(EventAlertWords)
check_attachment_add = make_checker(EventAttachmentAdd)
check_attachment_remove = make_checker(EventAttachmentRemove)
check_attachment_update = make_checker(EventAttachmentUpdate)
check_channel_folder_add = make_checker(EventChannelFolderAdd)
check_custom_profile_fields = make_checker(EventCustomProfileFields)
check_default_stream_groups = make_checker(EventDefaultStreamGroups)
check_default_streams = make_checker(EventDefaultStreams)

View File

@@ -66,6 +66,22 @@ class EventAttachmentUpdate(BaseEvent):
upload_space_used: int
class ChannelFolderForEventChannelFolderAdd(BaseModel):
id: int
name: str
description: str
rendered_description: str
date_created: int
creator_id: int
is_archived: bool
class EventChannelFolderAdd(BaseEvent):
type: Literal["channel_folder"]
op: Literal["add"]
channel_folder: ChannelFolderForEventChannelFolderAdd
class DetailedCustomProfileCore(BaseModel):
id: int
type: int

View File

@@ -19,6 +19,7 @@ from zerver.lib import emoji
from zerver.lib.alert_words import user_alert_words
from zerver.lib.avatar import avatar_url
from zerver.lib.bot_config import load_bot_config_template
from zerver.lib.channel_folders import get_channel_folders_in_realm
from zerver.lib.compatibility import is_outdated_server
from zerver.lib.default_streams import get_default_stream_ids_for_realm
from zerver.lib.exceptions import JsonableError
@@ -764,6 +765,11 @@ def fetch_initial_state_data(
state["unsubscribed"] = sub_info.unsubscribed
state["never_subscribed"] = sub_info.never_subscribed
if want("channel_folders") and user_profile is not None:
# TODO: Spectators should get the channel folders that
# contain atleast one web-public channel.
state["channel_folders"] = get_channel_folders_in_realm(user_profile.realm)
if want("update_message_flags") and want("message"):
# Keeping unread_msgs updated requires both message flag updates and
# message updates. This is due to the fact that new messages will not
@@ -1870,6 +1876,12 @@ def apply_event(
else:
fields = ["stream_id", "topic_name", "visibility_policy", "last_updated"]
state["user_topics"].append({x: event[x] for x in fields})
elif event["type"] == "channel_folder":
if event["op"] == "add":
state["channel_folders"].append(event["channel_folder"])
state["channel_folders"].sort(key=lambda folder: folder["id"])
else:
raise AssertionError("Unexpected event type {type}/{op}".format(**event))
elif event["type"] == "has_zoom_token":
state["has_zoom_token"] = event["value"]
elif event["type"] == "web_reload_client":