//! Mixer-kanaler — HTTP API for delt lydmixer-tilstand. //! //! Skriver direkte til PG; NOTIFY-trigger propagerer endringer til //! WebSocket-klienter. use axum::{extract::State, http::StatusCode, Json}; use serde::{Deserialize, Serialize}; use crate::auth::AuthUser; use crate::AppState; #[derive(Serialize)] pub struct ErrorResponse { pub error: String, } fn bad_request(msg: &str) -> (StatusCode, Json) { (StatusCode::BAD_REQUEST, Json(ErrorResponse { error: msg.to_string() })) } fn internal_error(msg: &str) -> (StatusCode, Json) { (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: msg.to_string() })) } // ============================================================================= // Create mixer channel // ============================================================================= #[derive(Deserialize)] pub struct CreateMixerChannelRequest { pub room_id: String, pub target_user_id: String, } #[derive(Serialize)] pub struct MixerChannelResponse { pub ok: bool, } pub async fn create_mixer_channel( State(state): State, _user: AuthUser, Json(req): Json, ) -> Result, (StatusCode, Json)> { if req.room_id.is_empty() || req.target_user_id.is_empty() { return Err(bad_request("room_id og target_user_id er påkrevd")); } sqlx::query( r#" INSERT INTO mixer_channels (room_id, target_user_id, updated_by, updated_at) VALUES ($1, $2, $3, now()) ON CONFLICT (room_id, target_user_id) DO NOTHING "#, ) .bind(&req.room_id) .bind(&req.target_user_id) .bind(&req.target_user_id) // updated_by = the user joining .execute(&state.db) .await .map_err(|e| { tracing::error!("Feil ved opprettelse av mixer-kanal: {e}"); internal_error("Databasefeil") })?; Ok(Json(MixerChannelResponse { ok: true })) } // ============================================================================= // Set gain // ============================================================================= #[derive(Deserialize)] pub struct SetGainRequest { pub room_id: String, pub target_user_id: String, pub gain: f64, } pub async fn set_gain( State(state): State, user: AuthUser, Json(req): Json, ) -> Result, (StatusCode, Json)> { sqlx::query( "UPDATE mixer_channels SET gain = $1, updated_by = $4, updated_at = now() WHERE room_id = $2 AND target_user_id = $3" ) .bind(req.gain.clamp(0.0, 1.5)) .bind(&req.room_id) .bind(&req.target_user_id) .bind(user.node_id.to_string()) .execute(&state.db) .await .map_err(|e| { tracing::error!("Feil ved set_gain: {e}"); internal_error("Databasefeil") })?; Ok(Json(MixerChannelResponse { ok: true })) } // ============================================================================= // Set mute // ============================================================================= #[derive(Deserialize)] pub struct SetMuteRequest { pub room_id: String, pub target_user_id: String, pub is_muted: bool, } pub async fn set_mute( State(state): State, user: AuthUser, Json(req): Json, ) -> Result, (StatusCode, Json)> { sqlx::query( "UPDATE mixer_channels SET is_muted = $1, updated_by = $4, updated_at = now() WHERE room_id = $2 AND target_user_id = $3" ) .bind(req.is_muted) .bind(&req.room_id) .bind(&req.target_user_id) .bind(user.node_id.to_string()) .execute(&state.db) .await .map_err(|e| { tracing::error!("Feil ved set_mute: {e}"); internal_error("Databasefeil") })?; Ok(Json(MixerChannelResponse { ok: true })) } // ============================================================================= // Toggle effect // ============================================================================= #[derive(Deserialize)] pub struct ToggleEffectRequest { pub room_id: String, pub target_user_id: String, pub effect_name: String, } pub async fn toggle_effect( State(state): State, user: AuthUser, Json(req): Json, ) -> Result, (StatusCode, Json)> { // Atomisk toggle via PG JSON-operasjoner — unngår race condition ved // samtidige oppdateringer fra flere klienter. sqlx::query( r#"UPDATE mixer_channels SET active_effects = ( CASE WHEN (active_effects::jsonb->>$3)::boolean IS NOT DISTINCT FROM true THEN (active_effects::jsonb || jsonb_build_object($3, false))::text ELSE (active_effects::jsonb || jsonb_build_object($3, true))::text END ), updated_by = $4, updated_at = now() WHERE room_id = $1 AND target_user_id = $2"#, ) .bind(&req.room_id) .bind(&req.target_user_id) .bind(&req.effect_name) .bind(user.node_id.to_string()) .execute(&state.db) .await .map_err(|e| { tracing::error!("Feil ved toggle_effect: {e}"); internal_error("Databasefeil") })?; Ok(Json(MixerChannelResponse { ok: true })) } // ============================================================================= // Set role // ============================================================================= #[derive(Deserialize)] pub struct SetMixerRoleRequest { pub room_id: String, pub target_user_id: String, pub role: String, } pub async fn set_mixer_role( State(state): State, user: AuthUser, Json(req): Json, ) -> Result, (StatusCode, Json)> { let valid_roles = ["editor", "viewer"]; if !valid_roles.contains(&req.role.as_str()) { return Err(bad_request("Ugyldig rolle. Gyldige: editor, viewer")); } sqlx::query( "UPDATE mixer_channels SET role = $1, updated_by = $4, updated_at = now() WHERE room_id = $2 AND target_user_id = $3" ) .bind(&req.role) .bind(&req.room_id) .bind(&req.target_user_id) .bind(user.node_id.to_string()) .execute(&state.db) .await .map_err(|e| { tracing::error!("Feil ved set_mixer_role: {e}"); internal_error("Databasefeil") })?; Ok(Json(MixerChannelResponse { ok: true })) }