Updated Grid
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::grid::model::CellPatch;
|
||||
use dashmap::DashMap;
|
||||
use serenity::{
|
||||
all::{Cache, Http},
|
||||
@@ -30,4 +31,11 @@ pub struct AppState {
|
||||
/// Per-map WebSocket broadcast channels for real-time collaboration.
|
||||
/// Key is the CSPRNG map ID (TEXT).
|
||||
pub map_rooms: Arc<DashMap<String, broadcast::Sender<String>>>,
|
||||
/// Per-map buffered single-cell paints awaiting the next 100 ms DB flush.
|
||||
/// Drained by a per-map background task; broadcast to clients happens
|
||||
/// immediately — only the DB write is deferred.
|
||||
pub map_paint_buffer: Arc<DashMap<String, Arc<tokio::sync::Mutex<Vec<CellPatch>>>>>,
|
||||
/// Sentinel set — tracks which map IDs already have a DB flush task running
|
||||
/// so we don't spawn duplicate tasks when multiple clients connect.
|
||||
pub map_flush_tasks: Arc<DashMap<String, bool>>,
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ use siren_bot::{
|
||||
join_voice_channel,
|
||||
pause::pause_track,
|
||||
play::enqueue_track,
|
||||
queue::{TrackInfo, get_is_paused, get_queue},
|
||||
queue::{TrackInfo, get_current_position, get_is_paused, get_queue, set_loop_current},
|
||||
resume::resume_track,
|
||||
skip::skip_track,
|
||||
stop::stop_track,
|
||||
@@ -39,6 +39,7 @@ pub fn get_guild_routes() -> Router<Arc<AppState>> {
|
||||
.route("/resume", post(resume_audio))
|
||||
.route("/stop", post(stop_audio))
|
||||
.route("/skip", post(skip_audio))
|
||||
.route("/loop", post(set_loop_audio))
|
||||
.route("/status", get(audio_status))
|
||||
}
|
||||
|
||||
@@ -47,6 +48,13 @@ pub fn get_guild_routes() -> Router<Arc<AppState>> {
|
||||
#[derive(Deserialize)]
|
||||
struct PlayTrackRequest {
|
||||
url: String,
|
||||
#[serde(default)]
|
||||
loop_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SetLoopRequest {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
/// Resolve the Discord snowflake for a local user from `user_connections`.
|
||||
@@ -130,7 +138,13 @@ async fn play_audio(
|
||||
// Play the track
|
||||
let manager = get_songbird();
|
||||
let _channel_id = join_voice_channel(&state.cache, manager, &guild_id, &user_id).await?;
|
||||
enqueue_track(manager, guild_id.to_owned(), &payload.url).await?;
|
||||
enqueue_track(
|
||||
manager,
|
||||
guild_id.to_owned(),
|
||||
&payload.url,
|
||||
payload.loop_enabled,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -216,10 +230,37 @@ async fn skip_audio(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ── POST /api/audio/{guild_id}/loop ──────────────────────────────────────────
|
||||
|
||||
async fn set_loop_audio(
|
||||
SessionAuthorization(session): SessionAuthorization,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(guild_id): Path<u64>,
|
||||
Json(payload): Json<SetLoopRequest>,
|
||||
) -> Result<()> {
|
||||
session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;
|
||||
log::debug!("<{}> Setting loop={}", guild_id, payload.enabled);
|
||||
|
||||
let guild_id = match state.cache.guild(guild_id) {
|
||||
Some(guild) => guild.id,
|
||||
None => return Err(Error::not_found("Guild not found".to_string())),
|
||||
};
|
||||
|
||||
let found = set_loop_current(guild_id.get(), payload.enabled).await;
|
||||
if !found {
|
||||
return Err(Error::not_found(
|
||||
"No track is currently playing".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct AudioStatus {
|
||||
voice_channel: Option<String>,
|
||||
is_paused: bool,
|
||||
/// Elapsed playback position of the current track in seconds.
|
||||
position_secs: f64,
|
||||
current_track: Option<TrackInfo>,
|
||||
queue: Vec<TrackInfo>,
|
||||
}
|
||||
@@ -238,19 +279,17 @@ async fn audio_status(
|
||||
|
||||
// ── Voice channel: look up the bot's own voice state + channel name from cache ──
|
||||
let bot_user_id = state.cache.current_user().id;
|
||||
let voice_channel = state
|
||||
.cache
|
||||
.guild(guild_id_snowflake)
|
||||
.and_then(|guild| {
|
||||
let ch_id = guild
|
||||
.voice_states
|
||||
.get(&bot_user_id)
|
||||
.and_then(|vs| vs.channel_id)?;
|
||||
guild.channels.get(&ch_id).map(|ch| ch.name.clone())
|
||||
});
|
||||
let voice_channel = state.cache.guild(guild_id_snowflake).and_then(|guild| {
|
||||
let ch_id = guild
|
||||
.voice_states
|
||||
.get(&bot_user_id)
|
||||
.and_then(|vs| vs.channel_id)?;
|
||||
guild.channels.get(&ch_id).map(|ch| ch.name.clone())
|
||||
});
|
||||
|
||||
// ── Playback paused state (delegated to siren-bot to keep songbird internal) ──
|
||||
let is_paused = get_is_paused(guild_id).await;
|
||||
// ── Playback paused state + position (delegated to siren-bot to keep songbird internal) ──
|
||||
let (is_paused, position_secs) =
|
||||
tokio::join!(get_is_paused(guild_id), get_current_position(guild_id));
|
||||
|
||||
// ── Queue metadata from our store (index 0 = currently playing) ──
|
||||
let mut full_queue = get_queue(guild_id);
|
||||
@@ -263,6 +302,7 @@ async fn audio_status(
|
||||
Ok(Json(AudioStatus {
|
||||
voice_channel,
|
||||
is_paused,
|
||||
position_secs,
|
||||
current_track,
|
||||
queue: full_queue,
|
||||
}))
|
||||
|
||||
@@ -35,9 +35,9 @@ pub fn get_routes() -> Router<Arc<AppState>> {
|
||||
.route("/register", post(register))
|
||||
.route("/login", post(login))
|
||||
.route("/logout", post(logout))
|
||||
.route("/me", get(me))
|
||||
.route("/user", get(get_self))
|
||||
.route("/profile", put(update_profile))
|
||||
.route("/change-password", post(change_password))
|
||||
.route("/password", put(update_password))
|
||||
.route("/connections/{provider}", delete(disconnect_provider))
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ async fn logout(
|
||||
(jar.add(removal), StatusCode::NO_CONTENT)
|
||||
}
|
||||
|
||||
async fn me(SessionAuthorization(session): SessionAuthorization) -> Result<Json<UserInfo>> {
|
||||
async fn get_self(SessionAuthorization(session): SessionAuthorization) -> Result<Json<UserInfo>> {
|
||||
let session = session.ok_or_else(|| Error::from(StatusCode::UNAUTHORIZED))?;
|
||||
Ok(Json(load_user_info(session.user_id).await?))
|
||||
}
|
||||
@@ -375,7 +375,7 @@ async fn update_profile(
|
||||
Ok(Json(load_user_info(session.user_id).await?))
|
||||
}
|
||||
|
||||
async fn change_password(
|
||||
async fn update_password(
|
||||
SessionAuthorization(session): SessionAuthorization,
|
||||
Json(payload): Json<ChangePasswordPayload>,
|
||||
) -> Result<StatusCode> {
|
||||
|
||||
@@ -21,6 +21,7 @@ use axum::{
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use model::{
|
||||
AccessRequestWithUser,
|
||||
CellPatch,
|
||||
ClientMessage,
|
||||
CreateAccessRequestPayload,
|
||||
CreateMapPayload,
|
||||
@@ -37,8 +38,11 @@ use model::{
|
||||
UpdatePermissionPayload,
|
||||
};
|
||||
use siren_core::utils::csprng;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::broadcast;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tokio::{
|
||||
sync::{broadcast, broadcast::error::RecvError},
|
||||
time::Duration,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub fn get_routes() -> Router<Arc<AppState>> {
|
||||
@@ -52,10 +56,10 @@ pub fn get_routes() -> Router<Arc<AppState>> {
|
||||
.route("/maps/{id}/permissions", put(update_permission))
|
||||
.route("/maps/{id}/favorite", post(favorite_map))
|
||||
.route("/maps/{id}/favorite", delete(unfavorite_map))
|
||||
.route("/maps/{id}/access-requests", post(create_access_request))
|
||||
.route("/maps/{id}/access-requests", get(list_access_requests))
|
||||
.route("/maps/{id}/access", post(create_access_request))
|
||||
.route("/maps/{id}/access", get(list_access_requests))
|
||||
.route(
|
||||
"/maps/{id}/access-requests/{request_id}",
|
||||
"/maps/{id}/access/{request_id}",
|
||||
put(resolve_access_request),
|
||||
)
|
||||
.route("/maps/{id}/ws", get(ws_handler))
|
||||
@@ -126,7 +130,9 @@ pub async fn list_maps(
|
||||
"SELECT
|
||||
gm.id, gm.name, gm.public_access, gm.owner_id,
|
||||
u.username AS owner_username,
|
||||
gm.colors, gm.created_at, gm.updated_at,
|
||||
gm.colors,
|
||||
gm.units_per_square, gm.unit_label, gm.movement_rule,
|
||||
gm.created_at, gm.updated_at,
|
||||
mp.role AS user_role,
|
||||
(mf.user_id IS NOT NULL) AS is_favorited
|
||||
FROM grid_maps gm
|
||||
@@ -235,18 +241,47 @@ pub async fn update_map(
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref mr) = payload.movement_rule {
|
||||
if !matches!(mr.as_str(), "free" | "alternating") {
|
||||
return Err(Error::new(422, "Invalid movement_rule value".into()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ups) = payload.units_per_square {
|
||||
if ups < 1 {
|
||||
return Err(Error::new(422, "units_per_square must be >= 1".into()));
|
||||
}
|
||||
}
|
||||
|
||||
let new_name = payload.name.as_deref().unwrap_or(&map.name);
|
||||
let new_pa = payload
|
||||
.public_access
|
||||
.as_deref()
|
||||
.unwrap_or(&map.public_access);
|
||||
let new_ups = payload.units_per_square.unwrap_or(map.units_per_square);
|
||||
let new_ul = payload
|
||||
.unit_label
|
||||
.as_deref()
|
||||
.unwrap_or(&map.unit_label)
|
||||
.to_string();
|
||||
let new_mr = payload
|
||||
.movement_rule
|
||||
.as_deref()
|
||||
.unwrap_or(&map.movement_rule)
|
||||
.to_string();
|
||||
|
||||
let updated: GridMap = sqlx::query_as(
|
||||
"UPDATE grid_maps SET name = $1, public_access = $2, updated_at = NOW()
|
||||
WHERE id = $3 RETURNING *",
|
||||
"UPDATE grid_maps
|
||||
SET name = $1, public_access = $2,
|
||||
units_per_square = $3, unit_label = $4, movement_rule = $5,
|
||||
updated_at = NOW()
|
||||
WHERE id = $6 RETURNING *",
|
||||
)
|
||||
.bind(new_name)
|
||||
.bind(new_pa)
|
||||
.bind(new_ups)
|
||||
.bind(&new_ul)
|
||||
.bind(&new_mr)
|
||||
.bind(&id)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
@@ -595,16 +630,51 @@ async fn handle_socket(
|
||||
|
||||
let editor = can_edit(&map_state.map, &session).await;
|
||||
|
||||
// ── Broadcast channel (1024 slots — reduces RecvError::Lagged risk) ──────
|
||||
let tx = state
|
||||
.map_rooms
|
||||
.entry(map_id.clone())
|
||||
.or_insert_with(|| {
|
||||
let (tx, _) = broadcast::channel(256);
|
||||
let (tx, _) = broadcast::channel(1024);
|
||||
tx
|
||||
})
|
||||
.clone();
|
||||
let mut rx = tx.subscribe();
|
||||
|
||||
// ── Per-map paint buffer (deferred DB writes) ─────────────────────────────
|
||||
let paint_buffer = state
|
||||
.map_paint_buffer
|
||||
.entry(map_id.clone())
|
||||
.or_insert_with(|| Arc::new(tokio::sync::Mutex::new(Vec::new())))
|
||||
.clone();
|
||||
|
||||
// Start the 100 ms DB-flush task exactly once per map_id.
|
||||
state
|
||||
.map_flush_tasks
|
||||
.entry(map_id.clone())
|
||||
.or_insert_with(|| {
|
||||
let buf = paint_buffer.clone();
|
||||
let mid = map_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut ticker = tokio::time::interval(Duration::from_millis(100));
|
||||
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
// Consume the initial immediate tick so the first flush waits 100 ms.
|
||||
ticker.tick().await;
|
||||
loop {
|
||||
ticker.tick().await;
|
||||
let cells: Vec<CellPatch> = {
|
||||
let mut guard = buf.lock().await;
|
||||
if guard.is_empty() {
|
||||
continue;
|
||||
}
|
||||
std::mem::take(&mut *guard)
|
||||
};
|
||||
flush_paint_buffer(&mid, cells).await;
|
||||
}
|
||||
});
|
||||
true
|
||||
});
|
||||
|
||||
let (mut ws_tx, mut ws_rx) = socket.split();
|
||||
|
||||
let init_msg = ServerMessage::State {
|
||||
@@ -616,20 +686,52 @@ async fn handle_socket(
|
||||
let _ = ws_tx.send(Message::Text(json.into())).await;
|
||||
}
|
||||
|
||||
// ── Send task: forwards broadcast messages AND sends periodic pings ───────
|
||||
let map_id_for_log = map_id.clone();
|
||||
let mut send_task = tokio::spawn(async move {
|
||||
while let Ok(json) = rx.recv().await {
|
||||
if ws_tx.send(Message::Text(json.into())).await.is_err() {
|
||||
break;
|
||||
let mut ping_interval = tokio::time::interval(Duration::from_secs(30));
|
||||
ping_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
|
||||
// Skip the first immediate tick so the first ping fires after 30 s.
|
||||
ping_interval.tick().await;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = rx.recv() => {
|
||||
match result {
|
||||
Ok(json) => {
|
||||
if ws_tx.send(Message::Text(json.into())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(RecvError::Lagged(n)) => {
|
||||
// The receiver fell too far behind. Close the socket so the
|
||||
// client reconnects and receives a fresh full-state message.
|
||||
log::warn!(
|
||||
"[WS] map {map_id_for_log}: receiver lagged by {n} messages — closing for reconnect"
|
||||
);
|
||||
break;
|
||||
}
|
||||
Err(_) => break, // channel closed
|
||||
}
|
||||
}
|
||||
_ = ping_interval.tick() => {
|
||||
// Send a WebSocket ping to keep the connection alive through
|
||||
// proxies and NAT that drop idle connections.
|
||||
if ws_tx.send(Message::Ping(vec![].into())).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// ── Receive task: handles incoming client messages ────────────────────────
|
||||
let tx_clone = tx.clone();
|
||||
let mut recv_task = tokio::spawn(async move {
|
||||
while let Some(Ok(msg)) = ws_rx.next().await {
|
||||
match msg {
|
||||
Message::Text(text) => {
|
||||
handle_client_message(&text, &map_id, editor, &tx_clone).await;
|
||||
handle_client_message(&text, &map_id, editor, &tx_clone, paint_buffer.clone()).await;
|
||||
}
|
||||
Message::Close(_) => break,
|
||||
_ => {}
|
||||
@@ -664,11 +766,18 @@ async fn fetch_map_state(map_id: &str) -> crate::error::Result<MapState> {
|
||||
Ok(MapState { map, cells, tokens })
|
||||
}
|
||||
|
||||
/// Handles a single message received from a WebSocket client.
|
||||
///
|
||||
/// `paint_buffer` — shared per-map buffer for deferred single-cell DB writes.
|
||||
/// Single-cell paints are queued here (not written to the DB immediately) and
|
||||
/// flushed every 100 ms by a background task, while still being broadcast
|
||||
/// instantly to all connected clients.
|
||||
async fn handle_client_message(
|
||||
raw: &str,
|
||||
map_id: &str,
|
||||
can_edit: bool,
|
||||
tx: &broadcast::Sender<String>,
|
||||
paint_buffer: Arc<tokio::sync::Mutex<Vec<CellPatch>>>,
|
||||
) {
|
||||
let client_msg: ClientMessage = match serde_json::from_str(raw) {
|
||||
Ok(m) => m,
|
||||
@@ -691,26 +800,18 @@ async fn handle_client_message(
|
||||
let pool = siren_core::data::pool();
|
||||
|
||||
let server_msg: Option<ServerMessage> = match client_msg {
|
||||
// Single-cell paints are queued for deferred DB persistence and
|
||||
// broadcast immediately so all clients see the change in real time.
|
||||
ClientMessage::PaintCell { x, y, color } => {
|
||||
let result = sqlx::query(
|
||||
"INSERT INTO grid_cells (map_id, x, y, color)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (map_id, x, y) DO UPDATE SET color = EXCLUDED.color",
|
||||
)
|
||||
.bind(map_id)
|
||||
.bind(x)
|
||||
.bind(y)
|
||||
.bind(&color)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Some(ServerMessage::CellPainted { x, y, color }),
|
||||
Err(e) => {
|
||||
log::error!("DB error painting cell: {e}");
|
||||
None
|
||||
}
|
||||
{
|
||||
let mut buf = paint_buffer.lock().await;
|
||||
buf.push(CellPatch {
|
||||
x,
|
||||
y,
|
||||
color: color.clone(),
|
||||
});
|
||||
}
|
||||
Some(ServerMessage::CellPainted { x, y, color })
|
||||
}
|
||||
|
||||
ClientMessage::PaintCells { cells } => {
|
||||
@@ -795,6 +896,7 @@ async fn handle_client_message(
|
||||
y: token.y,
|
||||
label: token.label,
|
||||
color: token.color,
|
||||
size: token.size,
|
||||
}),
|
||||
Err(e) => {
|
||||
log::error!("DB error adding token: {e}");
|
||||
@@ -823,6 +925,45 @@ async fn handle_client_message(
|
||||
}
|
||||
}
|
||||
|
||||
ClientMessage::UpdateToken { id, label, color } => {
|
||||
let result =
|
||||
sqlx::query("UPDATE grid_tokens SET label = $1, color = $2 WHERE id = $3 AND map_id = $4")
|
||||
.bind(&label)
|
||||
.bind(&color)
|
||||
.bind(&id)
|
||||
.bind(map_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(r) if r.rows_affected() > 0 => Some(ServerMessage::TokenUpdated { id, label, color }),
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
log::error!("DB error updating token: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClientMessage::ResizeToken { id, size } => {
|
||||
let size = size.max(1).min(9);
|
||||
let result = sqlx::query("UPDATE grid_tokens SET size = $1 WHERE id = $2 AND map_id = $3")
|
||||
.bind(size)
|
||||
.bind(&id)
|
||||
.bind(map_id)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(r) if r.rows_affected() > 0 => Some(ServerMessage::TokenResized { id, size }),
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
log::error!("DB error resizing token: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ClientMessage::DeleteToken { id } => {
|
||||
let result = sqlx::query("DELETE FROM grid_tokens WHERE id = $1 AND map_id = $2")
|
||||
.bind(&id)
|
||||
@@ -864,3 +1005,53 @@ async fn handle_client_message(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists a batch of buffered single-cell paints to the database.
|
||||
///
|
||||
/// Deduplicates cells by coordinate (last write wins) before issuing the
|
||||
/// upserts, so rapid repaints of the same cell only generate one DB write
|
||||
/// per flush interval.
|
||||
async fn flush_paint_buffer(map_id: &str, cells: Vec<CellPatch>) {
|
||||
if cells.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Deduplicate: for the same (x, y) keep only the last color written.
|
||||
let mut deduped: HashMap<(i32, i32), String> = HashMap::with_capacity(cells.len());
|
||||
for cell in cells {
|
||||
deduped.insert((cell.x, cell.y), cell.color);
|
||||
}
|
||||
|
||||
let pool = siren_core::data::pool();
|
||||
let mut tx_db = match pool.begin().await {
|
||||
Ok(t) => t,
|
||||
Err(e) => {
|
||||
log::error!("[flush] DB transaction error for map {map_id}: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for ((x, y), color) in &deduped {
|
||||
let res = sqlx::query(
|
||||
"INSERT INTO grid_cells (map_id, x, y, color)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (map_id, x, y) DO UPDATE SET color = EXCLUDED.color",
|
||||
)
|
||||
.bind(map_id)
|
||||
.bind(x)
|
||||
.bind(y)
|
||||
.bind(color)
|
||||
.execute(&mut *tx_db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
log::error!("[flush] DB error for map {map_id} cell ({x},{y}): {e}");
|
||||
let _ = tx_db.rollback().await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = tx_db.commit().await {
|
||||
log::error!("[flush] DB commit error for map {map_id}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,6 +41,12 @@ pub struct GridMap {
|
||||
pub public_access: String,
|
||||
pub owner_id: Uuid,
|
||||
pub colors: Vec<String>,
|
||||
/// Real-world units represented by one grid square
|
||||
pub units_per_square: i32,
|
||||
/// Label for the unit, e.g. "ft" or "m".
|
||||
pub unit_label: String,
|
||||
/// Diagonal movement rule: "free" or "alternating"
|
||||
pub movement_rule: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
@@ -56,6 +62,9 @@ pub struct ListedMap {
|
||||
pub owner_id: Uuid,
|
||||
pub owner_username: String,
|
||||
pub colors: Vec<String>,
|
||||
pub units_per_square: i32,
|
||||
pub unit_label: String,
|
||||
pub movement_rule: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
/// The authenticated caller's role on this map, or NULL if they only have it
|
||||
@@ -80,6 +89,9 @@ fn default_private() -> String {
|
||||
pub struct UpdateMapPayload {
|
||||
pub name: Option<String>,
|
||||
pub public_access: Option<String>,
|
||||
pub units_per_square: Option<i32>,
|
||||
pub unit_label: Option<String>,
|
||||
pub movement_rule: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
@@ -138,6 +150,12 @@ pub struct GridToken {
|
||||
pub y: i32,
|
||||
pub label: String,
|
||||
pub color: String,
|
||||
#[serde(default = "default_token_size")]
|
||||
pub size: i32,
|
||||
}
|
||||
|
||||
fn default_token_size() -> i32 {
|
||||
1
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
@@ -176,6 +194,15 @@ pub enum ClientMessage {
|
||||
DeleteToken {
|
||||
id: String,
|
||||
},
|
||||
UpdateToken {
|
||||
id: String,
|
||||
label: String,
|
||||
color: String,
|
||||
},
|
||||
ResizeToken {
|
||||
id: String,
|
||||
size: i32,
|
||||
},
|
||||
UpdateColors {
|
||||
colors: Vec<String>,
|
||||
},
|
||||
@@ -207,6 +234,7 @@ pub enum ServerMessage {
|
||||
y: i32,
|
||||
label: String,
|
||||
color: String,
|
||||
size: i32,
|
||||
},
|
||||
TokenMoved {
|
||||
id: String,
|
||||
@@ -216,6 +244,15 @@ pub enum ServerMessage {
|
||||
TokenDeleted {
|
||||
id: String,
|
||||
},
|
||||
TokenUpdated {
|
||||
id: String,
|
||||
label: String,
|
||||
color: String,
|
||||
},
|
||||
TokenResized {
|
||||
id: String,
|
||||
size: i32,
|
||||
},
|
||||
ColorsUpdated {
|
||||
colors: Vec<String>,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user