1use super::*;
8
9const CGI: u16 = 251;
10const PATH: &str = "/cgi-bin/micromsg-bin/statusnotify";
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13#[serde(rename_all = "camelCase", deny_unknown_fields)]
14pub struct SessionReadNotification {
15 pub peer_username: String,
16 pub last_message_server_id: String,
17 pub last_message_create_time: u32,
18 pub client_message_id: String,
20}
21
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct SessionReadReply {
24 pub service_code: i32,
25 pub accepted: bool,
26}
27
28fn bounded_identity(value: &str, max: usize) -> bool {
29 !value.is_empty() && value.len() <= max && !value.chars().any(char::is_control)
30}
31
32fn request(state: &AccountState, notification: &SessionReadNotification) -> Result<Vec<u8>> {
33 state.validate()?;
34 ensure!(
35 bounded_identity(&state.own_username, 256)
36 && bounded_identity(¬ification.peer_username, 512)
37 && bounded_identity(¬ification.client_message_id, 256),
38 "Invalid read notification identity"
39 );
40 let id = notification.last_message_server_id.parse::<u64>();
41 ensure!(
42 id.is_ok_and(|id| id != 0 && id.to_string() == notification.last_message_server_id)
43 && notification.last_message_create_time != 0,
44 "Invalid read notification boundary"
45 );
46 let argument = serde_json::to_vec(&serde_json::json!({
47 "messageSvrId": notification.last_message_server_id,
48 "MsgCreateTime": notification.last_message_create_time.to_string(),
49 }))?;
50 p::message(&[
51 p::bytes(1, &wire::base_request(&state.device, state.uin, 0)?),
52 p::uint(2, 1),
53 p::bytes(3, state.own_username.as_bytes()),
54 p::bytes(4, notification.peer_username.as_bytes()),
55 p::bytes(5, notification.client_message_id.as_bytes()),
56 p::bytes(
57 8,
58 &p::message(&[p::bytes(1, b"lastMessage"), p::bytes(2, &argument)])?,
59 ),
60 ])
61}
62
63fn response(bytes: &[u8]) -> Result<SessionReadReply> {
64 let fields = p::decode(bytes)?;
65 let base = p::decode(p::data(&fields, 1)?)?;
66 let raw = p::scalar(&base, 1)?;
67 let code = raw as u32 as i32;
68 ensure!(
69 raw <= u32::MAX as u64 || raw == code as i64 as u64,
70 "Invalid read notification response status"
71 );
72 Ok(SessionReadReply {
73 service_code: code,
74 accepted: code == 0,
75 })
76}
77
78impl NativeClient {
79 pub fn notify_read_session(
82 &self,
83 state: &mut AccountState,
84 notification: &SessionReadNotification,
85 ) -> Result<SessionReadReply> {
86 let body = request(state, notification)?;
87 response(&self.authenticated_rpc(state, CGI, PATH, &body)?)
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn state() -> AccountState {
96 AccountState {
97 schema: 1,
98 device: DeviceProfile {
99 device_id: b"0123456789abcdef".to_vec(),
100 device_type: "UnifiedPCLinux x86_64".into(),
101 client_version: -227271398,
102 },
103 uin: 42,
104 own_username: "synthetic_user".into(),
105 cookie: vec![],
106 session_key: vec![1; 16],
107 request_key: vec![2; 24],
108 response_key: vec![3; 24],
109 auto_auth_key: vec![4; 16],
110 key_info: vec![],
111 database_password: vec![],
112 auth_snapshot: Default::default(),
113 long_host: "long.weixin.qq.com".into(),
114 short_host: "short.weixin.qq.com".into(),
115 }
116 }
117
118 fn notification() -> SessionReadNotification {
119 SessionReadNotification {
120 peer_username: "synthetic_room@chatroom".into(),
121 last_message_server_id: u64::MAX.to_string(),
122 last_message_create_time: 1_800_000_000,
123 client_message_id: "read-fixture-stable-id".into(),
124 }
125 }
126
127 #[test]
128 fn read_session_uses_proven_fields_and_lossless_boundary_strings() {
129 assert_eq!(CGI, 251);
130 assert_eq!(PATH, "/cgi-bin/micromsg-bin/statusnotify");
131 let input = notification();
132 let body = request(&state(), &input).unwrap();
133 assert_eq!(body, request(&state(), &input).unwrap());
134 let outer = p::decode(&body).unwrap();
135 assert_eq!(
136 outer.iter().map(|field| field.number).collect::<Vec<_>>(),
137 [1, 2, 3, 4, 5, 8]
138 );
139 let base = p::decode(p::data(&outer, 1).unwrap()).unwrap();
140 assert_eq!(p::scalar(&base, 2).unwrap(), 42);
141 assert_eq!(p::scalar(&outer, 2).unwrap(), 1);
142 assert_eq!(p::data(&outer, 3).unwrap(), b"synthetic_user");
143 assert_eq!(p::data(&outer, 4).unwrap(), b"synthetic_room@chatroom");
144 assert_eq!(p::data(&outer, 5).unwrap(), b"read-fixture-stable-id");
145 let extra = p::decode(p::data(&outer, 8).unwrap()).unwrap();
146 assert_eq!(p::data(&extra, 1).unwrap(), b"lastMessage");
147 let argument: serde_json::Value =
148 serde_json::from_slice(p::data(&extra, 2).unwrap()).unwrap();
149 assert_eq!(argument["messageSvrId"], u64::MAX.to_string());
150 assert_eq!(argument["MsgCreateTime"], "1800000000");
151 }
152
153 #[test]
154 fn malformed_boundaries_and_identities_fail_before_transport() {
155 for id in ["", "0", "-1", "01", " 1", "+1", "18446744073709551616"] {
156 let mut input = notification();
157 input.last_message_server_id = id.into();
158 assert!(request(&state(), &input).is_err());
159 }
160 for value in [String::new(), "peer\nother".into(), "x".repeat(513)] {
161 let mut input = notification();
162 input.peer_username = value;
163 assert!(request(&state(), &input).is_err());
164 }
165 let mut input = notification();
166 input.last_message_create_time = 0;
167 assert!(request(&state(), &input).is_err());
168 let mut input = notification();
169 input.client_message_id.clear();
170 assert!(request(&state(), &input).is_err());
171 let mut invalid = state();
172 invalid.uin = 0;
173 assert!(request(&invalid, ¬ification()).is_err());
174 }
175
176 #[test]
177 fn reply_preserves_rejection_and_requires_unique_initialized_base_status() {
178 assert!(response(&p::bytes(1, &p::uint(1, 0))).unwrap().accepted);
179 let rejected = response(&p::bytes(1, &p::int32(1, -13))).unwrap();
180 assert_eq!(rejected.service_code, -13);
181 assert!(!rejected.accepted);
182 for invalid in [
183 vec![],
184 p::bytes(1, &[]),
185 p::uint(1, 0),
186 p::bytes(1, &p::uint(1, 1u64 << 32)),
187 [p::bytes(1, &p::uint(1, 0)), p::bytes(1, &p::uint(1, 0))].concat(),
188 ] {
189 assert!(response(&invalid).is_err());
190 }
191 }
192}