twilight_cache_inmemory/event/
reaction.rs

1use crate::{
2    CacheableModels, InMemoryCache, UpdateCache,
3    config::ResourceType,
4    traits::{CacheableCurrentUser, CacheableMessage},
5};
6use twilight_model::{
7    channel::message::{EmojiReactionType, Reaction, ReactionCountDetails},
8    gateway::payload::incoming::{
9        ReactionAdd, ReactionRemove, ReactionRemoveAll, ReactionRemoveEmoji,
10    },
11};
12
13impl<CacheModels: CacheableModels> UpdateCache<CacheModels> for ReactionAdd {
14    fn update(&self, cache: &InMemoryCache<CacheModels>) {
15        if !cache.wants(ResourceType::REACTION) {
16            return;
17        }
18
19        let key = self.0.message_id;
20
21        let Some(mut message) = cache.messages.get_mut(&key) else {
22            return;
23        };
24
25        if let Some(reaction) = message
26            .reactions_mut()
27            .iter_mut()
28            .find(|r| reactions_eq(&r.emoji, &self.0.emoji))
29        {
30            if !reaction.me
31                && let Some(current_user) = cache.current_user()
32                && current_user.id() == self.0.user_id
33            {
34                reaction.me = true;
35            }
36
37            reaction.count += 1;
38        } else {
39            let me = cache
40                .current_user()
41                .is_some_and(|user| user.id() == self.0.user_id);
42
43            message.add_reaction(Reaction {
44                burst_colors: Vec::new(),
45                count: 1,
46                count_details: ReactionCountDetails {
47                    burst: 0,
48                    normal: 1,
49                },
50                emoji: self.0.emoji.clone(),
51                me,
52                me_burst: false,
53            });
54        }
55    }
56}
57
58impl<CacheModels: CacheableModels> UpdateCache<CacheModels> for ReactionRemove {
59    fn update(&self, cache: &InMemoryCache<CacheModels>) {
60        if !cache.wants(ResourceType::REACTION) {
61            return;
62        }
63
64        let Some(mut message) = cache.messages.get_mut(&self.0.message_id) else {
65            return;
66        };
67
68        if let Some(reaction) = message
69            .reactions_mut()
70            .iter_mut()
71            .find(|r| reactions_eq(&r.emoji, &self.0.emoji))
72        {
73            if reaction.me
74                && let Some(current_user) = cache.current_user()
75                && current_user.id() == self.0.user_id
76            {
77                reaction.me = false;
78            }
79
80            if reaction.count > 1 {
81                reaction.count -= 1;
82            } else {
83                message.retain_reactions(|e| !(reactions_eq(&e.emoji, &self.0.emoji)));
84            }
85        }
86    }
87}
88
89impl<CacheModels: CacheableModels> UpdateCache<CacheModels> for ReactionRemoveAll {
90    fn update(&self, cache: &InMemoryCache<CacheModels>) {
91        if !cache.wants(ResourceType::REACTION) {
92            return;
93        }
94
95        let Some(mut message) = cache.messages.get_mut(&self.message_id) else {
96            return;
97        };
98
99        message.clear_reactions();
100    }
101}
102
103impl<CacheModels: CacheableModels> UpdateCache<CacheModels> for ReactionRemoveEmoji {
104    fn update(&self, cache: &InMemoryCache<CacheModels>) {
105        if !cache.wants(ResourceType::REACTION) {
106            return;
107        }
108
109        let Some(mut message) = cache.messages.get_mut(&self.message_id) else {
110            return;
111        };
112
113        let maybe_index = message
114            .reactions()
115            .iter()
116            .position(|r| reactions_eq(&r.emoji, &self.emoji));
117
118        if let Some(index) = maybe_index {
119            message.remove_reaction(index);
120        }
121    }
122}
123
124fn reactions_eq(a: &EmojiReactionType, b: &EmojiReactionType) -> bool {
125    match (a, b) {
126        (
127            EmojiReactionType::Custom { id: id_a, .. },
128            EmojiReactionType::Custom { id: id_b, .. },
129        ) => id_a == id_b,
130        (
131            EmojiReactionType::Unicode { name: name_a },
132            EmojiReactionType::Unicode { name: name_b },
133        ) => name_a == name_b,
134        _ => false,
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::reactions_eq;
141    use crate::{model::CachedMessage, test};
142    use twilight_model::{
143        channel::message::{EmojiReactionType, Reaction},
144        gateway::{
145            GatewayReaction,
146            payload::incoming::{ReactionRemove, ReactionRemoveAll, ReactionRemoveEmoji},
147        },
148        id::Id,
149    };
150
151    fn find_custom_react(msg: &CachedMessage) -> Option<&Reaction> {
152        msg.reactions.iter().find(|&r| {
153            reactions_eq(
154                &r.emoji,
155                &EmojiReactionType::Custom {
156                    animated: false,
157                    id: Id::new(6),
158                    name: None,
159                },
160            )
161        })
162    }
163
164    #[test]
165    fn reaction_add() {
166        let cache = test::cache_with_message_and_reactions();
167        let msg = cache.message(Id::new(4)).unwrap();
168
169        assert_eq!(msg.reactions.len(), 3);
170
171        let world_react = msg
172            .reactions
173            .iter()
174            .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ—ΊοΈ"));
175        let smiley_react = msg
176            .reactions
177            .iter()
178            .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ˜€"));
179        let custom_react = find_custom_react(&msg);
180
181        assert!(world_react.is_some());
182        assert_eq!(world_react.unwrap().count, 1);
183        assert!(smiley_react.is_some());
184        assert_eq!(smiley_react.unwrap().count, 2);
185        assert!(custom_react.is_some());
186        assert_eq!(custom_react.unwrap().count, 1);
187    }
188
189    #[test]
190    fn reaction_remove() {
191        let cache = test::cache_with_message_and_reactions();
192        cache.update(&ReactionRemove(GatewayReaction {
193            burst: false,
194            burst_colors: Vec::new(),
195            channel_id: Id::new(2),
196            emoji: EmojiReactionType::Unicode {
197                name: "πŸ˜€".to_owned(),
198            },
199            guild_id: Some(Id::new(1)),
200            member: None,
201            message_author_id: None,
202            message_id: Id::new(4),
203            user_id: Id::new(5),
204        }));
205        cache.update(&ReactionRemove(GatewayReaction {
206            burst: false,
207            burst_colors: Vec::new(),
208            channel_id: Id::new(2),
209            emoji: EmojiReactionType::Custom {
210                animated: false,
211                id: Id::new(6),
212                name: None,
213            },
214            guild_id: Some(Id::new(1)),
215            member: None,
216            message_author_id: None,
217            message_id: Id::new(4),
218            user_id: Id::new(5),
219        }));
220
221        let msg = cache.message(Id::new(4)).unwrap();
222
223        assert_eq!(msg.reactions.len(), 2);
224
225        let world_react = msg
226            .reactions
227            .iter()
228            .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ—ΊοΈ"));
229        let smiley_react = msg
230            .reactions
231            .iter()
232            .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ˜€"));
233        let custom_react = find_custom_react(&msg);
234
235        assert!(world_react.is_some());
236        assert_eq!(world_react.unwrap().count, 1);
237        assert!(smiley_react.is_some());
238        assert_eq!(smiley_react.unwrap().count, 1);
239        assert!(custom_react.is_none());
240    }
241
242    #[test]
243    fn reaction_remove_all() {
244        let cache = test::cache_with_message_and_reactions();
245        cache.update(&ReactionRemoveAll {
246            channel_id: Id::new(2),
247            message_id: Id::new(4),
248            guild_id: Some(Id::new(1)),
249        });
250
251        let msg = cache.message(Id::new(4)).unwrap();
252
253        assert_eq!(msg.reactions.len(), 0);
254    }
255
256    #[test]
257    fn reaction_remove_emoji() {
258        let cache = test::cache_with_message_and_reactions();
259        cache.update(&ReactionRemoveEmoji {
260            channel_id: Id::new(2),
261            emoji: EmojiReactionType::Unicode {
262                name: "πŸ˜€".to_owned(),
263            },
264            guild_id: Id::new(1),
265            message_id: Id::new(4),
266        });
267        cache.update(&ReactionRemoveEmoji {
268            channel_id: Id::new(2),
269            emoji: EmojiReactionType::Custom {
270                animated: false,
271                id: Id::new(6),
272                name: None,
273            },
274            guild_id: Id::new(1),
275            message_id: Id::new(4),
276        });
277
278        let msg = cache.message(Id::new(4)).unwrap();
279
280        assert_eq!(msg.reactions.len(), 1);
281
282        let world_react = msg
283            .reactions
284            .iter()
285            .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ—ΊοΈ"));
286        let smiley_react = msg
287            .reactions
288            .iter()
289            .find(|&r| matches!(&r.emoji, EmojiReactionType::Unicode {name} if name == "πŸ˜€"));
290        let custom_react = find_custom_react(&msg);
291
292        assert!(world_react.is_some());
293        assert_eq!(world_react.unwrap().count, 1);
294        assert!(smiley_react.is_none());
295        assert!(custom_react.is_none());
296    }
297}