discorb 0.19.0 → 0.20.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.
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