feat: add channel management dialogs and styling

feat: implement channel creation, settings, and deletion UI
feat: add RPC methods for channel operations
This commit is contained in:
retoor 2025-12-26 02:05:06 +01:00
parent 401e558011
commit 163828e213
11 changed files with 803 additions and 8 deletions

View File

@ -14,6 +14,14 @@
## Version 1.14.0 - 2025-12-26
Users can now create, configure settings for, and delete channels through dedicated dialog interfaces. Developers access new RPC methods to support these channel management operations.
**Changes:** 9 files, 801 lines
**Languages:** CSS (127 lines), HTML (517 lines), Python (157 lines)
## Version 1.13.0 - 2025-12-24 ## Version 1.13.0 - 2025-12-24
Improves performance in the balancer, socket service, and cache by removing async locks. Adds database connection injection for testing in the app and updates pytest configuration. Improves performance in the balancer, socket service, and cache by removing async locks. Adds database connection injection for testing in the app and updates pytest configuration.

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "Snek" name = "Snek"
version = "1.13.0" version = "1.14.0"
readme = "README.md" readme = "README.md"
#license = { file = "LICENSE", content-type="text/markdown" } #license = { file = "LICENSE", content-type="text/markdown" }
description = "Snek Chat Application by Molodetz" description = "Snek Chat Application by Molodetz"

View File

