discorb 0.19.0 → 0.20.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build_version.yml +2 -2
  3. data/.rubocop.yml +12 -75
  4. data/Changelog.md +10 -0
  5. data/Rakefile +482 -454
  6. data/lib/discorb/allowed_mentions.rb +68 -72
  7. data/lib/discorb/app_command/command.rb +466 -398
  8. data/lib/discorb/app_command/common.rb +65 -25
  9. data/lib/discorb/app_command/handler.rb +304 -266
  10. data/lib/discorb/app_command.rb +5 -5
  11. data/lib/discorb/application.rb +198 -197
  12. data/lib/discorb/asset.rb +101 -101
  13. data/lib/discorb/attachment.rb +134 -119
  14. data/lib/discorb/audit_logs.rb +412 -385
  15. data/lib/discorb/automod.rb +279 -269
  16. data/lib/discorb/channel/base.rb +107 -108
  17. data/lib/discorb/channel/category.rb +32 -32
  18. data/lib/discorb/channel/container.rb +44 -44
  19. data/lib/discorb/channel/dm.rb +26 -28
  20. data/lib/discorb/channel/guild.rb +311 -246
  21. data/lib/discorb/channel/stage.rb +156 -140
  22. data/lib/discorb/channel/text.rb +430 -336
  23. data/lib/discorb/channel/thread.rb +374 -325
  24. data/lib/discorb/channel/voice.rb +85 -79
  25. data/lib/discorb/channel.rb +5 -5
  26. data/lib/discorb/client.rb +635 -621
  27. data/lib/discorb/color.rb +178 -182
  28. data/lib/discorb/common.rb +168 -164
  29. data/lib/discorb/components/button.rb +107 -106
  30. data/lib/discorb/components/select_menu.rb +157 -145
  31. data/lib/discorb/components/text_input.rb +103 -106
  32. data/lib/discorb/components.rb +68 -66
  33. data/lib/discorb/dictionary.rb +135 -135
  34. data/lib/discorb/embed.rb +404 -398
  35. data/lib/discorb/emoji.rb +309 -302
  36. data/lib/discorb/emoji_table.rb +16099 -8857
  37. data/lib/discorb/error.rb +131 -131
  38. data/lib/discorb/event.rb +360 -314
  39. data/lib/discorb/event_handler.rb +39 -39
  40. data/lib/discorb/exe/about.rb +17 -17
  41. data/lib/discorb/exe/irb.rb +72 -67
  42. data/lib/discorb/exe/new.rb +323 -315
  43. data/lib/discorb/exe/run.rb +69 -68
  44. data/lib/discorb/exe/setup.rb +57 -55
  45. data/lib/discorb/exe/show.rb +12 -12
  46. data/lib/discorb/extend.rb +25 -45
  47. data/lib/discorb/extension.rb +89 -83
  48. data/lib/discorb/flag.rb +126 -128
  49. data/lib/discorb/gateway.rb +984 -804
  50. data/lib/discorb/gateway_events.rb +670 -638
  51. data/lib/discorb/gateway_requests.rb +45 -48
  52. data/lib/discorb/guild.rb +2115 -1626
  53. data/lib/discorb/guild_template.rb +280 -241
  54. data/lib/discorb/http.rb +247 -232
  55. data/lib/discorb/image.rb +42 -42
  56. data/lib/discorb/integration.rb +169 -161
  57. data/lib/discorb/intents.rb +161 -163
  58. data/lib/discorb/interaction/autocomplete.rb +76 -62
  59. data/lib/discorb/interaction/command.rb +279 -224
  60. data/lib/discorb/interaction/components.rb +114 -104
  61. data/lib/discorb/interaction/modal.rb +36 -32
  62. data/lib/discorb/interaction/response.rb +379 -336
  63. data/lib/discorb/interaction/root.rb +271 -257
  64. data/lib/discorb/interaction.rb +5 -5
  65. data/lib/discorb/invite.rb +154 -153
  66. data/lib/discorb/member.rb +344 -311
  67. data/lib/discorb/message.rb +615 -544
  68. data/lib/discorb/message_meta.rb +197 -186
  69. data/lib/discorb/modules.rb +371 -290
  70. data/lib/discorb/permission.rb +305 -291
  71. data/lib/discorb/presence.rb +352 -346
  72. data/lib/discorb/rate_limit.rb +81 -76
  73. data/lib/discorb/reaction.rb +55 -54
  74. data/lib/discorb/role.rb +272 -240
  75. data/lib/discorb/shard.rb +76 -74
  76. data/lib/discorb/sticker.rb +193 -171
  77. data/lib/discorb/user.rb +205 -188
  78. data/lib/discorb/utils/colored_puts.rb +16 -16
  79. data/lib/discorb/utils.rb +12 -16
  80. data/lib/discorb/voice_state.rb +305 -281
  81. data/lib/discorb/webhook.rb +537 -507
  82. data/lib/discorb.rb +62 -56
  83. data/sig/discorb/application.rbs +2 -0
  84. data/sig/discorb/automod.rbs +10 -1
  85. data/sig/discorb/guild.rbs +2 -0
  86. data/sig/discorb/message.rbs +2 -0
  87. data/sig/discorb/user.rbs +22 -20
  88. metadata +2 -2
