pub struct ProfileReader { /* private fields */ }Implementations§
Source§impl ProfileReader
impl ProfileReader
Sourcepub fn cached_image(
&self,
id: &SourceMessageId,
quality: ImageQuality,
) -> Result<CachedImage>
pub fn cached_image( &self, id: &SourceMessageId, quality: ImageQuality, ) -> Result<CachedImage>
Resolve one exact local message through its advertised original hash and hardlink namespace. Ambiguous groups are rejected. Standard/Thumbnail require the exact resource basename/type/status/encoded-size join. A suffix alone is not a quality proof; Original also requires the XML content MD5.
Source§impl ProfileReader
impl ProfileReader
Sourcepub fn cached_hardlink_file(
&self,
kind: HardlinkKind,
md5: &str,
) -> Result<CachedBytes>
pub fn cached_hardlink_file( &self, kind: HardlinkKind, md5: &str, ) -> Result<CachedBytes>
File/video hardlinks use one month directory and the observed zero second directory sentinel. Every candidate is content-MD5 verified; ambiguous bytes or unknown path layouts are refused rather than guessed.
Source§impl ProfileReader
impl ProfileReader
Sourcepub fn peer_metadata(
&self,
peers: &[String],
) -> Result<BTreeMap<String, PeerMetadata>>
pub fn peer_metadata( &self, peers: &[String], ) -> Result<BTreeMap<String, PeerMetadata>>
Read one page’s saved metadata without refreshing contacts or catalogs. Missing peers are omitted. A malformed extra buffer affects only its peer; an incompatible database schema remains an error.
Source§impl ProfileReader
impl ProfileReader
Sourcepub fn thread_pinned(
&self,
threads: &[String],
) -> Result<BTreeMap<String, Option<bool>>>
pub fn thread_pinned( &self, threads: &[String], ) -> Result<BTreeMap<String, Option<bool>>>
Read the saved pin bit for ordinary contacts and active groups in one query. Incomplete identities and unsupported built-in peers stay unknown.
Sourcepub fn thread_muted(
&self,
threads: &[String],
) -> Result<BTreeMap<String, Option<bool>>>
pub fn thread_muted( &self, threads: &[String], ) -> Result<BTreeMap<String, Option<bool>>>
Derive notification preferences for a page without refreshing contacts. Direct contacts and groups use different official fields; incomplete identities and unexpected values remain unknown.
Sourcepub fn group_avatar_members(
&self,
rooms: &[String],
) -> Result<BTreeMap<String, Vec<GroupMember>>>
pub fn group_avatar_members( &self, rooms: &[String], ) -> Result<BTreeMap<String, Vec<GroupMember>>>
Read avatar members for a page of groups with three batched queries. Validate membership against the same official room metadata used by group_members; an inconsistent room does not hide other valid rooms.
Source§impl ProfileReader
impl ProfileReader
Sourcepub fn open(
location: ProfileLocation,
keys: KeyFile,
options: ReadOptions,
) -> Result<Self>
pub fn open( location: ProfileLocation, keys: KeyFile, options: ReadOptions, ) -> Result<Self>
Examples found in repository?
4fn main() -> Result<(), Box<dyn std::error::Error>> {
5 let args: Vec<_> = std::env::args().skip(1).collect();
6 if args.len() != 3 || args[0] != "--stopped-or-consistent-copy" {
7 return Err("usage: contacts --stopped-or-consistent-copy ACCOUNT_ROOT KEYS_FILE".into());
8 }
9 let location = ProfileLocation::new(&args[1])?;
10 let keys = KeyFile::load(&args[2])?;
11 let reader = ProfileReader::open(
12 location,
13 keys,
14 ReadOptions {
15 stopped_or_consistent_snapshot: true,
16 ..Default::default()
17 },
18 )?;
19 println!(
20 "{} contacts",
21 reader.contacts(&ListQuery::default())?.items.len()
22 );
23 Ok(())
24}More examples
4fn run() -> Result<()> {
5 let a = std::env::args().skip(1).collect::<Vec<_>>();
6 if a.len() != 3 {
7 return Err(ProfileError::InvalidQuery);
8 }
9 let location = ProfileLocation::new(&a[0])?;
10 let keys = KeyFile::load(&a[1])?;
11 let input: IncomingText =
12 serde_json::from_slice(&std::fs::read(&a[2])?).map_err(|_| ProfileError::InvalidQuery)?;
13 let options = WriteOptions {
14 exclusive_ownership_confirmed: true,
15 allow_plaintext: false,
16 durable_intent_id: "explicit-disposable-fixture-validation".into(),
17 };
18 let first = apply_incoming_text(&location, &keys, &input, &options)?;
19 let retry = apply_incoming_text(&location, &keys, &input, &options)?;
20 let read = mark_read_messages(
21 &location,
22 &keys,
23 &input.peer,
24 std::slice::from_ref(&first.source_id),
25 &options,
26 )?;
27 let read_retry = mark_read_messages(
28 &location,
29 &keys,
30 &input.peer,
31 std::slice::from_ref(&first.source_id),
32 &options,
33 )?;
34 let reader = ProfileReader::open(
35 location,
36 keys,
37 ReadOptions {
38 stopped_or_consistent_snapshot: true,
39 ..Default::default()
40 },
41 )?;
42 let page = reader.messages(&MessageQuery {
43 chat: Some(input.peer),
44 limit: Some(200),
45 ..Default::default()
46 })?;
47 let found = page
48 .items
49 .iter()
50 .find(|v| v.id == first.source_id)
51 .ok_or(ProfileError::InvalidMetadata)?;
52 let exact = found.content.text.as_deref() == Some(input.content.as_str())
53 && found.source.text.as_deref() == Some(input.source_xml.as_str())
54 && found.server_message_id.as_deref() == Some(input.server_message_id.as_str());
55 if !exact || retry.message_inserted || read_retry.removed_unread_rows != 0 {
56 return Err(ProfileError::InvalidMetadata);
57 }
58 println!(
59 "{}",
60 serde_json::json!({"newTextInserted":first.message_inserted,"retryInsertedDuplicate":retry.message_inserted,"exactBodySourceAndServerIdentity":exact,"removedUnreadRows":read.removed_unread_rows,"readRetryRemovedRows":read_retry.removed_unread_rows,"unreadBefore":read.unread_count_before,"unreadAfter":read.unread_count_after,"crossDatabaseAtomic":first.cross_database_atomic,"networkUsed":false,"officialRuntimeUsed":false})
61 );
62 Ok(())
63}4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}pub fn generation(&self) -> &str
pub fn schemas(&self) -> &[SchemaReport]
Sourcepub fn coverage(&self) -> Coverage
pub fn coverage(&self) -> Coverage
Examples found in repository?
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}Sourcepub fn inventory(&self) -> &[DatabaseInventory]
pub fn inventory(&self) -> &[DatabaseInventory]
Examples found in repository?
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}pub fn require_writable(&self) -> Result<()>
Sourcepub fn contacts(&self, q: &ListQuery) -> Result<Page<Contact>>
pub fn contacts(&self, q: &ListQuery) -> Result<Page<Contact>>
Examples found in repository?
4fn main() -> Result<(), Box<dyn std::error::Error>> {
5 let args: Vec<_> = std::env::args().skip(1).collect();
6 if args.len() != 3 || args[0] != "--stopped-or-consistent-copy" {
7 return Err("usage: contacts --stopped-or-consistent-copy ACCOUNT_ROOT KEYS_FILE".into());
8 }
9 let location = ProfileLocation::new(&args[1])?;
10 let keys = KeyFile::load(&args[2])?;
11 let reader = ProfileReader::open(
12 location,
13 keys,
14 ReadOptions {
15 stopped_or_consistent_snapshot: true,
16 ..Default::default()
17 },
18 )?;
19 println!(
20 "{} contacts",
21 reader.contacts(&ListQuery::default())?.items.len()
22 );
23 Ok(())
24}More examples
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}pub fn thread_exists(&self, username: &str) -> Result<bool>
Sourcepub fn threads(&self, q: &ListQuery) -> Result<Page<Thread>>
pub fn threads(&self, q: &ListQuery) -> Result<Page<Thread>>
Examples found in repository?
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}pub fn labels(&self) -> Result<BTreeMap<String, String>>
Sourcepub fn message_references(
&self,
chat: Option<&str>,
max_items: usize,
) -> Result<Vec<MessageReference>>
pub fn message_references( &self, chat: Option<&str>, max_items: usize, ) -> Result<Vec<MessageReference>>
Capture complete history membership in one pass per shard. This reads
only IDs/timestamps; callers resolve delivered content with
messages_by_ids against this same snapshot. No state is cached across
snapshots, and incompatible message tables remain errors.
Sourcepub fn latest_messages(
&self,
chats: &[String],
own_username: Option<&str>,
) -> Result<BTreeMap<String, Message>>
pub fn latest_messages( &self, chats: &[String], own_username: Option<&str>, ) -> Result<BTreeMap<String, Message>>
Read the latest saved message for each requested conversation in one snapshot. One compound query per shard selects one indexed row per conversation; no point lookups or older-body decoding are needed.
Sourcepub fn messages(&self, q: &MessageQuery) -> Result<Page<Message>>
pub fn messages(&self, q: &MessageQuery) -> Result<Page<Message>>
Examples found in repository?
4fn run() -> Result<()> {
5 let a = std::env::args().skip(1).collect::<Vec<_>>();
6 if a.len() != 3 {
7 return Err(ProfileError::InvalidQuery);
8 }
9 let location = ProfileLocation::new(&a[0])?;
10 let keys = KeyFile::load(&a[1])?;
11 let input: IncomingText =
12 serde_json::from_slice(&std::fs::read(&a[2])?).map_err(|_| ProfileError::InvalidQuery)?;
13 let options = WriteOptions {
14 exclusive_ownership_confirmed: true,
15 allow_plaintext: false,
16 durable_intent_id: "explicit-disposable-fixture-validation".into(),
17 };
18 let first = apply_incoming_text(&location, &keys, &input, &options)?;
19 let retry = apply_incoming_text(&location, &keys, &input, &options)?;
20 let read = mark_read_messages(
21 &location,
22 &keys,
23 &input.peer,
24 std::slice::from_ref(&first.source_id),
25 &options,
26 )?;
27 let read_retry = mark_read_messages(
28 &location,
29 &keys,
30 &input.peer,
31 std::slice::from_ref(&first.source_id),
32 &options,
33 )?;
34 let reader = ProfileReader::open(
35 location,
36 keys,
37 ReadOptions {
38 stopped_or_consistent_snapshot: true,
39 ..Default::default()
40 },
41 )?;
42 let page = reader.messages(&MessageQuery {
43 chat: Some(input.peer),
44 limit: Some(200),
45 ..Default::default()
46 })?;
47 let found = page
48 .items
49 .iter()
50 .find(|v| v.id == first.source_id)
51 .ok_or(ProfileError::InvalidMetadata)?;
52 let exact = found.content.text.as_deref() == Some(input.content.as_str())
53 && found.source.text.as_deref() == Some(input.source_xml.as_str())
54 && found.server_message_id.as_deref() == Some(input.server_message_id.as_str());
55 if !exact || retry.message_inserted || read_retry.removed_unread_rows != 0 {
56 return Err(ProfileError::InvalidMetadata);
57 }
58 println!(
59 "{}",
60 serde_json::json!({"newTextInserted":first.message_inserted,"retryInsertedDuplicate":retry.message_inserted,"exactBodySourceAndServerIdentity":exact,"removedUnreadRows":read.removed_unread_rows,"readRetryRemovedRows":read_retry.removed_unread_rows,"unreadBefore":read.unread_count_before,"unreadAfter":read.unread_count_after,"crossDatabaseAtomic":first.cross_database_atomic,"networkUsed":false,"officialRuntimeUsed":false})
61 );
62 Ok(())
63}More examples
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}Sourcepub fn message(
&self,
id: &SourceMessageId,
own_username: Option<&str>,
) -> Result<Message>
pub fn message( &self, id: &SourceMessageId, own_username: Option<&str>, ) -> Result<Message>
Read one source-qualified observation without paging through history. Resolve sender numbers only through this database’s own Name2Id map.
Sourcepub fn messages_by_ids(
&self,
ids: &[SourceMessageId],
own_username: Option<&str>,
) -> Result<Vec<Message>>
pub fn messages_by_ids( &self, ids: &[SourceMessageId], own_username: Option<&str>, ) -> Result<Vec<Message>>
Resolve at most one page without decoding unrelated history. Missing rows are omitted; unreadable databases and incompatible records remain errors. Source-qualified IDs preserve each shard’s own sender namespace.
Sourcepub fn messages_by_server_id(
&self,
chat: &str,
server_id: &str,
own_username: Option<&str>,
) -> Result<Vec<Message>>
pub fn messages_by_server_id( &self, chat: &str, server_id: &str, own_username: Option<&str>, ) -> Result<Vec<Message>>
Resolve an unsigned server ID in one conversation only. Return up to two observations so callers can reject ambiguous IDs across shards.
Sourcepub fn visit_messages_by_server_ids<E>(
&self,
targets: &[(String, String)],
own_username: Option<&str>,
visit: impl FnMut(Message) -> Result<(), E>,
) -> Result<(), E>
pub fn visit_messages_by_server_ids<E>( &self, targets: &[(String, String)], own_username: Option<&str>, visit: impl FnMut(Message) -> Result<(), E>, ) -> Result<(), E>
Read quoted originals for one page from this captured snapshot. Group by conversation/shard, decode only matching rows, and retain at most two observations per identity just like the single-reference lookup. The visitor consumes each row before the next, avoiding a page of large bodies.
Sourcepub fn groups(&self, q: &ListQuery) -> Result<Page<Group>>
pub fn groups(&self, q: &ListQuery) -> Result<Page<Group>>
Examples found in repository?
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}pub fn group_mention_permissions( &self, room: &str, own: Option<&str>, ) -> Result<GroupMentionPermissions>
Sourcepub fn group_members(
&self,
room: &str,
q: &ListQuery,
) -> Result<Page<GroupMember>>
pub fn group_members( &self, room: &str, q: &ListQuery, ) -> Result<Page<GroupMember>>
Examples found in repository?
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}Sourcepub fn avatar(&self, username: &str) -> Result<CachedBytes>
pub fn avatar(&self, username: &str) -> Result<CachedBytes>
Examples found in repository?
4fn run() -> Result<()> {
5 let mut args = std::env::args().skip(1);
6 let path = args.next().ok_or(ProfileError::InvalidQuery)?;
7 let keys = args.next();
8 let location = ProfileLocation::new(path)?;
9 let (keys, plaintext) = if let Some(path) = keys {
10 (KeyFile::load(path)?, false)
11 } else {
12 (KeyFile::empty(location.account_directory()), true)
13 };
14 let r = ProfileReader::open(
15 location,
16 keys,
17 ReadOptions {
18 stopped_or_consistent_snapshot: true,
19 allow_plaintext: plaintext,
20 ..Default::default()
21 },
22 )?;
23 let mut contacts = 0;
24 let mut avatars = 0;
25 let mut q = ListQuery {
26 limit: Some(200),
27 ..Default::default()
28 };
29 loop {
30 let p = r.contacts(&q)?;
31 for c in &p.items {
32 if r.avatar(&c.username).is_ok() {
33 avatars += 1;
34 }
35 }
36 contacts += p.items.len();
37 q.cursor = p.next_cursor;
38 if q.cursor.is_none() {
39 break;
40 }
41 }
42 let mut threads = 0;
43 let mut q = ListQuery {
44 limit: Some(200),
45 ..Default::default()
46 };
47 loop {
48 let p = r.threads(&q)?;
49 threads += p.items.len();
50 q.cursor = p.next_cursor;
51 if q.cursor.is_none() {
52 break;
53 }
54 }
55 let mut groups = 0;
56 let mut members = 0;
57 let mut q = ListQuery {
58 limit: Some(200),
59 ..Default::default()
60 };
61 loop {
62 let p = r.groups(&q)?;
63 for g in &p.items {
64 let mut mq = ListQuery {
65 limit: Some(200),
66 ..Default::default()
67 };
68 loop {
69 let mp = r.group_members(&g.username, &mq)?;
70 members += mp.items.len();
71 mq.cursor = mp.next_cursor;
72 if mq.cursor.is_none() {
73 break;
74 }
75 }
76 }
77 groups += p.items.len();
78 q.cursor = p.next_cursor;
79 if q.cursor.is_none() {
80 break;
81 }
82 }
83 let mut messages = 0;
84 let mut unresolved = 0;
85 let mut decoded = 0;
86 let mut q = MessageQuery {
87 limit: Some(200),
88 ..Default::default()
89 };
90 loop {
91 let p = r.messages(&q)?;
92 for m in &p.items {
93 unresolved += usize::from(m.peer.is_none());
94 decoded += usize::from(m.content.text.is_some());
95 }
96 messages += p.items.len();
97 q.cursor = p.next_cursor;
98 if q.cursor.is_none() {
99 break;
100 }
101 }
102 println!(
103 "{}",
104 serde_json::json!({"databases":r.inventory().len(),"partial":r.coverage().partial,"contacts":contacts,"threads":threads,"groups":groups,"groupMembers":members,"cachedAvatars":avatars,"messages":messages,"decodedBodies":decoded,"unresolvedMessages":unresolved,"writesPerformed":false})
105 );
106 Ok(())
107}Sourcepub fn cached_voice(
&self,
database: &str,
chat: &str,
local_id: &str,
server_id: Option<&str>,
) -> Result<CachedBytes>
pub fn cached_voice( &self, database: &str, chat: &str, local_id: &str, server_id: Option<&str>, ) -> Result<CachedBytes>
Read one exact cached voice record. The supplied media-shard namespace is explicit; its Name2Id is not interchangeable with the message shard.