EDA.Cache (EDA v0.4.0)

Copy Markdown View Source

Unified cache interface for EDA.

This module provides a simple API for accessing cached Discord data. Data is automatically cached as events are received from the Gateway.

Examples

# Get a guild
guild = EDA.Cache.get_guild("123456789")

# Get a user
user = EDA.Cache.get_user("987654321")

# Get a channel
channel = EDA.Cache.get_channel("111222333")

# Get the bot user
me = EDA.Cache.me()

Obfuscated channels

From 2026-11-16 Discord redacts channels the bot cannot view rather than hiding them: they are still dispatched over the gateway, with name set to "___hidden___", sensitive fields nulled, and the CHANNEL_OBFUSCATED flag set. Before that date the behaviour is opt-in via config :eda, capabilities: [:channel_obfuscation].

EDA caches them like any other channel. They are dropped neither on write nor on read, because "this channel exists and I cannot see it" is real information — Discord explicitly expects apps that manage channels or compute permissions across a guild to detect them and surface that state. Hiding them in the cache would make a channel that demonstrably exists look absent.

Redaction nulls the sensitive fields rather than omitting them, which matters here: EDA.Cache.Channel.update/2 merges the incoming payload over the cached one, so a channel that becomes obfuscated has its name, topic, status and last_message_id genuinely replaced — the previously cached values do not survive. Verified against a live guild on 2026-09-19. Redaction is also selective: position, parent_id, nsfw, bitrate and rate_limit_per_user keep their real values.

The consequences are therefore yours to handle:

  • channels/0 and channels_for_guild/1 include them — reject with EDA.Channel.obfuscated?/1 before showing a channel list to a user;

  • their permission_overwrites hold a single synthetic @everyone VIEW_CHANNEL deny. EDA.Permission.in_channel/3 already refuses to compute from it and returns {:error, :channel_obfuscated}, so that path is safe;

  • a cache admission policy can drop them if you would rather not see them at all:

    config :eda,
      cache: [
        channels: [policy: fn _entity, _key, channel ->
          if EDA.Channel.obfuscated?(channel), do: :skip, else: :cache
        end]
      ]

Summary

Functions

Returns the number of cached channels.

Gets all cached channels.

Gets all channels for a guild.

Fetches a channel from cache, falling back to REST on miss.

Fetches a guild from cache, falling back to REST on miss.

Fetches a member from cache, falling back to REST on miss.

Fetches a role from cache, falling back to REST on miss.

Fetches a user from cache, falling back to REST on miss.

Gets a channel by ID.

Gets a guild by ID.

Gets a member in a guild.

Gets a user's presence in a guild.

Gets a role by ID.

Gets a user by ID.

Gets a user's voice state in a guild.

Returns the number of cached guilds.

Gets all cached guilds.

Gets the current bot user as an %EDA.User{} struct.

Returns the raw map of the current bot user (string keys).

Returns the total number of cached members.

Gets all members for a guild.

Gets all presences for a guild.

Stores the current bot user. Saves both the parsed %EDA.User{} struct and the raw map for backward compatibility.

Returns the total number of cached roles.

Gets all roles for a guild.

Returns the number of cached users.

Gets all cached users.

Gets all voice states for a specific channel in a guild.

Gets all voice states for a guild.

Functions

channel_count()

@spec channel_count() :: non_neg_integer()

Returns the number of cached channels.

channels()

@spec channels() :: [map()]

Gets all cached channels.

Includes channels Discord has obfuscated; see channels_for_guild/1.

channels_for_guild(guild_id)

@spec channels_for_guild(String.t() | integer()) :: [map()]

Gets all channels for a guild.

Channels the bot cannot view are included, with their metadata redacted by Discord (name is "___hidden___"). Filter them out when listing channels for a user:

guild_id
|> EDA.Cache.channels_for_guild()
|> Enum.reject(&EDA.Channel.obfuscated?/1)

See the "Obfuscated channels" section of EDA.Cache for why they are kept.

fetch_channel(channel_id)

@spec fetch_channel(String.t() | integer()) :: {:ok, map()} | {:error, term()}

Fetches a channel from cache, falling back to REST on miss.

fetch_guild(guild_id)

@spec fetch_guild(String.t() | integer()) :: {:ok, map()} | {:error, term()}

Fetches a guild from cache, falling back to REST on miss.

fetch_member(guild_id, user_id)

@spec fetch_member(String.t() | integer(), String.t() | integer()) ::
  {:ok, map()} | {:error, term()}

Fetches a member from cache, falling back to REST on miss.

fetch_role(guild_id, role_id)

@spec fetch_role(String.t() | integer(), String.t() | integer()) ::
  {:ok, map()} | {:error, term()}

Fetches a role from cache, falling back to REST on miss.

Note: the REST API returns all roles for the guild, so guild_id is required.

fetch_user(user_id)

@spec fetch_user(String.t() | integer()) :: {:ok, map()} | {:error, term()}

Fetches a user from cache, falling back to REST on miss.

get_channel(channel_id)

@spec get_channel(String.t() | integer()) :: map() | nil

Gets a channel by ID.

get_guild(guild_id)

@spec get_guild(String.t() | integer()) :: map() | nil

Gets a guild by ID.

get_member(guild_id, user_id)

@spec get_member(String.t() | integer(), String.t() | integer()) :: map() | nil

Gets a member in a guild.

get_presence(guild_id, user_id)

@spec get_presence(String.t() | integer(), String.t() | integer()) :: map() | nil

Gets a user's presence in a guild.

get_role(role_id)

@spec get_role(String.t() | integer()) :: map() | nil

Gets a role by ID.

get_user(user_id)

@spec get_user(String.t() | integer()) :: map() | nil

Gets a user by ID.

get_voice_state(guild_id, user_id)

@spec get_voice_state(String.t() | integer(), String.t() | integer()) :: map() | nil

Gets a user's voice state in a guild.

guild_count()

@spec guild_count() :: non_neg_integer()

Returns the number of cached guilds.

guilds()

@spec guilds() :: [map()]

Gets all cached guilds.

me()

@spec me() :: EDA.User.t() | map() | nil

Gets the current bot user as an %EDA.User{} struct.

Returns nil if the bot hasn't connected yet. The raw map is also stored for internal callers that need string-key access.

me_raw()

@spec me_raw() :: map() | nil

Returns the raw map of the current bot user (string keys).

Used internally by code that expects me["id"] string-key access (e.g., app_id/0, voice event routing).

member_count()

@spec member_count() :: non_neg_integer()

Returns the total number of cached members.

members(guild_id)

@spec members(String.t() | integer()) :: [map()]

Gets all members for a guild.

presences(guild_id)

@spec presences(String.t() | integer()) :: [map()]

Gets all presences for a guild.

put_me(user)

@spec put_me(map()) :: :ok

Stores the current bot user. Saves both the parsed %EDA.User{} struct and the raw map for backward compatibility.

role_count()

@spec role_count() :: non_neg_integer()

Returns the total number of cached roles.

roles(guild_id)

@spec roles(String.t() | integer()) :: [map()]

Gets all roles for a guild.

user_count()

@spec user_count() :: non_neg_integer()

Returns the number of cached users.

users()

@spec users() :: [map()]

Gets all cached users.

voice_channel_members(guild_id, channel_id)

@spec voice_channel_members(String.t() | integer(), String.t() | integer()) :: [map()]

Gets all voice states for a specific channel in a guild.

voice_states(guild_id)

@spec voice_states(String.t() | integer()) :: [map()]

Gets all voice states for a guild.