Skip to content

Commit

Permalink
Merge pull request #584 from Kraigie/jb3/sticker-revamps
Browse files Browse the repository at this point in the history
Add full support for guild stickers
  • Loading branch information
jb3 authored May 6, 2024
2 parents f7d1139 + 71b0d9a commit e07393f
Show file tree
Hide file tree
Showing 15 changed files with 525 additions and 128 deletions.
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)
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

0 comments on commit e07393f

Please # to comment.