1use crate::cli::commands::Node;
3use crate::types::{DeliveryStatus, Friend, Message};
4use axum::{
5 extract::{Path, Query, State},
6 http::StatusCode,
7 response::IntoResponse,
8 Json,
9};
10use base64::prelude::*;
11use libp2p::PeerId;
12use serde::{Deserialize, Serialize};
13use std::str::FromStr;
14use std::sync::Arc;
15use uuid::Uuid;
16
17#[derive(Serialize)]
19pub struct IdentityResponse {
20 peer_id: String,
22 hpke_public_key: String,
24}
25
26#[derive(Serialize)]
28pub struct FriendResponse {
29 peer_id: String,
31 e2e_public_key: String,
33 nickname: Option<String>,
35 online: bool,
37}
38
39#[derive(Deserialize)]
41pub struct AddFriendRequest {
42 peer_id: String,
44 e2e_public_key: String,
46 nickname: Option<String>,
48}
49
50#[derive(Serialize)]
52pub struct MessageResponse {
53 id: String,
55 sender: String,
57 recipient: String,
59 content: String,
61 timestamp: i64,
63 nonce: u64,
65 delivery_status: String,
67}
68
69#[derive(Deserialize)]
71pub struct SendMessageRequest {
72 content: String,
74}
75
76#[derive(Deserialize)]
78pub struct GetMessagesQuery {
79 #[serde(default)]
81 mode: MessageQueryMode,
82 #[serde(default = "default_limit")]
84 limit: usize,
85 before_id: Option<String>,
87 after_id: Option<String>,
89}
90
91#[derive(Deserialize, Default)]
93#[serde(rename_all = "lowercase")]
94enum MessageQueryMode {
95 #[default]
97 Latest,
98 Before,
100 After,
102}
103
104fn default_limit() -> usize {
106 50
107}
108
109#[derive(Serialize)]
111pub struct ConversationResponse {
112 peer_id: String,
114 nickname: Option<String>,
116 last_message: Option<MessageResponse>,
118 online: bool,
120}
121
122#[axum::debug_handler]
124pub async fn get_me(State(node): State<Arc<Node>>) -> impl IntoResponse {
125 let response = IdentityResponse {
126 peer_id: node.identity.peer_id.to_string(),
127 hpke_public_key: BASE64_STANDARD.encode(node.identity.hpke_public_key()),
128 };
129 Json(response)
130}
131
132#[axum::debug_handler]
134pub async fn list_friends(State(node): State<Arc<Node>>) -> impl IntoResponse {
135 match node.friends.list_friends().await {
136 Ok(friends) => {
137 let online_peers = node
138 .network
139 .get_connected_peers()
140 .await
141 .unwrap_or_default();
142
143 let response: Vec<FriendResponse> = friends
144 .into_iter()
145 .map(|f| FriendResponse {
146 online: online_peers.contains(&f.peer_id),
147 peer_id: f.peer_id.to_string(),
148 e2e_public_key: BASE64_STANDARD.encode(&f.e2e_public_key),
149 nickname: f.nickname,
150 })
151 .collect();
152
153 (StatusCode::OK, Json(response)).into_response()
154 }
155 Err(e) => (
156 StatusCode::INTERNAL_SERVER_ERROR,
157 format!("Failed to list friends: {}", e),
158 )
159 .into_response(),
160 }
161}
162
163#[axum::debug_handler]
165pub async fn add_friend(
166 State(node): State<Arc<Node>>,
167 Json(req): Json<AddFriendRequest>,
168) -> impl IntoResponse {
169 let peer_id = match PeerId::from_str(&req.peer_id) {
170 Ok(id) => id,
171 Err(e) => {
172 return (
173 StatusCode::BAD_REQUEST,
174 format!("Invalid peer ID: {}", e),
175 )
176 .into_response()
177 }
178 };
179
180 let e2e_public_key = match BASE64_STANDARD.decode(&req.e2e_public_key) {
181 Ok(key) => key,
182 Err(e) => {
183 return (
184 StatusCode::BAD_REQUEST,
185 format!("Invalid public key: {}", e),
186 )
187 .into_response()
188 }
189 };
190
191 let friend = Friend {
192 peer_id,
193 e2e_public_key,
194 nickname: req.nickname,
195 };
196
197 match node.friends.add_friend(friend).await {
198 Ok(_) => (StatusCode::CREATED, "Friend added").into_response(),
199 Err(e) => (
200 StatusCode::INTERNAL_SERVER_ERROR,
201 format!("Failed to add friend: {}", e),
202 )
203 .into_response(),
204 }
205}
206
207#[axum::debug_handler]
209pub async fn list_conversations(State(node): State<Arc<Node>>) -> impl IntoResponse {
210 let friends = match node.friends.list_friends().await {
211 Ok(f) => f,
212 Err(e) => {
213 return (
214 StatusCode::INTERNAL_SERVER_ERROR,
215 format!("Failed to list friends: {}", e),
216 )
217 .into_response()
218 }
219 };
220
221 let online_peers = node
222 .network
223 .get_connected_peers()
224 .await
225 .unwrap_or_default();
226
227 let mut conversations = Vec::new();
228
229 for friend in friends {
230 let messages = node
231 .history
232 .get_history(&node.identity.peer_id, &friend.peer_id, 1)
233 .await
234 .unwrap_or_default();
235
236 let mut last_message = None;
237 if let Some(msg) = messages.last() {
238 if let Some(content) = decrypt_message_content(msg, &node).await {
239 last_message = Some(MessageResponse {
240 id: msg.id.to_string(),
241 sender: msg.sender.to_string(),
242 recipient: msg.recipient.to_string(),
243 content,
244 timestamp: msg.timestamp,
245 nonce: msg.nonce,
246 delivery_status: format!("{:?}", msg.delivery_status),
247 });
248 }
249 }
250
251 conversations.push(ConversationResponse {
252 peer_id: friend.peer_id.to_string(),
253 nickname: friend.nickname,
254 last_message,
255 online: online_peers.contains(&friend.peer_id),
256 });
257 }
258
259 conversations.sort_by(|a, b| {
261 let a_ts = a.last_message.as_ref().map(|m| m.timestamp).unwrap_or(0);
262 let b_ts = b.last_message.as_ref().map(|m| m.timestamp).unwrap_or(0);
263 b_ts.cmp(&a_ts)
264 });
265
266 Json(conversations).into_response()
267}
268
269#[axum::debug_handler]
271pub async fn get_messages(
272 State(node): State<Arc<Node>>,
273 Path(peer_id_str): Path<String>,
274 Query(query): Query<GetMessagesQuery>,
275) -> impl IntoResponse {
276 let peer_id = match PeerId::from_str(&peer_id_str) {
277 Ok(id) => id,
278 Err(e) => {
279 return (
280 StatusCode::BAD_REQUEST,
281 format!("Invalid peer ID: {}", e),
282 )
283 .into_response()
284 }
285 };
286
287 let messages_result = match query.mode {
288 MessageQueryMode::Latest => {
289 node.history
290 .get_history(&node.identity.peer_id, &peer_id, query.limit)
291 .await
292 }
293 MessageQueryMode::Before => {
294 let before_id = match &query.before_id {
295 Some(id_str) => match Uuid::from_str(id_str) {
296 Ok(id) => id,
297 Err(e) => {
298 return (
299 StatusCode::BAD_REQUEST,
300 format!("Invalid before_id: {}", e),
301 )
302 .into_response()
303 }
304 },
305 None => {
306 return (StatusCode::BAD_REQUEST, "before_id is required for mode=before")
307 .into_response()
308 }
309 };
310 node.history
311 .get_messages_before(&node.identity.peer_id, &peer_id, &before_id, query.limit)
312 .await
313 }
314 MessageQueryMode::After => {
315 let after_id = match &query.after_id {
316 Some(id_str) => match Uuid::from_str(id_str) {
317 Ok(id) => id,
318 Err(e) => {
319 return (StatusCode::BAD_REQUEST, format!("Invalid after_id: {}", e))
320 .into_response()
321 }
322 },
323 None => {
324 return (StatusCode::BAD_REQUEST, "after_id is required for mode=after")
325 .into_response()
326 }
327 };
328 node.history
329 .get_messages_after(&node.identity.peer_id, &peer_id, &after_id, query.limit)
330 .await
331 }
332 };
333
334 match messages_result {
335 Ok(messages) => {
336 let mut response = Vec::new();
337 for msg in messages.iter() {
338 if let Some(content) = decrypt_message_content(msg, &node).await {
339 response.push(MessageResponse {
340 id: msg.id.to_string(),
341 sender: msg.sender.to_string(),
342 recipient: msg.recipient.to_string(),
343 content,
344 timestamp: msg.timestamp,
345 nonce: msg.nonce,
346 delivery_status: format!("{:?}", msg.delivery_status),
347 });
348 }
349 }
350
351 Json(response).into_response()
352 }
353 Err(e) => (
354 StatusCode::INTERNAL_SERVER_ERROR,
355 format!("Failed to get messages: {}", e),
356 )
357 .into_response(),
358 }
359}
360
361#[axum::debug_handler]
363pub async fn send_message(
364 State(node): State<Arc<Node>>,
365 Path(peer_id_str): Path<String>,
366 Json(req): Json<SendMessageRequest>,
367) -> impl IntoResponse {
368 let peer_id = match PeerId::from_str(&peer_id_str) {
369 Ok(id) => id,
370 Err(e) => {
371 return (
372 StatusCode::BAD_REQUEST,
373 format!("Invalid peer ID: {}", e),
374 )
375 .into_response()
376 }
377 };
378
379 let friend = match node.friends.get_friend(&peer_id).await {
380 Ok(Some(f)) => f,
381 Ok(None) => {
382 return (StatusCode::NOT_FOUND, "Friend not found").into_response();
383 }
384 Err(e) => {
385 return (
386 StatusCode::INTERNAL_SERVER_ERROR,
387 format!("Failed to get friend: {}", e),
388 )
389 .into_response()
390 }
391 };
392
393 let encrypted_content = match node
394 .identity
395 .encrypt_for(&friend.e2e_public_key, req.content.as_bytes())
396 {
397 Ok(c) => c,
398 Err(e) => {
399 return (
400 StatusCode::INTERNAL_SERVER_ERROR,
401 format!("Failed to encrypt message: {}", e),
402 )
403 .into_response()
404 }
405 };
406
407 let message = Message {
408 id: Uuid::new_v4(),
409 sender: node.identity.peer_id,
410 recipient: peer_id,
411 timestamp: chrono::Utc::now().timestamp_millis(),
412 content: encrypted_content,
413 nonce: rand::random(),
414 delivery_status: DeliveryStatus::Sent,
415 };
416
417 if let Err(e) = node.history.store_message(message.clone()).await {
418 return (
419 StatusCode::INTERNAL_SERVER_ERROR,
420 format!("Failed to store message: {}", e),
421 )
422 .into_response();
423 }
424
425 if let Err(e) = node.outbox.add_pending(message.clone()).await {
426 return (
427 StatusCode::INTERNAL_SERVER_ERROR,
428 format!("Failed to add message to outbox: {}", e),
429 )
430 .into_response();
431 }
432
433 let network_clone = node.network.clone();
435 let msg_clone = message.clone();
436 tokio::spawn(async move {
437 if let Err(e) = network_clone.send_message(peer_id, msg_clone).await {
438 tracing::debug!("Direct send failed, will retry via sync: {}", e);
439 }
440 });
441
442 (StatusCode::OK, Json(serde_json::json!({ "id": message.id }))).into_response()
443}
444
445#[axum::debug_handler]
447pub async fn mark_message_read(
448 State(node): State<Arc<Node>>,
449 Path(msg_id_str): Path<String>,
450) -> impl IntoResponse {
451 let msg_id = match Uuid::from_str(&msg_id_str) {
452 Ok(id) => id,
453 Err(e) => {
454 return (
455 StatusCode::BAD_REQUEST,
456 format!("Invalid message ID: {}", e),
457 )
458 .into_response()
459 }
460 };
461
462 let message = match node.history.get_message_by_id(&msg_id).await {
464 Ok(Some(msg)) => msg,
465 Ok(None) => {
466 return (StatusCode::NOT_FOUND, "Message not found").into_response();
467 }
468 Err(e) => {
469 return (
470 StatusCode::INTERNAL_SERVER_ERROR,
471 format!("Database error: {}", e),
472 )
473 .into_response()
474 }
475 };
476
477 if message.recipient != node.identity.peer_id {
479 return (
480 StatusCode::BAD_REQUEST,
481 "Can only mark received messages as read",
482 )
483 .into_response();
484 }
485
486 if let Err(e) = node
488 .history
489 .update_delivery_status(&msg_id, DeliveryStatus::Read)
490 .await
491 {
492 return (
493 StatusCode::INTERNAL_SERVER_ERROR,
494 format!("Failed to update status: {}", e),
495 )
496 .into_response();
497 }
498
499 let receipt = crate::types::ReadReceipt {
501 message_id: msg_id,
502 timestamp: chrono::Utc::now().timestamp_millis(),
503 };
504
505 let read_request = crate::types::ChatRequest::ReadReceipt { receipt };
506
507 let network_clone = node.network.clone();
509 let sender = message.sender;
510 tokio::spawn(async move {
511 if let Err(e) = network_clone.send_chat_request(sender, read_request).await {
512 tracing::debug!("Failed to send read receipt: {}", e);
513 }
514 });
515
516 StatusCode::OK.into_response()
517}
518
519#[axum::debug_handler]
521pub async fn get_online_peers(State(node): State<Arc<Node>>) -> impl IntoResponse {
522 match node.network.get_connected_peers().await {
523 Ok(peers) => {
524 let peer_ids: Vec<String> = peers.iter().map(|p| p.to_string()).collect();
525 Json(peer_ids).into_response()
526 }
527 Err(e) => (
528 StatusCode::INTERNAL_SERVER_ERROR,
529 format!("Failed to get online peers: {}", e),
530 )
531 .into_response(),
532 }
533}
534
535#[derive(Serialize)]
537pub struct SystemStatus {
538 connected_peers: usize,
540 known_mailboxes: usize,
542 pending_messages: usize,
544}
545
546#[axum::debug_handler]
548pub async fn get_system_status(State(node): State<Arc<Node>>) -> impl IntoResponse {
549 let connected_peers = node
550 .network
551 .get_connected_peers()
552 .await
553 .unwrap_or_default()
554 .len();
555
556 let known_mailboxes = {
557 let sync_engine = node.sync_engine.lock().await;
558 sync_engine.get_mailbox_providers().len()
559 };
560
561 let pending_messages = node.outbox.count_pending().await.unwrap_or(0);
562
563 Json(SystemStatus {
564 connected_peers,
565 known_mailboxes,
566 pending_messages,
567 })
568 .into_response()
569}
570
571async fn decrypt_message_content(msg: &Message, node: &Node) -> Option<String> {
573 let other_peer = if msg.sender == node.identity.peer_id {
575 &msg.recipient
576 } else {
577 &msg.sender
578 };
579
580 let friend = node.friends.get_friend(other_peer).await.ok()??;
582
583 let plaintext = node
585 .identity
586 .decrypt_from(&friend.e2e_public_key, &msg.content)
587 .ok()?;
588
589 String::from_utf8(plaintext).ok()
590}