@ -429,6 +429,20 @@ a {
margin-top: 0; margin-top: 0;
} }
.sidebar-add-btn {
color: #f05a28;
text-decoration: none;
font-size: 1.2em;
font-weight: bold;
margin-left: 8px;
opacity: 0.7;
transition: opacity 0.2s;
}
.sidebar-add-btn:hover {
opacity: 1;
}
.sidebar ul { .sidebar ul {
list-style: none; list-style: none;
margin-bottom: 15px; margin-bottom: 15px;
@ -657,6 +671,119 @@ dialog .dialog-button:disabled:focus {
outline: none; outline: none;
} }
dialog .dialog-form {
display: flex;
flex-direction: column;
gap: 12px;
}
dialog .dialog-input {
width: 100%;
padding: 10px 12px;
border: 1px solid #333;
border-radius: 4px;
background-color: #0f0f0f;
color: #e6e6e6;
font-family: 'Courier New', monospace;
font-size: 14px;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
box-sizing: border-box;
}
dialog .dialog-input:focus {
outline: none;
border-color: #f05a28;
box-shadow: 0 0 0 2px rgba(240, 90, 40, 0.2);
}
dialog .dialog-input::placeholder {
color: #555;
}
dialog .dialog-input.error {
border-color: #8b0000;
box-shadow: 0 0 0 2px rgba(139, 0, 0, 0.2);
}
dialog .dialog-checkbox-label {
display: flex;
align-items: center;
gap: 8px;
color: #e6e6e6;
font-size: 14px;
cursor: pointer;
}
dialog .dialog-checkbox-label input[type="checkbox"] {
width: 16px;
height: 16px;
accent-color: #f05a28;
}
dialog .dialog-suggestions {
max-height: 150px;
overflow-y: auto;
}
dialog .dialog-suggestion-item {
padding: 8px 12px;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
dialog .dialog-suggestion-item:hover {
background-color: #1a1a1a;
}
dialog .dialog-box-wide {
max-width: 500px;
}
dialog .dialog-label {
display: block;
font-size: 12px;
color: #888;
margin-bottom: 4px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
dialog .dialog-label-danger {
color: #8b0000;
}
dialog .dialog-divider {
height: 1px;
background-color: #333;
margin: 16px 0;
}
dialog .dialog-danger-zone {
padding: 12px;
border: 1px solid #8b0000;
border-radius: 4px;
background-color: rgba(139, 0, 0, 0.1);
}
dialog .dialog-button.danger {
background-color: #8b0000;
border-color: #8b0000;
color: #fff;
}
dialog .dialog-button.danger:hover {
background-color: #a00000;
border-color: #a00000;
}
dialog .dialog-button.danger:disabled {
background-color: #0a0a0a;
border-color: #222;
color: #555;
cursor: not-allowed;
}
.embed-url-link { .embed-url-link {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -0,0 +1,205 @@
<dialog id="channel-settings-dialog">
<div class="dialog-backdrop">
<div class="dialog-box dialog-box-wide">
<div class="dialog-title"><h2>Channel Settings</h2></div>
<div class="dialog-content">
<div class="dialog-form">
<label class="dialog-label">Name</label>
<input type="text" id="settings-channel-name" class="dialog-input" placeholder="Channel name" autocomplete="off">
<label class="dialog-label">Description</label>
<input type="text" id="settings-channel-description" class="dialog-input" placeholder="Description (optional)" autocomplete="off">
<label class="dialog-checkbox-label">
<input type="checkbox" id="settings-channel-private">
<span>Private channel</span>
</label>
<div class="dialog-divider"></div>
<div class="dialog-danger-zone">
<label class="dialog-label dialog-label-danger">Danger Zone</label>
<button class="dialog-button danger btn-delete-channel">Delete Channel</button>
</div>
</div>
</div>
<div class="dialog-actions">
<button class="dialog-button secondary btn-cancel">Cancel</button>
<button class="dialog-button primary btn-save">Save</button>
</div>
</div>
</div>
</dialog>
<dialog id="delete-channel-dialog">
<div class="dialog-backdrop">
<div class="dialog-box">
<div class="dialog-title"><h2>Delete Channel</h2></div>
<div class="dialog-content">
<p>This action cannot be undone. Type the channel name to confirm:</p>
<div class="dialog-form">
<input type="text" id="delete-channel-confirm" class="dialog-input" placeholder="Channel name" autocomplete="off">
</div>
</div>
<div class="dialog-actions">
<button class="dialog-button secondary btn-cancel">Cancel</button>
<button class="dialog-button danger btn-confirm-delete" disabled>Delete</button>
</div>
</div>
</div>
</dialog>
<script>
class ChannelSettingsDialog {
constructor() {
this.dialog = document.getElementById('channel-settings-dialog');
this.nameInput = document.getElementById('settings-channel-name');
this.descriptionInput = document.getElementById('settings-channel-description');
this.privateInput = document.getElementById('settings-channel-private');
this.saveButton = this.dialog.querySelector('.btn-save');
this.cancelButton = this.dialog.querySelector('.btn-cancel');
this.deleteButton = this.dialog.querySelector('.btn-delete-channel');
this.channelUid = null;
this.channelName = null;
this.isModerator = false;
this._initEventListeners();
}
_initEventListeners() {
this.cancelButton.addEventListener('click', () => this.close());
this.saveButton.addEventListener('click', () => this.save());
this.deleteButton.addEventListener('click', () => this.showDeleteConfirm());
}
async show(channelUid) {
this.channelUid = channelUid;
const channels = await window.app.rpc.getChannels();
const channel = channels.find(c => c.uid === channelUid);
if (!channel) {
alert('Channel not found');
return;
}
this.channelName = channel.name;
this.isModerator = channel.is_moderator;
this.nameInput.value = channel.name.replace(/^#/, '');
this.descriptionInput.value = '';
this.privateInput.checked = false;
if (!this.isModerator) {
this.nameInput.disabled = true;
this.descriptionInput.disabled = true;
this.privateInput.disabled = true;
this.saveButton.disabled = true;
this.deleteButton.disabled = true;
} else {
this.nameInput.disabled = false;
this.descriptionInput.disabled = false;
this.privateInput.disabled = false;
this.saveButton.disabled = false;
this.deleteButton.disabled = false;
}
this.dialog.showModal();
this.nameInput.focus();
}
close() {
this.dialog.close();
}
async save() {
const name = this.nameInput.value.trim();
if (!name) {
this.nameInput.classList.add('error');
return;
}
this.nameInput.classList.remove('error');
this.saveButton.disabled = true;
try {
const result = await window.app.rpc.updateChannel(
this.channelUid,
name,
this.descriptionInput.value.trim() || null,
this.privateInput.checked
);
if (result && result.success) {
this.close();
window.location.reload();
} else if (result && result.error) {
alert(result.error);
}
} catch (err) {
alert('Failed to update channel');
} finally {
this.saveButton.disabled = false;
}
}
showDeleteConfirm() {
this.close();
deleteChannelDialog.show(this.channelUid, this.channelName);
}
}
class DeleteChannelDialog {
constructor() {
this.dialog = document.getElementById('delete-channel-dialog');
this.confirmInput = document.getElementById('delete-channel-confirm');
this.deleteButton = this.dialog.querySelector('.btn-confirm-delete');
this.cancelButton = this.dialog.querySelector('.btn-cancel');
this.channelUid = null;
this.channelName = null;
this._initEventListeners();
}
_initEventListeners() {
this.cancelButton.addEventListener('click', () => this.close());
this.deleteButton.addEventListener('click', () => this.delete());
this.confirmInput.addEventListener('input', () => this.validateInput());
}
validateInput() {
const input = this.confirmInput.value.trim();
const name = this.channelName.replace(/^#/, '');
this.deleteButton.disabled = input !== name;
}
show(channelUid, channelName) {
this.channelUid = channelUid;
this.channelName = channelName;
this.confirmInput.value = '';
this.deleteButton.disabled = true;
this.dialog.showModal();
this.confirmInput.focus();
}
close() {
this.dialog.close();
}
async delete() {
this.deleteButton.disabled = true;
try {
const result = await window.app.rpc.deleteChannel(this.channelUid);
if (result && result.success) {
this.close();
window.location.href = '/';
} else if (result && result.error) {
alert(result.error);
}
} catch (err) {
alert('Failed to delete channel');
} finally {
this.deleteButton.disabled = false;
}
}
}
const channelSettingsDialog = new ChannelSettingsDialog();
const deleteChannelDialog = new DeleteChannelDialog();
function showChannelSettings() {
channelSettingsDialog.show('{{ channel.uid.value }}');
}
</script>

View File

@ -0,0 +1,91 @@
<dialog id="create-channel-dialog">
<div class="dialog-backdrop">
<div class="dialog-box">
<div class="dialog-title"><h2>Create Channel</h2></div>
<div class="dialog-content">
<div class="dialog-form">
<input type="text" id="channel-name-input" class="dialog-input" placeholder="Channel name" autocomplete="off">
<input type="text" id="channel-description-input" class="dialog-input" placeholder="Description (optional)" autocomplete="off">
<label class="dialog-checkbox-label">
<input type="checkbox" id="channel-private-input">
<span>Private channel</span>
</label>
</div>
</div>
<div class="dialog-actions">
<button class="dialog-button secondary btn-cancel">Cancel</button>
<button class="dialog-button primary btn-create">Create</button>
</div>
</div>
</div>
</dialog>
<script>
class CreateChannelDialog {
constructor() {
this.dialog = document.getElementById('create-channel-dialog');
this.nameInput = document.getElementById('channel-name-input');
this.descriptionInput = document.getElementById('channel-description-input');
this.privateInput = document.getElementById('channel-private-input');
this.createButton = this.dialog.querySelector('.btn-create');
this.cancelButton = this.dialog.querySelector('.btn-cancel');
this._initEventListeners();
}
_initEventListeners() {
this.cancelButton.addEventListener('click', () => this.close());
this.createButton.addEventListener('click', () => this.create());
this.nameInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') this.create();
});
}
show() {
this.nameInput.value = '';
this.descriptionInput.value = '';
this.privateInput.checked = false;
this.dialog.showModal();
this.nameInput.focus();
}
close() {
this.dialog.close();
}
async create() {
const name = this.nameInput.value.trim();
if (!name) {
this.nameInput.classList.add('error');
return;
}
this.nameInput.classList.remove('error');
this.createButton.disabled = true;
try {
const result = await window.app.rpc.createChannel(
name,
this.descriptionInput.value.trim() || null,
this.privateInput.checked
);
if (result && result.uid) {
this.close();
window.location.href = `/channel/${result.uid}.html`;
} else if (result && result.error) {
this.nameInput.classList.add('error');
this.nameInput.title = result.error;
}
} catch (err) {
this.nameInput.classList.add('error');
this.nameInput.title = 'Failed to create channel';
} finally {
this.createButton.disabled = false;
}
}
}
const createChannelDialog = new CreateChannelDialog();
function showCreateChannel() {
createChannelDialog.show();
}
</script>

View File

@ -35,6 +35,26 @@
{ {
command: "/live", command: "/live",
description: "Toggle live typing mode" description: "Toggle live typing mode"
},
{
command: "/create",
description: "Create a new channel"
},
{
command: "/invite",
description: "Invite a user to this channel"
},
{
command: "/leave",
description: "Leave this channel"
},
{
command: "/settings",
description: "Channel settings (moderators)"
},
{
command: "/container",
description: "Manage channel container"
} }
]; ];

View File

@ -0,0 +1,116 @@
<dialog id="invite-dialog">
<div class="dialog-backdrop">
<div class="dialog-box">
<div class="dialog-title"><h2>Invite User</h2></div>
<div class="dialog-content">
<div class="dialog-form">
<input type="text" id="invite-username-input" class="dialog-input" placeholder="Username" autocomplete="off">
<div id="invite-user-suggestions" class="dialog-suggestions"></div>
</div>
</div>
<div class="dialog-actions">
<button class="dialog-button secondary btn-cancel">Cancel</button>
<button class="dialog-button primary btn-invite">Invite</button>
</div>
</div>
</div>
</dialog>
<script>
class InviteDialog {
constructor() {
this.dialog = document.getElementById('invite-dialog');
this.usernameInput = document.getElementById('invite-username-input');
this.suggestions = document.getElementById('invite-user-suggestions');
this.inviteButton = this.dialog.querySelector('.btn-invite');
this.cancelButton = this.dialog.querySelector('.btn-cancel');
this.channelUid = null;
this._searchTimeout = null;
this._initEventListeners();
}
_initEventListeners() {
this.cancelButton.addEventListener('click', () => this.close());
this.inviteButton.addEventListener('click', () => this.invite());
this.usernameInput.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
this.invite();
} else {
this._debounceSearch();
}
});
}
_debounceSearch() {
clearTimeout(this._searchTimeout);
this._searchTimeout = setTimeout(() => this._search(), 300);
}
async _search() {
const query = this.usernameInput.value.trim();
if (query.length < 2) {
this.suggestions.innerHTML = '';
return;
}
try {
const users = await window.app.rpc.searchUser(query);
this.suggestions.innerHTML = '';
users.forEach(username => {
const div = document.createElement('div');
div.className = 'dialog-suggestion-item';
div.textContent = username;
div.addEventListener('click', () => {
this.usernameInput.value = username;
this.suggestions.innerHTML = '';
});
this.suggestions.appendChild(div);
});
} catch (err) {
this.suggestions.innerHTML = '';
}
}
show(channelUid) {
this.channelUid = channelUid;
this.usernameInput.value = '';
this.suggestions.innerHTML = '';
this.dialog.showModal();
this.usernameInput.focus();
}
close() {
this.dialog.close();
}
async invite() {
const username = this.usernameInput.value.trim();
if (!username) {
this.usernameInput.classList.add('error');
return;
}
this.usernameInput.classList.remove('error');
this.inviteButton.disabled = true;
try {
const result = await window.app.rpc.inviteUser(this.channelUid, username);
if (result && result.success) {
this.close();
} else if (result && result.error) {
this.usernameInput.classList.add('error');
this.usernameInput.title = result.error;
}
} catch (err) {
this.usernameInput.classList.add('error');
this.usernameInput.title = 'Failed to invite user';
} finally {
this.inviteButton.disabled = false;
}
}
}
const inviteDialog = new InviteDialog();
function showInvite() {
inviteDialog.show('{{ channel.uid.value }}');
}
</script>

