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}