Skip to main content

im_core/attachments/
service.rs

1pub struct AttachmentService<'a> {
2    client: &'a crate::core::ImClient,
3}
4
5struct ResolvedConversationAttachmentRequest {
6    target: crate::messages::MessageTarget,
7    request: super::AttachmentSendRequest,
8    client_message_id: crate::ids::MessageId,
9    target_did: Option<String>,
10    target_handle: Option<String>,
11    peer_scope: Option<crate::internal::local_state::owner_scope::DirectPeerScope>,
12    conversation_identity: crate::messages::ConversationIdentity,
13}
14
15impl<'a> AttachmentService<'a> {
16    pub(crate) fn new(client: &'a crate::core::ImClient) -> Self {
17        Self { client }
18    }
19
20    pub fn send(
21        &self,
22        target: crate::messages::MessageTarget,
23        request: super::AttachmentSendRequest,
24    ) -> crate::ImResult<super::AttachmentSendResult> {
25        if attachment_security_is_secure(&request.security) {
26            let result = self
27                .client
28                .messages()
29                .send_secure_attachment(message_request_from_attachment(target, request)?)?;
30            return attachment_send_result_from_secure_message(result);
31        }
32        let resolved_target = resolve_direct_target(self.client, &target)?;
33        let mut result = crate::internal::attachment_runtime::upload::AttachmentUploadRuntime::new(
34            self.client,
35            crate::internal::auth::session::FileSessionProvider::new(self.client),
36            crate::internal::transport::CoreHttpTransport::new(self.client),
37        )
38        .send(
39            crate::internal::attachment_runtime::upload::AttachmentSendInput {
40                target,
41                request,
42                resolved_target_did: resolved_target.target_did.clone(),
43                client_message_id: None,
44                credentials: None,
45            },
46        )?;
47        if result.target_kind != "group" {
48            crate::messages::normalize_direct_send_result_for_peer_scope(
49                &mut result.sdk_result,
50                resolved_target.peer_scope.as_ref(),
51                resolved_target.direct_handle.as_deref(),
52                Some(result.target_did.as_str()),
53            )?;
54        }
55        #[cfg(feature = "sqlite")]
56        {
57            let projection = if result.target_kind == "group" {
58                crate::internal::message_runtime::local_projection::persist_group_attachment_outgoing(
59                    self.client,
60                    &result.target_did,
61                    &result.manifest,
62                    &result.sdk_result,
63                )
64            } else {
65                crate::internal::message_runtime::local_projection::persist_direct_attachment_outgoing(
66                    self.client,
67                    &result.target_did,
68                    resolved_target.direct_handle.as_deref(),
69                    resolved_target.peer_scope.as_ref(),
70                    &result.manifest,
71                    &result.sdk_result,
72                )
73            };
74            match projection {
75                Ok(()) => self
76                    .client
77                    .emit_committed_local_message_projection("local_send"),
78                Err(err) => {
79                    result
80                        .sdk_result
81                        .warnings
82                        .push(format!("Failed to persist local attachment message: {err}"));
83                }
84            }
85        }
86        Ok(super::AttachmentSendResult::from_upload_result(result))
87    }
88
89    pub fn send_conversation(
90        &self,
91        request: super::SendConversationAttachmentRequest,
92    ) -> crate::ImResult<super::AttachmentSendResult> {
93        let resolved = self.resolve_conversation_attachment_request(request)?;
94        if attachment_security_is_secure(&resolved.request.security) {
95            let mut result = attachment_send_result_from_secure_message(
96                self.client.messages().send_secure_attachment(
97                    message_request_from_resolved_conversation_attachment(&resolved)?,
98                )?,
99            )?;
100            result.message.message.metadata.conversation_identity =
101                Some(resolved.conversation_identity);
102            return Ok(result);
103        }
104        let mut result = crate::internal::attachment_runtime::upload::AttachmentUploadRuntime::new(
105            self.client,
106            crate::internal::auth::session::FileSessionProvider::new(self.client),
107            crate::internal::transport::CoreHttpTransport::new(self.client),
108        )
109        .send(
110            crate::internal::attachment_runtime::upload::AttachmentSendInput {
111                target: resolved.target,
112                request: resolved.request,
113                resolved_target_did: resolved.target_did.clone(),
114                client_message_id: Some(resolved.client_message_id),
115                credentials: None,
116            },
117        )?;
118        apply_conversation_identity(
119            &mut result.sdk_result,
120            resolved.conversation_identity.clone(),
121        );
122        if result.target_kind != "group" {
123            crate::messages::normalize_direct_send_result_for_peer_scope(
124                &mut result.sdk_result,
125                resolved.peer_scope.as_ref(),
126                resolved.target_handle.as_deref(),
127                Some(result.target_did.as_str()),
128            )?;
129            apply_conversation_identity(
130                &mut result.sdk_result,
131                resolved.conversation_identity.clone(),
132            );
133        }
134        persist_attachment_projection_or_fail(
135            self.client,
136            result.target_kind,
137            result.target_did.as_str(),
138            resolved.target_handle.as_deref(),
139            resolved.peer_scope.as_ref(),
140            &result.manifest,
141            &result.sdk_result,
142        )?;
143        Ok(super::AttachmentSendResult::from_upload_result(result))
144    }
145
146    pub async fn send_conversation_async(
147        &self,
148        request: super::SendConversationAttachmentRequest,
149    ) -> crate::ImResult<super::AttachmentSendResult> {
150        let resolved = self.resolve_conversation_attachment_request(request)?;
151        if attachment_security_is_secure(&resolved.request.security) {
152            let mut result = attachment_send_result_from_secure_message(
153                self.client
154                    .messages()
155                    .send_secure_attachment_async(
156                        message_request_from_resolved_conversation_attachment(&resolved)?,
157                    )
158                    .await?,
159            )?;
160            result.message.message.metadata.conversation_identity =
161                Some(resolved.conversation_identity);
162            return Ok(result);
163        }
164        let mut result = crate::internal::attachment_runtime::upload::AttachmentUploadRuntime::new(
165            self.client,
166            crate::internal::auth::session::FileSessionProvider::new(self.client),
167            crate::internal::transport::CoreHttpTransport::new(self.client),
168        )
169        .send_async(
170            crate::internal::attachment_runtime::upload::AttachmentSendInput {
171                target: resolved.target,
172                request: resolved.request,
173                resolved_target_did: resolved.target_did.clone(),
174                client_message_id: Some(resolved.client_message_id),
175                credentials: None,
176            },
177        )
178        .await?;
179        apply_conversation_identity(
180            &mut result.sdk_result,
181            resolved.conversation_identity.clone(),
182        );
183        if result.target_kind != "group" {
184            crate::messages::normalize_direct_send_result_for_peer_scope(
185                &mut result.sdk_result,
186                resolved.peer_scope.as_ref(),
187                resolved.target_handle.as_deref(),
188                Some(result.target_did.as_str()),
189            )?;
190            apply_conversation_identity(
191                &mut result.sdk_result,
192                resolved.conversation_identity.clone(),
193            );
194        }
195        persist_attachment_projection_or_fail_async(
196            self.client,
197            result.target_kind,
198            result.target_did.as_str(),
199            resolved.target_handle.as_deref(),
200            resolved.peer_scope.as_ref(),
201            &result.manifest,
202            &result.sdk_result,
203        )
204        .await?;
205        Ok(super::AttachmentSendResult::from_upload_result(result))
206    }
207
208    pub async fn send_async(
209        &self,
210        target: crate::messages::MessageTarget,
211        request: super::AttachmentSendRequest,
212    ) -> crate::ImResult<super::AttachmentSendResult> {
213        if attachment_security_is_secure(&request.security) {
214            let result = self
215                .client
216                .messages()
217                .send_secure_attachment_async(message_request_from_attachment(target, request)?)
218                .await?;
219            return attachment_send_result_from_secure_message(result);
220        }
221        let resolved_target = resolve_direct_target_async(self.client, &target).await?;
222        let mut result = crate::internal::attachment_runtime::upload::AttachmentUploadRuntime::new(
223            self.client,
224            crate::internal::auth::session::FileSessionProvider::new(self.client),
225            crate::internal::transport::CoreHttpTransport::new(self.client),
226        )
227        .send_async(
228            crate::internal::attachment_runtime::upload::AttachmentSendInput {
229                target,
230                request,
231                resolved_target_did: resolved_target.target_did.clone(),
232                client_message_id: None,
233                credentials: None,
234            },
235        )
236        .await?;
237        if result.target_kind != "group" {
238            crate::messages::normalize_direct_send_result_for_peer_scope(
239                &mut result.sdk_result,
240                resolved_target.peer_scope.as_ref(),
241                resolved_target.direct_handle.as_deref(),
242                Some(result.target_did.as_str()),
243            )?;
244        }
245        #[cfg(feature = "sqlite")]
246        {
247            let projection = if result.target_kind == "group" {
248                crate::internal::message_runtime::local_projection::persist_group_attachment_outgoing_async(
249                    self.client,
250                    &result.target_did,
251                    &result.manifest,
252                    &result.sdk_result,
253                )
254                .await
255            } else {
256                crate::internal::message_runtime::local_projection::persist_direct_attachment_outgoing_async(
257                    self.client,
258                    &result.target_did,
259                    resolved_target.direct_handle.as_deref(),
260                    resolved_target.peer_scope.as_ref(),
261                    &result.manifest,
262                    &result.sdk_result,
263                )
264                .await
265            };
266            match projection {
267                Ok(()) => self
268                    .client
269                    .emit_committed_local_message_projection("local_send"),
270                Err(err) => {
271                    result
272                        .sdk_result
273                        .warnings
274                        .push(format!("Failed to persist local attachment message: {err}"));
275                }
276            }
277        }
278        Ok(super::AttachmentSendResult::from_upload_result(result))
279    }
280
281    pub fn download(
282        &self,
283        request: super::DownloadAttachmentRequest,
284    ) -> crate::ImResult<super::DownloadedAttachment> {
285        let resolved_peer_did = resolve_direct_thread_did(self.client, &request.thread)?;
286        crate::internal::attachment_runtime::download::AttachmentDownloadRuntime::new(
287            self.client,
288            crate::internal::auth::session::FileSessionProvider::new(self.client),
289            crate::internal::transport::CoreHttpTransport::new(self.client),
290        )
291        .download(
292            crate::internal::attachment_runtime::download::AttachmentDownloadInput {
293                request,
294                resolved_peer_did,
295            },
296        )
297        .map(|result| result.sdk_result)
298    }
299
300    pub async fn download_async(
301        &self,
302        request: super::DownloadAttachmentRequest,
303    ) -> crate::ImResult<super::DownloadedAttachment> {
304        let resolved_peer_did =
305            resolve_direct_thread_did_async(self.client, &request.thread).await?;
306        crate::internal::attachment_runtime::download::AttachmentDownloadRuntime::new(
307            self.client,
308            crate::internal::auth::session::FileSessionProvider::new(self.client),
309            crate::internal::transport::CoreHttpTransport::new(self.client),
310        )
311        .download_async(
312            crate::internal::attachment_runtime::download::AttachmentDownloadInput {
313                request,
314                resolved_peer_did,
315            },
316        )
317        .await
318        .map(|result| result.sdk_result)
319    }
320
321    fn resolve_conversation_attachment_request(
322        &self,
323        request: super::SendConversationAttachmentRequest,
324    ) -> crate::ImResult<ResolvedConversationAttachmentRequest> {
325        let (conversation, request_client_message_id, mut attachment) =
326            request.into_attachment_send_request();
327        let client_message_id = conversation_client_message_id(request_client_message_id)?;
328        attachment.delivery.idempotency_key = attachment
329            .delivery
330            .idempotency_key
331            .map(|value| value.trim().to_owned())
332            .filter(|value| !value.is_empty())
333            .or_else(|| Some(format!("op-{}", client_message_id.as_str())));
334        let resolved =
335            crate::messages::resolve_conversation_send_target(self.client, &conversation)?;
336        Ok(ResolvedConversationAttachmentRequest {
337            target: resolved.target,
338            request: attachment,
339            client_message_id,
340            target_did: resolved.target_did,
341            target_handle: resolved.target_handle,
342            peer_scope: resolved.peer_scope,
343            conversation_identity: crate::messages::ConversationIdentity::from_thread_ref_for_owner(
344                &conversation.as_thread_ref()?,
345                self.client.did().as_str(),
346            ),
347        })
348    }
349}
350
351fn attachment_security_is_secure(security: &crate::messages::MessageSecurityMode) -> bool {
352    matches!(
353        security,
354        crate::messages::MessageSecurityMode::E2eeRequired
355            | crate::messages::MessageSecurityMode::SecureDirect
356            | crate::messages::MessageSecurityMode::GroupE2ee
357    )
358}
359
360fn message_request_from_attachment(
361    target: crate::messages::MessageTarget,
362    request: super::AttachmentSendRequest,
363) -> crate::ImResult<crate::messages::SendMessageRequest> {
364    Ok(crate::messages::SendMessageRequest {
365        target,
366        body: crate::messages::MessageBody::Attachment {
367            input: request.input,
368            caption: request.caption,
369            mention_payload: request.mention_payload,
370            mime_type: request.mime_type,
371            filename: request.filename,
372        },
373        security: request.security,
374        client_message_id: None,
375        delivery: request.delivery,
376        delegated_signing: None,
377    })
378}
379
380fn message_request_from_resolved_conversation_attachment(
381    resolved: &ResolvedConversationAttachmentRequest,
382) -> crate::ImResult<crate::messages::SendMessageRequest> {
383    Ok(crate::messages::SendMessageRequest {
384        target: resolved.target.clone(),
385        body: crate::messages::MessageBody::Attachment {
386            input: resolved.request.input.clone(),
387            caption: resolved.request.caption.clone(),
388            mention_payload: resolved.request.mention_payload.clone(),
389            mime_type: resolved.request.mime_type.clone(),
390            filename: resolved.request.filename.clone(),
391        },
392        security: resolved.request.security.clone(),
393        client_message_id: Some(resolved.client_message_id.clone()),
394        delivery: resolved.request.delivery.clone(),
395        delegated_signing: None,
396    })
397}
398
399fn conversation_client_message_id(
400    client_message_id: Option<crate::ids::MessageId>,
401) -> crate::ImResult<crate::ids::MessageId> {
402    match client_message_id {
403        Some(id) => Ok(id),
404        None => crate::ids::MessageId::parse(format!(
405            "msg-{}",
406            crate::internal::wire::common::generate_operation_id()
407        )),
408    }
409}
410
411fn apply_conversation_identity(
412    result: &mut crate::messages::SendMessageResult,
413    identity: crate::messages::ConversationIdentity,
414) {
415    result.message.metadata.conversation_identity = Some(identity);
416}
417
418fn persist_attachment_projection_or_fail(
419    client: &crate::core::ImClient,
420    target_kind: &str,
421    target_did: &str,
422    target_handle: Option<&str>,
423    peer_scope: Option<&crate::internal::local_state::owner_scope::DirectPeerScope>,
424    manifest: &serde_json::Value,
425    sdk_result: &crate::messages::SendMessageResult,
426) -> crate::ImResult<()> {
427    #[cfg(feature = "sqlite")]
428    {
429        let projection = if target_kind == "group" {
430            crate::internal::message_runtime::local_projection::persist_group_attachment_outgoing(
431                client, target_did, manifest, sdk_result,
432            )
433        } else {
434            crate::internal::message_runtime::local_projection::persist_direct_attachment_outgoing(
435                client,
436                target_did,
437                target_handle,
438                peer_scope,
439                manifest,
440                sdk_result,
441            )
442        };
443        projection?;
444        client.emit_committed_local_message_projection("local_send");
445    }
446    #[cfg(not(feature = "sqlite"))]
447    {
448        let _ = (
449            client,
450            target_kind,
451            target_did,
452            target_handle,
453            peer_scope,
454            manifest,
455            sdk_result,
456        );
457    }
458    Ok(())
459}
460
461async fn persist_attachment_projection_or_fail_async(
462    client: &crate::core::ImClient,
463    target_kind: &str,
464    target_did: &str,
465    target_handle: Option<&str>,
466    peer_scope: Option<&crate::internal::local_state::owner_scope::DirectPeerScope>,
467    manifest: &serde_json::Value,
468    sdk_result: &crate::messages::SendMessageResult,
469) -> crate::ImResult<()> {
470    #[cfg(feature = "sqlite")]
471    {
472        let projection = if target_kind == "group" {
473            crate::internal::message_runtime::local_projection::persist_group_attachment_outgoing_async(
474                client,
475                target_did,
476                manifest,
477                sdk_result,
478            )
479            .await
480        } else {
481            crate::internal::message_runtime::local_projection::persist_direct_attachment_outgoing_async(
482                client,
483                target_did,
484                target_handle,
485                peer_scope,
486                manifest,
487                sdk_result,
488            )
489            .await
490        };
491        projection?;
492        client.emit_committed_local_message_projection("local_send");
493    }
494    #[cfg(not(feature = "sqlite"))]
495    {
496        let _ = (
497            client,
498            target_kind,
499            target_did,
500            target_handle,
501            peer_scope,
502            manifest,
503            sdk_result,
504        );
505    }
506    Ok(())
507}
508
509fn attachment_send_result_from_secure_message(
510    message: crate::messages::SendMessageResult,
511) -> crate::ImResult<super::AttachmentSendResult> {
512    let manifest = secure_attachment_manifest_from_message(&message)?;
513    let parsed = crate::attachments::manifest::parse_attachment_manifest(&manifest)?;
514    let descriptor = selected_attachment_descriptor(&parsed)?;
515    let target_kind = match &message.message.thread {
516        crate::messages::ThreadRef::Direct(_) => "agent".to_owned(),
517        crate::messages::ThreadRef::Group(_) => "group".to_owned(),
518        crate::messages::ThreadRef::Thread(_) => {
519            return Err(crate::ImError::unsupported("thread-attachment-send"));
520        }
521    };
522    let target_did = secure_attachment_target_did(&message);
523    let size_bytes = descriptor.size.trim().parse::<u64>().map_err(|_| {
524        crate::ImError::invalid_input(
525            Some("size".to_owned()),
526            "attachment size must be an unsigned integer",
527        )
528    })?;
529    let plaintext_size_bytes = descriptor
530        .plaintext_size
531        .as_deref()
532        .map(str::trim)
533        .filter(|value| !value.is_empty())
534        .map(|value| {
535            value.parse::<u64>().map_err(|_| {
536                crate::ImError::invalid_input(
537                    Some("plaintext_size".to_owned()),
538                    "attachment plaintext_size must be an unsigned integer",
539                )
540            })
541        })
542        .transpose()?;
543
544    Ok(super::AttachmentSendResult {
545        message,
546        target_kind,
547        target_did,
548        attachment: super::UploadedAttachment {
549            attachment_id: descriptor.attachment_id.clone(),
550            filename: descriptor.filename.clone(),
551            mime_type: descriptor.mime_type.clone(),
552            size_bytes,
553            size: descriptor.size.clone(),
554            digest_b64u: descriptor.digest_b64u.clone(),
555            object_uri: descriptor.object_uri.clone(),
556            object_encryption_mode: descriptor.object_encryption_mode(),
557            plaintext_size_bytes,
558        },
559        manifest,
560    })
561}
562
563fn secure_attachment_manifest_from_message(
564    message: &crate::messages::SendMessageResult,
565) -> crate::ImResult<serde_json::Value> {
566    let manifest = message
567        .message
568        .metadata
569        .attributes
570        .iter()
571        .find(|attribute| attribute.key == "attachment_manifest")
572        .map(|attribute| attribute.value.as_str())
573        .ok_or_else(|| crate::ImError::Internal {
574            message: "secure attachment result missing redacted attachment manifest".to_owned(),
575        })?;
576    serde_json::from_str(manifest).map_err(|err| crate::ImError::Serialization {
577        detail: err.to_string(),
578    })
579}
580
581fn selected_attachment_descriptor(
582    manifest: &crate::attachments::manifest::AttachmentManifest,
583) -> crate::ImResult<&crate::attachments::manifest::AttachmentDescriptor> {
584    if !manifest.primary_attachment_id.trim().is_empty() {
585        return manifest
586            .attachments
587            .iter()
588            .find(|attachment| attachment.attachment_id == manifest.primary_attachment_id)
589            .ok_or_else(|| {
590                crate::ImError::invalid_input(
591                    Some("primary_attachment_id".to_owned()),
592                    "attachment manifest primary attachment is missing",
593                )
594            });
595    }
596    if manifest.attachments.len() == 1 {
597        return Ok(&manifest.attachments[0]);
598    }
599    Err(crate::ImError::invalid_input(
600        Some("attachment_id".to_owned()),
601        "attachment_id is required for messages with multiple attachments",
602    ))
603}
604
605fn secure_attachment_target_did(message: &crate::messages::SendMessageResult) -> String {
606    match &message.message.thread {
607        crate::messages::ThreadRef::Direct(peer) => message
608            .message
609            .metadata
610            .attributes
611            .iter()
612            .find(|attribute| attribute.key == "resolved_target_did")
613            .map(|attribute| attribute.value.clone())
614            .filter(|value| !value.trim().is_empty())
615            .or_else(|| {
616                message
617                    .message
618                    .receiver
619                    .as_ref()
620                    .map(|peer| peer.as_str().to_owned())
621            })
622            .unwrap_or_else(|| peer.as_str().to_owned()),
623        crate::messages::ThreadRef::Group(group) => group.as_str().to_owned(),
624        crate::messages::ThreadRef::Thread(thread) => thread.as_str().to_owned(),
625    }
626}
627
628struct ResolvedDirectTarget {
629    target_did: Option<String>,
630    direct_handle: Option<String>,
631    peer_scope: Option<crate::internal::local_state::owner_scope::DirectPeerScope>,
632}
633
634fn resolve_direct_target(
635    client: &crate::core::ImClient,
636    target: &crate::messages::MessageTarget,
637) -> crate::ImResult<ResolvedDirectTarget> {
638    match target {
639        crate::messages::MessageTarget::Direct(peer) => resolve_peer(client, peer),
640        crate::messages::MessageTarget::Group(_) => Ok(ResolvedDirectTarget {
641            target_did: None,
642            direct_handle: None,
643            peer_scope: None,
644        }),
645    }
646}
647
648async fn resolve_direct_target_async(
649    client: &crate::core::ImClient,
650    target: &crate::messages::MessageTarget,
651) -> crate::ImResult<ResolvedDirectTarget> {
652    match target {
653        crate::messages::MessageTarget::Direct(peer) => resolve_peer_async(client, peer).await,
654        crate::messages::MessageTarget::Group(_) => Ok(ResolvedDirectTarget {
655            target_did: None,
656            direct_handle: None,
657            peer_scope: None,
658        }),
659    }
660}
661
662fn resolve_direct_thread_did(
663    client: &crate::core::ImClient,
664    thread: &crate::messages::ThreadRef,
665) -> crate::ImResult<Option<String>> {
666    match thread {
667        crate::messages::ThreadRef::Direct(peer) => resolve_peer_current_did(client, peer),
668        crate::messages::ThreadRef::Group(_) => Ok(None),
669        crate::messages::ThreadRef::Thread(_) => Ok(None),
670    }
671}
672
673async fn resolve_direct_thread_did_async(
674    client: &crate::core::ImClient,
675    thread: &crate::messages::ThreadRef,
676) -> crate::ImResult<Option<String>> {
677    match thread {
678        crate::messages::ThreadRef::Direct(peer) => {
679            resolve_peer_current_did_async(client, peer).await
680        }
681        crate::messages::ThreadRef::Group(_) => Ok(None),
682        crate::messages::ThreadRef::Thread(_) => Ok(None),
683    }
684}
685
686fn resolve_peer_current_did(
687    client: &crate::core::ImClient,
688    peer: &crate::ids::PeerRef,
689) -> crate::ImResult<Option<String>> {
690    let raw = peer.as_str().trim();
691    if raw.is_empty() || raw.starts_with("did:") {
692        return Ok(None);
693    }
694    crate::internal::handle_discovery::resolve_direct_handle(client, raw)
695        .map(|lookup| Some(lookup.target_did))
696}
697
698async fn resolve_peer_current_did_async(
699    client: &crate::core::ImClient,
700    peer: &crate::ids::PeerRef,
701) -> crate::ImResult<Option<String>> {
702    let raw = peer.as_str().trim();
703    if raw.is_empty() || raw.starts_with("did:") {
704        return Ok(None);
705    }
706    crate::internal::handle_discovery::resolve_direct_handle_async(client, raw)
707        .await
708        .map(|lookup| Some(lookup.target_did))
709}
710
711fn resolve_peer(
712    client: &crate::core::ImClient,
713    peer: &crate::ids::PeerRef,
714) -> crate::ImResult<ResolvedDirectTarget> {
715    let raw = peer.as_str().trim();
716    if raw.is_empty() || raw.starts_with("did:") {
717        return Ok(ResolvedDirectTarget {
718            target_did: None,
719            direct_handle: None,
720            peer_scope: None,
721        });
722    }
723    let lookup = crate::internal::handle_discovery::resolve_direct_handle(client, raw)?;
724    Ok(ResolvedDirectTarget {
725        target_did: Some(lookup.target_did),
726        direct_handle: Some(lookup.full_handle.clone()),
727        peer_scope: Some(
728            crate::internal::local_state::owner_scope::DirectPeerScope::new(
729                lookup.user_id,
730                lookup.full_handle,
731            )?,
732        ),
733    })
734}
735
736async fn resolve_peer_async(
737    client: &crate::core::ImClient,
738    peer: &crate::ids::PeerRef,
739) -> crate::ImResult<ResolvedDirectTarget> {
740    let raw = peer.as_str().trim();
741    if raw.is_empty() || raw.starts_with("did:") {
742        return Ok(ResolvedDirectTarget {
743            target_did: None,
744            direct_handle: None,
745            peer_scope: None,
746        });
747    }
748    let lookup =
749        crate::internal::handle_discovery::resolve_direct_handle_async(client, raw).await?;
750    Ok(ResolvedDirectTarget {
751        target_did: Some(lookup.target_did),
752        direct_handle: Some(lookup.full_handle.clone()),
753        peer_scope: Some(
754            crate::internal::local_state::owner_scope::DirectPeerScope::new(
755                lookup.user_id,
756                lookup.full_handle,
757            )?,
758        ),
759    })
760}