discorb 0.11.4 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3154cd1b844888bec798cdead182285e335733206758fddea8c2fd8e0015499e
4
- data.tar.gz: 9d0ba163982a7676045dce791409227f2c99c3e28bce29cae25033b85f7485a5
3
+ metadata.gz: d57f5fd6920be7c5549687ec85d3012dede743981482fc806099789d3d9f4048
4
+ data.tar.gz: 513c3e45c0da27f8e509c4ed7238c5f6da3cd39b61a0681874a97d507068ffc6
5
5
  SHA512:
6
- metadata.gz: 20ee7dd354fc5be471617ed61367ceb85bad364cd8e2afd95df9c46453665b61c15d387c2bafb36b295836f5cb02f699edfc3118af4fa6c39e30a52f72904e6d
7
- data.tar.gz: c860a7fa1335a7be40dafa7e9bda5bbfd6a7d7e7790ece875db350b154f3e7ab7f523387b5be4a77f4cae7f43c01b3a726f2c917dd81c2b97502f0097b9b5ecc
6
+ metadata.gz: 7603b6c7017898fbd705ffaa602f370111c6d122a2e68a7b6c017c6fa622d9b6990e684bf574104844e8dc6601176a47894815cfb3756ae16305fe74709da118
7
+ data.tar.gz: 0fa148e0c26c45a8aa0c3a9e2dbdfb4995bc9e939c9588d93b575b711baccc906b20ba3f8edfe4906ae5838565bd0be6befc5cf7e6b2ca25f1f465e21fa6622a
data/Changelog.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## v0.12
6
+
7
+ ### v0.12.0
8
+
9
+ - Refactor: Refactor the code
10
+ - Fix: Fix resuming gateway, finally
11
+ - Fix: Fix `@client` in slash command handler in extension
12
+
5
13
  ## v0.11
6
14
 
7
15
  ### v0.11.4
data/README.md CHANGED
@@ -4,7 +4,8 @@
4
4
  <a href="https://rubygems.org/gems/discorb"><img src="https://img.shields.io/gem/dt/discorb?logo=rubygems&logoColor=fff&label=Downloads&style=flat-square&labelColor=2f3136" alt="Gem"></a>
5
5
  <a href="https://rubygems.org/gems/discorb"><img src="https://img.shields.io/gem/v/discorb?logo=rubygems&logoColor=fff&label=Version&style=flat-square&labelColor=2f3136" alt="Gem"></a>
6
6
  <a href="https://discord.gg/hCP6zq8Vpj"><img src="https://img.shields.io/discord/863581274916913193?logo=discord&logoColor=fff&color=5865f2&label=Discord&style=flat-square&labelColor=2f3136" alt="Discord"></a>
7
- <a href="https://github.com/discorb-lib/discorb"><img src="https://img.shields.io/github/stars/discorb-lib/discorb?color=24292e&label=Stars&logo=GitHub&logoColor=fff&style=flat-square&labelColor=2f3136" alt="GitHub"></a></div>
7
+ <a href="https://github.com/discorb-lib/discorb"><img src="https://img.shields.io/github/stars/discorb-lib/discorb?color=24292e&label=Stars&logo=GitHub&logoColor=fff&style=flat-square&labelColor=2f3136" alt="GitHub"></a>
8
+ <a href="https://codeclimate.com/github/discorb-lib/discorb"><img alt="Code Climate maintainability" src="https://img.shields.io/codeclimate/maintainability/discorb-lib/discorb?logo=Code%20Climate&logoColor=ffffff&style=flat-square&labelColor=2f3136&label=Maintainability"></a></div>
8
9
 
9
10
  ----
10
11
 
@@ -172,6 +172,14 @@ module Discorb
172
172
  @id_map = Discorb::Dictionary.new
173
173
  end
174
174
 
175
+ # @private
176
+ def replace_block(instance)
177
+ current_block = @block.dup
178
+ @block = Proc.new do |*args|
179
+ instance.instance_exec(*args, &current_block)
180
+ end
181
+ end
182
+
175
183
  # @private