View File

@ -0,0 +1,65 @@
<dialog id="leave-channel-dialog">
<div class="dialog-backdrop">
<div class="dialog-box">
<div class="dialog-title"><h2>Leave Channel</h2></div>
<div class="dialog-content">
<p>Are you sure you want to leave this channel?</p>
</div>
<div class="dialog-actions">
<button class="dialog-button secondary btn-cancel">Cancel</button>
<button class="dialog-button primary btn-leave">Leave</button>
</div>
</div>
</div>
</dialog>
<script>
class LeaveChannelDialog {
constructor() {
this.dialog = document.getElementById('leave-channel-dialog');
this.leaveButton = this.dialog.querySelector('.btn-leave');
this.cancelButton = this.dialog.querySelector('.btn-cancel');
this.channelUid = null;
this._initEventListeners();
}
_initEventListeners() {
this.cancelButton.addEventListener('click', () => this.close());
this.leaveButton.addEventListener('click', () => this.leave());
}
show(channelUid) {
this.channelUid = channelUid;
this.dialog.showModal();
this.cancelButton.focus();
}
close() {
this.dialog.close();
}
async leave() {
this.leaveButton.disabled = true;
try {
const result = await window.app.rpc.leaveChannel(this.channelUid);
if (result && result.success) {
this.close();
window.location.href = '/';
} else if (result && result.error) {
alert(result.error);
}
} catch (err) {
alert('Failed to leave channel');
} finally {
this.leaveButton.disabled = false;
}
}
}
const leaveChannelDialog = new LeaveChannelDialog();
function showLeaveChannel() {
leaveChannelDialog.show('{{ channel.uid.value }}');
}
</script>

