p2p_chat/ui/runner/actions/commands/
send.rs

1//! This module contains the command handler for sending messages.
2use std::collections::HashSet;
3
4use anyhow::Result;
5use chrono::Utc;
6use libp2p::PeerId;
7use rand::random;
8use tracing::debug;
9use uuid::Uuid;
10
11use crate::cli::commands::MailboxDeliveryResult;
12use crate::types::{DeliveryStatus, Friend, Message};
13
14use super::super::context::CommandContext;
15use super::super::resolver::resolve_peer_id;
16
17/// Handles the 'send' command, encrypting and sending a message to a friend.
18///
19/// This function attempts direct delivery first. If direct delivery fails,
20/// it then attempts to store the message in available mailboxes.
21///
22/// Usage: `send <peer_id_or_nickname> <message...>`
23///
24/// # Arguments
25///
26/// * `parts` - A slice of strings representing the command arguments.
27/// * `context` - The `CommandContext` providing access to the application's state and network.
28///
29/// # Errors
30///
31/// This function returns an error if friend lookup, encryption, or message storage fails.
32pub async fn handle_send(parts: &[&str], context: &CommandContext) -> Result<()> {
33    if parts.len() < 3 {
34        context.emit_chat("Usage: send <peer_id_or_nickname> <message...>");
35        return Ok(());
36    }
37
38    let destination = parts[1];
39    let message_body = parts[2..].join(" ");
40
41    let recipient_peer_id = match resolve_peer_id(destination, context).await {
42        Ok(id) => id,
43        Err(e) => {
44            context.emit_chat(format!("❌ {}", e));
45            return Ok(());
46        }
47    };
48
49    let friend = match context
50        .node()
51        .friends
52        .get_friend(&recipient_peer_id)
53        .await?
54    {
55        Some(f) => f,
56        None => {
57            context.emit_chat("❌ Friend not found. Add them first with 'friend' command.");
58            return Ok(());
59        }
60    };
61
62    let encrypted_content = match context
63        .node()
64        .identity
65        .encrypt_for(&friend.e2e_public_key, message_body.as_bytes())
66    {
67        Ok(content) => content,
68        Err(e) => {
69            context.emit_chat(format!("❌ Encryption failed: {}", e));
70            return Ok(());
71        }
72    };
73
74    let message = Message {
75        id: Uuid::new_v4(),
76        sender: context.node().identity.peer_id,
77        recipient: recipient_peer_id,
78        timestamp: Utc::now().timestamp_millis(),
79        content: encrypted_content,
80        nonce: random(),
81        delivery_status: DeliveryStatus::Sending,
82    };
83
84    // Store message in history and outbox immediately
85    context
86        .node()
87        .history
88        .store_message(message.clone())
89        .await?;
90    context.node().outbox.add_pending(message.clone()).await?;
91
92    // Attempt direct delivery first
93    if attempt_direct_delivery(destination, &message, context).await? {
94        return Ok(());
95    }
96
97    // If direct delivery fails, attempt mailbox delivery
98    attempt_mailbox_delivery(destination, &message, &friend, context).await
99}
100
101/// Attempts to directly deliver a message to the recipient.
102///
103/// If the recipient is online and connected, the message is sent directly and
104/// removed from the outbox. Otherwise, it logs a message and returns `false`.
105///
106/// # Arguments
107///
108/// * `destination` - The display name or PeerId of the recipient.
109/// * `message` - The message to send.
110/// * `context` - The `CommandContext` for network interaction and chat output.
111///
112/// # Returns
113///
114/// `true` if direct delivery was successful, `false` otherwise.
115///
116/// # Errors
117///
118/// This function returns an error if removing the message from the outbox fails.
119async fn attempt_direct_delivery(
120    destination: &str,
121    message: &Message,
122    context: &CommandContext,
123) -> Result<bool> {
124    match context
125        .node()
126        .network
127        .send_message(message.recipient, message.clone())
128        .await
129    {
130        Ok(()) => {
131            context.node().outbox.remove_pending(&message.id).await?;
132            context.emit_chat(format!("✅ Message sent directly to {}", destination));
133            Ok(true)
134        }
135        Err(_) => {
136            context.emit_chat(format!(
137                "⚠️ {} is offline. Attempting mailbox delivery...",
138                destination
139            ));
140            Ok(false)
141        }
142    }
143}
144
145/// Attempts to deliver a message via mailbox providers.
146///
147/// If direct delivery fails, this function tries to find and use mailbox
148/// providers to store the message for the recipient.
149///
150/// # Arguments
151///
152/// * `destination` - The display name or PeerId of the recipient.
153/// * `message` - The message to deliver.
154/// * `friend` - The `Friend` object of the recipient.
155/// * `context` - The `CommandContext` for network interaction and chat output.
156///
157/// # Errors
158///
159/// This function returns an error if network communication or mailbox interaction fails.
160async fn attempt_mailbox_delivery(
161    destination: &str,
162    message: &Message,
163    friend: &Friend,
164    context: &CommandContext,
165) -> Result<()> {
166    let providers = {
167        let mut sync_engine = context.node().sync_engine.lock().await;
168        let current = sync_engine.get_mailbox_providers().clone();
169        if current.is_empty() {
170            debug!("No known mailboxes, triggering discovery");
171            if let Err(e) = sync_engine.discover_mailboxes().await {
172                debug!("Mailbox discovery failed: {}", e);
173            }
174            sync_engine.get_mailbox_providers().clone()
175        } else {
176            current
177        }
178    };
179
180    if !providers.is_empty() {
181        return deliver_via_mailboxes(destination, message, friend, context, providers.into_iter())
182            .await;
183    }
184
185    let emergency_set: HashSet<PeerId> = {
186        let sync_engine = context.node().sync_engine.lock().await;
187        sync_engine
188            .get_emergency_mailboxes()
189            .await
190            .into_iter()
191            .collect()
192    };
193
194    if emergency_set.is_empty() {
195        context.emit_chat(format!(
196            "⚠️ No mailboxes or connected peers available. Message queued for when {} comes online",
197            destination
198        ));
199        return Ok(());
200    }
201
202    deliver_via_mailboxes(
203        destination,
204        message,
205        friend,
206        context,
207        emergency_set.into_iter(),
208    )
209    .await
210}
211
212/// Delivers a message to a set of mailbox providers.
213///
214/// # Arguments
215///
216/// * `destination` - The display name or PeerId of the recipient.
217/// * `message` - The message to deliver.
218/// * `friend` - The `Friend` object of the recipient.
219/// * `context` - The `CommandContext` for network interaction and chat output.
220/// * `providers` - An iterator over `PeerId`s of mailbox providers.
221///
222/// # Errors
223///
224/// This function returns an error if the underlying mailbox forwarding fails.
225async fn deliver_via_mailboxes<I>(
226    destination: &str,
227    message: &Message,
228    friend: &Friend,
229    context: &CommandContext,
230    providers: I,
231) -> Result<()>
232where
233    I: IntoIterator<Item = PeerId>,
234{
235    let provider_set: HashSet<PeerId> = providers.into_iter().collect();
236    match context
237        .node()
238        .forward_to_mailboxes(message, friend, &provider_set)
239        .await
240    {
241        Ok(MailboxDeliveryResult::Success(count)) => {
242            context.node().outbox.remove_pending(&message.id).await?;
243            context.emit_chat(format!(
244                "📬 Message stored in {} network mailbox(es) for {}",
245                count, destination
246            ));
247        }
248        Ok(MailboxDeliveryResult::Failure) => {
249            context.emit_chat(format!(
250                "⚠️ Mailbox delivery failed. Message queued for retry when {} comes online",
251                destination
252            ));
253        }
254        Err(e) => {
255            context.emit_chat(format!("⚠️ Mailbox error: {}. Message queued for retry", e));
256        }
257    }
258
259    Ok(())
260}