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.
@@ -1,617 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
- module Discorb
4
- #
5
- # Represents a user interaction with the bot.
6
- #
7
- class Interaction < DiscordModel
8
- # @return [Discorb::Snowflake] The ID of the interaction.
9
- attr_reader :id
10
- # @return [Discorb::Snowflake] The ID of the application that created the interaction.
11
- attr_reader :application_id
12
- # @return [Symbol] The type of interaction.
13
- attr_reader :type
14
- # @return [Discorb::Member] The member that created the interaction.
15
- attr_reader :member
16
- # @return [Discorb::User] The user that created the interaction.
17
- attr_reader :user
18
- # @return [Integer] The type of interaction.
19
- # @note This is always `1` for now.
20
- attr_reader :version
21
- # @return [String] The token for the interaction.
22
- attr_reader :token
23
-
24
- # @!attribute [r] guild
25
- # @macro client_cache
26
- # @return [Discorb::Guild] The guild the interaction took place in.
27
- # @!attribute [r] channel
28
- # @macro client_cache
29
- # @return [Discorb::Channel] The channel the interaction took place in.
30
- # @!attribute [r] target
31
- # @return [Discorb::User, Discorb::Member] The user or member the interaction took place with.
32
-
33
- @interaction_type = nil
34
- @interaction_name = nil
35
-
36
- # @private
37
- def initialize(client, data)
38
- @client = client
39
- @id = Snowflake.new(data[:id])
40
- @application_id = Snowflake.new(data[:application_id])
41
- @type = self.class.interaction_name
42
- @type_id = self.class.interaction_type
43
- @guild_id = data[:guild_id] && Snowflake.new(data[:guild_id])
44
- @channel_id = data[:channel_id] && Snowflake.new(data[:channel_id])
45
- @member = guild.members[data[:member][:id]] || Member.new(@client, @guild_id, data[:member][:user], data[:member]) if data[:member]
46
- @user = @client.users[data[:user][:id]] || User.new(@client, data[:user]) if data[:user]
47
- @token = data[:token]
48
- @version = data[:version]
49
- @defered = false
50
- @responded = false
51
- _set_data(data[:data])
52
- end
53
-
54
- def guild
55
- @client.guilds[@guild_id]
56
- end
57
-
58
- def channel
59
- @client.channels[@channel_id]
60
- end
61
-
62
- def target
63
- @member || @user
64
- end
65
-
66
- alias fired_by target
67
- alias from target
68
-
69
- def inspect
70
- "#<#{self.class} id=#{@id}>"
71
- end
72
-
73
- class << self
74
- # @private
75
- attr_reader :interaction_type, :interaction_name, :event_name
76
-
77
- # @private
78
- def make_interaction(client, data)
79
- interaction = nil
80
- descendants.each do |klass|
81
- interaction = klass.make_interaction(client, data) if !klass.interaction_type.nil? && klass.interaction_type == data[:type]
82
- end
83
- if interaction.nil?
84
- client.log.warn("Unknown interaction type #{data[:type]}, initialized Interaction")
85
- interaction = Interaction.new(client, data)
86
- end
87
- interaction
88
- end
89
-
90
- # @private
91
- def descendants
92
- ObjectSpace.each_object(Class).select { |klass| klass < self }
93
- end
94
- end
95
-
96
- #
97
- # A module for response with source.
98
- #
99
- module SourceResponse
100
- #
101
- # Response with `DEFERRED_CHANNEL_MESSAGE_WITH_SOURCE`(`5`).
102
- #
103
- # @macro async
104
- # @macro http
105
- #
106
- # @param [Boolean] ephemeral Whether to make the response ephemeral.
107
- #
108
- def defer_source(ephemeral: false)
109
- Async do
110
- @client.http.post("/interactions/#{@id}/#{@token}/callback", {
111
- type: 5,
112
- data: {
113
- flags: (ephemeral ? 1 << 6 : 0),
114
- },
115
- }).wait
116
- @defered = true
117
- end
118
- end
119
-
120
- #
121
- # Response with `CHANNEL_MESSAGE_WITH_SOURCE`(`4`).
122
- #
123
- # @macro async
124
- # @macro http
125
- #
126
- # @param [String] content The content of the response.
127
- # @param [Boolean] tts Whether to send the message as text-to-speech.
128
- # @param [Discorb::Embed] embed The embed to send.
129
- # @param [Array<Discorb::Embed>] embeds The embeds to send. (max: 10)
130
- # @param [Discorb::AllowedMentions] allowed_mentions The allowed mentions to send.
131
- # @param [Array<Discorb::Component>, Array<Array<Discorb::Component>>] components The components to send.
132
- # @param [Boolean] ephemeral Whether to make the response ephemeral.
133
- #
134
- # @return [Discorb::Interaction::SourceResponse::CallbackMessage, Discorb::Webhook::Message] The callback message.
135
- #
136
- def post(content = nil, tts: false, embed: nil, embeds: nil, allowed_mentions: nil, components: nil, ephemeral: false)
137
- Async do
138
- payload = {}
139
- payload[:content] = content if content
140
- payload[:tts] = tts
141
- tmp_embed = if embed
142
- [embed]
143
- elsif embeds
144
- embeds
145
- end
146
- payload[:embeds] = tmp_embed.map(&:to_hash) if tmp_embed
147
- payload[:allowed_mentions] = allowed_mentions ? allowed_mentions.to_hash(@client.allowed_mentions) : @client.allowed_mentions.to_hash
148
- if components
149
- tmp_components = []
150
- tmp_row = []
151
- components.each do |c|
152
- case c
153
- when Array
154
- tmp_components << tmp_row
155
- tmp_row = []
156
- tmp_components << c
157
- when SelectMenu
158
- tmp_components << tmp_row
159
- tmp_row = []
160
- tmp_components << [c]
161
- else
162
- tmp_row << c
163
- end
164
- end
165
- tmp_components << tmp_row
166
- payload[:components] = tmp_components.filter { |c| c.length.positive? }.map { |c| { type: 1, components: c.map(&:to_hash) } }
167
- end
168
- payload[:flags] = (ephemeral ? 1 << 6 : 0)
169
-
170
- ret = if @responded
171
- _resp, data = @client.http.post("/webhooks/#{@application_id}/#{@token}", payload).wait
172
- webhook = Webhook::URLWebhook.new("/webhooks/#{@application_id}/#{@token}")
173
- Webhook::Message.new(webhook, data, @client)
174
- elsif @defered
175
- @client.http.patch("/webhooks/#{@application_id}/#{@token}/messages/@original", payload).wait
176
- CallbackMessage.new(@client, payload, @application_id, @token)
177
- else
178
- @client.http.post("/interactions/#{@id}/#{@token}/callback", { type: 4, data: payload }).wait
179
- CallbackMessage.new(@client, payload, @application_id, @token)
180
- end
181
- @responded = true
182
- ret
183
- end
184
- end
185
-
186
- class CallbackMessage
187
- # @private
188
- def initialize(client, data, application_id, token)
189
- @client = client
190
- @data = data
191
- @application_id = application_id
192
- @token = token
193
- end
194
-
195
- #
196
- # Edits the callback message.
197
- # @macro async
198
- # @macro http
199
- # @macro edit
200
- #
201
- # @param [String] content The new content of the message.
202
- # @param [Discorb::Embed] embed The new embed of the message.
203
- # @param [Array<Discorb::Embed>] embeds The new embeds of the message.
204
- # @param [Array<Discorb::Attachment>] attachments The attachments to remain.
205
- # @param [Discorb::File] file The file to send.
206
- # @param [Array<Discorb::File>] files The files to send.
207
- #
208
- def edit(
209
- content = :unset,
210
- embed: :unset, embeds: :unset,
211
- file: :unset, files: :unset,
212
- attachments: :unset
213
- )
214
- Async do
215
- payload = {}
216
- payload[:content] = content if content != :unset
217
- payload[:embeds] = embed ? [embed.to_hash] : [] if embed != :unset
218
- payload[:embeds] = embeds.map(&:to_hash) if embeds != :unset
219
- payload[:attachments] = attachments.map(&:to_hash) if attachments != :unset
220
- files = [file] if file != :unset
221
- if files == :unset
222
- headers = {
223
- "Content-Type" => "application/json",
224
- }
225
- else
226
- headers, payload = HTTP.multipart(payload, files)
227
- end
228
- @client.http.patch("/webhooks/#{@application_id}/#{@token}/messages/@original", payload, headers: headers).wait
229
- end
230
- end
231
-
232
- alias modify edit
233
-
234
- #
235
- # Deletes the callback message.
236
- # @note This will fail if the message is ephemeral.
237
- #
238
- def delete!
239
- Async do
240
- @client.http.delete("/webhooks/#{@application_id}/#{@token}/messages/@original").wait
241
- end
242
- end
243
- end
244
- end
245
-
246
- #
247
- # A module for response with update.
248
- #
249
- module UpdateResponse
250
- #
251
- # Response with `DEFERRED_UPDATE_MESSAGE`(`6`).
252
- #
253
- # @param [Boolean] ephemeral Whether to make the response ephemeral.
254
- #
255
- def defer_update(ephemeral: false)
256
- Async do
257
- @client.http.post("/interactions/#{@id}/#{@token}/callback", {
258
- type: 6,
259
- data: {
260
- flags: (ephemeral ? 1 << 6 : 0),
261
- },
262
- }).wait
263
- end
264
- end
265
-
266
- #
267
- # Response with `UPDATE_MESSAGE`(`7`).
268
- #
269
- # @macro async
270
- # @macro http
271
- #
272
- # @param [String] content The content of the response.
273
- # @param [Boolean] tts Whether to send the message as text-to-speech.
274
- # @param [Discorb::Embed] embed The embed to send.
275
- # @param [Array<Discorb::Embed>] embeds The embeds to send. (max: 10)
276
- # @param [Discorb::AllowedMentions] allowed_mentions The allowed mentions to send.
277
- # @param [Array<Discorb::Component>, Array<Array<Discorb::Component>>] components The components to send.
278
- # @param [Boolean] ephemeral Whether to make the response ephemeral.
279
- #
280
- def edit(content, tts: false, embed: nil, embeds: nil, allowed_mentions: nil, components: nil, ephemeral: false)
281
- Async do
282
- payload = {}
283
- payload[:content] = content if content
284
- payload[:tts] = tts
285
- tmp_embed = if embed
286
- [embed]
287
- elsif embeds
288
- embeds
289
- end
290
- payload[:embeds] = tmp_embed.map(&:to_hash) if tmp_embed
291
- payload[:allowed_mentions] = allowed_mentions ? allowed_mentions.to_hash(@client.allowed_mentions) : @client.allowed_mentions.to_hash
292
- if components
293
- tmp_components = []
294
- tmp_row = []
295
- components.each do |c|
296
- case c
297
- when Array
298
- tmp_components << tmp_row
299
- tmp_row = []
300
- tmp_components << c
301
- when SelectMenu
302
- tmp_components << tmp_row
303
- tmp_row = []
304
- tmp_components << [c]
305
- else
306
- tmp_row << c
307
- end
308
- end
309
- tmp_components << tmp_row
310
- payload[:components] = tmp_components.filter { |c| c.length.positive? }.map { |c| { type: 1, components: c.map(&:to_hash) } }
311
- end
312
- payload[:flags] = (ephemeral ? 1 << 6 : 0)
313
- @client.http.post("/interactions/#{@id}/#{@token}/callback", { type: 7, data: payload }).wait
314
- end
315
- end
316
- end
317
-
318
- private
319
-
320
- def _set_data(*)
321
- nil
322
- end
323
- end
324
-
325
- #
326
- # Represents a command interaction.
327
- #
328
- class CommandInteraction < Interaction
329
- @interaction_type = 2
330
- @interaction_name = :application_command
331
- include Interaction::SourceResponse
332
-
333
- #
334
- # Represents a slash command interaction.
335
- #
336
- class SlashCommand < CommandInteraction
337
- @command_type = 1
338
-
339
- private
340
-
341
- def _set_data(data)
342
- super
343
- Sync do
344
- name = data[:name]
345
- options = nil
346
- if (option = data[:options]&.first)
347
- case option[:type]
348
- when 1
349
- name += " #{option[:name]}"
350
- options = option[:options]
351
- when 2
352
- name += " #{option[:name]}"
353
- if (option_sub = option[:options]&.first)
354
- if option_sub[:type] == 1
355
- name += " #{option_sub[:name]}"
356
- options = option_sub[:options]
357
- else
358
- options = option[:options]
359
- end
360
- end
361
- else
362
- options = data[:options]
363
- end
364
- end
365
-
366
- unless (command = @client.bottom_commands.find { |c| c.to_s == name && c.type_raw == 1 })
367
- @client.log.warn "Unknown command name #{name}, ignoring"
368
- next
369
- end
370
-
371
- option_map = command.options.map { |k, v| [k.to_s, v[:default]] }.to_h
372
- options ||= []
373
- options.each do |option|
374
- val = case option[:type]
375
- when 3, 4, 5, 10
376
- option[:value]
377
- when 6
378
- guild.members[option[:value]] || guild.fetch_member(option[:value]).wait
379
- when 7
380
- guild.channels[option[:value]] || guild.fetch_channels.wait.find { |channel| channel.id == option[:value] }
381
- when 8
382
- guild.roles[option[:value]] || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
383
- when 9
384
- guild.members[option[:value]] || guild.roles[option[:value]] || guild.fetch_member(option[:value]).wait || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
385
- end
386
- option_map[option[:name]] = val
387
- end
388
-
389
- command.block.call(self, *command.options.map { |k, v| option_map[k.to_s] })
390
- end
391
- end
392
- end
393
-
394
- #
395
- # Represents a user context menu interaction.
396
- #
397
- class UserMenuCommand < CommandInteraction
398
- @command_type = 2
399
-
400
- # @return [Discorb::Member, Discorb::User] The target user.
401
- attr_reader :target
402
-
403
- private
404
-
405
- def _set_data(data)
406
- @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])
407
- @client.commands.find { |c| c.name == data[:name] && c.type_raw == 2 }.block.call(self, @target)
408
- end
409
- end
410
-
411
- #
412
- # Represents a message context menu interaction.
413
- #
414
- class MessageMenuCommand < CommandInteraction
415
- @command_type = 3
416
-
417
- # @return [Discorb::Message] The target message.
418
- attr_reader :target
419
-
420
- private
421
-
422
- def _set_data(data)
423
- @target = Message.new(@client, data[:resolved][:messages][data[:target_id].to_sym].merge(guild_id: @guild_id.to_s))
424
- @client.commands.find { |c| c.name == data[:name] && c.type_raw == 3 }.block.call(self, @target)
425
- end
426
- end
427
-
428
- private
429
-
430
- def _set_data(data)
431
- @name = data[:name]
432
- end
433
-
434
- class << self
435
- # @private
436
- attr_reader :command_type
437
-
438
- # @private
439
- def make_interaction(client, data)
440
- nested_classes.each do |klass|
441
- return klass.new(client, data) if !klass.command_type.nil? && klass.command_type == data[:data][:type]
442
- end
443
- client.log.warn("Unknown command type #{data[:type]}, initialized CommandInteraction")
444
- CommandInteraction.new(client, data)
445
- end
446
-
447
- # @private
448
- def nested_classes
449
- constants.select { |c| const_get(c).is_a? Class }.map { |c| const_get(c) }
450
- end
451
- end
452
- end
453
-
454
- #
455
- # Represents auto complete interaction.
456
- #
457
- class AutoComplete < Interaction
458
- @interaction_type = 4
459
- @interaction_name = :auto_complete
460
-
461
- def _set_data(data)
462
- super
463
- Sync do
464
- name = data[:name]
465
- options = nil
466
- if (option = data[:options]&.first)
467
- case option[:type]
468
- when 1
469
- name += " #{option[:name]}"
470
- options = option[:options]
471
- when 2
472
- name += " #{option[:name]}"
473
- if (option_sub = option[:options]&.first)
474
- if option_sub[:type] == 1
475
- name += " #{option_sub[:name]}"
476
- options = option_sub[:options]
477
- else
478
- options = option[:options]
479
- end
480
- end
481
- else
482
- options = data[:options]
483
- end
484
- end
485
-
486
- unless (command = @client.bottom_commands.find { |c| c.to_s == name && c.type_raw == 1 })
487
- @client.log.warn "Unknown command name #{name}, ignoring"
488
- next
489
- end
490
-
491
- option_map = command.options.map { |k, v| [k.to_s, v[:default]] }.to_h
492
- options ||= []
493
- options.each_with_index do |option|
494
- val = case option[:type]
495
- when 3, 4, 5, 10
496
- option[:value]
497
- when 6
498
- guild.members[option[:value]] || guild.fetch_member(option[:value]).wait
499
- when 7
500
- guild.channels[option[:value]] || guild.fetch_channels.wait.find { |channel| channel.id == option[:value] }
501
- when 8
502
- guild.roles[option[:value]] || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
503
- when 9
504
- guild.members[option[:value]] || guild.roles[option[:value]] || guild.fetch_member(option[:value]).wait || guild.fetch_roles.wait.find { |role| role.id == option[:value] }
505
- end
506
- option_map[option[:name]] = val
507
- end
508
- focused_index = options.find_index { |o| o[:focused] }
509
- val = command.options.values[focused_index][:autocomplete]&.call(self, *command.options.map { |k, v| option_map[k.to_s] })
510
- send_complete_result(val)
511
- end
512
- end
513
-
514
- def send_complete_result(val)
515
- @client.http.post("/interactions/#{@id}/#{@token}/callback", {
516
- type: 8,
517
- data: {
518
- choices: val.map do |vk, vv|
519
- {
520
- name: vk,
521
- value: vv,
522
- }
523
- end,
524
- },
525
- }).wait
526
- rescue Discorb::NotFoundError
527
- @client.log.warn "Failed to send auto complete result, This may be caused by the suggestion is taking too long (over 3 seconds) to respond", fallback: $stderr
528
- end
529
-
530
- class << self
531
- alias make_interaction new
532
- end
533
- end
534
-
535
- #
536
- # Represents a message component interaction.
537
- # @abstract
538
- #
539
- class MessageComponentInteraction < Interaction
540
- include Interaction::SourceResponse
541
- include Interaction::UpdateResponse
542
- # @return [String] The content of the response.
543
- attr_reader :custom_id
544
- # @return [Discorb::Message] The target message.
545
- attr_reader :message
546
-
547
- @interaction_type = 3
548
- @interaction_name = :message_component
549
-
550
- # @private
551
- def initialize(client, data)
552
- super
553
- @message = Message.new(@client, data[:message].merge({ member: data[:member] }))
554
- end
555
-
556
- class << self
557
- # @private
558
- attr_reader :component_type
559
-
560
- # @private
561
- def make_interaction(client, data)
562
- nested_classes.each do |klass|
563
- return klass.new(client, data) if !klass.component_type.nil? && klass.component_type == data[:data][:component_type]
564
- end
565
- client.log.warn("Unknown component type #{data[:component_type]}, initialized Interaction")
566
- MessageComponentInteraction.new(client, data)
567
- end
568
-
569
- # @private
570
- def nested_classes
571
- constants.select { |c| const_get(c).is_a? Class }.map { |c| const_get(c) }
572
- end
573
- end
574
-
575
- #
576
- # Represents a button interaction.
577
- #
578
- class Button < MessageComponentInteraction
579
- @component_type = 2
580
- @event_name = :button_click
581
- # @return [String] The custom id of the button.
582
- attr_reader :custom_id
583
-
584
- private
585
-
586
- def _set_data(data)
587
- @custom_id = data[:custom_id]
588
- end
589
- end
590
-
591
- #
592
- # Represents a select menu interaction.
593
- #
594
- class SelectMenu < MessageComponentInteraction
595
- @component_type = 3
596
- @event_name = :select_menu_select
597
- # @return [String] The custom id of the select menu.
598
- attr_reader :custom_id
599
- # @return [Array<String>] The selected options.
600
- attr_reader :values
601
-
602
- # @!attribute [r] value
603
- # @return [String] The first selected value.
604
-
605
- def value
606
- @values[0]
607
- end
608
-
609
- private
610
-
611
- def _set_data(data)
612
- @custom_id = data[:custom_id]
613
- @values = data[:values]
614
- end
615
- end
616
- end
1
+ %w[root response command components autocomplete].each do |file|
2
+ require_relative "interaction/#{file}.rb"
617
3
  end
