p2p_chat/ui/runner/actions/commands/
history.rs1use anyhow::Result;
3use chrono::{DateTime, Local, Utc};
4use libp2p::PeerId;
5
6use crate::types::Message;
7
8use super::super::context::CommandContext;
9use super::super::resolver::resolve_peer_id;
10
11const DEFAULT_HISTORY_LIMIT: usize = 20;
12const MAX_HISTORY_LIMIT: usize = 1000;
13
14pub async fn show_history(parts: &[&str], context: &CommandContext) -> Result<()> {
27 if parts.len() < 2 || parts.len() > 3 {
28 context.emit_chat("Usage: history <peer_id_or_nickname> [message_count]");
29 return Ok(());
30 }
31
32 let peer_id = match resolve_peer_id(parts[1], context).await {
33 Ok(id) => id,
34 Err(e) => {
35 context.emit_chat(format!("❌ {}", e));
36 return Ok(());
37 }
38 };
39
40 let limit = match parse_limit(parts.get(2)) {
41 Ok(limit) => limit,
42 Err(msg) => {
43 context.emit_chat(msg);
44 return Ok(());
45 }
46 };
47
48 match context
49 .node()
50 .history
51 .get_history(&context.node().identity.peer_id, &peer_id, limit)
52 .await
53 {
54 Ok(messages) => {
55 if messages.is_empty() {
56 context.emit_chat(format!("No message history with {}", peer_id));
57 return Ok(());
58 }
59
60 let mut output = format!(
61 "Message history with {} (last {} messages):",
62 peer_id,
63 messages.len()
64 );
65
66 for msg in messages {
67 output.push_str(&format!(
68 "\n [{}] {}",
69 format_timestamp(msg.timestamp),
70 format_direction(&msg, context).await
71 ));
72
73 let content = decrypt_content(&msg, context).await;
74 output.push(' ');
75 output.push_str(&content);
76 }
77
78 context.emit_history(output);
79 }
80 Err(e) => {
81 context.emit_chat(format!("❌ Failed to get message history: {}", e));
82 }
83 }
84
85 Ok(())
86}
87
88fn parse_limit(raw: Option<&&str>) -> Result<usize, String> {
98 match raw {
99 None => Ok(DEFAULT_HISTORY_LIMIT),
100 Some(value) => match value.parse::<usize>() {
101 Ok(count) if (1..=MAX_HISTORY_LIMIT).contains(&count) => Ok(count),
102 _ => Err("❌ Message count must be between 1 and 1000".to_string()),
103 },
104 }
105}
106
107fn format_timestamp(timestamp_ms: i64) -> String {
117 DateTime::<Utc>::from_timestamp_millis(timestamp_ms)
118 .map(|dt| {
119 dt.with_timezone(&Local)
120 .format("%Y-%m-%d %H:%M:%S")
121 .to_string()
122 })
123 .unwrap_or_else(|| "Invalid timestamp".to_string())
124}
125
126async fn format_direction(msg: &Message, context: &CommandContext) -> String {
137 if msg.sender == context.node().identity.peer_id {
138 let label = lookup_peer_label(msg.recipient, context).await;
139 format!("\x1b[94mYou -> {}\x1b[0m", label)
140 } else {
141 let label = lookup_peer_label(msg.sender, context).await;
142 format!("\x1b[92m{} -> You\x1b[0m", label)
143 }
144}
145
146async fn lookup_peer_label(peer_id: PeerId, context: &CommandContext) -> String {
157 match context
158 .node()
159 .friends
160 .get_friend(&peer_id)
161 .await
162 .ok()
163 .flatten()
164 {
165 Some(friend) => friend.nickname.unwrap_or_else(|| short_peer(peer_id)),
166 None => short_peer(peer_id),
167 }
168}
169
170fn short_peer(peer_id: PeerId) -> String {
180 let peer_str = peer_id.to_string();
181 if peer_str.len() > 8 {
182 format!("{}...", &peer_str[..8])
183 } else {
184 peer_str
185 }
186}
187
188async fn decrypt_content(msg: &Message, context: &CommandContext) -> String {
199 let other_pubkey = if msg.sender == context.node().identity.peer_id {
200 context
201 .node()
202 .friends
203 .get_friend(&msg.recipient)
204 .await
205 .ok()
206 .flatten()
207 .map(|f| f.e2e_public_key)
208 } else {
209 context
210 .node()
211 .friends
212 .get_friend(&msg.sender)
213 .await
214 .ok()
215 .flatten()
216 .map(|f| f.e2e_public_key)
217 };
218
219 match other_pubkey {
220 Some(pub_key) => match context.node().identity.decrypt_from(&pub_key, &msg.content) {
221 Ok(plaintext) => String::from_utf8_lossy(&plaintext).to_string(),
222 Err(_) => "[Decryption Failed]".to_string(),
223 },
224 None => "[Cannot decrypt - unknown peer]".to_string(),
225 }
226}