1use dashmap::{mapref::one::Ref, DashMap};
2use twilight_cache_inmemory::InMemoryCache;
3use twilight_http::{request::channel::webhook::CreateWebhook, Client};
4use twilight_model::{
5 channel::Webhook,
6 gateway::event::Event,
7 guild::Permissions,
8 id::{
9 marker::{ChannelMarker, UserMarker},
10 Id,
11 },
12};
13
14#[derive(thiserror::Error, Debug)]
15pub enum Error {
17 #[error("An error was returned by Twilight's HTTP client: {0}")]
19 Http(#[from] twilight_http::error::Error),
20 #[error(
23 "An error was returned by Twilight's HTTP client while deserializing the response: {0}"
24 )]
25 Deserialize(#[from] twilight_http::response::DeserializeBodyError),
26 #[error("An error was returned by Twilight while validating a request: {0}")]
28 Validation(#[from] twilight_validate::request::ValidationError),
29 #[error(
32 "An error was returned by Twilight while trying to get the permissions from the cache: {0}"
33 )]
34 CachePermissions(#[from] twilight_cache_inmemory::permission::ChannelError),
35}
36
37#[derive(Debug, Clone)]
38pub enum PermissionsSource<'cache> {
40 Given(Permissions),
42 Cached {
48 cache: &'cache InMemoryCache,
50 current_user_id: Id<UserMarker>,
52 },
53 Request,
59}
60
61impl PermissionsSource<'_> {
62 fn get(self, channel_id: Id<ChannelMarker>) -> Result<Permissions, Error> {
64 Ok(match self {
65 PermissionsSource::Given(permissions) => permissions,
66 PermissionsSource::Cached {
67 cache,
68 current_user_id,
69 } => cache
70 .permissions()
71 .in_channel(current_user_id, channel_id)?,
72 PermissionsSource::Request => Permissions::all(),
73 })
74 }
75}
76
77#[derive(Debug)]
79#[allow(clippy::module_name_repetitions)]
80pub struct WebhooksCache(DashMap<Id<ChannelMarker>, Webhook>);
81
82impl Default for WebhooksCache {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl WebhooksCache {
89 #[must_use]
94 pub fn new() -> Self {
95 Self(DashMap::new())
96 }
97
98 #[allow(clippy::unwrap_used)]
111 pub async fn get_infallible(
112 &self,
113 http: &Client,
114 channel_id: Id<ChannelMarker>,
115 name: &str,
116 ) -> Result<Ref<'_, Id<ChannelMarker>, Webhook>, Error> {
117 if let Some(webhook) = self.get(channel_id) {
118 Ok(webhook)
119 } else {
120 let webhook = if let Some(webhook) = http
121 .channel_webhooks(channel_id)
122 .await?
123 .models()
124 .await?
125 .into_iter()
126 .find(|w| w.token.is_some())
127 {
128 webhook
129 } else {
130 http.create_webhook(channel_id, name)?
131 .await?
132 .model()
133 .await?
134 };
135 self.0.insert(channel_id, webhook);
136 Ok(self.get(channel_id).unwrap())
137 }
138 }
139
140 pub async fn create(&self, create_webhook: CreateWebhook<'_>) -> Result<(), Error> {
146 let webhook = create_webhook.await?.model().await?;
147 self.0.insert(webhook.channel_id, webhook);
148
149 Ok(())
150 }
151
152 #[must_use]
154 pub fn get(
155 &self,
156 channel_id: Id<ChannelMarker>,
157 ) -> Option<Ref<'_, Id<ChannelMarker>, Webhook>> {
158 self.0.get(&channel_id)
159 }
160
161 #[allow(clippy::wildcard_enum_match_arm)]
182 pub async fn update(
183 &self,
184 event: &Event,
185 http: &Client,
186 permissions: PermissionsSource<'_>,
187 ) -> Result<(), Error> {
188 match event {
189 Event::ChannelDelete(channel) => {
190 self.0.remove(&channel.id);
191 }
192 Event::GuildDelete(guild) => self
193 .0
194 .retain(|_, webhook| webhook.guild_id != Some(guild.id)),
195 Event::WebhooksUpdate(update) => {
196 if !self.0.contains_key(&update.channel_id) {
197 return Ok(());
198 }
199
200 if !permissions
201 .get(update.channel_id)?
202 .contains(Permissions::MANAGE_WEBHOOKS)
203 {
204 self.0.remove(&update.channel_id);
205 return Ok(());
206 }
207
208 if let Ok(response) = http.channel_webhooks(update.channel_id).await {
209 if response
210 .models()
211 .await?
212 .iter()
213 .any(|webhook| webhook.token.is_some())
214 {
215 return Ok(());
216 }
217 };
218
219 self.0.remove(&update.channel_id);
220 }
221 _ => (),
222 };
223
224 Ok(())
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use twilight_http::Client;
231 use twilight_model::{
232 channel::{Channel, ChannelType, Webhook, WebhookType},
233 gateway::{
234 event::Event,
235 payload::incoming::{ChannelDelete, GuildDelete, WebhooksUpdate},
236 },
237 id::Id,
238 };
239
240 use crate::cache::{PermissionsSource, WebhooksCache};
241
242 const WEBHOOK: Webhook = Webhook {
243 id: Id::new(1),
244 channel_id: Id::new(1),
245 kind: WebhookType::Application,
246 application_id: None,
247 avatar: None,
248 guild_id: Some(Id::new(10)),
249 name: None,
250 source_channel: None,
251 source_guild: None,
252 token: None,
253 url: None,
254 user: None,
255 };
256
257 #[allow(clippy::unwrap_used)]
258 async fn mock_update(cache: &WebhooksCache, event: &Event) {
259 cache
260 .update(
261 event,
262 &Client::builder().build(),
263 PermissionsSource::Request,
264 )
265 .await
266 .unwrap();
267 }
268
269 #[test]
270 fn get() {
271 let cache = WebhooksCache::new();
272 cache.0.insert(Id::new(1), WEBHOOK);
273
274 assert!(cache.get(Id::new(2)).is_none());
275
276 assert_eq!(cache.get(Id::new(1)).as_deref(), Some(&WEBHOOK));
277 }
278
279 #[tokio::test]
280 async fn update() {
281 let cache = WebhooksCache::new();
282
283 cache.0.insert(Id::new(1), WEBHOOK);
284 mock_update(
285 &cache,
286 &Event::GuildDelete(GuildDelete {
287 id: Id::new(11),
288 unavailable: false,
289 }),
290 )
291 .await;
292 assert_eq!(cache.get(Id::new(1)).as_deref(), Some(&WEBHOOK));
293
294 cache.0.insert(Id::new(2), WEBHOOK);
295 mock_update(
296 &cache,
297 &Event::GuildDelete(GuildDelete {
298 id: Id::new(10),
299 unavailable: false,
300 }),
301 )
302 .await;
303 assert!(cache.get(Id::new(1)).is_none());
304 assert!(cache.get(Id::new(2)).is_none());
305
306 cache.0.insert(Id::new(3), WEBHOOK);
307 mock_update(
308 &cache,
309 &Event::ChannelDelete(Box::new(ChannelDelete(Channel {
310 id: Id::new(3),
311 guild_id: Some(Id::new(10)),
312 kind: ChannelType::GuildText,
313 application_id: None,
314 applied_tags: None,
315 available_tags: None,
316 bitrate: None,
317 default_auto_archive_duration: None,
318 default_reaction_emoji: None,
319 default_thread_rate_limit_per_user: None,
320 icon: None,
321 invitable: None,
322 last_message_id: None,
323 last_pin_timestamp: None,
324 member: None,
325 member_count: None,
326 message_count: None,
327 name: None,
328 newly_created: None,
329 nsfw: None,
330 owner_id: None,
331 parent_id: None,
332 permission_overwrites: None,
333 position: None,
334 rate_limit_per_user: None,
335 recipients: None,
336 rtc_region: None,
337 thread_metadata: None,
338 topic: None,
339 user_limit: None,
340 video_quality_mode: None,
341 flags: None,
342 }))),
343 )
344 .await;
345 assert!(cache.get(Id::new(3)).is_none());
346
347 cache.0.insert(Id::new(4), WEBHOOK);
348 mock_update(
349 &cache,
350 &Event::WebhooksUpdate(WebhooksUpdate {
351 channel_id: Id::new(12),
352 guild_id: Id::new(10),
353 }),
354 )
355 .await;
356 assert_eq!(cache.get(Id::new(4)).as_deref(), Some(&WEBHOOK));
357
358 cache.0.insert(Id::new(5), WEBHOOK);
359 mock_update(
360 &cache,
361 &Event::WebhooksUpdate(WebhooksUpdate {
362 channel_id: Id::new(5),
363 guild_id: Id::new(10),
364 }),
365 )
366 .await;
367 assert!(cache.get(Id::new(5)).is_none());
368 }
369}