1use super::*;
3use std::collections::HashSet;
4
5#[derive(Clone, Serialize, Deserialize)]
6pub struct MetadataReply<T> {
7 pub value: T,
8 pub raw: Vec<u8>,
9}
10#[derive(Clone, Serialize, Deserialize)]
11pub struct GroupMetadataPage {
12 pub contacts: Vec<ContactRecord>,
13 pub next_cursor: Option<Vec<u8>>,
14 pub more: bool,
15 pub wait_seconds: u32,
16 pub raw: Vec<u8>,
17}
18pub fn group_inventory_request(state: &AccountState, cursor: &[u8]) -> Result<Vec<u8>> {
19 ensure!(
20 !cursor.is_empty() && cursor.len() <= 65536,
21 "Invalid raw group cursor"
22 );
23 p::message(&[
24 p::bytes(1, &wire::base_request(&state.device, state.uin, 0)?),
25 p::bytes(2, cursor),
26 ])
27}
28pub fn decode_group_inventory(raw: &[u8]) -> Result<GroupMetadataPage> {
29 let fields = p::decode(raw)?;
30 let code = p::base_code(&fields)?;
31 ensure!(code == 0, "Group metadata service returned {code}");
32 let mut contacts = vec![];
33 let mut seen = HashSet::new();
34 for field in fields.iter().filter(|f| f.number == 2) {
35 let p::Value::Bytes(raw) = &field.value else {
36 bail!("Invalid group contact field")
37 };
38 let record = wire::decode_contact_record(raw)?;
39 ensure!(
40 seen.insert(record.username.clone()) && contacts.len() < 5000,
41 "Duplicate or excessive group metadata"
42 );
43 contacts.push(record);
44 }
45 let next_cursor = p::optional_data(&fields, 3)?.map(Vec::from);
46 ensure!(
47 next_cursor
48 .as_ref()
49 .is_none_or(|c| !c.is_empty() && c.len() <= 65536),
50 "Invalid next group cursor"
51 );
52 let control = p::optional_data(&fields, 4)?
53 .map(p::decode)
54 .transpose()?
55 .unwrap_or_default();
56 let scalar = |n| -> Result<u32> {
57 match p::optional(&control, n)? {
58 None => Ok(0),
59 Some(p::Value::Varint(n)) => Ok(u32::try_from(*n)?),
60 _ => bail!("Invalid group control"),
61 }
62 };
63 let more = scalar(1)? != 0;
64 let wait_seconds = scalar(2)?;
65 ensure!(
66 !more || next_cursor.is_some(),
67 "Continuing group page has no cursor"
68 );
69 Ok(GroupMetadataPage {
70 contacts,
71 next_cursor,
72 more,
73 wait_seconds,
74 raw: raw.to_vec(),
75 })
76}
77impl NativeClient {
78 pub fn group_inventory(
79 &self,
80 state: &mut AccountState,
81 cursor: &[u8],
82 ) -> Result<GroupMetadataPage> {
83 let request = group_inventory_request(state, cursor)?;
84 let raw = self.authenticated_rpc(
85 state,
86 15595,
87 "/cgi-bin/micromsg-bin/syncsimplechatroomdata",
88 &request,
89 )?;
90 let page = decode_group_inventory(&raw)?;
91 ensure!(
92 !page.more || page.next_cursor.as_deref() != Some(cursor),
93 "Group metadata made no cursor progress"
94 );
95 Ok(page)
96 }
97 pub fn lookup_contacts_raw(&self, state: &mut AccountState, ids: &[String]) -> Result<Vec<u8>> {
101 let request = wire::contact_request(state, ids)?;
102 let raw =
103 self.authenticated_rpc(state, 182, "/cgi-bin/micromsg-bin/getcontact", &request)?;
104 ensure!(
105 raw.len() <= crate::MAX_MESSAGE_BYTES,
106 "Contact response exceeds bound"
107 );
108 Ok(raw)
109 }
110 pub fn lookup_contacts_receipt(
111 &self,
112 state: &mut AccountState,
113 ids: &[String],
114 ) -> Result<MetadataReply<ContactLookup>> {
115 let raw = self.lookup_contacts_raw(state, ids)?;
116 Ok(MetadataReply {
117 value: wire::decode_contacts(&raw, ids)?,
118 raw,
119 })
120 }
121 pub fn lookup_room_receipt(
122 &self,
123 state: &mut AccountState,
124 id: &str,
125 version: u32,
126 ) -> Result<MetadataReply<RoomRoster>> {
127 let raw = self.lookup_room_raw(state, id, version)?;
128 Ok(MetadataReply {
129 value: wire::decode_room(&raw, id)?,
130 raw,
131 })
132 }
133 pub fn lookup_room_raw(
134 &self,
135 state: &mut AccountState,
136 id: &str,
137 version: u32,
138 ) -> Result<Vec<u8>> {
139 let request = wire::room_request(state, id, version)?;
140 self.authenticated_rpc(
141 state,
142 551,
143 "/cgi-bin/micromsg-bin/getchatroommemberdetail",
144 &request,
145 )
146 }
147 pub fn init_contacts_receipt(
148 &self,
149 state: &mut AccountState,
150 a: u32,
151 b: u32,
152 ) -> Result<MetadataReply<InitContactPage>> {
153 let request = wire::init_contacts_request(&state.own_username, a, b)?;
154 let raw =
155 self.authenticated_rpc(state, 851, "/cgi-bin/micromsg-bin/initcontact", &request)?;
156 let value = wire::decode_init_contacts(&raw)?;
157 ensure!(
158 !value.more || value.contact_sequence != a || value.room_sequence != b,
159 "Contact bootstrap made no cursor progress"
160 );
161 Ok(MetadataReply { value, raw })
162 }
163}
164#[cfg(test)]
165mod tests {
166 use super::*;
167 #[test]
168 fn group_cursor_is_raw_and_control_presence_is_optional() {
169 let raw = p::message(&[p::bytes(1, &p::uint(1, 0)), p::bytes(3, &[0, 255, 1])]).unwrap();
170 let page = decode_group_inventory(&raw).unwrap();
171 assert_eq!(page.next_cursor, Some(vec![0, 255, 1]));
172 assert!(!page.more);
173 assert_eq!(page.wait_seconds, 0);
174 assert_eq!(page.raw, raw);
175 let bad = p::message(&[p::bytes(1, &p::uint(1, 0)), p::bytes(4, &p::uint(1, 1))]).unwrap();
176 assert!(decode_group_inventory(&bad).is_err());
177 }
178 #[test]
179 fn rejection_does_not_require_success_fields() {
180 assert!(decode_group_inventory(&p::bytes(1, &p::int32(1, -13)))
181 .err()
182 .unwrap()
183 .to_string()
184 .contains("-13"));
185 }
186}