Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support multipart file uploads for interaction responses via webhook builder #178

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
19 changes: 14 additions & 5 deletions lib/discordrb/api/interaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,25 @@ module Discordrb::API::Interaction

# Respond to an interaction.
# https://discord.com/developers/docs/interactions/slash-commands#create-interaction-response
def create_interaction_response(interaction_token, interaction_id, type, content = nil, tts = nil, embeds = nil, allowed_mentions = nil, flags = nil, components = nil)
def create_interaction_response(interaction_token, interaction_id, type, content = nil, tts = nil, embeds = nil, allowed_mentions = nil, flags = nil, components = nil, attachments = nil)
data = { tts: tts, content: content, embeds: embeds, allowed_mentions: allowed_mentions, flags: flags, components: components }.compact

payload = { type: type, data: data }.to_json

if attachments
files = [*0...attachments.size].zip(attachments).to_h
payload = { **files, payload_json: payload }
end

headers = { content_type: :json } unless attachments

Discordrb::API.request(
:interactions_iid_token_callback,
interaction_id,
:post,
"#{Discordrb::API.api_base}/interactions/#{interaction_id}/#{interaction_token}/callback",
{ type: type, data: data }.to_json,
content_type: :json
payload,
headers
)
end

Expand All @@ -42,8 +51,8 @@ def get_original_interaction_response(interaction_token, application_id)

# Edit the original response to an interaction.
# https://discord.com/developers/docs/interactions/slash-commands#edit-original-interaction-response
def edit_original_interaction_response(interaction_token, application_id, content = nil, embeds = nil, allowed_mentions = nil, components = nil)
Discordrb::API::Webhook.token_edit_message(interaction_token, application_id, '@original', content, embeds, allowed_mentions, components)
def edit_original_interaction_response(interaction_token, application_id, content = nil, embeds = nil, allowed_mentions = nil, components = nil, attachments = nil)
Discordrb::API::Webhook.token_edit_message(interaction_token, application_id, '@original', content, embeds, allowed_mentions, components, attachments)
end

# Delete the original response to an interaction.
Expand Down
37 changes: 26 additions & 11 deletions lib/discordrb/api/webhook.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,27 @@ def token_webhook(webhook_token, webhook_id)

# Execute a webhook via token.
# https://discord.com/developers/docs/resources/webhook#execute-webhook
def token_execute_webhook(webhook_token, webhook_id, wait = false, content = nil, username = nil, avatar_url = nil, tts = nil, file = nil, embeds = nil, allowed_mentions = nil, flags = nil, components = nil)
def token_execute_webhook(webhook_token, webhook_id, wait = false, content = nil, username = nil, avatar_url = nil, tts = nil, file = nil, embeds = nil, allowed_mentions = nil, flags = nil, components = nil, attachments = nil)
body = { content: content, username: username, avatar_url: avatar_url, tts: tts, embeds: embeds&.map(&:to_hash), allowed_mentions: allowed_mentions, flags: flags, components: components }
body = if file
{ file: file, payload_json: body.to_json }
else
body.to_json
end

headers = { content_type: :json } unless file
payload = body.to_json

# since this method signature already had 'file' param, we'll prefer deprecated behavior for now
if file
payload = { file: file, payload_json: payload }
elsif attachments
files = [*0...attachments.size].zip(attachments).to_h
payload = { **files, payload_json: payload }
end

headers = { content_type: :json } unless attachments || file

Discordrb::API.request(
:webhooks_wid,
webhook_id,
:post,
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}?wait=#{wait}",
body,
payload,
headers
)
end
Expand Down Expand Up @@ -116,14 +121,24 @@ def token_get_message(webhook_token, webhook_id, message_id)

# Edit a webhook message via webhook token
# https://discord.com/developers/docs/resources/webhook#edit-webhook-message
def token_edit_message(webhook_token, webhook_id, message_id, content = nil, embeds = nil, allowed_mentions = nil, components = nil)
def token_edit_message(webhook_token, webhook_id, message_id, content = nil, embeds = nil, allowed_mentions = nil, components = nil, attachments = nil)
data = { content: content, embeds: embeds, allowed_mentions: allowed_mentions, components: components }