data/lib/discorb/log.rb CHANGED
@@ -65,10 +65,11 @@ module Discorb
65
65
  return
66
66
  end
67
67
 
68
+ time = Time.now.iso8601
68
69
  if @colorize_log
69
- @out.puts("\e[2;90m[#{Time.now.iso8601}] #{color}#{name}\e[m -- #{message}")
70
+ @out.puts("\e[90m#{time}\e[0m #{color}#{name.ljust(5)}\e[0m #{message}")
70
71
  else
71
- @out.puts("[#{Time.now.iso8601}] #{name} -- #{message}")
72
+ @out.puts("#{time} #{name.ljust(5)} #{message}")
72
73
  end
73
74
  end
74
75
  end
@@ -455,7 +455,9 @@ module Discorb
455
455
  # @macro async
456
456
  # @macro http
457
457
  #
458
- def unpin
458
+ # @param [String] reason The reason for unpinning the message.
459
+ #
460
+ def unpin(reason: nil)
459
461
  Async do
460
462
  channel.unpin_message(self, reason: reason).wait
461
463
  end
@@ -142,6 +142,48 @@ module Discorb
142
142
  end
143
143
  end
144
144
 
145
+ #
146
+ # Fetch the pinned messages in the channel.
147
+ # @macro async
148
+ # @macro http
149
+ #
150
+ # @return [Async::Task<Array<Discorb::Message>>] The pinned messages in the channel.
151
+ #
152
+ def fetch_pins
153
+ Async do
154
+ _resp, data = @client.http.get("/channels/#{channel_id.wait}/pins").wait
155
+ data.map { |pin| Message.new(@client, pin) }
156
+ end
157
+ end
158
+
159
+ #
160
+ # Pin a message in the channel.
161
+ # @macro async
162
+ # @macro http
163
+ #
164
+ # @param [Discorb::Message] message The message to pin.
165
+ # @param [String] reason The reason of pinning the message.
166
+ #
167
+ def pin_message(message, reason: nil)
168
+ Async do
169
+ @client.http.put("/channels/#{channel_id.wait}/pins/#{message.id}", {}, audit_log_reason: reason).wait
170
+ end
171
+ end
172
+
173
+ #
174
+ # Unpin a message in the channel.
175
+ # @macro async
176
+ # @macro http
177
+ #
178
+ # @param [Discorb::Message] message The message to unpin.
179
+ # @param [String] reason The reason of unpinning the message.
180
+ #
181
+ def unpin_message(message, reason: nil)
182
+ Async do
183
+ @client.http.delete("/channels/#{channel_id.wait}/pins/#{message.id}", audit_log_reason: reason).wait
184
+ end
185
+ end
186
+
145
187
  #
146
188
  # Trigger the typing indicator in the channel.
147
189
  # @macro async
@@ -24,7 +24,7 @@ module Discorb
24
24
  return if path.start_with?("https://")
25
25
 
26
26
  if @global
27
- time = b[:reset_at] - Time.now.to_i
27
+ time = b[:reset_at] - Time.now.to_f
28
28
  @client.log.info("global rate limit reached, waiting #{time} seconds")
29
29
  sleep(time)
30
30
  @global = false
@@ -54,7 +54,7 @@ module Discorb
54
54
  #
55
55
  def save(method, path, resp)
56
56
  if resp["X-Ratelimit-Global"] == "true"
57
- @global = Time.now.to_i + JSON.parse(resp.body, symbolize_names: true)[:retry_after]
57
+ @global = Time.now.to_f + JSON.parse(resp.body, symbolize_names: true)[:retry_after]
58
58
  end
59
59
  return unless resp["X-RateLimit-Remaining"]
60
60