176
184
  def to_hash
177
185
  {
@@ -314,6 +322,12 @@ module Discorb
314
322
  @name
315
323
  end
316
324
 
325
+ # @private
326
+ def block_replace(instance)
327
+ super
328
+ @commands.each { |c| c.replace_block(instance) }
329
+ end
330
+
317
331
  # @private
318
332
  def to_hash
319
333
  options_payload = @commands.map do |command|
@@ -395,10 +395,11 @@ module Discorb
395
395
  end
396
396
  end
397
397
  @commands.delete_if do |cmd|
398
- cmd.respond_to? :extension and cmd.extension == ins.name
398
+ cmd.respond_to? :extension and cmd.extension == ins.class.name
399
399
  end
400
400
  ins.class.commands.each do |cmd|
401
- cmd.define_singleton_method(:extension) { ins.name }
401
+ cmd.define_singleton_method(:extension) { ins.class.name }
402
+ cmd.replace_block(ins)
402
403
  @commands << cmd
403
404
  end
404
405
 
@@ -428,41 +429,10 @@ module Discorb
428
429
  when nil
429
430
  start_client(token)
430
431
  when "run"
431
- require "json"
432
- options = JSON.parse(ENV["DISCORB_CLI_OPTIONS"], symbolize_names: true)
433
- @daemon = options[:daemon]
434
-
435
- setup_commands(token) if options[:setup]
436
- if options[:log_level]
437
- if options[:log_level] == "none"
438
- @log.out = nil
439
- else
440
- @log.out = case options[:log_file]
441
- when nil, "stderr"
442
- $stderr
443
- when "stdout"
444
- $stdout
445
- else
446
- ::File.open(options[:log_file], "a")
447
- end
448
- @log.level = options[:log_level].to_sym
449
- @log.colorize_log = options[:log_color] == nil ? @log.out.isatty : options[:log_color]
450
- end
451
- end
432
+ before_run(token)
452
433
  start_client(token)
453
434
  when "setup"
454
- guild_ids = "global"
455
- if guilds = ENV["DISCORB_SETUP_GUILDS"]
456
- guild_ids = guilds.split(",")
457
- end
458
- if guild_ids == ["global"]
459
- guild_ids = false
460
- end
461
- setup_commands(token, guild_ids: guild_ids).wait
462
- @events[:setup]&.each do |event|
463
- event.call
464
- end
465
- self.on_setup if respond_to? :on_setup
435
+ run_setup(token)
466
436
  end
467
437
  end
468
438
 
@@ -478,10 +448,48 @@ module Discorb
478
448
 
479
449
  private
480
450
 
451
+ def before_run(token)
452
+ require "json"
453
+ options = JSON.parse(ENV["DISCORB_CLI_OPTIONS"], symbolize_names: true)
454
+ setup_commands(token) if options[:setup]
455
+ if options[:log_level]
456
+ if options[:log_level] == "none"
457
+ @log.out = nil
458
+ else
459
+ @log.out = case options[:log_file]
460
+ when nil, "stderr"
461
+ $stderr
462
+ when "stdout"
463
+ $stdout
464
+ else
465
+ ::File.open(options[:log_file], "a")
466
+ end
467
+ @log.level = options[:log_level].to_sym
468
+ @log.colorize_log = options[:log_color] == nil ? @log.out.isatty : options[:log_color]
469
+ end
470
+ end
471
+ end
472
+
473
+ def run_setup(token)
474
+ guild_ids = "global"
475
+ if guilds = ENV["DISCORB_SETUP_GUILDS"]
476
+ guild_ids = guilds.split(",")
477
+ end
478
+ if guild_ids == ["global"]
479
+ guild_ids = false
480
+ end
481
+ setup_commands(token, guild_ids: guild_ids).wait
482
+ @events[:setup]&.each do |event|
483
+ event.call
484
+ end
485
+ self.on_setup if respond_to? :on_setup
486
+ end
487
+
481
488
  def start_client(token)
482
489
  Async do |task|
483
- trap(:SIGINT) {
490
+ Signal.trap(:SIGINT) {
484
491
  @log.info "SIGINT received, closing..."
492
+ Signal.trap(:SIGINT, "DEFAULT")
485
493
  close!
486
494
  }
487
495
  @token = token.to_s
@@ -4,7 +4,7 @@ module Discorb
4
4
  # @return [String] The API base URL.
5
5
  API_BASE_URL = "https://discord.com/api/v9"
6
6
  # @return [String] The version of discorb.
7
- VERSION = "0.11.4"
7
+ VERSION = "0.12.0"
8
8
  # @return [String] The user agent for the bot.
9
9
  USER_AGENT = "DiscordBot (https://discorb-lib.github.io #{VERSION}) Ruby/#{RUBY_VERSION}"
10
10
 
data/lib/discorb/error.rb CHANGED
@@ -38,7 +38,8 @@ module Discorb
38
38
  # @abstract
39
39
  #
40
40
  class HTTPError < DiscorbError
41
- # @return [String] the HTTP response code.
41
+ # @return [String] the JSON response code.
42
+ # @see https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes
42
43
  attr_reader :code
43
44
  # @return [Net::HTTPResponse] the HTTP response.
44
45
  attr_reader :response
@@ -47,7 +48,7 @@ module Discorb
47
48
  def initialize(resp, data)
48
49
  @code = data[:code]
49
50
  @response = resp
50
- super(data[:message])
51
+ super(data[:message] + " (#{@code})")
51
52
  end
52
53
  end
53
54
 
@@ -60,9 +61,11 @@ module Discorb
60
61
  @code = data[:code]
61
62
  @response = resp
62
63
  DiscorbError.instance_method(:initialize).bind(self).call(
63
- [data[:message], enumerate_errors(data[:errors]).map do |ek, ev|
64
- "#{ek}=>#{ev}"
65
- end.join("\n")].join("\n")
64
+ [
65
+ data[:message] + " (#{@code})", enumerate_errors(data[:errors])
66
+ .map { |ek, ev| "#{ek}=>#{ev}" }
67
+ .join("\n"),
68
+ ].join("\n")
66
69
  )
67
70
  end
68
71
  end
@@ -482,38 +482,44 @@ module Discorb
482
482
  private
483
483
 
484
484
  def connect_gateway(reconnect)
485
- @log.info "Connecting to gateway."
485
+ if reconnect
486
+ @log.info "Reconnecting to gateway..."
487
+ else
488
+ @log.info "Connecting to gateway..."
489
+ end
486
490
  Async do
491
+ @connection&.close
487
492
  @http = HTTP.new(self)
488
493
  _, gateway_response = @http.get("/gateway").wait
489
494
  gateway_url = gateway_response[:url]
490
495
  endpoint = Async::HTTP::Endpoint.parse("#{gateway_url}?v=9&encoding=json&compress=zlib-stream",
491
496
  alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
492
497
  begin
493
- Async::WebSocket::Client.connect(endpoint, headers: [["User-Agent", Discorb::USER_AGENT]], handler: RawConnection) do |connection|
494
- @connection = connection
495
- @zlib_stream = Zlib::Inflate.new(Zlib::MAX_WBITS)
496
- @buffer = +""
497
- begin
498
- while (message = @connection.read)
499
- @buffer << message
500
- if message.end_with?((+"\x00\x00\xff\xff").force_encoding("ASCII-8BIT"))
501
- begin
502
- data = @zlib_stream.inflate(@buffer)
503
- @buffer = +""
504
- message = JSON.parse(data, symbolize_names: true)
505
- rescue JSON::ParserError
506
- @buffer = +""
507
- @log.error "Received invalid JSON from gateway."
508
- @log.debug "#{data}"
509
- else
510
- handle_gateway(message, reconnect)
511
- end
498
+ @connection = Async::WebSocket::Client.connect(endpoint, headers: [["User-Agent", Discorb::USER_AGENT]], handler: RawConnection)
499
+ @zlib_stream = Zlib::Inflate.new(Zlib::MAX_WBITS)
500
+ buffer = +""
501
+ begin
502
+ while (message = @connection.read)
503
+ buffer << message
504
+ if message.end_with?((+"\x00\x00\xff\xff").force_encoding("ASCII-8BIT"))
505
+ begin
506
+ data = @zlib_stream.inflate(buffer)
507
+ buffer = +""
508
+ message = JSON.parse(data, symbolize_names: true)
509
+ rescue JSON::ParserError
510
+ buffer = +""
511
+ @log.error "Received invalid JSON from gateway."
512
+ @log.debug "#{data}"
513
+ else
514
+ handle_gateway(message, reconnect)
512
515
  end
513
516
  end
514
- rescue EOFError, Async::Wrapper::Cancelled, Async::Wrapper::WaitError
515
- # Ignore
516
517
  end
518
+ rescue Async::Wrapper::Cancelled, OpenSSL::SSL::SSLError, Async::Wrapper::WaitError, EOFError => e
519
+ @log.error "Gateway connection closed: #{e.class}: #{e.message}"
520
+ connect_gateway(true)
521
+ else # should never happen
522
+ connect_gateway(true)
517
523
  end
518
524
  rescue Protocol::WebSocket::ClosedError => e
519
525
  @tasks.map(&:stop)
@@ -542,7 +548,7 @@ module Discorb
542
548
  connect_gateway(false)
543
549
  end
544
550
  rescue => e
545
- @log.error "Discord WebSocket error: #{e.message}"
551
+ @log.error "Discord WebSocket error: #{e.full_message}"
546
552
  connect_gateway(false)
547
553
  end
548
554
  end
@@ -1034,6 +1040,7 @@ module Discorb
1034
1040
  dispatch(interaction.class.event_name, interaction)
1035
1041
  when "RESUMED"
1036
1042
  @log.info("Successfully resumed connection")
1043
+ @tasks << handle_heartbeat
1037
1044
  dispatch(:resumed)
1038
1045
  else
1039
1046
  if respond_to?("event_" + event_name.downcase)
@@ -0,0 +1,49 @@
1
+ module Discorb
2
+ #
3
+ # Represents auto complete interaction.
4
+ #
5
+ class AutoComplete < Interaction
6
+ @interaction_type = 4
7
+ @interaction_name = :auto_complete
8
+
9
+ # @private
10
+ def _set_data(data)
11
+ super
12
+ Sync do
13
+ name, options = Discorb::CommandInteraction::SlashCommand.get_command_data(data)
14
+
15
+ unless (command = @client.bottom_commands.find { |c| c.to_s == name && c.type_raw == 1 })
16
+ @client.log.warn "Unknown command name #{name}, ignoring"
17
+ next
18
+ end
19
+
20
+ option_map = command.options.map { |k, v| [k.to_s, v[:default]] }.to_h
21
+ Discorb::CommandInteraction::SlashCommand.modify_option_map(option_map, options)
22
+ focused_index = options.find_index { |o| o[:focused] }
23
+ val = command.options.values[focused_index][:autocomplete]&.call(self, *command.options.map { |k, v| option_map[k.to_s] })
24
+ send_complete_result(val)
25
+ end
26
+ end
27
+
28
+ # @private
29
+ def send_complete_result(val)
30
+ @client.http.post("/interactions/#{@id}/#{@token}/callback", {
31
+ type: 8,
32
+ data: {
33
+ choices: val.map do |vk, vv|
34
+ {
35
+ name: vk,
36
+ value: vv,
37
+ }
38
+ end,
39
+ },
40
+ }).wait
41
+ rescue Discorb::NotFoundError
42
+ @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
43
+ end
44
+
45
+ class << self
46
+ alias make_interaction new
47
+ end
48
+ end
49
+ end
@@ -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