View File

@ -11,7 +11,7 @@
</ul> </ul>
{% if channels %} {% if channels %}
<h2 class="no-select">Channels</h2> <h2 class="no-select">Channels <a href="#" class="sidebar-add-btn" onclick="event.preventDefault(); showCreateChannel();" title="Create channel">+</a></h2>
<ul> <ul>
{% for channel in channels if not channel['is_private'] %} {% for channel in channels if not channel['is_private'] %}
<li id="channel-list-item-{{channel['uid']}}"><a class="no-select" {% if channel['color'] %}style="color: {{channel['color']}}"{% endif %} href="/channel/{{channel['uid']}}.html">{{channel['name']}} <span class="message-count">{% if channel['new_count'] %}({{ channel['new_count'] }}){% endif %}</span></a></li> <li id="channel-list-item-{{channel['uid']}}"><a class="no-select" {% if channel['color'] %}style="color: {{channel['color']}}"{% endif %} href="/channel/{{channel['uid']}}.html">{{channel['name']}} <span class="message-count">{% if channel['new_count'] %}({{ channel['new_count'] }}){% endif %}</span></a></li>

View File

@ -39,6 +39,10 @@
{% include "dialog_container.html" %} {% include "dialog_container.html" %}
{% include "dialog_help.html" %} {% include "dialog_help.html" %}
{% include "dialog_online.html" %} {% include "dialog_online.html" %}
{% include "dialog_create_channel.html" %}
{% include "dialog_invite.html" %}
{% include "dialog_leave_channel.html" %}
{% include "dialog_channel_settings.html" %}
<script type="module"> <script type="module">
import {app} from "/app.js"; import {app} from "/app.js";
@ -54,9 +58,11 @@ chatInputField.autoCompletions = {
"/clear": () => { messagesContainer.innerHTML = ''; }, "/clear": () => { messagesContainer.innerHTML = ''; },
"/live": () => { chatInputField.liveType = !chatInputField.liveType; }, "/live": () => { chatInputField.liveType = !chatInputField.liveType; },
"/help": showHelp, "/help": showHelp,
"/container": async() =>{ "/container": async() => { containerDialog.openWithStatus(); },
containerDialog.openWithStatus() "/create": showCreateChannel,
} "/invite": showInvite,
"/leave": showLeaveChannel,
"/settings": showChannelSettings
}; };
// --- Throttle utility --- // --- Throttle utility ---

