1use crate::{
2 gateway::presence::{Presence, PresenceListDeserializer},
3 guild::Member,
4 id::{
5 Id,
6 marker::{GuildMarker, UserMarker},
7 },
8};
9use serde::{
10 Deserialize, Serialize,
11 de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
12};
13use std::fmt::{Formatter, Result as FmtResult};
14
15#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
16pub struct MemberChunk {
17 pub chunk_count: u32,
18 pub chunk_index: u32,
19 pub guild_id: Id<GuildMarker>,
20 pub members: Vec<Member>,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub nonce: Option<String>,
23 pub not_found: Vec<Id<UserMarker>>,
24 #[serde(default)]
25 pub presences: Vec<Presence>,
26}
27
28#[derive(Debug, Deserialize)]
29#[serde(field_identifier, rename_all = "snake_case")]
30enum Field {
31 ChunkCount,
32 ChunkIndex,
33 GuildId,
34 Members,
35 Nonce,
36 NotFound,
37 Presences,
38}
39
40struct MemberChunkVisitor;
41
42impl<'de> Visitor<'de> for MemberChunkVisitor {
43 type Value = MemberChunk;
44
45 fn expecting(&self, f: &mut Formatter<'_>) -> FmtResult {
46 f.write_str("struct MemberChunk")
47 }
48
49 #[allow(clippy::too_many_lines)]
50 fn visit_map<V: MapAccess<'de>>(self, mut map: V) -> Result<Self::Value, V::Error> {
51 let mut chunk_count = None;
52 let mut chunk_index = None;
53 let mut guild_id = None;
54 let mut members = None;
55 let mut nonce = None;
56 let mut not_found = None;
57 let mut presences = None;
58
59 loop {
60 let key = match map.next_key() {
61 Ok(Some(key)) => key,
62 Ok(None) => break,
63 Err(_) => {
64 map.next_value::<IgnoredAny>()?;
65
66 continue;
67 }
68 };
69
70 match key {
71 Field::ChunkCount => {
72 if chunk_count.is_some() {
73 return Err(DeError::duplicate_field("chunk_count"));
74 }
75
76 chunk_count = Some(map.next_value()?);
77 }
78 Field::ChunkIndex => {
79 if chunk_index.is_some() {
80 return Err(DeError::duplicate_field("chunk_index"));
81 }
82
83 chunk_index = Some(map.next_value()?);
84 }
85 Field::GuildId => {
86 if guild_id.is_some() {
87 return Err(DeError::duplicate_field("guild_id"));
88 }
89
90 guild_id = Some(map.next_value()?);
91 }
92 Field::Members => {
93 if members.is_some() {
94 return Err(DeError::duplicate_field("members"));
95 }
96
97 members = Some(map.next_value()?);
98 }
99 Field::Nonce => {
100 if nonce.is_some() {
101 return Err(DeError::duplicate_field("nonce"));
102 }
103
104 nonce = Some(map.next_value()?);
105 }
106 Field::NotFound => {
107 if not_found.is_some() {
108 return Err(DeError::duplicate_field("not_found"));
109 }
110
111 not_found = Some(map.next_value()?);
112 }
113 Field::Presences => {
114 if presences.is_some() {
115 return Err(DeError::duplicate_field("presences"));
116 }
117
118 let deserializer = PresenceListDeserializer::new(Id::new(1));
119
120 presences = Some(map.next_value_seed(deserializer)?);
121 }
122 }
123 }
124
125 let chunk_count = chunk_count.ok_or_else(|| DeError::missing_field("chunk_count"))?;
126 let chunk_index = chunk_index.ok_or_else(|| DeError::missing_field("chunk_index"))?;
127 let guild_id = guild_id.ok_or_else(|| DeError::missing_field("guild_id"))?;
128 let members = members.ok_or_else(|| DeError::missing_field("members"))?;
129 let not_found = not_found.unwrap_or_default();
130 let mut presences = presences.unwrap_or_default();
131
132 for presence in &mut presences {
133 presence.guild_id = guild_id;
134 }
135
136 Ok(MemberChunk {
137 chunk_count,
138 chunk_index,
139 guild_id,
140 members,
141 nonce,
142 not_found,
143 presences,
144 })
145 }
146}
147
148impl<'de> Deserialize<'de> for MemberChunk {
149 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
150 const FIELDS: &[&str] = &[
151 "chunk_count",
152 "chunk_index",
153 "guild_id",
154 "members",
155 "nonce",
156 "not_found",
157 "presences",
158 ];
159
160 deserializer.deserialize_struct("MemberChunk", FIELDS, MemberChunkVisitor)
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::super::MemberChunk;
167 use crate::{
168 gateway::presence::{ClientStatus, Presence, Status, UserOrId},
169 guild::{Member, MemberFlags},
170 id::Id,
171 test::image_hash,
172 user::{User, UserFlags},
173 util::datetime::{Timestamp, TimestampParseError},
174 };
175 use std::str::FromStr;
176
177 #[allow(clippy::too_many_lines)]
178 #[test]
179 fn simple_member_chunk() -> Result<(), TimestampParseError> {
180 let joined_at = Some(Timestamp::from_str("2020-04-04T04:04:04.000000+00:00")?);
181 let flags = MemberFlags::BYPASSES_VERIFICATION | MemberFlags::DID_REJOIN;
182
183 let input = serde_json::json!({
184 "chunk_count": 1,
185 "chunk_index": 0,
186 "guild_id": "1",
187 "members": [{
188 "communication_disabled_until": null,
189 "deaf": false,
190 "flags": flags.bits(),
191 "hoisted_role": "6",
192 "joined_at": "2020-04-04T04:04:04.000000+00:00",
193 "mute": false,
194 "nick": "chunk",
195 "pending": true,
196 "roles": ["6"],
197 "user": {
198 "avatar": image_hash::AVATAR_INPUT,
199 "discriminator": "0001",
200 "global_name": "test",
201 "id": "5",
202 "public_flags": 131_072,
203 "username": "test",
204 },
205 }, {
206 "communication_disabled_until": null,
207 "deaf": false,
208 "flags": flags.bits(),
209 "hoisted_role": "6",
210 "joined_at": "2020-04-04T04:04:04.000000+00:00",
211 "mute": false,
212 "nick": "chunk",
213 "roles": ["6"],
214 "user": {
215 "avatar": image_hash::AVATAR_INPUT,
216 "discriminator": "0001",
217 "global_name": "test",
218 "id": "6",
219 "username": "test",
220 },
221 }, {
222 "communication_disabled_until": null,
223 "deaf": false,
224 "flags": flags.bits(),
225 "hoisted_role": "6",
226 "joined_at": "2020-04-04T04:04:04.000000+00:00",
227 "mute": false,
228 "nick": "chunk",
229 "roles": ["6"],
230 "user": {
231 "avatar": image_hash::AVATAR_INPUT,
232 "bot": true,
233 "discriminator": "0001",
234 "global_name": "test",
235 "id": "3",
236 "username": "test",
237 },
238 }, {
239 "communication_disabled_until": null,
240 "deaf": false,
241 "flags": flags.bits(),
242 "hoisted_role": "6",
243 "joined_at": "2020-04-04T04:04:04.000000+00:00",
244 "mute": false,
245 "nick": "chunk",
246 "roles": [
247 "6",
248 "7",
249 ],
250 "user": {
251 "avatar": image_hash::AVATAR_INPUT,
252 "bot": true,
253 "discriminator": "0001",
254 "global_name": "test",
255 "id": "2",
256 "username": "test",
257 },
258 }],
259 "presences": [{
260 "activities": [],
261 "client_status": {
262 "web": "online",
263 },
264 "guild_id": "1",
265 "status": "online",
266 "user": {
267 "id": "2",
268 },
269 }, {
270 "activities": [],
271 "client_status": {
272 "web": "online",
273 },
274 "guild_id": "1",
275 "status": "online",
276 "user": {
277 "id": "3",
278 },
279 }, {
280 "activities": [],
281 "client_status": {
282 "desktop": "dnd",
283 },
284 "guild_id": "1",
285 "status": "dnd",
286 "user": {
287 "id": "5",
288 },
289 }],
290 });
291
292 let expected = MemberChunk {
293 chunk_count: 1,
294 chunk_index: 0,
295 guild_id: Id::new(1),
296 members: Vec::from([
297 Member {
298 avatar: None,
299 avatar_decoration_data: None,
300 banner: None,
301 communication_disabled_until: None,
302 deaf: false,
303 flags,
304 joined_at,
305 mute: false,
306 nick: Some("chunk".to_owned()),
307 pending: false,
308 premium_since: None,
309 roles: vec![Id::new(6), Id::new(7)],
310 user: User {
311 id: Id::new(2),
312 accent_color: None,
313 avatar: Some(image_hash::AVATAR),
314 avatar_decoration: None,
315 avatar_decoration_data: None,
316 banner: None,
317 bot: true,
318 discriminator: 1,
319 name: "test".to_owned(),
320 mfa_enabled: None,
321 locale: None,
322 verified: None,
323 email: None,
324 flags: None,
325 global_name: Some("test".to_owned()),
326 premium_type: None,
327 primary_guild: None,
328 public_flags: None,
329 system: None,
330 },
331 },
332 Member {
333 avatar: None,
334 avatar_decoration_data: None,
335 banner: None,
336 communication_disabled_until: None,
337 deaf: false,
338 flags,
339 joined_at,
340 mute: false,
341 nick: Some("chunk".to_owned()),
342 pending: false,
343 premium_since: None,
344 roles: vec![Id::new(6)],
345 user: User {
346 id: Id::new(3),
347 accent_color: None,
348 avatar: Some(image_hash::AVATAR),
349 avatar_decoration: None,
350 avatar_decoration_data: None,
351 banner: None,
352 bot: true,
353 discriminator: 1,
354 name: "test".to_owned(),
355 mfa_enabled: None,
356 locale: None,
357 verified: None,
358 email: None,
359 flags: None,
360 global_name: Some("test".to_owned()),
361 premium_type: None,
362 primary_guild: None,
363 public_flags: None,
364 system: None,
365 },
366 },
367 Member {
368 avatar: None,
369 avatar_decoration_data: None,
370 banner: None,
371 communication_disabled_until: None,
372 deaf: false,
373 flags,
374 joined_at,
375 mute: false,
376 nick: Some("chunk".to_owned()),
377 pending: true,
378 premium_since: None,
379 roles: vec![Id::new(6)],
380 user: User {
381 id: Id::new(5),
382 accent_color: None,
383 avatar: Some(image_hash::AVATAR),
384 avatar_decoration: None,
385 avatar_decoration_data: None,
386 banner: None,
387 bot: false,
388 discriminator: 1,
389 name: "test".to_owned(),
390 mfa_enabled: None,
391 locale: None,
392 verified: None,
393 email: None,
394 flags: None,
395 global_name: Some("test".to_owned()),
396 premium_type: None,
397 primary_guild: None,
398 public_flags: Some(UserFlags::VERIFIED_DEVELOPER),
399 system: None,
400 },
401 },
402 Member {
403 avatar: None,
404 avatar_decoration_data: None,
405 banner: None,
406 communication_disabled_until: None,
407 deaf: false,
408 flags,
409 joined_at,
410 mute: false,
411 nick: Some("chunk".to_owned()),
412 pending: false,
413 premium_since: None,
414 roles: vec![Id::new(6)],
415 user: User {
416 id: Id::new(6),
417 accent_color: None,
418 avatar: Some(image_hash::AVATAR),
419 avatar_decoration: None,
420 avatar_decoration_data: None,
421 banner: None,
422 bot: false,
423 discriminator: 1,
424 name: "test".to_owned(),
425 mfa_enabled: None,
426 locale: None,
427 verified: None,
428 email: None,
429 flags: None,
430 global_name: Some("test".to_owned()),
431 premium_type: None,
432 primary_guild: None,
433 public_flags: None,
434 system: None,
435 },
436 },
437 ]),
438 nonce: None,
439 not_found: Vec::new(),
440 presences: Vec::from([
441 Presence {
442 activities: Vec::new(),
443 client_status: ClientStatus {
444 desktop: None,
445 mobile: None,
446 web: Some(Status::Online),
447 },
448 guild_id: Id::new(1),
449 status: Status::Online,
450 user: UserOrId::UserId { id: Id::new(2) },
451 },
452 Presence {
453 activities: Vec::new(),
454 client_status: ClientStatus {
455 desktop: None,
456 mobile: None,
457 web: Some(Status::Online),
458 },
459 guild_id: Id::new(1),
460 status: Status::Online,
461 user: UserOrId::UserId { id: Id::new(3) },
462 },
463 Presence {
464 activities: Vec::new(),
465 client_status: ClientStatus {
466 desktop: Some(Status::DoNotDisturb),
467 mobile: None,
468 web: None,
469 },
470 guild_id: Id::new(1),
471 status: Status::DoNotDisturb,
472 user: UserOrId::UserId { id: Id::new(5) },
473 },
474 ]),
475 };
476
477 let actual = serde_json::from_value::<MemberChunk>(input).unwrap();
478 assert_eq!(expected.chunk_count, actual.chunk_count);
479 assert_eq!(expected.chunk_index, actual.chunk_index);
480 assert_eq!(expected.guild_id, actual.guild_id);
481 assert_eq!(expected.nonce, actual.nonce);
482 assert_eq!(expected.not_found, actual.not_found);
483
484 for member in &actual.members {
485 assert!(expected.members.iter().any(|m| m == member));
486 }
487
488 for presences in &actual.presences {
489 assert!(expected.presences.iter().any(|p| p == presences));
490 }
491
492 Ok(())
493 }
494}