Add public work read model and smooth puzzle transitions
This commit is contained in:
@@ -61,6 +61,7 @@ pub fn build_router(state: AppState) -> Router {
|
||||
.merge(modules::square_hole::router(state.clone()))
|
||||
.merge(modules::jump_hop::router(state.clone()))
|
||||
.merge(modules::wooden_fish::router(state.clone()))
|
||||
.merge(modules::public_work::router(state.clone()))
|
||||
.merge(modules::puzzle::router(state.clone()))
|
||||
.merge(visual_novel_router(state.clone()))
|
||||
.route(
|
||||
|
||||
@@ -702,7 +702,7 @@ pub async fn list_custom_world_gallery(
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let entries = state
|
||||
.spacetime_client()
|
||||
.list_custom_world_gallery_entries()
|
||||
.list_public_work_gallery_entries()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
custom_world_error_response(&request_context, map_custom_world_client_error(error))
|
||||
@@ -713,7 +713,8 @@ pub async fn list_custom_world_gallery(
|
||||
CustomWorldGalleryResponse {
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|entry| map_custom_world_gallery_card_response(&state, entry))
|
||||
.filter(|entry| entry.source_type == "custom-world")
|
||||
.map(|entry| map_public_work_custom_world_gallery_card_response(&state, entry))
|
||||
.collect(),
|
||||
},
|
||||
))
|
||||
|
||||
@@ -149,6 +149,43 @@ pub(super) fn map_custom_world_gallery_card_response(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_public_work_custom_world_gallery_card_response(
|
||||
state: &AppState,
|
||||
entry: spacetime_client::PublicWorkGalleryEntryRecord,
|
||||
) -> CustomWorldGalleryCardResponse {
|
||||
let author = resolve_work_author_by_user_id(
|
||||
state,
|
||||
&entry.owner_user_id,
|
||||
Some(&entry.author_display_name),
|
||||
None,
|
||||
);
|
||||
CustomWorldGalleryCardResponse {
|
||||
owner_user_id: entry.owner_user_id,
|
||||
profile_id: entry.profile_id,
|
||||
public_work_code: entry.public_work_code,
|
||||
author_public_user_code: author.public_user_code.unwrap_or_default(),
|
||||
visibility: "published".to_string(),
|
||||
published_at: entry.published_at,
|
||||
updated_at: entry.updated_at,
|
||||
author_display_name: author.display_name,
|
||||
world_name: entry.world_name,
|
||||
subtitle: entry.subtitle,
|
||||
summary_text: entry.summary_text,
|
||||
cover_image_src: entry.cover_image_src,
|
||||
theme_mode: entry
|
||||
.theme_tags
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "mythic".to_string()),
|
||||
playable_npc_count: 0,
|
||||
landmark_count: 0,
|
||||
play_count: entry.play_count,
|
||||
remix_count: entry.remix_count,
|
||||
like_count: entry.like_count,
|
||||
recent_play_count_7d: entry.recent_play_count_7d,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_custom_world_work_summary_response(
|
||||
item: CustomWorldWorkSummaryRecord,
|
||||
) -> CustomWorldWorkSummaryResponse {
|
||||
|
||||
@@ -61,6 +61,7 @@ mod platform_errors;
|
||||
mod process_metrics;
|
||||
mod profile_identity;
|
||||
mod prompt;
|
||||
mod public_work;
|
||||
mod puzzle;
|
||||
mod puzzle_agent_turn;
|
||||
mod puzzle_gallery_cache;
|
||||
|
||||
@@ -11,6 +11,7 @@ pub mod jump_hop;
|
||||
pub mod match3d;
|
||||
pub mod platform;
|
||||
pub mod profile;
|
||||
pub mod public_work;
|
||||
pub mod puzzle;
|
||||
pub mod square_hole;
|
||||
pub mod story;
|
||||
|
||||
16
server-rs/crates/api-server/src/modules/public_work.rs
Normal file
16
server-rs/crates/api-server/src/modules/public_work.rs
Normal file
@@ -0,0 +1,16 @@
|
||||
use axum::{Router, routing::get};
|
||||
|
||||
use crate::{
|
||||
public_work::{get_public_work_detail, list_public_works},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
pub fn router(state: AppState) -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/public-works", get(list_public_works))
|
||||
.route(
|
||||
"/api/public-works/{public_work_code}",
|
||||
get(get_public_work_detail),
|
||||
)
|
||||
.with_state(state)
|
||||
}
|
||||
154
server-rs/crates/api-server/src/public_work.rs
Normal file
154
server-rs/crates/api-server/src/public_work.rs
Normal file
@@ -0,0 +1,154 @@
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Extension, Path, State},
|
||||
http::{HeaderName, StatusCode, header},
|
||||
response::Response,
|
||||
};
|
||||
use serde_json::{Value, json};
|
||||
use shared_contracts::public_work::{
|
||||
PublicWorkDetailEntryResponse, PublicWorkDetailResponse, PublicWorkGalleryEntryResponse,
|
||||
PublicWorkGalleryResponse,
|
||||
};
|
||||
use spacetime_client::{
|
||||
PublicWorkDetailEntryRecord, PublicWorkGalleryEntryRecord, SpacetimeClientError,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
||||
state::AppState, work_author::resolve_work_author_by_user_id,
|
||||
};
|
||||
|
||||
const PUBLIC_WORK_PROVIDER: &str = "public-work";
|
||||
|
||||
pub async fn list_public_works(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let items = state
|
||||
.spacetime_client()
|
||||
.list_public_work_gallery_entries()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
public_work_error_response(&request_context, map_public_work_client_error(error))
|
||||
})?
|
||||
.into_iter()
|
||||
.map(|entry| map_public_work_gallery_entry_response(&state, entry))
|
||||
.collect::<Vec<_>>();
|
||||
let total_count = items.len().min(u32::MAX as usize) as u32;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
PublicWorkGalleryResponse {
|
||||
items,
|
||||
has_more: false,
|
||||
next_cursor: None,
|
||||
total_count,
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_public_work_detail(
|
||||
State(state): State<AppState>,
|
||||
Path(public_work_code): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
if public_work_code.trim().is_empty() {
|
||||
return Err(public_work_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": PUBLIC_WORK_PROVIDER,
|
||||
"message": "publicWorkCode is required",
|
||||
})),
|
||||
));
|
||||
}
|
||||
|
||||
let item = state
|
||||
.spacetime_client()
|
||||
.get_public_work_detail_by_code(public_work_code)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
public_work_error_response(&request_context, map_public_work_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
PublicWorkDetailResponse {
|
||||
item: map_public_work_detail_entry_response(&state, item),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn map_public_work_gallery_entry_response(
|
||||
state: &AppState,
|
||||
entry: PublicWorkGalleryEntryRecord,
|
||||
) -> PublicWorkGalleryEntryResponse {
|
||||
let author = resolve_work_author_by_user_id(
|
||||
state,
|
||||
&entry.owner_user_id,
|
||||
Some(&entry.author_display_name),
|
||||
None,
|
||||
);
|
||||
|
||||
PublicWorkGalleryEntryResponse {
|
||||
source_type: entry.source_type,
|
||||
work_id: entry.work_id,
|
||||
profile_id: entry.profile_id,
|
||||
source_session_id: entry.source_session_id,
|
||||
public_work_code: entry.public_work_code,
|
||||
owner_user_id: entry.owner_user_id,
|
||||
author_display_name: author.display_name,
|
||||
world_name: entry.world_name,
|
||||
subtitle: entry.subtitle,
|
||||
summary_text: entry.summary_text,
|
||||
cover_image_src: entry.cover_image_src,
|
||||
cover_asset_id: entry.cover_asset_id,
|
||||
theme_tags: entry.theme_tags,
|
||||
play_count: entry.play_count,
|
||||
remix_count: entry.remix_count,
|
||||
like_count: entry.like_count,
|
||||
recent_play_count_7d: entry.recent_play_count_7d,
|
||||
published_at: entry.published_at,
|
||||
updated_at: entry.updated_at,
|
||||
sort_time_micros: entry.sort_time_micros,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_public_work_detail_entry_response(
|
||||
state: &AppState,
|
||||
entry: PublicWorkDetailEntryRecord,
|
||||
) -> PublicWorkDetailEntryResponse {
|
||||
PublicWorkDetailEntryResponse {
|
||||
entry: map_public_work_gallery_entry_response(state, entry.entry),
|
||||
detail_payload_json: entry.detail_payload_json,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_public_work_client_error(error: SpacetimeClientError) -> AppError {
|
||||
let status = match &error {
|
||||
SpacetimeClientError::Runtime(_) => StatusCode::BAD_REQUEST,
|
||||
SpacetimeClientError::Procedure(message)
|
||||
if message.contains("不存在")
|
||||
|| message.contains("not found")
|
||||
|| message.contains("does not exist") =>
|
||||
{
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
SpacetimeClientError::Timeout => StatusCode::GATEWAY_TIMEOUT,
|
||||
SpacetimeClientError::ConnectDropped => StatusCode::SERVICE_UNAVAILABLE,
|
||||
_ => StatusCode::BAD_GATEWAY,
|
||||
};
|
||||
|
||||
AppError::from_status(status).with_details(json!({
|
||||
"provider": "spacetimedb",
|
||||
"message": error.to_string(),
|
||||
}))
|
||||
}
|
||||
|
||||
fn public_work_error_response(request_context: &RequestContext, error: AppError) -> Response {
|
||||
let mut response = error.into_response_with_context(Some(request_context));
|
||||
response.headers_mut().insert(
|
||||
HeaderName::from_static("x-genarrative-provider"),
|
||||
header::HeaderValue::from_static(PUBLIC_WORK_PROVIDER),
|
||||
);
|
||||
response
|
||||
}
|
||||
@@ -1510,7 +1510,7 @@ pub async fn list_puzzle_gallery(
|
||||
let rebuild_started_at = std::time::Instant::now();
|
||||
let items = state
|
||||
.spacetime_client()
|
||||
.list_puzzle_gallery()
|
||||
.list_public_work_gallery_entries()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
puzzle_error_response(
|
||||
@@ -1523,7 +1523,8 @@ pub async fn list_puzzle_gallery(
|
||||
let response = build_puzzle_gallery_window_response(
|
||||
items
|
||||
.into_iter()
|
||||
.map(|item| map_puzzle_gallery_card_response(&state, item))
|
||||
.filter(|item| item.source_type == "puzzle")
|
||||
.map(|item| map_public_work_puzzle_gallery_card_response(&state, item))
|
||||
.collect(),
|
||||
);
|
||||
let cached_response = state
|
||||
@@ -1881,6 +1882,7 @@ pub async fn advance_puzzle_next_level(
|
||||
Err(error) if error.status() == StatusCode::UNSUPPORTED_MEDIA_TYPE => {
|
||||
AdvancePuzzleNextLevelRequest {
|
||||
target_profile_id: None,
|
||||
prefer_similar_work: false,
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
@@ -1901,6 +1903,7 @@ pub async fn advance_puzzle_next_level(
|
||||
run_id,
|
||||
owner_user_id: principal.subject().to_string(),
|
||||
target_profile_id: payload.target_profile_id,
|
||||
prefer_similar_work: payload.prefer_similar_work,
|
||||
advanced_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
|
||||
@@ -439,6 +439,46 @@ pub(super) fn map_puzzle_gallery_card_response(
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_public_work_puzzle_gallery_card_response(
|
||||
state: &PuzzleApiState,
|
||||
item: spacetime_client::PublicWorkGalleryEntryRecord,
|
||||
) -> PuzzleWorkSummaryResponse {
|
||||
let author = resolve_puzzle_work_author_by_user_id(
|
||||
state,
|
||||
&item.owner_user_id,
|
||||
Some(&item.author_display_name),
|
||||
None,
|
||||
);
|
||||
PuzzleWorkSummaryResponse {
|
||||
work_id: item.work_id,
|
||||
profile_id: item.profile_id,
|
||||
owner_user_id: item.owner_user_id,
|
||||
source_session_id: item.source_session_id,
|
||||
author_display_name: author.display_name,
|
||||
work_title: item.world_name.clone(),
|
||||
work_description: item.summary_text.clone(),
|
||||
level_name: item.world_name,
|
||||
summary: item.summary_text,
|
||||
theme_tags: item.theme_tags,
|
||||
cover_image_src: item.cover_image_src,
|
||||
cover_asset_id: item.cover_asset_id,
|
||||
publication_status: "published".to_string(),
|
||||
updated_at: item.updated_at,
|
||||
published_at: item.published_at,
|
||||
play_count: item.play_count,
|
||||
remix_count: item.remix_count,
|
||||
like_count: item.like_count,
|
||||
recent_play_count_7d: item.recent_play_count_7d,
|
||||
point_incentive_total_half_points: 0,
|
||||
point_incentive_claimed_points: 0,
|
||||
point_incentive_total_points: 0.0,
|
||||
point_incentive_claimable_points: 0,
|
||||
publish_ready: true,
|
||||
generation_status: Some("ready".to_string()),
|
||||
levels: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn map_puzzle_work_profile_response(
|
||||
state: &PuzzleApiState,
|
||||
item: PuzzleWorkProfileRecord,
|
||||
|
||||
Reference in New Issue
Block a user