#![deny(
clippy::all,
clippy::missing_const_for_fn,
clippy::pedantic,
future_incompatible,
missing_docs,
nonstandard_style,
rust_2018_idioms,
rustdoc::broken_intra_doc_links,
unsafe_code,
unused
)]
#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::unnecessary_wraps,
clippy::used_underscore_binding
)]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![doc = include_str!("../README.md")]
pub mod iter;
pub mod model;
#[cfg(feature = "permission-calculator")]
pub mod permission;
mod builder;
mod config;
mod event;
mod stats;
#[cfg(test)]
mod test;
pub use self::{
builder::InMemoryCacheBuilder,
config::{Config, ResourceType},
stats::InMemoryCacheStats,
};
#[cfg(feature = "permission-calculator")]
pub use self::permission::InMemoryCachePermissions;
use self::{
iter::InMemoryCacheIter,
model::{
CachedEmoji, CachedGuild, CachedMember, CachedMessage, CachedPresence, CachedSticker,
CachedVoiceState,
},
};
use dashmap::{
mapref::{entry::Entry, one::Ref},
DashMap, DashSet,
};
use std::{
collections::{BTreeSet, HashSet, VecDeque},
fmt::{Debug, Formatter, Result as FmtResult},
hash::Hash,
ops::Deref,
sync::Mutex,
};
use twilight_model::{
channel::{Channel, StageInstance},
gateway::event::Event,
guild::{GuildIntegration, Role},
id::{
marker::{
ChannelMarker, EmojiMarker, GuildMarker, IntegrationMarker, MessageMarker, RoleMarker,
StageMarker, StickerMarker, UserMarker,
},
Id,
},
user::{CurrentUser, User},
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct GuildResource<T> {
guild_id: Id<GuildMarker>,
value: T,
}
impl<T> GuildResource<T> {
pub const fn guild_id(&self) -> Id<GuildMarker> {
self.guild_id
}
pub const fn resource(&self) -> &T {
&self.value
}
}
impl<T> Deref for GuildResource<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
self.resource()
}
}
pub struct Reference<'a, K, V> {
inner: Ref<'a, K, V>,
}
impl<'a, K: Eq + Hash, V> Reference<'a, K, V> {
#[allow(clippy::missing_const_for_fn)]
fn new(inner: Ref<'a, K, V>) -> Self {
Self { inner }
}
pub fn key(&'a self) -> &'a K {
self.inner.key()
}
pub fn value(&'a self) -> &'a V {
self.inner.value()
}
}
impl<K: Eq + Hash, V: Debug> Debug for Reference<'_, K, V> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("Reference")
.field("inner", self.value())
.finish()
}
}
impl<'a, K: Eq + Hash, V> Deref for Reference<'a, K, V> {
type Target = V;
fn deref(&self) -> &Self::Target {
self.value()
}
}
fn upsert_guild_item<K: Eq + Hash, V: PartialEq>(
map: &DashMap<K, GuildResource<V>>,
guild_id: Id<GuildMarker>,
key: K,
value: V,
) {
match map.entry(key) {
Entry::Occupied(entry) if entry.get().value == value => {}
Entry::Occupied(mut entry) => {
entry.insert(GuildResource { guild_id, value });
}
Entry::Vacant(entry) => {
entry.insert(GuildResource { guild_id, value });
}
}
}
#[derive(Debug, Default)]
pub struct InMemoryCache {
config: Config,
channels: DashMap<Id<ChannelMarker>, Channel>,
channel_messages: DashMap<Id<ChannelMarker>, VecDeque<Id<MessageMarker>>>,
current_user: Mutex<Option<CurrentUser>>,
emojis: DashMap<Id<EmojiMarker>, GuildResource<CachedEmoji>>,
guilds: DashMap<Id<GuildMarker>, CachedGuild>,
guild_channels: DashMap<Id<GuildMarker>, HashSet<Id<ChannelMarker>>>,
guild_emojis: DashMap<Id<GuildMarker>, HashSet<Id<EmojiMarker>>>,
guild_integrations: DashMap<Id<GuildMarker>, HashSet<Id<IntegrationMarker>>>,
guild_members: DashMap<Id<GuildMarker>, HashSet<Id<UserMarker>>>,
guild_presences: DashMap<Id<GuildMarker>, HashSet<Id<UserMarker>>>,
guild_roles: DashMap<Id<GuildMarker>, HashSet<Id<RoleMarker>>>,
guild_stage_instances: DashMap<Id<GuildMarker>, HashSet<Id<StageMarker>>>,
guild_stickers: DashMap<Id<GuildMarker>, HashSet<Id<StickerMarker>>>,
integrations:
DashMap<(Id<GuildMarker>, Id<IntegrationMarker>), GuildResource<GuildIntegration>>,
members: DashMap<(Id<GuildMarker>, Id<UserMarker>), CachedMember>,
messages: DashMap<Id<MessageMarker>, CachedMessage>,
presences: DashMap<(Id<GuildMarker>, Id<UserMarker>), CachedPresence>,
roles: DashMap<Id<RoleMarker>, GuildResource<Role>>,
stage_instances: DashMap<Id<StageMarker>, GuildResource<StageInstance>>,
stickers: DashMap<Id<StickerMarker>, GuildResource<CachedSticker>>,
unavailable_guilds: DashSet<Id<GuildMarker>>,
users: DashMap<Id<UserMarker>, User>,
user_guilds: DashMap<Id<UserMarker>, BTreeSet<Id<GuildMarker>>>,
#[allow(clippy::type_complexity)]
voice_state_channels: DashMap<Id<ChannelMarker>, HashSet<(Id<GuildMarker>, Id<UserMarker>)>>,
voice_state_guilds: DashMap<Id<GuildMarker>, HashSet<Id<UserMarker>>>,
voice_states: DashMap<(Id<GuildMarker>, Id<UserMarker>), CachedVoiceState>,
}
impl InMemoryCache {
pub fn new() -> Self {
Self::default()
}
pub const fn builder() -> InMemoryCacheBuilder {
InMemoryCacheBuilder::new()
}
pub fn clear(&self) {
self.channels.clear();
self.channel_messages.clear();
self.current_user
.lock()
.expect("current user poisoned")
.take();
self.emojis.clear();
self.guilds.clear();
self.guild_channels.clear();
self.guild_emojis.clear();
self.guild_integrations.clear();
self.guild_members.clear();
self.guild_presences.clear();
self.guild_roles.clear();
self.guild_stage_instances.clear();
self.guild_stickers.clear();
self.integrations.clear();
self.members.clear();
self.messages.clear();
self.presences.clear();
self.roles.clear();
self.stickers.clear();
self.unavailable_guilds.clear();
self.users.clear();
self.voice_state_channels.clear();
self.voice_state_guilds.clear();
self.voice_states.clear();
}
pub const fn config(&self) -> &Config {
&self.config
}
#[allow(clippy::iter_not_returning_iterator)]
pub const fn iter(&self) -> InMemoryCacheIter<'_> {
InMemoryCacheIter::new(self)
}
pub const fn stats(&self) -> InMemoryCacheStats<'_> {
InMemoryCacheStats::new(self)
}
#[cfg(feature = "permission-calculator")]
pub const fn permissions(&self) -> InMemoryCachePermissions<'_> {
InMemoryCachePermissions::new(self)
}
pub fn update(&self, value: &impl UpdateCache) {
value.update(self);
}
pub fn current_user(&self) -> Option<CurrentUser> {
self.current_user
.lock()
.expect("current user poisoned")
.clone()
}
pub fn channel(
&self,
channel_id: Id<ChannelMarker>,
) -> Option<Reference<'_, Id<ChannelMarker>, Channel>> {
self.channels.get(&channel_id).map(Reference::new)
}
pub fn channel_messages(
&self,
channel_id: Id<ChannelMarker>,
) -> Option<Reference<'_, Id<ChannelMarker>, VecDeque<Id<MessageMarker>>>> {
self.channel_messages.get(&channel_id).map(Reference::new)
}
pub fn emoji(
&self,
emoji_id: Id<EmojiMarker>,
) -> Option<Reference<'_, Id<EmojiMarker>, GuildResource<CachedEmoji>>> {
self.emojis.get(&emoji_id).map(Reference::new)
}
pub fn guild(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, CachedGuild>> {
self.guilds.get(&guild_id).map(Reference::new)
}
pub fn guild_channels(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<ChannelMarker>>>> {
self.guild_channels.get(&guild_id).map(Reference::new)
}
pub fn guild_emojis(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<EmojiMarker>>>> {
self.guild_emojis.get(&guild_id).map(Reference::new)
}
pub fn guild_integrations(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<IntegrationMarker>>>> {
self.guild_integrations.get(&guild_id).map(Reference::new)
}
pub fn guild_members(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<UserMarker>>>> {
self.guild_members.get(&guild_id).map(Reference::new)
}
pub fn guild_presences(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<UserMarker>>>> {
self.guild_presences.get(&guild_id).map(Reference::new)
}
pub fn guild_roles(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<RoleMarker>>>> {
self.guild_roles.get(&guild_id).map(Reference::new)
}
pub fn guild_stage_instances(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<StageMarker>>>> {
self.guild_stage_instances
.get(&guild_id)
.map(Reference::new)
}
pub fn guild_stickers(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<StickerMarker>>>> {
self.guild_stickers.get(&guild_id).map(Reference::new)
}
pub fn guild_voice_states(
&self,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, Id<GuildMarker>, HashSet<Id<UserMarker>>>> {
self.voice_state_guilds.get(&guild_id).map(Reference::new)
}
#[allow(clippy::type_complexity)]
pub fn integration(
&self,
guild_id: Id<GuildMarker>,
integration_id: Id<IntegrationMarker>,
) -> Option<
Reference<'_, (Id<GuildMarker>, Id<IntegrationMarker>), GuildResource<GuildIntegration>>,
> {
self.integrations
.get(&(guild_id, integration_id))
.map(Reference::new)
}
#[allow(clippy::type_complexity)]
pub fn member(
&self,
guild_id: Id<GuildMarker>,
user_id: Id<UserMarker>,
) -> Option<Reference<'_, (Id<GuildMarker>, Id<UserMarker>), CachedMember>> {
self.members.get(&(guild_id, user_id)).map(Reference::new)
}
pub fn message(
&self,
message_id: Id<MessageMarker>,
) -> Option<Reference<'_, Id<MessageMarker>, CachedMessage>> {
self.messages.get(&message_id).map(Reference::new)
}
#[allow(clippy::type_complexity)]
pub fn presence(
&self,
guild_id: Id<GuildMarker>,
user_id: Id<UserMarker>,
) -> Option<Reference<'_, (Id<GuildMarker>, Id<UserMarker>), CachedPresence>> {
self.presences.get(&(guild_id, user_id)).map(Reference::new)
}
pub fn role(
&self,
role_id: Id<RoleMarker>,
) -> Option<Reference<'_, Id<RoleMarker>, GuildResource<Role>>> {
self.roles.get(&role_id).map(Reference::new)
}
pub fn stage_instance(
&self,
stage_id: Id<StageMarker>,
) -> Option<Reference<'_, Id<StageMarker>, GuildResource<StageInstance>>> {
self.stage_instances.get(&stage_id).map(Reference::new)
}
pub fn sticker(
&self,
sticker_id: Id<StickerMarker>,
) -> Option<Reference<'_, Id<StickerMarker>, GuildResource<CachedSticker>>> {
self.stickers.get(&sticker_id).map(Reference::new)
}
pub fn user(&self, user_id: Id<UserMarker>) -> Option<Reference<'_, Id<UserMarker>, User>> {
self.users.get(&user_id).map(Reference::new)
}
pub fn voice_channel_states(
&self,
channel_id: Id<ChannelMarker>,
) -> Option<VoiceChannelStates<'_>> {
let user_ids = self.voice_state_channels.get(&channel_id)?;
Some(VoiceChannelStates {
index: 0,
user_ids,
voice_states: &self.voice_states,
})
}
#[allow(clippy::type_complexity)]
pub fn voice_state(
&self,
user_id: Id<UserMarker>,
guild_id: Id<GuildMarker>,
) -> Option<Reference<'_, (Id<GuildMarker>, Id<UserMarker>), CachedVoiceState>> {
self.voice_states
.get(&(guild_id, user_id))
.map(Reference::new)
}
pub fn member_highest_role(
&self,
guild_id: Id<GuildMarker>,
user_id: Id<UserMarker>,
) -> Option<Id<RoleMarker>> {
let member = self.members.get(&(guild_id, user_id))?;
let mut highest_role: Option<(i64, Id<RoleMarker>)> = None;
for role_id in &member.roles {
if let Some(role) = self.role(*role_id) {
if let Some((position, id)) = highest_role {
if role.position < position || (role.position == position && role.id > id) {
continue;
}
}
highest_role = Some((role.position, role.id));
}
}
highest_role.map(|(_, id)| id)
}
fn new_with_config(config: Config) -> Self {
Self {
config,
..InMemoryCache::default()
}
}
const fn wants(&self, resource_type: ResourceType) -> bool {
self.config.resource_types().contains(resource_type)
}
}
mod private {
use twilight_model::gateway::{
event::Event,
payload::incoming::{
ChannelCreate, ChannelDelete, ChannelPinsUpdate, ChannelUpdate, GuildCreate,
GuildDelete, GuildEmojisUpdate, GuildStickersUpdate, GuildUpdate, IntegrationCreate,
IntegrationDelete, IntegrationUpdate, InteractionCreate, MemberAdd, MemberChunk,
MemberRemove, MemberUpdate, MessageCreate, MessageDelete, MessageDeleteBulk,
MessageUpdate, PresenceUpdate, ReactionAdd, ReactionRemove, ReactionRemoveAll,
ReactionRemoveEmoji, Ready, RoleCreate, RoleDelete, RoleUpdate, StageInstanceCreate,
StageInstanceDelete, StageInstanceUpdate, ThreadCreate, ThreadDelete, ThreadListSync,
ThreadUpdate, UnavailableGuild, UserUpdate, VoiceStateUpdate,
},
};
pub trait Sealed {}
impl Sealed for Event {}
impl Sealed for ChannelCreate {}
impl Sealed for ChannelDelete {}
impl Sealed for ChannelPinsUpdate {}
impl Sealed for ChannelUpdate {}
impl Sealed for GuildCreate {}
impl Sealed for GuildEmojisUpdate {}
impl Sealed for GuildDelete {}
impl Sealed for GuildStickersUpdate {}
impl Sealed for GuildUpdate {}
impl Sealed for IntegrationCreate {}
impl Sealed for IntegrationDelete {}
impl Sealed for IntegrationUpdate {}
impl Sealed for InteractionCreate {}
impl Sealed for MemberAdd {}
impl Sealed for MemberChunk {}
impl Sealed for MemberRemove {}
impl Sealed for MemberUpdate {}
impl Sealed for MessageCreate {}
impl Sealed for MessageDelete {}
impl Sealed for MessageDeleteBulk {}
impl Sealed for MessageUpdate {}
impl Sealed for PresenceUpdate {}
impl Sealed for ReactionAdd {}
impl Sealed for ReactionRemove {}
impl Sealed for ReactionRemoveAll {}
impl Sealed for ReactionRemoveEmoji {}
impl Sealed for Ready {}
impl Sealed for RoleCreate {}
impl Sealed for RoleDelete {}
impl Sealed for RoleUpdate {}
impl Sealed for StageInstanceCreate {}
impl Sealed for StageInstanceDelete {}
impl Sealed for StageInstanceUpdate {}
impl Sealed for ThreadCreate {}
impl Sealed for ThreadDelete {}
impl Sealed for ThreadListSync {}
impl Sealed for ThreadUpdate {}
impl Sealed for UnavailableGuild {}
impl Sealed for UserUpdate {}
impl Sealed for VoiceStateUpdate {}
}
pub trait UpdateCache: private::Sealed {
#[allow(unused_variables)]
fn update(&self, cache: &InMemoryCache) {}
}
pub struct VoiceChannelStates<'a> {
index: usize,
#[allow(clippy::type_complexity)]
user_ids: Ref<'a, Id<ChannelMarker>, HashSet<(Id<GuildMarker>, Id<UserMarker>)>>,
voice_states: &'a DashMap<(Id<GuildMarker>, Id<UserMarker>), CachedVoiceState>,
}
impl<'a> Iterator for VoiceChannelStates<'a> {
type Item = Reference<'a, (Id<GuildMarker>, Id<UserMarker>), CachedVoiceState>;
fn next(&mut self) -> Option<Self::Item> {
while let Some((guild_id, user_id)) = self.user_ids.iter().nth(self.index) {
if let Some(voice_state) = self.voice_states.get(&(*guild_id, *user_id)) {
self.index += 1;
return Some(Reference::new(voice_state));
}
}
None
}
}
impl UpdateCache for Event {
#[allow(clippy::cognitive_complexity, clippy::explicit_deref_methods)]
fn update(&self, c: &InMemoryCache) {
match self {
Event::ChannelCreate(v) => c.update(v.deref()),
Event::ChannelDelete(v) => c.update(v.deref()),
Event::ChannelPinsUpdate(v) => c.update(v),
Event::ChannelUpdate(v) => c.update(v.deref()),
Event::GuildCreate(v) => c.update(v.deref()),
Event::GuildDelete(v) => c.update(v),
Event::GuildEmojisUpdate(v) => c.update(v),
Event::GuildStickersUpdate(v) => c.update(v),
Event::GuildUpdate(v) => c.update(v.deref()),
Event::IntegrationCreate(v) => c.update(v.deref()),
Event::IntegrationDelete(v) => c.update(v.deref()),
Event::IntegrationUpdate(v) => c.update(v.deref()),
Event::InteractionCreate(v) => c.update(v.deref()),
Event::MemberAdd(v) => c.update(v.deref()),
Event::MemberRemove(v) => c.update(v),
Event::MemberUpdate(v) => c.update(v.deref()),
Event::MemberChunk(v) => c.update(v),
Event::MessageCreate(v) => c.update(v.deref()),
Event::MessageDelete(v) => c.update(v),
Event::MessageDeleteBulk(v) => c.update(v),
Event::MessageUpdate(v) => c.update(v.deref()),
Event::PresenceUpdate(v) => c.update(v.deref()),
Event::ReactionAdd(v) => c.update(v.deref()),
Event::ReactionRemove(v) => c.update(v.deref()),
Event::ReactionRemoveAll(v) => c.update(v),
Event::ReactionRemoveEmoji(v) => c.update(v),
Event::Ready(v) => c.update(v.deref()),
Event::RoleCreate(v) => c.update(v),
Event::RoleDelete(v) => c.update(v),
Event::RoleUpdate(v) => c.update(v),
Event::StageInstanceCreate(v) => c.update(v),
Event::StageInstanceDelete(v) => c.update(v),
Event::StageInstanceUpdate(v) => c.update(v),
Event::ThreadCreate(v) => c.update(v.deref()),
Event::ThreadUpdate(v) => c.update(v.deref()),
Event::ThreadDelete(v) => c.update(v),
Event::ThreadListSync(v) => c.update(v),
Event::UnavailableGuild(v) => c.update(v),
Event::UserUpdate(v) => c.update(v),
Event::VoiceStateUpdate(v) => c.update(v.deref()),
Event::AutoModerationActionExecution(_)
| Event::AutoModerationRuleCreate(_)
| Event::AutoModerationRuleDelete(_)
| Event::AutoModerationRuleUpdate(_)
| Event::BanAdd(_)
| Event::BanRemove(_)
| Event::CommandPermissionsUpdate(_)
| Event::GatewayClose(_)
| Event::GatewayHeartbeat(_)
| Event::GatewayHeartbeatAck
| Event::GatewayHello(_)
| Event::GatewayInvalidateSession(_)
| Event::GatewayReconnect
| Event::GiftCodeUpdate
| Event::GuildIntegrationsUpdate(_)
| Event::GuildScheduledEventCreate(_)
| Event::GuildScheduledEventDelete(_)
| Event::GuildScheduledEventUpdate(_)
| Event::GuildScheduledEventUserAdd(_)
| Event::GuildScheduledEventUserRemove(_)
| Event::InviteCreate(_)
| Event::InviteDelete(_)
| Event::PresencesReplace
| Event::Resumed
| Event::ThreadMembersUpdate(_)
| Event::ThreadMemberUpdate(_)
| Event::TypingStart(_)
| Event::VoiceServerUpdate(_)
| Event::WebhooksUpdate(_) => {}
}
}
}
#[cfg(test)]
mod tests {
use crate::{test, InMemoryCache};
use twilight_model::{
gateway::payload::incoming::RoleDelete,
guild::{Member, Permissions, Role},
id::Id,
util::Timestamp,
};
#[test]
fn syntax_update() {
let cache = InMemoryCache::new();
cache.update(&RoleDelete {
guild_id: Id::new(1),
role_id: Id::new(1),
});
}
#[test]
fn clear() {
let cache = InMemoryCache::new();
cache.cache_emoji(Id::new(1), test::emoji(Id::new(3), None));
cache.cache_member(Id::new(2), test::member(Id::new(4), Id::new(2)));
cache.clear();
assert!(cache.emojis.is_empty());
assert!(cache.members.is_empty());
}
#[test]
fn highest_role() {
let joined_at = Timestamp::from_secs(1_632_072_645).expect("non zero");
let cache = InMemoryCache::new();
let guild_id = Id::new(1);
let user = test::user(Id::new(1));
cache.cache_member(
guild_id,
Member {
avatar: None,
communication_disabled_until: None,
deaf: false,
guild_id,
joined_at,
mute: false,
nick: None,
pending: false,
premium_since: None,
roles: vec![Id::new(1), Id::new(2)],
user,
},
);
cache.cache_roles(
guild_id,
vec![
Role {
color: 0,
hoist: false,
icon: None,
id: Id::new(1),
managed: false,
mentionable: false,
name: "test".to_owned(),
permissions: Permissions::empty(),
position: 0,
tags: None,
unicode_emoji: None,
},
Role {
color: 0,
hoist: false,
icon: None,
id: Id::new(2),
managed: false,
mentionable: false,
name: "test".to_owned(),
permissions: Permissions::empty(),
position: 1,
tags: None,
unicode_emoji: None,
},
],
);
assert_eq!(
cache.member_highest_role(guild_id, Id::new(1)),
Some(Id::new(2))
);
}
}