1use anyhow::Context as _;
11use moka::future::{Cache, CacheBuilder};
12use serenity::all::{ChannelId, GuildChannel, GuildId};
13use serenity::client::Context;
14use std::collections::HashMap;
15use std::sync::LazyLock;
16
17pub struct CacheArgs {
19 pub guild_id: GuildId,
21 pub channel_id: ChannelId,
23}
24
25fn channel_cache<K, V>(name: &str) -> Cache<K, V>
28where
29 K: std::hash::Hash + Eq + Send + Sync + 'static,
30 V: Clone + Send + Sync + 'static,
31{
32 CacheBuilder::new(500)
33 .name(name)
34 .time_to_idle(std::time::Duration::from_secs(3600))
35 .time_to_live(std::time::Duration::from_secs(43200))
36 .build()
37}
38
39pub static GUILD_CHANNEL_LIST_CACHE: LazyLock<Cache<GuildId, HashMap<ChannelId, GuildChannel>>> =
41 LazyLock::new(|| channel_cache("guild_channel_list_cache"));
42
43pub static GUILD_CHANNEL_CACHE: LazyLock<Cache<ChannelId, GuildChannel>> =
45 LazyLock::new(|| channel_cache("guild_channel_cache"));
46
47impl CacheArgs {
48 #[tracing::instrument(
55 skip(self, ctx),
56 fields(guild_id = %self.guild_id, channel_id = %self.channel_id)
57 )]
58 pub async fn get(&self, ctx: &Context) -> anyhow::Result<GuildChannel> {
59 if let Some(channel) = GUILD_CHANNEL_CACHE.get(&self.channel_id).await {
60 tracing::debug!("channel cache hit");
61 return Ok(channel);
62 }
63 tracing::debug!("channel cache miss");
64
65 let channel_list = GUILD_CHANNEL_LIST_CACHE
69 .try_get_with(self.guild_id, self.get_channel_list_from_api(ctx))
70 .await
71 .map_err(|e| anyhow::anyhow!("Failed to get channel list: {e}"))?;
72
73 let channel = match channel_list.get(&self.channel_id).cloned() {
74 Some(c) => c,
75 None => {
76 tracing::debug!("channel not in list, searching active threads");
79 let data = self
80 .guild_id
81 .get_active_threads(&ctx.http)
82 .await
83 .context("Failed to get active threads")?;
84 data.threads
85 .iter()
86 .find(|t| t.id == self.channel_id)
87 .cloned()
88 .ok_or_else(|| {
89 tracing::debug!("channel not found in guild or active threads");
90 anyhow::anyhow!("Channel not found in cache")
91 })?
92 }
93 };
94
95 GUILD_CHANNEL_CACHE
96 .insert(self.channel_id, channel.clone())
97 .await;
98 tracing::trace!("inserted channel into cache");
99 Ok(channel)
100 }
101
102 #[tracing::instrument(skip(self, ctx), fields(guild_id = %self.guild_id))]
107 async fn get_channel_list_from_api(
108 &self,
109 ctx: &Context,
110 ) -> anyhow::Result<HashMap<ChannelId, GuildChannel>> {
111 tracing::debug!("fetching channel list from Discord API");
112 let started = std::time::Instant::now();
113 let channels = self
114 .guild_id
115 .channels(&ctx.http)
116 .await
117 .context("Failed to get channel list")?;
118
119 tracing::debug!(
120 channels = channels.len(),
121 elapsed_ms = started.elapsed().as_millis(),
122 "fetched channel list from Discord API"
123 );
124
125 Ok(channels)
126 }
127}