payload = data.to_json
if attachments
files = [*0...attachments.size].zip(attachments).to_h
payload = { **files, payload_json: payload }
end

headers = { content_type: :json } unless attachments

Discordrb::API.request(
:webhooks_wid_messages,
webhook_id,
:patch,
"#{Discordrb::API.api_base}/webhooks/#{webhook_id}/#{webhook_token}/messages/#{message_id}",
{ content: content, embeds: embeds, allowed_mentions: allowed_mentions, components: components }.to_json,
content_type: :json
payload,
headers
)
end

Expand Down
47 changes: 35 additions & 12 deletions lib/discordrb/data/interaction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,20 @@ def respond(content: nil, tts: nil, embeds: nil, allowed_mentions: nil, flags: 0
yield(builder, view) if block_given?

components ||= view
data = builder.to_json_hash

Discordrb::API::Interaction.create_interaction_response(@token, @id, CALLBACK_TYPES[:channel_message], data[:content], tts, data[:embeds], data[:allowed_mentions], flags, components.to_a)
data = builder.to_payload_hash

Discordrb::API::Interaction.create_interaction_response(
@token,
@id,
CALLBACK_TYPES[:channel_message],
data[:content],
tts,
data[:embeds],
data[:allowed_mentions],
flags,
components.to_a,
data[:attachments]
)

return unless wait

Expand Down Expand Up @@ -147,7 +158,7 @@ def show_modal(title:, custom_id:, components: nil)
nil
end

# Respond to the creation of this interaction. An interaction must be responded to or deferred,
# For components, edit the message the component was attached to.
# The response may be modified with {Interaction#edit_response} or deleted with {Interaction#delete_response}.
# Further messages can be sent with {Interaction#send_message}.
# @param content [String] The content of the message.
Expand All @@ -170,9 +181,9 @@ def update_message(content: nil, tts: nil, embeds: nil, allowed_mentions: nil, f
yield(builder, view) if block_given?

components ||= view
data = builder.to_json_hash
data = builder.to_payload_hash

Discordrb::API::Interaction.create_interaction_response(@token, @id, CALLBACK_TYPES[:update_message], data[:content], tts, data[:embeds], data[:allowed_mentions], flags, components.to_a)
Discordrb::API::Interaction.create_interaction_response(@token, @id, CALLBACK_TYPES[:update_message], data[:content], tts, data[:embeds], data[:allowed_mentions], flags, components.to_a, data[:attachments])

return unless wait

Expand All @@ -195,8 +206,8 @@ def edit_response(content: nil, embeds: nil, allowed_mentions: nil, components:
yield(builder, view) if block_given?

components ||= view
data = builder.to_json_hash
resp = Discordrb::API::Interaction.edit_original_interaction_response(@token, @application_id, data[:content], data[:embeds], data[:allowed_mentions], components.to_a)
data = builder.to_payload_hash
resp = Discordrb::API::Interaction.edit_original_interaction_response(@token, @application_id, data[:content], data[:embeds], data[:allowed_mentions], components.to_a, data[:attachments])

Interactions::Message.new(JSON.parse(resp), @bot, @interaction)
end
Expand All @@ -223,10 +234,22 @@ def send_message(content: nil, embeds: nil, tts: false, allowed_mentions: nil, f
yield builder, view if block_given?

components ||= view
data = builder.to_json_hash
data = builder.to_payload_hash

resp = Discordrb::API::Webhook.token_execute_webhook(
@token, @application_id, true, data[:content], nil, nil, tts, nil, data[:embeds], data[:allowed_mentions], flags, components.to_a
@token,
@application_id,
true,
data[:content],
nil, # username
nil, # avatar_url
tts,
data[:file], # deprecated
data[:embeds],
data[:allowed_mentions],
flags,
components.to_a,
data[:attachments]
)
Interactions::Message.new(JSON.parse(resp), @bot, @interaction)
end
Expand All @@ -244,10 +267,10 @@ def edit_message(message, content: nil, embeds: nil, allowed_mentions: nil, comp
yield builder, view if block_given?

components ||= view
data = builder.to_json_hash
data = builder.to_payload_hash

resp = Discordrb::API::Webhook.token_edit_message(
@token, @application_id, message.resolve_id, data[:content], data[:embeds], data[:allowed_mentions], components.to_a
@token, @application_id, message.resolve_id, data[:content], data[:embeds], data[:allowed_mentions], components.to_a, data[:attachments]
)
Interactions::Message.new(JSON.parse(resp), @bot, @interaction)
end
Expand Down
39 changes: 35 additions & 4 deletions lib/discordrb/webhooks/builder.rb
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
# frozen_string_literal: true

require 'discordrb'
require 'discordrb/webhooks/embeds'

module Discordrb::Webhooks
# A class that acts as a builder for a webhook message object.
class Builder
def initialize(content: '', username: nil, avatar_url: nil, tts: false, file: nil, embeds: [], allowed_mentions: nil)
def initialize(content: '', username: nil, avatar_url: nil, tts: false, file: nil, embeds: [], allowed_mentions: nil, attachments: [])
@content = content
@username = username
@avatar_url = avatar_url
@tts = tts
@file = file
@embeds = embeds
@allowed_mentions = allowed_mentions
@attachments = attachments
end

# The content of the message. May be 2000 characters long at most.
Expand All @@ -36,16 +38,27 @@ def initialize(content: '', username: nil, avatar_url: nil, tts: false, file: ni
# Sets a file to be sent together with the message. Mutually exclusive with embeds; a webhook message can contain
# either a file to be sent or an embed.
# @param file [File] A file to be sent.
# @deprecated Use {#attachments=} instead.
def file=(file)
Discordrb::LOGGER.warn('The `file` attribute for the webhook builder is deprecated. Please use `attachments` instead.')
raise ArgumentError, 'Embeds and files are mutually exclusive!' unless @embeds.empty?

@file = file
end

# Sets files to be sent together with the message. Mutually exclusive with embeds; a webhook message can contain
# either attachments to be sent or an embed.
# @param attachments [Array<File>] Files to be sent.
def attachments=(attachments)
raise ArgumentError, 'Embeds and attachments are mutually exclusive!' unless @embeds.empty?

@attachments = attachments
end

# Adds an embed to this message.
# @param embed [Embed] The embed to add.
def <<(embed)
raise ArgumentError, 'Embeds and files are mutually exclusive!' if @file
raise ArgumentError, 'Embeds and attachments are mutually exclusive!' unless @attachments.empty?

@embeds << embed
end
Expand All @@ -66,8 +79,12 @@ def add_embed(embed = nil)
end

# @return [File, nil] the file attached to this message.
# @deprecated Use {#attachments} instead.
attr_reader :file

# @return [Array<File>] the files attached to this message.
attr_reader :attachments

# @return [Array<Embed>] the embeds attached to this message.
attr_reader :embeds

Expand All @@ -89,14 +106,28 @@ def to_json_hash

# @return [Hash] a hash representation of the created message, for multipart format.
def to_multipart_hash
{
hash = {
content: @content,
username: @username,
avatar_url: @avatar_url,
tts: @tts,
file: @file,
file: @file, # deprecated
allowed_mentions: @allowed_mentions&.to_hash
}

# if file is specified, prefer old (deprecated) behavior for compatibility
return hash if file

hash[:attachments] = @attachments
hash
end

# @return [Hash] a hash representation of the created message, for either json or multipart format depending if a
# file is present in the builder
def to_payload_hash
return to_multipart_hash if !@attachments.empty? || @file

to_json_hash
end
end
end