From 31353199b140c071f1a4505a4227d76dbf1c51c1 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 01:06:13 +0100 Subject: [PATCH 1/8] Add Sticker asset CDN constants --- lib/nostrum/constants.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/nostrum/constants.ex b/lib/nostrum/constants.ex index 040403a83..1d5154500 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" @@ -152,6 +153,8 @@ 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 thread_with_message(channel_id, message_id), do: "/channels/#{channel_id}/messages/#{message_id}/threads" From 678b44c3678e53b55e5b8213a742e1ef39ae4e45 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 05:08:55 +0100 Subject: [PATCH 2/8] Move sticker.ex out of lib/nostrum/struct/message/ --- lib/nostrum/struct/{message => }/sticker.ex | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename lib/nostrum/struct/{message => }/sticker.ex (100%) diff --git a/lib/nostrum/struct/message/sticker.ex b/lib/nostrum/struct/sticker.ex similarity index 100% rename from lib/nostrum/struct/message/sticker.ex rename to lib/nostrum/struct/sticker.ex From 59218d98479da6159cd1c5c5eb4127bb56a4ce98 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 01:06:36 +0100 Subject: [PATCH 3/8] Refactor Sticker struct and add new methods Refactors the previous Sticker struct that lived under message and add new cdn_url/1 method for finding the CDN URL of a sticker. --- lib/nostrum/struct/message.ex | 5 +- lib/nostrum/struct/sticker.ex | 112 ++++++++++++++++++++------- test/nostrum/struct/sticker_test.exs | 7 ++ 3 files changed, 95 insertions(+), 29 deletions(-) create mode 100644 test/nostrum/struct/sticker_test.exs 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/sticker.ex b/lib/nostrum/struct/sticker.ex index f5777fcb6..88f2399a5 100644 --- a/lib/nostrum/struct/sticker.ex +++ b/lib/nostrum/struct/sticker.ex @@ -1,13 +1,11 @@ -defmodule Nostrum.Struct.Message.Sticker do +defmodule Nostrum.Struct.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) + A `Nostrum.Struct.Sticker` represents a sticker that can be sent inside a + `Nostrum.Struct.Message`. """ - @moduledoc since: "0.5.0" alias Nostrum.Struct.{Guild, User} - alias Nostrum.{Snowflake, Util} + alias Nostrum.{Constants, Snowflake, Util} defstruct [ :id, @@ -24,12 +22,12 @@ defmodule Nostrum.Struct.Message.Sticker do ] @typedoc """ - Id of the sticker + ID of the sticker """ @type id :: Snowflake.t() @typedoc """ - Id of the pack the sticker is from + ID of the pack the sticker is from """ @type pack_id :: Snowflake.t() @@ -44,46 +42,55 @@ defmodule Nostrum.Struct.Message.Sticker do @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. + 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() | nil + @type tags :: String.t() @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 + Whether the sticker is a standard (platform made) sticker or a custom guild sticker. """ - @type type :: integer() + @type type :: :standard | :guild @typedoc """ - [Discord API Sticker Object Format Type Documentation](https://discord.com/developers/docs/resources/sticker#sticker-object-sticker-format-types) + Format of the sticker. - - `1` - `PNG` - - `2` - `APNG` - - `3` - `LOTTIE` + This field is used to determine the return URL in `cdn_url/1`. """ - @type format_type :: integer() + @type format_type :: :png | :apng | :lottie | :gif @typedoc """ - Whether this guild sticker can be used, may be false due to loss of Server Boosts + 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 + 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 + User that uploaded the guild sticker. + + `nil` if the sticker is a built-in (type `:standard`) sticker. """ - @type user :: User.t() + @type user :: User.t() | nil @typedoc """ - The sticker's sort order within its pack + The sticker's sort order within its pack. + + Sometimes provided for stickers with type `:standard` that are in a pack. """ - @type sort_value :: integer() + @type sort_value :: integer() | nil @type t :: %__MODULE__{ id: id, @@ -108,7 +115,60 @@ defmodule Nostrum.Struct.Message.Sticker do |> 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/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 From 04902b32638a216b60841ab8f24f611243ecc969 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 01:07:09 +0100 Subject: [PATCH 4/8] Add stickers attribute to guild struct --- lib/nostrum/struct/guild.ex | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) 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)) From 5c5dbe5d15850050f23686eb3d3571ccd2600652 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 02:20:16 +0100 Subject: [PATCH 5/8] Add API methods for interacting with Stickers --- lib/nostrum/api.ex | 145 +++++++++++++++++++++++++++++++++++++-- lib/nostrum/constants.ex | 4 ++ 2 files changed, 145 insertions(+), 4 deletions(-) diff --git a/lib/nostrum/api.ex b/lib/nostrum/api.ex index 616f532f8..a4dcd325d 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,135 @@ 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 the `t:Nostrum.Struct.Guild.AuditLog.t/0` for the given `guild_id`. @@ -4373,8 +4505,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 +4527,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/constants.ex b/lib/nostrum/constants.ex index 1d5154500..e1d4d2458 100644 --- a/lib/nostrum/constants.ex +++ b/lib/nostrum/constants.ex @@ -68,6 +68,10 @@ 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 guild_scheduled_events(guild_id), do: "/guilds/#{guild_id}/scheduled-events" def guild_scheduled_event(guild_id, event_id), From efa772b3379f50d9e2b670c3b8ebf61637bd4f86 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 02:33:29 +0100 Subject: [PATCH 6/8] Update caches to support stickers Update cache bases and our implementations to support stickers and allow the proper dispatching of sticker update events. --- lib/nostrum/cache/guild_cache.ex | 13 +++++++++++++ lib/nostrum/cache/guild_cache/ets.ex | 12 ++++++++++++ lib/nostrum/cache/guild_cache/mnesia.ex | 15 +++++++++++++++ lib/nostrum/cache/guild_cache/noop.ex | 6 ++++++ lib/nostrum/shard/dispatch.ex | 3 +++ 5 files changed, 49 insertions(+) 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/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 From 635994e591baa63a461e0b7bef5e78b57ee958a9 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 02:52:46 +0100 Subject: [PATCH 7/8] Add support for Sticker Packs --- lib/nostrum/api.ex | 15 ++++ lib/nostrum/constants.ex | 3 + lib/nostrum/struct/sticker/pack.ex | 95 +++++++++++++++++++++++ test/nostrum/struct/sticker/pack_test.exs | 7 ++ 4 files changed, 120 insertions(+) create mode 100644 lib/nostrum/struct/sticker/pack.ex create mode 100644 test/nostrum/struct/sticker/pack_test.exs diff --git a/lib/nostrum/api.ex b/lib/nostrum/api.ex index a4dcd325d..accf3f1bd 100644 --- a/lib/nostrum/api.ex +++ b/lib/nostrum/api.ex @@ -1548,6 +1548,21 @@ defmodule Nostrum.Api 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`. diff --git a/lib/nostrum/constants.ex b/lib/nostrum/constants.ex index e1d4d2458..297adf2c1 100644 --- a/lib/nostrum/constants.ex +++ b/lib/nostrum/constants.ex @@ -72,6 +72,8 @@ defmodule Nostrum.Constants do 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), @@ -158,6 +160,7 @@ defmodule Nostrum.Constants do 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/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 From 71b0d9a30e5d1d05754ec5cc74ce0e23d58a5025 Mon Sep 17 00:00:00 2001 From: Joe Banks Date: Sun, 5 May 2024 03:12:19 +0100 Subject: [PATCH 8/8] Update permissions for guild expressions - Rename the manage_emojis_and_stickers permission to `manage_guild_expressions` - Add the new `create_guild_expressions` permission - Add a legacy mapping system for creating bitsets with the old permission name as a *bit* of backwards compatibility --- lib/nostrum/permission.ex | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) 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 """