discorb 0.7.3 → 0.8.2

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.
@@ -9,7 +9,7 @@ def convert_role(guild, string)
9
9
  end
10
10
  end
11
11
 
12
- client.once :ready do
12
+ client.once :standby do
13
13
  puts "Logged in as #{client.user}"
14
14
  end
15
15
 
@@ -2,7 +2,7 @@ require "discorb"
2
2
 
3
3
  client = Discorb::Client.new
4
4
 
5
- client.once :ready do
5
+ client.once :standby do
6
6
  puts "Logged in as #{client.user}"
7
7
  end
8
8
 
@@ -5,6 +5,7 @@ require "logger"
5
5
 
6
6
  require "async"
7
7
  require "async/websocket/client"
8
+ require_relative "./utils/colored_puts"
8
9
 
9
10
  module Discorb
10
11
  #
@@ -64,11 +65,12 @@ module Discorb
64
65
  # @param [Boolean] colorize_log Whether to colorize the log.
65
66
  # @param [:debug, :info, :warn, :error, :critical] log_level The log level.
66
67
  # @param [Boolean] wait_until_ready Whether to delay event dispatch until ready.
68
+ # @param [Boolean] fetch_member Whether to fetch member on ready. This may slow down the client. Default to `false`.
67
69
  #
68
70
  def initialize(
69
71
  allowed_mentions: nil, intents: nil, message_caches: 1000,
70
72
  log: nil, colorize_log: false, log_level: :info,
71
- wait_until_ready: true
73
+ wait_until_ready: true, fetch_member: false
72
74
  )
73
75
  @allowed_mentions = allowed_mentions || AllowedMentions.new(everyone: true, roles: true, users: true)
74
76
  @intents = (intents or Intents.default)
@@ -91,6 +93,7 @@ module Discorb
91
93
  @commands = []
92
94
  @bottom_commands = []
93
95
  @status = :initialized
96
+ @fetch_member = fetch_member
94
97
  set_default_events
95
98
  end
96
99
 
@@ -299,9 +302,15 @@ module Discorb
299
302
  #
300
303
  # @param [Discorb::Activity] activity The activity to update.
301
304
  # @param [:online, :idle, :dnd, :invisible] status The status to update.
302
- #
303
- def update_presence(activity = nil, status: nil)
304
- payload = {}
305
+ # @param [String] afk Whether to set the client as AFK.
306
+ #
307
+ def update_presence(activity = nil, status: nil, afk: false)
308
+ payload = {
309
+ activities: [],
310
+ status: status,
311
+ afk: nil,
312
+ since: nil,
313
+ }
305
314
  if !activity.nil?
306
315
  payload[:activities] = [activity.to_hash]
307
316
  end
@@ -405,7 +414,8 @@ module Discorb
405
414
  when "run"
406
415
  require "json"
407
416
  options = JSON.parse(ENV["DISCORB_CLI_OPTIONS"], symbolize_names: true)
408
- Process.daemon if options[:daemon]
417
+ @daemon = options[:daemon]
418
+
409
419
  setup_commands(token) if options[:setup]
410
420
  if options[:log_level]
411
421
  if options[:log_level] == "none"
@@ -483,6 +493,18 @@ module Discorb
483
493
  message = "An error occurred while dispatching #{event_name}:\n#{e.full_message}"
484
494
  @log.error message, fallback: $stderr
485
495
  end
496
+
497
+ once :standby do
498
+ if @daemon
499
+ title = "discorb: #{@user}"
500
+ Process.setproctitle title
501
+ sputs "Your discorb client is now in standby mode."
502
+ iputs "Process ID: #{Process.pid}"
503
+ iputs "Title: #{title}"
504
+
505
+ Process.daemon
506
+ end
507
+ end
486
508
  end
487
509
  end
488
510
  end
@@ -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.7.3"
7
+ VERSION = "0.8.2"
8
8
  # @return [String] The user agent for the bot.
