communitas_core/
linking_service.rs1use crate::entity_service::{Entity, EntityService, EntityServiceError};
20use crate::gossip::contact_storage::{
21 ContactRecord, ContactResult, ContactStorageError, ContactStore,
22};
23use crate::identity::validate_id_words;
24use crate::security::input_validation::InputValidator;
25use std::sync::Arc;
26use thiserror::Error;
27use tokio::sync::RwLock;
28
29#[derive(Debug, Error)]
31pub enum LinkingError {
32 #[error("Invalid four-word address: {0}")]
33 InvalidFourWords(String),
34
35 #[error("Entity not found: {0}")]
36 EntityNotFound(String),
37
38 #[error("Contact not found: {0}")]
39 ContactNotFound(String),
40
41 #[error("Entity service error: {0}")]
42 EntityServiceError(#[from] EntityServiceError),
43
44 #[error("Contact storage error: {0}")]
45 ContactStorageError(#[from] ContactStorageError),
46
47 #[error("Already linked: {0}")]
48 AlreadyLinked(String),
49
50 #[error("Validation error: {0}")]
51 ValidationError(String),
52}
53
54pub type LinkingResult<T> = Result<T, LinkingError>;
55
56#[derive(Debug, Clone)]
58pub struct SyncResult {
59 pub success: bool,
61 pub id: String,
63 pub changes_pushed: usize,
65 pub changes_pulled: usize,
67 pub error: Option<String>,
69}
70
71impl SyncResult {
72 pub fn success(id: String, pushed: usize, pulled: usize) -> Self {
74 Self {
75 success: true,
76 id,
77 changes_pushed: pushed,
78 changes_pulled: pulled,
79 error: None,
80 }
81 }
82
83 pub fn failure(id: String, error: String) -> Self {
85 Self {
86 success: false,
87 id,
88 changes_pushed: 0,
89 changes_pulled: 0,
90 error: Some(error),
91 }
92 }
93}
94
95pub struct LinkingService {
97 entity_service: Arc<RwLock<EntityService>>,
98 contact_store: Arc<ContactStore>,
99 validator: InputValidator,
100}
101
102impl LinkingService {
103 pub fn new(
105 entity_service: Arc<RwLock<EntityService>>,
106 contact_store: Arc<ContactStore>,
107 ) -> Self {
108 Self {
109 entity_service,
110 contact_store,
111 validator: InputValidator::new(),
112 }
113 }
114
115 pub fn validate_four_words(&self, four_words: &str) -> LinkingResult<String> {
123 let normalized = self
125 .validator
126 .validate_four_words(four_words)
127 .map_err(|e| LinkingError::ValidationError(e.to_string()))?;
128
129 if !validate_id_words(&normalized) {
131 return Err(LinkingError::InvalidFourWords(format!(
132 "'{}' contains words not in dictionary",
133 normalized
134 )));
135 }
136
137 Ok(normalized)
138 }
139
140 pub fn is_valid_four_words(&self, four_words: &str) -> bool {
142 self.validate_four_words(four_words).is_ok()
143 }
144
145 pub async fn link_entity(&self, entity_id: &str, four_words: &str) -> LinkingResult<Entity> {
154 let normalized = self.validate_four_words(four_words)?;
156
157 let entity_service = self.entity_service.write().await;
159 let entity = entity_service
160 .link_entity_to_network(entity_id, &normalized)
161 .await?;
162
163 Ok(entity)
164 }
165
166 pub async fn link_contact(
175 &self,
176 contact_id: &str,
177 four_words: &str,
178 ) -> LinkingResult<ContactRecord> {
179 let normalized = self.validate_four_words(four_words)?;
181
182 let contact = self
184 .contact_store
185 .link_contact(contact_id, &normalized)
186 .await?;
187
188 Ok(contact)
189 }
190
191 pub async fn get_local_only_entities(&self) -> LinkingResult<Vec<Entity>> {
193 let entity_service = self.entity_service.read().await;
194 let all_entities = entity_service.list_entities().await?;
195
196 Ok(all_entities
197 .into_iter()
198 .filter(|e| e.is_local_only)
199 .collect())
200 }
201
202 pub async fn get_linked_entities(&self) -> LinkingResult<Vec<Entity>> {
204 let entity_service = self.entity_service.read().await;
205 let all_entities = entity_service.list_entities().await?;
206
207 Ok(all_entities.into_iter().filter(|e| e.is_linked()).collect())
208 }
209
210 pub async fn get_local_only_contacts(&self) -> Vec<ContactRecord> {
212 self.contact_store.local_only().await
213 }
214
215 pub async fn get_linked_contacts(&self) -> Vec<ContactRecord> {
217 self.contact_store.network_linked().await
218 }
219
220 pub async fn mark_entity_synced(&self, entity_id: &str) -> LinkingResult<Entity> {
222 let entity_service = self.entity_service.write().await;
223 let entity = entity_service.mark_entity_synced(entity_id).await?;
224 Ok(entity)
225 }
226
227 pub async fn mark_contact_synced(&self, contact_id: &str) -> LinkingResult<ContactRecord> {
229 let contact = self
230 .contact_store
231 .get_by_id(contact_id)
232 .await
233 .ok_or_else(|| LinkingError::ContactNotFound(contact_id.to_string()))?;
234
235 let mut updated = contact;
237 updated.mark_synced();
238
239 let contacts = self.contact_store.all().await;
243 self.contact_store.clear().await;
244 for c in contacts {
245 if c.id == contact_id {
246 let _ = self.contact_store.add(updated.clone()).await;
247 } else {
248 let _ = self.contact_store.add(c).await;
249 }
250 }
251
252 Ok(updated)
253 }
254
255 pub async fn create_local_entity(
257 &self,
258 name: String,
259 entity_type: crate::legacy_crdt::EntityType,
260 description: Option<String>,
261 created_by: String,
262 ) -> LinkingResult<Entity> {
263 let entity_service = self.entity_service.write().await;
264 let entity = entity_service
265 .create_local_entity(name, entity_type, description, created_by)
266 .await?;
267 Ok(entity)
268 }
269
270 pub async fn create_local_contact(&self, display_name: String) -> ContactResult<ContactRecord> {
272 let contact = ContactRecord::new_local(display_name);
273 self.contact_store.add(contact.clone()).await?;
274 Ok(contact)
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use super::*;
281 use crate::crdt_manager::CrdtManager;
282 use crate::legacy_crdt::EntityType;
283 use tempfile::TempDir;
284
285 async fn create_test_service() -> (LinkingService, TempDir) {
286 let temp_dir = TempDir::new().unwrap();
287 let data_dir = temp_dir.path().to_path_buf();
288
289 let crdt_manager = Arc::new(CrdtManager::new(data_dir.clone()).await.unwrap());
290 let entity_service = Arc::new(RwLock::new(EntityService::new(crdt_manager)));
291 let contact_store = Arc::new(ContactStore::new());
292
293 let service = LinkingService::new(entity_service, contact_store);
294 (service, temp_dir)
295 }
296
297 async fn create_simple_test_service() -> LinkingService {
298 let temp_dir = TempDir::new().unwrap();
299 let data_dir = temp_dir.path().to_path_buf();
300
301 let crdt_manager = Arc::new(CrdtManager::new(data_dir).await.unwrap());
302 let entity_service = Arc::new(RwLock::new(EntityService::new(crdt_manager)));
303 let contact_store = Arc::new(ContactStore::new());
304
305 LinkingService::new(entity_service, contact_store)
306 }
307
308 #[tokio::test]
309 async fn test_validate_four_words_format() {
310 let service = create_simple_test_service().await;
311
312 let _result = service.validate_four_words("hello-world-test-network");
314 assert!(
317 service
318 .validator
319 .validate_four_words("hello-world-test-network")
320 .is_ok()
321 );
322
323 assert!(service.validate_four_words("only-three-words").is_err());
325 assert!(service.validate_four_words("").is_err());
326 assert!(
327 service
328 .validate_four_words("too-many-words-here-now")
329 .is_err()
330 );
331 }
332
333 #[tokio::test]
334 async fn test_create_local_contact() {
335 let (service, _temp_dir) = create_test_service().await;
336
337 let contact = service
338 .create_local_contact("Alice".to_string())
339 .await
340 .unwrap();
341
342 assert!(contact.is_local_only);
343 assert!(contact.four_words.is_none());
344 assert_eq!(contact.display_name, Some("Alice".to_string()));
345 assert_eq!(contact.effective_name(), "Alice");
346 }
347
348 #[tokio::test]
349 async fn test_get_local_only_contacts() {
350 let (service, _temp_dir) = create_test_service().await;
351
352 service
354 .create_local_contact("Alice".to_string())
355 .await
356 .unwrap();
357 service
358 .create_local_contact("Bob".to_string())
359 .await
360 .unwrap();
361
362 let linked = ContactRecord::new("ocean-forest-moon-star".to_string());
364 service.contact_store.add(linked).await.unwrap();
365
366 let local_only = service.get_local_only_contacts().await;
368 assert_eq!(local_only.len(), 2);
369
370 let linked = service.get_linked_contacts().await;
372 assert_eq!(linked.len(), 1);
373 }
374
375 #[tokio::test]
376 async fn test_create_local_entity() {
377 let (service, _temp_dir) = create_test_service().await;
378
379 let entity = service
380 .create_local_entity(
381 "Test Org".to_string(),
382 EntityType::Organisation,
383 Some("A test organisation".to_string()),
384 "creator-id".to_string(),
385 )
386 .await
387 .unwrap();
388
389 assert!(entity.is_local_only);
390 assert!(entity.network_four_words.is_none());
391 assert_eq!(entity.name, "Test Org");
392 assert!(!entity.is_linked());
393 }
394}