Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Add full support for guild stickers #584

Merged
merged 8 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
160 changes: 156 additions & 4 deletions lib/nostrum/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ defmodule Nostrum.Api do
```
"""

@crlf "\r\n"

import Nostrum.Snowflake, only: [is_snowflake: 1]

alias Nostrum.Api.Ratelimiter
Expand All @@ -58,6 +60,7 @@ defmodule Nostrum.Api do
Invite,
Message,
Message.Poll,
Sticker,
ThreadMember,
User,
Webhook
Expand Down Expand Up @@ -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)
jchristgit marked this conversation as resolved.
Show resolved Hide resolved

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`.

Expand Down Expand Up @@ -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)
Expand All @@ -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}|
]
Expand Down
13 changes: 13 additions & 0 deletions lib/nostrum/cache/guild_cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions lib/nostrum/cache/guild_cache/ets.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()}
Expand Down
15 changes: 15 additions & 0 deletions lib/nostrum/cache/guild_cache/mnesia.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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()}
Expand Down
6 changes: 6 additions & 0 deletions lib/nostrum/cache/guild_cache/noop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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})}

Expand Down
10 changes: 10 additions & 0 deletions lib/nostrum/constants.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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"

Expand Down
13 changes: 11 additions & 2 deletions lib/nostrum/permission.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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 ::
Expand Down Expand Up @@ -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,
Expand All @@ -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)

Expand Down Expand Up @@ -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 """
Expand Down
3 changes: 3 additions & 0 deletions lib/nostrum/shard/dispatch.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading