Skip to main content

babyrite/
cache.rs

1//! Cache module for guild channels.
2//!
3//! This module provides caching functionality for guild channels using moka cache.
4//! It includes two caches:
5//! - [`GUILD_CHANNEL_LIST_CACHE`]: Caches the list of channels for each guild.
6//! - [`GUILD_CHANNEL_CACHE`]: Caches individual guild channels.
7//!
8//! The [`CacheArgs`] struct is used to retrieve channels from the cache or fetch them from the API if not found.
9
10use anyhow::Context as _;
11use moka::future::{Cache, CacheBuilder};
12use once_cell::sync::Lazy;
13use serenity::all::{ChannelId, GuildChannel, GuildId};
14use serenity::client::Context;
15use std::collections::HashMap;
16
17/// Arguments for cache operations.
18pub struct CacheArgs {
19    /// The ID of the guild.
20    pub guild_id: GuildId,
21    /// The ID of the channel.
22    pub channel_id: ChannelId,
23}
24
25/// Cache for guild channel lists.
26///
27/// Maps guild IDs to their channel lists. TTL: 12 hours, TTI: 1 hour.
28pub static GUILD_CHANNEL_LIST_CACHE: Lazy<Cache<GuildId, HashMap<ChannelId, GuildChannel>>> = {
29    Lazy::new(|| {
30        CacheBuilder::new(500)
31            .name("guild_channel_list_cache")
32            .time_to_idle(std::time::Duration::from_secs(3600))
33            .time_to_live(std::time::Duration::from_secs(43200))
34            .build()
35    })
36};
37
38/// Cache for individual guild channels.
39///
40/// Maps channel IDs to their channel data. TTL: 12 hours, TTI: 1 hour.
41pub static GUILD_CHANNEL_CACHE: Lazy<Cache<ChannelId, GuildChannel>> = {
42    Lazy::new(|| {
43        CacheBuilder::new(500)
44            .name("guild_channel_cache")
45            .time_to_idle(std::time::Duration::from_secs(3600))
46            .time_to_live(std::time::Duration::from_secs(43200))
47            .build()
48    })
49};
50
51impl CacheArgs {
52    /// Retrieves a guild channel from cache or fetches it from the API.
53    ///
54    /// The lookup order is:
55    /// 1. Individual channel cache
56    /// 2. Guild channel list cache
57    /// 3. Discord API (with cache update)
58    pub async fn get(&self, ctx: &Context) -> anyhow::Result<GuildChannel> {
59        match GUILD_CHANNEL_CACHE.get(&self.channel_id).await {
60            Some(channel) => Ok(channel),
61            None => {
62                if let Some(channels) = GUILD_CHANNEL_LIST_CACHE.get(&self.guild_id).await {
63                    return channels
64                        .get(&self.channel_id)
65                        .cloned()
66                        .ok_or_else(|| anyhow::anyhow!("Channel not found in cache"));
67                }
68
69                let channel_list = self.get_channel_list_from_api(ctx).await?;
70                let channel = match channel_list.get(&self.channel_id).cloned() {
71                    Some(c) => c,
72                    None => {
73                        let data = self
74                            .guild_id
75                            .get_active_threads(&ctx.http)
76                            .await
77                            .context("Failed to get active threads")?;
78                        data.threads
79                            .iter()
80                            .find(|t| t.id == self.channel_id)
81                            .cloned()
82                            .ok_or_else(|| anyhow::anyhow!("Channel not found in cache"))?
83                    }
84                };
85
86                GUILD_CHANNEL_CACHE
87                    .insert(self.channel_id, channel.clone())
88                    .await;
89                Ok(channel)
90            }
91        }
92    }
93
94    /// Fetches the channel list from the Discord API and updates the cache.
95    async fn get_channel_list_from_api(
96        &self,
97        ctx: &Context,
98    ) -> anyhow::Result<HashMap<ChannelId, GuildChannel>> {
99        let guild = ctx
100            .http
101            .get_guild(self.guild_id)
102            .await
103            .context("Failed to get guild")?;
104        let channels = guild
105            .channels(&ctx)
106            .await
107            .context("Failed to get channel list")?;
108
109        GUILD_CHANNEL_LIST_CACHE
110            .insert(self.guild_id, channels.clone())
111            .await;
112
113        Ok(channels)
114    }
115}