View File

@ -954,6 +954,163 @@ class RPCView(BaseView):
logger.warning(f"stars_render failed: {safe_str(ex)}") logger.warning(f"stars_render failed: {safe_str(ex)}")
return False return False
async def createChannel(self, name, description=None, is_private=False):
self._require_login()
self._require_services()
if not name or not isinstance(name, str):
return {"error": "Invalid channel name", "success": False}
name = name.strip()
if not name:
return {"error": "Channel name cannot be empty", "success": False}
try:
channel = await self.services.channel.create(
label=name,
created_by_uid=self.user_uid,
description=description,
is_private=is_private,
is_listed=True
)
if not channel:
return {"error": "Failed to create channel", "success": False}
await self.services.channel_member.create(
channel["uid"],
self.user_uid,
is_moderator=True
)
await self.services.socket.subscribe(self.ws, channel["uid"], self.user_uid)
return {
"success": True,
"uid": channel["uid"],
"name": channel["label"]
}
except Exception as ex:
logger.warning(f"Failed to create channel: {safe_str(ex)}")
return {"error": safe_str(ex), "success": False}
async def inviteUser(self, channel_uid, username):
self._require_login()
self._require_services()
if not channel_uid or not isinstance(channel_uid, str):
return {"error": "Invalid channel", "success": False}
if not username or not isinstance(username, str):
return {"error": "Invalid username", "success": False}
try:
channel_member = await self.services.channel_member.get(
channel_uid=channel_uid, user_uid=self.user_uid
)
if not channel_member:
raise PermissionError("Not a member of this channel")
user = await self.services.user.get(username=username.strip())
if not user:
return {"error": "User not found", "success": False}
existing = await self.services.channel_member.get(
channel_uid=channel_uid, user_uid=user["uid"]
)
if existing and not existing["deleted_at"]:
return {"error": "User is already a member", "success": False}
result = await self.services.channel_member.create(
channel_uid,
user["uid"],
is_moderator=False
)
if not result:
return {"error": "Failed to invite user", "success": False}
return {"success": True, "username": user["username"]}
except PermissionError as ex:
return {"error": safe_str(ex), "success": False}
except Exception as ex:
logger.warning(f"Failed to invite user: {safe_str(ex)}")
return {"error": "Failed to invite user", "success": False}
async def leaveChannel(self, channel_uid):
self._require_login()
self._require_services()
if not channel_uid or not isinstance(channel_uid, str):
return {"error": "Invalid channel", "success": False}
try:
channel = await self.services.channel.get(uid=channel_uid)
if not channel:
return {"error": "Channel not found", "success": False}
if channel["tag"] == "public":
return {"error": "Cannot leave the public channel", "success": False}
channel_member = await self.services.channel_member.get(
channel_uid=channel_uid, user_uid=self.user_uid
)
if not channel_member:
return {"error": "Not a member of this channel", "success": False}
channel_member["deleted_at"] = now()
await self.services.channel_member.save(channel_member)
await self.services.socket.unsubscribe(self.ws, channel_uid, self.user_uid)
return {"success": True}
except Exception as ex:
logger.warning(f"Failed to leave channel: {safe_str(ex)}")
return {"error": "Failed to leave channel", "success": False}
async def updateChannel(self, channel_uid, name, description=None, is_private=False):
self._require_login()
self._require_services()
if not channel_uid or not isinstance(channel_uid, str):
return {"error": "Invalid channel", "success": False}
if not name or not isinstance(name, str):
return {"error": "Invalid channel name", "success": False}
name = name.strip()
if not name:
return {"error": "Channel name cannot be empty", "success": False}
try:
channel_member = await self.services.channel_member.get(
channel_uid=channel_uid, user_uid=self.user_uid
)
if not channel_member or not channel_member["is_moderator"]:
return {"error": "Only moderators can update channel settings", "success": False}
channel = await self.services.channel.get(uid=channel_uid)
if not channel:
return {"error": "Channel not found", "success": False}
if channel["tag"] == "public":
return {"error": "Cannot modify the public channel", "success": False}
if not name.startswith("#") and channel["is_listed"]:
name = f"#{name}"
channel["label"] = name
channel["description"] = description
channel["is_private"] = is_private
await self.services.channel.save(channel)
async for member in self.services.channel_member.find(
channel_uid=channel_uid, deleted_at=None
):
member["label"] = name
await self.services.channel_member.save(member)
return {"success": True, "name": channel["label"]}
except Exception as ex:
logger.warning(f"Failed to update channel: {safe_str(ex)}")
return {"error": "Failed to update channel", "success": False}
async def deleteChannel(self, channel_uid):
self._require_login()
self._require_services()
if not channel_uid or not isinstance(channel_uid, str):
return {"error": "Invalid channel", "success": False}
try:
channel_member = await self.services.channel_member.get(
channel_uid=channel_uid, user_uid=self.user_uid
)
if not channel_member or not channel_member["is_moderator"]:
return {"error": "Only moderators can delete channels", "success": False}
channel = await self.services.channel.get(uid=channel_uid)
if not channel:
return {"error": "Channel not found", "success": False}
if channel["tag"] == "public":
return {"error": "Cannot delete the public channel", "success": False}
channel["deleted_at"] = now()
await self.services.channel.save(channel)
async for member in self.services.channel_member.find(
channel_uid=channel_uid, deleted_at=None
):
member["deleted_at"] = now()
await self.services.channel_member.save(member)
return {"success": True}
except Exception as ex:
logger.warning(f"Failed to delete channel: {safe_str(ex)}")
return {"error": "Failed to delete channel", "success": False}
async def get(self): async def get(self):
ws = None ws = None
rpc = None rpc = None