Skip to main content

pubky_messenger/
client.rs

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/// Profile information from Pubky
12#[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/// A user that is being followed
21#[derive(Debug, Serialize, Deserialize, Clone)]
22pub struct FollowedUser {
23    pub name: Option<String>,
24    pub pubky: String,
25}
26
27/// Main client for private messaging
28pub struct PrivateMessengerClient {
29    client: pubky::Client,
30    keypair: Keypair,
31}
32
33impl PrivateMessengerClient {
34    /// Create a new client from a keypair
35    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    /// Create a new client from a recovery file
44    ///
45    /// # Parameters
46    /// - `recovery_file_bytes`: The bytes of the .pkarr recovery file
47    /// - `passphrase`: Optional passphrase to decrypt the file (defaults to empty string)
48    pub fn from_recovery_file(
49        recovery_file_bytes: &[u8],
50        passphrase: Option<&str>,
51    ) -> Result<Self> {
52        // Use provided passphrase or default to empty string
53        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    /// Create a new client from a 12-word mnemonic recovery phrase
62    ///
63    /// # Parameters
64    /// - `mnemonic_phrase`: The 12-word BIP39 mnemonic phrase
65    /// - `passphrase`: Optional passphrase for additional security (defaults to empty string)
66    /// - `language`: Optional language for the mnemonic (defaults to English)
67    pub fn from_recovery_phrase(
68        mnemonic_phrase: &str,
69        passphrase: Option<&str>,
70        language: Option<Language>,
71    ) -> Result<Self> {
72        // Use provided language or default to English
73        let lang = language.unwrap_or(Language::English);
74
75        // Use provided passphrase or default to empty string
76        let pass = passphrase.unwrap_or("");
77
78        // Parse and validate the mnemonic
79        let mnemonic = Mnemonic::parse_in(lang, mnemonic_phrase)
80            .map_err(|e| anyhow!("Invalid mnemonic phrase: {}", e))?;
81
82        // Convert to seed with passphrase
83        let seed = mnemonic.to_seed(pass);
84
85        // Take first 32 bytes as the ed25519 secret key
86        let secret_key_bytes: [u8; 32] = seed[..32]
87            .try_into()
88            .map_err(|_| anyhow!("Failed to extract secret key from seed"))?;
89
90        // Create keypair from secret key
91        let keypair = Keypair::from_secret_key(&secret_key_bytes);
92
93        Self::new(keypair)
94    }
95
96    /// Sign in to Pubky
97    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    /// Send an encrypted message to a recipient
105    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    /// Get all messages in a conversation
128    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        // Check both user's paths
133        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        // Collect URLs from both paths
139        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        // Process each message
152        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        // Sort by timestamp
176        all_messages.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
177        Ok(all_messages)
178    }
179
180    /// Get the user's own profile
181    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    /// Get followed users with their profiles
200    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        // Fetch profiles in parallel
219        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    /// Get profile for a specific user
240    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    /// Get followed users for a specific pubky
270    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        // Fetch profiles in parallel
286        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    /// Follow a user by adding them to our follow list
307    pub async fn put_follow(&self, target_pubky: &str) -> Result<()> {
308        // Get current timestamp
309        let timestamp = std::time::SystemTime::now()
310            .duration_since(std::time::UNIX_EPOCH)?
311            .as_secs();
312
313        // Create follow data with timestamp
314        let follow_data = serde_json::json!({
315            "created_at": timestamp
316        });
317
318        // Construct the follow URL
319        let follow_url = format!(
320            "pubky://{}/pub/pubky.app/follows/{}",
321            self.keypair.public_key(),
322            target_pubky
323        );
324
325        // Send PUT request with follow data
326        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    /// Unfollow a user by removing them from our follow list
340    pub async fn delete_follow(&self, target_pubky: &str) -> Result<()> {
341        // Construct the follow URL
342        let follow_url = format!(
343            "pubky://{}/pub/pubky.app/follows/{}",
344            self.keypair.public_key(),
345            target_pubky
346        );
347
348        // Send DELETE request
349        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    /// Get the public key of this client
362    pub fn public_key(&self) -> PublicKey {
363        self.keypair.public_key()
364    }
365
366    /// Get the public key as a string
367    pub fn public_key_string(&self) -> String {
368        self.keypair.public_key().to_string()
369    }
370
371    /// Delete a single message by its ID from a conversation
372    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    /// Delete multiple messages by their IDs from a conversation
391    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        // Create delete futures for all messages
399        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        // Execute all deletions in parallel
413        let results = join_all(delete_futures).await;
414
415        // Check for any failures
416        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    /// Clear all sent messages in a conversation with a specific pubky
436    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        // List all messages in the conversation
441        let urls = match self.client.list(&self_path) {
442            Ok(list_builder) => match list_builder.send().await {
443                Ok(urls) => urls,
444                Err(_) => {
445                    // No messages to clear
446                    return Ok(());
447                }
448            },
449            Err(_) => {
450                // No messages to clear
451                return Ok(());
452            }
453        };
454
455        // If no messages, return early
456        if urls.is_empty() {
457            return Ok(());
458        }
459
460        // Delete messages in smaller batches to avoid rate limiting
461        const BATCH_SIZE: usize = 5;
462        for chunk in urls.chunks(BATCH_SIZE) {
463            // Create delete futures for this batch
464            let delete_futures: Vec<_> = chunk
465                .iter()
466                .map(|url| async move { self.client.delete(url).send().await })
467                .collect();
468
469            // Execute batch deletions in parallel
470            let results = join_all(delete_futures).await;
471
472            // Check for any failures
473            for (i, result) in results.iter().enumerate() {
474                match result {
475                    Ok(response) if !response.status().is_success() => {
476                        // Retry once on rate limiting
477                        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            // Add a small delay between batches to avoid rate limiting
503            if chunk.len() == BATCH_SIZE {
504                tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
505            }
506        }
507
508        Ok(())
509    }
510}