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 serenity::all::{ChannelId, GuildChannel, GuildId};
13use serenity::client::Context;
14use std::collections::HashMap;
15use std::sync::LazyLock;
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: LazyLock<Cache<GuildId, HashMap<ChannelId, GuildChannel>>> = {
29    LazyLock::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: LazyLock<Cache<ChannelId, GuildChannel>> = {
42    LazyLock::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                let channel_list =
63                    if let Some(channels) = GUILD_CHANNEL_LIST_CACHE.get(&self.guild_id).await {
64                        channels
65                    } else {
66                        self.get_channel_list_from_api(ctx).await?
67                    };
68
69                let channel = match channel_list.get(&self.channel_id).cloned() {
70                    Some(c) => c,
71                    None => {
72                        let data = self
73                            .guild_id
74                            .get_active_threads(&ctx.http)
75                            .await
76                            .context("Failed to get active threads")?;
77                        data.threads
78                            .iter()
79                            .find(|t| t.id == self.channel_id)
80                            .cloned()
81                            .ok_or_else(|| anyhow::anyhow!("Channel not found in cache"))?
82                    }
83                };
84
85                GUILD_CHANNEL_CACHE
86                    .insert(self.channel_id, channel.clone())
87                    .await;
88                Ok(channel)
89            }
90        }
91    }
92
93    /// Fetches the channel list from the Discord API and updates the cache.
94    async fn get_channel_list_from_api(
95        &self,
96        ctx: &Context,
97    ) -> anyhow::Result<HashMap<ChannelId, GuildChannel>> {
98        let guild = ctx
99            .http
100            .get_guild(self.guild_id)
101            .await
102            .context("Failed to get guild")?;
103        let channels = guild
104            .channels(&ctx)
105            .await
106            .context("Failed to get channel list")?;
107
108        GUILD_CHANNEL_LIST_CACHE
109            .insert(self.guild_id, channels.clone())
110            .await;
111
112        Ok(channels)
113    }
114}