1use serde::{Deserialize, Serialize};
2use std::collections::VecDeque;
3use std::sync::Arc;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
6pub struct SendMessageRequest {
7 pub target: MessageTarget,
8 pub body: MessageBody,
9 pub security: MessageSecurityMode,
10 pub client_message_id: Option<crate::ids::MessageId>,
11 pub delivery: MessageDeliveryOptions,
12 #[serde(default, skip_serializing_if = "Option::is_none")]
13 pub delegated_signing: Option<DelegatedSigningOptions>,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
17pub struct DelegatedSigningOptions {
18 #[serde(default, skip_serializing_if = "Option::is_none")]
19 pub logical_sender_did: Option<String>,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
21 pub signing_verification_method: Option<String>,
22 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub signing_key_ref: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub actor_agent_did: Option<String>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub enum MessageTarget {
30 Direct(crate::ids::PeerRef),
31 Group(crate::ids::GroupRef),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub enum MessageBody {
36 Text {
37 text: String,
38 kind: MessageKind,
39 },
40 Payload {
41 payload: serde_json::Value,
42 },
43 Attachment {
44 input: crate::attachments::AttachmentInput,
45 caption: Option<String>,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
47 mention_payload: Option<serde_json::Value>,
48 mime_type: Option<String>,
49 filename: Option<String>,
50 },
51}
52
53#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
54pub enum MessageKind {
55 Text,
56 Markdown,
57}
58
59#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60pub enum MessageSecurityMode {
61 DefaultPlain,
62 Plain,
63 E2eeRequired,
64 SecureDirect,
65 GroupE2ee,
66}
67
68pub type MessageSecurityPolicy = MessageSecurityMode;
69
70#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
71pub struct MessageDeliveryOptions {
72 pub idempotency_key: Option<String>,
73 pub wait_for_final_acceptance: bool,
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77pub struct SendMessageResult {
78 pub message: Message,
79 pub delivery: DeliveryState,
80 pub warnings: Vec<String>,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84pub enum DeliveryState {
85 Accepted,
86 Sent,
87 StoredLocally,
88 Failed { reason: String },
89}
90
91#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92pub struct Message {
93 pub id: crate::ids::MessageId,
94 pub thread: ThreadRef,
95 pub direction: MessageDirection,
96 pub sender: crate::ids::PeerRef,
97 pub receiver: Option<crate::ids::PeerRef>,
98 pub group: Option<crate::ids::GroupRef>,
99 pub body: MessageBodyView,
100 pub sent_at: Option<String>,
101 pub received_at: Option<String>,
102 pub metadata: MessageMetadata,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
106pub struct MessagePage {
107 pub items: Vec<Message>,
108 pub next_cursor: Option<crate::ids::Cursor>,
109 pub has_more: bool,
110 pub source: Option<String>,
111 pub resolved_dids: Vec<crate::ids::Did>,
112 pub warnings: Vec<String>,
113}
114
115impl MessagePage {
116 pub fn into_page(self) -> crate::ids::Page<Message> {
117 crate::ids::Page {
118 items: self.items,
119 next_cursor: self.next_cursor,
120 has_more: self.has_more,
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
126pub enum MessageDirection {
127 Outgoing,
128 Incoming,
129 Unknown,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub enum MessageBodyView {
134 Text { text: String, kind: MessageKind },
135 Payload { payload: serde_json::Value },
136 Unsupported { content_type: Option<String> },
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
140pub struct MessageMetadata {
141 #[serde(default)]
142 pub operation_id: Option<String>,
143 #[serde(default)]
144 pub delivery_state: Option<String>,
145 #[serde(default)]
146 pub send_state: Option<MessageSendState>,
147 #[serde(default)]
148 pub retry_plan: Option<MessageRetryPlan>,
149 #[serde(default)]
150 pub server_sequence: Option<i64>,
151 #[serde(default)]
152 pub content_type: Option<String>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub conversation_identity: Option<ConversationIdentity>,
155 #[serde(default)]
156 pub attributes: Vec<MessageMetadataAttribute>,
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
160pub struct ConversationIdentity {
161 pub conversation_id: String,
162 pub canonical_thread_kind: String,
163 pub canonical_thread_id: String,
164 pub storage_thread_ref: ConversationStorageThreadRef,
165 #[serde(default)]
166 pub aliases: Vec<ConversationAlias>,
167 pub identity_scope: ConversationIdentityScope,
168 pub migration_state: ConversationMigrationState,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172pub struct ConversationStorageThreadRef {
173 pub kind: String,
174 pub id: String,
175}
176
177#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
178pub struct ConversationAlias {
179 pub kind: String,
180 pub id: String,
181 pub source: ConversationAliasSource,
182}
183
184#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
185#[serde(rename_all = "snake_case")]
186pub enum ConversationAliasSource {
187 LegacyDirectDid,
188 OldFlutterSortedDirect,
189 PeerScopeStorage,
190 GroupStorage,
191 ThreadStorage,
192 Unknown,
193}
194
195#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
196#[serde(rename_all = "snake_case")]
197pub enum ConversationIdentityScope {
198 Direct,
199 Group,
200 Thread,
201 Mail,
202 Unknown,
203}
204
205#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
206#[serde(rename_all = "snake_case")]
207pub enum ConversationMigrationState {
208 Canonical,
209 AliasResolved,
210 LegacyInput,
211 Unknown,
212}
213
214#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
215pub struct MessageSendState {
216 pub state: MessageSendStateKind,
217 pub operation_id: Option<String>,
218 pub message_id: Option<crate::ids::MessageId>,
219 pub reason: Option<String>,
220 pub updated_at: Option<String>,
221}
222
223#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
224#[serde(rename_all = "snake_case")]
225pub enum MessageSendStateKind {
226 Pending,
227 Accepted,
228 Sent,
229 StoredLocally,
230 Failed,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
234pub struct MessageRetryPlan {
235 pub retryable: bool,
236 pub action: MessageRetryAction,
237 pub operation_id: Option<String>,
238 pub message_id: Option<crate::ids::MessageId>,
239 pub reason: Option<String>,
240}
241
242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(rename_all = "snake_case")]
244pub enum MessageRetryAction {
245 None,
246 RetryDirectText,
247 RetryGroupText,
248 RetryDirectPayload,
249 RetryGroupPayload,
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub struct MessageMetadataAttribute {
254 pub key: String,
255 pub value: String,
256}
257
258#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
259pub enum ThreadRef {
260 Direct(crate::ids::PeerRef),
261 Group(crate::ids::GroupRef),
262 Thread(crate::ids::ThreadId),
263}
264
265impl ConversationIdentity {
266 pub fn from_thread_ref(thread: &ThreadRef) -> Self {
267 let (kind, id) = thread_ref_parts(thread);
268 Self::from_storage_parts(kind, id)
269 }
270
271 pub fn from_thread_ref_for_owner(thread: &ThreadRef, owner_did: &str) -> Self {
272 let (kind, id) = thread_ref_parts(thread);
273 Self::from_storage_parts_for_owner(kind, id, owner_did)
274 }
275
276 pub fn from_storage_parts(kind: impl Into<String>, id: impl Into<String>) -> Self {
277 Self::from_storage_parts_with_owner(kind, id, None)
278 }
279
280 pub fn from_storage_parts_for_owner(
281 kind: impl Into<String>,
282 id: impl Into<String>,
283 owner_did: &str,
284 ) -> Self {
285 Self::from_storage_parts_with_owner(kind, id, Some(owner_did))
286 }
287
288 fn from_storage_parts_with_owner(
289 kind: impl Into<String>,
290 id: impl Into<String>,
291 owner_did: Option<&str>,
292 ) -> Self {
293 let kind = kind.into();
294 let id = id.into();
295 let conversation_id = canonical_conversation_id_for_storage_parts(&kind, &id, owner_did);
296 let identity_scope = identity_scope_for_kind_id(&kind, &id);
297 let migration_state =
298 migration_state_for_storage_parts(&kind, &id, &conversation_id, &identity_scope);
299 let aliases = aliases_for_storage_parts(&kind, &id, &conversation_id, &identity_scope);
300 Self {
301 conversation_id: conversation_id.clone(),
302 canonical_thread_kind: canonical_thread_kind(&identity_scope).to_owned(),
303 canonical_thread_id: conversation_id,
304 storage_thread_ref: ConversationStorageThreadRef { kind, id },
305 aliases,
306 identity_scope,
307 migration_state,
308 }
309 }
310}
311
312pub fn direct_peer_scope_thread_id(
313 user_id: impl AsRef<str>,
314 full_handle: impl AsRef<str>,
315) -> crate::ImResult<crate::ids::ThreadId> {
316 let scope = crate::internal::local_state::owner_scope::DirectPeerScope::new(
317 user_id.as_ref().to_owned(),
318 full_handle.as_ref().to_owned(),
319 )?;
320 crate::ids::ThreadId::parse(
321 crate::internal::local_state::owner_scope::direct_conversation_id_for_peer_scope(&scope),
322 )
323}
324
325#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
326pub struct InboxQuery {
327 pub scope: InboxScope,
328 pub limit: crate::ids::PageLimit,
329 pub cursor: Option<crate::ids::Cursor>,
330 pub unread_only: bool,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub inbox_history_options: Option<InboxHistoryOptions>,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336pub enum InboxScope {
337 All,
338 DirectOnly,
339 GroupOnly,
340}
341
342#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
343pub struct HistoryQuery {
344 pub limit: crate::ids::PageLimit,
345 pub cursor: Option<crate::ids::Cursor>,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
347 pub inbox_history_options: Option<InboxHistoryOptions>,
348}
349
350#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
351pub struct LocalHistoryQuery {
352 pub limit: crate::ids::PageLimit,
353 pub cursor: Option<crate::ids::Cursor>,
354}
355
356#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
357pub struct InboxHistoryOptions {
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub inbox_owner_did: Option<String>,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub inbox_auth_verification_method: Option<String>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
363 pub inbox_auth_key_ref: Option<String>,
364 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub inbox_auth: Option<InboxAuth>,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
369#[serde(tag = "kind", rename_all = "snake_case")]
370pub enum InboxAuth {
371 ScopedInboxToken { token: ScopedInboxToken },
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375pub struct ScopedInboxToken {
376 pub token: String,
377}
378
379#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
380pub struct MarkReadResult {
381 pub updated_count: u32,
382 pub message_ids: Vec<crate::ids::MessageId>,
383 pub warnings: Vec<String>,
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387pub struct MarkThreadReadRequest {
388 pub thread: ThreadRef,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
390 pub watermark: Option<ReadWatermark>,
391 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub fallback_max_message_ids: Option<u32>,
393}
394
395#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
396pub struct MarkConversationReadRequest {
397 pub conversation: ConversationReadRef,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
399 pub watermark: Option<ReadWatermark>,
400 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub fallback_max_message_ids: Option<u32>,
402}
403
404#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
405pub struct ReadWatermark {
406 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub last_read_message_id: Option<crate::ids::MessageId>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
409 pub last_read_thread_seq: Option<String>,
410 #[serde(default, skip_serializing_if = "Option::is_none")]
411 pub read_at: Option<chrono::DateTime<chrono::Utc>>,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
415pub struct MarkThreadReadResult {
416 pub updated_count: u32,
417 pub remote_acknowledged: bool,
418 pub partial: bool,
419 pub fallback_used: bool,
420 pub pending_remote_ack: bool,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub effective_watermark: Option<ReadWatermark>,
423 pub legacy_message_ids: Vec<crate::ids::MessageId>,
424 pub warnings: Vec<String>,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428pub struct ConversationReadRef {
429 pub conversation_id: String,
430}
431
432impl ConversationReadRef {
433 pub fn new(conversation_id: impl Into<String>) -> crate::ImResult<Self> {
434 let conversation_id = conversation_id.into();
435 let conversation_id = conversation_id.trim();
436 if conversation_id.is_empty() {
437 return Err(crate::ImError::invalid_input(
438 Some("conversation_id".to_owned()),
439 "conversation_id must not be empty",
440 ));
441 }
442 Ok(Self {
443 conversation_id: conversation_id.to_owned(),
444 })
445 }
446
447 pub fn as_thread_ref(&self) -> crate::ImResult<ThreadRef> {
448 crate::ids::ThreadId::parse(&self.conversation_id).map(ThreadRef::Thread)
449 }
450}
451
452#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
453pub struct SendConversationTextRequest {
454 pub conversation: ConversationReadRef,
455 pub text: String,
456 pub markdown: bool,
457 pub security: MessageSecurityMode,
458 pub client_message_id: Option<crate::ids::MessageId>,
459 pub idempotency_key: Option<String>,
460 pub wait_for_final_acceptance: bool,
461 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub delegated_signing: Option<DelegatedSigningOptions>,
463}
464
465#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
466pub struct SendConversationPayloadRequest {
467 pub conversation: ConversationReadRef,
468 pub payload: serde_json::Value,
469 pub security: MessageSecurityMode,
470 pub client_message_id: Option<crate::ids::MessageId>,
471 pub idempotency_key: Option<String>,
472 pub wait_for_final_acceptance: bool,
473 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub delegated_signing: Option<DelegatedSigningOptions>,
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
478pub struct SyncThreadAfterRequest {
479 pub thread: ThreadRef,
480 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub after_server_seq: Option<String>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub limit: Option<u32>,
484}
485
486#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
487pub struct SyncConversationAfterRequest {
488 pub conversation: ConversationReadRef,
489 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub after_server_seq: Option<String>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub limit: Option<u32>,
493}
494
495#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
496pub struct SyncThreadAfterResult {
497 pub messages: Vec<Message>,
498 pub next_after_server_seq: Option<String>,
499 pub has_more: bool,
500 pub warnings: Vec<String>,
501}
502
503#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
504pub struct SyncDeltaRequest {
505 #[serde(default, skip_serializing_if = "Option::is_none")]
506 pub limit: Option<u32>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
508 pub device_id: Option<String>,
509 #[serde(default, skip_serializing_if = "Option::is_none")]
510 pub reason: Option<String>,
511}
512
513#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
514pub struct SyncDeltaResult {
515 pub events_applied: u32,
516 pub pages_fetched: u32,
517 pub last_applied_event_seq: Option<String>,
518 pub has_more: bool,
519 pub snapshot_required: bool,
520 #[serde(default, skip_serializing_if = "Option::is_none")]
521 pub retention_floor_event_seq: Option<String>,
522 pub warnings: Vec<String>,
523}
524
525#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
526pub struct ConversationListSnapshot {
527 pub format_version: u32,
528 pub im_schema_version: i64,
529 pub owner_identity_id: String,
530 pub owner_did: String,
531 pub generated_at_ms: i64,
532 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub summary_version: Option<String>,
534 pub unread_total: u32,
535 pub items: Vec<ConversationSnapshotItem>,
536}
537
538#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
539#[serde(tag = "type", rename_all = "snake_case")]
540pub enum ConversationStorePatch {
541 Reset {
542 owner_identity_id: String,
543 owner_did: String,
544 version: u64,
545 unread_total: u32,
546 items: Vec<ConversationSnapshotItem>,
547 },
548 Upsert {
549 owner_identity_id: String,
550 owner_did: String,
551 version: u64,
552 unread_total: u32,
553 item: ConversationSnapshotItem,
554 index: u32,
555 },
556 Remove {
557 owner_identity_id: String,
558 owner_did: String,
559 version: u64,
560 unread_total: u32,
561 thread_kind: String,
562 thread_id: String,
563 #[serde(default, skip_serializing_if = "Option::is_none")]
564 conversation_identity: Option<ConversationIdentity>,
565 },
566 Reorder {
567 owner_identity_id: String,
568 owner_did: String,
569 version: u64,
570 unread_total: u32,
571 thread_kind: String,
572 thread_id: String,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
574 conversation_identity: Option<ConversationIdentity>,
575 index: u32,
576 },
577 RepairRequired {
578 owner_identity_id: String,
579 owner_did: String,
580 version: u64,
581 unread_total: u32,
582 reason: String,
583 },
584}
585
586#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
587#[serde(tag = "type", rename_all = "snake_case")]
588pub enum ThreadMessageStorePatch {
589 Reset {
590 owner_identity_id: String,
591 owner_did: String,
592 version: u64,
593 thread_kind: String,
594 thread_id: String,
595 #[serde(default, skip_serializing_if = "Option::is_none")]
596 conversation_identity: Option<ConversationIdentity>,
597 items: Vec<Message>,
598 },
599 Upsert {
600 owner_identity_id: String,
601 owner_did: String,
602 version: u64,
603 thread_kind: String,
604 thread_id: String,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
606 conversation_identity: Option<ConversationIdentity>,
607 message: Message,
608 index: u32,
609 },
610 Remove {
611 owner_identity_id: String,
612 owner_did: String,
613 version: u64,
614 thread_kind: String,
615 thread_id: String,
616 #[serde(default, skip_serializing_if = "Option::is_none")]
617 conversation_identity: Option<ConversationIdentity>,
618 message_id: String,
619 },
620 RepairRequired {
621 owner_identity_id: String,
622 owner_did: String,
623 version: u64,
624 thread_kind: String,
625 thread_id: String,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 conversation_identity: Option<ConversationIdentity>,
628 reason: String,
629 },
630}
631
632pub struct ConversationPatchSession {
633 pub(crate) store: Arc<crate::internal::runtime_store::conversation_store::ConversationStore>,
634 pub(crate) receiver: tokio::sync::broadcast::Receiver<ConversationStorePatch>,
635 pub(crate) initial: VecDeque<ConversationStorePatch>,
636 closed: bool,
637}
638
639impl ConversationPatchSession {
640 pub(crate) fn new(
641 store: Arc<crate::internal::runtime_store::conversation_store::ConversationStore>,
642 receiver: tokio::sync::broadcast::Receiver<ConversationStorePatch>,
643 initial: Vec<ConversationStorePatch>,
644 ) -> Self {
645 Self {
646 store,
647 receiver,
648 initial: initial.into(),
649 closed: false,
650 }
651 }
652
653 pub async fn next_patch(&mut self) -> Option<ConversationStorePatch> {
654 if self.closed {
655 return None;
656 }
657 if let Some(patch) = self.initial.pop_front() {
658 return Some(patch);
659 }
660 loop {
661 match self.receiver.recv().await {
662 Ok(patch) => return Some(patch),
663 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
664 return Some(self.store.repair_required_patch("subscriber_lag"));
665 }
666 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
667 }
668 }
669 }
670
671 pub async fn stop(&mut self) -> crate::ImResult<()> {
672 self.closed = true;
673 self.receiver.resubscribe();
674 Ok(())
675 }
676}
677
678pub struct ThreadMessagePatchSession {
679 pub(crate) store: Arc<crate::internal::runtime_store::message_store::MessageStore>,
680 pub(crate) receiver: tokio::sync::broadcast::Receiver<ThreadMessageStorePatch>,
681 pub(crate) initial: VecDeque<ThreadMessageStorePatch>,
682 pub(crate) thread: ThreadRef,
683 pub(crate) limit: u32,
684 closed: bool,
685}
686
687impl ThreadMessagePatchSession {
688 pub(crate) fn new(
689 store: Arc<crate::internal::runtime_store::message_store::MessageStore>,
690 receiver: tokio::sync::broadcast::Receiver<ThreadMessageStorePatch>,
691 initial: Vec<ThreadMessageStorePatch>,
692 thread: ThreadRef,
693 limit: u32,
694 ) -> Self {
695 Self {
696 store,
697 receiver,
698 initial: initial.into(),
699 thread,
700 limit,
701 closed: false,
702 }
703 }
704
705 pub async fn next_patch(&mut self) -> Option<ThreadMessageStorePatch> {
706 if self.closed {
707 return None;
708 }
709 if let Some(patch) = self.initial.pop_front() {
710 return Some(patch);
711 }
712 loop {
713 match self.receiver.recv().await {
714 Ok(patch) if thread_patch_matches(&patch, &self.thread) => return Some(patch),
715 Ok(_) => continue,
716 Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
717 return Some(self.store.repair_required_patch(
718 &self.thread,
719 self.limit,
720 "subscriber_lag",
721 ));
722 }
723 Err(tokio::sync::broadcast::error::RecvError::Closed) => return None,
724 }
725 }
726 }
727
728 pub async fn stop(&mut self) -> crate::ImResult<()> {
729 self.closed = true;
730 self.receiver.resubscribe();
731 Ok(())
732 }
733}
734
735fn thread_patch_matches(patch: &ThreadMessageStorePatch, thread: &ThreadRef) -> bool {
736 let (expected_kind, expected_id) = thread_ref_parts(thread);
737 match patch {
738 ThreadMessageStorePatch::Reset {
739 thread_kind,
740 thread_id,
741 ..
742 }
743 | ThreadMessageStorePatch::Upsert {
744 thread_kind,
745 thread_id,
746 ..
747 }
748 | ThreadMessageStorePatch::Remove {
749 thread_kind,
750 thread_id,
751 ..
752 }
753 | ThreadMessageStorePatch::RepairRequired {
754 thread_kind,
755 thread_id,
756 ..
757 } => thread_kind == &expected_kind && thread_id == &expected_id,
758 }
759}
760
761pub(crate) fn thread_ref_parts(thread: &ThreadRef) -> (String, String) {
762 match thread {
763 ThreadRef::Direct(peer) => ("direct".to_owned(), peer.as_str().to_owned()),
764 ThreadRef::Group(group) => ("group".to_owned(), group.as_str().to_owned()),
765 ThreadRef::Thread(thread) => ("thread".to_owned(), thread.as_str().to_owned()),
766 }
767}
768
769fn canonical_conversation_id_for_storage_parts(
770 kind: &str,
771 id: &str,
772 owner_did: Option<&str>,
773) -> String {
774 match kind.trim() {
775 "direct" => crate::internal::local_state::owner_scope::direct_conversation_id(id),
776 "group" => crate::internal::local_state::owner_scope::group_conversation_id(id),
777 "thread" if id.trim().starts_with("dm:peer-scope:") => id.trim().to_owned(),
778 "thread" if id.trim().starts_with("dm:") => owner_did
779 .and_then(|owner_did| {
780 crate::internal::local_state::owner_scope::direct_conversation_id_from_thread_alias(
781 id, owner_did,
782 )
783 })
784 .unwrap_or_else(|| id.trim().to_owned()),
785 "thread" if id.trim().starts_with("group:") => id.trim().to_owned(),
786 "thread" if id.trim().starts_with("mail:") => id.trim().to_owned(),
787 "thread" => id.trim().to_owned(),
788 _ if !id.trim().is_empty() => id.trim().to_owned(),
789 _ => "unknown".to_owned(),
790 }
791}
792
793fn identity_scope_for_kind_id(kind: &str, id: &str) -> ConversationIdentityScope {
794 match kind.trim() {
795 "direct" => ConversationIdentityScope::Direct,
796 "group" => ConversationIdentityScope::Group,
797 "thread" if id.trim().starts_with("dm:") => ConversationIdentityScope::Direct,
798 "thread" if id.trim().starts_with("group:") => ConversationIdentityScope::Group,
799 "thread" if id.trim().starts_with("mail:") => ConversationIdentityScope::Mail,
800 "thread" => ConversationIdentityScope::Thread,
801 _ => ConversationIdentityScope::Unknown,
802 }
803}
804
805fn canonical_thread_kind(scope: &ConversationIdentityScope) -> &'static str {
806 match scope {
807 ConversationIdentityScope::Direct => "direct",
808 ConversationIdentityScope::Group => "group",
809 ConversationIdentityScope::Mail => "mail",
810 ConversationIdentityScope::Thread => "thread",
811 ConversationIdentityScope::Unknown => "unknown",
812 }
813}
814
815fn migration_state_for_storage_parts(
816 kind: &str,
817 id: &str,
818 conversation_id: &str,
819 scope: &ConversationIdentityScope,
820) -> ConversationMigrationState {
821 match (kind.trim(), scope) {
822 ("direct", ConversationIdentityScope::Direct) => ConversationMigrationState::LegacyInput,
823 ("group", ConversationIdentityScope::Group) => ConversationMigrationState::LegacyInput,
824 ("thread", ConversationIdentityScope::Direct)
825 if id.trim().starts_with("dm:") && !id.trim().starts_with("dm:peer-scope:") =>
826 {
827 ConversationMigrationState::LegacyInput
828 }
829 ("thread", _) if id.trim() == conversation_id => ConversationMigrationState::Canonical,
830 ("thread", _) => ConversationMigrationState::AliasResolved,
831 _ => ConversationMigrationState::Unknown,
832 }
833}
834
835fn aliases_for_storage_parts(
836 kind: &str,
837 id: &str,
838 conversation_id: &str,
839 scope: &ConversationIdentityScope,
840) -> Vec<ConversationAlias> {
841 let source = match (kind.trim(), scope) {
842 ("direct", ConversationIdentityScope::Direct) => ConversationAliasSource::LegacyDirectDid,
843 ("group", ConversationIdentityScope::Group) => ConversationAliasSource::GroupStorage,
844 ("thread", ConversationIdentityScope::Direct)
845 if id.trim().starts_with("dm:peer-scope:") =>
846 {
847 ConversationAliasSource::PeerScopeStorage
848 }
849 ("thread", ConversationIdentityScope::Direct) if is_old_flutter_direct_alias(id) => {
850 ConversationAliasSource::OldFlutterSortedDirect
851 }
852 ("thread", ConversationIdentityScope::Direct) if id.trim().starts_with("dm:") => {
853 ConversationAliasSource::LegacyDirectDid
854 }
855 ("thread", _) => ConversationAliasSource::ThreadStorage,
856 _ => ConversationAliasSource::Unknown,
857 };
858 if id.trim() == conversation_id.trim() && source == ConversationAliasSource::PeerScopeStorage {
859 return Vec::new();
860 }
861 vec![ConversationAlias {
862 kind: kind.trim().to_owned(),
863 id: id.trim().to_owned(),
864 source,
865 }]
866}
867
868fn is_old_flutter_direct_alias(id: &str) -> bool {
869 id.trim()
870 .strip_prefix("dm:")
871 .map(|value| value.contains(":did:"))
872 .unwrap_or(false)
873}
874
875#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
876pub struct ConversationSnapshotItem {
877 pub thread_kind: String,
878 pub thread_id: String,
879 #[serde(default, skip_serializing_if = "Option::is_none")]
880 pub conversation_identity: Option<ConversationIdentity>,
881 pub participants: Vec<String>,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
883 pub last_message: Option<ConversationSnapshotMessage>,
884 pub unread_count: u32,
885 #[serde(default)]
886 pub unread_mention_count: u32,
887 #[serde(default, skip_serializing_if = "Option::is_none")]
888 pub first_unread_mention_message_id: Option<String>,
889 pub message_count: u32,
890 #[serde(default, skip_serializing_if = "Option::is_none")]
891 pub last_message_at: Option<String>,
892}
893
894#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
895pub struct ConversationSnapshotMessage {
896 pub id: String,
897 pub thread_kind: String,
898 pub thread_id: String,
899 #[serde(default, skip_serializing_if = "Option::is_none")]
900 pub conversation_identity: Option<ConversationIdentity>,
901 pub direction: String,
902 pub sender: String,
903 #[serde(default, skip_serializing_if = "Option::is_none")]
904 pub receiver: Option<String>,
905 #[serde(default, skip_serializing_if = "Option::is_none")]
906 pub group: Option<String>,
907 pub body: ConversationSnapshotMessageBody,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
909 pub sent_at: Option<String>,
910 #[serde(default, skip_serializing_if = "Option::is_none")]
911 pub received_at: Option<String>,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
913 pub server_sequence: Option<i64>,
914 #[serde(default, skip_serializing_if = "Option::is_none")]
915 pub content_type: Option<String>,
916 #[serde(default)]
917 pub attributes: Vec<MessageMetadataAttribute>,
918}
919
920#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
921pub struct ConversationSnapshotMessageBody {
922 #[serde(default, skip_serializing_if = "Option::is_none")]
923 pub text: Option<String>,
924 #[serde(default, skip_serializing_if = "Option::is_none")]
925 pub kind: Option<String>,
926 #[serde(default, skip_serializing_if = "Option::is_none")]
927 pub payload_json: Option<String>,
928 #[serde(default, skip_serializing_if = "Option::is_none")]
929 pub unsupported_content_type: Option<String>,
930}
931
932#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
933pub struct Conversation {
934 pub thread: ThreadRef,
935 #[serde(default, skip_serializing_if = "Option::is_none")]
936 pub conversation_identity: Option<ConversationIdentity>,
937 pub title: Option<String>,
938 pub participants: Vec<crate::ids::PeerRef>,
939 pub last_message: Option<Message>,
940 pub unread_count: u32,
941 #[serde(default)]
942 pub unread_mention_count: u32,
943 #[serde(default, skip_serializing_if = "Option::is_none")]
944 pub first_unread_mention_message_id: Option<crate::ids::MessageId>,
945 pub message_count: u32,
946 pub last_message_at: Option<String>,
947}
948
949#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
950pub struct ConversationQuery {
951 pub limit: crate::ids::PageLimit,
952 #[serde(default, skip_serializing_if = "Option::is_none")]
953 pub cursor: Option<crate::ids::Cursor>,
954 pub include_groups: bool,
955 pub include_direct: bool,
956 pub unread_only: bool,
957}
958
959#[cfg(test)]
960mod conversation_identity_tests {
961 use super::*;
962
963 #[test]
964 fn conversation_identity_direct_ref_uses_ownerless_conversation_id() {
965 let peer = crate::ids::PeerRef::parse("did:example:bob", "").unwrap();
966 let identity = ConversationIdentity::from_thread_ref(&ThreadRef::Direct(peer));
967
968 assert_eq!(identity.conversation_id, "dm:did:example:bob");
969 assert_eq!(identity.canonical_thread_kind, "direct");
970 assert_eq!(identity.canonical_thread_id, "dm:did:example:bob");
971 assert_eq!(identity.storage_thread_ref.kind, "direct");
972 assert_eq!(identity.storage_thread_ref.id, "did:example:bob");
973 assert_eq!(identity.identity_scope, ConversationIdentityScope::Direct);
974 assert_eq!(
975 identity.migration_state,
976 ConversationMigrationState::LegacyInput
977 );
978 assert_eq!(identity.aliases.len(), 1);
979 assert_eq!(
980 identity.aliases[0].source,
981 ConversationAliasSource::LegacyDirectDid
982 );
983 }
984
985 #[test]
986 fn conversation_identity_peer_scope_thread_is_canonical_direct_storage() {
987 let thread_id =
988 crate::messages::direct_peer_scope_thread_id("user-1", "alice.anpclaw.com").unwrap();
989 let identity = ConversationIdentity::from_thread_ref(&ThreadRef::Thread(thread_id));
990
991 assert!(identity.conversation_id.starts_with("dm:peer-scope:v1:"));
992 assert_eq!(identity.canonical_thread_kind, "direct");
993 assert_eq!(identity.canonical_thread_id, identity.conversation_id);
994 assert_eq!(identity.storage_thread_ref.kind, "thread");
995 assert_eq!(identity.storage_thread_ref.id, identity.conversation_id);
996 assert_eq!(identity.identity_scope, ConversationIdentityScope::Direct);
997 assert_eq!(
998 identity.migration_state,
999 ConversationMigrationState::Canonical
1000 );
1001 assert!(identity.aliases.is_empty());
1002 }
1003
1004 #[test]
1005 fn conversation_identity_old_flutter_sorted_direct_is_alias_input() {
1006 let identity = ConversationIdentity::from_storage_parts(
1007 "thread",
1008 "dm:did:example:alice:did:example:bob",
1009 );
1010
1011 assert_eq!(
1012 identity.conversation_id,
1013 "dm:did:example:alice:did:example:bob"
1014 );
1015 assert_eq!(identity.canonical_thread_kind, "direct");
1016 assert_eq!(identity.identity_scope, ConversationIdentityScope::Direct);
1017 assert_eq!(
1018 identity.migration_state,
1019 ConversationMigrationState::LegacyInput
1020 );
1021 assert_eq!(identity.aliases.len(), 1);
1022 assert_eq!(
1023 identity.aliases[0].source,
1024 ConversationAliasSource::OldFlutterSortedDirect
1025 );
1026 }
1027
1028 #[test]
1029 fn conversation_identity_old_flutter_sorted_direct_resolves_with_owner() {
1030 let identity = ConversationIdentity::from_storage_parts_for_owner(
1031 "thread",
1032 "dm:did:example:alice:did:example:bob",
1033 "did:example:alice",
1034 );
1035
1036 assert_eq!(identity.conversation_id, "dm:did:example:bob");
1037 assert_eq!(identity.canonical_thread_kind, "direct");
1038 assert_eq!(identity.canonical_thread_id, "dm:did:example:bob");
1039 assert_eq!(identity.storage_thread_ref.kind, "thread");
1040 assert_eq!(
1041 identity.storage_thread_ref.id,
1042 "dm:did:example:alice:did:example:bob"
1043 );
1044 assert_eq!(identity.identity_scope, ConversationIdentityScope::Direct);
1045 assert_eq!(
1046 identity.migration_state,
1047 ConversationMigrationState::LegacyInput
1048 );
1049 assert_eq!(identity.aliases.len(), 1);
1050 assert_eq!(
1051 identity.aliases[0].source,
1052 ConversationAliasSource::OldFlutterSortedDirect
1053 );
1054 }
1055
1056 #[test]
1057 fn conversation_identity_group_ref_uses_group_conversation_id() {
1058 let group = crate::ids::GroupRef::parse("did:example:group").unwrap();
1059 let identity = ConversationIdentity::from_thread_ref(&ThreadRef::Group(group));
1060
1061 assert_eq!(identity.conversation_id, "group:did:example:group");
1062 assert_eq!(identity.canonical_thread_kind, "group");
1063 assert_eq!(identity.identity_scope, ConversationIdentityScope::Group);
1064 assert_eq!(
1065 identity.migration_state,
1066 ConversationMigrationState::LegacyInput
1067 );
1068 }
1069}