lib/nostrum/struct/guild/member.ex

defmodule Nostrum.Struct.Guild.Member do
  @moduledoc ~S"""
  Struct representing a Discord guild member.

  A `Nostrum.Struct.Guild.Member` stores a `Nostrum.Struct.User`'s properties
  pertaining to a specific `Nostrum.Struct.Guild`.

  ## Mentioning Members in Messages

  A `Nostrum.Struct.Guild.Member` can be mentioned in message content using the `String.Chars`
  protocol or `mention/1`.

  ```Elixir
  member = %Nostrum.Struct.Guild.Member{user: Nostrum.Struct.User{id: 120571255635181568}}
  Nostrum.Api.create_message!(184046599834435585, "#{member}")
  %Nostrum.Struct.Message{content: "<@120571255635181568>"}

  member = %Nostrum.Struct.Guild.Member{user: Nostrum.Struct.User{id: 89918932789497856}}
  Nostrum.Api.create_message!(280085880452939778, "#{Nostrum.Struct.Guild.Member.mention(member)}")
  %Nostrum.Struct.Message{content: "<@89918932789497856>"}
  ```
  """

  alias Nostrum.Permission
  alias Nostrum.Struct.{Channel, Guild, User}
  alias Nostrum.Struct.Guild.Role
  alias Nostrum.{Snowflake, Util}

  defstruct [
    :user,
    :nick,
    :roles,
    :joined_at,
    :deaf,
    :mute,
    :communication_disabled_until,
    :premium_since
  ]

  defimpl String.Chars do
    def to_string(member), do: @for.mention(member)
  end

  @typedoc """
  The user struct. This field can be `nil` if the Member struct came as a partial Member object included
  in a message received from a guild channel.
  """
  @type user :: User.t() | nil

  @typedoc "The nickname of the user"
  @type nick :: String.t() | nil

  @typedoc "A list of role ids"
  @type roles :: [Role.id()]

  @typedoc """
  Date the user joined the guild.
  If you dont request offline guild members this field will be `nil` for any members that come online.
  """
  @type joined_at :: String.t() | nil

  @typedoc """
  Whether the user is deafened.
  If you dont request offline guild members this field will be `nil` for any members that come online.
  """
  @type deaf :: boolean | nil

  @typedoc """
  Whether the user is muted.
  If you dont request offline guild members this field will be `nil` for any members that come online.
  """
  @type mute :: boolean | nil

  @typedoc """
  Current timeout status of the user. If user is currently timed out this will be a `t:DateTime.t/0` of the unmute time, it will be `nil` or a date in the past if the user is not currently timed out.
  """
  @type communication_disabled_until :: DateTime.t() | nil

  @typedoc """
  Current guild booster status of the user. If user is currently boosting a guild this will be a `t:DateTime.t/0` since the start of the boosting, it will be `nil` if the user is not currently boosting the guild.
  """
  @type premium_since :: DateTime.t() | nil

  @type t :: %__MODULE__{
          user: user,
          nick: nick,
          roles: roles,
          joined_at: joined_at,
          deaf: deaf,
          mute: mute,
          communication_disabled_until: communication_disabled_until,
          premium_since: premium_since
        }

  @doc ~S"""
  Formats a `Nostrum.Struct.Guild.Member` into a mention.

  ## Examples

  ```Elixir
  iex> member = %Nostrum.Struct.Guild.Member{user: %Nostrum.Struct.User{id: 177888205536886784}}
  ...> Nostrum.Struct.Guild.Member.mention(member)
  "<@177888205536886784>"
  ```
  """
  @spec mention(t) :: String.t()
  def mention(%__MODULE__{user: user}), do: User.mention(user)

  @doc """
  Returns a member's guild permissions.

  ## Examples

  ```Elixir
  guild = Nostrum.Cache.GuildCache.get!(279093381723062272)
  member = Map.get(guild.members, 177888205536886784)
  Nostrum.Struct.Guild.Member.guild_permissions(member, guild)
  #=> [:administrator]
  ```
  """
  @spec guild_permissions(t, Guild.t()) :: [Permission.t()]
  def guild_permissions(member, guild)

  def guild_permissions(%__MODULE__{user: %{id: user_id}}, %Guild{owner_id: owner_id})
      when user_id === owner_id,
      do: Permission.all()

  def guild_permissions(%__MODULE__{} = member, %Guild{} = guild) do
    use Bitwise

    everyone_role_id = guild.id
    member_role_ids = member.roles ++ [everyone_role_id]

    member_permissions =
      member_role_ids
      |> Enum.map(&Map.get(guild.roles, &1))
      |> Enum.filter(&(!match?(nil, &1)))
      |> Enum.reduce(0, fn role, bitset_acc ->
        bitset_acc ||| role.permissions
      end)
      |> Permission.from_bitset()

    if Enum.member?(member_permissions, :administrator) do
      Permission.all()
    else
      member_permissions
    end
  end

  @doc """
  Returns a member's permissions in a guild channel, based on its `Nostrum.Struct.Overwrite`s.

  ## Examples

  ```Elixir
  guild = Nostrum.Cache.GuildCache.get!(279093381723062272)
  member = Map.get(guild.members, 177888205536886784)
  channel_id = 381889573426429952
  Nostrum.Struct.Guild.Member.guild_channel_permissions(member, guild, channel_id)
  #=> [:manage_messages]
  ```
  """
  @spec guild_channel_permissions(t, Guild.t(), Channel.id()) :: [Permission.t()]
  def guild_channel_permissions(%__MODULE__{} = member, guild, channel_id) do
    use Bitwise

    guild_perms = guild_permissions(member, guild)

    if Enum.member?(guild_perms, :administrator) do
      Permission.all()
    else
      channel = Map.get(guild.channels, channel_id)

      everyone_role_id = guild.id
      role_ids = [everyone_role_id | member.roles]
      overwrite_ids = role_ids ++ [member.user.id]

      {allow, deny} =
        channel.permission_overwrites
        |> Enum.filter(&(&1.id in overwrite_ids))
        |> Enum.map(fn overwrite -> {overwrite.allow, overwrite.deny} end)
        |> Enum.reduce({0, 0}, fn {allow, deny}, {allow_acc, deny_acc} ->
          {allow_acc ||| allow, deny_acc ||| deny}
        end)

      allow_perms = allow |> Permission.from_bitset()
      deny_perms = deny |> Permission.from_bitset()

      guild_perms
      |> Enum.reject(&(&1 in deny_perms))
      |> Enum.concat(allow_perms)
      |> Enum.dedup()
    end
  end

  @doc """
  Return the topmost role of the given member on the given guild.

  The topmost role is determined via `t:Nostrum.Struct.Guild.Role.position`.

  ## Parameters

  - `member`: The member whose top role to return.
  - `guild`: The guild which the member belongs to.

  ## Return value

  The topmost role of the member on the given guild, if the member has roles
  assigned. Otherwise, `nil` is returned.
  """
  @doc since: "0.5.0"
  @spec top_role(__MODULE__.t(), Guild.t()) :: Role.t() | nil
  def top_role(%__MODULE__{roles: member_roles}, %Guild{roles: guild_roles}) do
    guild_roles
    |> Stream.filter(fn {id, _role} -> id in member_roles end)
    |> Stream.map(fn {_id, role} -> role end)
    |> Enum.max_by(& &1.position, fn -> nil end)
  end

  @doc false
  def p_encode do
    %__MODULE__{
      user: User.p_encode()
    }
  end

  @doc false
  def to_struct(map) do
    new =
      map
      |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end)
      |> Map.update(:user, nil, &Util.cast(&1, {:struct, User}))
      |> Map.update(:roles, nil, &Util.cast(&1, {:list, Snowflake}))
      |> Map.update(:communication_disabled_until, nil, &Util.maybe_to_datetime/1)
      |> Map.update(:premium_since, nil, &Util.maybe_to_datetime/1)

    struct(__MODULE__, new)
  end
end