1use crate::nostr_identity::NostrIdentity;
8use crate::SignalBridgeError;
9use libsignal_protocol::{DeviceId, IdentityKey, ProtocolAddress, SessionStore};
10use rusqlite::OptionalExtension;
11use sha2::{Digest, Sha256};
12use std::sync::{Arc, Mutex};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15#[derive(Clone, Debug)]
17pub struct ContactInfo {
18 pub rdx_fingerprint: String,
20 pub nostr_pubkey: String,
22 pub user_alias: Option<String>,
24 pub has_active_session: bool,
26}
27
28pub struct ContactManager {
30 storage: Arc<Mutex<rusqlite::Connection>>,
31}
32
33impl ContactManager {
34 pub fn new(storage_connection: Arc<Mutex<rusqlite::Connection>>) -> Self {
36 Self {
37 storage: storage_connection,
38 }
39 }
40
41 pub async fn add_contact_from_identity_key(
49 &mut self,
50 identity_key: &IdentityKey,
51 ) -> Result<String, SignalBridgeError> {
52 let rdx = Self::generate_identity_fingerprint_from_key(identity_key);
53
54 let nostr_pubkey = NostrIdentity::derive_public_key_from_peer_identity(identity_key)?;
55
56 let auto_alias = format!("Unknown-{}", &rdx[4..12]);
57
58 let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
59 let conn_lock = self.storage.lock().unwrap();
60
61 conn_lock
62 .execute(
63 "INSERT OR REPLACE INTO contacts
64 (rdx_fingerprint, nostr_pubkey, user_alias, signal_identity_key, first_seen, last_updated)
65 VALUES (?1, ?2, ?3, ?4,
66 COALESCE((SELECT first_seen FROM contacts WHERE rdx_fingerprint = ?1), ?5),
67 ?5)",
68 rusqlite::params![rdx, nostr_pubkey.to_hex(), auto_alias, identity_key.serialize(), now],
69 )
70 .map_err(|e| SignalBridgeError::Storage(e.to_string()))?;
71
72 Ok(rdx)
73 }
74
75 pub async fn add_contact_from_bundle(
85 &mut self,
86 bundle_bytes: &[u8],
87 user_alias: Option<&str>,
88 _session_store: &mut impl SessionStore,
89 ) -> Result<String, SignalBridgeError> {
90 use crate::SerializablePreKeyBundle;
91
92 let bundle: SerializablePreKeyBundle = bincode::deserialize(bundle_bytes).map_err(|e| {
93 SignalBridgeError::InvalidInput(format!("Failed to deserialize bundle: {}", e))
94 })?;
95
96 let identity_key = IdentityKey::decode(&bundle.identity_key)
97 .map_err(|e| SignalBridgeError::Protocol(e.to_string()))?;
98
99 let nostr_pubkey = NostrIdentity::derive_public_key_from_peer_identity(&identity_key)?;
100
101 let rdx = Self::generate_identity_fingerprint_from_key(&identity_key);
102
103 let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
104 let conn_lock = self.storage.lock().unwrap();
105
106 conn_lock
107 .execute(
108 "INSERT OR REPLACE INTO contacts
109 (rdx_fingerprint, nostr_pubkey, user_alias, signal_identity_key, first_seen, last_updated)
110 VALUES (?1, ?2, ?3, ?4,
111 COALESCE((SELECT first_seen FROM contacts WHERE rdx_fingerprint = ?1), ?5),
112 ?5)",
113 rusqlite::params![rdx, nostr_pubkey.to_hex(), user_alias, bundle.identity_key, now],
114 )
115 .map_err(|e| SignalBridgeError::Storage(e.to_string()))?;
116
117 Ok(rdx)
118 }
119
120 pub async fn lookup_contact(
126 &mut self,
127 identifier: &str,
128 session_store: &mut impl SessionStore,
129 ) -> Result<ContactInfo, SignalBridgeError> {
130 let contact = {
131 let conn_lock = self.storage.lock().unwrap();
132
133 let mut stmt = conn_lock
134 .prepare(
135 "SELECT rdx_fingerprint, nostr_pubkey, user_alias
136 FROM contacts
137 WHERE rdx_fingerprint = ?1 OR user_alias = ?1 OR nostr_pubkey = ?1",
138 )
139 .map_err(|e| SignalBridgeError::Storage(e.to_string()))?;
140
141 stmt.query_row(rusqlite::params![identifier], |row| {
142 Ok(ContactInfo {
143 rdx_fingerprint: row.get(0)?,
144 nostr_pubkey: row.get(1)?,
145 user_alias: row.get(2)?,
146 has_active_session: false,
147 })
148 })
149 .map_err(|e| SignalBridgeError::Storage(format!("Contact not found: {}", e)))?
150 };
151
152 let address =
153 ProtocolAddress::new(contact.rdx_fingerprint.clone(), DeviceId::new(1).unwrap());
154 let has_session = session_store.load_session(&address).await?.is_some();
155
156 Ok(ContactInfo {
157 has_active_session: has_session,
158 ..contact
159 })
160 }
161
162 pub async fn assign_contact_alias(
169 &mut self,
170 identifier: &str,
171 new_alias: &str,
172 session_store: &mut impl SessionStore,
173 ) -> Result<(), SignalBridgeError> {
174 let contact = self.lookup_contact(identifier, session_store).await?;
175
176 let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
177 let conn_lock = self.storage.lock().unwrap();
178
179 let existing: Option<String> = conn_lock
181 .query_row(
182 "SELECT rdx_fingerprint FROM contacts WHERE user_alias = ?1",
183 rusqlite::params![new_alias],
184 |row| row.get(0),
185 )
186 .optional()
187 .map_err(|e| SignalBridgeError::Storage(e.to_string()))?;
188
189 if let Some(existing_rdx) = existing {
190 if existing_rdx != contact.rdx_fingerprint {
191 return Err(SignalBridgeError::InvalidInput(format!(
192 "Alias '{}' is already assigned to contact {}. Remove it first before reassigning.",
193 new_alias, existing_rdx
194 )));
195 }
196 }
198
199 let updated = conn_lock
201 .execute(
202 "UPDATE contacts SET user_alias = ?1, last_updated = ?2
203 WHERE rdx_fingerprint = ?3",
204 rusqlite::params![new_alias, now, contact.rdx_fingerprint],
205 )
206 .map_err(|e| SignalBridgeError::Storage(e.to_string()))?;
207
208 if updated == 0 {
209 return Err(SignalBridgeError::InvalidInput(format!(
210 "Contact not found: {}",
211 identifier
212 )));
213 }
214
215 Ok(())
216 }
217
218 pub async fn list_contacts(
223 &mut self,
224 session_store: &mut impl SessionStore,
225 ) -> Result<Vec<ContactInfo>, SignalBridgeError> {
226 let contacts = {
227 let conn_lock = self.storage.lock().unwrap();
228
229 let mut stmt = conn_lock
230 .prepare(
231 "SELECT rdx_fingerprint, nostr_pubkey, user_alias FROM contacts
232 ORDER BY last_updated DESC",
233 )
234 .map_err(|e| SignalBridgeError::Storage(e.to_string()))?;
235
236 let contacts_iter = stmt
237 .query_map([], |row| {
238 Ok(ContactInfo {
239 rdx_fingerprint: row.get(0)?,
240 nostr_pubkey: row.get(1)?,
241 user_alias: row.get(2)?,
242 has_active_session: false,
243 })
244 })
245 .map_err(|e| SignalBridgeError::Storage(e.to_string()))?;
246
247 contacts_iter
248 .filter_map(Result::ok)
249 .collect::<Vec<ContactInfo>>()
250 };
251
252 let mut result = Vec::new();
253 for mut contact in contacts {
254 let address =
255 ProtocolAddress::new(contact.rdx_fingerprint.clone(), DeviceId::new(1).unwrap());
256 contact.has_active_session = session_store.load_session(&address).await?.is_some();
257 result.push(contact);
258 }
259
260 Ok(result)
261 }
262
263 pub fn generate_identity_fingerprint_from_key(identity_key: &IdentityKey) -> String {
271 let mut hasher = Sha256::new();
272 hasher.update(identity_key.serialize());
273 hasher.update(b"radix-identity-fingerprint");
274 let result = hasher.finalize();
275 format!("RDX:{:x}", result)
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::keys::generate_identity_key_pair;
283
284 #[tokio::test]
285 async fn test_add_contact_from_identity_key() {
286 use crate::memory_storage::MemoryStorage;
287
288 let storage_conn = Arc::new(Mutex::new(rusqlite::Connection::open_in_memory().unwrap()));
289
290 {
291 let conn = storage_conn.lock().unwrap();
292 conn.execute(
293 "CREATE TABLE contacts (
294 rdx_fingerprint TEXT PRIMARY KEY,
295 nostr_pubkey TEXT NOT NULL,
296 user_alias TEXT,
297 signal_identity_key BLOB NOT NULL,
298 first_seen INTEGER NOT NULL,
299 last_updated INTEGER NOT NULL
300 )",
301 [],
302 )
303 .unwrap();
304 }
305
306 let mut manager = ContactManager::new(storage_conn.clone());
307 let mut session_store = MemoryStorage::new().session_store;
308
309 let peer_identity = generate_identity_key_pair().await.unwrap();
310 let peer_identity_key = peer_identity.identity_key();
311
312 let rdx_fingerprint = manager
313 .add_contact_from_identity_key(peer_identity_key)
314 .await
315 .unwrap();
316
317 assert!(
318 rdx_fingerprint.starts_with("RDX:"),
319 "RDX fingerprint should start with RDX:"
320 );
321
322 let contact = manager
323 .lookup_contact(&rdx_fingerprint, &mut session_store)
324 .await
325 .unwrap();
326
327 assert_eq!(contact.rdx_fingerprint, rdx_fingerprint);
328 assert!(
329 !contact.nostr_pubkey.is_empty(),
330 "Nostr pubkey should be derived from identity key"
331 );
332 assert!(
333 contact.user_alias.as_ref().unwrap().starts_with("Unknown-"),
334 "Auto-generated alias should start with Unknown-"
335 );
336 assert!(!contact.has_active_session, "No session should exist yet");
337
338 let contact_by_nostr = manager
339 .lookup_contact(&contact.nostr_pubkey, &mut session_store)
340 .await
341 .unwrap();
342 assert_eq!(contact_by_nostr.rdx_fingerprint, rdx_fingerprint);
343 }
344}