p2p_chat/web/
api.rs

1//! This module defines the HTTP API endpoints for the web user interface.
2use 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/// Response structure for the user's identity.
18#[derive(Serialize)]
19pub struct IdentityResponse {
20    /// The user's Peer ID.
21    peer_id: String,
22    /// The user's HPKE public key, base64 encoded.
23    hpke_public_key: String,
24}
25
26/// Response structure for a friend.
27#[derive(Serialize)]
28pub struct FriendResponse {
29    /// The friend's Peer ID.
30    peer_id: String,
31    /// The friend's E2E public key, base64 encoded.
32    e2e_public_key: String,
33    /// The friend's nickname, if set.
34    nickname: Option<String>,
35    /// Whether the friend is currently online.
36    online: bool,
37}
38
39/// Request structure for adding a new friend.
40#[derive(Deserialize)]
41pub struct AddFriendRequest {
42    /// The Peer ID of the friend to add.
43    peer_id: String,
44    /// The E2E public key of the friend, base64 encoded.
45    e2e_public_key: String,
46    /// An optional nickname for the friend.
47    nickname: Option<String>,
48}
49
50/// Response structure for a message.
51#[derive(Serialize)]
52pub struct MessageResponse {
53    /// The unique ID of the message.
54    id: String,
55    /// The sender's Peer ID.
56    sender: String,
57    /// The recipient's Peer ID.
58    recipient: String,
59    /// The message content.
60    content: String,
61    /// The timestamp of the message (milliseconds since epoch).
62    timestamp: i64,
63    /// The cryptographic nonce used for encryption.
64    nonce: u64,
65    /// The delivery status of the message.
66    delivery_status: String,
67}
68
69/// Request structure for sending a new message.
70#[derive(Deserialize)]
71pub struct SendMessageRequest {
72    /// The content of the message.
73    content: String,
74}
75
76/// Query parameters for fetching messages.
77#[derive(Deserialize)]
78pub struct GetMessagesQuery {
79    /// The mode for querying messages (latest, before, or after a specific message).
80    #[serde(default)]
81    mode: MessageQueryMode,
82    /// The maximum number of messages to retrieve.
83    #[serde(default = "default_limit")]
84    limit: usize,
85    /// The ID of the message to fetch messages before (used with `Before` mode).
86    before_id: Option<String>,
87    /// The ID of the message to fetch messages after (used with `After` mode).
88    after_id: Option<String>,
89}
90
91/// Defines the mode for querying messages.
92#[derive(Deserialize, Default)]
93#[serde(rename_all = "lowercase")]
94enum MessageQueryMode {
95    /// Fetch the latest messages.
96    #[default]
97    Latest,
98    /// Fetch messages before a specific message ID.
99    Before,
100    /// Fetch messages after a specific message ID.
101    After,
102}
103
104/// Default limit for message queries.
105fn default_limit() -> usize {
106    50
107}
108
109/// Response structure for a conversation summary.
110#[derive(Serialize)]
111pub struct ConversationResponse {
112    /// The Peer ID of the other participant in the conversation.
113    peer_id: String,
114    /// The nickname of the other participant.
115    nickname: Option<String>,
116    /// The last message in the conversation, if any.
117    last_message: Option<MessageResponse>,
118    /// Whether the other participant is currently online.
119    online: bool,
120}
121
122/// Retrieves the user's identity information.
123#[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/// Lists all friends of the user.
133#[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/// Adds a new friend to the user's friend list.
164#[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/// Lists all conversations, including the last message and online status of friends.
208#[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    // Sort by last message timestamp
260    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/// Retrieves messages for a specific conversation.
270#[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/// Sends a message to a specific peer.
362#[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    // Try direct send in background (delivery confirmation will update status)
434    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/// Marks a specific message as read.
446#[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    // Get the message to find the sender.
463    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    // Only mark as read if we're the recipient.
478    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    // Update local status to Read.
487    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    // Send read receipt to sender.
500    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    // Best effort - don't wait for result.
508    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/// Retrieves a list of currently online peers.
520#[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/// Response structure for system status.
536#[derive(Serialize)]
537pub struct SystemStatus {
538    /// The number of currently connected peers.
539    connected_peers: usize,
540    /// The number of known mailbox providers.
541    known_mailboxes: usize,
542    /// The number of messages pending delivery in the outbox.
543    pending_messages: usize,
544}
545
546/// Retrieves the current system status.
547#[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
571/// Helper function to decrypt message content for web API responses.
572async fn decrypt_message_content(msg: &Message, node: &Node) -> Option<String> {
573    // Determine which peer's public key to use for decryption.
574    let other_peer = if msg.sender == node.identity.peer_id {
575        &msg.recipient
576    } else {
577        &msg.sender
578    };
579
580    // Get the friend's public key.
581    let friend = node.friends.get_friend(other_peer).await.ok()??;
582
583    // Decrypt using the friend's public key.
584    let plaintext = node
585        .identity
586        .decrypt_from(&friend.e2e_public_key, &msg.content)
587        .ok()?;
588
589    String::from_utf8(plaintext).ok()
590}