Skip to main content

im_core/directory/
service.rs

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}