discorb 0.11.4 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
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