Cross-server network: shared economy groups for balances and guild banks
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjMbLQujBHunCCu5GpsHaT
This commit is contained in:
9 files changed
+137
-39
No files matched your search
@@ -672,6 +672,10 @@ const MIGRATIONS: &[&str] = &[
|
||||
r#"
|
||||
ALTER TABLE groups ADD COLUMN discord_role TEXT NOT NULL DEFAULT '';
|
||||
"#,
|
||||
// Servers can share one economy (balances and guild banks) by sharing a group name.
|
||||
r#"
|
||||
ALTER TABLE game_servers ADD COLUMN economy_group TEXT NOT NULL DEFAULT '';
|
||||
"#,
|
||||
];
|
||||
|
||||
pub async fn connect(data_dir: &Path) -> Result<SqlitePool> {
|
||||
|
||||
@@ -50,9 +50,9 @@ pub async fn server_adjust_balance(GameServer(server): GameServer, State(state):
|
||||
if !p.delta.is_finite() || p.delta == 0.0 || p.delta.abs() > 1e12 { return Err(AppError::bad_request("Invalid amount")); }
|
||||
let (mut tx, previous) = begin_operation(&state, server.id, &p.operation_id).await?;
|
||||
if let Some(previous) = previous { return Ok(Json(previous)); }
|
||||
ensure_balance(&mut tx, server.id, &p.uuid, &p.username).await?;
|
||||
ensure_balance(&mut tx, server.economy_id, &p.uuid, &p.username).await?;
|
||||
let balance: Option<f64> = sqlx::query_scalar("UPDATE server_economy SET balance = balance + ?, updated_at = ? WHERE server_id = ? AND uuid = ? AND balance + ? >= 0 RETURNING balance")
|
||||
.bind(p.delta).bind(chrono::Utc::now().to_rfc3339()).bind(server.id).bind(&p.uuid).bind(p.delta).fetch_optional(&mut *tx).await?;
|
||||
.bind(p.delta).bind(chrono::Utc::now().to_rfc3339()).bind(server.economy_id).bind(&p.uuid).bind(p.delta).fetch_optional(&mut *tx).await?;
|
||||
let Some(balance) = balance else { return Err(AppError::bad_request("Insufficient funds")); };
|
||||
let (from, from_name, to, to_name) = if p.delta > 0.0 { ("server", "Server Shop", p.uuid.as_str(), p.username.as_str()) } else { (p.uuid.as_str(), p.username.as_str(), "server", "Server Shop") };
|
||||
sqlx::query("INSERT INTO economy_transactions(server_id, from_uuid, from_name, to_uuid, to_name, amount, description, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
||||
@@ -229,7 +229,7 @@ pub async fn server_get_balance(
|
||||
let bal_opt: Option<f64> = sqlx::query_scalar(
|
||||
"SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&payload.uuid)
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
@@ -243,7 +243,7 @@ pub async fn server_get_balance(
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT(server_id, uuid) DO UPDATE SET username = excluded.username",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&payload.uuid)
|
||||
.bind(&username)
|
||||
.bind(starting_balance)
|
||||
@@ -288,7 +288,7 @@ pub async fn server_transfer(
|
||||
let from_bal: Option<f64> = sqlx::query_scalar(
|
||||
"SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&payload.from_uuid)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
@@ -302,7 +302,7 @@ pub async fn server_transfer(
|
||||
sqlx::query("UPDATE server_economy SET balance = ?, updated_at = ? WHERE server_id = ? AND uuid = ?")
|
||||
.bind(new_from_bal)
|
||||
.bind(&now)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&payload.from_uuid)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -316,7 +316,7 @@ pub async fn server_transfer(
|
||||
username = excluded.username,
|
||||
updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&payload.to_uuid)
|
||||
.bind(&payload.to_name)
|
||||
.bind(payload.amount)
|
||||
@@ -369,7 +369,7 @@ pub async fn server_sync_balance(
|
||||
username = excluded.username,
|
||||
updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&payload.uuid)
|
||||
.bind(&payload.username)
|
||||
.bind(payload.new_balance)
|
||||
@@ -391,7 +391,7 @@ pub async fn server_baltop(
|
||||
ORDER BY balance DESC
|
||||
LIMIT 20",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.fetch_all(&state.db)
|
||||
.await?;
|
||||
|
||||
@@ -502,35 +502,35 @@ pub async fn server_market_buy(
|
||||
Some(m)
|
||||
} else { None };
|
||||
|
||||
ensure_balance(&mut tx, server.id, &payload.buyer_uuid, &payload.buyer_name).await?;
|
||||
ensure_balance(&mut tx, server.economy_id, &payload.buyer_uuid, &payload.buyer_name).await?;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
|
||||
// Buyer pays: their own balance, or their guild's bank.
|
||||
let new_balance = if let Some(m) = &buyer_guild {
|
||||
let left = crate::routes::guild_bank::adjust_wallet(&mut tx, server.id, &m.guild_id, -price).await?;
|
||||
crate::routes::guild_bank::log(&mut tx, server.id, &m.guild_id, &payload.buyer_uuid, "purchase", price, &format!("Market: {amount}x {item_name}")).await?;
|
||||
let left = crate::routes::guild_bank::adjust_wallet(&mut tx, server.economy_id, &m.guild_id, -price).await?;
|
||||
crate::routes::guild_bank::log(&mut tx, server.economy_id, &m.guild_id, &payload.buyer_uuid, "purchase", price, &format!("Market: {amount}x {item_name}")).await?;
|
||||
left
|
||||
} else {
|
||||
let buyer_bal: f64 = sqlx::query_scalar("SELECT balance FROM server_economy WHERE server_id = ? AND uuid = ?")
|
||||
.bind(server.id).bind(&payload.buyer_uuid).fetch_optional(&mut *tx).await?.unwrap_or(0.0);
|
||||
.bind(server.economy_id).bind(&payload.buyer_uuid).fetch_optional(&mut *tx).await?.unwrap_or(0.0);
|
||||
if buyer_bal < price {
|
||||
return Err(AppError::bad_request("Insufficient funds"));
|
||||
}
|
||||
sqlx::query("UPDATE server_economy SET balance = balance - ?, updated_at = ? WHERE server_id = ? AND uuid = ?")
|
||||
.bind(price).bind(&now).bind(server.id).bind(&payload.buyer_uuid).execute(&mut *tx).await?;
|
||||
.bind(price).bind(&now).bind(server.economy_id).bind(&payload.buyer_uuid).execute(&mut *tx).await?;
|
||||
buyer_bal - price
|
||||
};
|
||||
|
||||
// Seller is paid: the guild bank for guild listings, otherwise the player.
|
||||
let (to_uuid, to_name) = if let Some((guild_id, tag)) = &seller_guild {
|
||||
crate::routes::guild_bank::adjust_wallet(&mut tx, server.id, guild_id, price).await?;
|
||||
crate::routes::guild_bank::log(&mut tx, server.id, guild_id, &seller_uuid, "sale", price, &format!("Market: {amount}x {item_name}")).await?;
|
||||
crate::routes::guild_bank::adjust_wallet(&mut tx, server.economy_id, guild_id, price).await?;
|
||||
crate::routes::guild_bank::log(&mut tx, server.economy_id, guild_id, &seller_uuid, "sale", price, &format!("Market: {amount}x {item_name}")).await?;
|
||||
(format!("guild:{guild_id}"), format!("[{tag}] guild bank"))
|
||||
} else {
|
||||
// A seller without an account first gets the usual starting balance, then the sale on top.
|
||||
ensure_balance(&mut tx, server.id, &seller_uuid, &seller_name).await?;
|
||||
ensure_balance(&mut tx, server.economy_id, &seller_uuid, &seller_name).await?;
|
||||
sqlx::query("UPDATE server_economy SET balance = balance + ?, username = ?, updated_at = ? WHERE server_id = ? AND uuid = ?")
|
||||
.bind(price).bind(&seller_name).bind(&now).bind(server.id).bind(&seller_uuid)
|
||||
.bind(price).bind(&seller_name).bind(&now).bind(server.economy_id).bind(&seller_uuid)
|
||||
.execute(&mut *tx).await?;
|
||||
(seller_uuid.clone(), seller_name.clone())
|
||||
};
|
||||
|
||||
@@ -101,7 +101,7 @@ pub async fn server_bank_info(GameServer(server): GameServer, State(state): Stat
|
||||
return Ok(Json(json!({ "guild": null })));
|
||||
};
|
||||
let balance: f64 = sqlx::query_scalar("SELECT balance FROM guild_wallets WHERE server_id = ? AND guild_id = ?")
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&m.guild_id)
|
||||
.fetch_optional(&mut *conn)
|
||||
.await?
|
||||
@@ -110,7 +110,7 @@ pub async fn server_bank_info(GameServer(server): GameServer, State(state): Stat
|
||||
"SELECT t.kind, COALESCE((SELECT gm.name FROM guild_members gm WHERE gm.guild_id = t.guild_id AND gm.uuid = t.actor_uuid), 'Former member'), t.amount, t.note, t.created_at
|
||||
FROM guild_wallet_transactions t WHERE t.server_id = ? AND t.guild_id = ? ORDER BY t.id DESC LIMIT 8",
|
||||
)
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&m.guild_id)
|
||||
.fetch_all(&mut *conn)
|
||||
.await?;
|
||||
@@ -119,7 +119,7 @@ pub async fn server_bank_info(GameServer(server): GameServer, State(state): Stat
|
||||
"role": m.role,
|
||||
"can_spend": m.can_spend(),
|
||||
"balance": balance,
|
||||
"my_balance": player_balance(&mut conn, server.id, &p.uuid).await?,
|
||||
"my_balance": player_balance(&mut conn, server.economy_id, &p.uuid).await?,
|
||||
"recent": recent.into_iter().map(|(kind, who, amount, note, at)| json!({ "kind": kind, "who": who, "amount": amount, "note": note, "at": at })).collect::<Vec<_>>(),
|
||||
})))
|
||||
}
|
||||
@@ -149,21 +149,21 @@ pub async fn server_bank_transfer(GameServer(server): GameServer, State(state):
|
||||
if withdraw && !m.can_spend() {
|
||||
return Err(AppError::forbidden("Only guild leaders and officers can withdraw"));
|
||||
}
|
||||
ensure_balance(&mut tx, server.id, &p.uuid, &p.username).await?;
|
||||
ensure_balance(&mut tx, server.economy_id, &p.uuid, &p.username).await?;
|
||||
let (guild_delta, player_delta) = if withdraw { (-p.amount, p.amount) } else { (p.amount, -p.amount) };
|
||||
let moved: Option<f64> = sqlx::query_scalar(
|
||||
"UPDATE server_economy SET balance = balance + ?, updated_at = ? WHERE server_id = ? AND uuid = ? AND balance + ? >= 0 RETURNING balance",
|
||||
)
|
||||
.bind(player_delta)
|
||||
.bind(chrono::Utc::now().to_rfc3339())
|
||||
.bind(server.id)
|
||||
.bind(server.economy_id)
|
||||
.bind(&p.uuid)
|
||||
.bind(player_delta)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
let my_balance = moved.ok_or_else(|| AppError::bad_request("Insufficient funds"))?;
|
||||
let balance = adjust_wallet(&mut tx, server.id, &m.guild_id, guild_delta).await?;
|
||||
log(&mut tx, server.id, &m.guild_id, &p.uuid, &p.action, p.amount, "").await?;
|
||||
let balance = adjust_wallet(&mut tx, server.economy_id, &m.guild_id, guild_delta).await?;
|
||||
log(&mut tx, server.economy_id, &m.guild_id, &p.uuid, &p.action, p.amount, "").await?;
|
||||
let message = if withdraw { format!("Withdrew ${:.2} from [{}] bank.", p.amount, m.tag) } else { format!("Deposited ${:.2} into [{}] bank.", p.amount, m.tag) };
|
||||
finish_operation(tx, server.id, &p.operation_id, json!({ "ok": true, "balance": balance, "my_balance": my_balance, "message": message })).await
|
||||
}
|
||||
@@ -186,9 +186,9 @@ pub async fn server_bank_credit(GameServer(server): GameServer, State(state): St
|
||||
return Ok(Json(previous));
|
||||
}
|
||||
let m = membership(&mut tx, &server, &p.uuid).await?.ok_or_else(|| AppError::forbidden("You are not in a guild"))?;
|
||||
let balance = adjust_wallet(&mut tx, server.id, &m.guild_id, p.amount).await?;
|
||||
let balance = adjust_wallet(&mut tx, server.economy_id, &m.guild_id, p.amount).await?;
|
||||
let note = if p.description.is_empty() { "Items sold to the server shop" } else { &p.description };
|
||||
log(&mut tx, server.id, &m.guild_id, &p.uuid, "sale", p.amount, note).await?;
|
||||
log(&mut tx, server.economy_id, &m.guild_id, &p.uuid, "sale", p.amount, note).await?;
|
||||
let message = format!("Sold for ${:.2}, paid into the [{}] bank.", p.amount, m.tag);
|
||||
finish_operation(tx, server.id, &p.operation_id, json!({ "ok": true, "balance": balance, "message": message })).await
|
||||
}
|
||||
@@ -223,11 +223,11 @@ pub async fn server_bank_pay(GameServer(server): GameServer, State(state): State
|
||||
.fetch_optional(&mut *tx)
|
||||
.await?;
|
||||
let (target_id, target_name, target_tag) = target.ok_or_else(|| AppError::not_found("No other guild with that tag"))?;
|
||||
let balance = adjust_wallet(&mut tx, server.id, &m.guild_id, -p.amount).await?;
|
||||
adjust_wallet(&mut tx, server.id, &target_id, p.amount).await?;
|
||||
let balance = adjust_wallet(&mut tx, server.economy_id, &m.guild_id, -p.amount).await?;
|
||||
adjust_wallet(&mut tx, server.economy_id, &target_id, p.amount).await?;
|
||||
let note = p.note.chars().take(60).collect::<String>();
|
||||
log(&mut tx, server.id, &m.guild_id, &p.uuid, "transfer_out", p.amount, &format!("to [{target_tag}] {target_name} {note}")).await?;
|
||||
log(&mut tx, server.id, &target_id, &p.uuid, "transfer_in", p.amount, &format!("from [{}] {} {note}", m.tag, m.name)).await?;
|
||||
log(&mut tx, server.economy_id, &m.guild_id, &p.uuid, "transfer_out", p.amount, &format!("to [{target_tag}] {target_name} {note}")).await?;
|
||||
log(&mut tx, server.economy_id, &target_id, &p.uuid, "transfer_in", p.amount, &format!("from [{}] {} {note}", m.tag, m.name)).await?;
|
||||
let message = format!("Paid ${:.2} from [{}] to [{}] {}.", p.amount, m.tag, target_tag, target_name);
|
||||
finish_operation(tx, server.id, &p.operation_id, json!({ "ok": true, "balance": balance, "message": message })).await
|
||||
}
|
||||
@@ -148,15 +148,16 @@ pub async fn guild_wallet(
|
||||
let member: bool = sqlx::query_scalar("SELECT EXISTS(SELECT 1 FROM guild_members gm JOIN guilds g ON g.id=gm.guild_id JOIN game_servers s ON s.instance_id=g.instance_id WHERE gm.guild_id=? AND gm.uuid=? AND s.id=?)")
|
||||
.bind(&guild_id).bind(&auth.uuid).bind(query.server_id).fetch_one(&state.db).await?;
|
||||
if !member { return Err(AppError::forbidden("Guild membership is required")); }
|
||||
let sid = crate::routes::servers::economy_scope(&state.db, query.server_id).await?;
|
||||
let balance: f64 = sqlx::query_scalar("SELECT balance FROM guild_wallets WHERE guild_id=? AND server_id=?")
|
||||
.bind(&guild_id).bind(query.server_id).fetch_optional(&state.db).await?.unwrap_or(0.0);
|
||||
.bind(&guild_id).bind(sid).fetch_optional(&state.db).await?.unwrap_or(0.0);
|
||||
let rows: Vec<(i64, String, String, f64, String, String)> = sqlx::query_as("SELECT id,actor_uuid,kind,amount,created_at,note FROM guild_wallet_transactions WHERE guild_id=? AND server_id=? ORDER BY id DESC LIMIT 30")
|
||||
.bind(&guild_id).bind(query.server_id).fetch_all(&state.db).await?;
|
||||
.bind(&guild_id).bind(sid).fetch_all(&state.db).await?;
|
||||
let role: String = sqlx::query_scalar("SELECT gm.role FROM guild_members gm WHERE gm.guild_id=? AND gm.uuid=?")
|
||||
.bind(&guild_id).bind(&auth.uuid).fetch_one(&state.db).await?;
|
||||
// A player's first transfer creates their balance at 1000, so show that until then.
|
||||
let my_balance: f64 = sqlx::query_scalar("SELECT balance FROM server_economy WHERE server_id=? AND uuid=?")
|
||||
.bind(query.server_id).bind(&auth.uuid).fetch_optional(&state.db).await?.unwrap_or(1000.0);
|
||||
.bind(sid).bind(&auth.uuid).fetch_optional(&state.db).await?.unwrap_or(1000.0);
|
||||
Ok(Json(serde_json::json!({"balance":balance,"role":role,"my_balance":my_balance,"currency_symbol":"$","transactions":rows.into_iter().map(|(id,actor,kind,amount,created_at,note)| serde_json::json!({"id":id,"actor_uuid":actor,"kind":kind,"amount":amount,"created_at":created_at,"note":note})).collect::<Vec<_>>()})))
|
||||
}
|
||||
|
||||
@@ -176,6 +177,9 @@ async fn wallet_transfer(state: &AppState, auth: &AuthUser, guild_id: &str, p: G
|
||||
.bind(guild_id).bind(&auth.uuid).bind(p.server_id).fetch_optional(&state.db).await?;
|
||||
let Some(role) = role else { return Err(AppError::forbidden("Guild membership is required")); };
|
||||
if withdraw && role != "leader" && role != "officer" { return Err(AppError::forbidden("Only guild leaders and officers can withdraw")); }
|
||||
// Servers in one economy group share balances and guild banks.
|
||||
let mut p = p;
|
||||
p.server_id = crate::routes::servers::economy_scope(&state.db, p.server_id).await?;
|
||||
let now = chrono::Utc::now().to_rfc3339();
|
||||
let mut tx = state.db.begin().await?;
|
||||
// The first write serializes transfers across concurrent requests.
|
||||
|
||||
@@ -63,6 +63,14 @@ pub struct ServerRow {
|
||||
pub instance_id: String,
|
||||
pub map_url: String,
|
||||
pub live_map_enabled: bool,
|
||||
/// Servers with the same non-empty name share player balances and guild banks.
|
||||
#[sqlx(default)]
|
||||
pub economy_group: String,
|
||||
/// The server whose balances this one uses: itself, or the oldest in its economy group.
|
||||
/// Only filled in for token-authenticated game server requests.
|
||||
#[sqlx(default)]
|
||||
#[serde(skip)]
|
||||
pub economy_id: i64,
|
||||
pub id: i64,
|
||||
pub name: String,
|
||||
#[serde(skip)]
|
||||
@@ -122,10 +130,24 @@ impl FromRequestParts<AppState> for GameServer {
|
||||
.bind(hash_token(token.trim()))
|
||||
.fetch_optional(&state.db)
|
||||
.await?;
|
||||
row.map(GameServer).ok_or_else(|| AppError::unauthorized("unknown server token — create one under Servers in the panel"))
|
||||
let mut server = row.ok_or_else(|| AppError::unauthorized("unknown server token — create one under Servers in the panel"))?;
|
||||
server.economy_id = economy_scope(&state.db, server.id).await?;
|
||||
Ok(GameServer(server))
|
||||
}
|
||||
}
|
||||
|
||||
/// Which server's balances `server_id` uses (see [`ServerRow::economy_group`]).
|
||||
pub async fn economy_scope(db: &sqlx::SqlitePool, server_id: i64) -> AppResult<i64> {
|
||||
Ok(sqlx::query_scalar(
|
||||
"SELECT COALESCE((SELECT MIN(b.id) FROM game_servers b WHERE b.economy_group <> '' AND b.economy_group = a.economy_group COLLATE NOCASE), a.id)
|
||||
FROM game_servers a WHERE a.id = ?",
|
||||
)
|
||||
.bind(server_id)
|
||||
.fetch_optional(db)
|
||||
.await?
|
||||
.unwrap_or(server_id))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Hello {
|
||||
@@ -845,6 +867,8 @@ pub struct ServerInput {
|
||||
map_url: String,
|
||||
#[serde(default)]
|
||||
live_map_enabled: bool,
|
||||
#[serde(default)]
|
||||
economy_group: String,
|
||||
}
|
||||
|
||||
fn default_access() -> String {
|
||||
@@ -862,6 +886,10 @@ impl ServerInput {
|
||||
if self.access == "groups" && self.allowed_groups.is_empty() {
|
||||
return Err(AppError::bad_request("pick at least one group"));
|
||||
}
|
||||
let group = self.economy_group.trim();
|
||||
if group.chars().count() > 32 || !group.chars().all(|c| c.is_alphanumeric() || matches!(c, ' ' | '-' | '_')) {
|
||||
return Err(AppError::bad_request("economy groups are up to 32 letters, digits, spaces, - or _"));
|
||||
}
|
||||
if !self.map_url.trim().is_empty() {
|
||||
let url = reqwest::Url::parse(self.map_url.trim()).map_err(|_| AppError::bad_request("map URL must be an absolute URL"))?;
|
||||
if url.scheme() != "https" && !(url.scheme() == "http" && matches!(url.host_str(), Some("localhost" | "127.0.0.1"))) {
|
||||
@@ -890,7 +918,7 @@ pub async fn create(_: AdminUser, State(state): State<AppState>, Json(input): Js
|
||||
input.validate()?;
|
||||
let token = new_token();
|
||||
let id: i64 = sqlx::query_scalar(
|
||||
"INSERT INTO game_servers (name, token_hash, token_hint, access, allowed_groups, require_launcher, created_at, instance_id, map_url, live_map_enabled) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
||||
"INSERT INTO game_servers (name, token_hash, token_hint, access, allowed_groups, require_launcher, created_at, instance_id, map_url, live_map_enabled, economy_group) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id",
|
||||
)
|
||||
.bind(input.name.trim())
|
||||
.bind(hash_token(&token))
|
||||
@@ -902,6 +930,7 @@ pub async fn create(_: AdminUser, State(state): State<AppState>, Json(input): Js
|
||||
.bind(&input.instance_id)
|
||||
.bind(input.map_url.trim())
|
||||
.bind(input.live_map_enabled)
|
||||
.bind(input.economy_group.trim())
|
||||
.fetch_one(&state.db)
|
||||
.await?;
|
||||
let server = get_server(&state, id).await?;
|
||||
@@ -916,7 +945,7 @@ pub async fn update(
|
||||
) -> AppResult<Json<ServerView>> {
|
||||
input.validate()?;
|
||||
get_server(&state, id).await?;
|
||||
sqlx::query("UPDATE game_servers SET name = ?, access = ?, allowed_groups = ?, require_launcher = ?, instance_id = ?, map_url = ?, live_map_enabled = ? WHERE id = ?")
|
||||
sqlx::query("UPDATE game_servers SET name = ?, access = ?, allowed_groups = ?, require_launcher = ?, instance_id = ?, map_url = ?, live_map_enabled = ?, economy_group = ? WHERE id = ?")
|
||||
.bind(input.name.trim())
|
||||
.bind(&input.access)
|
||||
.bind(serde_json::to_string(&input.allowed_groups).unwrap_or_else(|_| "[]".into()))
|
||||
@@ -924,6 +953,7 @@ pub async fn update(
|
||||
.bind(&input.instance_id)
|
||||
.bind(input.map_url.trim())
|
||||
.bind(input.live_map_enabled)
|
||||
.bind(input.economy_group.trim())
|
||||
.bind(id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
@@ -943,6 +973,17 @@ pub async fn regenerate_token(_: AdminUser, State(state): State<AppState>, Path(
|
||||
}
|
||||
|
||||
pub async fn remove(_: AdminUser, State(state): State<AppState>, Path(id): Path<i64>) -> AppResult<Json<Value>> {
|
||||
// If this server holds its group's shared balances, hand them to the next server first.
|
||||
let group: String = sqlx::query_scalar("SELECT economy_group FROM game_servers WHERE id = ?").bind(id).fetch_optional(&state.db).await?.unwrap_or_default();
|
||||
if !group.is_empty() && economy_scope(&state.db, id).await? == id {
|
||||
let next: Option<i64> = sqlx::query_scalar("SELECT MIN(id) FROM game_servers WHERE id <> ? AND economy_group = ? COLLATE NOCASE").bind(id).bind(&group).fetch_one(&state.db).await?;
|
||||
if let Some(next) = next {
|
||||
for table in ["server_economy", "guild_wallets", "guild_wallet_transactions"] {
|
||||
let sql = if table == "guild_wallet_transactions" { format!("UPDATE {table} SET server_id = ? WHERE server_id = ?") } else { format!("UPDATE OR IGNORE {table} SET server_id = ? WHERE server_id = ?") };
|
||||
sqlx::query(&sql).bind(next).bind(id).execute(&state.db).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
let done = sqlx::query("DELETE FROM game_servers WHERE id = ?").bind(id).execute(&state.db).await?;
|
||||
if done.rows_affected() == 0 {
|
||||
return Err(AppError::not_found("server not found"));
|
||||
|
||||
Reference in new issue
Block a user