Skip to content
Clone

feat(chat): add live handler module (meet routes and openapi)

zhenyi committed
parent e643ef3 commit 5979b6b Browse files

4 files changed

+162 −0

File tree 4
Added chat/live/handler/meet.rs View chat/live/handler/meet.rs
@@ -0,0 +1,85 @@
1use axum::extract::{Path, Query, State};
2use export::data::{AppRequest, AppRequestData, AppResponseData};
3use jwt::extractor::AuthUser;
4use serde::{Deserialize, Serialize};
5use utoipa::ToSchema;
6use uuid::Uuid;
7
8use crate::error::LiveResult;
9use crate::service::LiveService;
10use crate::types::{JoinInfo, MeetPresence, ParticipantRecord};
11
12/// Request body for `POST /live/meet/join`.
13#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
14pub(crate) struct JoinRoomCmd {
15 pub channel_id: Uuid,
16}
17
18/// Request body for `POST /live/meet/leave`.
19#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
20pub(crate) struct LeaveRoomCmd {
21 pub meet_id: Uuid,
22}
23
24/// Request body for `POST /live/meet/end`.
25#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
26pub(crate) struct EndRoomCmd {
27 pub channel_id: Uuid,
28}
29
30/// Query for `GET /live/meet/history`.
31#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
32pub(crate) struct HistoryQuery {
33 pub limit: u32,
34}
35
36#[utoipa::path(post, path = "/live/meet/join", tag = "live", request_body = AppRequestData<JoinRoomCmd>, responses((status = 200, description = "Join (or start) the meeting", body = AppResponseData<JoinInfo>)), security(("bearer_auth" = [])))]
37pub(crate) async fn join(
38 State(svc): State<LiveService>,
39 AuthUser { claims }: AuthUser,
40 cmd: AppRequest<JoinRoomCmd>,
41) -> LiveResult<AppResponseData<JoinInfo>> {
42 let info = svc.join_room(claims.sub, cmd.channel_id).await?;
43 crate::metrics::token_minted();
44 Ok(AppResponseData::ok(info))
45}
46
47#[utoipa::path(post, path = "/live/meet/leave", tag = "live", request_body = AppRequestData<LeaveRoomCmd>, responses((status = 200, description = "Leave the meeting")))]
48pub(crate) async fn leave(
49 State(svc): State<LiveService>,
50 AuthUser { claims }: AuthUser,
51 cmd: AppRequest<LeaveRoomCmd>,
52) -> LiveResult<AppResponseData<()>> {
53 svc.leave_room(claims.sub, cmd.meet_id).await?;
54 Ok(AppResponseData::ok(()))
55}
56
57#[utoipa::path(post, path = "/live/meet/end", tag = "live", request_body = AppRequestData<EndRoomCmd>, responses((status = 200, description = "End the meeting for a channel")))]
58pub(crate) async fn end(
59 State(svc): State<LiveService>,
60 AuthUser { claims }: AuthUser,
61 cmd: AppRequest<EndRoomCmd>,
62) -> LiveResult<AppResponseData<()>> {
63 svc.end_room(claims.sub, cmd.channel_id).await?;
64 Ok(AppResponseData::ok(()))
65}
66
67#[utoipa::path(get, path = "/live/meet/{channel_id}/presence", tag = "live", responses((status = 200, description = "Current presence", body = AppResponseData<MeetPresence>)))]
68pub(crate) async fn presence(
69 State(svc): State<LiveService>,
70 AuthUser { claims }: AuthUser,
71 Path(channel_id): Path<Uuid>,
72) -> LiveResult<AppResponseData<MeetPresence>> {
73 let presence = svc.presence(claims.sub, channel_id).await?;
74 Ok(AppResponseData::ok(presence))
75}
76
77#[utoipa::path(get, path = "/live/meet/history", tag = "live", responses((status = 200, description = "Participation history", body = AppResponseData<Vec<ParticipantRecord>>)))]
78pub(crate) async fn history(
79 State(svc): State<LiveService>,
80 AuthUser { claims }: AuthUser,
81 Query(q): Query<HistoryQuery>,
82) -> LiveResult<AppResponseData<Vec<ParticipantRecord>>> {
83 let records = svc.list_history(claims.sub, q.limit).await?;
84 Ok(AppResponseData::ok(records))
85}
No newline at end of file
Added chat/live/handler/mod.rs View chat/live/handler/mod.rs
@@ -0,0 +1,6 @@
1pub(crate) mod meet;
2pub(crate) mod openapi;
3pub(crate) mod routes;
4
5pub use openapi::LiveApiDoc;
6pub use routes::live_router;
No newline at end of file
Added chat/live/handler/openapi.rs View chat/live/handler/openapi.rs
@@ -0,0 +1,35 @@
1use utoipa::OpenApi;
2
3use crate::types::{JoinInfo, MeetPresence, ParticipantRecord};
4
5#[allow(missing_debug_implementations)]
6#[derive(OpenApi)]
7#[openapi(
8 info(
9 title = "Live API",
10 version = "1.0.0",
11 description = "LiveKit meeting rooms: join/leave/end, presence and participation history."
12 ),
13 tags(
14 (name = "live", description = "Meeting room endpoints.")
15 ),
16 paths(
17 crate::handler::meet::join,
18 crate::handler::meet::leave,
19 crate::handler::meet::end,
20 crate::handler::meet::presence,
21 crate::handler::meet::history,
22 ),
23 components(
24 schemas(
25 JoinInfo,
26 MeetPresence,
27 ParticipantRecord,
28 crate::handler::meet::JoinRoomCmd,
29 crate::handler::meet::LeaveRoomCmd,
30 crate::handler::meet::EndRoomCmd,
31 crate::handler::meet::HistoryQuery,
32 )
33 )
34)]
35pub struct LiveApiDoc;
No newline at end of file
Added chat/live/handler/routes.rs View chat/live/handler/routes.rs
@@ -0,0 +1,36 @@
1use axum::Router;
2use axum::middleware::from_fn_with_state;
3use axum::routing::{get, post};
4
5use crate::service::LiveService;
6use jwt::inject_jwt_ctx;
7use rate_limit::ratelimit;
8
9/// Builds the standalone live-service router.
10///
11/// - `POST /live/webhook` is public: LiveKit signs it with the webhook secret
12/// and no bearer token is involved.
13/// - All `/live/meet/*` routes require a valid access token (JWT middleware).
14pub fn live_router(svc: LiveService) -> Router {
15 let public = Router::new().route(
16 "/live/webhook",
17 post(crate::webhook::handle_webhook),
18 );
19
20 let protected = Router::new()
21 .route("/live/meet/join", post(super::meet::join))
22 .route("/live/meet/leave", post(super::meet::leave))
23 .route("/live/meet/end", post(super::meet::end))
24 .route(
25 "/live/meet/{channel_id}/presence",
26 get(super::meet::presence),
27 )
28 .route("/live/meet/history", get(super::meet::history))
29 .layer(from_fn_with_state(svc.clone(), inject_jwt_ctx::<LiveService>));
30
31 Router::new()
32 .merge(public)
33 .merge(protected)
34 .layer(from_fn_with_state(svc.clone(), ratelimit::<LiveService>))
35 .with_state(svc)
36}
No newline at end of file