1pub struct DirectoryService<'a> {
2 client: &'a crate::core::ImClient,
3}
4
5impl<'a> DirectoryService<'a> {
6 pub(crate) fn new(client: &'a crate::core::ImClient) -> Self {
7 Self { client }
8 }
9
10 pub fn resolve_peer(
11 &self,
12 peer: crate::ids::PeerRef,
13 ) -> crate::ImResult<super::DirectoryResolution> {
14 if peer.as_str().trim().is_empty() {
15 return Err(crate::ImError::invalid_input(
16 Some("peer".to_string()),
17 "peer must not be empty",
18 ));
19 }
20 self.resolve_peer_with_runtime(
21 peer,
22 crate::internal::transport::CoreHttpTransport::new(self.client),
23 )
24 .map(|result| result.resolution)
25 }
26
27 pub async fn resolve_peer_async(
28 &self,
29 peer: crate::ids::PeerRef,
30 ) -> crate::ImResult<super::DirectoryResolution> {
31 if peer.as_str().trim().is_empty() {
32 return Err(crate::ImError::invalid_input(
33 Some("peer".to_string()),
34 "peer must not be empty",
35 ));
36 }
37 self.resolve_peer_with_runtime_async(
38 peer,
39 crate::internal::transport::CoreHttpTransport::new(self.client),
40 )
41 .await
42 .map(|result| result.resolution)
43 }
44
45 pub(crate) fn resolve_peer_with_runtime<T>(
46 &self,
47 peer: crate::ids::PeerRef,
48 transport: T,
49 ) -> crate::ImResult<crate::internal::directory_runtime::DirectoryResolveResult>
50 where
51 T: crate::internal::transport::RpcTransport,
52 {
53 let result =
54 crate::internal::directory_runtime::DirectoryRuntime::new(self.client, transport)
55 .resolve_peer(peer)?;
56 #[cfg(feature = "sqlite")]
57 crate::internal::contact_store::projection::project_directory_resolution(
58 self.client,
59 &result.resolution,
60 );
61 Ok(result)
62 }
63
64 pub(crate) async fn resolve_peer_with_runtime_async<T>(
65 &self,
66 peer: crate::ids::PeerRef,
67 transport: T,
68 ) -> crate::ImResult<crate::internal::directory_runtime::DirectoryResolveResult>
69 where
70 T: crate::internal::transport::AsyncRpcTransport,
71 {
72 let result =
73 crate::internal::directory_runtime::DirectoryRuntime::new(self.client, transport)
74 .resolve_peer_async(peer)
75 .await?;
76 #[cfg(feature = "sqlite")]
77 crate::internal::contact_store::projection::project_directory_resolution_async(
78 self.client,
79 &result.resolution,
80 )
81 .await?;
82 Ok(result)
83 }
84
85 pub fn lookup_handle(
86 &self,
87 handle: crate::ids::Handle,
88 ) -> crate::ImResult<super::HandleLookupResult> {
89 if handle.as_str().trim().is_empty() {
90 return Err(crate::ImError::invalid_input(
91 Some("handle".to_string()),
92 "handle must not be empty",
93 ));
94 }
95 self.lookup_handle_with_runtime(
96 handle,
97 crate::internal::transport::CoreHttpTransport::new(self.client),
98 )
99 }
100
101 pub async fn lookup_handle_async(
102 &self,
103 handle: crate::ids::Handle,
104 ) -> crate::ImResult<super::HandleLookupResult> {
105 if handle.as_str().trim().is_empty() {
106 return Err(crate::ImError::invalid_input(
107 Some("handle".to_string()),
108 "handle must not be empty",
109 ));
110 }
111 self.lookup_handle_with_runtime_async(
112 handle,
113 crate::internal::transport::CoreHttpTransport::new(self.client),
114 )
115 .await
116 }
117
118 pub(crate) fn lookup_handle_with_runtime<T>(
119 &self,
120 handle: crate::ids::Handle,
121 transport: T,
122 ) -> crate::ImResult<super::HandleLookupResult>
123 where
124 T: crate::internal::transport::RpcTransport,
125 {
126 let result =
127 crate::internal::directory_runtime::DirectoryRuntime::new(self.client, transport)
128 .lookup_handle(handle)?;
129 #[cfg(feature = "sqlite")]
130 project_handle_lookup(self.client, &result);
131 Ok(result)
132 }
133
134 pub(crate) async fn lookup_handle_with_runtime_async<T>(
135 &self,
136 handle: crate::ids::Handle,
137 transport: T,
138 ) -> crate::ImResult<super::HandleLookupResult>
139 where
140 T: crate::internal::transport::AsyncRpcTransport,
141 {
142 let result =
143 crate::internal::directory_runtime::DirectoryRuntime::new(self.client, transport)
144 .lookup_handle_async(handle)
145 .await?;
146 #[cfg(feature = "sqlite")]
147 project_handle_lookup_async(self.client, &result).await?;
148 Ok(result)
149 }
150
151 pub fn public_profile(
152 &self,
153 subject: super::IdentitySubject,
154 ) -> crate::ImResult<super::PublicProfile> {
155 validate_identity_subject(&subject)?;
156 self.public_profile_with_runtime(
157 subject,
158 crate::internal::transport::CoreHttpTransport::new(self.client),
159 )
160 }
161
162 pub async fn public_profile_async(
163 &self,
164 subject: super::IdentitySubject,
165 ) -> crate::ImResult<super::PublicProfile> {
166 validate_identity_subject(&subject)?;
167 self.public_profile_with_runtime_async(
168 subject,
169 crate::internal::transport::CoreHttpTransport::new(self.client),
170 )
171 .await
172 }
173
174 pub(crate) fn public_profile_with_runtime<T>(
175 &self,
176 subject: super::IdentitySubject,
177 transport: T,
178 ) -> crate::ImResult<super::PublicProfile>
179 where
180 T: crate::internal::transport::RpcTransport,
181 {
182 let result =
183 crate::internal::directory_runtime::DirectoryRuntime::new(self.client, transport)
184 .public_profile(subject)?;
185 #[cfg(feature = "sqlite")]
186 crate::internal::contact_store::projection::project_directory_resolution(
187 self.client,
188 &crate::directory::DirectoryResolution {
189 input: result.did.as_str().to_string(),
190 did: result.did.clone(),
191 handle: result.handle.clone(),
192 profile: Some(result.profile.clone()),
193 warnings: result.warnings.clone(),
194 },
195 );
196 Ok(result)
197 }
198
199 pub(crate) async fn public_profile_with_runtime_async<T>(
200 &self,
201 subject: super::IdentitySubject,
202 transport: T,
203 ) -> crate::ImResult<super::PublicProfile>
204 where
205 T: crate::internal::transport::AsyncRpcTransport,
206 {
207 let result =
208 crate::internal::directory_runtime::DirectoryRuntime::new(self.client, transport)
209 .public_profile_async(subject)
210 .await?;
211 #[cfg(feature = "sqlite")]
212 crate::internal::contact_store::projection::project_directory_resolution_async(
213 self.client,
214 &crate::directory::DirectoryResolution {
215 input: result.did.as_str().to_string(),
216 did: result.did.clone(),
217 handle: result.handle.clone(),
218 profile: Some(result.profile.clone()),
219 warnings: result.warnings.clone(),
220 },
221 )
222 .await?;
223 Ok(result)
224 }
225
226 pub fn save_contact(
227 &self,
228 request: super::SaveContactRequest,
229 ) -> crate::ImResult<super::Contact> {
230 validate_save_contact(&request)?;
231 let (did, handle) = contact_target_from_request(self, &request)?;
232 let mut request = request;
233 if request.did.is_none() {
234 request.did = Some(did.clone());
235 }
236 if request.handle.is_none() {
237 request.handle = handle;
238 }
239 #[cfg(all(feature = "sqlite", feature = "blocking"))]
240 {
241 let mut connection = crate::internal::contact_store::open_writable(self.client)?;
242 let record = crate::internal::contact_store::projection::record_from_save_request(
243 self.client,
244 &request,
245 did.clone(),
246 )?;
247 crate::internal::contact_store::records::upsert_contact(&mut connection, record)?;
248 let record = crate::internal::contact_store::records::get_contact_by_did(
249 &connection,
250 self.owner_identity_id(),
251 self.owner_did().as_str(),
252 request
253 .did
254 .as_ref()
255 .map_or_else(|| request.peer.as_str(), crate::ids::Did::as_str),
256 )?;
257 crate::internal::contact_store::records::contact_to_dto(&record)
258 }
259 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
260 {
261 let _ = did;
262 Err(crate::ImError::unsupported("sync-directory-save-contact"))
263 }
264 #[cfg(not(feature = "sqlite"))]
265 {
266 let _ = did;
267 Err(crate::ImError::unsupported("directory-save-contact"))
268 }
269 }
270
271 pub async fn save_contact_async(
272 &self,
273 request: super::SaveContactRequest,
274 ) -> crate::ImResult<super::Contact> {
275 validate_save_contact(&request)?;
276 let (did, handle) = contact_target_from_request_async(self, &request).await?;
277 let mut request = request;
278 if request.did.is_none() {
279 request.did = Some(did.clone());
280 }
281 if request.handle.is_none() {
282 request.handle = handle;
283 }
284 #[cfg(feature = "sqlite")]
285 {
286 let record = crate::internal::contact_store::projection::record_from_save_request(
287 self.client,
288 &request,
289 did.clone(),
290 )?;
291 let db = self.client.core_inner().local_state_db().await?;
292 db.upsert_contact(record).await?;
293 let record = db
294 .get_contact_by_did(
295 self.owner_identity_id(),
296 self.owner_did().as_str(),
297 request
298 .did
299 .as_ref()
300 .map_or_else(|| request.peer.as_str(), crate::ids::Did::as_str),
301 )
302 .await?;
303 crate::internal::contact_store::records::contact_to_dto(&record)
304 }
305 #[cfg(not(feature = "sqlite"))]
306 {
307 let _ = did;
308 Err(crate::ImError::unsupported("directory-save-contact"))
309 }
310 }
311
312 pub fn contacts(
313 &self,
314 query: super::ContactListQuery,
315 ) -> crate::ImResult<crate::ids::Page<super::Contact>> {
316 if query.limit.is_some_and(|limit| limit.0 == 0) {
317 return Err(crate::ImError::invalid_input(
318 Some("limit".to_string()),
319 "limit must be greater than zero",
320 ));
321 }
322 #[cfg(all(feature = "sqlite", feature = "blocking"))]
323 {
324 let connection = crate::internal::contact_store::open_writable(self.client)?;
325 let limit = query.limit.map(|limit| i64::from(limit.0)).unwrap_or(100);
326 let contacts = crate::internal::contact_store::records::list_contacts(
327 &connection,
328 self.owner_identity_id(),
329 self.owner_did().as_str(),
330 limit,
331 )?;
332 let items = contacts
333 .iter()
334 .map(crate::internal::contact_store::records::contact_to_dto)
335 .collect::<crate::ImResult<Vec<_>>>()?;
336 Ok(crate::ids::Page {
337 items,
338 next_cursor: None,
339 has_more: false,
340 })
341 }
342 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
343 {
344 let _ = query;
345 Err(crate::ImError::unsupported("sync-directory-contacts"))
346 }
347 #[cfg(not(feature = "sqlite"))]
348 {
349 let _ = query;
350 Err(crate::ImError::unsupported("directory-contacts"))
351 }
352 }
353
354 pub async fn contacts_async(
355 &self,
356 query: super::ContactListQuery,
357 ) -> crate::ImResult<crate::ids::Page<super::Contact>> {
358 if query.limit.is_some_and(|limit| limit.0 == 0) {
359 return Err(crate::ImError::invalid_input(
360 Some("limit".to_string()),
361 "limit must be greater than zero",
362 ));
363 }
364 #[cfg(feature = "sqlite")]
365 {
366 let limit = query.limit.map(|limit| i64::from(limit.0)).unwrap_or(100);
367 let db = self.client.core_inner().local_state_db().await?;
368 let contacts = db
369 .list_contacts(self.owner_identity_id(), self.owner_did().as_str(), limit)
370 .await?;
371 let items = contacts
372 .iter()
373 .map(crate::internal::contact_store::records::contact_to_dto)
374 .collect::<crate::ImResult<Vec<_>>>()?;
375 Ok(crate::ids::Page {
376 items,
377 next_cursor: None,
378 has_more: false,
379 })
380 }
381 #[cfg(not(feature = "sqlite"))]
382 {
383 let _ = query;
384 Err(crate::ImError::unsupported("directory-contacts"))
385 }
386 }
387
388 pub fn hydrate_display_profiles(
389 &self,
390 request: super::DisplayProfileBatchRequest,
391 ) -> crate::ImResult<Vec<super::DisplayProfile>> {
392 validate_display_profile_batch_request(&request)?;
393 #[cfg(all(feature = "sqlite", feature = "blocking"))]
394 {
395 let connection = crate::internal::contact_store::open_writable(self.client)?;
396 request
397 .peers
398 .into_iter()
399 .map(|peer| {
400 let record = local_contact_for_peer(
401 |did| {
402 crate::internal::contact_store::records::get_contact_by_did(
403 &connection,
404 self.owner_identity_id(),
405 self.owner_did().as_str(),
406 did,
407 )
408 },
409 |handle| {
410 crate::internal::contact_store::records::get_current_contact_by_handle(
411 &connection,
412 self.owner_identity_id(),
413 self.owner_did().as_str(),
414 handle,
415 )
416 },
417 &peer,
418 );
419 display_profile_from_local_result(peer, record)
420 })
421 .collect()
422 }
423 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
424 {
425 let _ = request;
426 Err(crate::ImError::unsupported(
427 "sync-directory-hydrate-display-profiles",
428 ))
429 }
430 #[cfg(not(feature = "sqlite"))]
431 {
432 let _ = request;
433 Err(crate::ImError::unsupported(
434 "directory-hydrate-display-profiles",
435 ))
436 }
437 }
438
439 pub async fn hydrate_display_profiles_async(
440 &self,
441 request: super::DisplayProfileBatchRequest,
442 ) -> crate::ImResult<Vec<super::DisplayProfile>> {
443 validate_display_profile_batch_request(&request)?;
444 #[cfg(feature = "sqlite")]
445 {
446 let db = self.client.core_inner().local_state_db().await?;
447 let mut result = Vec::with_capacity(request.peers.len());
448 for peer in request.peers {
449 let record = if peer.as_str().trim().starts_with("did:") {
450 db.get_contact_by_did(
451 self.owner_identity_id(),
452 self.owner_did().as_str(),
453 peer.as_str(),
454 )
455 .await
456 } else {
457 db.get_current_contact_by_handle(
458 self.owner_identity_id(),
459 self.owner_did().as_str(),
460 peer.as_str(),
461 )
462 .await
463 };
464 result.push(display_profile_from_local_result(peer, record)?);
465 }
466 Ok(result)
467 }
468 #[cfg(not(feature = "sqlite"))]
469 {
470 let _ = request;
471 Err(crate::ImError::unsupported(
472 "directory-hydrate-display-profiles",
473 ))
474 }
475 }
476
477 pub fn relation_status(
478 &self,
479 peer: crate::ids::PeerRef,
480 ) -> crate::ImResult<super::RelationStatus> {
481 if peer.as_str().trim().is_empty() {
482 return Err(crate::ImError::invalid_input(
483 Some("peer".to_string()),
484 "peer must not be empty",
485 ));
486 }
487 #[cfg(all(feature = "sqlite", feature = "blocking"))]
488 {
489 let connection = crate::internal::contact_store::open_writable(self.client)?;
490 let record = if peer.as_str().trim().starts_with("did:") {
491 crate::internal::contact_store::records::get_contact_by_did(
492 &connection,
493 self.owner_identity_id(),
494 self.owner_did().as_str(),
495 peer.as_str(),
496 )
497 .ok()
498 } else {
499 crate::internal::contact_store::records::get_current_contact_by_handle(
500 &connection,
501 self.owner_identity_id(),
502 self.owner_did().as_str(),
503 peer.as_str(),
504 )
505 .ok()
506 };
507 crate::internal::contact_store::records::relation_status_from_record(peer, record)
508 }
509 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
510 {
511 let _ = peer;
512 Err(crate::ImError::unsupported(
513 "sync-directory-relation-status",
514 ))
515 }
516 #[cfg(not(feature = "sqlite"))]
517 {
518 let _ = peer;
519 Err(crate::ImError::unsupported("directory-relation-status"))
520 }
521 }
522
523 pub async fn relation_status_async(
524 &self,
525 peer: crate::ids::PeerRef,
526 ) -> crate::ImResult<super::RelationStatus> {
527 if peer.as_str().trim().is_empty() {
528 return Err(crate::ImError::invalid_input(
529 Some("peer".to_string()),
530 "peer must not be empty",
531 ));
532 }
533 #[cfg(feature = "sqlite")]
534 {
535 let db = self.client.core_inner().local_state_db().await?;
536 let record = if peer.as_str().trim().starts_with("did:") {
537 db.get_contact_by_did(
538 self.owner_identity_id(),
539 self.owner_did().as_str(),
540 peer.as_str(),
541 )
542 .await
543 .ok()
544 } else {
545 db.get_current_contact_by_handle(
546 self.owner_identity_id(),
547 self.owner_did().as_str(),
548 peer.as_str(),
549 )
550 .await
551 .ok()
552 };
553 crate::internal::contact_store::records::relation_status_from_record(peer, record)
554 }
555 #[cfg(not(feature = "sqlite"))]
556 {
557 let _ = peer;
558 Err(crate::ImError::unsupported("directory-relation-status"))
559 }
560 }
561
562 pub fn owner_did(&self) -> &crate::ids::Did {
563 self.client.did()
564 }
565
566 pub fn follow(&self, request: super::FollowRequest) -> crate::ImResult<super::FollowResult> {
567 validate_peer(request.peer.as_str())?;
568 validate_not_self_peer(self.client, &request.peer)?;
569 #[cfg(not(feature = "blocking"))]
570 {
571 let _ = request;
572 Err(crate::ImError::unsupported("sync-directory-follow"))
573 }
574 #[cfg(feature = "blocking")]
575 self.follow_with_runtime(
576 request,
577 crate::internal::auth::session::FileSessionProvider::new(self.client),
578 crate::internal::transport::CoreHttpTransport::new(self.client),
579 )
580 }
581
582 pub async fn follow_async(
583 &self,
584 request: super::FollowRequest,
585 ) -> crate::ImResult<super::FollowResult> {
586 validate_peer(request.peer.as_str())?;
587 self.follow_with_runtime_async(
588 request,
589 crate::internal::auth::session::FileSessionProvider::new(self.client),
590 crate::internal::transport::CoreHttpTransport::new(self.client),
591 )
592 .await
593 }
594
595 pub(crate) fn follow_with_runtime<P, T>(
596 &self,
597 request: super::FollowRequest,
598 session_provider: P,
599 transport: T,
600 ) -> crate::ImResult<super::FollowResult>
601 where
602 P: crate::internal::auth::session::SessionProvider,
603 T: crate::internal::transport::AuthenticatedRpcTransport
604 + crate::internal::transport::RpcTransport,
605 {
606 crate::internal::relationship_runtime::RelationshipRuntime::new(
607 self.client,
608 session_provider,
609 transport,
610 )
611 .follow(request)
612 }
613
614 pub(crate) async fn follow_with_runtime_async<P, T>(
615 &self,
616 request: super::FollowRequest,
617 session_provider: P,
618 transport: T,
619 ) -> crate::ImResult<super::FollowResult>
620 where
621 P: crate::internal::auth::session::AsyncSessionProvider,
622 T: crate::internal::transport::AsyncAuthenticatedRpcTransport
623 + crate::internal::transport::AsyncRpcTransport,
624 {
625 crate::internal::relationship_runtime::RelationshipRuntime::new(
626 self.client,
627 session_provider,
628 transport,
629 )
630 .follow_async(request)
631 .await
632 }
633
634 pub fn unfollow(
635 &self,
636 request: super::UnfollowRequest,
637 ) -> crate::ImResult<super::UnfollowResult> {
638 validate_peer(request.peer.as_str())?;
639 validate_not_self_peer(self.client, &request.peer)?;
640 #[cfg(not(feature = "blocking"))]
641 {
642 let _ = request;
643 Err(crate::ImError::unsupported("sync-directory-unfollow"))
644 }
645 #[cfg(feature = "blocking")]
646 self.unfollow_with_runtime(
647 request,
648 crate::internal::auth::session::FileSessionProvider::new(self.client),
649 crate::internal::transport::CoreHttpTransport::new(self.client),
650 )
651 }
652
653 pub async fn unfollow_async(
654 &self,
655 request: super::UnfollowRequest,
656 ) -> crate::ImResult<super::UnfollowResult> {
657 validate_peer(request.peer.as_str())?;
658 self.unfollow_with_runtime_async(
659 request,
660 crate::internal::auth::session::FileSessionProvider::new(self.client),
661 crate::internal::transport::CoreHttpTransport::new(self.client),
662 )
663 .await
664 }
665
666 pub(crate) fn unfollow_with_runtime<P, T>(
667 &self,
668 request: super::UnfollowRequest,
669 session_provider: P,
670 transport: T,
671 ) -> crate::ImResult<super::UnfollowResult>
672 where
673 P: crate::internal::auth::session::SessionProvider,
674 T: crate::internal::transport::AuthenticatedRpcTransport
675 + crate::internal::transport::RpcTransport,
676 {
677 crate::internal::relationship_runtime::RelationshipRuntime::new(
678 self.client,
679 session_provider,
680 transport,
681 )
682 .unfollow(request)
683 }
684
685 pub(crate) async fn unfollow_with_runtime_async<P, T>(
686 &self,
687 request: super::UnfollowRequest,
688 session_provider: P,
689 transport: T,
690 ) -> crate::ImResult<super::UnfollowResult>
691 where
692 P: crate::internal::auth::session::AsyncSessionProvider,
693 T: crate::internal::transport::AsyncAuthenticatedRpcTransport
694 + crate::internal::transport::AsyncRpcTransport,
695 {
696 crate::internal::relationship_runtime::RelationshipRuntime::new(
697 self.client,
698 session_provider,
699 transport,
700 )
701 .unfollow_async(request)
702 .await
703 }
704
705 pub fn relationship_status(
706 &self,
707 peer: crate::ids::PeerRef,
708 ) -> crate::ImResult<super::RelationshipStatus> {
709 validate_peer(peer.as_str())?;
710 #[cfg(not(feature = "blocking"))]
711 {
712 let _ = peer;
713 Err(crate::ImError::unsupported(
714 "sync-directory-relationship-status",
715 ))
716 }
717 #[cfg(feature = "blocking")]
718 self.relationship_status_with_runtime(
719 peer,
720 crate::internal::auth::session::FileSessionProvider::new(self.client),
721 crate::internal::transport::CoreHttpTransport::new(self.client),
722 )
723 }
724
725 pub async fn relationship_status_async(
726 &self,
727 peer: crate::ids::PeerRef,
728 ) -> crate::ImResult<super::RelationshipStatus> {
729 validate_peer(peer.as_str())?;
730 self.relationship_status_with_runtime_async(
731 peer,
732 crate::internal::auth::session::FileSessionProvider::new(self.client),
733 crate::internal::transport::CoreHttpTransport::new(self.client),
734 )
735 .await
736 }
737
738 pub(crate) fn relationship_status_with_runtime<P, T>(
739 &self,
740 peer: crate::ids::PeerRef,
741 session_provider: P,
742 transport: T,
743 ) -> crate::ImResult<super::RelationshipStatus>
744 where
745 P: crate::internal::auth::session::SessionProvider,
746 T: crate::internal::transport::AuthenticatedRpcTransport
747 + crate::internal::transport::RpcTransport,
748 {
749 crate::internal::relationship_runtime::RelationshipRuntime::new(
750 self.client,
751 session_provider,
752 transport,
753 )
754 .relationship_status(peer)
755 }
756
757 pub(crate) async fn relationship_status_with_runtime_async<P, T>(
758 &self,
759 peer: crate::ids::PeerRef,
760 session_provider: P,
761 transport: T,
762 ) -> crate::ImResult<super::RelationshipStatus>
763 where
764 P: crate::internal::auth::session::AsyncSessionProvider,
765 T: crate::internal::transport::AsyncAuthenticatedRpcTransport
766 + crate::internal::transport::AsyncRpcTransport,
767 {
768 crate::internal::relationship_runtime::RelationshipRuntime::new(
769 self.client,
770 session_provider,
771 transport,
772 )
773 .relationship_status_async(peer)
774 .await
775 }
776
777 pub fn followers(
778 &self,
779 query: super::RelationshipListQuery,
780 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>> {
781 validate_relationship_list_query(&query)?;
782 #[cfg(not(feature = "blocking"))]
783 {
784 let _ = query;
785 Err(crate::ImError::unsupported("sync-directory-followers"))
786 }
787 #[cfg(feature = "blocking")]
788 self.followers_with_runtime(
789 query,
790 crate::internal::auth::session::FileSessionProvider::new(self.client),
791 crate::internal::transport::CoreHttpTransport::new(self.client),
792 )
793 }
794
795 pub async fn followers_async(
796 &self,
797 query: super::RelationshipListQuery,
798 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>> {
799 validate_relationship_list_query(&query)?;
800 self.followers_with_runtime_async(
801 query,
802 crate::internal::auth::session::FileSessionProvider::new(self.client),
803 crate::internal::transport::CoreHttpTransport::new(self.client),
804 )
805 .await
806 }
807
808 pub(crate) fn followers_with_runtime<P, T>(
809 &self,
810 query: super::RelationshipListQuery,
811 session_provider: P,
812 transport: T,
813 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>>
814 where
815 P: crate::internal::auth::session::SessionProvider,
816 T: crate::internal::transport::AuthenticatedRpcTransport
817 + crate::internal::transport::RpcTransport,
818 {
819 crate::internal::relationship_runtime::RelationshipRuntime::new(
820 self.client,
821 session_provider,
822 transport,
823 )
824 .followers(query)
825 }
826
827 pub(crate) async fn followers_with_runtime_async<P, T>(
828 &self,
829 query: super::RelationshipListQuery,
830 session_provider: P,
831 transport: T,
832 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>>
833 where
834 P: crate::internal::auth::session::AsyncSessionProvider,
835 T: crate::internal::transport::AsyncAuthenticatedRpcTransport
836 + crate::internal::transport::AsyncRpcTransport,
837 {
838 crate::internal::relationship_runtime::RelationshipRuntime::new(
839 self.client,
840 session_provider,
841 transport,
842 )
843 .followers_async(query)
844 .await
845 }
846
847 pub fn following(
848 &self,
849 query: super::RelationshipListQuery,
850 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>> {
851 validate_relationship_list_query(&query)?;
852 #[cfg(not(feature = "blocking"))]
853 {
854 let _ = query;
855 Err(crate::ImError::unsupported("sync-directory-following"))
856 }
857 #[cfg(feature = "blocking")]
858 self.following_with_runtime(
859 query,
860 crate::internal::auth::session::FileSessionProvider::new(self.client),
861 crate::internal::transport::CoreHttpTransport::new(self.client),
862 )
863 }
864
865 pub async fn following_async(
866 &self,
867 query: super::RelationshipListQuery,
868 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>> {
869 validate_relationship_list_query(&query)?;
870 self.following_with_runtime_async(
871 query,
872 crate::internal::auth::session::FileSessionProvider::new(self.client),
873 crate::internal::transport::CoreHttpTransport::new(self.client),
874 )
875 .await
876 }
877
878 pub(crate) fn following_with_runtime<P, T>(
879 &self,
880 query: super::RelationshipListQuery,
881 session_provider: P,
882 transport: T,
883 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>>
884 where
885 P: crate::internal::auth::session::SessionProvider,
886 T: crate::internal::transport::AuthenticatedRpcTransport
887 + crate::internal::transport::RpcTransport,
888 {
889 crate::internal::relationship_runtime::RelationshipRuntime::new(
890 self.client,
891 session_provider,
892 transport,
893 )
894 .following(query)
895 }
896
897 pub(crate) async fn following_with_runtime_async<P, T>(
898 &self,
899 query: super::RelationshipListQuery,
900 session_provider: P,
901 transport: T,
902 ) -> crate::ImResult<crate::ids::Page<super::RelationshipListItem>>
903 where
904 P: crate::internal::auth::session::AsyncSessionProvider,
905 T: crate::internal::transport::AsyncAuthenticatedRpcTransport
906 + crate::internal::transport::AsyncRpcTransport,
907 {
908 crate::internal::relationship_runtime::RelationshipRuntime::new(
909 self.client,
910 session_provider,
911 transport,
912 )
913 .following_async(query)
914 .await
915 }
916
917 fn owner_identity_id(&self) -> &str {
918 self.client.current_identity().id.as_str()
919 }
920}
921
922fn validate_peer(peer: &str) -> crate::ImResult<()> {
923 if peer.trim().is_empty() {
924 return Err(crate::ImError::invalid_input(
925 Some("peer".to_string()),
926 "peer must not be empty",
927 ));
928 }
929 Ok(())
930}
931
932fn validate_display_profile_batch_request(
933 request: &crate::directory::DisplayProfileBatchRequest,
934) -> crate::ImResult<()> {
935 if request
936 .peers
937 .iter()
938 .any(|peer| peer.as_str().trim().is_empty())
939 {
940 return Err(crate::ImError::invalid_input(
941 Some("peers".to_string()),
942 "peers must not contain empty values",
943 ));
944 }
945 Ok(())
946}
947
948#[cfg(all(feature = "sqlite", feature = "blocking"))]
949fn local_contact_for_peer<FDid, FHandle>(
950 by_did: FDid,
951 by_handle: FHandle,
952 peer: &crate::ids::PeerRef,
953) -> crate::ImResult<crate::internal::contact_store::records::ContactRecord>
954where
955 FDid: FnOnce(&str) -> crate::ImResult<crate::internal::contact_store::records::ContactRecord>,
956 FHandle:
957 FnOnce(&str) -> crate::ImResult<crate::internal::contact_store::records::ContactRecord>,
958{
959 if peer.as_str().trim().starts_with("did:") {
960 by_did(peer.as_str())
961 } else {
962 by_handle(peer.as_str())
963 }
964}
965
966#[cfg(feature = "sqlite")]
967fn display_profile_from_local_result(
968 peer: crate::ids::PeerRef,
969 record: crate::ImResult<crate::internal::contact_store::records::ContactRecord>,
970) -> crate::ImResult<crate::directory::DisplayProfile> {
971 match record {
972 Ok(record) => crate::internal::contact_store::records::display_profile_from_record(&record),
973 Err(crate::ImError::PeerNotFound { .. }) => display_profile_cache_miss(peer),
974 Err(err) => Err(err),
975 }
976}
977
978#[cfg(feature = "sqlite")]
979fn display_profile_cache_miss(
980 peer: crate::ids::PeerRef,
981) -> crate::ImResult<crate::directory::DisplayProfile> {
982 let mut warnings = vec!["display profile cache miss".to_string()];
983 let (did, handle) = if peer.as_str().trim().starts_with("did:") {
984 (Some(crate::ids::Did::parse(peer.as_str())?), None)
985 } else {
986 warnings.push("peer did is unknown until remote handle resolution".to_string());
987 (None, Some(crate::ids::Handle::parse(peer.as_str(), "")?))
988 };
989 Ok(crate::directory::DisplayProfile {
990 did,
991 handle,
992 display_name: None,
993 avatar_uri: None,
994 avatar_url: None,
995 profile_uri: None,
996 subject_type: None,
997 cache_hit: false,
998 warnings,
999 })
1000}
1001
1002fn validate_not_self_peer(
1003 client: &crate::core::ImClient,
1004 peer: &crate::ids::PeerRef,
1005) -> crate::ImResult<()> {
1006 if peer.as_str().trim() == client.did().as_str() {
1007 return Err(crate::ImError::invalid_input(
1008 Some("peer".to_string()),
1009 "cannot follow self",
1010 ));
1011 }
1012 Ok(())
1013}
1014
1015fn validate_identity_subject(subject: &super::IdentitySubject) -> crate::ImResult<()> {
1016 match subject {
1017 super::IdentitySubject::Did(did) if did.as_str().trim().is_empty() => Err(
1018 crate::ImError::invalid_input(Some("did".to_string()), "did must not be empty"),
1019 ),
1020 super::IdentitySubject::Handle(handle) if handle.as_str().trim().is_empty() => Err(
1021 crate::ImError::invalid_input(Some("handle".to_string()), "handle must not be empty"),
1022 ),
1023 super::IdentitySubject::Any(value) if value.trim().is_empty() => Err(
1024 crate::ImError::invalid_input(Some("subject".to_string()), "subject must not be empty"),
1025 ),
1026 _ => Ok(()),
1027 }
1028}
1029
1030#[cfg(feature = "sqlite")]
1031fn project_handle_lookup(client: &crate::core::ImClient, lookup: &super::HandleLookupResult) {
1032 crate::internal::contact_store::projection::project_directory_resolution(
1033 client,
1034 &handle_lookup_resolution(lookup),
1035 );
1036}
1037
1038#[cfg(feature = "sqlite")]
1039async fn project_handle_lookup_async(
1040 client: &crate::core::ImClient,
1041 lookup: &super::HandleLookupResult,
1042) -> crate::ImResult<()> {
1043 crate::internal::contact_store::projection::project_directory_resolution_async(
1044 client,
1045 &handle_lookup_resolution(lookup),
1046 )
1047 .await
1048}
1049
1050#[cfg(feature = "sqlite")]
1051fn handle_lookup_resolution(lookup: &super::HandleLookupResult) -> super::DirectoryResolution {
1052 super::DirectoryResolution {
1053 input: lookup.handle.as_str().to_owned(),
1054 did: lookup.did.clone(),
1055 handle: Some(lookup.handle.clone()),
1056 profile: lookup.profile.clone(),
1057 warnings: lookup.warnings.clone(),
1058 }
1059}
1060
1061fn validate_save_contact(request: &super::SaveContactRequest) -> crate::ImResult<()> {
1062 if request.peer.as_str().trim().is_empty() {
1063 return Err(crate::ImError::invalid_input(
1064 Some("peer".to_string()),
1065 "peer must not be empty",
1066 ));
1067 }
1068 if request
1069 .relationship
1070 .as_deref()
1071 .is_some_and(|value| value.trim().is_empty())
1072 {
1073 return Err(crate::ImError::invalid_input(
1074 Some("relationship".to_string()),
1075 "relationship must not be empty when provided",
1076 ));
1077 }
1078 Ok(())
1079}
1080
1081fn contact_target_from_request(
1082 service: &DirectoryService<'_>,
1083 request: &super::SaveContactRequest,
1084) -> crate::ImResult<(crate::ids::Did, Option<crate::ids::Handle>)> {
1085 if let Some(did) = &request.did {
1086 return Ok((did.clone(), request.handle.clone()));
1087 }
1088 if request.peer.as_str().starts_with("did:") {
1089 return crate::ids::Did::parse(request.peer.as_str())
1090 .map(|did| (did, request.handle.clone()));
1091 }
1092 let resolved = service.resolve_peer(request.peer.clone())?;
1093 Ok((resolved.did, request.handle.clone().or(resolved.handle)))
1094}
1095
1096async fn contact_target_from_request_async(
1097 service: &DirectoryService<'_>,
1098 request: &super::SaveContactRequest,
1099) -> crate::ImResult<(crate::ids::Did, Option<crate::ids::Handle>)> {
1100 if let Some(did) = &request.did {
1101 return Ok((did.clone(), request.handle.clone()));
1102 }
1103 if request.peer.as_str().starts_with("did:") {
1104 return crate::ids::Did::parse(request.peer.as_str())
1105 .map(|did| (did, request.handle.clone()));
1106 }
1107 let resolved = service.resolve_peer_async(request.peer.clone()).await?;
1108 Ok((resolved.did, request.handle.clone().or(resolved.handle)))
1109}
1110
1111fn validate_relationship_list_query(query: &super::RelationshipListQuery) -> crate::ImResult<()> {
1112 if query.limit.is_some_and(|limit| limit.0 == 0) {
1113 return Err(crate::ImError::invalid_input(
1114 Some("limit".to_string()),
1115 "limit must be greater than zero",
1116 ));
1117 }
1118 Ok(())
1119}