Public landing page live map; docs for Discord, live embeds, email

This commit is contained in:
Claude committed 2026-10-01 01:52:54 +00:00
1 parent cd985a1a7b
commit aae0a5a5ff
11 files changed
+195 -10

No files matched your search

+2
View File
@@ -65,6 +65,8 @@ pub fn api(state: &AppState) -> Router<AppState> {
.route("/guilds/{id}/invites", post(map::send_invite))
.route("/leaderboard", get(servers::global_leaderboard))
.route("/landing", get(landing::public_landing))
.route("/public/map/{id}", get(worldmap::public_info))
.route("/public/map/{id}/overlay", get(worldmap::public_overlay))
.route("/email/unsubscribe", get(emails::unsubscribe))
.route("/launcher/authlib-injector.json", get(account::authlib_index))
.route("/launcher/authlib-injector.jar", get(account::authlib_jar))
+46
View File
@@ -171,3 +171,49 @@ pub async fn tile(State(state): State<AppState>, Path((id, dim, z, x, file)): Pa
}
resp
}
// ---------------------------------------------------------------------------
// Public map (landing page)
// ---------------------------------------------------------------------------
/// The landing page's map section decides what the public may see. Without an enabled map section, nothing is public.
async fn landing_map(state: &AppState, requested: i64) -> AppResult<(i64, Value)> {
let cfg = crate::routes::landing::get_config(state).await?;
let block = cfg.blocks.iter().find(|b| b.block_type == "map" && b.enabled).ok_or_else(|| AppError::not_found("no public map"))?;
let wanted = block.options.get("server_id").and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok()))).unwrap_or(0);
let id: i64 = if wanted > 0 {
wanted
} else {
sqlx::query_scalar("SELECT COALESCE(MIN(id), 0) FROM game_servers WHERE live_map_enabled = 1").fetch_one(&state.db).await?
};
if id == 0 || (requested != 0 && requested != id) {
return Err(AppError::not_found("no public map"));
}
let server = get_server(state, id).await?;
if !server.map_enabled {
return Err(AppError::not_found("no public map"));
}
let flag = |key: &str, default: bool| block.options.get(key).and_then(|v| v.as_bool().or_else(|| v.as_str().map(|s| s == "true"))).unwrap_or(default);
Ok((id, json!({ "claims": flag("show_claims", true), "pins": flag("show_pins", true), "players": flag("show_players", false) })))
}
/// `GET /api/v1/public/map/{server}`: tile key and dimensions for the landing page map (0 = the server the page chose).
pub async fn public_info(State(state): State<AppState>, Path(requested): Path<i64>) -> AppResult<Json<Value>> {
let (id, layers) = landing_map(&state, requested).await?;
let mut v = info(&state, id, true).await;
v["server_id"] = json!(id);
v["layers"] = layers;
Ok(Json(v))
}
pub async fn public_overlay(State(state): State<AppState>, Path(requested): Path<i64>) -> AppResult<Json<Value>> {
let (id, layers) = landing_map(&state, requested).await?;
let overlay = state.worldmap.overlay(id);
let on = |k: &str| layers[k].as_bool().unwrap_or(false);
Ok(Json(json!({
"players": if on("players") { json!(state.worldmap.live_players(id)) } else { json!([]) },
"claims": if on("claims") { overlay.get("claims").cloned().unwrap_or(json!([])) } else { json!([]) },
"pins": if on("pins") { overlay.get("pins").cloned().unwrap_or(json!([])) } else { json!([]) },
"updated": state.worldmap.stats(id).last_overlay_at,
})))
}
+52
View File
@@ -0,0 +1,52 @@
mod common;
use common::*;
fn tile(zoom: u8, x: i32, y: i32) -> Vec<u8> {
let png = [0x89, b'P', b'N', b'G', 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3, 4];
let mut v = vec![zoom];
v.extend(x.to_be_bytes());
v.extend(y.to_be_bytes());
v.extend((png.len() as u32).to_be_bytes());
v.extend(png);
v
}
#[tokio::test]
async fn the_landing_page_decides_what_the_public_sees_of_the_map() {
let t = setup().await;
let admin = t.login("admin", "supersecret").await;
let (_, srv) = t.call("POST", "/api/admin/servers", Some(&admin), Some(json!({"name": "SMP", "instance_id": "smp"}))).await;
let key = srv["token"].as_str().unwrap().to_string();
assert_eq!(t.call_raw("POST", "/api/server/v1/map/tiles?dim=minecraft:overworld", Some(&key), tile(0, 0, 0)).await.0, StatusCode::OK);
t.call("POST", "/api/server/v1/map/players", Some(&key), Some(json!({"players": [{"uuid": "11111111-1111-1111-1111-111111111111", "name": "Steve", "dimension": "minecraft:overworld", "x": 1.0, "y": 64.0, "z": 2.0, "yaw": 0.0}]}))).await;
t.call("POST", "/api/server/v1/map/overlay", Some(&key), Some(json!({"claims": [{"id": "c"}], "pins": [{"id": "p"}]}))).await;
// No map section on the landing page: nothing is public.
assert_eq!(t.call("GET", "/api/v1/public/map/0", None, None).await.0, StatusCode::NOT_FOUND);
let landing = |opts: Value| json!({"blocks": [{"id": "m1", "type": "map", "enabled": true, "options": opts}]});
assert_eq!(t.call("PUT", "/api/admin/landing", Some(&admin), Some(landing(json!({"show_players": false})))).await.0, StatusCode::OK);
let (s, info) = t.call("GET", "/api/v1/public/map/0", None, None).await;
assert_eq!(s, StatusCode::OK, "{info}");
assert_eq!(info["ready"], true);
assert_eq!(info["layers"]["players"], false);
assert_eq!(info["layers"]["claims"], true);
// The tile key from the public info loads tiles without signing in.
let token = info["token"].as_str().unwrap();
let (s, png) = t.fetch(&format!("/api/map/{}/overworld/0/0/0.png?t={token}", info["server_id"])).await;
assert_eq!((s, png.len()), (StatusCode::OK, 12));
// Players stay hidden unless the admin turns them on.
let (_, o) = t.call("GET", "/api/v1/public/map/0/overlay", None, None).await;
assert_eq!(o["players"].as_array().unwrap().len(), 0);
assert_eq!(o["claims"].as_array().unwrap().len(), 1);
t.call("PUT", "/api/admin/landing", Some(&admin), Some(landing(json!({"show_players": true, "show_pins": false})))).await;
let (_, o) = t.call("GET", "/api/v1/public/map/0/overlay", None, None).await;
assert_eq!(o["players"][0]["name"], "Steve");
assert_eq!(o["pins"].as_array().unwrap().len(), 0);
// Another server's map isn't exposed, and turning the section off closes it.
assert_eq!(t.call("GET", "/api/v1/public/map/999", None, None).await.0, StatusCode::NOT_FOUND);
t.call("PUT", "/api/admin/landing", Some(&admin), Some(json!({"blocks": [{"id": "m1", "type": "map", "enabled": false, "options": {}}]}))).await;
assert_eq!(t.call("GET", "/api/v1/public/map/0", None, None).await.0, StatusCode::NOT_FOUND);
}