Current section

Files

Jump to
crux_structs lib structs permissions.ex
Raw

lib/structs/permissions.ex

defmodule Crux.Structs.Permissions do
@moduledoc """
Custom non discord api struct to help with working with permissions.
"""
use Bitwise
alias Crux.Structs
@permissions %{
create_instant_invite: 1 <<< 0,
kick_members: 1 <<< 1,
ban_members: 1 <<< 2,
administrator: 1 <<< 3,
manage_channels: 1 <<< 4,
manage_guild: 1 <<< 5,
add_reactions: 1 <<< 6,
view_audit_log: 1 <<< 7,
# 8
# 9
view_channel: 1 <<< 10,
send_messages: 1 <<< 11,
send_tts_message: 1 <<< 12,
manage_messages: 1 <<< 13,
embed_links: 1 <<< 14,
attach_files: 1 <<< 15,
read_message_history: 1 <<< 16,
mention_everyone: 1 <<< 17,
use_external_emojis: 1 <<< 18,
# 19
connect: 1 <<< 20,
speak: 1 <<< 21,
mute_members: 1 <<< 22,
deafen_members: 1 <<< 23,
move_members: 1 <<< 24,
use_vad: 1 <<< 25,
change_nickname: 1 <<< 26,
manage_nicknames: 1 <<< 27,
manage_roles: 1 <<< 28,
manage_webhooks: 1 <<< 29,
manage_emojis: 1 <<< 30
}
@doc """
Returns a map of permissions, keyed under their permission name with the bit value.
"""
@spec permissions() :: %{permission_name() => non_neg_integer()}
def permissions, do: @permissions
@permission_names @permissions |> Map.keys()
@doc """
Returns a list of permission keys.
"""
@spec permission_names() :: [permission_name()]
def permission_names, do: @permission_names
@permission_all @permissions |> Map.values() |> Enum.reduce(&|||/2)
@doc """
Returns the integer value of all permissions together.
"""
@spec permission_all :: pos_integer()
def permission_all, do: @permission_all
@typedoc """
Union type of all valid permission name atoms
"""
@type permission_name ::
:create_instant_invite
| :kick_members
| :ban_members
| :administrator
| :manage_channels
| :manage_guild
| :add_reactions
| :view_audit_log
| :view_channel
| :send_messages
| :send_tts_message
| :manage_messages
| :embed_links
| :attach_files
| :read_message_histroy
| :mention_everyone
| :use_external_emojis
| :connect
| :speak
| :mute_members
| :deafen_members
| :move_members
| :use_vad
| :change_nickname
| :manage_nicknames
| :manage_roles
| :manage_webhooks
| :manage_emojis
defstruct(bitfield: 0)
@typedoc """
Represents a `Crux.Struct.Permissions`.
"""
@type t :: %__MODULE__{
bitfield: non_neg_integer()
}
@typedoc """
All valid types which can be directly resolved into a permissions bitfield.
"""
@type permissions :: t() | [permission_name()] | non_neg_integer() | permission_name()
@doc ~S"""
Resolves a `t:permissions/0` into a bitfield representing the set permissions.
## Examples
```elixir
# A single bitflag
iex> 0x8
...> |> Crux.Structs.Permissions.resolve()
0x8
# A single permissions constant
iex> :administrator
...> |> Crux.Structs.Permissions.resolve()
0x8
# A list of bitflags
iex> [0x8, 0x4]
...> |> Crux.Structs.Permissions.resolve()
0xC
# A list of permissions constants
iex> [:administrator, :ban_members]
...> |> Crux.Structs.Permissions.resolve()
0xC
iex> [:manage_roles, 0x400, 0x800, :add_reactions]
...> |> Crux.Structs.Permissions.resolve()
0x10000C40
```
"""
@spec resolve(permissions :: permissions()) :: non_neg_integer()
def resolve(permissions)
def resolve(%__MODULE__{bitfield: bitfield}), do: bitfield
def resolve(permissions) when is_integer(permissions) and permissions >= 0 do
Enum.reduce(@permissions, 0, fn {_name, value}, acc ->
if (permissions &&& value) == value, do: acc ||| value, else: acc
end)
end
def resolve(permissions) when permissions in @permission_names do
Map.get(@permissions, permissions)
end
def resolve(permissions) when is_list(permissions) do
Enum.map(permissions, &resolve/1)
|> Enum.reduce(&|||/2)
end
def resolve(permissions) do
raise """
Expected a permission_name atom, a non negative integer, or a list of them.
Received:
#{inspect(permissions)}
"""
end
@doc """
Creates a new `Crux.Structs.Permissions` struct from a valid `t:permissions/0`.
"""
@spec new(permissions :: permissions()) :: t()
def new(permissions), do: %__MODULE__{bitfield: resolve(permissions)}
@doc """
Serializes permissions into a map keyed by `t:permission_name/0` with a boolean indicating whether the permission_name is set.
> The administrator flag implicitly grants all permissions.
"""
@spec to_map(permissions :: permissions()) :: %{permission_name() => boolean()}
def to_map(permissions) do
permissions = resolve(permissions)
Enum.reduce(@permissions, %{}, fn {name, val}, acc ->
acc |> Map.put(name, has(permissions, val))
end)
end
@doc ~S"""
Serializes permissions into a list of set `t:permission_name/0`s.
> The administrator flag implicitly grants all permissions.
## Examples
```elixir
iex> 0x30
...> |> Crux.Structs.Permissions.to_list()
[:manage_guild, :manage_channels]
```
"""
@spec to_list(permissions :: permissions()) :: [permission_name()]
def to_list(permissions) do
permissions = resolve(permissions)
Enum.reduce(@permissions, [], fn {name, val}, acc ->
if has(permissions, val), do: [name | acc], else: acc
end)
end
@doc ~S"""
Adds permissions to the base permissions.
## Examples
```elixir
iex> :administrator
...> |> Crux.Structs.Permissions.add(:manage_guild)
%Crux.Structs.Permissions{bitfield: 0x28}
```
"""
@spec add(base :: permissions(), to_add :: permissions()) :: t()
def add(base, to_add) do
to_add = to_add |> resolve()
base
|> resolve()
|> bor(to_add)
|> new()
end
@doc ~S"""
Removes permissions from the base permissions
## Examples
```elixir
iex> [0x8, 0x10, 0x20]
...> |> Crux.Structs.Permissions.remove([0x10, 0x20])
%Crux.Structs.Permissions{bitfield: 0x8}
```
"""
@spec remove(base :: permissions(), to_remove :: permissions()) :: t()
def remove(base, to_remove) do
to_remove = to_remove |> resolve() |> bnot()
base
|> resolve()
|> band(to_remove)
|> new()
end
@doc ~S"""
Check whether the second permissions are all present in the first.
## Examples
```elixir
# All permissions except administrator set, but administrator overrides
iex> Crux.Structs.Permissions.has(0x8, 0x7ff7fcf7)
true
iex> Crux.Structs.Permissions.has([:send_messages, :view_channel, :read_message_history], [:send_messages, :view_channel])
true
iex> Crux.Structs.Permissions.has(:administrator, 0x8)
true
iex> Crux.Structs.Permissions.has(0x8, :administrator)
true
```
"""
@spec has(permissions(), permissions()) :: boolean()
def has(current, want) do
current = resolve(current)
if (current &&& 0x8) == 0x8 do
true
else
want = resolve(want)
(current &&& want) == want
end
end
@doc """
Resolves permissions for a user in a guild, optionally including channel permission overwrites.
> Raises when the member is not cached.
> The administrator flag or being owner implicitly grants all permissions.
"""
@spec from(
member :: Structs.Member.t() | Structs.User.t() | Crux.Rest.snowflake(),
guild :: Structs.Guild.t(),
channel :: Structs.Channel.t() | nil
) :: t()
def from(member, guild, channel \\ nil)
def from(%Structs.Member{user: user_id}, guild, channel), do: from(user_id, guild, channel)
def from(%Structs.User{id: user_id}, guild, channel), do: from(user_id, guild, channel)
def from(user_id, %Structs.Guild{owner_id: owner_id, members: members} = guild, nil) do
cond do
user_id == owner_id ->
@permission_all
|> new()
Map.has_key?(members, user_id) ->
_from(user_id, guild, nil)
true ->
raise """
There is no member with the ID "#{user_id}" in the cache of the guild.
The member is uncached or not in the guild.
"""
end
end
def from(user_id, %Structs.Guild{} = guild, %Structs.Channel{} = channel) do
permissions = from(user_id, guild)
if has(permissions, :administrator) do
@permission_all
|> new()
else
_from(user_id, guild, channel, permissions)
end
end
defp _from(user_id, %{id: guild_id, members: members, roles: roles}, nil) do
member = Map.get(members, user_id)
permissions =
roles
|> Map.get(guild_id)
|> Map.get(:permissions)
permissions =
roles
|> Map.take(member.roles)
|> Map.values()
|> Enum.reduce(permissions, fn %{permissions: permissions}, acc ->
acc ||| permissions
end)
if has(permissions, :administrator) do
@permission_all
else
permissions
end
|> new()
end
defp _from(
user_id,
%Structs.Guild{id: guild_id, members: members},
%Structs.Channel{permission_overwrites: overwrites},
%__MODULE__{bitfield: permissions}
) do
permissions =
overwrites
|> Map.get(guild_id)
|> apply_overwrite(permissions)
role_ids = members |> Map.get(user_id) |> Map.get(:roles)
permissions =
overwrites
|> Map.take(role_ids)
|> Map.values()
|> Enum.reduce(%{allow: 0, deny: 0}, &acc_overwrite/2)
|> apply_overwrite(permissions)
overwrites
|> Map.get(user_id)
|> apply_overwrite(permissions)
|> new()
end
defp acc_overwrite(nil, acc), do: acc
defp acc_overwrite(%{allow: allow, deny: deny}, %{} = acc) do
acc
|> Map.update!(:allow, &(&1 ||| allow))
|> Map.update!(:deny, &(&1 ||| deny))
end
defp apply_overwrite(nil, permissions), do: permissions
defp apply_overwrite(%{allow: allow, deny: deny}, permissions) do
permissions = permissions &&& ~~~deny
permissions = permissions ||| allow
permissions
end
end