Discord permission flags and calculator.
Computes effective permissions for a member at guild or channel level, following Discord's official algorithm with the 3-tier overwrite cascade.
Features
- Correct 3-tier overwrite cascade: @everyone → roles (merged) → member
- Access gates: returns
0if VIEW_CHANNEL is missing, or if VOICE_CONNECT is missing on voice/stage channels has_permission?/3: one-call convenience for permission checks- Pure bitwise hot path: no atom-list conversion during calculation
- All 50+ Discord permissions up to date (bit 52)
- Nil-safe: returns
{:error, reason}instead of crashing on missing data
Usage
# Check if a member can manage messages in a channel
EDA.Permission.has_permission?(guild_id, user_id, channel_id, :manage_messages)
# Get all effective permissions in a channel
{:ok, bitset} = EDA.Permission.in_channel(guild_id, user_id, channel_id)
perms = EDA.Permission.to_list(bitset)
# Guild-level permissions
{:ok, bitset} = EDA.Permission.in_guild(guild_id, user_id)
Summary
Types
A channel category a permission can apply to.
A full permission derivation, as returned by explain/3.
A step in a permission derivation.
Functions
Returns the bitset with all permissions set.
Returns all known permission flag atoms.
Returns true if the permission applies to the given channel.
Returns true for a permission that can meaningfully appear in a channel overwrite.
The channel kinds a permission applies to.
Explains how a member's channel permissions were derived.
Computes effective guild-level permissions for a member already in hand: an EDA.Member, or a
raw member map such as the one an interaction carries.
Returns the flag atom for a bit value, or :error.
Returns true for a permission that only has meaning at guild level.
Checks if a specific flag is set in a bitset.
Checks if a member has a specific permission at guild level.
Checks if a member has a specific permission in a channel.
Computes effective channel-level permissions for a member.
Computes effective guild-level permissions for a member.
Lists the permissions in a bitset that have no effect in the given channel.
Returns the bit value for a permission flag.
Converts a list of flag atoms to a combined bitset.
Converts a bitset to a list of flag atoms. Unknown bits are skipped.
Types
@type bitset() :: non_neg_integer()
@type channel_kind() :: :text | :voice | :stage
A channel category a permission can apply to.
@type explanation() :: %{ effective: bitset(), base: bitset(), steps: [step()], gates: [atom()], denied_by: atom() | nil }
A full permission derivation, as returned by explain/3.
@type flag() ::
:create_instant_invite
| :kick_members
| :ban_members
| :administrator
| :manage_channels
| :manage_guild
| :add_reactions
| :view_audit_log
| :priority_speaker
| :stream
| :view_channel
| :send_messages
| :send_tts_messages
| :manage_messages
| :embed_links
| :attach_files
| :read_message_history
| :mention_everyone
| :use_external_emojis
| :view_guild_insights
| :connect
| :speak
| :mute_members
| :deafen_members
| :move_members
| :use_vad
| :change_nickname
| :manage_nicknames
| :manage_roles
| :manage_webhooks
| :manage_guild_expressions
| :use_application_commands
| :request_to_speak
| :manage_events
| :manage_threads
| :create_public_threads
| :create_private_threads
| :use_external_stickers
| :send_messages_in_threads
| :use_embedded_activities
| :moderate_members
| :view_creator_monetization_analytics
| :use_soundboard
| :create_guild_expressions
| :create_events
| :use_external_sounds
| :send_voice_messages
| :set_voice_channel_status
| :send_polls
| :use_external_apps
| :pin_messages
| :bypass_slowmode
@type step() :: %{ :stage => atom(), :result => bitset(), optional(:allow) => bitset(), optional(:deny) => bitset(), optional(:gate) => atom() }
A step in a permission derivation.
:stage is :owner, :administrator, :role_base, :everyone_overwrite,
:role_overwrites, :member_overwrite or :gate. Overwrite stages carry the
:allow and :deny bitsets that were applied; gate stages carry :gate.
:result is the running permission bitset after that step.
Functions
@spec all() :: bitset()
Returns the bitset with all permissions set.
@spec all_flags() :: [flag()]
Returns all known permission flag atoms.
@spec applies_to?(flag(), channel_kind() | integer() | map()) :: boolean()
Returns true if the permission applies to the given channel.
The second argument is a channel kind (:text, :voice, :stage), a raw Discord
channel type integer, or a channel struct or map. Categories accept every kind, since
their overwrites cascade to children of any type.
Examples
iex> EDA.Permission.applies_to?(:request_to_speak, :stage)
true
iex> EDA.Permission.applies_to?(:request_to_speak, :text)
false
iex> EDA.Permission.applies_to?(:kick_members, :text)
false
Returns true for a permission that can meaningfully appear in a channel overwrite.
Examples
iex> EDA.Permission.channel?(:send_messages)
true
iex> EDA.Permission.channel?(:administrator)
false
@spec channel_types(flag()) :: [channel_kind()]
The channel kinds a permission applies to.
An empty list means the permission is guild-level only — setting it in a channel overwrite has no effect.
Examples
iex> EDA.Permission.channel_types(:send_messages)
[:text, :voice, :stage]
iex> EDA.Permission.channel_types(:kick_members)
[]
iex> EDA.Permission.channel_types(:request_to_speak)
[:stage]
@spec explain(String.t(), String.t(), String.t()) :: {:ok, explanation()} | {:error, term()}
Explains how a member's channel permissions were derived.
in_channel/3 answers what a member may do; this answers why. Neither JDA nor
Nostrum exposes the derivation, and "why can't my bot post here" is usually answered
by guesswork against an opaque integer.
Returns the same :effective bitset as in_channel/3, plus:
:base— guild-level permissions from the member's roles, before overwrites;:steps— the derivation in order, each with the running:result. Overwrite steps carry the:allow/:denybitsets that were applied;:gates— which access gates fired (:timed_out,:no_view_channel,:no_connect);:denied_by— the gate that reduced the result to zero, ornil.
Owner and administrator short-circuit to every permission, and say so in a single step.
Examples
{:ok, why} = EDA.Permission.explain(guild_id, user_id, channel_id)
why.denied_by
#=> :no_view_channel
Enum.map(why.steps, & &1.stage)
#=> [:role_base, :everyone_overwrite, :role_overwrites, :member_overwrite, :gate]
# what the @everyone overwrite took away
why.steps
|> Enum.find(&(&1.stage == :everyone_overwrite))
|> Map.fetch!(:deny)
|> EDA.Permission.to_list()
#=> [:send_messages]
@spec for_member(EDA.Member.t() | map(), String.t() | integer()) :: {:ok, bitset()} | {:error, term()}
Computes effective guild-level permissions for a member already in hand: an EDA.Member, or a
raw member map such as the one an interaction carries.
Unlike in_guild/2, the member need not be cached — without the GUILD_MEMBERS intent, most
are not. Only the guild and its roles are read from the cache.
For the channel an interaction was used in, Discord already sends the member's permissions
there, overwrites included, as member.permissions.
Returns {:ok, bitset} or {:error, reason}.
{:ok, perms} = EDA.Permission.for_member(interaction.member, interaction.guild_id)
EDA.Permission.has?(perms, :ban_members)
Returns the flag atom for a bit value, or :error.
Returns true for a permission that only has meaning at guild level.
Examples
iex> EDA.Permission.guild_only?(:kick_members)
true
iex> EDA.Permission.guild_only?(:send_messages)
false
Checks if a specific flag is set in a bitset.
Checks if a member has a specific permission at guild level.
Checks if a member has a specific permission in a channel.
Convenience function — most common use case for bots.
Computes effective channel-level permissions for a member.
Returns {:ok, bitset} or {:error, reason}.
Algorithm (matches Discord's official spec + JDA)
- Owner → ALL_PERMISSIONS
- Compute guild base permissions
- ADMINISTRATOR → ALL_PERMISSIONS (skips all overwrites)
- Apply 3-tier overwrite cascade: a. @everyone role overwrite b. All role overwrites (merged via OR, then applied) c. Member-specific overwrite (highest priority)
- Access gate: no VIEW_CHANNEL → 0
- Access gate: voice/stage channel + no CONNECT → 0
Obfuscated channels
Returns {:error, :channel_obfuscated} for a channel Discord has redacted because
the bot cannot view it (see EDA.Channel.obfuscated?/1). Such a channel carries a
single synthetic overwrite denying VIEW_CHANNEL to @everyone, which is
indistinguishable from a real one — computing from it would return a confident but
meaningless answer, so the ambiguity is surfaced to the caller instead.
has_permission?/4 maps this to false, like any other error.
Computes effective guild-level permissions for a member.
Returns {:ok, bitset} or {:error, reason}.
Algorithm
- Guild owner → ALL_PERMISSIONS
- OR all role permission bits together
- If ADMINISTRATOR is set → ALL_PERMISSIONS
@spec inapplicable(bitset(), channel_kind() | integer() | map()) :: [flag()]
Lists the permissions in a bitset that have no effect in the given channel.
Use it to catch a meaningless overwrite before sending it — Discord accepts
KICK_MEMBERS in a channel overwrite and silently ignores it. Neither JDA nor
Nostrum offers this check.
Examples
iex> bitset = EDA.Permission.to_bitset([:send_messages, :kick_members])
iex> EDA.Permission.inapplicable(bitset, :text)
[:kick_members]
iex> EDA.Permission.inapplicable(EDA.Permission.to_bitset([:send_messages]), :text)
[]
Returns the bit value for a permission flag.
Converts a list of flag atoms to a combined bitset.
Converts a bitset to a list of flag atoms. Unknown bits are skipped.