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.
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]
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.