1use crate::{
2 gateway::presence::{Presence, PresenceListDeserializer},
3 guild::Member,
4 id::{
5 marker::{GuildMarker, UserMarker},
6 Id,
7 },
8};
9use serde::{
10 de::{Deserializer, Error as DeError, IgnoredAny, MapAccess, Visitor},
11 Deserialize, Serialize,
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 communication_disabled_until: None,
300 deaf: false,
301 flags,
302 joined_at,
303 mute: false,
304 nick: Some("chunk".to_owned()),
305 pending: false,
306 premium_since: None,
307 roles: vec![Id::new(6), Id::new(7)],
308 user: User {
309 id: Id::new(2),
310 accent_color: None,
311 avatar: Some(image_hash::AVATAR),
312 avatar_decoration: None,
313 avatar_decoration_data: None,
314 banner: None,
315 bot: true,
316 discriminator: 1,
317 name: "test".to_owned(),
318 mfa_enabled: None,
319 locale: None,
320 verified: None,
321 email: None,
322 flags: None,
323 global_name: Some("test".to_owned()),
324 premium_type: None,
325 system: None,
326 public_flags: None,
327 },
328 },
329 Member {
330 avatar: None,
331 communication_disabled_until: None,
332 deaf: false,
333 flags,
334 joined_at,
335 mute: false,
336 nick: Some("chunk".to_owned()),
337 pending: false,
338 premium_since: None,
339 roles: vec![Id::new(6)],
340 user: User {
341 id: Id::new(3),
342 accent_color: None,
343 avatar: Some(image_hash::AVATAR),
344 avatar_decoration: None,
345 avatar_decoration_data: None,
346 banner: None,
347 bot: true,
348 discriminator: 1,
349 name: "test".to_owned(),
350 mfa_enabled: None,
351 locale: None,
352 verified: None,
353 email: None,
354 flags: None,
355 global_name: Some("test".to_owned()),
356 premium_type: None,
357 system: None,
358 public_flags: None,
359 },
360 },
361 Member {
362 avatar: None,
363 communication_disabled_until: None,
364 deaf: false,
365 flags,
366 joined_at,
367 mute: false,
368 nick: Some("chunk".to_owned()),
369 pending: true,
370 premium_since: None,
371 roles: vec![Id::new(6)],
372 user: User {
373 id: Id::new(5),
374 accent_color: None,
375 avatar: Some(image_hash::AVATAR),
376 avatar_decoration: None,
377 avatar_decoration_data: None,
378 banner: None,
379 bot: false,
380 discriminator: 1,
381 name: "test".to_owned(),
382 mfa_enabled: None,
383 locale: None,
384 verified: None,
385 email: None,
386 flags: None,
387 global_name: Some("test".to_owned()),
388 premium_type: None,
389 system: None,
390 public_flags: Some(UserFlags::VERIFIED_DEVELOPER),
391 },
392 },
393 Member {
394 avatar: None,
395 communication_disabled_until: None,
396 deaf: false,
397 flags,
398 joined_at,
399 mute: false,
400 nick: Some("chunk".to_owned()),
401 pending: false,
402 premium_since: None,
403 roles: vec![Id::new(6)],
404 user: User {
405 id: Id::new(6),
406 accent_color: None,
407 avatar: Some(image_hash::AVATAR),
408 avatar_decoration: None,
409 avatar_decoration_data: None,
410 banner: None,
411 bot: false,
412 discriminator: 1,
413 name: "test".to_owned(),
414 mfa_enabled: None,
415 locale: None,
416 verified: None,
417 email: None,
418 flags: None,
419 global_name: Some("test".to_owned()),
420 premium_type: None,
421 system: None,
422 public_flags: None,
423 },
424 },
425 ]),
426 nonce: None,
427 not_found: Vec::new(),
428 presences: Vec::from([
429 Presence {
430 activities: Vec::new(),
431 client_status: ClientStatus {
432 desktop: None,
433 mobile: None,
434 web: Some(Status::Online),
435 },
436 guild_id: Id::new(1),
437 status: Status::Online,
438 user: UserOrId::UserId { id: Id::new(2) },
439 },
440 Presence {
441 activities: Vec::new(),
442 client_status: ClientStatus {
443 desktop: None,
444 mobile: None,
445 web: Some(Status::Online),
446 },
447 guild_id: Id::new(1),
448 status: Status::Online,
449 user: UserOrId::UserId { id: Id::new(3) },
450 },
451 Presence {
452 activities: Vec::new(),
453 client_status: ClientStatus {
454 desktop: Some(Status::DoNotDisturb),
455 mobile: None,
456 web: None,
457 },
458 guild_id: Id::new(1),
459 status: Status::DoNotDisturb,
460 user: UserOrId::UserId { id: Id::new(5) },
461 },
462 ]),
463 };
464
465 let actual = serde_json::from_value::<MemberChunk>(input).unwrap();
466 assert_eq!(expected.chunk_count, actual.chunk_count);
467 assert_eq!(expected.chunk_index, actual.chunk_index);
468 assert_eq!(expected.guild_id, actual.guild_id);
469 assert_eq!(expected.nonce, actual.nonce);
470 assert_eq!(expected.not_found, actual.not_found);
471
472 for member in &actual.members {
473 assert!(expected.members.iter().any(|m| m == member));
474 }
475
476 for presences in &actual.presences {
477 assert!(expected.presences.iter().any(|p| p == presences));
478 }
479
480 Ok(())
481 }
482}