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