Skip to main content

wechat_protocol/
group_detail.rs

1use super::*;
2
3/// Extract only the proved field-control IDs. Version ordering is deliberately
4/// not guessed: callers preserve these fields and retain the unresolved update.
5pub fn controlled_contact_fields(raw: &[u8]) -> Result<Vec<u32>> {
6    let f = p::decode(raw)?;
7    let Some(raw) = p::optional_data(&f, 89)? else {
8        return Ok(vec![]);
9    };
10    let list = p::decode(raw)?;
11    let mut controls = 0u32;
12    let mut ids = std::collections::BTreeSet::new();
13    for field in list.iter().filter(|f| f.number == 2) {
14        controls += 1;
15        ensure!(controls <= 256, "contact_control_limit");
16        let p::Value::Bytes(raw) = &field.value else {
17            bail!("invalid_contact_control");
18        };
19        let info = p::decode(raw)?;
20        for n in [1, 3] {
21            if let Some(value) = p::optional(&info, n)? {
22                let p::Value::Varint(v) = value else {
23                    bail!("invalid_contact_control");
24                };
25                u32::try_from(*v)?;
26            }
27        }
28        if let Some(value) = p::optional(&info, 2)? {
29            ensure!(
30                matches!(value, p::Value::Varint(_)),
31                "invalid_contact_control"
32            );
33        }
34        let mut count = 0u32;
35        for field in info.iter().filter(|f| f.number == 4) {
36            count += 1;
37            ensure!(count <= 256, "contact_control_limit");
38            let p::Value::Bytes(raw) = &field.value else {
39                bail!("invalid_contact_control");
40            };
41            let field = p::decode(raw)?;
42            let id = u32::try_from(p::scalar(&field, 1)?)?;
43            ensure!(
44                field.len() == 1
45                    && matches!(
46                        id,
47                        8 | 10 | 11 | 12 | 27 | 59 | 60 | 61 | 62 | 68 | 83 | 84 | 87
48                    ),
49                "unsupported_contact_update_control"
50            );
51            ids.insert(id);
52        }
53        if let Some(p::Value::Varint(n)) = p::optional(&info, 3)? {
54            ensure!(*n == u64::from(count), "contact_control_count_mismatch");
55        }
56    }
57    match p::optional(&list, 1)? {
58        None => {}
59        Some(p::Value::Varint(n)) => {
60            ensure!(*n == u64::from(controls), "contact_control_count_mismatch")
61        }
62        _ => bail!("invalid_contact_control"),
63    }
64    Ok(ids.into_iter().collect())
65}
66
67#[derive(Clone, Serialize, Deserialize)]
68pub struct GroupDetail {
69    pub group_id: String,
70    pub version: u32,
71    pub announcement: String,
72    pub editor: String,
73    pub published_at: u32,
74    pub status: u32,
75    pub xml: String,
76}
77pub fn group_detail_request(state: &AccountState, group: &str) -> Result<Vec<u8>> {
78    ordinary_identity(group)?;
79    ensure!(group.ends_with("@chatroom"), "invalid_group_identity");
80    p::message(&[
81        p::bytes(1, &wire::base_request(&state.device, state.uin, 0)?),
82        p::bytes(2, group.as_bytes()),
83    ])
84}
85pub fn decode_group_detail(raw: &[u8], group: &str) -> Result<GroupDetail> {
86    let f = p::decode(raw)?;
87    ensure!(p::base_code(&f)? == 0, "group_detail_rejected");
88    let text = |n| -> Result<String> {
89        Ok(std::str::from_utf8(p::optional_data(&f, n)?.unwrap_or_default())?.into())
90    };
91    let number = |n| -> Result<u32> {
92        match p::optional(&f, n)? {
93            None => Ok(0),
94            Some(p::Value::Varint(n)) => Ok(u32::try_from(*n)?),
95            _ => bail!("invalid_group_detail"),
96        }
97    };
98    // The successful detail read uses generated defaults. Omission here means
99    // an empty announcement, unlike omission in a sparse ModContact patch.
100    Ok(GroupDetail {
101        group_id: group.into(),
102        version: number(3)?,
103        announcement: text(2)?,
104        editor: text(4)?,
105        published_at: number(5)?,
106        status: number(6)?,
107        xml: text(12)?,
108    })
109}
110impl NativeClient {
111    pub fn group_detail_raw(&self, state: &mut AccountState, group: &str) -> Result<Vec<u8>> {
112        self.authenticated_rpc(
113            state,
114            223,
115            "/cgi-bin/micromsg-bin/getchatroominfodetail",
116            &group_detail_request(state, group)?,
117        )
118    }
119}
120#[cfg(test)]
121mod tests {
122    use super::*;
123    #[test]
124    fn contact_controls_preserve_ids_without_guessing_version_order() {
125        let info = |count, id| {
126            p::message(&[
127                p::uint(1, 1),
128                p::uint(2, u64::MAX),
129                p::uint(3, count),
130                p::bytes(4, &p::uint(1, id)),
131            ])
132            .unwrap()
133        };
134        let wrap = |entries: Vec<Vec<u8>>| {
135            let mut list = vec![p::uint(1, entries.len() as u64)];
136            list.extend(entries.iter().map(|e| p::bytes(2, e)));
137            p::bytes(89, &p::message(&list).unwrap())
138        };
139        assert_eq!(
140            controlled_contact_fields(&wrap(vec![info(1, 68), info(1, 10), info(1, 10)])).unwrap(),
141            [10, 68]
142        );
143        assert!(controlled_contact_fields(&wrap(vec![info(2, 10)])).is_err());
144        assert!(controlled_contact_fields(&wrap(vec![info(1, 999)])).is_err());
145        assert!(controlled_contact_fields(&p::bytes(89, &p::bytes(1, b"wrong type"))).is_err());
146    }
147    #[test]
148    fn successful_omission_clears_but_failure_and_wrong_types_do_not() {
149        let raw = p::message(&[
150            p::bytes(1, &p::int32(1, 0)),
151            p::uint(3, 5),
152            p::bytes(4, b"editor"),
153            p::uint(5, 100),
154        ])
155        .unwrap();
156        let detail = decode_group_detail(&raw, "room@chatroom").unwrap();
157        assert_eq!(detail.announcement, "");
158        assert_eq!(detail.xml, "");
159        assert_eq!(detail.editor, "editor");
160        assert_eq!(detail.published_at, 100);
161        assert!(decode_group_detail(&p::bytes(1, &p::int32(1, -1)), "room@chatroom").is_err());
162        assert!(decode_group_detail(
163            &p::message(&[p::bytes(1, &p::uint(1, 0)), p::bytes(5, b"bad")]).unwrap(),
164            "room@chatroom"
165        )
166        .is_err());
167    }
168}