parent
0c0aaa7048
commit
e4cc833ccc
4 changed files with 143 additions and 1 deletions
27
src/db.rs
27
src/db.rs
|
|
@ -139,6 +139,23 @@ pub async fn create_guild(
|
||||||
Ok(map_guild(guild))
|
Ok(map_guild(guild))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_guild_by_id(db: &DatabaseConnection, guild_id: Uuid) -> Result<Option<Guild>> {
|
||||||
|
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<bool> {
|
||||||
|
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(
|
pub async fn create_invite(
|
||||||
db: &DatabaseConnection,
|
db: &DatabaseConnection,
|
||||||
guild_id: Uuid,
|
guild_id: Uuid,
|
||||||
|
|
@ -612,6 +629,16 @@ pub async fn create_sound(
|
||||||
Ok(map_sound(model))
|
Ok(map_sound(model))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_sound_by_id(db: &DatabaseConnection, id: Uuid) -> Result<Option<SoundboardSound>> {
|
||||||
|
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 {
|
fn map_sound(model: soundboard_sounds::Model) -> SoundboardSound {
|
||||||
SoundboardSound {
|
SoundboardSound {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ use crate::{
|
||||||
models::{Guild, SoundboardSound},
|
models::{Guild, SoundboardSound},
|
||||||
voice,
|
voice,
|
||||||
};
|
};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
pub fn routes() -> Router<AppState> {
|
pub fn routes() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
|
@ -43,6 +44,10 @@ pub fn routes() -> Router<AppState> {
|
||||||
"/guilds/{guild_id}/sounds",
|
"/guilds/{guild_id}/sounds",
|
||||||
get(list_sounds).post(upload_sound),
|
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", post(create_channel))
|
||||||
.route(
|
.route(
|
||||||
"/channels/{channel_id}/messages",
|
"/channels/{channel_id}/messages",
|
||||||
|
|
@ -759,6 +764,57 @@ async fn upload_sound(
|
||||||
Ok(Json(sound))
|
Ok(Json(sound))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn delete_sound_post(
|
||||||
|
state: State<AppState>,
|
||||||
|
user: AuthUser,
|
||||||
|
path: Path<(Uuid, Uuid)>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
delete_sound(state, user, path).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_sound(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
user: AuthUser,
|
||||||
|
Path((guild_id, sound_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError> {
|
||||||
|
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(
|
async fn ensure_guild_member(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
guild_id: Uuid,
|
guild_id: Uuid,
|
||||||
|
|
|
||||||
|
|
@ -1049,13 +1049,46 @@ function renderSounds(sounds) {
|
||||||
sounds.forEach(sound => {
|
sounds.forEach(sound => {
|
||||||
const item = document.createElement('div');
|
const item = document.createElement('div');
|
||||||
item.className = 'sound-item';
|
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 = `
|
item.innerHTML = `
|
||||||
<div class="sound-icon">${sound.icon}</div>
|
<div class="sound-icon">${sound.icon}</div>
|
||||||
<div class="sound-name">${sound.name}</div>
|
<div class="sound-name">${sound.name}</div>
|
||||||
|
${canDelete ? `<button class="sound-delete" title="Delete sound"><i data-lucide="x"></i></button>` : ''}
|
||||||
`;
|
`;
|
||||||
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);
|
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) {
|
function playRemoteSound(url) {
|
||||||
|
|
|
||||||
|
|
@ -729,6 +729,32 @@ select {
|
||||||
transform: scale(0.95);
|
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 {
|
.sound-icon {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue