tulpje-cache 0.4.0

A redis cache for twilight
Documentation
use std::{hash::Hash, mem};

use serde::{Deserialize, Serialize, de::DeserializeOwned};
use twilight_model::{
    gateway::payload::incoming::GuildUpdate,
    guild::{
        AfkTimeout, DefaultMessageNotificationLevel, ExplicitContentFilter, Guild, GuildFeature,
        MfaLevel, NSFWLevel, Permissions, PremiumTier, SystemChannelFlags, VerificationLevel,
    },
    id::{
        Id,
        marker::{ApplicationMarker, ChannelMarker, GuildMarker, UserMarker},
    },
    util::{ImageHash, Timestamp},
};

use crate::{
    Cache, Error,
    repository::{MappedSetRepository, Repository},
};

#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CachedGuild {
    pub afk_channel_id: Option<Id<ChannelMarker>>,
    pub afk_timeout: AfkTimeout,
    pub application_id: Option<Id<ApplicationMarker>>,
    pub banner: Option<ImageHash>,
    pub default_message_notifications: DefaultMessageNotificationLevel,
    pub description: Option<String>,
    pub discovery_splash: Option<ImageHash>,
    pub explicit_content_filter: ExplicitContentFilter,
    pub features: Vec<GuildFeature>,
    pub icon: Option<ImageHash>,
    pub id: Id<GuildMarker>,
    pub joined_at: Option<Timestamp>,
    pub large: bool,
    pub max_members: Option<u64>,
    pub max_presences: Option<u64>,
    pub max_video_channel_users: Option<u64>,
    pub member_count: Option<u64>,
    pub mfa_level: MfaLevel,
    pub name: String,
    pub nsfw_level: NSFWLevel,
    pub owner_id: Id<UserMarker>,
    pub owner: Option<bool>,
    pub permissions: Option<Permissions>,
    pub preferred_locale: String,
    pub premium_progress_bar_enabled: bool,
    pub premium_subscription_count: Option<u64>,
    pub premium_tier: PremiumTier,
    pub public_updates_channel_id: Option<Id<ChannelMarker>>,
    pub rules_channel_id: Option<Id<ChannelMarker>>,
    pub safety_alerts_channel_id: Option<Id<ChannelMarker>>,
    pub splash: Option<ImageHash>,
    pub system_channel_id: Option<Id<ChannelMarker>>,
    pub system_channel_flags: SystemChannelFlags,
    pub unavailable: Option<bool>,
    pub vanity_url_code: Option<String>,
    pub verification_level: VerificationLevel,
    pub widget_channel_id: Option<Id<ChannelMarker>>,
    pub widget_enabled: Option<bool>,
}

impl CachedGuild {
    pub(crate) fn update_with_guild_update(&mut self, guild_update: &GuildUpdate) {
        self.afk_channel_id = guild_update.afk_channel_id;
        self.afk_timeout = guild_update.afk_timeout;
        self.banner = guild_update.banner;
        self.default_message_notifications = guild_update.default_message_notifications;
        self.description = guild_update.description.clone();
        self.features = guild_update.features.clone();
        self.icon = guild_update.icon;
        self.max_members = guild_update.max_members;
        self.max_presences = Some(guild_update.max_presences.unwrap_or(25000));
        self.mfa_level = guild_update.mfa_level;
        self.name = guild_update.name.clone();
        self.nsfw_level = guild_update.nsfw_level;
        self.owner = guild_update.owner;
        self.owner_id = guild_update.owner_id;
        self.permissions = guild_update.permissions;
        self.preferred_locale = guild_update.preferred_locale.clone();
        self.premium_tier = guild_update.premium_tier;
        self.premium_subscription_count
            .replace(guild_update.premium_subscription_count.unwrap_or_default());
        self.splash = guild_update.splash;
        self.system_channel_id = guild_update.system_channel_id;
        self.verification_level = guild_update.verification_level;
        self.vanity_url_code = guild_update.vanity_url_code.clone();
        self.widget_channel_id = guild_update.widget_channel_id;
        self.widget_enabled = guild_update.widget_enabled;
    }
}

impl From<Guild> for CachedGuild {
    fn from(guild: Guild) -> Self {
        let Guild {
            afk_channel_id,
            afk_timeout,
            application_id,
            banner,
            default_message_notifications,
            description,
            discovery_splash,
            explicit_content_filter,
            features,
            icon,
            id,
            joined_at,
            large,
            max_members,
            max_presences,
            max_video_channel_users,
            member_count,
            mfa_level,
            name,
            nsfw_level,
            owner_id,
            owner,
            permissions,
            preferred_locale,
            premium_progress_bar_enabled,
            premium_subscription_count,
            premium_tier,
            public_updates_channel_id,
            rules_channel_id,
            safety_alerts_channel_id,
            splash,
            system_channel_flags,
            system_channel_id,
            unavailable,
            vanity_url_code,
            verification_level,
            widget_channel_id,
            widget_enabled,
            ..
        } = guild;

        Self {
            afk_channel_id,
            afk_timeout,
            application_id,
            banner,
            default_message_notifications,
            description,
            discovery_splash,
            explicit_content_filter,
            features,
            icon,
            id,
            joined_at,
            large,
            max_members,
            max_presences,
            max_video_channel_users,
            member_count,
            mfa_level,
            name,
            nsfw_level,
            owner_id,
            owner,
            permissions,
            preferred_locale,
            premium_progress_bar_enabled,
            premium_subscription_count,
            premium_tier,
            public_updates_channel_id,
            rules_channel_id,
            safety_alerts_channel_id,
            splash,
            system_channel_id,
            system_channel_flags,
            unavailable,
            vanity_url_code,
            verification_level,
            widget_channel_id,
            widget_enabled,
        }
    }
}

impl PartialEq<Guild> for CachedGuild {
    fn eq(&self, other: &Guild) -> bool {
        self.afk_channel_id == other.afk_channel_id
            && self.afk_timeout == other.afk_timeout
            && self.application_id == other.application_id
            && self.banner == other.banner
            && self.default_message_notifications == other.default_message_notifications
            && self.description == other.description
            && self.discovery_splash == other.discovery_splash
            && self.explicit_content_filter == other.explicit_content_filter
            && self.features == other.features
            && self.icon == other.icon
            && self.joined_at == other.joined_at
            && self.large == other.large
            && self.max_members == other.max_members
            && self.max_presences == other.max_presences
            && self.max_video_channel_users == other.max_video_channel_users
            && self.member_count == other.member_count
            && self.mfa_level == other.mfa_level
            && self.name == other.name
            && self.nsfw_level == other.nsfw_level
            && self.owner_id == other.owner_id
            && self.owner == other.owner
            && self.permissions == other.permissions
            && self.preferred_locale == other.preferred_locale
            && self.premium_progress_bar_enabled == other.premium_progress_bar_enabled
            && self.premium_subscription_count == other.premium_subscription_count
            && self.premium_tier == other.premium_tier
            && self.public_updates_channel_id == other.public_updates_channel_id
            && self.rules_channel_id == other.rules_channel_id
            && self.safety_alerts_channel_id == other.safety_alerts_channel_id
            && self.splash == other.splash
            && self.system_channel_id == other.system_channel_id
            && self.system_channel_flags == other.system_channel_flags
            && self.unavailable == other.unavailable
            && self.vanity_url_code == other.vanity_url_code
            && self.verification_level == other.verification_level
            && self.widget_channel_id == other.widget_channel_id
            && self.widget_enabled == other.widget_enabled
    }
}

impl Cache {
    pub(crate) async fn unavailable_guild(&self, guild_id: Id<GuildMarker>) -> Result<(), Error> {
        self.unavailable_guilds.insert(&guild_id).await?;
        self.delete_guild(guild_id, true).await?;

        Ok(())
    }

    pub(crate) async fn cache_guild(&self, mut guild: Guild) -> Result<(), Error> {
        for channel in &mut guild.channels {
            channel.guild_id = Some(guild.id);
        }
        for thread in &mut guild.threads {
            thread.guild_id = Some(guild.id);
        }

        self.cache_channels(mem::take(&mut guild.channels)).await?;
        self.cache_channels(mem::take(&mut guild.threads)).await?;
        self.cache_emojis(guild.id, mem::take(&mut guild.emojis))
            .await?;
        self.cache_members(guild.id, mem::take(&mut guild.members))
            .await?;
        self.cache_presences(guild.id, mem::take(&mut guild.presences))
            .await?;
        self.cache_roles(guild.id, mem::take(&mut guild.roles))
            .await?;
        self.cache_stickers(guild.id, mem::take(&mut guild.stickers))
            .await?;
        self.cache_voice_states(mem::take(&mut guild.voice_states))
            .await?;
        self.cache_stage_instances(guild.id, mem::take(&mut guild.stage_instances))
            .await?;
        self.cache_guild_scheduled_events(guild.id, mem::take(&mut guild.guild_scheduled_events))
            .await?;

        self.unavailable_guilds.remove(&guild.id).await?;
        self.guilds
            .insert(&guild.id, &CachedGuild::from(guild.clone()))
            .await?;

        Ok(())
    }

    pub(crate) async fn delete_guild(
        &self,
        guild_id: Id<GuildMarker>,
        unavailable: bool,
    ) -> Result<(), Error> {
        async fn remove_ids<
            T: Eq + Hash + Serialize + DeserializeOwned,
            U: Serialize + DeserializeOwned,
        >(
            guild_map: &MappedSetRepository<Id<GuildMarker>, T>,
            container: &Repository<T, U>,
            guild_id: Id<GuildMarker>,
        ) -> Result<(), Error> {
            let ids = guild_map.members(&guild_id).await?;
            container.remove_multi(&ids).await?;
            Ok(())
        }

        if unavailable {
            if let Some(mut guild) = self.guilds.get(&guild_id).await? {
                guild.unavailable = Some(true);
                self.guilds.insert(&guild_id, &guild).await?;
            }
        } else {
            self.guilds.remove(&guild_id).await?;
        }

        remove_ids(&self.guild_channels, &self.channels, guild_id).await?;
        remove_ids(&self.guild_emojis, &self.emojis, guild_id).await?;
        remove_ids(&self.guild_roles, &self.roles, guild_id).await?;
        remove_ids(&self.guild_stickers, &self.stickers, guild_id).await?;

        self.voice_state_guilds.clear(&guild_id).await?;

        let members_to_remove: Vec<_> = self
            .guild_members
            .members(&guild_id)
            .await?
            .into_iter()
            .map(|user_id| (guild_id, user_id))
            .collect();
        self.members.remove_multi(members_to_remove.iter()).await?;
        self.guild_members.clear(&guild_id).await?;

        let presences_to_remove: Vec<_> = self
            .guild_presences
            .members(&guild_id)
            .await?
            .into_iter()
            .map(|user_id| (guild_id, user_id))
            .collect();
        self.presences
            .remove_multi(presences_to_remove.iter())
            .await?;
        self.guild_presences.clear(&guild_id).await?;

        Ok(())
    }

    pub(crate) async fn update_guild(&self, guild_update: &GuildUpdate) -> Result<(), Error> {
        let Some(mut guild) = self.guilds.get(&guild_update.id).await? else {
            return Ok(());
        };

        guild.update_with_guild_update(guild_update);
        self.guilds.insert(&guild.id, &guild).await?;

        Ok(())
    }
}