Skip to main content

wechat_protocol/
revoke.rs

1//! One authenticated attempt to recall an exact own message.
2//!
3//! The Linux 4.1.13.23 AMD64 typed wrapper uses CGI 594. Its builder supplies
4//! source local ID/time, sender/destination and the full 64-bit server identity.
5//! Callers must persist admission before calling; errors never authorize retry.
6use super::*;
7
8const CGI: u16 = 594;
9const PATH: &str = "/cgi-bin/micromsg-bin/revokemsg";
10
11#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
12#[serde(rename_all = "camelCase", deny_unknown_fields)]
13pub struct MessageRecall {
14    pub peer_username: String,
15    pub server_id: String,
16    pub local_id: u32,
17    pub create_time: u32,
18    pub client_message_id: String,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct MessageRecallReply {
23    pub service_code: i32,
24    pub accepted: bool,
25}
26
27impl MessageRecall {
28    /// The native nonzero-local-ID formatter is peer_time_localId, with both
29    /// numbers rendered as unsigned decimal. Keep it with the frozen target.
30    pub fn for_source(
31        peer_username: String,
32        server_id: String,
33        local_id: u32,
34        create_time: u32,
35    ) -> Self {
36        let client_message_id = format!("{peer_username}_{create_time}_{local_id}");
37        Self {
38            peer_username,
39            server_id,
40            local_id,
41            create_time,
42            client_message_id,
43        }
44    }
45    pub fn validate(&self) -> Result<()> {
46        let identity = |value: &str, max: usize| {
47            !value.is_empty() && value.len() <= max && !value.chars().any(char::is_control)
48        };
49        ensure!(
50            identity(&self.peer_username, 512) && identity(&self.client_message_id, 1024),
51            "Invalid recall identity"
52        );
53        ensure!(
54            self.local_id != 0 && self.create_time != 0,
55            "Invalid recall source"
56        );
57        ensure!(
58            self.server_id
59                .parse::<u64>()
60                .is_ok_and(|n| n != 0 && n.to_string() == self.server_id),
61            "Invalid recall server identity"
62        );
63        Ok(())
64    }
65}
66
67fn request(state: &AccountState, recall: &MessageRecall) -> Result<Vec<u8>> {
68    state.validate()?;
69    recall.validate()?;
70    ensure!(
71        !state.own_username.is_empty()
72            && state.own_username.len() <= 256
73            && !state.own_username.chars().any(char::is_control),
74        "Invalid recall sender"
75    );
76    p::message(&[
77        p::bytes(1, &wire::base_request(&state.device, state.uin, 0)?),
78        p::bytes(2, recall.client_message_id.as_bytes()),
79        p::uint(3, u64::from(recall.local_id)),
80        p::uint(4, u64::from(recall.create_time)),
81        p::uint(5, 0),
82        p::bytes(6, state.own_username.as_bytes()),
83        p::bytes(7, recall.peer_username.as_bytes()),
84        p::uint(8, 0),
85        p::uint(9, recall.server_id.parse()?),
86    ])
87}
88
89fn response(bytes: &[u8]) -> Result<MessageRecallReply> {
90    let fields = p::decode(bytes)?;
91    let base = p::decode(p::data(&fields, 1)?)?;
92    let raw = p::scalar(&base, 1)?;
93    let code = raw as u32 as i32;
94    ensure!(
95        raw <= u32::MAX as u64 || raw == code as i64 as u64,
96        "Invalid recall response status"
97    );
98    // Numbered optional reply strings are not target IDs or success evidence.
99    // Keep their unknown semantics out of the public result and logs.
100    p::optional_data(&fields, 2)?;
101    p::optional_data(&fields, 3)?;
102    Ok(MessageRecallReply {
103        service_code: code,
104        accepted: code == 0,
105    })
106}
107
108impl NativeClient {
109    /// Submit once without credential renewal, login fallback or automatic retry.
110    pub fn recall_message(
111        &self,
112        state: &mut AccountState,
113        recall: &MessageRecall,
114    ) -> Result<MessageRecallReply> {
115        let body = request(state, recall)?;
116        response(&self.authenticated_rpc(state, CGI, PATH, &body)?)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    fn state() -> AccountState {
124        AccountState {
125            schema: 1,
126            device: DeviceProfile {
127                device_id: b"0123456789abcdef".to_vec(),
128                device_type: "UnifiedPCLinux x86_64".into(),
129                client_version: -227271398,
130            },
131            uin: 42,
132            own_username: "synthetic_user".into(),
133            cookie: vec![],
134            session_key: vec![1; 16],
135            request_key: vec![2; 24],
136            response_key: vec![3; 24],
137            auto_auth_key: vec![4; 16],
138            key_info: vec![],
139            database_password: vec![],
140            auth_snapshot: Default::default(),
141            long_host: "long.weixin.qq.com".into(),
142            short_host: "short.weixin.qq.com".into(),
143        }
144    }
145    fn recall() -> MessageRecall {
146        MessageRecall::for_source(
147            "synthetic_peer".into(),
148            u64::MAX.to_string(),
149            u32::MAX,
150            1_800_000_000,
151        )
152    }
153    #[test]
154    fn native_client_identity_uses_exact_peer_time_and_unsigned_local_id() {
155        assert_eq!(
156            MessageRecall::for_source(
157                "synthetic_room@chatroom".into(),
158                "20".into(),
159                u32::MAX,
160                u32::MAX
161            )
162            .client_message_id,
163            "synthetic_room@chatroom_4294967295_4294967295"
164        );
165        assert_eq!(
166            MessageRecall::for_source("peer_with_underscore".into(), "20".into(), 1, 100)
167                .client_message_id,
168            "peer_with_underscore_100_1"
169        );
170    }
171    #[test]
172    fn native_recall_uses_source_fields_and_full_server_identity() {
173        assert_eq!(CGI, 594);
174        assert_eq!(PATH, "/cgi-bin/micromsg-bin/revokemsg");
175        let input = recall();
176        let body = request(&state(), &input).unwrap();
177        assert_eq!(body, request(&state(), &input).unwrap());
178        let f = p::decode(&body).unwrap();
179        assert_eq!(
180            f.iter().map(|f| f.number).collect::<Vec<_>>(),
181            [1, 2, 3, 4, 5, 6, 7, 8, 9]
182        );
183        assert_eq!(
184            p::scalar(&p::decode(p::data(&f, 1).unwrap()).unwrap(), 2).unwrap(),
185            42
186        );
187        assert_eq!(p::data(&f, 2).unwrap(), input.client_message_id.as_bytes());
188        assert_eq!(p::scalar(&f, 3).unwrap(), u64::from(u32::MAX));
189        assert_eq!(p::scalar(&f, 4).unwrap(), 1_800_000_000);
190        assert_eq!(p::scalar(&f, 5).unwrap(), 0);
191        assert_eq!(p::data(&f, 6).unwrap(), b"synthetic_user");
192        assert_eq!(p::data(&f, 7).unwrap(), b"synthetic_peer");
193        assert_eq!(p::scalar(&f, 8).unwrap(), 0);
194        assert_eq!(p::scalar(&f, 9).unwrap(), u64::MAX);
195    }
196    #[test]
197    fn invalid_source_or_lossy_id_is_rejected_before_transport() {
198        for sid in ["", "0", "01", "-1", "+1", " 1", "18446744073709551616"] {
199            let mut r = recall();
200            r.server_id = sid.into();
201            assert!(request(&state(), &r).is_err());
202        }
203        let mut r = recall();
204        r.local_id = 0;
205        assert!(request(&state(), &r).is_err());
206        r = recall();
207        r.create_time = 0;
208        assert!(request(&state(), &r).is_err());
209        for peer in ["".into(), "peer\nother".into(), "x".repeat(513)] {
210            r = recall();
211            r.peer_username = peer;
212            assert!(request(&state(), &r).is_err());
213        }
214    }
215    #[test]
216    fn only_unique_initialized_base_response_confirms_acceptance() {
217        assert!(response(&p::bytes(1, &p::uint(1, 0))).unwrap().accepted);
218        let rejected = response(&p::bytes(1, &p::int32(1, -13))).unwrap();
219        assert_eq!(rejected.service_code, -13);
220        assert!(!rejected.accepted);
221        for bytes in [
222            vec![],
223            p::bytes(1, &[]),
224            p::uint(1, 0),
225            p::bytes(1, &p::uint(1, 1u64 << 32)),
226            [p::bytes(1, &p::uint(1, 0)), p::bytes(1, &p::uint(1, 0))].concat(),
227            [p::bytes(1, &p::uint(1, 0)), p::uint(2, 1)].concat(),
228        ] {
229            assert!(response(&bytes).is_err());
230        }
231    }
232}