synops/maskinrommet/src/stdb.rs
vegard 445f32de69 Sanntidslyd: kommunikasjonsnode → LiveKit-rom (oppgave 11.2)
Kobler kommunikasjonsnoder til LiveKit for sanntidslyd.
Bruker sender join_communication-intensjon, maskinrommet validerer
tilgang og returnerer signert LiveKit JWT-token + rom-URL.

Nye komponenter:
- maskinrommet/src/livekit.rs: JWT token-generering (HS256-signert
  med LIVEKIT_API_SECRET, 1-times TTL, publisher/subscriber-roller)
- POST /intentions/join_communication: validerer deltaker-edge,
  genererer token, oppretter rom i STDB, oppdaterer node-metadata
- POST /intentions/leave_communication: fjerner deltaker fra STDB
- POST /intentions/close_communication: stenger rom (krever owner)
- SpacetimeDB: live_room + room_participant tabeller for sanntids
  deltakerliste (frontend abonnerer via WebSocket)

SpacetimeDB-modul publisert som synops-v2 (ny identitet etter
at den opprinnelige ikke lenger var tilgjengelig). .env oppdatert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:54:40 +00:00

397 lines
10 KiB
Rust

// SpacetimeDB HTTP-klient for maskinrommet.
//
// Kaller STDB-reducere via HTTP JSON API.
// Maskinrommet eier all skriving — denne klienten er eneste vei inn.
//
// API-format: POST /v1/database/{db}/call/{reducer}
// Body: JSON-objekt med navngitte parametre.
// Auth: Bearer-token fra STDB-identitet.
//
// Ref: docs/retninger/datalaget.md, docs/infra/synkronisering.md
use reqwest::Client;
use serde::Serialize;
/// SpacetimeDB-klient som kaller reducere via HTTP.
#[derive(Clone)]
pub struct StdbClient {
client: Client,
base_url: String,
database: String,
token: String,
}
impl StdbClient {
/// Opprett ny klient. `base_url` er STDB-serverens URL (f.eks. "http://spacetimedb:3000").
pub fn new(base_url: &str, database: &str, token: &str) -> Self {
Self {
client: Client::new(),
base_url: base_url.trim_end_matches('/').to_string(),
database: database.to_string(),
token: token.to_string(),
}
}
/// Hent en ny identitet og token fra STDB-serveren.
/// Brukes ved oppstart hvis ingen token er konfigurert.
pub async fn create_identity(base_url: &str) -> Result<(String, String), StdbError> {
let client = Client::new();
let url = format!("{}/v1/identity", base_url.trim_end_matches('/'));
let resp = client.post(&url).send().await?;
if !resp.status().is_success() {
return Err(StdbError::Http(format!(
"Kunne ikke opprette identitet: {}",
resp.status()
)));
}
let body: serde_json::Value = resp.json().await?;
let identity = body["identity"]
.as_str()
.ok_or_else(|| StdbError::Http("Mangler identity i respons".into()))?
.to_string();
let token = body["token"]
.as_str()
.ok_or_else(|| StdbError::Http("Mangler token i respons".into()))?
.to_string();
Ok((identity, token))
}
/// Kall en reducer med navngitte parametre (JSON-objekt).
async fn call_reducer<T: Serialize>(&self, reducer: &str, args: &T) -> Result<(), StdbError> {
let url = format!(
"{}/v1/database/{}/call/{}",
self.base_url, self.database, reducer
);
let resp = self
.client
.post(&url)
.bearer_auth(&self.token)
.json(args)
.send()
.await?;
if resp.status().is_success() {
Ok(())
} else {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
Err(StdbError::Reducer {
reducer: reducer.to_string(),
status: status.as_u16(),
message: body,
})
}
}
// =========================================================================
// Node-operasjoner
// =========================================================================
pub async fn create_node(
&self,
id: &str,
node_kind: &str,
title: &str,
content: &str,
visibility: &str,
metadata: &str,
created_by: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
node_kind: &'a str,
title: &'a str,
content: &'a str,
visibility: &'a str,
metadata: &'a str,
created_by: &'a str,
}
self.call_reducer(
"create_node",
&Args {
id,
node_kind,
title,
content,
visibility,
metadata,
created_by,
},
)
.await
}
pub async fn update_node(
&self,
id: &str,
node_kind: &str,
title: &str,
content: &str,
visibility: &str,
metadata: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
node_kind: &'a str,
title: &'a str,
content: &'a str,
visibility: &'a str,
metadata: &'a str,
}
self.call_reducer(
"update_node",
&Args {
id,
node_kind,
title,
content,
visibility,
metadata,
},
)
.await
}
pub async fn delete_node(&self, id: &str) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
}
self.call_reducer("delete_node", &Args { id }).await
}
// =========================================================================
// Edge-operasjoner
// =========================================================================
pub async fn create_edge(
&self,
id: &str,
source_id: &str,
target_id: &str,
edge_type: &str,
metadata: &str,
system: bool,
created_by: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
source_id: &'a str,
target_id: &'a str,
edge_type: &'a str,
metadata: &'a str,
system: bool,
created_by: &'a str,
}
self.call_reducer(
"create_edge",
&Args {
id,
source_id,
target_id,
edge_type,
metadata,
system,
created_by,
},
)
.await
}
pub async fn update_edge(
&self,
id: &str,
edge_type: &str,
metadata: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
edge_type: &'a str,
metadata: &'a str,
}
self.call_reducer("update_edge", &Args { id, edge_type, metadata })
.await
}
pub async fn delete_edge(&self, id: &str) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
id: &'a str,
}
self.call_reducer("delete_edge", &Args { id }).await
}
// =========================================================================
// NodeAccess-operasjoner
// =========================================================================
pub async fn upsert_node_access(
&self,
subject_id: &str,
object_id: &str,
access: &str,
via_edge: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
subject_id: &'a str,
object_id: &'a str,
access: &'a str,
via_edge: &'a str,
}
self.call_reducer(
"upsert_node_access",
&Args {
subject_id,
object_id,
access,
via_edge,
},
)
.await
}
pub async fn delete_node_access(
&self,
subject_id: &str,
object_id: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
subject_id: &'a str,
object_id: &'a str,
}
self.call_reducer("delete_node_access", &Args { subject_id, object_id })
.await
}
// =========================================================================
// Live-rom (LiveKit)
// =========================================================================
pub async fn create_live_room(
&self,
room_id: &str,
communication_id: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
communication_id: &'a str,
}
self.call_reducer("create_live_room", &Args { room_id, communication_id })
.await
}
pub async fn add_room_participant(
&self,
room_id: &str,
user_id: &str,
display_name: &str,
role: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
user_id: &'a str,
display_name: &'a str,
role: &'a str,
}
self.call_reducer(
"add_room_participant",
&Args { room_id, user_id, display_name, role },
)
.await
}
pub async fn remove_room_participant(
&self,
room_id: &str,
user_id: &str,
) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
user_id: &'a str,
}
self.call_reducer("remove_room_participant", &Args { room_id, user_id })
.await
}
pub async fn close_live_room(&self, room_id: &str) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Args<'a> {
room_id: &'a str,
}
self.call_reducer("close_live_room", &Args { room_id }).await
}
// =========================================================================
// Vedlikehold
// =========================================================================
/// Tøm alle noder og edges. Brukes ved warmup for å unngå duplikater.
pub async fn clear_all(&self) -> Result<(), StdbError> {
#[derive(Serialize)]
struct Empty {}
self.call_reducer("clear_all", &Empty {}).await
}
}
// =============================================================================
// Feilhåndtering
// =============================================================================
#[derive(Debug)]
pub enum StdbError {
/// HTTP-transportfeil (nettverk, timeout)
Http(String),
/// Reducer returnerte feil (400, 500, etc.)
Reducer {
reducer: String,
status: u16,
message: String,
},
}
impl std::fmt::Display for StdbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StdbError::Http(msg) => write!(f, "STDB HTTP-feil: {msg}"),
StdbError::Reducer {
reducer,
status,
message,
} => write!(f, "STDB reducer {reducer} feilet ({status}): {message}"),
}
}
}
impl std::error::Error for StdbError {}
impl From<reqwest::Error> for StdbError {
fn from(e: reqwest::Error) -> Self {
StdbError::Http(e.to_string())
}
}