From 477281a78f41834bb048ddaa3754b6a88cebceb8 Mon Sep 17 00:00:00 2001 From: pavel Date: Tue, 24 Feb 2026 02:06:16 +0100 Subject: [PATCH 1/3] fix soundboard --- src/voice.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/voice.rs b/src/voice.rs index b15b81c..a0a34f0 100644 --- a/src/voice.rs +++ b/src/voice.rs @@ -259,7 +259,10 @@ impl VoiceHub { return; }; - for peer in room.values() { + for (peer_id, peer) in room.iter() { + if *peer_id == user_id { + continue; + } let _ = peer.tx.send(ServerEvent::PlaySound { user_id, sound_url: sound_url.clone(), From 0c0aaa704852f65d61df1c97680bbacaca2a8d55 Mon Sep 17 00:00:00 2001 From: pavel Date: Tue, 24 Feb 2026 02:07:18 +0100 Subject: [PATCH 2/3] fix lost files --- .forgejo/workflows/pipeline.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.forgejo/workflows/pipeline.yaml b/.forgejo/workflows/pipeline.yaml index 4adede5..87b1268 100644 --- a/.forgejo/workflows/pipeline.yaml +++ b/.forgejo/workflows/pipeline.yaml @@ -37,7 +37,7 @@ jobs: mkdir -p ~/discord cp target/release/chattz ~/discord/discord chmod +x ~/discord/discord - mkdir -p ~/discord/static - rm -rf ~/discord/static/* + mkdir -p ~/discord/static/uploads + find ~/discord/static -mindepth 1 -maxdepth 1 ! -name 'uploads' -exec rm -rf {} + cp -r static/* ~/discord/static/ systemctl --user start discord \ No newline at end of file From e4cc833ccc9b14d7d90b462a3399ebe1f3b33ac5 Mon Sep 17 00:00:00 2001 From: pavel Date: Tue, 24 Feb 2026 02:12:26 +0100 Subject: [PATCH 3/3] del --- src/db.rs | 27 +++++++++++++++++++++++ src/handlers.rs | 56 +++++++++++++++++++++++++++++++++++++++++++++++ static/app.js | 35 ++++++++++++++++++++++++++++- static/styles.css | 26 ++++++++++++++++++++++ 4 files changed, 143 insertions(+), 1 deletion(-) diff --git a/src/db.rs b/src/db.rs index 2e26c94..d4364bc 100644 --- a/src/db.rs +++ b/src/db.rs @@ -139,6 +139,23 @@ pub async fn create_guild( Ok(map_guild(guild)) } +pub async fn get_guild_by_id(db: &DatabaseConnection, guild_id: Uuid) -> Result> { + let row = guilds::Entity::find_by_id(guild_id).one(db).await?; + Ok(row.map(map_guild)) +} + +pub async fn is_guild_owner( + db: &DatabaseConnection, + guild_id: Uuid, + user_id: Uuid, +) -> Result { + let guild = guilds::Entity::find_by_id(guild_id).one(db).await?; + match guild { + Some(g) => Ok(g.owner_user_id == user_id), + None => Ok(false), + } +} + pub async fn create_invite( db: &DatabaseConnection, guild_id: Uuid, @@ -612,6 +629,16 @@ pub async fn create_sound( Ok(map_sound(model)) } +pub async fn get_sound_by_id(db: &DatabaseConnection, id: Uuid) -> Result> { + let row = soundboard_sounds::Entity::find_by_id(id).one(db).await?; + Ok(row.map(map_sound)) +} + +pub async fn delete_sound(db: &DatabaseConnection, id: Uuid) -> Result<()> { + soundboard_sounds::Entity::delete_by_id(id).exec(db).await?; + Ok(()) +} + fn map_sound(model: soundboard_sounds::Model) -> SoundboardSound { SoundboardSound { id: model.id, diff --git a/src/handlers.rs b/src/handlers.rs index 788688d..fd7a593 100644 --- a/src/handlers.rs +++ b/src/handlers.rs @@ -15,6 +15,7 @@ use crate::{ models::{Guild, SoundboardSound}, voice, }; +use tracing::info; pub fn routes() -> Router { Router::new() @@ -43,6 +44,10 @@ pub fn routes() -> Router { "/guilds/{guild_id}/sounds", get(list_sounds).post(upload_sound), ) + .route( + "/guilds/{guild_id}/sounds/{sound_id}", + post(delete_sound_post).delete(delete_sound), + ) .route("/channels", post(create_channel)) .route( "/channels/{channel_id}/messages", @@ -759,6 +764,57 @@ async fn upload_sound( Ok(Json(sound)) } +async fn delete_sound_post( + state: State, + user: AuthUser, + path: Path<(Uuid, Uuid)>, +) -> Result { + delete_sound(state, user, path).await +} + +async fn delete_sound( + State(state): State, + user: AuthUser, + Path((guild_id, sound_id)): Path<(Uuid, Uuid)>, +) -> Result { + ensure_guild_member(&state, guild_id, user.id).await?; + + let sound = db::get_sound_by_id(&state.db, sound_id) + .await + .map_err(|e| ApiError::internal(&e.to_string()))? + .ok_or_else(|| ApiError { + status: StatusCode::NOT_FOUND, + message: "sound not found".to_string(), + })?; + + if sound.guild_id != guild_id { + return Err(ApiError::bad_request("sound does not belong to this guild")); + } + + let is_owner = db::is_guild_owner(&state.db, guild_id, user.id) + .await + .map_err(|e| ApiError::internal(&e.to_string()))?; + + if sound.created_by_user_id != user.id && !is_owner { + return Err(ApiError { + status: StatusCode::FORBIDDEN, + message: "you do not have permission to delete this sound".to_string(), + }); + } + + // Delete file from disk + let relative_path = sound.file_path.trim_start_matches('/'); + if let Err(e) = tokio::fs::remove_file(relative_path).await { + info!("failed to delete sound file {}: {}", relative_path, e); + } + + db::delete_sound(&state.db, sound_id) + .await + .map_err(|e| ApiError::internal(&format!("failed to delete sound record: {e}")))?; + + Ok(StatusCode::NO_CONTENT) +} + async fn ensure_guild_member( state: &AppState, guild_id: Uuid, diff --git a/static/app.js b/static/app.js index f9aad1a..acd46f9 100644 --- a/static/app.js +++ b/static/app.js @@ -1049,13 +1049,46 @@ function renderSounds(sounds) { sounds.forEach(sound => { const item = document.createElement('div'); item.className = 'sound-item'; + + // Only show delete button if user is creator or owner + const canDelete = state.user && (state.user.id === sound.created_by_user_id || (state.guilds.find(g => g.id === state.selectedGuildId)?.owner_user_id === state.user.id)); + item.innerHTML = `
${sound.icon}
${sound.name}
+ ${canDelete ? `` : ''} `; - item.onclick = () => playRemoteSound(sound.file_path); + + item.onclick = (e) => { + if (e.target.closest('.sound-delete')) { + e.stopPropagation(); + if (confirm(`Are you sure you want to delete "${sound.name}"?`)) { + deleteSound(sound.id); + } + return; + } + playRemoteSound(sound.file_path); + }; + el.soundboardGrid.appendChild(item); }); + lucide.createIcons(); +} + +async function deleteSound(soundId) { + if (!state.selectedGuildId) return; + try { + const response = await fetch(`/guilds/${state.selectedGuildId}/sounds/${soundId}`, { + method: 'DELETE' + }); + if (!response.ok) { + const err = await response.json(); + throw new Error(err.error || 'Delete failed'); + } + await loadSounds(); + } catch (err) { + alert(err.message); + } } function playRemoteSound(url) { diff --git a/static/styles.css b/static/styles.css index 7319228..a6e26d9 100644 --- a/static/styles.css +++ b/static/styles.css @@ -729,6 +729,32 @@ select { transform: scale(0.95); } +.sound-delete { + position: absolute; + top: 4px; + right: 4px; + width: 18px; + height: 18px; + border-radius: 50%; + background: rgba(0, 0, 0, 0.6); + color: var(--text-muted); + display: grid; + place-items: center; + opacity: 0; + transition: all var(--transition); + z-index: 2; +} + +.sound-delete:hover { + background: var(--danger); + color: #fff; + transform: scale(1.1); +} + +.sound-item:hover .sound-delete { + opacity: 1; +} + .sound-icon { font-size: 24px; }