@@ -1,621 +1,635 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "logger"
5
-
6
- require "async"
7
- require "async/websocket/client"
8
- require_relative "./utils/colored_puts"
9
-
10
- module Discorb
11
- #
12
- # Class for connecting to the Discord server.
13
- #
14
- class Client
15
- # @return [Discorb::Intents] The intents that the client is currently using.
16
- attr_accessor :intents
17
- # @return [Discorb::Application] The application that the client is using.
18
- # @return [nil] If never fetched application by {#fetch_application}.
19
- attr_reader :application
20
- # @return [Discorb::HTTP] The http client.
21
- attr_reader :http
22
- # @return [Integer] The heartbeat interval.
23
- attr_reader :heartbeat_interval
24
- # @return [Integer] The API version of the Discord gateway.
25
- # @return [nil] If not connected to the gateway.
26
- attr_reader :api_version
27
- # @return [String] The token of the client.
28
- attr_reader :token
29
- # @return [Discorb::AllowedMentions] The allowed mentions that the client is using.
30
- attr_reader :allowed_mentions
31
- # @return [Discorb::ClientUser] The client user.
32
- attr_reader :user
33
- # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Guild}] A dictionary of guilds.
34
- attr_reader :guilds
35
- # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::User}] A dictionary of users.
36
- attr_reader :users
37
- # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Channel}] A dictionary of channels.
38
- attr_reader :channels
39
- # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Emoji}] A dictionary of emojis.
40
- attr_reader :emojis
41
- # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Message}] A dictionary of messages.
42
- attr_reader :messages
43
- # @return [Array<Discorb::ApplicationCommand::Command>] The commands that the client is using.
44
- attr_reader :commands
45
- # @return [Float] The ping of the client.
46
- # @note This will be calculated from heartbeat and heartbeat_ack.
47
- # @return [nil] If not connected to the gateway.
48
- attr_reader :ping
49
- # @return [:initialized, :running, :closed] The status of the client.
50
- attr_reader :status
51
- # @return [Hash{String => Discorb::Extension}] The loaded extensions.
52
- attr_reader :extensions
53
- # @return [Hash{Integer => Discorb::Shard}] The shards of the client.
54
- attr_reader :shards
55
- # @private
56
- # @return [Hash{Discorb::Snowflake => Discorb::ApplicationCommand::Command}] The commands on the top level.
57
- attr_reader :callable_commands
58
- # @private
59
- # @return [{String => Thread::Mutex}] A hash of mutexes.
60
- attr_reader :mutex
61
-
62
- # @!attribute [r] session_id
63
- # @return [String] The session ID of the client or current shard.
64
- # @return [nil] If not connected to the gateway.
65
- # @!attribute [r] shard
66
- # @return [Discorb::Shard] The current shard. This is implemented with Thread variables.
67
- # @return [nil] If client has no shard.
68
- # @!attribute [r] shard_id
69
- # @return [Integer] The current shard ID. This is implemented with Thread variables.
70
- # @return [nil] If client has no shard.
71
- # @!attribute [r] logger
72
- # @return [Logger] The logger.
73
-
74
- #
75
- # Initializes a new client.
76
- #
77
- # @param [Discorb::AllowedMentions] allowed_mentions The allowed mentions that the client is using.
78
- # @param [Discorb::Intents] intents The intents that the client is currently using.
79
- # @param [Integer] message_caches The number of messages to cache.
80
- # @param [Logger] logger The IO object to use for logging.
81
- # @param [:debug, :info, :warn, :error, :critical] log_level The log level.
82
- # @param [Boolean] wait_until_ready Whether to delay event dispatch until ready.
83
- # @param [Boolean] fetch_member Whether to fetch member on ready. This may slow down the client. Default to `false`.
84
- # @param [String] title
85
- # The title of the process. `false` to default of ruby, `nil` to `discorb: User#0000`. Default to `nil`.
86
- #
87
- def initialize(
88
- allowed_mentions: nil, intents: nil, message_caches: 1000,
89
- logger: nil,
90
- wait_until_ready: true, fetch_member: false,
91
- title: nil
92
- )
93
- @allowed_mentions = allowed_mentions || AllowedMentions.new(everyone: true, roles: true, users: true)
94
- @intents = (intents or Intents.default)
95
- @events = {}
96
- @api_version = nil
97
- @logger = logger || Logger.new(
98
- $stdout,
99
- progname: "discorb",
100
- level: Logger::ERROR,
101
- )
102
- @user = nil
103
- @users = Discorb::Dictionary.new
104
- @channels = Discorb::Dictionary.new
105
- @guilds = Discorb::Dictionary.new(sort: ->(k) { k[0].to_i })
106
- @emojis = Discorb::Dictionary.new
107
- @messages = Discorb::Dictionary.new(limit: message_caches)
108
- @application = nil
109
- @last_s = nil
110
- @identify_presence = nil
111
- @wait_until_ready = wait_until_ready
112
- @ready = false
113
- @tasks = []
114
- @conditions = {}
115
- @commands = []
116
- @callable_commands = []
117
- @status = :initialized
118
- @fetch_member = fetch_member
119
- @title = title
120
- @extensions = {}
121
- @mutex = {}
122
- @shards = {}
123
- set_default_events
124
- end
125
-
126
- #
127
- # Registers an event handler.
128
- # @see file:docs/Events.md Events Documentation
129
- #
130
- # @param [Symbol] event_name The name of the event.
131
- # @param [Symbol] id Custom ID of the event.
132
- # @param [Hash] metadata The metadata of the event.
133
- # @param [Proc] block The block to execute when the event is triggered.
134
- #
135
- # @return [Discorb::EventHandler] The event.
136
- #
137
- def on(event_name, id: nil, **metadata, &block)
138
- ne = EventHandler.new(block, id, metadata)
139
- @events[event_name] ||= []
140
- @events[event_name].delete_if { |e| e.metadata[:override] }
141
- @events[event_name] << ne
142
- ne
143
- end
144
-
145
- #
146
- # Almost same as {#on}, but only triggers the event once.
147
- #
148
- # @param (see #on)
149
- #
150
- # @return [Discorb::EventHandler] The event.
151
- #
152
- def once(event_name, id: nil, **metadata, &block)
153
- metadata[:once] = true
154
- on(event_name, id: id, **metadata, &block)
155
- end
156
-
157
- #
158
- # Remove event by ID.
159
- #
160
- # @param [Symbol] event_name The name of the event.
161
- # @param [Symbol] id The ID of the event.
162
- #
163
- def remove_event(event_name, id)
164
- @events[event_name].delete_if { |e| e.id == id }
165
- end
166
-
167
- #
168
- # Dispatch an event.
169
- # @async
170
- #
171
- # @param [Symbol] event_name The name of the event.
172
- # @param [Object] args The arguments to pass to the event.
173
- #
174
- # @return [Async::Task<void>] The task.
175
- #
176
- def dispatch(event_name, *args)
177
- Async do
178
- if (conditions = @conditions[event_name])
179
- ids = Set[*conditions.map(&:first).map(&:object_id)]
180
- conditions.delete_if do |condition|
181
- next unless ids.include?(condition.first.object_id)
182
-
183
- check_result = condition[1].nil? || condition[1].call(*args)
184
- if check_result
185
- condition.first.signal(args)
186
- true
187
- else
188
- false
189
- end
190
- end
191
- end
192
- events = @events[event_name].dup || []
193
- if respond_to?("on_" + event_name.to_s)
194
- event_method = method("on_" + event_name.to_s)
195
- class << event_method
196
- def id
197
- "method"
198
- end
199
- end
200
- events << event_method
201
- end
202
- if events.nil?
203
- logger.debug "Event #{event_name} doesn't have any proc, skipping"
204
- next
205
- end
206
- logger.debug "Dispatching event #{event_name}"
207
- events.each do |block|
208
- Async do
209
- Async(annotation: "Discorb event: #{event_name}") do |_task|
210
- @events[event_name].delete(block) if block.is_a?(Discorb::EventHandler) && block.metadata[:once]
211
- block.call(*args)
212
- logger.debug "Dispatched proc with ID #{block.id.inspect}"
213
- rescue StandardError, ScriptError => e
214
- if event_name == :error
215
- raise e
216
- else
217
- dispatch(:error, event_name, args, e)
218
- end
219
- end
220
- end
221
- end
222
- end
223
- end
224
-
225
- #
226
- # Fetch user from ID.
227
- # @async
228
- #
229
- # @param [#to_s] id <description>
230
- #
231
- # @return [Async::Task<Discorb::User>] The user.
232
- #
233
- # @raise [Discorb::NotFoundError] If the user doesn't exist.
234
- #
235
- def fetch_user(id)
236
- Async do
237
- _resp, data = @http.request(Route.new("/users/#{id}", "//users/:user_id", :get)).wait
238
- User.new(self, data)
239
- end
240
- end
241
-
242
- #
243
- # Fetch channel from ID.
244
- # @async
245
- #
246
- # @param [#to_s] id The ID of the channel.
247
- #
248
- # @return [Async::Task<Discorb::Channel>] The channel.
249
- #
250
- # @raise [Discorb::NotFoundError] If the channel doesn't exist.
251
- #
252
- def fetch_channel(id)
253
- Async do
254
- _resp, data = @http.request(Route.new("/channels/#{id}", "//channels/:channel_id", :get)).wait
255
- Channel.make_channel(self, data)
256
- end
257
- end
258
-
259
- #
260
- # Fetch guild from ID.
261
- # @async
262
- #
263
- # @param [#to_s] id <description>
264
- #
265
- # @return [Async::Task<Discorb::Guild>] The guild.
266
- #
267
- # @raise [Discorb::NotFoundError] If the guild doesn't exist.
268
- #
269
- def fetch_guild(id)
270
- Async do
271
- _resp, data = @http.request(Route.new("/guilds/#{id}", "//guilds/:guild_id", :get)).wait
272
- Guild.new(self, data, false)
273
- end
274
- end
275
-
276
- #
277
- # Fetch invite from code.
278
- # @async
279
- #
280
- # @param [String] code The code of the invite.
281
- # @param [Boolean] with_count Whether to include the count of the invite.
282
- # @param [Boolean] with_expiration Whether to include the expiration of the invite.
283
- #
284
- # @return [Async::Task<Discorb::Invite>] The invite.
285
- #
286
- def fetch_invite(code, with_count: true, with_expiration: true)
287
- Async do
288
- _resp, data = @http.request(
289
- Route.new(
290
- "/invites/#{code}?with_count=#{with_count}&with_expiration=#{with_expiration}",
291
- "//invites/:code",
292
- :get
293
- )
294
- ).wait
295
- Invite.new(self, data, false)
296
- end
297
- end
298
-
299
- #
300
- # Fetch webhook from ID.
301
- # If application was cached, it will be used.
302
- # @async
303
- #
304
- # @param [Boolean] force Whether to force the fetch.
305
- #
306
- # @return [Async::Task<Discorb::Application>] The application.
307
- #
308
- def fetch_application(force: false)
309
- Async do
310
- next @application if @application && !force
311
-
312
- _resp, data = @http.request(Route.new("/oauth2/applications/@me", "//oauth2/applications/@me", :get)).wait
313
- @application = Application.new(self, data)
314
- @application
315
- end
316
- end
317
-
318
- #
319
- # Fetch nitro sticker pack from ID.
320
- # @async
321
- #
322
- # @return [Async::Task<Array<Discorb::Sticker::Pack>>] The packs.
323
- #
324
- def fetch_nitro_sticker_packs
325
- Async do
326
- _resp, data = @http.request(Route.new("/sticker-packs", "//sticker-packs", :get)).wait
327
- data[:sticker_packs].map { |pack| Sticker::Pack.new(self, pack) }
328
- end
329
- end
330
-
331
- #
332
- # Update presence of the client.
333
- #
334
- # @param [Discorb::Activity] activity The activity to update.
335
- # @param [:online, :idle, :dnd, :invisible] status The status to update.
336
- #
337
- def update_presence(activity = nil, status: nil)
338
- payload = {
339
- activities: [],
340
- status: status,
341
- since: nil,
342
- afk: nil,
343
- }
344
- payload[:activities] = [activity.to_hash] unless activity.nil?
345
- payload[:status] = status unless status.nil?
346
- if connection
347
- Async do
348
- send_gateway(3, **payload)
349
- end
350
- else
351
- @identify_presence = payload
352
- end
353
- end
354
-
355
- alias change_presence update_presence
356
-
357
- #
358
- # Method to wait for a event.
359
- # @async
360
- #
361
- # @param [Symbol] event The name of the event.
362
- # @param [Integer] timeout The timeout in seconds.
363
- # @param [Proc] check The check to use.
364
- #
365
- # @return [Async::Task<Object>] The result of the event.
366
- #
367
- # @raise [Discorb::TimeoutError] If the event didn't occur in time.
368
- #
369
- def event_lock(event, timeout = nil, &check)
370
- Async do |task|
371
- condition = Async::Condition.new
372
- @conditions[event] ||= []
373
- @conditions[event] << [condition, check]
374
- if timeout.nil?
375
- value = condition.wait
376
- else
377
- timeout_task = task.with_timeout(timeout) do
378
- condition.wait
379
- rescue Async::TimeoutError
380
- @conditions[event].delete_if { |c| c.first == condition }
381
- raise Discorb::TimeoutError, "Timeout waiting for event #{event}", cause: nil
382
- end
383
- value = timeout_task
384
- end
385
- value.length <= 1 ? value.first : value
386
- end
387
- end
388
-
389
- alias await event_lock
390
-
391
- def inspect
392
- "#<#{self.class} user=\"#{user}\">"
393
- end
394
-
395
- #
396
- # Load the extension.
397
- #
398
- # @param [Class, Discorb::Extension] ext The extension to load.
399
- # @param [Object] ... The arguments to pass to the `ext#initialize`.
400
- #
401
- def load_extension(ext, ...)
402
- case ext
403
- when Class
404
- raise ArgumentError, "#{ext} is not a extension" unless ext < Discorb::Extension
405
-
406
- ins = ext.new(self, ...)
407
- when Discorb::Extension
408
- ins = ext
409
- else
410
- raise ArgumentError, "#{ext} is not a extension"
411
- end
412
-
413
- @events.each_value do |event|
414
- event.delete_if { |c| c.metadata[:extension] == ins.class.name }
415
- end
416
- ins.events.each do |name, events|
417
- @events[name] ||= []
418
- events.each do |event|
419
- @events[name] << event
420
- end
421
- end
422
- @commands.delete_if do |cmd|
423
- cmd.respond_to? :extension and cmd.extension == ins.class.name
424
- end
425
- ins.class.commands.each do |cmd|
426
- cmd.define_singleton_method(:extension) { ins.class.name }
427
- cmd.replace_block(ins)
428
- cmd.block.define_singleton_method(:self_replaced) { true }
429
- @commands << cmd
430
- end
431
-
432
- cls = ins.class
433
- cls.loaded(self, ...) if cls.respond_to? :loaded
434
- ins.class.callable_commands.each do |cmd|
435
- unless cmd.respond_to? :self_replaced
436
- cmd.define_singleton_method(:extension) { ins.class.name }
437
- cmd.replace_block(ins)
438
- cmd.block.define_singleton_method(:self_replaced) { true }
439
- end
440
- @callable_commands << cmd
441
- end
442
- @extensions[ins.class.name] = ins
443
- ins
444
- end
445
-
446
- include Discorb::Gateway::Handler
447
- include Discorb::ApplicationCommand::Handler
448
-
449
- #
450
- # Starts the client.
451
- # @note This method behavior will change by CLI.
452
- # @see file:docs/cli.md CLI documentation
453
- #
454
- # @param [String, nil] token The token to use.
455
- #
456
- # @note If the token is nil, you should use `discorb run` with the `-e` or `--env` option.
457
- #
458
- def run(token = nil, shards: nil, shard_count: nil)
459
- token ||= ENV.fetch("DISCORB_CLI_TOKEN", nil)
460
- raise ArgumentError, "Token is not specified, and -e/--env is not specified" if token.nil?
461
-
462
- case ENV.fetch("DISCORB_CLI_FLAG", nil)
463
- when nil
464
- start_client(token, shards: shards, shard_count: shard_count)
465
- when "run"
466
- before_run(token)
467
- start_client(token, shards: shards, shard_count: shard_count)
468
- when "setup"
469
- run_setup(token)
470
- end
471
- end
472
-
473
- #
474
- # Stops the client.
475
- #
476
- def close
477
- if @shards.any?
478
- @shards.each_value(&:close)
479
- else
480
- @connection.send_close
481
- end
482
- @tasks.each(&:stop)
483
- @status = :closed
484
- end
485
-
486
- def session_id
487
- if shard
488
- shard.session_id
489
- else
490
- @session_id
491
- end
492
- end
493
-
494
- def logger
495
- shard&.logger || @logger
496
- end
497
-
498
- def shard
499
- Thread.current.thread_variable_get("shard")
500
- end
501
-
502
- def shard_id
503
- Thread.current.thread_variable_get("shard_id")
504
- end
505
-
506
- private
507
-
508
- def before_run(token)
509
- require "json"
510
- options = JSON.parse(ENV.fetch("DISCORB_CLI_OPTIONS", nil), symbolize_names: true)
511
- setup_commands(token) if options[:setup]
512
- end
513
-
514
- def run_setup(token)
515
- # @type var guild_ids: Array[String] | false
516
- guild_ids = false
517
- if guilds = ENV.fetch("DISCORB_SETUP_GUILDS", nil)
518
- guild_ids = guilds.split(",")
519
- end
520
- guild_ids = false if guild_ids == ["global"]
521
- setup_commands(token, guild_ids: guild_ids).wait
522
- clear_commands(token, ENV.fetch("DISCORB_SETUP_CLEAR_GUILDS", "").split(","))
523
- if ENV.fetch("DISCORB_SETUP_SCRIPT", nil) == "true"
524
- @events[:setup]&.each(&:call)
525
- self.on_setup if respond_to? :on_setup
526
- end
527
- end
528
-
529
- def set_status(status, shard)
530
- if shard.nil?
531
- @status = status
532
- else
533
- @shards[shard].status = status
534
- end
535
- end
536
-
537
- def connection
538
- if shard_id
539
- @shards[shard_id].connection
540
- else
541
- @connection
542
- end
543
- end
544
-
545
- def connection=(value)
546
- if shard_id
547
- @shards[shard_id].connection = value
548
- else
549
- @connection = value
550
- end
551
- end
552
-
553
- def session_id=(value)
554
- sid = shard_id
555
- if sid
556
- @shards[sid].session_id = value
557
- else
558
- @session_id = value
559
- end
560
- end
561
-
562
- def start_client(token, shards: nil, shard_count: nil)
563
- @token = token.to_s
564
- @shard_count = shard_count
565
- Signal.trap(:SIGINT) do
566
- logger.info "SIGINT received, closing..."
567
- Signal.trap(:SIGINT, "DEFAULT")
568
- close
569
- end
570
- if shards.nil?
571
- main_loop(nil)
572
- else
573
- @shards = shards.to_h.with_index do |shard, i|
574
- [shard, Shard.new(self, shard, shard_count, i)]
575
- end
576
- @shards.values[..-1].each_with_index do |shard, i|
577
- shard.next_shard = @shards.values[i + 1]
578
- end
579
- @shards.each_value { |s| s.thread.join }
580
- end
581
- end
582
-
583
- def main_loop(shard)
584
- set_status(:running, shard)
585
- connect_gateway(false).wait
586
- rescue StandardError
587
- set_status(:closed, shard)
588
- raise
589
- end
590
-
591
- def main_task
592
- if shard_id
593
- shard.main_task
594
- else
595
- @main_task
596
- end
597
- end
598
-
599
- def main_task=(value)
600
- if shard_id
601
- shard.main_task = value
602
- else
603
- @main_task = value
604
- end
605
- end
606
-
607
- def set_default_events
608
- on :error, override: true do |event_name, _args, e|
609
- message = "An error occurred while dispatching #{event_name}:\n#{e.full_message}"
610
- logger.error message
611
- end
612
-
613
- once :standby do
614
- next if @title == false
615
-
616
- title = @title || ENV.fetch("DISCORB_CLI_TITLE", nil) || "discorb: #{@user}"
617
- Process.setproctitle title
618
- end
619
- end
620
- end
621
- end
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "logger"
5
+
6
+ require "async"
7
+ require "async/websocket/client"
8
+ require_relative "./utils/colored_puts"
9
+
10
+ module Discorb
11
+ #
12
+ # Class for connecting to the Discord server.
13
+ #
14
+ class Client
15
+ # @return [Discorb::Intents] The intents that the client is currently using.
16
+ attr_accessor :intents
17
+ # @return [Discorb::Application] The application that the client is using.
18
+ # @return [nil] If never fetched application by {#fetch_application}.
19
+ attr_reader :application
20
+ # @return [Discorb::HTTP] The http client.
21
+ attr_reader :http
22
+ # @return [Integer] The heartbeat interval.
23
+ attr_reader :heartbeat_interval
24
+ # @return [Integer] The API version of the Discord gateway.
25
+ # @return [nil] If not connected to the gateway.
26
+ attr_reader :api_version
27
+ # @return [String] The token of the client.
28
+ attr_reader :token
29
+ # @return [Discorb::AllowedMentions] The allowed mentions that the client is using.
30
+ attr_reader :allowed_mentions
31
+ # @return [Discorb::ClientUser] The client user.
32
+ attr_reader :user
33
+ # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Guild}] A dictionary of guilds.
34
+ attr_reader :guilds
35
+ # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::User}] A dictionary of users.
36
+ attr_reader :users
37
+ # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Channel}] A dictionary of channels.
38
+ attr_reader :channels
39
+ # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Emoji}] A dictionary of emojis.
40
+ attr_reader :emojis
41
+ # @return [Discorb::Dictionary{Discorb::Snowflake => Discorb::Message}] A dictionary of messages.
42
+ attr_reader :messages
43
+ # @return [Array<Discorb::ApplicationCommand::Command>] The commands that the client is using.
44
+ attr_reader :commands
45
+ # @return [Float] The ping of the client.
46
+ # @note This will be calculated from heartbeat and heartbeat_ack.
47
+ # @return [nil] If not connected to the gateway.
48
+ attr_reader :ping
49
+ # @return [:initialized, :running, :closed] The status of the client.
50
+ attr_reader :status
51
+ # @return [Hash{String => Discorb::Extension}] The loaded extensions.
52
+ attr_reader :extensions
53
+ # @return [Hash{Integer => Discorb::Shard}] The shards of the client.
54
+ attr_reader :shards
55
+ # @private
56
+ # @return [Hash{Discorb::Snowflake => Discorb::ApplicationCommand::Command}] The commands on the top level.
57
+ attr_reader :callable_commands
58
+ # @private
59
+ # @return [{String => Thread::Mutex}] A hash of mutexes.
60
+ attr_reader :mutex
61
+
62
+ # @!attribute [r] session_id
63
+ # @return [String] The session ID of the client or current shard.
64
+ # @return [nil] If not connected to the gateway.
65
+ # @!attribute [r] shard
66
+ # @return [Discorb::Shard] The current shard. This is implemented with Thread variables.
67
+ # @return [nil] If client has no shard.
68
+ # @!attribute [r] shard_id
69
+ # @return [Integer] The current shard ID. This is implemented with Thread variables.
70
+ # @return [nil] If client has no shard.
71
+ # @!attribute [r] logger
72
+ # @return [Logger] The logger.
73
+
74
+ #
75
+ # Initializes a new client.
76
+ #
77
+ # @param [Discorb::AllowedMentions] allowed_mentions The allowed mentions that the client is using.
78
+ # @param [Discorb::Intents] intents The intents that the client is currently using.
79
+ # @param [Integer] message_caches The number of messages to cache.
80
+ # @param [Logger] logger The IO object to use for logging.
81
+ # @param [:debug, :info, :warn, :error, :critical] log_level The log level.
82
+ # @param [Boolean] wait_until_ready Whether to delay event dispatch until ready.
83
+ # @param [Boolean] fetch_member Whether to fetch member on ready. This may slow down the client. Default to `false`.
84
+ # @param [String] title
85
+ # The title of the process. `false` to default of ruby, `nil` to `discorb: User#0000`. Default to `nil`.
86
+ #
87
+ def initialize(
88
+ allowed_mentions: nil,
89
+ intents: nil,
90
+ message_caches: 1000,
91
+ logger: nil,
92
+ wait_until_ready: true,
93
+ fetch_member: false,
94
+ title: nil
95
+ )
96
+ @allowed_mentions =
97
+ allowed_mentions ||
98
+ AllowedMentions.new(everyone: true, roles: true, users: true)
99
+ @intents = (intents or Intents.default)
100
+ @events = {}
101
+ @api_version = nil
102
+ @logger =
103
+ logger || Logger.new($stdout, progname: "discorb", level: Logger::ERROR)
104
+ @user = nil
105
+ @users = Discorb::Dictionary.new
106
+ @channels = Discorb::Dictionary.new
107
+ @guilds = Discorb::Dictionary.new(sort: ->(k) { k[0].to_i })
108
+ @emojis = Discorb::Dictionary.new
109
+ @messages = Discorb::Dictionary.new(limit: message_caches)
110
+ @application = nil
111
+ @last_s = nil
112
+ @identify_presence = nil
113
+ @wait_until_ready = wait_until_ready
114
+ @ready = false
115
+ @tasks = []
116
+ @conditions = {}
117
+ @commands = []
118
+ @callable_commands = []
119
+ @status = :initialized
120
+ @fetch_member = fetch_member
121
+ @title = title
122
+ @extensions = {}
123
+ @mutex = {}
124
+ @shards = {}
125
+ set_default_events
126
+ end
127
+
128
+ #
129
+ # Registers an event handler.
130
+ # @see file:docs/Events.md Events Documentation
131
+ #
132
+ # @param [Symbol] event_name The name of the event.
133
+ # @param [Symbol] id Custom ID of the event.
134
+ # @param [Hash] metadata The metadata of the event.
135
+ # @param [Proc] block The block to execute when the event is triggered.
136
+ #
137
+ # @return [Discorb::EventHandler] The event.
138
+ #
139
+ def on(event_name, id: nil, **metadata, &block)
140
+ ne = EventHandler.new(block, id, metadata)
141
+ @events[event_name] ||= []
142
+ @events[event_name].delete_if { |e| e.metadata[:override] }
143
+ @events[event_name] << ne
144
+ ne
145
+ end
146
+
147
+ #
148
+ # Almost same as {#on}, but only triggers the event once.
149
+ #
150
+ # @param (see #on)
151
+ #
152
+ # @return [Discorb::EventHandler] The event.
153
+ #
154
+ def once(event_name, id: nil, **metadata, &block)
155
+ metadata[:once] = true
156
+ on(event_name, id: id, **metadata, &block)
157
+ end
158
+
159
+ #
160
+ # Remove event by ID.
161
+ #
162
+ # @param [Symbol] event_name The name of the event.
163
+ # @param [Symbol] id The ID of the event.
164
+ #
165
+ def remove_event(event_name, id)
166
+ @events[event_name].delete_if { |e| e.id == id }
167
+ end
168
+
169
+ #
170
+ # Dispatch an event.
171
+ # @async
172
+ #
173
+ # @param [Symbol] event_name The name of the event.
174
+ # @param [Object] args The arguments to pass to the event.
175
+ #
176
+ # @return [Async::Task<void>] The task.
177
+ #
178
+ def dispatch(event_name, *args)
179
+ Async do
180
+ if (conditions = @conditions[event_name])
181
+ ids = Set[*conditions.map(&:first).map(&:object_id)]
182
+ conditions.delete_if do |condition|
183
+ next unless ids.include?(condition.first.object_id)
184
+
185
+ check_result = condition[1].nil? || condition[1].call(*args)
186
+ if check_result
187
+ condition.first.signal(args)
188
+ true
189
+ else
190
+ false
191
+ end
192
+ end
193
+ end
194
+ events = @events[event_name].dup || []
195
+ if respond_to?("on_#{event_name}")
196
+ event_method = method("on_#{event_name}")
197
+ class << event_method
198
+ def id
199
+ "method"
200
+ end
201
+ end
202
+ events << event_method
203
+ end
204
+ if events.nil?
205
+ logger.debug "Event #{event_name} doesn't have any proc, skipping"
206
+ next
207
+ end
208
+ logger.debug "Dispatching event #{event_name}"
209
+ events.each do |block|
210
+ Async do
211
+ Async(annotation: "Discorb event: #{event_name}") do |_task|
212
+ if block.is_a?(Discorb::EventHandler) && block.metadata[:once]
213
+ @events[event_name].delete(block)
214
+ end
215
+ block.call(*args)
216
+ logger.debug "Dispatched proc with ID #{block.id.inspect}"
217
+ rescue StandardError, ScriptError => e
218
+ if event_name == :error
219
+ raise e
220
+ else
221
+ dispatch(:error, event_name, args, e)
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ #
230
+ # Fetch user from ID.
231
+ # @async
232
+ #
233
+ # @param [#to_s] id <description>
234
+ #
235
+ # @return [Async::Task<Discorb::User>] The user.
236
+ #
237
+ # @raise [Discorb::NotFoundError] If the user doesn't exist.
238
+ #
239
+ def fetch_user(id)
240
+ Async do
241
+ _resp, data =
242
+ @http.request(
243
+ Route.new("/users/#{id}", "//users/:user_id", :get)
244
+ ).wait
245
+ User.new(self, data)
246
+ end
247
+ end
248
+
249
+ #
250
+ # Fetch channel from ID.
251
+ # @async
252
+ #
253
+ # @param [#to_s] id The ID of the channel.
254
+ #
255
+ # @return [Async::Task<Discorb::Channel>] The channel.
256
+ #
257
+ # @raise [Discorb::NotFoundError] If the channel doesn't exist.
258
+ #
259
+ def fetch_channel(id)
260
+ Async do
261
+ _resp, data =
262
+ @http.request(
263
+ Route.new("/channels/#{id}", "//channels/:channel_id", :get)
264
+ ).wait
265
+ Channel.make_channel(self, data)
266
+ end
267
+ end
268
+
269
+ #
270
+ # Fetch guild from ID.
271
+ # @async
272
+ #
273
+ # @param [#to_s] id <description>
274
+ #
275
+ # @return [Async::Task<Discorb::Guild>] The guild.
276
+ #
277
+ # @raise [Discorb::NotFoundError] If the guild doesn't exist.
278
+ #
279
+ def fetch_guild(id)
280
+ Async do
281
+ _resp, data =
282
+ @http.request(
283
+ Route.new("/guilds/#{id}", "//guilds/:guild_id", :get)
284
+ ).wait
285
+ Guild.new(self, data, false)
286
+ end
287
+ end
288
+
289
+ #
290
+ # Fetch invite from code.
291
+ # @async
292
+ #
293
+ # @param [String] code The code of the invite.
294
+ # @param [Boolean] with_count Whether to include the count of the invite.
295
+ # @param [Boolean] with_expiration Whether to include the expiration of the invite.
296
+ #
297
+ # @return [Async::Task<Discorb::Invite>] The invite.
298
+ #
299
+ def fetch_invite(code, with_count: true, with_expiration: true)
300
+ Async do
301
+ _resp, data =
302
+ @http.request(
303
+ Route.new(
304
+ "/invites/#{code}?with_count=#{with_count}&with_expiration=#{with_expiration}",
305
+ "//invites/:code",
306
+ :get
307
+ )
308
+ ).wait
309
+ Invite.new(self, data, false)
310
+ end
311
+ end
312
+
313
+ #
314
+ # Fetch webhook from ID.
315
+ # If application was cached, it will be used.
316
+ # @async
317
+ #
318
+ # @param [Boolean] force Whether to force the fetch.
319
+ #
320
+ # @return [Async::Task<Discorb::Application>] The application.
321
+ #
322
+ def fetch_application(force: false)
323
+ Async do
324
+ next @application if @application && !force
325
+
326
+ _resp, data =
327
+ @http.request(
328
+ Route.new(
329
+ "/oauth2/applications/@me",
330
+ "//oauth2/applications/@me",
331
+ :get
332
+ )
333
+ ).wait
334
+ @application = Application.new(self, data)
335
+ @application
336
+ end
337
+ end
338
+
339
+ #
340
+ # Fetch nitro sticker pack from ID.
341
+ # @async
342
+ #
343
+ # @return [Async::Task<Array<Discorb::Sticker::Pack>>] The packs.
344
+ #
345
+ def fetch_nitro_sticker_packs
346
+ Async do
347
+ _resp, data =
348
+ @http.request(
349
+ Route.new("/sticker-packs", "//sticker-packs", :get)
350
+ ).wait
351
+ data[:sticker_packs].map { |pack| Sticker::Pack.new(self, pack) }
352
+ end
353
+ end
354
+
355
+ #
356
+ # Update presence of the client.
357
+ #
358
+ # @param [Discorb::Activity] activity The activity to update.
359
+ # @param [:online, :idle, :dnd, :invisible] status The status to update.
360
+ #
361
+ def update_presence(activity = nil, status: nil)
362
+ payload = { activities: [], status: status, since: nil, afk: nil }
363
+ payload[:activities] = [activity.to_hash] unless activity.nil?
364
+ payload[:status] = status unless status.nil?
365
+ if connection
366
+ Async { send_gateway(3, **payload) }
367
+ else
368
+ @identify_presence = payload
369
+ end
370
+ end
371
+
372
+ alias change_presence update_presence
373
+
374
+ #
375
+ # Method to wait for a event.
376
+ # @async
377
+ #
378
+ # @param [Symbol] event The name of the event.
379
+ # @param [Integer] timeout The timeout in seconds.
380
+ # @param [Proc] check The check to use.
381
+ #
382
+ # @return [Async::Task<Object>] The result of the event.
383
+ #
384
+ # @raise [Discorb::TimeoutError] If the event didn't occur in time.
385
+ #
386
+ def event_lock(event, timeout = nil, &check)
387
+ Async do |task|
388
+ condition = Async::Condition.new
389
+ @conditions[event] ||= []
390
+ @conditions[event] << [condition, check]
391
+ if timeout.nil?
392
+ value = condition.wait
393
+ else
394
+ timeout_task =
395
+ task.with_timeout(timeout) do
396
+ condition.wait
397
+ rescue Async::TimeoutError
398
+ @conditions[event].delete_if { |c| c.first == condition }
399
+ raise Discorb::TimeoutError,
400
+ "Timeout waiting for event #{event}",
401
+ cause: nil
402
+ end
403
+ value = timeout_task
404
+ end
405
+ value.length <= 1 ? value.first : value
406
+ end
407
+ end
408
+
409
+ alias await event_lock
410
+
411
+ def inspect
412
+ "#<#{self.class} user=\"#{user}\">"
413
+ end
414
+
415
+ #
416
+ # Load the extension.
417
+ #
418
+ # @param [Class, Discorb::Extension] ext The extension to load.
419
+ # @param [Object] ... The arguments to pass to the `ext#initialize`.
420
+ #
421
+ def load_extension(ext, ...)
422
+ case ext
423
+ when Class
424
+ unless ext < Discorb::Extension
425
+ raise ArgumentError, "#{ext} is not a extension"
426
+ end
427
+
428
+ ins = ext.new(self, ...)
429
+ when Discorb::Extension
430
+ ins = ext
431
+ else
432
+ raise ArgumentError, "#{ext} is not a extension"
433
+ end
434
+
435
+ @events.each_value do |event|
436
+ event.delete_if { |c| c.metadata[:extension] == ins.class.name }
437
+ end
438
+ ins.events.each do |name, events|
439
+ @events[name] ||= []
440
+ events.each { |event| @events[name] << event }
441
+ end
442
+ @commands.delete_if do |cmd|
443
+ cmd.respond_to? :extension and cmd.extension == ins.class.name
444
+ end
445
+ ins.class.commands.each do |cmd|
446
+ cmd.define_singleton_method(:extension) { ins.class.name }
447
+ cmd.replace_block(ins)
448
+ cmd.block.define_singleton_method(:self_replaced) { true }
449
+ @commands << cmd
450
+ end
451
+
452
+ cls = ins.class
453
+ cls.loaded(self, ...) if cls.respond_to? :loaded
454
+ ins.class.callable_commands.each do |cmd|
455
+ unless cmd.respond_to? :self_replaced
456
+ cmd.define_singleton_method(:extension) { ins.class.name }
457
+ cmd.replace_block(ins)
458
+ cmd.block.define_singleton_method(:self_replaced) { true }
459
+ end
460
+ @callable_commands << cmd
461
+ end
462
+ @extensions[ins.class.name] = ins
463
+ ins
464
+ end
465
+
466
+ include Discorb::Gateway::Handler
467
+ include Discorb::ApplicationCommand::Handler
468
+
469
+ #
470
+ # Starts the client.
471
+ # @note This method behavior will change by CLI.
472
+ # @see file:docs/cli.md CLI documentation
473
+ #
474
+ # @param [String, nil] token The token to use.
475
+ #
476
+ # @note If the token is nil, you should use `discorb run` with the `-e` or `--env` option.
477
+ #
478
+ def run(token = nil, shards: nil, shard_count: nil)
479
+ token ||= ENV.fetch("DISCORB_CLI_TOKEN", nil)
480
+ if token.nil?
481
+ raise ArgumentError,
482
+ "Token is not specified, and -e/--env is not specified"
483
+ end
484
+
485
+ case ENV.fetch("DISCORB_CLI_FLAG", nil)
486
+ when nil
487
+ start_client(token, shards: shards, shard_count: shard_count)
488
+ when "run"
489
+ before_run(token)
490
+ start_client(token, shards: shards, shard_count: shard_count)
491
+ when "setup"
492
+ run_setup(token)
493
+ end
494
+ end
495
+
496
+ #
497
+ # Stops the client.
498
+ #
499
+ def close
500
+ @shards.any? ? @shards.each_value(&:close) : @connection.send_close
501
+ @tasks.each(&:stop)
502
+ @status = :closed
503
+ end
504
+
505
+ def session_id
506
+ shard ? shard.session_id : @session_id
507
+ end
508
+
509
+ def logger
510
+ shard&.logger || @logger
511
+ end
512
+
513
+ def shard
514
+ Thread.current.thread_variable_get("shard")
515
+ end
516
+
517
+ def shard_id
518
+ Thread.current.thread_variable_get("shard_id")
519
+ end
520
+
521
+ private
522
+
523
+ def before_run(token)
524
+ require "json"
525
+ options =
526
+ JSON.parse(ENV.fetch("DISCORB_CLI_OPTIONS", nil), symbolize_names: true)
527
+ setup_commands(token) if options[:setup]
528
+ end
529
+
530
+ def run_setup(token)
531
+ # @type var guild_ids: Array[String] | false
532
+ guild_ids = false
533
+ if guilds = ENV.fetch("DISCORB_SETUP_GUILDS", nil)
534
+ guild_ids = guilds.split(",")
535
+ end
536
+ guild_ids = false if guild_ids == ["global"]
537
+ setup_commands(token, guild_ids: guild_ids).wait
538
+ clear_commands(
539
+ token,
540
+ ENV.fetch("DISCORB_SETUP_CLEAR_GUILDS", "").split(",")
541
+ )
542
+ if ENV.fetch("DISCORB_SETUP_SCRIPT", nil) == "true"
543
+ @events[:setup]&.each(&:call)
544
+ on_setup if respond_to? :on_setup
545
+ end
546
+ end
547
+
548
+ def set_status(status, shard)
549
+ if shard.nil?
550
+ @status = status
551
+ else
552
+ @shards[shard].status = status
553
+ end
554
+ end
555
+
556
+ def connection
557
+ shard_id ? @shards[shard_id].connection : @connection
558
+ end
559
+
560
+ def connection=(value)
561
+ if shard_id
562
+ @shards[shard_id].connection = value
563
+ else
564
+ @connection = value
565
+ end
566
+ end
567
+
568
+ def session_id=(value)
569
+ sid = shard_id
570
+ if sid
571
+ @shards[sid].session_id = value
572
+ else
573
+ @session_id = value
574
+ end
575
+ end
576
+
577
+ def start_client(token, shards: nil, shard_count: nil)
578
+ @token = token.to_s
579
+ @shard_count = shard_count
580
+ Signal.trap(:SIGINT) do
581
+ logger.info "SIGINT received, closing..."
582
+ Signal.trap(:SIGINT, "DEFAULT")
583
+ close
584
+ end
585
+ if shards.nil?
586
+ main_loop(nil)
587
+ else
588
+ @shards =
589
+ shards.to_h.with_index do |shard, i|
590
+ [shard, Shard.new(self, shard, shard_count, i)]
591
+ end
592
+ @shards.values[..-1].each_with_index do |shard, i|
593
+ shard.next_shard = @shards.values[i + 1]
594
+ end
595
+ @shards.each_value { |s| s.thread.join }
596
+ end
597
+ end
598
+
599
+ def main_loop(shard)
600
+ set_status(:running, shard)
601
+ connect_gateway(false).wait
602
+ rescue StandardError
603
+ set_status(:closed, shard)
604
+ raise
605
+ end
606
+
607
+ def main_task
608
+ shard_id ? shard.main_task : @main_task
609
+ end
610
+
611
+ def main_task=(value)
612
+ if shard_id
613
+ shard.main_task = value
614
+ else
615
+ @main_task = value
616
+ end
617
+ end
618
+
619
+ def set_default_events
620
+ on :error, override: true do |event_name, _args, e|
621
+ message =
622
+ "An error occurred while dispatching #{event_name}:\n#{e.full_message}"
623
+ logger.error message
624
+ end
625
+
626
+ once :standby do
627
+ next if @title == false
628
+
629
+ title =
630
+ @title || ENV.fetch("DISCORB_CLI_TITLE", nil) || "discorb: #{@user}"
631
+ Process.setproctitle title
632
+ end
633
+ end
634
+ end
635
+ end