old backend core lib
This commit is contained in:
@@ -11,7 +11,7 @@ use std::{sync::Arc, time::Duration};
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::{
|
||||
DefaultBodyLimit, Path, Request, State,
|
||||
DefaultBodyLimit, Path, Query, Request, State,
|
||||
ws::{CloseFrame, Message, WebSocket, WebSocketUpgrade, close_code},
|
||||
},
|
||||
http::{HeaderMap, HeaderValue, StatusCode, header},
|
||||
@@ -37,6 +37,7 @@ use crate::{
|
||||
GiftDetails, GiftEvent, LiveEvent, LiveEventPayload, PlatformViewer,
|
||||
},
|
||||
repository::{ComponentView, RepositoryError},
|
||||
song_request::{SONG_REQUEST_KIND, SongListScope, SongRequestError},
|
||||
};
|
||||
|
||||
const SECURE_SESSION_COOKIE: &str = "__Host-lxc_session";
|
||||
@@ -80,6 +81,22 @@ pub fn router(state: AppState) -> Router {
|
||||
"/api/v1/components/{public_id}/stream",
|
||||
get(component_stream),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/components/{id}/song-requests",
|
||||
get(list_song_requests),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/components/{id}/song-requests/{request_id}/promote",
|
||||
post(promote_song_request),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/components/{id}/song-requests/{request_id}/complete",
|
||||
post(complete_song_request),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/components/{id}/song-requests/{request_id}/cancel",
|
||||
post(cancel_song_request),
|
||||
)
|
||||
.route("/api/{*path}", any(api_not_found))
|
||||
.route("/", get(spa_page))
|
||||
.route("/login", get(spa_page))
|
||||
@@ -645,12 +662,127 @@ async fn put_component_settings(
|
||||
let message = settings_message(
|
||||
&component,
|
||||
&session.user.room_id,
|
||||
"overlay.settings.updated",
|
||||
"component.settings.updated",
|
||||
);
|
||||
state.hub.publish(component.id, Arc::new(message));
|
||||
if component.kind == "danmaku_overlay" {
|
||||
state.hub.publish(
|
||||
component.id,
|
||||
Arc::new(settings_message(
|
||||
&component,
|
||||
&session.user.room_id,
|
||||
"overlay.settings.updated",
|
||||
)),
|
||||
);
|
||||
}
|
||||
Ok(Json(json!({"settings":component.settings})))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SongRequestsQuery {
|
||||
scope: Option<String>,
|
||||
cursor: Option<i64>,
|
||||
limit: Option<i64>,
|
||||
}
|
||||
|
||||
async fn list_song_requests(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path(id): Path<Uuid>,
|
||||
Query(query): Query<SongRequestsQuery>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
let session = require_session(&state, &headers).await?;
|
||||
let component = state.repository.get_component(session.user.id, id).await?;
|
||||
require_song_component(&component)?;
|
||||
let scope = match query.scope.as_deref().unwrap_or("active") {
|
||||
"active" => SongListScope::Active,
|
||||
"history" => SongListScope::History,
|
||||
_ => {
|
||||
return Err(ApiError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"invalid_scope",
|
||||
"scope must be active or history",
|
||||
));
|
||||
}
|
||||
};
|
||||
let page = state
|
||||
.song_requests
|
||||
.list_page(
|
||||
&component,
|
||||
scope,
|
||||
query.cursor.unwrap_or(0),
|
||||
query.limit.unwrap_or(50),
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(serde_json::to_value(page).map_err(internal)?))
|
||||
}
|
||||
|
||||
async fn promote_song_request(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path((id, request_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
same_origin(&state, &headers)?;
|
||||
let session = require_session(&state, &headers).await?;
|
||||
let component = state.repository.get_component(session.user.id, id).await?;
|
||||
require_song_component(&component)?;
|
||||
state
|
||||
.song_requests
|
||||
.promote(&component, request_id, &session.user.room_id)
|
||||
.await?;
|
||||
Ok(Json(json!({"ok":true})))
|
||||
}
|
||||
|
||||
async fn complete_song_request(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path((id, request_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
change_song_request(state, headers, id, request_id, false).await
|
||||
}
|
||||
|
||||
async fn cancel_song_request(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
Path((id, request_id)): Path<(Uuid, Uuid)>,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
change_song_request(state, headers, id, request_id, true).await
|
||||
}
|
||||
|
||||
async fn change_song_request(
|
||||
state: AppState,
|
||||
headers: HeaderMap,
|
||||
component_id: Uuid,
|
||||
request_id: Uuid,
|
||||
cancelled: bool,
|
||||
) -> Result<Json<Value>, ApiError> {
|
||||
same_origin(&state, &headers)?;
|
||||
let session = require_session(&state, &headers).await?;
|
||||
let component = state
|
||||
.repository
|
||||
.get_component(session.user.id, component_id)
|
||||
.await?;
|
||||
require_song_component(&component)?;
|
||||
state
|
||||
.song_requests
|
||||
.finish(&component, request_id, cancelled, &session.user.room_id)
|
||||
.await?;
|
||||
Ok(Json(json!({"ok":true})))
|
||||
}
|
||||
|
||||
fn require_song_component(component: &ComponentInstance) -> Result<(), ApiError> {
|
||||
if component.kind == SONG_REQUEST_KIND {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ApiError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
"song_component_not_found",
|
||||
"Song request component not found",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_component_token(
|
||||
State(state): State<AppState>,
|
||||
headers: HeaderMap,
|
||||
@@ -877,7 +1009,7 @@ async fn component_ws(mut socket: WebSocket, state: AppState, component_id: Uuid
|
||||
let mut receiver = state.hub.subscribe(component_id);
|
||||
if socket
|
||||
.send(Message::Text(
|
||||
json!({"version":COMPONENT_PROTOCOL_VERSION,"type":"authenticated","componentId":component_id})
|
||||
json!({"version":COMPONENT_PROTOCOL_VERSION,"type":"authenticated","componentId":component_id,"componentKind":component.kind})
|
||||
.to_string()
|
||||
.into(),
|
||||
))
|
||||
@@ -893,19 +1025,54 @@ async fn component_ws(mut socket: WebSocket, state: AppState, component_id: Uuid
|
||||
return;
|
||||
}
|
||||
};
|
||||
let snapshot = settings_message(&component, &room_id, "overlay.settings.snapshot");
|
||||
let snapshot = settings_message(&component, &room_id, "component.settings.snapshot");
|
||||
if send_component_message(&mut socket, &snapshot)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if component.kind == "danmaku_overlay"
|
||||
&& send_component_message(
|
||||
&mut socket,
|
||||
&settings_message(&component, &room_id, "overlay.settings.snapshot"),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
let runtime = match state.registry.runtime(&component.kind) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(_) => {
|
||||
close_ws(&mut socket, "Component is unavailable").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
let component_snapshots = match runtime.snapshot(&component, &room_id).await {
|
||||
Ok(messages) => messages,
|
||||
Err(error) => {
|
||||
warn!(component_id = %component.id, %error, "component snapshot failed");
|
||||
close_ws(&mut socket, "Component snapshot is unavailable").await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
for message in component_snapshots {
|
||||
if send_component_message(&mut socket, &message).await.is_err() {
|
||||
return;
|
||||
}
|
||||
}
|
||||
loop {
|
||||
tokio::select! {
|
||||
event = receiver.recv() => match event {
|
||||
Ok(message) => {
|
||||
if send_component_message(&mut socket, &message).await.is_err() { break; }
|
||||
}
|
||||
// A durable component cannot safely guess dropped deltas. A
|
||||
// disconnect makes the browser obtain a fresh paged snapshot;
|
||||
// passive visual components can keep their legacy best-effort
|
||||
// behavior.
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) if component.kind == SONG_REQUEST_KIND => break,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue,
|
||||
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
|
||||
},
|
||||
@@ -945,17 +1112,12 @@ fn settings_message(
|
||||
room_id: &str,
|
||||
event_type: &str,
|
||||
) -> ComponentMessage {
|
||||
ComponentMessage {
|
||||
owner_id: component.owner_id,
|
||||
component_id: component.id,
|
||||
source_id: component.source_id,
|
||||
version: COMPONENT_PROTOCOL_VERSION,
|
||||
id: Uuid::new_v4(),
|
||||
occurred_at: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
|
||||
room_id: room_id.to_owned(),
|
||||
event_type: event_type.into(),
|
||||
payload: json!({"settings":component.settings}),
|
||||
}
|
||||
ComponentMessage::new(
|
||||
component,
|
||||
room_id,
|
||||
event_type,
|
||||
json!({"settings":component.settings}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn require_session(
|
||||
@@ -1256,6 +1418,25 @@ impl From<RepositoryError> for ApiError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SongRequestError> for ApiError {
|
||||
fn from(error: SongRequestError) -> Self {
|
||||
match error {
|
||||
SongRequestError::NotFound => Self::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
"song_request_not_found",
|
||||
"Song request was not found",
|
||||
),
|
||||
SongRequestError::Conflict(message) => {
|
||||
Self::new(StatusCode::CONFLICT, "song_request_conflict", message)
|
||||
}
|
||||
SongRequestError::Invalid(message) => {
|
||||
Self::new(StatusCode::BAD_REQUEST, "invalid_song_request", message)
|
||||
}
|
||||
other => internal(other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn internal(error: impl std::fmt::Display) -> ApiError {
|
||||
error!(%error, "internal operation failed");
|
||||
ApiError::new(
|
||||
|
||||
Reference in New Issue
Block a user