use super::super::sse::SseEvent;
use super::super::state::AppState;
use super::super::{api_error, bad_request, not_found};
use crate as x0x;
use axum::extract::{Path, State};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use axum::Json;
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use serde::Deserialize;
use std::sync::Arc;
use x0x::logging::LogHexId;
pub(in crate::server) struct RestSubscription {
topic: String,
forwarder: tokio::task::JoinHandle<()>,
}
#[derive(Debug, Deserialize)]
pub(in crate::server) struct PublishRequest {
topic: String,
payload: String,
}
#[derive(Debug, Deserialize)]
pub(in crate::server) struct SubscribeRequest {
topic: String,
}
pub(in crate::server) async fn publish(
State(state): State<Arc<AppState>>,
Json(req): Json<PublishRequest>,
) -> impl IntoResponse {
if req.topic.is_empty() {
return bad_request("topic must not be empty");
}
let payload = match BASE64.decode(&req.payload) {
Ok(p) => p,
Err(e) => {
return bad_request(format!(
"invalid base64 in payload field: {e}. \
The payload must be base64-encoded \
(e.g., use `echo -n \"hello\" | base64`)"
));
}
};
match state.agent.publish(&req.topic, payload).await {
Ok(()) => (StatusCode::OK, Json(serde_json::json!({ "ok": true }))),
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")),
}
}
pub(in crate::server) async fn subscribe(
State(state): State<Arc<AppState>>,
Json(req): Json<SubscribeRequest>,
) -> impl IntoResponse {
match state.agent.subscribe(&req.topic).await {
Ok(sub) => {
let id = format!("{:016x}", rand::random::<u64>());
let broadcast_tx = state.broadcast_tx.clone();
let topic = req.topic.clone();
let mut recv_sub = sub;
let sub_id = id.clone();
let forwarder = tokio::spawn(async move {
while let Some(msg) = recv_sub.recv().await {
tracing::info!(
topic = %topic,
sub_id = %sub_id,
payload_len = msg.payload.len(),
"[5/6 x0xd] received from subscriber channel, broadcasting to SSE"
);
let event = SseEvent {
event_type: "message".to_string(),
data: serde_json::json!({
"subscription_id": sub_id,
"topic": topic,
"payload": BASE64.encode(&msg.payload),
"sender": msg.sender.map(|s| hex::encode(s.0)),
"verified": msg.verified,
"trust_level": msg.trust_level.map(|t| t.to_string()),
}),
};
match broadcast_tx.send(event) {
Ok(n) => tracing::info!(
topic = %topic,
receivers = n,
"[5/6 x0xd] broadcast sent to {n} SSE receivers"
),
Err(_) => tracing::warn!(
topic = %LogHexId::topic(&topic),
"[5/6 x0xd] broadcast send failed (no SSE receivers)"
),
}
}
});
let mut subs = state.subscriptions.write().await;
subs.insert(
id.clone(),
RestSubscription {
topic: req.topic.clone(),
forwarder,
},
);
(
StatusCode::OK,
Json(serde_json::json!({ "ok": true, "subscription_id": id })),
)
}
Err(e) => api_error(StatusCode::INTERNAL_SERVER_ERROR, format!("{e}")),
}
}
pub(in crate::server) async fn unsubscribe(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
) -> impl IntoResponse {
let mut subs = state.subscriptions.write().await;
if let Some(sub) = subs.remove(&id) {
sub.forwarder.abort();
tracing::info!(
sub_id = %id,
topic = %sub.topic,
"unsubscribed: forwarder aborted, gossip subscription released"
);
(StatusCode::OK, Json(serde_json::json!({ "ok": true })))
} else {
not_found("subscription not found")
}
}