This commit is contained in:
pavel 2026-02-13 19:52:42 +01:00
commit d1a68c635c
5 changed files with 384 additions and 94 deletions

View file

@ -11,7 +11,7 @@ use uuid::Uuid;
use crate::{
AppState,
auth::{self, ApiError, AuthUser},
db, voice,
chat, db, voice,
};
pub fn routes() -> Router<AppState> {
@ -22,17 +22,27 @@ pub fn routes() -> Router<AppState> {
.route("/auth/logout", post(auth_logout))
.route("/me", get(me))
.route("/dms", get(list_dm_conversations))
.route("/dms/{other_user_id}/messages", get(list_dm_messages).post(send_dm_message))
.route(
"/dms/{other_user_id}/messages",
get(list_dm_messages).post(send_dm_message),
)
.route("/rtc-config", get(rtc_config))
.route("/guilds", get(list_guilds).post(create_guild))
.route("/guilds/{guild_id}/members", get(list_guild_members))
.route("/guilds/{guild_id}/channels", get(list_channels))
.route("/guilds/{guild_id}/voice-presence", get(guild_voice_presence))
.route(
"/guilds/{guild_id}/voice-presence",
get(guild_voice_presence),
)
.route("/guilds/{guild_id}/invites", post(create_invite))
.route("/invites/{code}/join", post(join_invite))
.route("/channels", post(create_channel))
.route("/channels/{channel_id}/messages", get(list_messages).post(send_message))
.route(
"/channels/{channel_id}/messages",
get(list_messages).post(send_message),
)
.route("/channels/{channel_id}/voice/ws", get(voice_ws))
.route("/ws", get(chat_ws))
}
pub async fn health() -> &'static str {
@ -142,7 +152,10 @@ async fn auth_callback(
.map_err(|e| ApiError::bad_request(&format!("token exchange failed: {e}")))?;
if !token_res.status().is_success() {
let text = token_res.text().await.unwrap_or_else(|_| "<no body>".to_string());
let text = token_res
.text()
.await
.unwrap_or_else(|_| "<no body>".to_string());
return Err(ApiError::bad_request(&format!(
"token exchange returned non-success: {text}"
)));
@ -162,7 +175,10 @@ async fn auth_callback(
.map_err(|e| ApiError::bad_request(&format!("userinfo request failed: {e}")))?;
if !userinfo_res.status().is_success() {
let text = userinfo_res.text().await.unwrap_or_else(|_| "<no body>".to_string());
let text = userinfo_res
.text()
.await
.unwrap_or_else(|_| "<no body>".to_string());
return Err(ApiError::bad_request(&format!(
"userinfo returned non-success: {text}"
)));
@ -185,9 +201,12 @@ async fn auth_callback(
.await
.map_err(|e| ApiError::internal(&format!("failed to persist user: {e}")))?;
let session_cookie =
auth::new_session_cookie(user.id, &state.settings.session_secret, state.settings.cookie_secure)
.map_err(|e| ApiError::internal(&e.to_string()))?;
let session_cookie = auth::new_session_cookie(
user.id,
&state.settings.session_secret,
state.settings.cookie_secure,
)
.map_err(|e| ApiError::internal(&e.to_string()))?;
let mut headers = HeaderMap::new();
headers.append(
@ -232,7 +251,10 @@ struct RtcIceServer {
credential: Option<String>,
}
async fn rtc_config(State(state): State<AppState>, _user: AuthUser) -> Result<impl IntoResponse, ApiError> {
async fn rtc_config(
State(state): State<AppState>,
_user: AuthUser,
) -> Result<impl IntoResponse, ApiError> {
let mut ice_servers = Vec::new();
if !state.settings.stun_urls.is_empty() {
@ -441,9 +463,13 @@ async fn create_channel(
return Err(ApiError::bad_request("channel name must be 1..64 chars"));
}
let kind = body.kind.unwrap_or_else(|| db::CHANNEL_KIND_TEXT.to_string());
let kind = body
.kind
.unwrap_or_else(|| db::CHANNEL_KIND_TEXT.to_string());
if kind != db::CHANNEL_KIND_TEXT && kind != db::CHANNEL_KIND_VOICE {
return Err(ApiError::bad_request("channel kind must be 'text' or 'voice'"));
return Err(ApiError::bad_request(
"channel kind must be 'text' or 'voice'",
));
}
let channel = db::create_channel(&state.db, body.guild_id, trimmed, &kind)
@ -497,10 +523,31 @@ async fn send_message(
return Err(ApiError::bad_request("message body must be 1..4000 chars"));
}
db::create_message(&state.db, channel_id, user.id, content)
let message = db::create_message(&state.db, channel_id, user.id, content)
.await
.map_err(|e| ApiError::internal(&format!("failed to create message: {e}")))?;
// Broadcast to all guild members
let guild_id = db::guild_id_for_channel(&state.db, channel_id)
.await
.map_err(|e| ApiError::internal(&format!("channel lookup failed: {e}")))?
.ok_or_else(|| ApiError::internal("channel not found after creation"))?;
let members = db::list_guild_member_ids(&state.db, guild_id)
.await
.map_err(|e| ApiError::internal(&format!("failed to list guild members: {e}")))?;
state
.chat
.broadcast_to_many(
members,
chat::ServerEvent::MessageCreated {
channel_id,
message: serde_json::to_value(&message).unwrap_or_default(),
},
)
.await;
Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true })))
}
@ -557,10 +604,32 @@ async fn send_dm_message(
return Err(ApiError::bad_request("message body must be 1..4000 chars"));
}
db::create_direct_message(&state.db, user.id, other_user_id, content)
let message = db::create_direct_message(&state.db, user.id, other_user_id, content)
.await
.map_err(|e| ApiError::internal(&format!("failed to create dm message: {e}")))?;
// Broadcast to both users
state
.chat
.broadcast_to_user(
user.id,
chat::ServerEvent::DmCreated {
other_user_id,
message: serde_json::to_value(&message).unwrap_or_default(),
},
)
.await;
state
.chat
.broadcast_to_user(
other_user_id,
chat::ServerEvent::DmCreated {
other_user_id: user.id,
message: serde_json::to_value(&message).unwrap_or_default(),
},
)
.await;
Ok((StatusCode::CREATED, Json(SendMessageResponse { ok: true })))
}
@ -581,15 +650,27 @@ async fn voice_ws(
})?;
if channel.kind != db::CHANNEL_KIND_VOICE {
return Err(ApiError::bad_request("voice websocket requires a voice channel"));
return Err(ApiError::bad_request(
"voice websocket requires a voice channel",
));
}
Ok(ws.on_upgrade(move |socket| {
voice::handle_socket(state, socket, channel_id, user.id)
}))
Ok(ws.on_upgrade(move |socket| voice::handle_socket(state, socket, channel_id, user.id)))
}
async fn ensure_guild_member(state: &AppState, guild_id: Uuid, user_id: Uuid) -> Result<(), ApiError> {
async fn chat_ws(
ws: WebSocketUpgrade,
State(state): State<AppState>,
user: AuthUser,
) -> Result<impl IntoResponse, ApiError> {
Ok(ws.on_upgrade(move |socket| chat::handle_socket(state, socket, user.id)))
}
async fn ensure_guild_member(
state: &AppState,
guild_id: Uuid,
user_id: Uuid,
) -> Result<(), ApiError> {
let is_member = db::is_member_of_guild(&state.db, guild_id, user_id)
.await
.map_err(|e| ApiError::internal(&format!("membership check failed: {e}")))?;
@ -604,7 +685,11 @@ async fn ensure_guild_member(state: &AppState, guild_id: Uuid, user_id: Uuid) ->
Ok(())
}
async fn ensure_channel_member(state: &AppState, channel_id: Uuid, user_id: Uuid) -> Result<(), ApiError> {
async fn ensure_channel_member(
state: &AppState,
channel_id: Uuid,
user_id: Uuid,
) -> Result<(), ApiError> {
let guild_id = db::guild_id_for_channel(&state.db, channel_id)
.await
.map_err(|e| ApiError::internal(&format!("channel lookup failed: {e}")))?