diff --git a/lib/nostrum/api.ex b/lib/nostrum/api.ex index 616f532f8..accf3f1bd 100644 --- a/lib/nostrum/api.ex +++ b/lib/nostrum/api.ex @@ -41,6 +41,8 @@ defmodule Nostrum.Api do ``` """ + @crlf "\r\n" + import Nostrum.Snowflake, only: [is_snowflake: 1] alias Nostrum.Api.Ratelimiter @@ -58,6 +60,7 @@ defmodule Nostrum.Api do Invite, Message, Message.Poll, + Sticker, ThreadMember, User, Webhook @@ -1416,6 +1419,150 @@ defmodule Nostrum.Api do |> bangify end + @doc ~S""" + Fetch a sticker with the provided ID. + + Returns a `t:Nostrum.Struct.Sticker.t/0`. + """ + @spec get_sticker(Snowflake.t()) :: {:ok, Sticker.t()} | error + def get_sticker(sticker_id) do + request(:get, Constants.sticker(sticker_id)) + |> handle_request_with_decode({:struct, Sticker}) + end + + @doc ~S""" + List all stickers in the provided guild. + + Returns a list of `t:Nostrum.Struct.Sticker.t/0`. + """ + @spec list_guild_stickers(Guild.id()) :: {:ok, [Sticker.t()]} | error + def list_guild_stickers(guild_id) do + request(:get, Constants.guild_stickers(guild_id)) + |> handle_request_with_decode({:list, {:struct, Sticker}}) + end + + @doc ~S""" + Return the specified sticker from the specified guild. + + Returns a `t:Nostrum.Struct.Sticker.t/0`. + """ + @spec get_guild_sticker(Guild.id(), Sticker.id()) :: Sticker.t() | error + def get_guild_sticker(guild_id, sticker_id) do + request(:get, Constants.guild_sticker(guild_id, sticker_id)) + |> handle_request_with_decode({:struct, Sticker}) + end + + @doc ~S""" + Create a sticker in a guild. + + Every guild has five free sticker slots by default, and each Boost level will + grant access to more slots. + + Uploaded stickers are constrained to 5 seconds in length for animated stickers, and 320 x 320 pixels. + + Stickers in the [Lottie file format](https://airbnb.design/lottie/) can only + be uploaded on guilds that have either the `VERIFIED` and/or the `PARTNERED` + guild feature. + + ## Parameters + + - `name`: Name of the sticker (2-30 characters) + - `description`: Description of the sticker (2-100 characters) + - `tags`: Autocomplete/suggestion tags for the sticker (max 200 characters) + - `file`: A path to a file to upload or a map of `name` (file name) and `body` (file data). + - `reason` (optional): audit log reason to attach to this event + + ## Returns + + Returns a `t:Nostrum.Struct.Sticker.t/0` on success. + """ + @spec create_guild_sticker( + Guild.id(), + Sticker.name(), + Sticker.description(), + Sticker.tags(), + String.t() | %{body: iodata(), name: String.t()}, + String.t() | nil + ) :: {:ok, Sticker.t()} | error + def create_guild_sticker(guild_id, name, description, tags, file, reason \\ nil) do + opts = %{ + name: name, + description: description, + tags: tags + } + + boundary = generate_boundary() + + multipart = create_multipart([], Jason.encode_to_iodata!(opts), boundary) + + headers = + maybe_add_reason(reason, [ + {"content-type", "multipart/form-data; boundary=#{boundary}"} + ]) + + file = create_file_part_for_multipart(file, nil, boundary, "file") + + %{ + method: :post, + route: Constants.guild_stickers(guild_id), + body: + {:multipart, + [ + ~s|--#{boundary}#{@crlf}|, + file + | multipart + ]}, + params: [], + headers: headers + } + |> request() + |> handle_request_with_decode({:struct, Sticker}) + end + + @doc ~S""" + Modify a guild sticker with the specified ID. + + Pass in a map of properties to update, with any of the following keys: + + - `name`: Name of the sticker (2-30 characters) + - `description`: Description of the sticker (2-100 characters) + - `tags`: Autocomplete/suggestion tags for the sticker (max 200 characters) + + Returns an updated sticker on update completion. + """ + @spec modify_guild_sticker(Guild.id(), Sticker.id(), %{ + name: Sticker.name() | nil, + description: Sticker.description() | nil, + tags: Sticker.tags() | nil + }) :: {:ok, Sticker.t()} | error + def modify_guild_sticker(guild_id, sticker_id, options) do + request(:patch, Constants.guild_sticker(guild_id, sticker_id), options) + |> handle_request_with_decode({:struct, Sticker}) + end + + @doc ~S""" + Delete a guild sticker with the specified ID. + """ + @spec delete_guild_sticker(Guild.id(), Sticker.id()) :: {:ok} | error + def delete_guild_sticker(guild_id, sticker_id) do + request(:delete, Constants.guild_sticker(guild_id, sticker_id)) + end + + @doc ~S""" + Get a list of available sticker packs. + """ + @spec get_sticker_packs() :: {:ok, [Sticker.Pack.t()]} | error + def get_sticker_packs do + resp = + request(:get, Constants.sticker_packs()) + |> handle_request_with_decode() + + case resp do + {:ok, %{sticker_packs: packs}} -> {:ok, Util.cast(packs, {:list, {:struct, Sticker.Pack}})} + _ -> resp + end + end + @doc ~S""" Get the `t:Nostrum.Struct.Guild.AuditLog.t/0` for the given `guild_id`. @@ -4373,8 +4520,6 @@ defmodule Nostrum.Api do end end - @crlf "\r\n" - defp create_multipart(files, json, boundary) do json_mime = MIME.type("json") json_size = :erlang.iolist_size(json) @@ -4397,16 +4542,23 @@ defmodule Nostrum.Api do ] end - defp create_file_part_for_multipart(file, index, boundary) do + defp create_file_part_for_multipart(file, index, boundary, name_override \\ nil) do {body, name} = get_file_contents(file) file_mime = MIME.from_path(name) file_size = :erlang.iolist_size(body) + field_name = + if name_override do + name_override + else + "files[#{index}]" + end + [ ~s|content-length: #{file_size}#{@crlf}|, ~s|content-type: #{file_mime}#{@crlf}|, - ~s|content-disposition: form-data; name="files[#{index}]"; filename="#{name}"#{@crlf}#{@crlf}|, + ~s|content-disposition: form-data; name="#{field_name}"; filename="#{name}"#{@crlf}#{@crlf}|, body, ~s|#{@crlf}--#{boundary}#{@crlf}| ] diff --git a/lib/nostrum/cache/guild_cache.ex b/lib/nostrum/cache/guild_cache.ex index c6931233c..95054a079 100644 --- a/lib/nostrum/cache/guild_cache.ex +++ b/lib/nostrum/cache/guild_cache.ex @@ -42,6 +42,7 @@ defmodule Nostrum.Cache.GuildCache do alias Nostrum.Struct.Emoji alias Nostrum.Struct.Guild alias Nostrum.Struct.Guild.Role + alias Nostrum.Struct.Sticker alias Nostrum.Util @configured_cache :nostrum @@ -146,6 +147,16 @@ defmodule Nostrum.Cache.GuildCache do @callback emoji_update(Guild.id(), emojis :: [map()]) :: {old_emojis :: [Emoji.t()], new_emojis :: [Emoji.t()]} + @doc """ + Update the sticker list of the given guild from upstream data. + + Discord sends us a complete list of stickers on an update, which is passed here. + + Return the old list of stickers before the update, and the updated list of stickers. + """ + @callback stickers_update(Guild.id(), stickers :: [map()]) :: + {old_stickers :: [Sticker.t()], new_stickers :: [Sticker.t()]} + @doc """ Create a role on the given guild from upstream data. @@ -248,6 +259,8 @@ defmodule Nostrum.Cache.GuildCache do @doc false defdelegate emoji_update(guild_id, emojis), to: @configured_cache @doc false + defdelegate stickers_update(guild_id, stickers), to: @configured_cache + @doc false defdelegate role_create(guild_id, role), to: @configured_cache @doc false defdelegate role_delete(guild_id, role), to: @configured_cache diff --git a/lib/nostrum/cache/guild_cache/ets.ex b/lib/nostrum/cache/guild_cache/ets.ex index dbdf2605b..62c11d55f 100644 --- a/lib/nostrum/cache/guild_cache/ets.ex +++ b/lib/nostrum/cache/guild_cache/ets.ex @@ -24,6 +24,7 @@ defmodule Nostrum.Cache.GuildCache.ETS do alias Nostrum.Struct.Emoji alias Nostrum.Struct.Guild alias Nostrum.Struct.Guild.Role + alias Nostrum.Struct.Sticker alias Nostrum.Util use Supervisor @@ -124,6 +125,17 @@ defmodule Nostrum.Cache.GuildCache.ETS do {guild.emojis, casted} end + @doc "Update the sticker list for the given guild in the cache." + @impl GuildCache + @spec stickers_update(Guild.id(), [map()]) :: {[Sticker.t()], [Sticker.t()]} + def stickers_update(guild_id, stickers) do + [{_id, guild}] = :ets.lookup(@table_name, guild_id) + casted = Util.cast(stickers, {:list, {:struct, Sticker}}) + new = %{guild | stickers: casted} + true = :ets.update_element(@table_name, guild_id, {2, new}) + {guild.stickers, casted} + end + @doc "Create the given role in the given guild in the cache." @impl GuildCache @spec role_create(Guild.id(), map()) :: {Guild.id(), Role.t()} diff --git a/lib/nostrum/cache/guild_cache/mnesia.ex b/lib/nostrum/cache/guild_cache/mnesia.ex index bc7f272f4..04f279559 100644 --- a/lib/nostrum/cache/guild_cache/mnesia.ex +++ b/lib/nostrum/cache/guild_cache/mnesia.ex @@ -17,6 +17,7 @@ if Code.ensure_loaded?(:mnesia) do alias Nostrum.Struct.Emoji alias Nostrum.Struct.Guild alias Nostrum.Struct.Guild.Role + alias Nostrum.Struct.Sticker alias Nostrum.Util use Supervisor @@ -161,6 +162,20 @@ if Code.ensure_loaded?(:mnesia) do {old_emojis, new_emojis} end + @impl GuildCache + @doc "Update the sticker list for the given guild in the cache." + @spec stickers_update(Guild.id(), [map()]) :: {[Sticker.t()], [Sticker.t()]} + def stickers_update(guild_id, stickers) do + new_stickers = Util.cast(stickers, {:list, {:struct, Sticker}}) + + old_stickers = + update_guild!(guild_id, fn guild -> + {%{guild | stickers: new_stickers}, guild.stickers} + end) + + {old_stickers, new_stickers} + end + @impl GuildCache @doc "Create the given role in the given guild in the cache." @spec role_create(Guild.id(), map()) :: {Guild.id(), Role.t()} diff --git a/lib/nostrum/cache/guild_cache/noop.ex b/lib/nostrum/cache/guild_cache/noop.ex index b08a609bc..b87909c2b 100644 --- a/lib/nostrum/cache/guild_cache/noop.ex +++ b/lib/nostrum/cache/guild_cache/noop.ex @@ -53,6 +53,12 @@ defmodule Nostrum.Cache.GuildCache.NoOp do {[], casted} end + @impl GuildCache + def stickers_update(_guild_id, stickers) do + casted = Util.cast(stickers, {:list, {:struct, Sticker}}) + {[], casted} + end + @impl GuildCache def role_create(guild_id, role), do: {guild_id, Util.cast(role, {:struct, Role})} diff --git a/lib/nostrum/constants.ex b/lib/nostrum/constants.ex index 040403a83..297adf2c1 100644 --- a/lib/nostrum/constants.ex +++ b/lib/nostrum/constants.ex @@ -5,6 +5,7 @@ defmodule Nostrum.Constants do def base_route, do: "/api/v10" def base_url, do: "https://#{domain()}#{base_route()}" def cdn_url, do: "https://cdn.discordapp.com" + def media_url, do: "https://media.discordapp.net" def gateway, do: "/gateway" def gateway_bot, do: "/gateway/bot" @@ -67,6 +68,12 @@ defmodule Nostrum.Constants do def guild_emojis(guild_id), do: "/guilds/#{guild_id}/emojis" def guild_emoji(guild_id, emoji_id), do: "/guilds/#{guild_id}/emojis/#{emoji_id}" + def sticker(sticker_id), do: "/stickers/#{sticker_id}" + def guild_stickers(guild_id), do: "/guilds/#{guild_id}/stickers" + def guild_sticker(guild_id, sticker_id), do: "/guilds/#{guild_id}/stickers/#{sticker_id}" + + def sticker_packs, do: "/sticker-packs" + def guild_scheduled_events(guild_id), do: "/guilds/#{guild_id}/scheduled-events" def guild_scheduled_event(guild_id, event_id), @@ -152,6 +159,9 @@ defmodule Nostrum.Constants do "/guilds/#{guild_id}/users/#{user_id}/avatars/#{avatar_hash}.#{image_format}" end + def cdn_sticker(id, image_format), do: "/stickers/#{id}.#{image_format}" + def cdn_sticker_pack(id), do: "/app-assets/710982414301790216/store/#{id}.png" + def thread_with_message(channel_id, message_id), do: "/channels/#{channel_id}/messages/#{message_id}/threads" diff --git a/lib/nostrum/permission.ex b/lib/nostrum/permission.ex index 6aa23c2de..29d81fe0f 100644 --- a/lib/nostrum/permission.ex +++ b/lib/nostrum/permission.ex @@ -44,10 +44,11 @@ defmodule Nostrum.Permission do | :manage_nicknames | :manage_roles | :manage_webhooks - | :manage_emojis_and_stickers + | :manage_guild_expressions | :view_guild_insights | :use_application_commands | :moderate_members + | :create_guild_expressions | :send_polls @type text_permission :: @@ -115,7 +116,7 @@ defmodule Nostrum.Permission do manage_nicknames: 1 <<< 27, manage_roles: 1 <<< 28, manage_webhooks: 1 <<< 29, - manage_emojis_and_stickers: 1 <<< 30, + manage_guild_expressions: 1 <<< 30, use_application_commands: 1 <<< 31, request_to_speak: 1 <<< 32, manage_events: 1 <<< 33, @@ -126,9 +127,14 @@ defmodule Nostrum.Permission do send_messages_in_threads: 1 <<< 38, use_embedded_activities: 1 <<< 39, moderate_members: 1 <<< 40, + create_guild_expressions: 1 <<< 43, send_polls: 1 <<< 49 } + @legacy_perm_names %{ + manage_emojis_and_stickers: :manage_guild_expressions + } + @bit_to_permission_map Map.new(@permission_to_bit_map, fn {k, v} -> {v, k} end) @permission_list Map.keys(@permission_to_bit_map) @@ -233,6 +239,9 @@ defmodule Nostrum.Permission do ``` """ @spec to_bit(t) :: bit + def to_bit(permission) when is_map_key(@legacy_perm_names, permission), + do: to_bit(@legacy_perm_names[permission]) + def to_bit(permission) when is_permission(permission), do: @permission_to_bit_map[permission] @doc """ diff --git a/lib/nostrum/shard/dispatch.ex b/lib/nostrum/shard/dispatch.ex index f1b7c9912..56656f836 100644 --- a/lib/nostrum/shard/dispatch.ex +++ b/lib/nostrum/shard/dispatch.ex @@ -171,6 +171,9 @@ defmodule Nostrum.Shard.Dispatch do def handle_event(:GUILD_EMOJIS_UPDATE = event, p, state), do: {event, GuildCache.emoji_update(p.guild_id, p.emojis), state} + def handle_event(:GUILD_STICKERS_UPDATE = event, p, state), + do: {event, GuildCache.stickers_update(p.guild_id, p.stickers), state} + def handle_event(:GUILD_INTEGRATIONS_UPDATE = event, p, state) do {event, GuildIntegrationsUpdate.to_struct(p), state} end diff --git a/lib/nostrum/struct/guild.ex b/lib/nostrum/struct/guild.ex index 04c005268..7c0e688f0 100644 --- a/lib/nostrum/struct/guild.ex +++ b/lib/nostrum/struct/guild.ex @@ -15,7 +15,7 @@ defmodule Nostrum.Struct.Guild do Struct representing a Discord guild. """ - alias Nostrum.Struct.{Channel, Emoji, User} + alias Nostrum.Struct.{Channel, Emoji, Sticker, User} alias Nostrum.Struct.Guild.{Role, ScheduledEvent} alias Nostrum.{Constants, Snowflake, Util} @@ -49,7 +49,8 @@ defmodule Nostrum.Struct.Guild do :channels, :guild_scheduled_events, :vanity_url_code, - :threads + :threads, + :stickers ] @typedoc "The guild's id" @@ -161,6 +162,9 @@ defmodule Nostrum.Struct.Guild do @typedoc since: "0.5.1" @type threads :: %{required(Channel.id()) => Channel.t()} | nil + @typedoc "Custom stickers registered to the guild" + @type stickers :: [Sticker.t()] + @typedoc """ A `Nostrum.Struct.Guild` that is sent on user-specific rest endpoints. """ @@ -193,7 +197,8 @@ defmodule Nostrum.Struct.Guild do voice_states: nil, channels: nil, vanity_url_code: nil, - threads: nil + threads: nil, + stickers: nil } @typedoc """ @@ -222,6 +227,7 @@ defmodule Nostrum.Struct.Guild do rules_channel_id: rules_channel_id, public_updates_channel_id: public_updates_channel_id, vanity_url_code: vanity_url_code, + stickers: stickers, joined_at: nil, large: nil, unavailable: nil, @@ -265,7 +271,8 @@ defmodule Nostrum.Struct.Guild do channels: nil, guild_scheduled_events: nil, vanity_url_code: nil, - threads: nil + threads: nil, + stickers: nil } @typedoc """ @@ -301,7 +308,8 @@ defmodule Nostrum.Struct.Guild do channels: channels, guild_scheduled_events: guild_scheduled_events, vanity_url_code: vanity_url_code, - threads: threads + threads: threads, + stickers: stickers } @type t :: @@ -379,6 +387,7 @@ defmodule Nostrum.Struct.Guild do |> Map.update(:afk_channel_id, nil, &Util.cast(&1, Snowflake)) |> Map.update(:roles, nil, &Util.cast(&1, {:index, [:id], {:struct, Role}})) |> Map.update(:emojis, nil, &Util.cast(&1, {:list, {:struct, Emoji}})) + |> Map.update(:stickers, nil, &Util.cast(&1, {:list, {:struct, Sticker}})) |> Map.update(:application_id, nil, &Util.cast(&1, Snowflake)) |> Map.update(:widget_channel_id, nil, &Util.cast(&1, Snowflake)) |> Map.update(:system_channel_id, nil, &Util.cast(&1, Snowflake)) diff --git a/lib/nostrum/struct/message.ex b/lib/nostrum/struct/message.ex index 00f52139b..c48cb9a16 100644 --- a/lib/nostrum/struct/message.ex +++ b/lib/nostrum/struct/message.ex @@ -6,7 +6,7 @@ defmodule Nostrum.Struct.Message do [Discord API Message Object Documentation](https://discord.com/developers/docs/resources/channel#message-object). """ - alias Nostrum.Struct.{Channel, Embed, Guild, Interaction, User} + alias Nostrum.Struct.{Channel, Embed, Guild, Interaction, Sticker, User} alias Nostrum.Struct.Guild.{Member, Role} alias Nostrum.Struct.Message.{ @@ -16,8 +16,7 @@ defmodule Nostrum.Struct.Message do Component, Poll, Reaction, - Reference, - Sticker + Reference } alias Nostrum.{Snowflake, Util} diff --git a/lib/nostrum/struct/message/sticker.ex b/lib/nostrum/struct/message/sticker.ex deleted file mode 100644 index f5777fcb6..000000000 --- a/lib/nostrum/struct/message/sticker.ex +++ /dev/null @@ -1,114 +0,0 @@ -defmodule Nostrum.Struct.Message.Sticker do - @moduledoc """ - A `Nostrum.Struct.Message.Sticker` represents a sticker that can be sent inside a `Nostrum.Struct.Message`. - - More information can be found on the [Discord API Sticker Object Documentation.](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-structure) - """ - @moduledoc since: "0.5.0" - - alias Nostrum.Struct.{Guild, User} - alias Nostrum.{Snowflake, Util} - - defstruct [ - :id, - :pack_id, - :name, - :description, - :tags, - :type, - :format_type, - :available, - :guild_id, - :user, - :sort_value - ] - - @typedoc """ - Id of the sticker - """ - @type id :: Snowflake.t() - - @typedoc """ - Id of the pack the sticker is from - """ - @type pack_id :: Snowflake.t() - - @typedoc """ - Name of the sticker - """ - @type name :: String.t() | nil - - @typedoc """ - Description of the sticker - """ - @type description :: String.t() | nil - - @typedoc """ - Discord name of a unicode emoji representing the sticker's expression. for standard stickers, a comma-separated list of related expressions. - """ - @type tags :: String.t() | nil - - @typedoc """ - [Discord API Sticker Object Type Documentation](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-types) - - - `1` - `STANDARD` an official sticker in a pack, part of Nitro or in a removed purchasable pack - - `2` - `GUILD` a sticker uploaded to a Boosted guild for the guild's members - """ - @type type :: integer() - - @typedoc """ - [Discord API Sticker Object Format Type Documentation](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) - - - `1` - `PNG` - - `2` - `APNG` - - `3` - `LOTTIE` - """ - @type format_type :: integer() - - @typedoc """ - Whether this guild sticker can be used, may be false due to loss of Server Boosts - """ - @type available :: boolean - - @typedoc """ - Id of the guild that owns this sticker - """ - @type guild_id :: Guild.id() | nil - - @typedoc """ - User that uploaded the guild sticker - """ - @type user :: User.t() - - @typedoc """ - The sticker's sort order within its pack - """ - @type sort_value :: integer() - - @type t :: %__MODULE__{ - id: id, - pack_id: pack_id, - name: name, - description: description, - tags: tags, - type: type, - format_type: format_type, - available: available, - guild_id: guild_id, - user: user, - sort_value: sort_value - } - - @doc false - def to_struct(map) do - new = - map - |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end) - |> Map.update(:id, nil, &Util.cast(&1, Snowflake)) - |> Map.update(:pack_id, nil, &Util.cast(&1, Snowflake)) - |> Map.update(:guild_id, nil, &Util.cast(&1, Snowflake)) - |> Map.update(:user, nil, &Util.cast(&1, {:struct, User})) - - struct(__MODULE__, new) - end -end diff --git a/lib/nostrum/struct/sticker.ex b/lib/nostrum/struct/sticker.ex new file mode 100644 index 000000000..88f2399a5 --- /dev/null +++ b/lib/nostrum/struct/sticker.ex @@ -0,0 +1,174 @@ +defmodule Nostrum.Struct.Sticker do + @moduledoc """ + A `Nostrum.Struct.Sticker` represents a sticker that can be sent inside a + `Nostrum.Struct.Message`. + """ + + alias Nostrum.Struct.{Guild, User} + alias Nostrum.{Constants, Snowflake, Util} + + defstruct [ + :id, + :pack_id, + :name, + :description, + :tags, + :type, + :format_type, + :available, + :guild_id, + :user, + :sort_value + ] + + @typedoc """ + ID of the sticker + """ + @type id :: Snowflake.t() + + @typedoc """ + ID of the pack the sticker is from + """ + @type pack_id :: Snowflake.t() + + @typedoc """ + Name of the sticker + """ + @type name :: String.t() | nil + + @typedoc """ + Description of the sticker + """ + @type description :: String.t() | nil + + @typedoc """ + Tags used by the Discord client to auto-complete a sticker. + + For default sticker packs, this is a comma-separated list. For guild stickers, + this is the name of the unicode emoji associated by the sticker creator with + the sticker. + + This is technically a free-text field so consistency in formatting is not guaranteed. + """ + @type tags :: String.t() + + @typedoc """ + Whether the sticker is a standard (platform made) sticker or a custom guild sticker. + """ + @type type :: :standard | :guild + + @typedoc """ + Format of the sticker. + + This field is used to determine the return URL in `cdn_url/1`. + """ + @type format_type :: :png | :apng | :lottie | :gif + + @typedoc """ + Whether this guild sticker can be used. + + May be false due to loss of Server Boosts + """ + @type available :: boolean + + @typedoc """ + ID of the guild that owns this sticker. + + `nil` if the sticker is a built-in (type `:standard`) sticker. + """ + @type guild_id :: Guild.id() | nil + + @typedoc """ + User that uploaded the guild sticker. + + `nil` if the sticker is a built-in (type `:standard`) sticker. + """ + @type user :: User.t() | nil + + @typedoc """ + The sticker's sort order within its pack. + + Sometimes provided for stickers with type `:standard` that are in a pack. + """ + @type sort_value :: integer() | nil + + @type t :: %__MODULE__{ + id: id, + pack_id: pack_id, + name: name, + description: description, + tags: tags, + type: type, + format_type: format_type, + available: available, + guild_id: guild_id, + user: user, + sort_value: sort_value + } + + @doc false + def to_struct(map) do + new = + map + |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end) + |> Map.update(:id, nil, &Util.cast(&1, Snowflake)) + |> Map.update(:pack_id, nil, &Util.cast(&1, Snowflake)) + |> Map.update(:guild_id, nil, &Util.cast(&1, Snowflake)) + |> Map.update(:user, nil, &Util.cast(&1, {:struct, User})) + |> Map.update(:format_type, nil, &cast_format_type/1) + |> Map.update(:type, nil, &cast_type/1) + + struct(__MODULE__, new) + end + + @doc false + defp cast_format_type(format_type) do + case format_type do + 1 -> :png + 2 -> :apng + 3 -> :lottie + 4 -> :gif + end + end + + @doc false + defp cast_type(type) do + case type do + 1 -> :standard + 2 -> :guild + end + end + + @doc ~S""" + Fetch a CDN URL for the sticker object. + + `:png` and `:apng` stickers will return a `.png` URL, `:gif` will return a + `.gif` URL and `:lottie` will return a `.json` URL. + + ### Examples + + ```elixir + iex> sticker = %Nostrum.Struct.Sticker{format_type: :gif, id: 112233445566778899} + iex> Nostrum.Struct.Sticker.cdn_url sticker + "https://media.discordapp.net/stickers/112233445566778899.gif" + ``` + + ```elixir + iex> sticker = %Nostrum.Struct.Sticker{format_type: :apng, id: 998877665544332211} + iex> Nostrum.Struct.Sticker.cdn_url sticker + "https://cdn.discordapp.com/stickers/998877665544332211.png" + ``` + """ + @spec cdn_url(t()) :: String.t() + def cdn_url(%{format_type: :gif, id: id}) do + Constants.media_url() <> Constants.cdn_sticker(id, "gif") + end + + def cdn_url(%{format_type: :lottie, id: id}) do + Constants.cdn_url() <> Constants.cdn_sticker(id, "json") + end + + def cdn_url(%{format_type: format, id: id}) when format in [:png, :apng] do + Constants.cdn_url() <> Constants.cdn_sticker(id, "png") + end +end diff --git a/lib/nostrum/struct/sticker/pack.ex b/lib/nostrum/struct/sticker/pack.ex new file mode 100644 index 000000000..f67c1bf7b --- /dev/null +++ b/lib/nostrum/struct/sticker/pack.ex @@ -0,0 +1,95 @@ +defmodule Nostrum.Struct.Sticker.Pack do + @moduledoc """ + Represents a platform-curated sticker pack on Discord + """ + + defstruct [ + :id, + :stickers, + :name, + :sku_id, + :cover_sticker_id, + :description, + :banner_asset_id + ] + + alias Nostrum.{Constants, Snowflake, Util} + alias Nostrum.Struct.Sticker + + @typedoc """ + ID of the sticker pack. + """ + @type id :: Snowflake.t() + + @typedoc """ + A list of stickers contained within the pack. + """ + @type stickers :: [Sticker.t()] + + @typedoc """ + Name of the pack. + """ + @type name :: String.t() + + @typedoc """ + SKU ID of the sticker pack. + """ + @type sku_id :: Snowflake.t() + + @typedoc """ + ID of a sticker contained within the pack that should be the cover. + """ + @type cover_sticker_id :: Sticker.id() + + @typedoc """ + Marketing description of the sticker pack. + """ + @type description :: String.t() + + @typedoc """ + Asset ID of the banner for this sticker pack. + """ + @type banner_asset_id :: Snowflake.t() + + @type t :: %__MODULE__{ + id: id, + stickers: stickers, + name: name, + sku_id: sku_id, + cover_sticker_id: cover_sticker_id, + description: description, + banner_asset_id: banner_asset_id + } + + @doc false + def to_struct(map) do + new = + map + |> Map.new(fn {k, v} -> {Util.maybe_to_atom(k), v} end) + |> Map.update(:stickers, nil, &Util.cast(&1, {:list, {:struct, Sticker}})) + |> Map.update(:id, nil, &Util.cast(&1, Snowflake)) + |> Map.update(:sku_id, nil, &Util.cast(&1, Snowflake)) + |> Map.update(:cover_sticker_id, nil, &Util.cast(&1, Snowflake)) + |> Map.update(:banner_asset_id, nil, &Util.cast(&1, Snowflake)) + + struct(__MODULE__, new) + end + + @doc ~S""" + Return the banner pack URL for a given sticker pack. + + This is a marketing banner provided by Discord for their platform curated sticker packs. + + ### Examples + + ```elixir + iex> pack = %Nostrum.Struct.Sticker.Pack{banner_asset_id: 112233445566778899} + iex> Nostrum.Struct.Sticker.Pack.banner_url pack + "https://cdn.discordapp.com/app-assets/710982414301790216/store/112233445566778899.png" + ``` + """ + @spec banner_url(t()) :: String.t() + def banner_url(%{banner_asset_id: id}) do + Constants.cdn_url() <> Constants.cdn_sticker_pack(id) + end +end diff --git a/test/nostrum/struct/sticker/pack_test.exs b/test/nostrum/struct/sticker/pack_test.exs new file mode 100644 index 000000000..70d32466d --- /dev/null +++ b/test/nostrum/struct/sticker/pack_test.exs @@ -0,0 +1,7 @@ +defmodule Nostrum.Struct.Sticker.PackTest do + use ExUnit.Case, async: true + + alias Nostrum.Struct.Sticker.Pack + + doctest Pack +end diff --git a/test/nostrum/struct/sticker_test.exs b/test/nostrum/struct/sticker_test.exs new file mode 100644 index 000000000..3cea91c9e --- /dev/null +++ b/test/nostrum/struct/sticker_test.exs @@ -0,0 +1,7 @@ +defmodule Nostrum.Struct.StickerTest do + use ExUnit.Case, async: true + + alias Nostrum.Struct.Sticker + + doctest Sticker +end