9
9
  USER_AGENT = "DiscordBot (https://github.com/discorb-lib/discorb #{VERSION}) Ruby/#{RUBY_VERSION}"
10
10
 
data/lib/discorb/error.rb CHANGED
@@ -92,10 +92,12 @@ module Discorb
92
92
  def initialize(resp, client)
93
93
  @client = client
94
94
  @client.close!
95
- DiscorbError.instance_method(:initialize).bind(self).call(<<~MESSAGE)
95
+ message = <<~MESSAGE
96
96
  The client is banned from CloudFlare.
97
97
  Hint: Try to decrease the number of requests per second, e.g. Use sleep in between requests.
98
98
  MESSAGE
99
+ $stderr.puts message
100
+ DiscorbError.instance_method(:initialize).bind(self).call(message)
99
101
  end
100
102
  end
101
103
 
@@ -11,5 +11,5 @@ informations = {
11
11
  }
12
12
 
13
13
  informations.each do |key, value|
14
- puts "\e[90m#{key}:\e[m #{value}"
14
+ puts "\e[90m#{key.rjust(informations.keys.map(&:size).max)}:\e[m #{value}"
15
15
  end
@@ -16,7 +16,7 @@ FILES = {
16
16
 
17
17
  client = Discorb::Client.new # Create client for connecting to Discord
18
18
 
19
- client.once :ready do
19
+ client.once :standby do
20
20
  puts "Logged in as #{client.user}" # Prints username of logged in user
21
21
  end
22
22
 
@@ -23,7 +23,7 @@ opt.parse!(ARGV)
23
23
  client = Discorb::Client.new(intents: Discorb::Intents.from_value(intents_value))
24
24
  $messages = []
25
25
 
26
- client.on :ready do
26
+ client.on :standby do
27
27
  puts "\e[96mLogged in as #{client.user}\e[m"
28
28
 
29
29
  def message
data/lib/discorb/file.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "mime/types"
4
+ require "stringio"
4
5
 
5
6
  module Discorb
6
7
  #
@@ -62,8 +63,13 @@ module Discorb
62
63
  def initialize(io, filename = nil, content_type: nil)
63
64
  @io = io
64
65
  @filename = filename || (io.respond_to?(:path) ? io.path : io.object_id)
65
- @content_type = content_type || MIME::Types.type_for(@filename)[0].to_s
66
+ @content_type = content_type || MIME::Types.type_for(@filename.to_s)[0].to_s
66
67
  @content_type = "application/octet-stream" if @content_type == ""
67
68
  end
69
+
70
+ def self.from_string(string, filename: nil, content_type: nil)
71
+ io = StringIO.new(string)
72
+ new(io, filename, content_type: content_type)
73
+ end
68
74
  end
69
75
  end
data/lib/discorb/flag.rb CHANGED
@@ -8,6 +8,7 @@ module Discorb
8
8
  class Flag
9
9
  # @return [Hash{Symbol => Boolean}] the values of the flag.
10
10
  attr_reader :values
11
+ alias to_h values
11
12
  # @return [Integer] the value of the flag.
12
13
  attr_reader :value
13
14
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "async/http"
4
4
  require "async/websocket"
5
+ require "async/barrier"
6
+ require "async/semaphore"
5
7
  require "json"
6
8
  require "zlib"
7
9
 
@@ -488,7 +490,7 @@ module Discorb
488
490
  _, gateway_response = @http.get("/gateway").wait
489
491
  gateway_url = gateway_response[:url]
490
492
  endpoint = Async::HTTP::Endpoint.parse("#{gateway_url}?v=9&encoding=json&compress=zlib-stream",
491
- alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
493
+ alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
492
494
  begin
493
495
  Async::WebSocket::Client.connect(endpoint, headers: [["User-Agent", Discorb::USER_AGENT]], handler: RawConnection) do |connection|
494
496
  @connection = connection
@@ -497,9 +499,17 @@ module Discorb
497
499
  while (message = @connection.read)
498
500
  @buffer << message
499
501
  if message.end_with?((+"\x00\x00\xff\xff").force_encoding("ASCII-8BIT"))
500
- message = JSON.parse(@zlib_stream.inflate(@buffer), symbolize_names: true)
501
- @buffer = +""
502
- handle_gateway(message)
502
+ begin
503
+ data = @zlib_stream.inflate(@buffer)
504
+ @buffer = +""
505
+ message = JSON.parse(data, symbolize_names: true)
506
+ rescue JSON::ParserError
507
+ @buffer = +""
508
+ @log.error "Received invalid JSON from gateway."
509
+ @log.debug data
510
+ else
511
+ handle_gateway(message)
512
+ end
503
513
  end
504
514
  end
505
515
  end
@@ -510,7 +520,9 @@ module Discorb
510
520
  raise ClientError.new("Authentication failed."), cause: nil
511
521
  when "Discord WebSocket requesting client reconnect."
512
522
  @log.info "Discord WebSocket requesting client reconnect"
513
- @tasks.map(&:stop)
523
+ connect_gateway(false)
524
+ else
525
+ @log.error "Discord WebSocket closed: #{e.message}"
514
526
  connect_gateway(false)
515
527
  end
516
528
  rescue EOFError, Async::Wrapper::Cancelled
@@ -522,7 +534,7 @@ module Discorb
522
534
  def send_gateway(opcode, **value)
523
535
  @connection.write({ op: opcode, d: value }.to_json)
524
536
  @connection.flush
525
- @log.debug "Sent message with opcode #{opcode}: #{value.to_json.gsub(@token, "[Token]")}"
537
+ @log.debug "Sent message: #{{ op: opcode, d: value }.to_json.gsub(@token, "[Token]")}"
526
538
  end
527
539
 
528
540
  def handle_gateway(payload)
@@ -579,15 +591,18 @@ module Discorb
579
591
  end
580
592
  end
581
593
 
582
- def handle_heartbeat(interval)
594
+ def handle_heartbeat
583
595
  Async do |task|
596
+ interval = @heartbeat_interval
584
597
  sleep((interval / 1000.0 - 1) * rand)
585
598
  loop do
586
- @heartbeat_before = Time.now.to_f
587
- @connection.write({ op: 1, d: @last_s }.to_json)
588
- @connection.flush
589
- @log.debug "Sent opcode 1."
590
- @log.debug "Waiting for heartbeat."
599
+ unless @connection.closed?
600
+ @heartbeat_before = Time.now.to_f
601
+ @connection.write({ op: 1, d: @last_s }.to_json)
602
+ @connection.flush
603
+ @log.debug "Sent opcode 1."
604
+ @log.debug "Waiting for heartbeat."
605
+ end
591
606
  sleep(interval / 1000.0 - 1)
592
607
  end
593
608
  end
@@ -605,19 +620,17 @@ module Discorb
605
620
  @user = ClientUser.new(self, data[:user])
606
621
  @uncached_guilds = data[:guilds].map { |g| g[:id] }
607
622
  if @uncached_guilds == [] or !@intents.guilds
608
- @ready = true
609
- dispatch(:ready)
610
- @log.info("Successfully connected to Discord.")
623
+ ready
611
624
  end
612
- @tasks << handle_heartbeat(@heartbeat_interval)
625
+ dispatch(:ready)
626
+ @tasks << handle_heartbeat
613
627
  when "GUILD_CREATE"
614
628
  if @uncached_guilds.include?(data[:id])
615
629
  Guild.new(self, data, true)
616
630
  @uncached_guilds.delete(data[:id])
617
631
  if @uncached_guilds == []
618
- @ready = true
619
- dispatch(:ready)
620
- @log.info("Successfully connected to Discord, and cached all guilds.")
632
+ @log.debug "All guilds cached"
633
+ ready
621
634
  end
622
635
  elsif @guilds.has?(data[:id])
623
636
  @guilds[data[:id]].send(:_set_data, data)
@@ -839,12 +852,12 @@ module Discorb
839
852
  dispatch(:voice_state_update, old, current)
840
853
  if old&.channel != current&.channel
841
854
  dispatch(:voice_channel_update, old, current)
842
- case [old&.channel, current&.channel]
843
- in [nil, _]
855
+ case [old&.channel.nil?, current&.channel.nil?]
856
+ when [true, false]
844
857
  dispatch(:voice_channel_connect, current)
845
- in [_, nil]
858
+ when [false, true]
846
859
  dispatch(:voice_channel_disconnect, old)
847
- in _
860
+ when [false, false]
848
861
  dispatch(:voice_channel_move, old, current)
849
862
  end
850
863
  end
@@ -980,9 +993,9 @@ module Discorb
980
993
  dispatch(:reaction_add, ReactionEvent.new(self, data))
981
994
  when "MESSAGE_REACTION_REMOVE"
982
995
  if (target_message = @messages[data[:message_id]]) &&
983
- (target_reaction = target_message.reactions.find do |r|
984
- data[:emoji][:id].nil? ? r.emoji.name == data[:emoji][:name] : r.emoji.id == data[:emoji][:id]
985
- end)
996
+ (target_reaction = target_message.reactions.find do |r|
997
+ data[:emoji][:id].nil? ? r.emoji.name == data[:emoji][:name] : r.emoji.id == data[:emoji][:id]
998
+ end)
986
999
  target_reaction.instance_variable_set(:@count, target_reaction.count - 1)
987
1000
  target_message.reactions.delete(target_reaction) if target_reaction.count.zero?
988
1001
  end
@@ -994,7 +1007,7 @@ module Discorb
994
1007
  dispatch(:reaction_remove_all, ReactionRemoveAllEvent.new(self, data))
995
1008
  when "MESSAGE_REACTION_REMOVE_EMOJI"
996
1009
  if (target_message = @messages[data[:message_id]]) &&
997
- (target_reaction = target_message.reactions.find { |r| data[:emoji][:id].nil? ? r.name == data[:emoji][:name] : r.id == data[:emoji][:id] })
1010
+ (target_reaction = target_message.reactions.find { |r| data[:emoji][:id].nil? ? r.name == data[:emoji][:name] : r.id == data[:emoji][:id] })
998
1011
  target_message.reactions.delete(target_reaction)
999
1012
  end
1000
1013
  dispatch(:reaction_remove_emoji, ReactionRemoveEmojiEvent.new(data))
@@ -1016,26 +1029,56 @@ module Discorb
1016
1029
  end
1017
1030
  end
1018
1031
  end
1032
+
1033
+ def ready
1034
+ Async do
1035
+ if @fetch_member
1036
+ @log.debug "Fetching members"
1037
+ barrier = Async::Barrier.new
1038
+ semaphore = Async::Semaphore.new(@guilds.length)
1039
+
1040
+ @guilds.each do |guild|
1041
+ semaphore.async(parent: barrier) do
1042
+ guild.fetch_members
1043
+ end
1044
+ end
1045
+ semaphore.__send__(:wait)
1046
+ end
1047
+ @ready = true
1048
+ dispatch(:standby)
1049
+ @log.info("Client is ready!")
1050
+ end
1051
+ end
1019
1052
  end
1020
-
1053
+
1021
1054
  #
1022
1055
  # A class for connecting websocket with raw bytes data.
1023
1056
  # @private
1024
1057
  #
1025
- class RawConnection < Async::WebSocket::Connection
1026
- def initialize(...)
1058
+ class RawConnection < Async::WebSocket::Connection
1059
+ def initialize(*, **)
1060
+ super
1061
+ @closed = false
1062
+ end
1063
+
1064
+ def closed?
1065
+ @closed
1066
+ end
1067
+
1068
+ def close
1027
1069
  super
1070
+ @closed = true
1028
1071
  end
1029
-
1030
- def parse(buffer)
1031
- # noop
1072
+
1073
+ def parse(buffer)
1074
+ # noop
1032
1075
  buffer.to_s
1033
- end
1034
-
1035
- def dump(object)
1036
- # noop
1076
+ end
1077
+
1078
+ def dump(object)
1079
+ # noop
1037
1080
  object.to_s
1038
- end
1081
+ end
1039
1082
  end
1040
1083
  end
1041
- end
1084
+ end
data/lib/discorb/guild.rb CHANGED
@@ -502,7 +502,6 @@ module Discorb
502
502
  # Fetch a member in the guild.
503
503
  # @macro async
504
504
  # @macro http
505
- # @macro members_intent
506
505
  #
507
506
  # @param [#to_s] id The ID of the member to fetch.
508
507
  #
@@ -519,6 +518,39 @@ module Discorb
519
518
  end
520
519
  end
521
520
 
521
+ # Fetch members in the guild.
522
+ # @macro async
523
+ # @macro http
524
+ # @macro members_intent
525
+ #
526
+ # @param [Integer] limit The maximum number of members to fetch, 0 for all.
527
+ # @param [Integer] after The ID of the member to start fetching after.
528
+ #
529
+ # @return [Async::Task<Array<Discorb::Member>>] The list of members.
530
+ #
531
+ def fetch_members(limit: 0, after: nil)
532
+ Async do
533
+ unless limit == 0
534
+ _resp, data = @client.http.get("/guilds/#{@id}/members?#{URI.encode_www_form({ after: after, limit: limit })}").wait
535
+ next data[:members].map { |m| Member.new(@client, @id, m[:user], m) }
536
+ end
537
+ ret = []
538
+ after = 0
539
+ loop do
540
+ params = { after: after, limit: 100 }
541
+ _resp, data = @client.http.get("/guilds/#{@id}/members?#{URI.encode_www_form(params)}").wait
542
+ ret += data.map { |m| Member.new(@client, @id, m[:user], m) }
543
+ after = data.last[:user][:id]
544
+ if data.length != 1000
545
+ break
546
+ end
547
+ end
548
+ ret
549
+ end
550
+ end
551
+
552
+ alias fetch_member_list fetch_members
553
+
522
554
  #
523
555
  # Search for members by name in the guild.
524
556
  # @macro async
data/lib/discorb/http.rb CHANGED
@@ -221,7 +221,7 @@ module Discorb
221
221
  { "User-Agent" => USER_AGENT, "authorization" => "Bot #{@client.token}",
222
222
  "content-type" => "application/json" }
223
223
  end
224
- ret.merge(headers) if !headers.nil? && headers.length.positive?
224
+ ret.merge!(headers) if !headers.nil? && headers.length.positive?
225
225
  ret["X-Audit-Log-Reason"] = audit_log_reason unless audit_log_reason.nil?
226
226
  ret
227
227
  end
@@ -242,12 +242,13 @@ module Discorb
242
242
  else
243
243
  API_BASE_URL + path
244
244
  end
245
- URI(full_path).path
245
+ uri = URI(full_path)
246
+ full_path.sub(uri.scheme + "://" + uri.host, "")
246
247
  end
247
248
 
248
249
  def get_response_data(resp)
249
- if resp["Via"].nil?
250
- raise CloudFlareBanError.new(@client, resp)
250
+ if resp["Via"].nil? && resp.code == "429"
251
+ raise CloudFlareBanError.new(resp, @client)
251
252
  end
252
253
  rd = resp.body
253
254
  if rd.nil? || rd.empty?
@@ -82,7 +82,7 @@ module Discorb
82
82
  if @raw_value.key?(name)
83
83
  @raw_value[name]
84
84
  elsif name.end_with?("=") && @raw_value.key?(name[0..-2].to_sym)
85
- raise ArgumentError, "true/false expected" if (!args.is_a? TrueClass) || args.is_a?(FalseClass)
85
+ raise ArgumentError, "true/false expected" unless args.is_a? TrueClass or args.is_a?(FalseClass)
86
86
 
87
87
  @raw_value[name[0..-2].to_sym] = args
88
88
  else
@@ -108,6 +108,10 @@ module Discorb
108
108
  "#<#{self.class} value=#{value}>"
109
109
  end
110
110
 
111
+ def to_h
112
+ @raw_value
113
+ end
114
+
111
115
  class << self
112
116
  # Create new intent object from raw value.
113
117
  # @param value [Integer] The value of the intent.
@@ -121,12 +125,12 @@ module Discorb
121
125
 
122
126
  # Create new intent object with default values.
123
127
  def default
124
- from_value(32_509)
128
+ from_value(32509)
125
129
  end
126
130
 
127
131
  # Create new intent object with all intents.
128
132
  def all
129
- from_value(32_767)
133
+ from_value(32767)
130
134
  end
131
135
 
132
136
  # Create new intent object with no intents.
@@ -213,6 +213,55 @@ module Discorb
213
213
  !@updated_at.nil?
214
214
  end
215
215
 
216
+ #
217
+ # Removes the mentions from the message.
218
+ #
219
+ # @param [Boolean] user Whether to clean user mentions.
220
+ # @param [Boolean] channel Whether to clean channel mentions.
221
+ # @param [Boolean] role Whether to clean role mentions.
222
+ # @param [Boolean] emoji Whether to clean emoji.
223
+ # @param [Boolean] everyone Whether to clean `@everyone` and `@here`.
224
+ # @param [Boolean] codeblock Whether to clean codeblocks.
225
+ #
226
+ # @return [String] The cleaned content of the message.
227
+ #
228
+ def clean_content(user: true, channel: true, role: true, emoji: true, everyone: true, codeblock: false)
229
+ ret = @content.dup
230
+ ret.gsub!(/<@!?(\d+)>/) do |match|
231
+ member = guild&.members&.[]($1)
232
+ member ||= @client.users[$1]
233
+ member ? "@#{member.name}" : "@Unknown User"
234
+ end if user
235
+ ret.gsub!(/<#(\d+)>/) do |match|
236
+ channel = @client.channels[$1]
237
+ channel ? "<##{channel.id}>" : "#Unknown Channel"
238
+ end
239
+ ret.gsub!(/<@&(\d+)>/) do |match|
240
+ role = guild&.roles&.[]($1)
241
+ role ? "@#{role.name}" : "@Unknown Role"
242
+ end if role
243
+ ret.gsub!(/<a?:([a-zA-Z0-9_]+):\d+>/) do |match|
244
+ $1
245
+ end if emoji
246
+ ret.gsub!(/@(everyone|here)/, "@\u200b\\1") if everyone
247
+ unless codeblock
248
+ codeblocks = ret.split("```", -1)
249
+ original_codeblocks = @content.scan(/```(.+?)```/m)
250
+ res = []
251
+ max = codeblocks.length
252
+ codeblocks.each_with_index do |codeblock, i|
253
+ if max % 2 == 0 && i == max - 1 or i.even?
254
+ res << codeblock
255
+ else
256
+ res << original_codeblocks[i / 2]
257
+ end
258
+ end
259
+ res.join("```")
260
+ else
261
+ ret
262
+ end
263
+ end
264
+
216
265
  #
217
266
  # Edit the message.
218
267
  #
@@ -59,7 +59,8 @@ module Discorb
59
59
  end
60
60
  files = [file] if file
61
61
  if files
62
- headers, payload = HTTP.multipart(payload, files)
62
+ seperator, payload = HTTP.multipart(payload, files)
63
+ headers = { "content-type" => "multipart/form-data; boundary=#{seperator}" }
63
64
  else
64
65
  headers = {}
65
66
  end
data/po/yard.pot CHANGED
@@ -203,7 +203,7 @@ msgid "client = Discorb::Client.new"
203
203
  msgstr ""
204
204
 
205
205
  #: ../README.md:35
206
- msgid "client.once :ready do\n"
206
+ msgid "client.once :standby do\n"
207
207
  " puts \"Logged in as #{client.user}\"\n"
208
208
  "end"
209
209
  msgstr ""