Skip to content

Commit 81a87aa

Browse files
committed
feat:
1 parent 3b4e585 commit 81a87aa

3 files changed

Lines changed: 96 additions & 126 deletions

File tree

src/cli.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ pub enum Commands {
181181
},
182182
/// Get direct messages sent to any trade keys
183183
GetDmUser {
184+
/// Pubkey of the user to get direct messages from
185+
#[arg(short, long)]
186+
pubkey: String,
187+
/// Order id to get the trade keys from
188+
#[arg(short, long)]
189+
order_id: Uuid,
184190
/// Since time of the messages in minutes
185191
#[arg(short, long)]
186192
#[clap(default_value_t = 30)]
@@ -565,7 +571,11 @@ impl Commands {
565571
Commands::GetDm { since, from_user } => {
566572
execute_get_dm(since, false, from_user, ctx).await
567573
}
568-
Commands::GetDmUser { since } => execute_get_dm_user(since, ctx).await,
574+
Commands::GetDmUser {
575+
pubkey,
576+
order_id,
577+
since,
578+
} => execute_get_dm_user(PublicKey::from_str(pubkey)?, *order_id, since, ctx).await,
569579
Commands::GetAdminDm { since, from_user } => {
570580
execute_get_dm(since, true, from_user, ctx).await
571581
}

src/cli/get_dm_user.rs

Lines changed: 80 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -3,142 +3,102 @@ use crate::db::Order;
33
use crate::parser::common::{
44
print_info_line, print_key_value, print_no_data_message, print_section_header,
55
};
6-
use crate::parser::dms::print_direct_messages;
76
use crate::util::messaging::{derive_shared_key_bytes, fetch_gift_wraps_for_shared_key};
8-
use crate::util::{fetch_events_list, Event, ListKind};
97
use anyhow::Result;
10-
use mostro_core::prelude::*;
118
use nostr_sdk::prelude::*;
9+
use uuid::Uuid;
1210

13-
pub async fn execute_get_dm_user(since: &i64, ctx: &Context) -> Result<()> {
14-
// Get all trade keys from orders
15-
let mut trade_keys_hex = Order::get_all_trade_keys(&ctx.pool).await?;
16-
17-
// Include Mostro pubkey so we also fetch messages addressed to Mostro
18-
let admin_pubkey_hex = ctx.mostro_pubkey.to_hex();
19-
if !trade_keys_hex.iter().any(|k| k == &admin_pubkey_hex) {
20-
trade_keys_hex.push(admin_pubkey_hex);
21-
}
22-
// De-duplicate any repeated keys coming from DB/admin
23-
trade_keys_hex.sort();
24-
trade_keys_hex.dedup();
25-
26-
// Check if the trade keys are empty
27-
if trade_keys_hex.is_empty() {
28-
print_no_data_message("No trade keys found in orders");
29-
return Ok(());
30-
}
31-
11+
/// Fetch user-to-user chat messages over a shared conversation key.
12+
///
13+
/// CLI parameters:
14+
/// - `pubkey`: counterparty pubkey
15+
/// - `order_id`: order used to look up the trade keys
16+
/// - `since`: minutes back in time to include
17+
pub async fn execute_get_dm_user(
18+
pubkey: PublicKey,
19+
order_id: Uuid,
20+
since: &i64,
21+
ctx: &Context,
22+
) -> Result<()> {
3223
print_section_header("📨 Fetch User Direct Messages");
33-
print_key_value(
34-
"🔍",
35-
"Searching for DMs in trade keys",
36-
&format!("{}", trade_keys_hex.len()),
37-
);
24+
print_key_value("👥", "Counterparty", &pubkey.to_string());
25+
print_key_value("📋", "Order ID", &order_id.to_string());
3826
print_key_value("⏰", "Since", &format!("{} minutes ago", since));
39-
print_info_line("💡", "Fetching direct messages...");
27+
print_info_line("💡", "Fetching shared-key chat messages...");
4028
println!();
4129

42-
let direct_messages = fetch_events_list(
43-
ListKind::DirectMessagesUser,
44-
None,
45-
None,
46-
None,
47-
ctx,
48-
Some(since),
49-
)
50-
.await?;
30+
// 1. Get the order and its trade keys
31+
let order = Order::get_by_id(&ctx.pool, &order_id.to_string())
32+
.await
33+
.map_err(|e| anyhow::anyhow!("Failed to load order {order_id}: {e}"))?;
5134

52-
// Extract (Message, u64, PublicKey) tuples from Event::MessageTuple variants (classic DMs)
53-
let mut dm_events: Vec<(Message, u64, PublicKey)> = Vec::new();
54-
for event in direct_messages {
55-
if let Event::MessageTuple(tuple) = event {
56-
dm_events.push(*tuple);
57-
}
35+
let trade_keys_str = order
36+
.trade_keys
37+
.clone()
38+
.ok_or_else(|| anyhow::anyhow!("Missing trade keys for order {order_id}"))?;
39+
let trade_keys =
40+
Keys::parse(&trade_keys_str).map_err(|e| anyhow::anyhow!("Invalid trade keys: {e}"))?;
41+
42+
// 2. Derive the shared conversation key (trade private key + counterparty pubkey)
43+
let shared_key_bytes = derive_shared_key_bytes(&trade_keys, &pubkey).map_err(|e| {
44+
log::warn!(
45+
"get_dm_user: could not derive shared key (trade + counterparty): {}",
46+
e
47+
);
48+
anyhow::anyhow!("Could not derive shared key for chat with counterparty")
49+
})?;
50+
51+
let shared_keys = SecretKey::from_slice(&shared_key_bytes)
52+
.map(Keys::new)
53+
.map_err(|e| anyhow::anyhow!("Could not build Keys from shared key: {e}"))?;
54+
55+
// 3. Fetch all gift wraps addressed to this shared key and decrypt them
56+
let mut messages = fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys).await?;
57+
58+
// 4. Apply "since" filter (minutes back from now)
59+
if *since > 0 {
60+
let cutoff_ts = chrono::Utc::now()
61+
.checked_sub_signed(chrono::Duration::minutes(*since))
62+
.unwrap()
63+
.timestamp();
64+
messages.retain(|(_, ts, _)| (*ts as i64) >= cutoff_ts);
5865
}
5966

60-
// Also fetch and decrypt shared-key custom wraps (shared key = trade_keys + identity_keys)
61-
let trade_keys_hex_list = Order::get_all_trade_keys(&ctx.pool).await?;
62-
let identity_pubkey = ctx.identity_keys.public_key();
63-
for trade_hex in trade_keys_hex_list {
64-
let trade_keys = match Keys::parse(&trade_hex) {
65-
Ok(k) => k,
66-
Err(e) => {
67-
log::warn!("get_dm_user: could not parse trade_keys: {}", e);
68-
continue;
69-
}
70-
};
71-
let shared_key = match derive_shared_key_bytes(&trade_keys, &identity_pubkey) {
72-
Ok(b) => b,
73-
Err(e) => {
74-
log::warn!(
75-
"get_dm_user: could not derive shared key (trade + identity): {}",
76-
e
77-
);
78-
continue;
79-
}
80-
};
81-
let shared_keys = match SecretKey::from_slice(&shared_key) {
82-
Ok(sk) => Keys::new(sk),
83-
Err(e) => {
84-
log::warn!("get_dm_user: could not build Keys from shared key: {}", e);
85-
continue;
86-
}
87-
};
88-
let shared_msgs = match fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys).await {
89-
Ok(m) => m,
90-
Err(e) => {
91-
log::warn!(
92-
"get_dm_user: failed to fetch gift wraps for shared key: {}",
93-
e
94-
);
95-
continue;
96-
}
97-
};
98-
for (content, ts, sender_pubkey) in shared_msgs {
99-
let parsed: (Message, Option<String>) = match serde_json::from_str(&content) {
100-
Ok(m) => m,
101-
Err(e) => {
102-
log::warn!("get_dm_user: could not parse shared-key DM content: {}", e);
103-
continue;
104-
}
105-
};
106-
dm_events.push((parsed.0, ts as u64, sender_pubkey));
107-
}
67+
// 5. Keep only messages sent by the counterparty (not our own side)
68+
messages.retain(|(_, _, sender_pk)| *sender_pk == pubkey);
69+
70+
if messages.is_empty() {
71+
print_no_data_message("📭 No chat messages found for this shared conversation key.");
72+
return Ok(());
73+
}
10874

109-
// Also fetch shared-key wraps for (trade_keys + mostro_pubkey) so we see admin replies
110-
// (send_admin_dm_attach uses that derivation when we send to admin; admin uses same key to reply)
111-
let shared_key_admin = match derive_shared_key_bytes(&trade_keys, &ctx.mostro_pubkey) {
112-
Ok(b) => b,
113-
Err(e) => {
114-
log::warn!(
115-
"get_dm_user: could not derive shared key (trade + mostro): {}",
116-
e
117-
);
118-
continue;
119-
}
75+
// 6. Pretty-print the messages
76+
println!("");
77+
print_section_header("💬 Shared-Key Chat Messages");
78+
79+
for (idx, (content, ts, sender_pk)) in messages.iter().enumerate() {
80+
let date = match chrono::DateTime::from_timestamp(*ts as i64, 0) {
81+
Some(dt) => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
82+
None => "Invalid timestamp".to_string(),
12083
};
121-
let shared_keys_admin = match SecretKey::from_slice(&shared_key_admin) {
122-
Ok(sk) => Keys::new(sk),
123-
Err(e) => {
124-
log::warn!("get_dm_user: could not build Keys from shared key (admin): {}", e);
125-
continue;
126-
}
84+
85+
// Mark messages from the counterparty vs our own future messages (if any)
86+
let from_label = if *sender_pk == pubkey {
87+
format!("👤 Counterparty ({sender_pk})")
88+
} else {
89+
format!("🧑 You ({sender_pk})")
12790
};
128-
if let Ok(admin_msgs) = fetch_gift_wraps_for_shared_key(&ctx.client, &shared_keys_admin).await {
129-
for (content, ts, sender_pubkey) in admin_msgs {
130-
if let Ok((parsed, _)) = serde_json::from_str::<(Message, Option<String>)>(&content) {
131-
dm_events.push((parsed, ts as u64, sender_pubkey));
132-
}
133-
}
134-
}
135-
}
13691

137-
if dm_events.is_empty() {
138-
print_no_data_message("You don't have any direct messages in your trade keys");
139-
return Ok(());
92+
println!("📄 Message {}:", idx + 1);
93+
println!("─────────────────────────────────────");
94+
println!("⏰ Time: {}", date);
95+
println!("📨 From: {}", from_label);
96+
println!("📝 Content:");
97+
for line in content.lines() {
98+
println!(" {}", line);
99+
}
100+
println!();
140101
}
141102

142-
print_direct_messages(&dm_events, Some(ctx.mostro_pubkey)).await?;
143103
Ok(())
144104
}

src/util/messaging.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,13 @@ pub fn derive_shared_key_bytes(local_keys: &Keys, other_pubkey: &PublicKey) -> R
7676
/// Build a NIP-59 gift wrap event to a recipient pubkey (e.g. shared key pubkey).
7777
/// Rumor content is Mostro protocol format: JSON of (Message, Option<String>).
7878
async fn build_custom_wrap_event(
79-
sender: &Keys,
79+
sender_keys: &Keys,
8080
recipient_pubkey: &PublicKey,
8181
message: &str,
8282
) -> Result<Event> {
8383
let inner_message = EventBuilder::text_note(message)
84-
.build(sender.public_key())
85-
.sign(sender)
84+
.build(sender_keys.public_key())
85+
.sign(sender_keys)
8686
.await?;
8787

8888
// Ephemeral key for the custom wrap
@@ -120,7 +120,7 @@ async fn build_custom_wrap_event(
120120
/// (who derive the same shared key) can fetch and decrypt the event.
121121
pub async fn send_admin_chat_message_via_shared_key(
122122
client: &Client,
123-
admin_keys: &Keys,
123+
sender_keys: &Keys,
124124
shared_keys: &Keys,
125125
content: &str,
126126
) -> Result<()> {
@@ -129,7 +129,7 @@ pub async fn send_admin_chat_message_via_shared_key(
129129
return Err(anyhow::anyhow!("Cannot send empty chat message"));
130130
}
131131
let recipient_pubkey = shared_keys.public_key();
132-
let event = build_custom_wrap_event(admin_keys, &recipient_pubkey, content).await?;
132+
let event = build_custom_wrap_event(sender_keys, &recipient_pubkey, content).await?;
133133
client.send_event(&event).await?;
134134
Ok(())
135135
}

0 commit comments

Comments
 (0)