Skip to main content

wechat_protocol/
group_inventory.rs

1//! Separate group catalog and lossless read-only metadata response receipts.
2use 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    /// Verified native business payload, retained before strict contact decoding.
98    /// Callers must protect this account data at rest. The transport's global
99    /// message bound applies; no identities or opaque fields are exposed in logs.
100    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}