1pub struct SecureService<'a> {
2 client: &'a crate::core::ImClient,
3}
4
5impl<'a> SecureService<'a> {
6 pub(crate) fn new(client: &'a crate::core::ImClient) -> Self {
7 Self { client }
8 }
9
10 pub fn direct(&self, peer: crate::ids::PeerRef) -> DirectSecureConversation<'a> {
11 DirectSecureConversation {
12 client: self.client,
13 peer,
14 }
15 }
16
17 pub fn group(&self, group: crate::ids::GroupRef) -> GroupSecureConversation<'a> {
18 GroupSecureConversation {
19 client: self.client,
20 group,
21 }
22 }
23
24 pub fn outbox(&self) -> SecureOutboxService<'a> {
25 SecureOutboxService {
26 client: self.client,
27 }
28 }
29}
30
31pub struct DirectSecureConversation<'a> {
32 client: &'a crate::core::ImClient,
33 peer: crate::ids::PeerRef,
34}
35
36impl DirectSecureConversation<'_> {
37 pub fn status(&self) -> crate::ImResult<super::DirectSecureStatus> {
38 #[cfg(feature = "blocking")]
39 {
40 direct_status_to_dto(
41 crate::internal::secure_direct::status::direct_status_for_client(
42 self.client,
43 self.peer.clone(),
44 )?,
45 )
46 }
47 #[cfg(not(feature = "blocking"))]
48 {
49 let _ = self.client.current_identity();
50 Err(crate::ImError::unsupported("sync-direct-secure-status"))
51 }
52 }
53
54 pub async fn status_async(&self) -> crate::ImResult<super::DirectSecureStatus> {
55 direct_status_to_dto(
56 crate::internal::secure_direct::status::direct_status_for_client_async(
57 self.client,
58 self.peer.clone(),
59 )
60 .await?,
61 )
62 }
63
64 pub fn prepare(&self) -> crate::ImResult<super::DirectSecurePrepareResult> {
65 #[cfg(feature = "blocking")]
66 {
67 let plan = crate::internal::secure_direct::prepare::prepare_direct_for_client(
68 self.client,
69 self.peer.clone(),
70 )?;
71 direct_prepare_to_dto(plan)
72 }
73 #[cfg(not(feature = "blocking"))]
74 {
75 let _ = self.client.current_identity();
76 Err(crate::ImError::unsupported("sync-direct-secure-prepare"))
77 }
78 }
79
80 pub async fn prepare_async(&self) -> crate::ImResult<super::DirectSecurePrepareResult> {
81 let plan = crate::internal::secure_direct::prepare::prepare_direct_for_client_async(
82 self.client,
83 self.peer.clone(),
84 )
85 .await?;
86 direct_prepare_to_dto(plan)
87 }
88
89 pub fn repair(&self) -> crate::ImResult<super::DirectSecureRepairResult> {
90 #[cfg(feature = "blocking")]
91 {
92 direct_repair_for_client(self.client, self.peer.clone())
93 }
94 #[cfg(not(feature = "blocking"))]
95 {
96 let _ = self.client.current_identity();
97 Err(crate::ImError::unsupported("sync-direct-secure-repair"))
98 }
99 }
100
101 pub async fn repair_async(&self) -> crate::ImResult<super::DirectSecureRepairResult> {
102 let client = self.client.clone();
103 let peer = self.peer.clone();
104 let plan =
105 crate::internal::secure_direct::status::repair_direct_for_client_async(&client, peer)
106 .await?;
107 direct_repair_plan_to_dto_with_prepare(self.client, self.peer.clone(), plan).await
108 }
109}
110
111fn direct_prepare_to_dto(
112 plan: crate::internal::secure_direct::prepare::DirectSecurePreparePlan,
113) -> crate::ImResult<super::DirectSecurePrepareResult> {
114 let mut warnings = plan.status.warnings;
115 if plan.prepared_local_send_state {
116 warnings.push("direct E2EE local send state prepared".to_owned());
117 }
118 Ok(super::DirectSecurePrepareResult {
119 peer: plan.status.peer,
120 state: plan.status.state,
121 can_send_secure: plan.status.can_send_secure,
122 warnings,
123 })
124}
125
126fn direct_repair_for_client(
127 client: &crate::core::ImClient,
128 peer: crate::ids::PeerRef,
129) -> crate::ImResult<super::DirectSecureRepairResult> {
130 #[cfg(feature = "blocking")]
131 {
132 let plan =
133 crate::internal::secure_direct::status::repair_direct_for_client(client, peer.clone())?;
134 direct_repair_plan_to_dto(client, peer, plan)
135 }
136 #[cfg(not(feature = "blocking"))]
137 {
138 let _ = (client, peer);
139 Err(crate::ImError::unsupported("sync-direct-secure-repair"))
140 }
141}
142
143async fn direct_repair_plan_to_dto_with_prepare(
144 client: &crate::core::ImClient,
145 peer: crate::ids::PeerRef,
146 plan: crate::internal::secure_direct::status::DirectSecureRepairPlan,
147) -> crate::ImResult<super::DirectSecureRepairResult> {
148 let prepare_result =
149 crate::internal::secure_direct::prepare::prepare_direct_for_client_async(client, peer)
150 .await;
151 Ok(direct_repair_plan_to_dto_with_prepare_result(
152 plan,
153 prepare_result,
154 ))
155}
156
157fn direct_repair_plan_to_dto(
158 client: &crate::core::ImClient,
159 peer: crate::ids::PeerRef,
160 plan: crate::internal::secure_direct::status::DirectSecureRepairPlan,
161) -> crate::ImResult<super::DirectSecureRepairResult> {
162 Ok(direct_repair_plan_to_dto_with_prepare_result(
163 plan,
164 crate::internal::secure_direct::prepare::prepare_direct_for_client(client, peer),
165 ))
166}
167
168fn direct_repair_plan_to_dto_with_prepare_result(
169 plan: crate::internal::secure_direct::status::DirectSecureRepairPlan,
170 prepare_result: crate::ImResult<
171 crate::internal::secure_direct::prepare::DirectSecurePreparePlan,
172 >,
173) -> super::DirectSecureRepairResult {
174 let mut warnings = plan.status.warnings;
175 let mut state = plan.status.state;
176 let mut problem = plan.status.problem;
177 let mut prepared_local_send_state = false;
178 match prepare_result {
179 Ok(prepare_plan) => {
180 warnings.extend(prepare_plan.status.warnings);
181 prepared_local_send_state = prepare_plan.prepared_local_send_state;
182 if prepared_local_send_state {
183 state = prepare_plan.status.state;
184 problem = prepare_plan.status.problem;
185 warnings.push("direct E2EE local send state prepared".to_owned());
186 }
187 }
188 Err(err) => {
189 warnings.push(format!("direct E2EE prekey preparation failed: {err}"));
190 }
191 }
192 if plan.requeued_outbox_count > 0 {
193 warnings.push(format!(
194 "{} failed secure outbox item(s) were moved back to queued",
195 plan.requeued_outbox_count
196 ));
197 }
198 super::DirectSecureRepairResult {
199 peer: plan.status.peer,
200 state,
201 repaired: plan.removed_session
202 || plan.requeued_outbox_count > 0
203 || prepared_local_send_state,
204 problem,
205 prepared_local_send_state,
206 warnings,
207 }
208}
209
210fn direct_status_to_dto(
211 status: crate::internal::secure_direct::status::DirectSecureLocalStatus,
212) -> crate::ImResult<super::DirectSecureStatus> {
213 Ok(super::DirectSecureStatus {
214 peer: status.peer,
215 resolved_peer: status.resolved_peer,
216 state: status.state,
217 can_send_secure: status.can_send_secure,
218 pending_outbox_count: status.pending_outbox_count,
219 problem: status.problem,
220 warnings: status.warnings,
221 })
222}
223
224pub struct GroupSecureConversation<'a> {
225 client: &'a crate::core::ImClient,
226 group: crate::ids::GroupRef,
227}
228
229impl GroupSecureConversation<'_> {
230 pub fn status(&self) -> crate::ImResult<super::GroupSecureStatus> {
231 #[cfg(all(feature = "group-e2ee", feature = "blocking"))]
232 {
233 let provider =
234 match crate::internal::group_e2ee::storage::native_provider_for_client(self.client)
235 {
236 Ok(provider) => provider,
237 Err(err) => return Ok(group_status_unavailable(self.group.clone(), err)),
238 };
239 let status = crate::internal::group_e2ee::status::GroupE2eeStatusRuntime::new(
240 self.client,
241 crate::internal::auth::session::FileSessionProvider::new(self.client),
242 crate::internal::transport::CoreHttpTransport::new(self.client),
243 provider,
244 )
245 .status(crate::internal::group_e2ee::status::GroupE2eeStatusInput {
246 group: self.group.clone(),
247 credentials: None,
248 notice_limit: 50,
249 });
250 match status {
251 Ok(status) => group_status_to_dto(status),
252 Err(err) if group_status_can_downgrade_error(&err) => {
253 Ok(group_status_unavailable(self.group.clone(), err))
254 }
255 Err(err) => Err(err),
256 }
257 }
258 #[cfg(all(feature = "group-e2ee", not(feature = "blocking")))]
259 {
260 let _ = self.client.current_identity();
261 Err(crate::ImError::unsupported("sync-group-secure-status"))
262 }
263 #[cfg(not(feature = "group-e2ee"))]
264 {
265 let _ = self.client.current_identity();
266 Ok(super::GroupSecureStatus {
267 group: self.group.clone(),
268 state: super::GroupSecureState::Unavailable,
269 can_send_secure: false,
270 local_readiness: super::GroupSecureLocalReadiness {
271 has_local_state: false,
272 has_active_membership: false,
273 },
274 pending_work: super::GroupSecurePendingWork::default(),
275 problem: Some(super::SecureProblem {
276 code: super::SecureProblemCode::Unsupported,
277 message: "group-e2ee-status is not available yet".to_owned(),
278 retryable: false,
279 }),
280 warnings: Vec::new(),
281 })
282 }
283 }
284
285 pub fn prepare(&self) -> crate::ImResult<super::GroupSecurePrepareResult> {
286 let status = self.status()?;
287 Ok(super::GroupSecurePrepareResult {
288 group: status.group,
289 state: status.state,
290 can_send_secure: status.can_send_secure,
291 warnings: status.warnings,
292 })
293 }
294
295 pub async fn status_async(&self) -> crate::ImResult<super::GroupSecureStatus> {
296 #[cfg(feature = "group-e2ee")]
297 {
298 let provider =
299 match crate::internal::group_e2ee::storage::native_provider_for_client(self.client)
300 {
301 Ok(provider) => provider,
302 Err(err) => return Ok(group_status_unavailable(self.group.clone(), err)),
303 };
304 let status = crate::internal::group_e2ee::status::GroupE2eeStatusRuntime::new(
305 self.client,
306 crate::internal::auth::session::FileSessionProvider::new(self.client),
307 crate::internal::transport::CoreHttpTransport::new(self.client),
308 provider,
309 )
310 .status_async(crate::internal::group_e2ee::status::GroupE2eeStatusInput {
311 group: self.group.clone(),
312 credentials: None,
313 notice_limit: 50,
314 })
315 .await;
316 match status {
317 Ok(status) => group_status_to_dto(status),
318 Err(err) if group_status_can_downgrade_error(&err) => {
319 Ok(group_status_unavailable(self.group.clone(), err))
320 }
321 Err(err) => Err(err),
322 }
323 }
324 #[cfg(not(feature = "group-e2ee"))]
325 {
326 let _ = self.client.current_identity();
327 Ok(super::GroupSecureStatus {
328 group: self.group.clone(),
329 state: super::GroupSecureState::Unavailable,
330 can_send_secure: false,
331 local_readiness: super::GroupSecureLocalReadiness {
332 has_local_state: false,
333 has_active_membership: false,
334 },
335 pending_work: super::GroupSecurePendingWork::default(),
336 problem: Some(super::SecureProblem {
337 code: super::SecureProblemCode::Unsupported,
338 message: "group-e2ee-status is not available yet".to_owned(),
339 retryable: false,
340 }),
341 warnings: Vec::new(),
342 })
343 }
344 }
345
346 pub async fn prepare_async(&self) -> crate::ImResult<super::GroupSecurePrepareResult> {
347 let status = self.status_async().await?;
348 Ok(super::GroupSecurePrepareResult {
349 group: status.group,
350 state: status.state,
351 can_send_secure: status.can_send_secure,
352 warnings: status.warnings,
353 })
354 }
355
356 pub fn repair(&self) -> crate::ImResult<super::GroupSecureRepairResult> {
357 #[cfg(all(feature = "group-e2ee", feature = "blocking"))]
358 {
359 let provider =
360 crate::internal::group_e2ee::storage::native_provider_for_client(self.client)?;
361 group_repair_to_dto(
362 crate::internal::group_e2ee::repair::GroupE2eeRepairRuntime::new(
363 self.client,
364 crate::internal::auth::session::FileSessionProvider::new(self.client),
365 crate::internal::transport::CoreHttpTransport::new(self.client),
366 provider,
367 )
368 .repair(
369 crate::internal::group_e2ee::repair::GroupE2eeRepairInput {
370 group: self.group.clone(),
371 credentials: None,
372 notice_limit: 50,
373 },
374 )?,
375 )
376 }
377 #[cfg(all(feature = "group-e2ee", not(feature = "blocking")))]
378 {
379 let _ = self.client.current_identity();
380 Err(crate::ImError::unsupported("sync-group-secure-repair"))
381 }
382 #[cfg(not(feature = "group-e2ee"))]
383 {
384 Err(crate::ImError::unsupported("group-e2ee"))
385 }
386 }
387
388 pub async fn repair_async(&self) -> crate::ImResult<super::GroupSecureRepairResult> {
389 #[cfg(feature = "group-e2ee")]
390 {
391 let provider =
392 crate::internal::group_e2ee::storage::native_provider_for_client(self.client)?;
393 group_repair_to_dto(
394 crate::internal::group_e2ee::repair::GroupE2eeRepairRuntime::new(
395 self.client,
396 crate::internal::auth::session::FileSessionProvider::new(self.client),
397 crate::internal::transport::CoreHttpTransport::new(self.client),
398 provider,
399 )
400 .repair_async(crate::internal::group_e2ee::repair::GroupE2eeRepairInput {
401 group: self.group.clone(),
402 credentials: None,
403 notice_limit: 50,
404 })
405 .await?,
406 )
407 }
408 #[cfg(not(feature = "group-e2ee"))]
409 {
410 Err(crate::ImError::unsupported("group-e2ee"))
411 }
412 }
413}
414
415#[cfg(feature = "group-e2ee")]
416fn group_status_unavailable(
417 group: crate::ids::GroupRef,
418 err: crate::ImError,
419) -> super::GroupSecureStatus {
420 let problem_code = group_status_problem_code(&err);
421 super::GroupSecureStatus {
422 group,
423 state: super::GroupSecureState::Unavailable,
424 can_send_secure: false,
425 local_readiness: super::GroupSecureLocalReadiness {
426 has_local_state: false,
427 has_active_membership: false,
428 },
429 pending_work: super::GroupSecurePendingWork::default(),
430 problem: Some(super::SecureProblem {
431 code: problem_code,
432 message: format!("group E2EE status is unavailable: {err}"),
433 retryable: true,
434 }),
435 warnings: vec!["group E2EE status is unavailable".to_owned()],
436 }
437}
438
439#[cfg(feature = "group-e2ee")]
440fn group_status_can_downgrade_error(err: &crate::ImError) -> bool {
441 matches!(
442 err,
443 crate::ImError::IdentityRequired
444 | crate::ImError::IdentityNotFound { .. }
445 | crate::ImError::DefaultIdentityMissing
446 | crate::ImError::IdentityNotReady { .. }
447 | crate::ImError::AuthRequired
448 | crate::ImError::SessionExpired
449 | crate::ImError::TransportUnavailable { .. }
450 | crate::ImError::LocalStateUnavailable { .. }
451 | crate::ImError::PathUnavailable { .. }
452 | crate::ImError::CredentialFileUnreadable { .. }
453 )
454}
455
456#[cfg(feature = "group-e2ee")]
457fn group_status_problem_code(err: &crate::ImError) -> super::SecureProblemCode {
458 match err {
459 crate::ImError::TransportUnavailable { .. } => {
460 super::SecureProblemCode::TransportUnavailable
461 }
462 crate::ImError::LocalStateUnavailable { .. } => {
463 super::SecureProblemCode::LocalStateUnavailable
464 }
465 _ => super::SecureProblemCode::IdentityNotReady,
466 }
467}
468
469#[cfg(feature = "group-e2ee")]
470fn group_status_to_dto(
471 status: crate::internal::group_e2ee::status::GroupE2eeStatusResult,
472) -> crate::ImResult<super::GroupSecureStatus> {
473 Ok(super::GroupSecureStatus {
474 group: status.group,
475 state: status.state,
476 can_send_secure: status.can_send_secure,
477 local_readiness: status.local_readiness,
478 pending_work: status.pending_work,
479 problem: status.problem,
480 warnings: status.warnings,
481 })
482}
483
484#[cfg(feature = "group-e2ee")]
485fn group_repair_to_dto(
486 result: crate::internal::group_e2ee::repair::GroupE2eeRepairResult,
487) -> crate::ImResult<super::GroupSecureRepairResult> {
488 Ok(super::GroupSecureRepairResult {
489 group: result.group,
490 state: result.state,
491 repaired: result.repaired,
492 problem: result.problem,
493 warnings: result.warnings,
494 })
495}
496
497pub struct SecureOutboxService<'a> {
498 client: &'a crate::core::ImClient,
499}
500
501impl SecureOutboxService<'_> {
502 pub fn list_failed(&self) -> crate::ImResult<Vec<super::SecureOutboxEntry>> {
503 #[cfg(all(feature = "sqlite", feature = "blocking"))]
504 {
505 let connection = crate::internal::local_state::open_writable(
506 &self.client.core_inner().sdk_paths().local_state.sqlite_path,
507 )?;
508 let scope =
509 crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
510 let entries = crate::internal::store::e2ee_outbox::list_e2ee_outbox(
511 &connection,
512 &scope,
513 Some("failed"),
514 )?
515 .iter()
516 .map(crate::internal::store::e2ee_outbox::secure_outbox_entry_from_record)
517 .collect::<crate::ImResult<Vec<_>>>()?;
518 Ok(entries)
519 }
520 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
521 {
522 let _ = self.client.current_identity();
523 Err(crate::ImError::unsupported("sync-secure-outbox"))
524 }
525 #[cfg(not(feature = "sqlite"))]
526 {
527 let _ = self.client.current_identity();
528 Ok(Vec::new())
529 }
530 }
531
532 pub async fn list_failed_async(&self) -> crate::ImResult<Vec<super::SecureOutboxEntry>> {
533 #[cfg(feature = "sqlite")]
534 {
535 let db = self.client.core_inner().local_state_db().await?;
536 let scope =
537 crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
538 db.list_e2ee_outbox(scope, Some("failed".to_owned()))
539 .await?
540 .iter()
541 .map(crate::internal::store::e2ee_outbox::secure_outbox_entry_from_record)
542 .collect::<crate::ImResult<Vec<_>>>()
543 }
544 #[cfg(not(feature = "sqlite"))]
545 {
546 let _ = self.client.current_identity();
547 Ok(Vec::new())
548 }
549 }
550
551 pub fn retry(
552 &self,
553 outbox_id: super::SecureOutboxId,
554 ) -> crate::ImResult<super::SecureOutboxResult> {
555 #[cfg(all(feature = "sqlite", feature = "blocking"))]
556 {
557 let connection = crate::internal::local_state::open_writable(
558 &self.client.core_inner().sdk_paths().local_state.sqlite_path,
559 )?;
560 let scope =
561 crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
562 let outbox_id_value = outbox_id.as_str().to_owned();
563 outbox_result_from_record(
564 outbox_id,
565 crate::internal::store::e2ee_outbox::retry_e2ee_outbox(
566 &connection,
567 &scope,
568 &outbox_id_value,
569 )?,
570 )
571 }
572 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
573 {
574 let _ = outbox_id;
575 Err(crate::ImError::unsupported("sync-secure-outbox"))
576 }
577 #[cfg(not(feature = "sqlite"))]
578 {
579 let _ = outbox_id;
580 Err(crate::ImError::unsupported("secure-outbox"))
581 }
582 }
583
584 pub async fn retry_async(
585 &self,
586 outbox_id: super::SecureOutboxId,
587 ) -> crate::ImResult<super::SecureOutboxResult> {
588 #[cfg(feature = "sqlite")]
589 {
590 let db = self.client.core_inner().local_state_db().await?;
591 let scope =
592 crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
593 let outbox_id_value = outbox_id.as_str().to_owned();
594 outbox_result_from_record(
595 outbox_id,
596 db.retry_e2ee_outbox(scope, outbox_id_value).await?,
597 )
598 }
599 #[cfg(not(feature = "sqlite"))]
600 {
601 let _ = outbox_id;
602 Err(crate::ImError::unsupported("secure-outbox"))
603 }
604 }
605
606 pub fn drop(
607 &self,
608 outbox_id: super::SecureOutboxId,
609 ) -> crate::ImResult<super::SecureOutboxResult> {
610 #[cfg(all(feature = "sqlite", feature = "blocking"))]
611 {
612 let connection = crate::internal::local_state::open_writable(
613 &self.client.core_inner().sdk_paths().local_state.sqlite_path,
614 )?;
615 let scope =
616 crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
617 let outbox_id_value = outbox_id.as_str().to_owned();
618 outbox_result_from_record(
619 outbox_id,
620 crate::internal::store::e2ee_outbox::drop_e2ee_outbox(
621 &connection,
622 &scope,
623 &outbox_id_value,
624 )?,
625 )
626 }
627 #[cfg(all(feature = "sqlite", not(feature = "blocking")))]
628 {
629 let _ = outbox_id;
630 Err(crate::ImError::unsupported("sync-secure-outbox"))
631 }
632 #[cfg(not(feature = "sqlite"))]
633 {
634 let _ = outbox_id;
635 Err(crate::ImError::unsupported("secure-outbox"))
636 }
637 }
638
639 pub async fn drop_async(
640 &self,
641 outbox_id: super::SecureOutboxId,
642 ) -> crate::ImResult<super::SecureOutboxResult> {
643 #[cfg(feature = "sqlite")]
644 {
645 let db = self.client.core_inner().local_state_db().await?;
646 let scope =
647 crate::internal::store::e2ee_outbox::E2eeOutboxOwnerScope::for_client(self.client);
648 let outbox_id_value = outbox_id.as_str().to_owned();
649 outbox_result_from_record(
650 outbox_id,
651 db.drop_e2ee_outbox(scope, outbox_id_value).await?,
652 )
653 }
654 #[cfg(not(feature = "sqlite"))]
655 {
656 let _ = outbox_id;
657 Err(crate::ImError::unsupported("secure-outbox"))
658 }
659 }
660}
661
662#[cfg(feature = "sqlite")]
663fn outbox_result_from_record(
664 id: super::SecureOutboxId,
665 record: Option<crate::internal::store::e2ee_outbox::E2eeOutboxRecord>,
666) -> crate::ImResult<super::SecureOutboxResult> {
667 let Some(record) = record else {
668 return Err(crate::ImError::MessageNotFound {
669 message_id: id.as_str().to_owned(),
670 });
671 };
672 Ok(super::SecureOutboxResult {
673 id,
674 status: match record.local_status.trim() {
675 "sending" => super::SecureOutboxStatus::Sending,
676 "failed" => super::SecureOutboxStatus::Failed,
677 "sent" => super::SecureOutboxStatus::Sent,
678 "dropped" => super::SecureOutboxStatus::Dropped,
679 _ => super::SecureOutboxStatus::Queued,
680 },
681 delivery: None,
682 warnings: Vec::new(),
683 })
684}