1use anyhow::{anyhow, Result};
2use bip39::{Language, Mnemonic};
3use futures::future::join_all;
4use pkarr::{Keypair, PublicKey};
5use pubky_common::recovery_file;
6use serde::{Deserialize, Serialize};
7
8use crate::crypto::generate_conversation_path;
9use crate::message::{DecryptedMessage, PrivateMessage};
10
11#[derive(Debug, Deserialize, Serialize, Clone)]
13pub struct PubkyProfile {
14 pub name: String,
15 pub bio: Option<String>,
16 pub image: Option<String>,
17 pub status: Option<String>,
18}
19
20#[derive(Debug, Serialize, Deserialize, Clone)]
22pub struct FollowedUser {
23 pub name: Option<String>,
24 pub pubky: String,
25}
26
27pub struct PrivateMessengerClient {
29 client: pubky::Client,
30 keypair: Keypair,
31}
32
33impl PrivateMessengerClient {
34 pub fn new(keypair: Keypair) -> Result<Self> {
36 let client = pubky::Client::builder()
37 .build()
38 .map_err(|e| anyhow!("Failed to create pubky client: {}", e))?;
39
40 Ok(Self { client, keypair })
41 }
42
43 pub fn from_recovery_file(
49 recovery_file_bytes: &[u8],
50 passphrase: Option<&str>,
51 ) -> Result<Self> {
52 let pass = passphrase.unwrap_or("");
54
55 let keypair = recovery_file::decrypt_recovery_file(recovery_file_bytes, pass)
56 .map_err(|e| anyhow!("Failed to decrypt recovery file: {:?}", e))?;
57
58 Self::new(keypair)
59 }
60
61 pub fn from_recovery_phrase(
68 mnemonic_phrase: &str,
69 passphrase: Option<&str>,
70 language: Option<Language>,
71 ) -> Result<Self> {
72 let lang = language.unwrap_or(Language::English);
74
75 let pass = passphrase.unwrap_or("");
77
78 let mnemonic = Mnemonic::parse_in(lang, mnemonic_phrase)
80 .map_err(|e| anyhow!("Invalid mnemonic phrase: {}", e))?;
81
82 let seed = mnemonic.to_seed(pass);
84
85 let secret_key_bytes: [u8; 32] = seed[..32]
87 .try_into()
88 .map_err(|_| anyhow!("Failed to extract secret key from seed"))?;
89
90 let keypair = Keypair::from_secret_key(&secret_key_bytes);
92
93 Self::new(keypair)
94 }
95
96 pub async fn sign_in(&self) -> Result<pubky_common::session::Session> {
98 self.client
99 .signin(&self.keypair)
100 .await
101 .map_err(|e| anyhow!("Failed to sign in: {}", e))
102 }
103
104 pub async fn send_message(&self, recipient: &PublicKey, content: &str) -> Result<String> {
106 let message = PrivateMessage::new(&self.keypair, recipient, content)?;
107 let msg_id = PrivateMessage::generate_id();
108 let serialized = serde_json::to_string(&message)?;
109
110 let private_path = generate_conversation_path(&self.keypair, recipient)?;
111 let path = format!(
112 "pubky://{}{}{}",
113 self.keypair.public_key(),
114 private_path,
115 format!("{}.json", msg_id)
116 );
117
118 let response = self.client.put(&path).body(serialized).send().await?;
119
120 if !response.status().is_success() {
121 return Err(anyhow!("Failed to store message: {}", response.status()));
122 }
123
124 Ok(msg_id)
125 }
126
127 pub async fn get_messages(&self, other_pubky: &PublicKey) -> Result<Vec<DecryptedMessage>> {
129 let mut all_messages = Vec::new();
130 let private_path = generate_conversation_path(&self.keypair, other_pubky)?;
131
132 let self_path = format!("pubky://{}{}", self.keypair.public_key(), private_path);
134 let other_path = format!("pubky://{}{}", other_pubky, private_path);
135
136 let mut urls = Vec::new();
137
138 if let Ok(list_builder) = self.client.list(&self_path) {
140 if let Ok(self_urls) = list_builder.send().await {
141 urls.extend(self_urls);
142 }
143 }
144
145 if let Ok(list_builder) = self.client.list(&other_path) {
146 if let Ok(other_urls) = list_builder.send().await {
147 urls.extend(other_urls);
148 }
149 }
150
151 for url in urls.iter() {
153 let response = self.client.get(url).send().await?;
154 if response.status().is_success() {
155 let response_text = response.text().await?;
156
157 if let Ok(message) = serde_json::from_str::<PrivateMessage>(&response_text) {
158 if let Ok(content) = message.decrypt_content(&self.keypair, other_pubky) {
159 if let Ok(sender) = message.decrypt_sender(&self.keypair, other_pubky) {
160 let verified =
161 message.verify_signature(&content, &sender).unwrap_or(false);
162
163 all_messages.push(DecryptedMessage {
164 sender,
165 content,
166 timestamp: message.timestamp,
167 verified,
168 });
169 }
170 }
171 }
172 }
173 }
174
175 all_messages.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
177 Ok(all_messages)
178 }
179
180 pub async fn get_own_profile(&self) -> Result<Option<PubkyProfile>> {
182 let profile_url = format!(
183 "pubky://{}/pub/pubky.app/profile.json",
184 self.keypair.public_key()
185 );
186 let response = self.client.get(&profile_url).send().await?;
187
188 if response.status().is_success() {
189 let profile_data = response.text().await?;
190 match serde_json::from_str::<PubkyProfile>(&profile_data) {
191 Ok(profile) => Ok(Some(profile)),
192 Err(_) => Ok(None),
193 }
194 } else {
195 Ok(None)
196 }
197 }
198
199 pub async fn get_followed_users(&self) -> Result<Vec<FollowedUser>> {
201 let follows_url = format!(
202 "pubky://{}/pub/pubky.app/follows/",
203 self.keypair.public_key()
204 );
205 let response = self.client.get(&follows_url).send().await?;
206
207 if !response.status().is_success() {
208 return Ok(Vec::new());
209 }
210
211 let follows_response = response.text().await?;
212 let follow_urls: Vec<String> = follows_response
213 .lines()
214 .filter(|line| !line.is_empty())
215 .map(|url| url.to_string())
216 .collect();
217
218 let profile_futures: Vec<_> = follow_urls
220 .iter()
221 .map(|follow_url| {
222 let url = follow_url.clone();
223 async move { self.get_user_profile(&url).await }
224 })
225 .collect();
226
227 let results = join_all(profile_futures).await;
228
229 let mut users = Vec::new();
230 for result in results {
231 if let Ok(user) = result {
232 users.push(user);
233 }
234 }
235
236 Ok(users)
237 }
238
239 async fn get_user_profile(&self, follow_url: &str) -> Result<FollowedUser> {
241 let pubky_id = follow_url
242 .split('/')
243 .last()
244 .ok_or_else(|| anyhow!("Failed to extract pubky from URL"))?;
245
246 let profile_url = format!("pubky://{}/pub/pubky.app/profile.json", pubky_id);
247 let response = self.client.get(&profile_url).send().await?;
248
249 if response.status().is_success() {
250 let profile_data = response.text().await?;
251 match serde_json::from_str::<PubkyProfile>(&profile_data) {
252 Ok(profile) => Ok(FollowedUser {
253 name: Some(profile.name),
254 pubky: pubky_id.to_string(),
255 }),
256 Err(_) => Ok(FollowedUser {
257 name: None,
258 pubky: pubky_id.to_string(),
259 }),
260 }
261 } else {
262 Ok(FollowedUser {
263 name: None,
264 pubky: pubky_id.to_string(),
265 })
266 }
267 }
268
269 pub async fn get_followed_users_for(&self, pubky: &str) -> Result<Vec<FollowedUser>> {
271 let follows_url = format!("pubky://{}/pub/pubky.app/follows/", pubky);
272 let response = self.client.get(&follows_url).send().await?;
273
274 if !response.status().is_success() {
275 return Ok(Vec::new());
276 }
277
278 let follows_response = response.text().await?;
279 let follow_urls: Vec<String> = follows_response
280 .lines()
281 .filter(|line| !line.is_empty())
282 .map(|url| url.to_string())
283 .collect();
284
285 let profile_futures: Vec<_> = follow_urls
287 .iter()
288 .map(|follow_url| {
289 let url = follow_url.clone();
290 async move { self.get_user_profile(&url).await }
291 })
292 .collect();
293
294 let results = join_all(profile_futures).await;
295
296 let mut users = Vec::new();
297 for result in results {
298 if let Ok(user) = result {
299 users.push(user);
300 }
301 }
302
303 Ok(users)
304 }
305
306 pub async fn put_follow(&self, target_pubky: &str) -> Result<()> {
308 let timestamp = std::time::SystemTime::now()
310 .duration_since(std::time::UNIX_EPOCH)?
311 .as_secs();
312
313 let follow_data = serde_json::json!({
315 "created_at": timestamp
316 });
317
318 let follow_url = format!(
320 "pubky://{}/pub/pubky.app/follows/{}",
321 self.keypair.public_key(),
322 target_pubky
323 );
324
325 let response = self.client
327 .put(&follow_url)
328 .body(follow_data.to_string())
329 .send()
330 .await?;
331
332 if !response.status().is_success() {
333 return Err(anyhow!("Failed to create follow: {}", response.status()));
334 }
335
336 Ok(())
337 }
338
339 pub async fn delete_follow(&self, target_pubky: &str) -> Result<()> {
341 let follow_url = format!(
343 "pubky://{}/pub/pubky.app/follows/{}",
344 self.keypair.public_key(),
345 target_pubky
346 );
347
348 let response = self.client
350 .delete(&follow_url)
351 .send()
352 .await?;
353
354 if !response.status().is_success() {
355 return Err(anyhow!("Failed to delete follow: {}", response.status()));
356 }
357
358 Ok(())
359 }
360
361 pub fn public_key(&self) -> PublicKey {
363 self.keypair.public_key()
364 }
365
366 pub fn public_key_string(&self) -> String {
368 self.keypair.public_key().to_string()
369 }
370
371 pub async fn delete_message(&self, message_id: &str, other_pubky: &PublicKey) -> Result<()> {
373 let private_path = generate_conversation_path(&self.keypair, other_pubky)?;
374 let url = format!(
375 "pubky://{}{}{}",
376 self.keypair.public_key(),
377 private_path,
378 format!("{}.json", message_id)
379 );
380
381 let response = self.client.delete(&url).send().await?;
382
383 if !response.status().is_success() {
384 return Err(anyhow!("Failed to delete message: {}", response.status()));
385 }
386
387 Ok(())
388 }
389
390 pub async fn delete_messages(
392 &self,
393 message_ids: Vec<String>,
394 other_pubky: &PublicKey,
395 ) -> Result<()> {
396 let private_path = generate_conversation_path(&self.keypair, other_pubky)?;
397
398 let delete_futures: Vec<_> = message_ids
400 .iter()
401 .map(|msg_id| {
402 let url = format!(
403 "pubky://{}{}{}",
404 self.keypair.public_key(),
405 private_path,
406 format!("{}.json", msg_id)
407 );
408 async move { self.client.delete(&url).send().await }
409 })
410 .collect();
411
412 let results = join_all(delete_futures).await;
414
415 for (i, result) in results.iter().enumerate() {
417 match result {
418 Ok(response) if !response.status().is_success() => {
419 return Err(anyhow!(
420 "Failed to delete message {}: {}",
421 message_ids[i],
422 response.status()
423 ));
424 }
425 Err(e) => {
426 return Err(anyhow!("Failed to delete message {}: {}", message_ids[i], e));
427 }
428 _ => {}
429 }
430 }
431
432 Ok(())
433 }
434
435 pub async fn clear_messages(&self, other_pubky: &PublicKey) -> Result<()> {
437 let private_path = generate_conversation_path(&self.keypair, other_pubky)?;
438 let self_path = format!("pubky://{}{}", self.keypair.public_key(), private_path);
439
440 let urls = match self.client.list(&self_path) {
442 Ok(list_builder) => match list_builder.send().await {
443 Ok(urls) => urls,
444 Err(_) => {
445 return Ok(());
447 }
448 },
449 Err(_) => {
450 return Ok(());
452 }
453 };
454
455 if urls.is_empty() {
457 return Ok(());
458 }
459
460 const BATCH_SIZE: usize = 5;
462 for chunk in urls.chunks(BATCH_SIZE) {
463 let delete_futures: Vec<_> = chunk
465 .iter()
466 .map(|url| async move { self.client.delete(url).send().await })
467 .collect();
468
469 let results = join_all(delete_futures).await;
471
472 for (i, result) in results.iter().enumerate() {
474 match result {
475 Ok(response) if !response.status().is_success() => {
476 if response.status() == 429 {
478 tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
479 let retry = self.client.delete(&chunk[i]).send().await?;
480 if !retry.status().is_success() {
481 return Err(anyhow!(
482 "Failed to delete message at {} after retry: {}",
483 chunk[i],
484 retry.status()
485 ));
486 }
487 } else {
488 return Err(anyhow!(
489 "Failed to delete message at {}: {}",
490 chunk[i],
491 response.status()
492 ));
493 }
494 }
495 Err(e) => {
496 return Err(anyhow!("Failed to delete message at {}: {}", chunk[i], e));
497 }
498 _ => {}
499 }
500 }
501
502 if chunk.len() == BATCH_SIZE {
504 tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
505 }
506 }
507
508 Ok(())
509 }
510}