tulpje_cache/models/
member.rs

1use std::ops::Deref;
2
3use serde::{Deserialize, Serialize};
4use twilight_model::{
5    application::interaction::InteractionMember,
6    gateway::payload::incoming::MemberUpdate,
7    guild::{Member, MemberFlags, PartialMember},
8    id::{
9        Id,
10        marker::{GuildMarker, RoleMarker, UserMarker},
11    },
12    user::AvatarDecorationData,
13    util::{ImageHash, Timestamp},
14};
15
16use crate::{Cache, Error};
17
18/// Computed components required to complete a full cached interaction member
19/// by implementing [`CacheableMember`].
20#[derive(Clone, Debug, Eq, PartialEq)]
21pub struct ComputedInteractionMember {
22    pub avatar: Option<ImageHash>,
23    pub deaf: Option<bool>,
24    pub interaction_member: InteractionMember,
25    pub mute: Option<bool>,
26    pub user_id: Id<UserMarker>,
27}
28
29impl Deref for ComputedInteractionMember {
30    type Target = InteractionMember;
31
32    fn deref(&self) -> &Self::Target {
33        &self.interaction_member
34    }
35}
36
37#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
38pub struct CachedMember {
39    pub avatar: Option<ImageHash>,
40    pub avatar_decoration_data: Option<AvatarDecorationData>,
41    pub banner: Option<ImageHash>,
42    pub communication_disabled_until: Option<Timestamp>,
43    pub deaf: Option<bool>,
44    pub flags: MemberFlags,
45    pub joined_at: Option<Timestamp>,
46    pub mute: Option<bool>,
47    pub nick: Option<String>,
48    pub pending: bool,
49    pub premium_since: Option<Timestamp>,
50    pub roles: Vec<Id<RoleMarker>>,
51    pub user_id: Id<UserMarker>,
52}
53
54impl CachedMember {
55    pub(crate) fn update_with_member_update(&mut self, member_update: &MemberUpdate) {
56        self.avatar = member_update.avatar;
57        self.deaf = member_update.deaf.or(self.deaf);
58        self.mute = member_update.mute.or(self.mute);
59        self.nick.clone_from(&member_update.nick);
60        self.roles.clone_from(&member_update.roles);
61        self.joined_at = member_update.joined_at;
62        self.pending = member_update.pending;
63        self.communication_disabled_until = member_update.communication_disabled_until;
64    }
65}
66
67impl From<Member> for CachedMember {
68    fn from(member: Member) -> Self {
69        let Member {
70            avatar,
71            avatar_decoration_data,
72            banner,
73            communication_disabled_until,
74            deaf,
75            flags,
76            joined_at,
77            mute,
78            nick,
79            pending,
80            premium_since,
81            roles,
82            user,
83        } = member;
84
85        Self {
86            avatar,
87            avatar_decoration_data,
88            banner,
89            communication_disabled_until,
90            deaf: Some(deaf),
91            flags,
92            joined_at,
93            mute: Some(mute),
94            nick,
95            pending,
96            premium_since,
97            roles,
98            user_id: user.id,
99        }
100    }
101}
102
103impl From<(Id<UserMarker>, PartialMember)> for CachedMember {
104    fn from((user_id, member): (Id<UserMarker>, PartialMember)) -> Self {
105        #[expect(
106            clippy::unneeded_field_pattern,
107            reason = "clearer that we're explicitly skipping those fields"
108        )]
109        let PartialMember {
110            avatar,
111            avatar_decoration_data,
112            banner,
113            communication_disabled_until,
114            deaf,
115            flags,
116            joined_at,
117            mute,
118            nick,
119            permissions: _,
120            premium_since,
121            roles,
122            user,
123        } = member;
124
125        Self {
126            avatar,
127            avatar_decoration_data,
128            banner,
129            communication_disabled_until,
130            deaf: Some(deaf),
131            flags,
132            joined_at,
133            mute: Some(mute),
134            nick,
135            pending: false,
136            premium_since,
137            roles,
138            user_id: user.map_or(user_id, |user| user.id),
139        }
140    }
141}
142
143impl From<ComputedInteractionMember> for CachedMember {
144    fn from(member: ComputedInteractionMember) -> Self {
145        let ComputedInteractionMember {
146            avatar,
147            deaf,
148            mute,
149            user_id,
150            interaction_member,
151        } = member;
152
153        #[expect(
154            clippy::unneeded_field_pattern,
155            reason = "clearer that we're explicitly skipping those fields"
156        )]
157        let InteractionMember {
158            avatar: _,
159            avatar_decoration_data,
160            banner,
161            communication_disabled_until,
162            flags,
163            joined_at,
164            nick,
165            pending,
166            permissions: _,
167            premium_since,
168            roles,
169        } = interaction_member;
170
171        Self {
172            avatar,
173            avatar_decoration_data,
174            banner,
175            communication_disabled_until,
176            deaf,
177            flags,
178            joined_at,
179            mute,
180            nick,
181            pending,
182            premium_since,
183            roles,
184            user_id,
185        }
186    }
187}
188
189impl PartialEq<Member> for CachedMember {
190    fn eq(&self, other: &Member) -> bool {
191        self.avatar == other.avatar
192            && self.communication_disabled_until == other.communication_disabled_until
193            && self.deaf == Some(other.deaf)
194            && self.joined_at == other.joined_at
195            && self.mute == Some(other.mute)
196            && self.nick == other.nick
197            && self.pending == other.pending
198            && self.premium_since == other.premium_since
199            && self.roles == other.roles
200            && self.user_id == other.user.id
201    }
202}
203
204impl PartialEq<PartialMember> for CachedMember {
205    fn eq(&self, other: &PartialMember) -> bool {
206        self.communication_disabled_until == other.communication_disabled_until
207            && self.deaf == Some(other.deaf)
208            && self.joined_at == other.joined_at
209            && self.mute == Some(other.mute)
210            && self.nick == other.nick
211            && self.premium_since == other.premium_since
212            && self.roles == other.roles
213    }
214}
215
216impl PartialEq<InteractionMember> for CachedMember {
217    fn eq(&self, other: &InteractionMember) -> bool {
218        self.joined_at == other.joined_at
219            && self.nick == other.nick
220            && self.premium_since == other.premium_since
221            && self.roles == other.roles
222    }
223}
224
225impl Cache {
226    pub(crate) async fn cache_members(
227        &self,
228        guild_id: Id<GuildMarker>,
229        members: impl IntoIterator<Item = Member>,
230    ) -> Result<(), Error> {
231        for member in members {
232            self.cache_member(guild_id, member).await?;
233        }
234
235        Ok(())
236    }
237
238    pub(crate) async fn cache_member(
239        &self,
240        guild_id: Id<GuildMarker>,
241        member: Member,
242    ) -> Result<(), Error> {
243        let member_id = member.user.id;
244        let id = (guild_id, member_id);
245
246        if self.members.get(&id).await?.is_some_and(|m| m == member) {
247            return Ok(());
248        }
249
250        self.cache_user(&member.user, Some(guild_id)).await?;
251        self.members
252            .insert(&id, &CachedMember::from(member.clone()))
253            .await?;
254        self.guild_members.insert(&guild_id, &member_id).await?;
255
256        Ok(())
257    }
258
259    pub(crate) async fn cache_borrowed_partial_member(
260        &self,
261        guild_id: Id<GuildMarker>,
262        member: &PartialMember,
263        user_id: Id<UserMarker>,
264    ) -> Result<(), Error> {
265        let id = (guild_id, user_id);
266
267        if self.members.get(&id).await?.is_some_and(|m| m == *member) {
268            return Ok(());
269        }
270
271        self.guild_members.insert(&guild_id, &user_id).await?;
272
273        self.members
274            .insert(&id, &CachedMember::from((user_id, member.clone())))
275            .await?;
276
277        Ok(())
278    }
279
280    pub(crate) async fn cache_borrowed_interaction_member(
281        &self,
282        guild_id: Id<GuildMarker>,
283        member: &InteractionMember,
284        user_id: Id<UserMarker>,
285    ) -> Result<(), Error> {
286        let id = (guild_id, user_id);
287
288        let (avatar, deaf, mute) = match self.members.get(&id).await? {
289            Some(m) if &m == member => return Ok(()),
290            Some(m) => (m.avatar, m.deaf, m.mute),
291            None => (None, None, None),
292        };
293
294        self.guild_members.insert(&guild_id, &user_id).await?;
295
296        let cached = CachedMember::from(ComputedInteractionMember {
297            avatar,
298            deaf,
299            interaction_member: member.clone(),
300            mute,
301            user_id,
302        });
303
304        self.members.insert(&id, &cached).await?;
305
306        Ok(())
307    }
308}