1mod api;
6mod websocket;
7
8use crate::cli::commands::{Node, UiNotification};
9use anyhow::Result;
10use axum::{
11 http::{header, StatusCode, Uri},
12 response::Response,
13 routing::get,
14 Router,
15};
16use rust_embed::RustEmbed;
17use std::sync::Arc;
18use tokio::sync::{broadcast, mpsc};
19use tower_http::cors::CorsLayer;
20use tracing::info;
21use websocket::{WebSocketMessage, WebSocketState};
22
23#[derive(RustEmbed)]
25#[folder = "web-ui/dist"]
26struct Assets;
27
28pub async fn start_server(
44 node: Arc<Node>,
45 port: u16,
46 mut ui_notify_rx: mpsc::UnboundedReceiver<UiNotification>,
47) -> Result<()> {
48 let (broadcast_tx, _) = broadcast::channel::<WebSocketMessage>(100);
49
50 let ws_state = Arc::new(WebSocketState {
51 broadcast_tx: broadcast_tx.clone(),
52 });
53
54 let node_clone = node.clone();
56 tokio::spawn(async move {
57 while let Some(notification) = ui_notify_rx.recv().await {
58 match notification {
59 UiNotification::NewMessage(msg) => {
60 let other_peer = if msg.sender == node_clone.identity.peer_id {
62 &msg.recipient
63 } else {
64 &msg.sender
65 };
66
67 let content = match node_clone.friends.get_friend(other_peer).await {
68 Ok(Some(friend)) => {
69 match node_clone
70 .identity
71 .decrypt_from(&friend.e2e_public_key, &msg.content)
72 {
73 Ok(plaintext) => match String::from_utf8(plaintext) {
74 Ok(s) => s,
75 Err(_) => continue, },
77 Err(_) => continue, }
79 }
80 _ => continue, };
82
83 let ws_msg = WebSocketMessage::NewMessage {
84 id: msg.id.to_string(),
85 sender: msg.sender.to_string(),
86 recipient: msg.recipient.to_string(),
87 content,
88 timestamp: msg.timestamp,
89 nonce: msg.nonce,
90 delivery_status: format!("{:?}", msg.delivery_status),
91 };
92 let _ = broadcast_tx.send(ws_msg);
93 }
94 UiNotification::PeerConnected(peer_id) => {
95 let ws_msg = WebSocketMessage::PeerConnected {
96 peer_id: peer_id.to_string(),
97 };
98 let _ = broadcast_tx.send(ws_msg);
99 }
100 UiNotification::PeerDisconnected(peer_id) => {
101 let ws_msg = WebSocketMessage::PeerDisconnected {
102 peer_id: peer_id.to_string(),
103 };
104 let _ = broadcast_tx.send(ws_msg);
105 }
106 UiNotification::DeliveryStatusUpdate { message_id, new_status } => {
107 let ws_msg = WebSocketMessage::DeliveryStatusUpdate {
108 message_id: message_id.to_string(),
109 new_status: format!("{:?}", new_status),
110 };
111 let _ = broadcast_tx.send(ws_msg);
112 }
113 }
114 }
115 });
116
117 let api_router = Router::new()
118 .route("/api/me", get(api::get_me))
119 .route("/api/friends", get(api::list_friends).post(api::add_friend))
120 .route("/api/conversations", get(api::list_conversations))
121 .route("/api/conversations/:peer_id/messages", get(api::get_messages))
122 .route("/api/conversations/:peer_id/messages", axum::routing::post(api::send_message))
123 .route("/api/messages/:msg_id/read", axum::routing::post(api::mark_message_read))
124 .route("/api/peers/online", get(api::get_online_peers))
125 .route("/api/system/status", get(api::get_system_status))
126 .with_state(node);
127
128 let ws_router = Router::new()
129 .route("/ws", get(websocket::ws_handler))
130 .with_state(ws_state);
131
132 let app = Router::new()
133 .merge(api_router)
134 .merge(ws_router)
135 .fallback(static_handler)
136 .layer(CorsLayer::permissive());
137
138 let addr = format!("127.0.0.1:{}", port);
139 let listener = tokio::net::TcpListener::bind(&addr).await?;
140
141 info!("Web server listening on http://{}", addr);
142
143 axum::serve(listener, app).await?;
144
145 Ok(())
146}
147
148async fn static_handler(uri: Uri) -> Response {
162 let path = uri.path().trim_start_matches('/');
163
164 if path.is_empty() || path == "index.html" {
165 return serve_file("index.html");
166 }
167
168 if Assets::get(path).is_some() {
170 serve_file(path)
171 } else {
172 serve_file("index.html")
173 }
174}
175
176fn serve_file(path: &str) -> Response {
186 match Assets::get(path) {
187 Some(content) => {
188 let mime = mime_guess::from_path(path).first_or_octet_stream();
189 let body = content.data.into_owned();
190
191 Response::builder()
192 .status(StatusCode::OK)
193 .header(header::CONTENT_TYPE, mime.as_ref())
194 .body(body.into())
195 .unwrap()
196 }
197 None => {
198 Response::builder()
199 .status(StatusCode::NOT_FOUND)
200 .header(header::CONTENT_TYPE, "text/plain")
201 .body("404 Not Found".into())
202 .unwrap()
203 }
204 }
205}