diff --git a/lib/nostrum/constants.ex b/lib/nostrum/constants.ex index 851ddcba6..040403a83 100644 --- a/lib/nostrum/constants.ex +++ b/lib/nostrum/constants.ex @@ -148,6 +148,10 @@ defmodule Nostrum.Constants do def cdn_icon(id, icon, image_format), do: "/icons/#{id}/#{icon}.#{image_format}" def cdn_splash(id, splash, image_format), do: "/splashes/#{id}/#{splash}.#{image_format}" + def cdn_guild_avatar(guild_id, user_id, avatar_hash, image_format) do + "/guilds/#{guild_id}/users/#{user_id}/avatars/#{avatar_hash}.#{image_format}" + end + def thread_with_message(channel_id, message_id), do: "/channels/#{channel_id}/messages/#{message_id}/threads" diff --git a/lib/nostrum/struct/guild/member.ex b/lib/nostrum/struct/guild/member.ex index ac30fe399..e5fa39831 100644 --- a/lib/nostrum/struct/guild/member.ex +++ b/lib/nostrum/struct/guild/member.ex @@ -21,21 +21,23 @@ defmodule Nostrum.Struct.Guild.Member do ``` """ - alias Nostrum.Permission alias Nostrum.Struct.{Channel, Guild, User} alias Nostrum.Struct.Guild.Role - alias Nostrum.{Snowflake, Util} + alias Nostrum.{Constants, Permission, Snowflake, Util} import Bitwise defstruct [ - :user_id, - :nick, - :roles, - :joined_at, + :avatar, + :communication_disabled_until, :deaf, + :flags, + :joined_at, :mute, - :communication_disabled_until, - :premium_since + :nick, + :pending, + :premium_since, + :roles, + :user_id ] defimpl String.Chars do @@ -93,15 +95,40 @@ defmodule Nostrum.Struct.Guild.Member do """ @type premium_since :: DateTime.t() | nil + @typedoc """ + Avatar hash of the custom avatar set by the user in the guild. + + If animated, this is prefixed with `a_`. + + You can use `avatar_url/3` to fetch a full-formed URL of this asset. + """ + @type avatar :: String.t() | nil + + @typedoc """ + Current guild member gate status. `false` if user has yet to pass the membership screening + configuration for the guild, `true` if the member has passed. + """ + @type pending :: boolean | nil + + @typedoc """ + Guild member flags represented as a bitset. + + Look at the `Nostrum.Struct.Guild.Member.Flags` module for guidance parsing this value. + """ + @type flags :: pos_integer() | nil + @type t :: %__MODULE__{ - user_id: user_id, - nick: nick, - roles: roles, - joined_at: joined_at, + avatar: avatar, + communication_disabled_until: communication_disabled_until, deaf: deaf, + flags: flags, + joined_at: joined_at, mute: mute, - communication_disabled_until: communication_disabled_until, - premium_since: premium_since + nick: nick, + pending: pending, + premium_since: premium_since, + roles: roles, + user_id: user_id } @doc ~S""" @@ -120,6 +147,43 @@ defmodule Nostrum.Struct.Guild.Member do "<@#{user_id}>" end + @doc ~S""" + Returns a guild-specific avatar URL for a `Nostrum.Struct.Guild.Member`. + + Supported formats are `png` (default), `jpg`, `webp` and `gif`. + + As mentioned in the avatar hash typedoc, if the avatar hash begins with `a_`, the + avatar is animated and can be returned as a gif. + + ## Examples + + ```elixir + iex> member = %Nostrum.Struct.Guild.Member{ + ...> user_id: 165023948638126080, + ...> avatar: "4c8319db8ea745275a1399f8f8aa74ab" + ...> } + iex> guild_id = 1226944827137069107 + iex> Nostrum.Struct.Guild.Member.avatar_url(member, guild_id) + "https://cdn.discordapp.com/guilds/1226944827137069107/users/165023948638126080/avatars/4c8319db8ea745275a1399f8f8aa74ab.png" + ``` + """ + @spec avatar_url(t, Nostrum.Struct.Guild.id()) :: String.t() | nil + def avatar_url(member, guild_id, image_format \\ "png") + + def avatar_url(%{avatar: nil}, _, _) do + nil + end + + def avatar_url(%{user_id: user_id, avatar: avatar}, guild_id, image_format) do + Constants.cdn_url() <> + Constants.cdn_guild_avatar( + guild_id, + user_id, + avatar, + image_format + ) + end + @doc """ Returns a member's guild permissions. diff --git a/lib/nostrum/struct/guild/member/flags.ex b/lib/nostrum/struct/guild/member/flags.ex new file mode 100644 index 000000000..3f32b8b30 --- /dev/null +++ b/lib/nostrum/struct/guild/member/flags.ex @@ -0,0 +1,104 @@ +defmodule Nostrum.Struct.Guild.Member.Flags do + @moduledoc """ + Struct representing the flags a guild member can have. + """ + + import Bitwise + + defstruct did_rejoin: false, + completed_onboarding: false, + bypasses_verification: false, + started_onboarding: false + + @typedoc """ + Member has left and rejoined the guild + """ + @type did_rejoin :: boolean + + @typedoc """ + Member has completed onboarding + """ + @type completed_onboarding :: boolean + + @typedoc """ + Member is exempt from guild verification requirements + """ + @type bypasses_verification :: boolean + + @typedoc """ + Member has started onboarding + """ + @type started_onboarding :: boolean + + @type flags :: %__MODULE__{ + did_rejoin: did_rejoin, + completed_onboarding: completed_onboarding, + bypasses_verification: bypasses_verification, + started_onboarding: started_onboarding + } + + @type t :: flags + + @flag_values [ + did_rejoin: 1 <<< 0, + completed_onboarding: 1 <<< 1, + bypasses_verification: 1 <<< 2, + started_onboarding: 1 <<< 3 + ] + + @doc """ + Constructs a flag struct based on an integer from the Discord API, normally from `t:Nostrum.Struct.Guild.Member.flags/0`. + + ## Examples + + ```elixir + iex> Nostrum.Struct.Guild.Member.Flags.from_integer(9) + %Nostrum.Struct.Guild.Member.Flags{ + did_rejoin: true, + completed_onboarding: false, + bypasses_verification: false, + started_onboarding: true + } + ``` + """ + @spec from_integer(integer()) :: t + def from_integer(flag_value) do + boolean_list = + Enum.map(@flag_values, fn {flag, value} -> + {flag, (flag_value &&& value) == value} + end) + + struct(__MODULE__, boolean_list) + end + + @doc """ + Convert a flag struct to an integer value. + + ## Examples + + ```elixir + iex> my_flags = %Nostrum.Struct.Guild.Member.Flags{ + ...> did_rejoin: true, + ...> completed_onboarding: false, + ...> bypasses_verification: false, + ...> started_onboarding: true + ...> } + iex> Nostrum.Struct.Guild.Member.Flags.to_integer(my_flags) + 9 + ``` + """ + @spec to_integer(t) :: integer() + def to_integer(flag_struct) do + booleans = + flag_struct + |> Map.from_struct() + |> Map.to_list() + + Enum.reduce(booleans, 0, fn {flag, enabled}, flag_value -> + case enabled do + true -> flag_value ||| @flag_values[flag] + false -> flag_value + end + end) + end +end diff --git a/test/nostrum/struct/guild/member/flags_test.exs b/test/nostrum/struct/guild/member/flags_test.exs new file mode 100644 index 000000000..48dde2dd4 --- /dev/null +++ b/test/nostrum/struct/guild/member/flags_test.exs @@ -0,0 +1,7 @@ +defmodule Nostrum.Struct.Guild.Member.FlagsTest do + use ExUnit.Case, async: true + + alias Nostrum.Struct.Guild.Member.Flags + + doctest Flags +end