EDA.Permission (EDA v0.4.0)

Copy Markdown View Source

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 0 if 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

bitset()

@type bitset() :: non_neg_integer()

channel_kind()

@type channel_kind() :: :text | :voice | :stage

A channel category a permission can apply to.

explanation()

@type explanation() :: %{
  effective: bitset(),
  base: bitset(),
  steps: [step()],
  gates: [atom()],
  denied_by: atom() | nil
}

A full permission derivation, as returned by explain/3.

flag()

@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

step()

@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

all()

@spec all() :: bitset()

Returns the bitset with all permissions set.

all_flags()

@spec all_flags() :: [flag()]

Returns all known permission flag atoms.

applies_to?(flag, kind)

@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

channel?(flag)

@spec channel?(flag()) :: boolean()

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

channel_types(flag)

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

explain(guild_id, user_id, channel_id)

@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/:deny bitsets 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, or nil.

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]

from_bit(bit)

@spec from_bit(bitset()) :: {:ok, flag()} | :error

Returns the flag atom for a bit value, or :error.

guild_only?(flag)

@spec guild_only?(flag()) :: boolean()

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

has?(bitset, flag)

@spec has?(bitset(), flag()) :: boolean()

Checks if a specific flag is set in a bitset.

has_guild_permission?(guild_id, user_id, permission)

@spec has_guild_permission?(String.t(), String.t(), flag()) :: boolean()

Checks if a member has a specific permission at guild level.

has_permission?(guild_id, user_id, channel_id, permission)

@spec has_permission?(String.t(), String.t(), String.t(), flag()) :: boolean()

Checks if a member has a specific permission in a channel.

Convenience function — most common use case for bots.

in_channel(guild_id, user_id, channel_id)

@spec in_channel(String.t(), String.t(), String.t()) ::
  {:ok, bitset()} | {:error, term()}

Computes effective channel-level permissions for a member.

Returns {:ok, bitset} or {:error, reason}.

Algorithm (matches Discord's official spec + JDA)

  1. Owner → ALL_PERMISSIONS
  2. Compute guild base permissions
  3. ADMINISTRATOR → ALL_PERMISSIONS (skips all overwrites)
  4. Apply 3-tier overwrite cascade: a. @everyone role overwrite b. All role overwrites (merged via OR, then applied) c. Member-specific overwrite (highest priority)
  5. Access gate: no VIEW_CHANNEL → 0
  6. 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.

in_guild(guild_id, user_id)

@spec in_guild(String.t(), String.t()) :: {:ok, bitset()} | {:error, term()}

Computes effective guild-level permissions for a member.

Returns {:ok, bitset} or {:error, reason}.

Algorithm

  1. Guild owner → ALL_PERMISSIONS
  2. OR all role permission bits together
  3. If ADMINISTRATOR is set → ALL_PERMISSIONS

inapplicable(bitset, channel)

@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)
[]

to_bit(flag)

@spec to_bit(flag()) :: bitset()

Returns the bit value for a permission flag.

to_bitset(flags)

@spec to_bitset([flag()]) :: bitset()

Converts a list of flag atoms to a combined bitset.

to_list(bitset)

@spec to_list(bitset()) :: [flag()]

Converts a bitset to a list of flag atoms. Unknown bits are skipped.