1use 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 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 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 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}