discorb 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/package_register.yml +34 -0
- data/Changelog.md +23 -0
- data/README.md +4 -3
- data/discorb.gemspec +0 -2
- data/docs/cli/{init.md → new.md} +0 -0
- data/docs/cli/setup.md +1 -1
- data/docs/cli.md +1 -1
- data/docs/events.md +5 -0
- data/lib/discorb/app_command.rb +14 -0
- data/lib/discorb/channel.rb +0 -42
- data/lib/discorb/client.rb +45 -42
- data/lib/discorb/color.rb +1 -0
- data/lib/discorb/common.rb +7 -1
- data/lib/discorb/error.rb +8 -5
- data/lib/discorb/gateway.rb +60 -47
- data/lib/discorb/interaction/autocomplete.rb +49 -0
- data/lib/discorb/interaction/command.rb +142 -0
- data/lib/discorb/interaction/components.rb +85 -0
- data/lib/discorb/interaction/response.rb +188 -0
- data/lib/discorb/interaction/root.rb +94 -0
- data/lib/discorb/interaction.rb +2 -616
- data/lib/discorb/log.rb +3 -2
- data/lib/discorb/message.rb +3 -1
- data/lib/discorb/modules.rb +42 -0
- data/lib/discorb/rate_limit.rb +2 -2
- metadata +9 -4
@@ -0,0 +1,142 @@
|
|
1
|
+
module Discorb
|
2
|
+
#
|
3
|
+
# Represents a command interaction.
|
4
|
+
#
|
5
|
+
class CommandInteraction < Interaction
|
6
|
+
@interaction_type = 2
|
7
|
+
@interaction_name = :application_command
|
8
|
+
include Interaction::SourceResponse
|
9
|
+
|
10
|
+
#
|
11
|
+
# Represents a slash command interaction.
|
12
|
+
#
|
13
|
+
class SlashCommand < CommandInteraction
|
14
|
+
@command_type = 1
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def _set_data(data)
|
19
|
+
super
|
20
|
+
|
21
|
+
name, options = SlashCommand.get_command_data(data)
|
22
|
+
|
23
|
+
unless (command = @client.bottom_commands.find { |c| c.to_s == name && c.type_raw == 1 })
|
24
|
+
@client.log.warn "Unknown command name #{name}, ignoring"
|
25
|
+
return
|
26
|
+
end
|
27
|
+
|
28
|
+
option_map = command.options.map { |k, v| [k.to_s, v[:default]] }.to_h
|
29
|
+
SlashCommand.modify_option_map(option_map, options)
|
30
|
+
|
31
|
+
command.block.call(self, *command.options.map { |k, v| option_map[k.to_s] })
|
32
|
+
end
|
33
|
+
|
34
|
+
class << self
|
35
|
+
# @private
|
36
|
+
def get_command_data(data)
|
37
|
+
name = data[:name]
|
38
|
+
options = nil
|
39
|
+
return name, options unless (option = data[:options]&.first)
|
40
|
+
|
41
|
+
case option[:type]
|
42
|
+
when 1
|
43
|
+
name += " #{option[:name]}"
|
44
|
+
options = option[:options]
|
45
|
+
when 2
|
46
|
+
name += " #{option[:name]}"
|
47
|
+
unless option[:options]&.first&.[](:type) == 1
|
48
|
+
options = option[:options]
|
49
|
+
return name, options
|
50
|
+
end
|
51
|
+
option_sub = option[:options]&.first
|
52
|
+
name += " #{option_sub[:name]}"
|
53
|
+
options = option_sub[:options]
|
54
|
+
else
|
55
|
+
options = data[:options]
|
56
|
+
end
|
57
|
+
|
58
|
+
return name, options
|
59
|
+
end
|
60
|
+
|
61
|
+
# @private
|
62
|
+
def modify_option_map(option_map, options)
|
63
|
+
options ||= []
|
64
|
+
options.each_with_index do |option|
|
65
|
+
val = case option[:type]
|
66
|
+
when 3, 4, 5, 10
|
67
|
+
option[:value]
|
68
|
+
when 6
|
69
|
+
guild.members[option[:value]] || guild.fetch_member(option[:value]).wait
|
70
|
+
when 7
|
71
|
+
guild.channels[option[:value]] || guild.fetch_channels.wait.find { |channel| channel.id == option[:value] }
|
72
|
+
when 8
|
73
|
+
guild.roles[option[:value]] || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
|
74
|
+
when 9
|
75
|
+
guild.members[option[:value]] || guild.roles[option[:value]] || guild.fetch_member(option[:value]).wait || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
|
76
|
+
end
|
77
|
+
option_map[option[:name]] = val
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Represents a user context menu interaction.
|
85
|
+
#
|
86
|
+
class UserMenuCommand < CommandInteraction
|
87
|
+
@command_type = 2
|
88
|
+
|
89
|
+
# @return [Discorb::Member, Discorb::User] The target user.
|
90
|
+
attr_reader :target
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def _set_data(data)
|
95
|
+
@target = guild.members[data[:target_id]] || Discorb::Member.new(@client, @guild_id, data[:resolved][:users][data[:target_id].to_sym], data[:resolved][:members][data[:target_id].to_sym])
|
96
|
+
@client.commands.find { |c| c.name == data[:name] && c.type_raw == 2 }.block.call(self, @target)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
#
|
101
|
+
# Represents a message context menu interaction.
|
102
|
+
#
|
103
|
+
class MessageMenuCommand < CommandInteraction
|
104
|
+
@command_type = 3
|
105
|
+
|
106
|
+
# @return [Discorb::Message] The target message.
|
107
|
+
attr_reader :target
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def _set_data(data)
|
112
|
+
@target = Message.new(@client, data[:resolved][:messages][data[:target_id].to_sym].merge(guild_id: @guild_id.to_s))
|
113
|
+
@client.commands.find { |c| c.name == data[:name] && c.type_raw == 3 }.block.call(self, @target)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def _set_data(data)
|
120
|
+
@name = data[:name]
|
121
|
+
end
|
122
|
+
|
123
|
+
class << self
|
124
|
+
# @private
|
125
|
+
attr_reader :command_type
|
126
|
+
|
127
|
+
# @private
|
128
|
+
def make_interaction(client, data)
|
129
|
+
nested_classes.each do |klass|
|
130
|
+
return klass.new(client, data) if !klass.command_type.nil? && klass.command_type == data[:data][:type]
|
131
|
+
end
|
132
|
+
client.log.warn("Unknown command type #{data[:type]}, initialized CommandInteraction")
|
133
|
+
CommandInteraction.new(client, data)
|
134
|
+
end
|
135
|
+
|
136
|
+
# @private
|
137
|
+
def nested_classes
|
138
|
+
constants.select { |c| const_get(c).is_a? Class }.map { |c| const_get(c) }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Discorb
|
2
|
+
|
3
|
+
#
|
4
|
+
# Represents a message component interaction.
|
5
|
+
# @abstract
|
6
|
+
#
|
7
|
+
class MessageComponentInteraction < Interaction
|
8
|
+
include Interaction::SourceResponse
|
9
|
+
include Interaction::UpdateResponse
|
10
|
+
# @return [String] The content of the response.
|
11
|
+
attr_reader :custom_id
|
12
|
+
# @return [Discorb::Message] The target message.
|
13
|
+
attr_reader :message
|
14
|
+
|
15
|
+
@interaction_type = 3
|
16
|
+
@interaction_name = :message_component
|
17
|
+
|
18
|
+
# @private
|
19
|
+
def initialize(client, data)
|
20
|
+
super
|
21
|
+
@message = Message.new(@client, data[:message].merge({ member: data[:member] }))
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# @private
|
26
|
+
attr_reader :component_type
|
27
|
+
|
28
|
+
# @private
|
29
|
+
def make_interaction(client, data)
|
30
|
+
nested_classes.each do |klass|
|
31
|
+
return klass.new(client, data) if !klass.component_type.nil? && klass.component_type == data[:data][:component_type]
|
32
|
+
end
|
33
|
+
client.log.warn("Unknown component type #{data[:component_type]}, initialized Interaction")
|
34
|
+
MessageComponentInteraction.new(client, data)
|
35
|
+
end
|
36
|
+
|
37
|
+
# @private
|
38
|
+
def nested_classes
|
39
|
+
constants.select { |c| const_get(c).is_a? Class }.map { |c| const_get(c) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Represents a button interaction.
|
45
|
+
#
|
46
|
+
class Button < MessageComponentInteraction
|
47
|
+
@component_type = 2
|
48
|
+
@event_name = :button_click
|
49
|
+
# @return [String] The custom id of the button.
|
50
|
+
attr_reader :custom_id
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def _set_data(data)
|
55
|
+
@custom_id = data[:custom_id]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Represents a select menu interaction.
|
61
|
+
#
|
62
|
+
class SelectMenu < MessageComponentInteraction
|
63
|
+
@component_type = 3
|
64
|
+
@event_name = :select_menu_select
|
65
|
+
# @return [String] The custom id of the select menu.
|
66
|
+
attr_reader :custom_id
|
67
|
+
# @return [Array<String>] The selected options.
|
68
|
+
attr_reader :values
|
69
|
+
|
70
|
+
# @!attribute [r] value
|
71
|
+
# @return [String] The first selected value.
|
72
|
+
|
73
|
+
def value
|
74
|
+
@values[0]
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def _set_data(data)
|
80
|
+
@custom_id = data[:custom_id]
|
81
|
+
@values = data[:values]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
module Discorb
|
2
|
+
class Interaction
|
3
|
+
#
|
4
|
+
# A module for response with source.
|
5
|
+
#
|
6
|
+
module SourceResponse
|
7
|
+
#
|
8
|
+
# Response with `DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE`(`5`).
|
9
|
+
#
|
10
|
+
# @macro async
|
11
|
+
# @macro http
|
12
|
+
#
|
13
|
+
# @param [Boolean] ephemeral Whether to make the response ephemeral.
|
14
|
+
#
|
15
|
+
def defer_source(ephemeral: false)
|
16
|
+
Async do
|
17
|
+
@client.http.post("/interactions/#{@id}/#{@token}/callback", {
|
18
|
+
type: 5,
|
19
|
+
data: {
|
20
|
+
flags: (ephemeral ? 1 << 6 : 0),
|
21
|
+
},
|
22
|
+
}).wait
|
23
|
+
@defered = true
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Response with `CHANNEL_MESSAGE_WITH_SOURCE`(`4`).
|
29
|
+
#
|
30
|
+
# @macro async
|
31
|
+
# @macro http
|
32
|
+
#
|
33
|
+
# @param [String] content The content of the response.
|
34
|
+
# @param [Boolean] tts Whether to send the message as text-to-speech.
|
35
|
+
# @param [Discorb::Embed] embed The embed to send.
|
36
|
+
# @param [Array<Discorb::Embed>] embeds The embeds to send. (max: 10)
|
37
|
+
# @param [Discorb::AllowedMentions] allowed_mentions The allowed mentions to send.
|
38
|
+
# @param [Array<Discorb::Component>, Array<Array<Discorb::Component>>] components The components to send.
|
39
|
+
# @param [Boolean] ephemeral Whether to make the response ephemeral.
|
40
|
+
#
|
41
|
+
# @return [Discorb::Interaction::SourceResponse::CallbackMessage, Discorb::Webhook::Message] The callback message.
|
42
|
+
#
|
43
|
+
def post(content = nil, tts: false, embed: nil, embeds: nil, allowed_mentions: nil, components: nil, ephemeral: false)
|
44
|
+
Async do
|
45
|
+
payload = {}
|
46
|
+
payload[:content] = content if content
|
47
|
+
payload[:tts] = tts
|
48
|
+
payload[:embeds] = (embeds || [embed])&.map { |e| e&.to_hash }.filter { _1 }
|
49
|
+
payload[:allowed_mentions] = allowed_mentions&.to_hash(@client.allowed_mentions) || @client.allowed_mentions.to_hash
|
50
|
+
payload[:components] = Component.to_payload(components) if components
|
51
|
+
payload[:flags] = (ephemeral ? 1 << 6 : 0)
|
52
|
+
|
53
|
+
ret = if @responded
|
54
|
+
_resp, data = @client.http.post("/webhooks/#{@application_id}/#{@token}", payload).wait
|
55
|
+
webhook = Webhook::URLWebhook.new("/webhooks/#{@application_id}/#{@token}")
|
56
|
+
Webhook::Message.new(webhook, data, @client)
|
57
|
+
elsif @defered
|
58
|
+
@client.http.patch("/webhooks/#{@application_id}/#{@token}/messages/@original", payload).wait
|
59
|
+
CallbackMessage.new(@client, payload, @application_id, @token)
|
60
|
+
else
|
61
|
+
@client.http.post("/interactions/#{@id}/#{@token}/callback", { type: 4, data: payload }).wait
|
62
|
+
CallbackMessage.new(@client, payload, @application_id, @token)
|
63
|
+
end
|
64
|
+
@responded = true
|
65
|
+
ret
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
class CallbackMessage
|
70
|
+
# @private
|
71
|
+
def initialize(client, data, application_id, token)
|
72
|
+
@client = client
|
73
|
+
@data = data
|
74
|
+
@application_id = application_id
|
75
|
+
@token = token
|
76
|
+
end
|
77
|
+
|
78
|
+
#
|
79
|
+
# Edits the callback message.
|
80
|
+
# @macro async
|
81
|
+
# @macro http
|
82
|
+
# @macro edit
|
83
|
+
#
|
84
|
+
# @param [String] content The new content of the message.
|
85
|
+
# @param [Discorb::Embed] embed The new embed of the message.
|
86
|
+
# @param [Array<Discorb::Embed>] embeds The new embeds of the message.
|
87
|
+
# @param [Array<Discorb::Attachment>] attachments The attachments to remain.
|
88
|
+
# @param [Discorb::File] file The file to send.
|
89
|
+
# @param [Array<Discorb::File>] files The files to send.
|
90
|
+
#
|
91
|
+
def edit(
|
92
|
+
content = :unset,
|
93
|
+
embed: :unset, embeds: :unset,
|
94
|
+
file: :unset, files: :unset,
|
95
|
+
attachments: :unset
|
96
|
+
)
|
97
|
+
Async do
|
98
|
+
payload = {}
|
99
|
+
payload[:content] = content if content != :unset
|
100
|
+
payload[:embeds] = embed ? [embed.to_hash] : [] if embed != :unset
|
101
|
+
payload[:embeds] = embeds.map(&:to_hash) if embeds != :unset
|
102
|
+
payload[:attachments] = attachments.map(&:to_hash) if attachments != :unset
|
103
|
+
files = [file] if file != :unset
|
104
|
+
if files == :unset
|
105
|
+
headers = {
|
106
|
+
"Content-Type" => "application/json",
|
107
|
+
}
|
108
|
+
else
|
109
|
+
headers, payload = HTTP.multipart(payload, files)
|
110
|
+
end
|
111
|
+
@client.http.patch("/webhooks/#{@application_id}/#{@token}/messages/@original", payload, headers: headers).wait
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
alias modify edit
|
116
|
+
|
117
|
+
#
|
118
|
+
# Deletes the callback message.
|
119
|
+
# @note This will fail if the message is ephemeral.
|
120
|
+
#
|
121
|
+
def delete!
|
122
|
+
Async do
|
123
|
+
@client.http.delete("/webhooks/#{@application_id}/#{@token}/messages/@original").wait
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
#
|
130
|
+
# A module for response with update.
|
131
|
+
#
|
132
|
+
module UpdateResponse
|
133
|
+
#
|
134
|
+
# Response with `DEFERRED_UPDATE_MESSAGE`(`6`).
|
135
|
+
#
|
136
|
+
# @param [Boolean] ephemeral Whether to make the response ephemeral.
|
137
|
+
#
|
138
|
+
def defer_update(ephemeral: false)
|
139
|
+
Async do
|
140
|
+
@client.http.post("/interactions/#{@id}/#{@token}/callback", {
|
141
|
+
type: 6,
|
142
|
+
data: {
|
143
|
+
flags: (ephemeral ? 1 << 6 : 0),
|
144
|
+
},
|
145
|
+
}).wait
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
#
|
150
|
+
# Response with `UPDATE_MESSAGE`(`7`).
|
151
|
+
#
|
152
|
+
# @macro async
|
153
|
+
# @macro http
|
154
|
+
#
|
155
|
+
# @param [String] content The content of the response.
|
156
|
+
# @param [Boolean] tts Whether to send the message as text-to-speech.
|
157
|
+
# @param [Discorb::Embed] embed The embed to send.
|
158
|
+
# @param [Array<Discorb::Embed>] embeds The embeds to send. (max: 10)
|
159
|
+
# @param [Discorb::AllowedMentions] allowed_mentions The allowed mentions to send.
|
160
|
+
# @param [Array<Discorb::Component>, Array<Array<Discorb::Component>>] components The components to send.
|
161
|
+
# @param [Boolean] ephemeral Whether to make the response ephemeral.
|
162
|
+
#
|
163
|
+
def edit(content, tts: false, embed: nil, embeds: nil, allowed_mentions: nil, components: nil, ephemeral: false)
|
164
|
+
Async do
|
165
|
+
payload = {}
|
166
|
+
payload[:content] = content if content
|
167
|
+
payload[:tts] = tts
|
168
|
+
tmp_embed = if embed
|
169
|
+
[embed]
|
170
|
+
elsif embeds
|
171
|
+
embeds
|
172
|
+
end
|
173
|
+
payload[:embeds] = tmp_embed.map(&:to_hash) if tmp_embed
|
174
|
+
payload[:allowed_mentions] = allowed_mentions ? allowed_mentions.to_hash(@client.allowed_mentions) : @client.allowed_mentions.to_hash
|
175
|
+
payload[:components] = Component.to_payload(components) if components
|
176
|
+
payload[:flags] = (ephemeral ? 1 << 6 : 0)
|
177
|
+
@client.http.post("/interactions/#{@id}/#{@token}/callback", { type: 7, data: payload }).wait
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def _set_data(*)
|
185
|
+
nil
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module Discorb
|
2
|
+
#
|
3
|
+
# Represents a user interaction with the bot.
|
4
|
+
#
|
5
|
+
class Interaction < DiscordModel
|
6
|
+
# @return [Discorb::Snowflake] The ID of the interaction.
|
7
|
+
attr_reader :id
|
8
|
+
# @return [Discorb::Snowflake] The ID of the application that created the interaction.
|
9
|
+
attr_reader :application_id
|
10
|
+
# @return [Symbol] The type of interaction.
|
11
|
+
attr_reader :type
|
12
|
+
# @return [Discorb::Member] The member that created the interaction.
|
13
|
+
attr_reader :member
|
14
|
+
# @return [Discorb::User] The user that created the interaction.
|
15
|
+
attr_reader :user
|
16
|
+
# @return [Integer] The type of interaction.
|
17
|
+
# @note This is always `1` for now.
|
18
|
+
attr_reader :version
|
19
|
+
# @return [String] The token for the interaction.
|
20
|
+
attr_reader :token
|
21
|
+
|
22
|
+
# @!attribute [r] guild
|
23
|
+
# @macro client_cache
|
24
|
+
# @return [Discorb::Guild] The guild the interaction took place in.
|
25
|
+
# @!attribute [r] channel
|
26
|
+
# @macro client_cache
|
27
|
+
# @return [Discorb::Channel] The channel the interaction took place in.
|
28
|
+
# @!attribute [r] target
|
29
|
+
# @return [Discorb::User, Discorb::Member] The user or member the interaction took place with.
|
30
|
+
|
31
|
+
@interaction_type = nil
|
32
|
+
@interaction_name = nil
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def initialize(client, data)
|
36
|
+
@client = client
|
37
|
+
@id = Snowflake.new(data[:id])
|
38
|
+
@application_id = Snowflake.new(data[:application_id])
|
39
|
+
@type = self.class.interaction_name
|
40
|
+
@type_id = self.class.interaction_type
|
41
|
+
@guild_id = data[:guild_id] && Snowflake.new(data[:guild_id])
|
42
|
+
@channel_id = data[:channel_id] && Snowflake.new(data[:channel_id])
|
43
|
+
@member = guild.members[data[:member][:id]] || Member.new(@client, @guild_id, data[:member][:user], data[:member]) if data[:member]
|
44
|
+
@user = @client.users[data[:user][:id]] || User.new(@client, data[:user]) if data[:user]
|
45
|
+
@token = data[:token]
|
46
|
+
@version = data[:version]
|
47
|
+
@defered = false
|
48
|
+
@responded = false
|
49
|
+
_set_data(data[:data])
|
50
|
+
end
|
51
|
+
|
52
|
+
def guild
|
53
|
+
@client.guilds[@guild_id]
|
54
|
+
end
|
55
|
+
|
56
|
+
def channel
|
57
|
+
@client.channels[@channel_id]
|
58
|
+
end
|
59
|
+
|
60
|
+
def target
|
61
|
+
@member || @user
|
62
|
+
end
|
63
|
+
|
64
|
+
alias fired_by target
|
65
|
+
alias from target
|
66
|
+
|
67
|
+
def inspect
|
68
|
+
"#<#{self.class} id=#{@id}>"
|
69
|
+
end
|
70
|
+
|
71
|
+
class << self
|
72
|
+
# @private
|
73
|
+
attr_reader :interaction_type, :interaction_name, :event_name
|
74
|
+
|
75
|
+
# @private
|
76
|
+
def make_interaction(client, data)
|
77
|
+
interaction = nil
|
78
|
+
descendants.each do |klass|
|
79
|
+
interaction = klass.make_interaction(client, data) if !klass.interaction_type.nil? && klass.interaction_type == data[:type]
|
80
|
+
end
|
81
|
+
if interaction.nil?
|
82
|
+
client.log.warn("Unknown interaction type #{data[:type]}, initialized Interaction")
|
83
|
+
interaction = Interaction.new(client, data)
|
84
|
+
end
|
85
|
+
interaction
|
86
|
+
end
|
87
|
+
|
88
|
+
# @private
|
89
|
+
def descendants
|
90
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|