twilight_model/gateway/payload/incoming/
member_chunk.rs

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}