onyxcord 1.1.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 (133) hide show
  1. checksums.yaml +7 -0
  2. data/.devcontainer/Dockerfile +13 -0
  3. data/.devcontainer/devcontainer.json +29 -0
  4. data/.devcontainer/postcreate.sh +4 -0
  5. data/.github/CONTRIBUTING.md +13 -0
  6. data/.github/ISSUE_TEMPLATE/bug_report.md +38 -0
  7. data/.github/ISSUE_TEMPLATE/feature_request.md +24 -0
  8. data/.github/pull_request_template.md +37 -0
  9. data/.github/workflows/ci.yml +78 -0
  10. data/.github/workflows/codeql.yml +65 -0
  11. data/.github/workflows/deploy.yml +54 -0
  12. data/.github/workflows/release.yml +51 -0
  13. data/.gitignore +16 -0
  14. data/.markdownlint.json +4 -0
  15. data/.overcommit.yml +7 -0
  16. data/.rspec +2 -0
  17. data/.rubocop.yml +129 -0
  18. data/.yardopts +1 -0
  19. data/CHANGELOG.md +0 -0
  20. data/Gemfile +7 -0
  21. data/LICENSE.txt +21 -0
  22. data/README.md +305 -0
  23. data/Rakefile +17 -0
  24. data/bin/console +15 -0
  25. data/bin/setup +7 -0
  26. data/lib/onyxcord/allowed_mentions.rb +43 -0
  27. data/lib/onyxcord/api/application.rb +316 -0
  28. data/lib/onyxcord/api/channel.rb +700 -0
  29. data/lib/onyxcord/api/interaction.rb +67 -0
  30. data/lib/onyxcord/api/invite.rb +44 -0
  31. data/lib/onyxcord/api/server.rb +775 -0
  32. data/lib/onyxcord/api/user.rb +158 -0
  33. data/lib/onyxcord/api/webhook.rb +163 -0
  34. data/lib/onyxcord/api.rb +335 -0
  35. data/lib/onyxcord/await.rb +51 -0
  36. data/lib/onyxcord/bot.rb +1971 -0
  37. data/lib/onyxcord/cache.rb +326 -0
  38. data/lib/onyxcord/colour_rgb.rb +43 -0
  39. data/lib/onyxcord/commands/command_bot.rb +511 -0
  40. data/lib/onyxcord/commands/container.rb +112 -0
  41. data/lib/onyxcord/commands/events.rb +11 -0
  42. data/lib/onyxcord/commands/parser.rb +327 -0
  43. data/lib/onyxcord/commands/rate_limiter.rb +144 -0
  44. data/lib/onyxcord/configuration.rb +125 -0
  45. data/lib/onyxcord/container.rb +988 -0
  46. data/lib/onyxcord/data/activity.rb +271 -0
  47. data/lib/onyxcord/data/application.rb +341 -0
  48. data/lib/onyxcord/data/attachment.rb +91 -0
  49. data/lib/onyxcord/data/audit_logs.rb +438 -0
  50. data/lib/onyxcord/data/avatar_decoration.rb +26 -0
  51. data/lib/onyxcord/data/call.rb +22 -0
  52. data/lib/onyxcord/data/channel.rb +1355 -0
  53. data/lib/onyxcord/data/channel_tag.rb +69 -0
  54. data/lib/onyxcord/data/collectibles.rb +47 -0
  55. data/lib/onyxcord/data/component.rb +583 -0
  56. data/lib/onyxcord/data/embed.rb +258 -0
  57. data/lib/onyxcord/data/emoji.rb +123 -0
  58. data/lib/onyxcord/data/install_params.rb +24 -0
  59. data/lib/onyxcord/data/integration.rb +144 -0
  60. data/lib/onyxcord/data/interaction.rb +1141 -0
  61. data/lib/onyxcord/data/invite.rb +137 -0
  62. data/lib/onyxcord/data/member.rb +528 -0
  63. data/lib/onyxcord/data/message.rb +612 -0
  64. data/lib/onyxcord/data/message_activity.rb +41 -0
  65. data/lib/onyxcord/data/overwrite.rb +109 -0
  66. data/lib/onyxcord/data/poll.rb +365 -0
  67. data/lib/onyxcord/data/primary_server.rb +60 -0
  68. data/lib/onyxcord/data/profile.rb +79 -0
  69. data/lib/onyxcord/data/reaction.rb +64 -0
  70. data/lib/onyxcord/data/recipient.rb +34 -0
  71. data/lib/onyxcord/data/role.rb +449 -0
  72. data/lib/onyxcord/data/role_connection_data.rb +69 -0
  73. data/lib/onyxcord/data/role_subscription.rb +41 -0
  74. data/lib/onyxcord/data/scheduled_event.rb +513 -0
  75. data/lib/onyxcord/data/server.rb +1614 -0
  76. data/lib/onyxcord/data/server_preview.rb +68 -0
  77. data/lib/onyxcord/data/snapshot.rb +112 -0
  78. data/lib/onyxcord/data/team.rb +98 -0
  79. data/lib/onyxcord/data/timestamp.rb +69 -0
  80. data/lib/onyxcord/data/user.rb +324 -0
  81. data/lib/onyxcord/data/voice_region.rb +46 -0
  82. data/lib/onyxcord/data/voice_state.rb +41 -0
  83. data/lib/onyxcord/data/webhook.rb +238 -0
  84. data/lib/onyxcord/data.rb +57 -0
  85. data/lib/onyxcord/errors.rb +246 -0
  86. data/lib/onyxcord/event_executor.rb +80 -0
  87. data/lib/onyxcord/events/await.rb +48 -0
  88. data/lib/onyxcord/events/bans.rb +60 -0
  89. data/lib/onyxcord/events/channels.rb +225 -0
  90. data/lib/onyxcord/events/generic.rb +129 -0
  91. data/lib/onyxcord/events/guilds.rb +269 -0
  92. data/lib/onyxcord/events/integrations.rb +100 -0
  93. data/lib/onyxcord/events/interactions.rb +624 -0
  94. data/lib/onyxcord/events/invites.rb +127 -0
  95. data/lib/onyxcord/events/lifetime.rb +31 -0
  96. data/lib/onyxcord/events/members.rb +110 -0
  97. data/lib/onyxcord/events/message.rb +399 -0
  98. data/lib/onyxcord/events/polls.rb +118 -0
  99. data/lib/onyxcord/events/presence.rb +131 -0
  100. data/lib/onyxcord/events/raw.rb +74 -0
  101. data/lib/onyxcord/events/reactions.rb +218 -0
  102. data/lib/onyxcord/events/roles.rb +87 -0
  103. data/lib/onyxcord/events/scheduled_events.rb +171 -0
  104. data/lib/onyxcord/events/threads.rb +100 -0
  105. data/lib/onyxcord/events/typing.rb +73 -0
  106. data/lib/onyxcord/events/voice_server_update.rb +48 -0
  107. data/lib/onyxcord/events/voice_state_update.rb +106 -0
  108. data/lib/onyxcord/events/webhooks.rb +65 -0
  109. data/lib/onyxcord/gateway.rb +890 -0
  110. data/lib/onyxcord/id_object.rb +39 -0
  111. data/lib/onyxcord/light/data.rb +62 -0
  112. data/lib/onyxcord/light/integrations.rb +73 -0
  113. data/lib/onyxcord/light/light_bot.rb +58 -0
  114. data/lib/onyxcord/light.rb +8 -0
  115. data/lib/onyxcord/logger.rb +120 -0
  116. data/lib/onyxcord/message_components.rb +70 -0
  117. data/lib/onyxcord/paginator.rb +60 -0
  118. data/lib/onyxcord/permissions.rb +255 -0
  119. data/lib/onyxcord/rate_limiter/gateway.rb +42 -0
  120. data/lib/onyxcord/rate_limiter/rest.rb +89 -0
  121. data/lib/onyxcord/version.rb +7 -0
  122. data/lib/onyxcord/voice/encoder.rb +115 -0
  123. data/lib/onyxcord/voice/network.rb +380 -0
  124. data/lib/onyxcord/voice/opcodes.rb +29 -0
  125. data/lib/onyxcord/voice/sodium.rb +157 -0
  126. data/lib/onyxcord/voice/timer.rb +19 -0
  127. data/lib/onyxcord/voice/voice_bot.rb +386 -0
  128. data/lib/onyxcord/webhooks.rb +14 -0
  129. data/lib/onyxcord/websocket.rb +62 -0
  130. data/lib/onyxcord.rb +180 -0
  131. data/onyxcord-webhooks.gemspec +30 -0
  132. data/onyxcord.gemspec +50 -0
  133. metadata +421 -0
@@ -0,0 +1,1971 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rest-client'
4
+ require 'zlib'
5
+
6
+ require 'onyxcord/configuration'
7
+ require 'onyxcord/event_executor'
8
+ require 'onyxcord/events/message'
9
+ require 'onyxcord/events/typing'
10
+ require 'onyxcord/events/lifetime'
11
+ require 'onyxcord/events/presence'
12
+ require 'onyxcord/events/voice_state_update'
13
+ require 'onyxcord/events/channels'
14
+ require 'onyxcord/events/members'
15
+ require 'onyxcord/events/roles'
16
+ require 'onyxcord/events/guilds'
17
+ require 'onyxcord/events/await'
18
+ require 'onyxcord/events/bans'
19
+ require 'onyxcord/events/raw'
20
+ require 'onyxcord/events/reactions'
21
+ require 'onyxcord/events/webhooks'
22
+ require 'onyxcord/events/invites'
23
+ require 'onyxcord/events/interactions'
24
+ require 'onyxcord/events/threads'
25
+ require 'onyxcord/events/integrations'
26
+ require 'onyxcord/events/scheduled_events'
27
+ require 'onyxcord/events/polls'
28
+
29
+ require 'onyxcord/api'
30
+ require 'onyxcord/api/channel'
31
+ require 'onyxcord/api/server'
32
+ require 'onyxcord/api/invite'
33
+ require 'onyxcord/api/interaction'
34
+ require 'onyxcord/api/application'
35
+
36
+ require 'onyxcord/errors'
37
+ require 'onyxcord/message_components'
38
+ require 'onyxcord/data'
39
+ require 'onyxcord/await'
40
+ require 'onyxcord/container'
41
+ require 'onyxcord/websocket'
42
+ require 'onyxcord/cache'
43
+ require 'onyxcord/gateway'
44
+
45
+ require 'onyxcord/voice/voice_bot'
46
+
47
+ module OnyxCord
48
+ # Represents a Discord bot, including servers, users, etc.
49
+ class Bot
50
+ # The list of currently running threads used to parse and call events.
51
+ # The threads will have a local variable `:onyxcord_name` in the format of `et-1234`, where
52
+ # "et" stands for "event thread" and the number is a continually incrementing number representing
53
+ # how many events were executed before.
54
+ # @return [Array<Thread>] The threads.
55
+ attr_reader :event_threads
56
+
57
+ # @return [true, false] whether or not the bot should parse its own messages. Off by default.
58
+ attr_accessor :should_parse_self
59
+
60
+ # The bot's name which onyxcord sends to Discord when making any request, so Discord can identify bots with the
61
+ # same codebase. Not required but I recommend setting it anyway.
62
+ # @return [String] The bot's name.
63
+ attr_accessor :name
64
+
65
+ # @return [Array(Integer, Integer)] the current shard key
66
+ attr_reader :shard_key
67
+
68
+ # @return [Hash<Symbol => Await>] the list of registered {Await}s.
69
+ attr_reader :awaits
70
+
71
+ # The gateway connection is an internal detail that is useless to most people. It is however essential while
72
+ # debugging or developing onyxcord itself, or while writing very custom bots.
73
+ # @return [Gateway] the underlying {Gateway} object.
74
+ attr_reader :gateway
75
+
76
+ # @return [:raw, :hybrid, :object] the dispatch mode used by this bot.
77
+ attr_reader :mode
78
+
79
+ # @return [Hash] the normalized cache policy for this bot.
80
+ attr_reader :cache_policy
81
+
82
+ # @return [OnyxCord::EventExecutor::Inline, OnyxCord::EventExecutor::Pool]
83
+ attr_reader :event_executor
84
+
85
+ include EventContainer
86
+ include Cache
87
+
88
+ # Makes a new bot with the given authentication data. It will be ready to be added event handlers to and can
89
+ # eventually be run with {#run}.
90
+ #
91
+ # As support for logging in using username and password has been removed in version 3.0.0, only a token login is
92
+ # possible. Be sure to specify the `type` parameter as `:user` if you're logging in as a user.
93
+ #
94
+ # Simply creating a bot won't be enough to start sending messages etc. with, only a limited set of methods can
95
+ # be used after logging in. If you want to do something when the bot has connected successfully, either do it in the
96
+ # {#ready} event, or use the {#run} method with the :async parameter and do the processing after that.
97
+ # @param log_mode [Symbol] The mode this bot should use for logging. See {Logger#mode=} for a list of modes.
98
+ # @param token [String] The token that should be used to log in. If your bot is a bot account, you have to specify
99
+ # this. If you're logging in as a user, make sure to also set the account type to :user so onyxcord doesn't think
100
+ # you're trying to log in as a bot.
101
+ # @param client_id [Integer] If you're logging in as a bot, the bot's client ID. This is optional, and may be fetched
102
+ # from the API by calling {Bot#bot_application} (see {Application}).
103
+ # @param type [Symbol] This parameter lets you manually overwrite the account type. This needs to be set when
104
+ # logging in as a user, otherwise onyxcord will treat you as a bot account. Valid values are `:user` and `:bot`.
105
+ # @param name [String] Your bot's name. This will be sent to Discord with any API requests, who will use this to
106
+ # trace the source of excessive API requests; it's recommended to set this to something if you make bots that many
107
+ # people will host on their servers separately.
108
+ # @param fancy_log [true, false] Whether the output log should be made extra fancy using ANSI escape codes. (Your
109
+ # terminal may not support this.)
110
+ # @param suppress_ready [true, false] Whether the READY packet should be exempt from being printed to console.
111
+ # Useful for very large bots running in debug or verbose log_mode.
112
+ # @param parse_self [true, false] Whether the bot should react on its own messages. It's best to turn this off
113
+ # unless you really need this so you don't inadvertently create infinite loops.
114
+ # @param shard_id [Integer] The number of the shard this bot should handle. See
115
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
116
+ # @param num_shards [Integer] The total number of shards that should be running. See
117
+ # https://github.com/discord/discord-api-docs/issues/17 for how to do sharding.
118
+ # @param redact_token [true, false] Whether the bot should redact the token in logs. Default is true.
119
+ # @param ignore_bots [true, false] Whether the bot should ignore bot accounts or not. Default is false.
120
+ # @param compress_mode [:none, :large, :stream] Sets which compression mode should be used when connecting
121
+ # to Discord's gateway. `:none` will request that no payloads are received compressed (not recommended for
122
+ # production bots). `:large` will request that large payloads are received compressed. `:stream` will request
123
+ # that all data be received in a continuous compressed stream.
124
+ # @param intents [:all, :unprivileged, Array<Symbol>, :none, Integer] Gateway intents that this bot requires. `:all` will
125
+ # request all intents. `:unprivileged` will request only intents that are not defined as "Privileged". `:none`
126
+ # will request no intents. An array of symbols will request only those intents specified. An integer value will request
127
+ # exactly all the intents specified in the bitwise value.
128
+ # @see OnyxCord::INTENTS
129
+ def initialize(
130
+ log_mode: :normal,
131
+ token: nil, client_id: nil,
132
+ type: nil, name: '', fancy_log: false, suppress_ready: false, parse_self: false,
133
+ shard_id: nil, num_shards: nil, redact_token: true, ignore_bots: false,
134
+ compress_mode: :large, intents: :minimal,
135
+ mode: nil, cache: nil, event_executor: nil, event_workers: nil
136
+ )
137
+ config = OnyxCord.configuration
138
+ @mode = config.normalize_mode(mode)
139
+ @cache_policy = config.normalize_cache(cache)
140
+ executor_type = config.normalize_event_executor(event_executor)
141
+ executor_workers = config.normalize_event_workers(event_workers)
142
+
143
+ LOGGER.mode = log_mode
144
+ LOGGER.token = token if redact_token
145
+
146
+ @should_parse_self = parse_self
147
+
148
+ @client_id = client_id
149
+
150
+ @type = type || :bot
151
+ @name = name
152
+
153
+ @shard_key = num_shards ? [shard_id, num_shards] : nil
154
+
155
+ LOGGER.fancy = fancy_log
156
+ @prevent_ready = suppress_ready
157
+
158
+ @compress_mode = compress_mode
159
+
160
+ raise 'Token string is empty or nil' if token.nil? || token.empty?
161
+
162
+ @intents = case intents
163
+ when :all
164
+ ALL_INTENTS
165
+ when :unprivileged
166
+ UNPRIVILEGED_INTENTS
167
+ when :minimal
168
+ MINIMAL_INTENTS
169
+ when :none
170
+ NO_INTENTS
171
+ else
172
+ calculate_intents(intents)
173
+ end
174
+
175
+ @token = process_token(@type, token)
176
+ @gateway = Gateway.new(self, @token, @shard_key, @compress_mode, @intents)
177
+
178
+ init_cache
179
+
180
+ @voices = {}
181
+ @should_connect_to_voice = {}
182
+
183
+ @ignored_ids = Set.new
184
+ @ignore_bots = ignore_bots
185
+
186
+ @current_thread = 0
187
+ @current_thread_mutex = Mutex.new
188
+ @event_executor = EventExecutor.build(executor_type, workers: executor_workers)
189
+ @event_threads = @event_executor.threads
190
+
191
+ @status = :online
192
+
193
+ @application_commands = {}
194
+ @request_members_rl = {}
195
+ end
196
+
197
+ # The list of users the bot shares a server with.
198
+ # @return [Hash<Integer => User>] The users by ID.
199
+ def users
200
+ gateway_check
201
+ unavailable_servers_check
202
+ @users
203
+ end
204
+
205
+ # The list of servers the bot is currently in.
206
+ # @return [Hash<Integer => Server>] The servers by ID.
207
+ def servers
208
+ gateway_check
209
+ unavailable_servers_check
210
+ @servers
211
+ end
212
+
213
+ # The list of members in threads the bot can see.
214
+ # @return [Hash<Integer => Hash<Integer => Hash<String => Object>>]
215
+ def thread_members
216
+ gateway_check
217
+ unavailable_servers_check
218
+ @thread_members
219
+ end
220
+
221
+ # @overload emoji(id)
222
+ # Return an emoji by its ID
223
+ # @param id [String, Integer] The emoji's ID.
224
+ # @return [Emoji, nil] the emoji object. `nil` if the emoji was not found.
225
+ # @overload emoji
226
+ # The list of emoji the bot can use.
227
+ # @return [Array<Emoji>] the emoji available.
228
+ def emoji(id = nil)
229
+ if (id = id&.resolve_id)
230
+ @servers.each_value do |server|
231
+ emoji = server.emojis[id]
232
+ return emoji if emoji
233
+ end
234
+ else
235
+ hash = {}
236
+ @servers.each_value do |server|
237
+ hash.merge!(server.emojis)
238
+ end
239
+
240
+ hash
241
+ end
242
+ end
243
+
244
+ alias_method :emojis, :emoji
245
+ alias_method :all_emoji, :emoji
246
+
247
+ # Finds an emoji by its name.
248
+ # @param name [String] The emoji name that should be resolved.
249
+ # @return [GlobalEmoji, nil] the emoji identified by the name, or `nil` if it couldn't be found.
250
+ def find_emoji(name)
251
+ LOGGER.out("Resolving emoji #{name}")
252
+ emoji.find { |element| element.name == name }
253
+ end
254
+
255
+ # The bot's user profile. This special user object can be used
256
+ # to edit user data like the current username (see {Profile#username=}).
257
+ # @return [Profile] The bot's profile that can be used to edit data.
258
+ def profile
259
+ return @profile if @profile
260
+
261
+ response = OnyxCord::API::User.profile(@token)
262
+ @profile = Profile.new(JSON.parse(response), self)
263
+ end
264
+
265
+ alias_method :bot_user, :profile
266
+
267
+ # The bot's OAuth application.
268
+ # @return [Application] The bot's application info.
269
+ def bot_application
270
+ response = API.oauth_application(token)
271
+ Application.new(JSON.parse(response), self)
272
+ end
273
+
274
+ alias_method :bot_app, :bot_application
275
+ alias_method :application, :bot_application
276
+
277
+ # Get the role connection metadata records associated with this application.
278
+ # @return [Array<RoleConnectionMetadata>] the role connection metadata records associated with this application.
279
+ def role_connection_metadata_records
280
+ response = API::Application.get_application_role_connection_metadata_records(@bot.token, @id)
281
+ JSON.parse(response).map { |role_connection| RoleConnectionMetadata.new(role_connection, @bot) }
282
+ end
283
+
284
+ # The Discord API token received when logging in. Useful to explicitly call
285
+ # {API} methods.
286
+ # @return [String] The API token.
287
+ def token
288
+ API.bot_name = @name
289
+ @token
290
+ end
291
+
292
+ # @return [String] the raw token, without any prefix
293
+ # @see #token
294
+ def raw_token
295
+ @token.split(' ').last
296
+ end
297
+
298
+ # Runs the bot, which logs into Discord and connects the WebSocket. This
299
+ # prevents all further execution unless it is executed with
300
+ # `background` = `true`.
301
+ # @param background [true, false] If it is `true`, then the bot will run in
302
+ # another thread to allow further execution. If it is `false`, this method
303
+ # will block until {#stop} is called. If the bot is run with `true`, make
304
+ # sure to eventually call {#join} so the script doesn't stop prematurely.
305
+ # @note Running the bot in the background means that you can call some
306
+ # methods that require a gateway connection *before* that connection is
307
+ # established. In most cases an exception will be raised if you try to do
308
+ # this. If you need a way to safely run code after the bot is fully
309
+ # connected, use a {#ready} event handler instead.
310
+ def run(background = false)
311
+ @gateway.run_async
312
+ return if background
313
+
314
+ debug('Oh wait! Not exiting yet as run was run synchronously.')
315
+ @gateway.sync
316
+ end
317
+
318
+ # Joins the bot's connection thread with the current thread.
319
+ # This blocks execution until the websocket stops, which should only happen
320
+ # manually triggered. or due to an error. This is necessary to have a
321
+ # continuously running bot.
322
+ def join
323
+ @gateway.sync
324
+ end
325
+ alias_method :sync, :join
326
+
327
+ # Stops the bot gracefully, disconnecting the websocket without immediately killing the thread. This means that
328
+ # Discord is immediately aware of the closed connection and makes the bot appear offline instantly.
329
+ # @note This method no longer takes an argument as of 3.4.0
330
+ def stop(_no_sync = nil)
331
+ @gateway.stop
332
+ @event_executor.shutdown
333
+ nil
334
+ end
335
+
336
+ # @return [true, false] whether or not the bot is currently connected to Discord.
337
+ def connected?
338
+ @gateway.open?
339
+ end
340
+
341
+ # Makes the bot join an invite to a server.
342
+ # @param invite [String, Invite] The invite to join. For possible formats see {#resolve_invite_code}.
343
+ def accept_invite(invite)
344
+ resolved = invite(invite).code
345
+ API::Invite.accept(token, resolved)
346
+ end
347
+
348
+ # Creates an OAuth invite URL that can be used to invite this bot to a particular server.
349
+ # @param server [Server, nil] The server the bot should be invited to, or nil if a general invite should be created.
350
+ # @param permission_bits [String, Integer] Permission bits that should be appended to invite url.
351
+ # @param redirect_uri [String] Redirect URI that should be appended to invite url.
352
+ # @param scopes [Array<String>] Scopes that should be appended to invite url.
353
+ # @return [String] the OAuth invite URL.
354
+ def invite_url(server: nil, permission_bits: nil, redirect_uri: nil, scopes: ['bot'])
355
+ @client_id ||= bot_application.id
356
+
357
+ query = URI.encode_www_form({
358
+ client_id: @client_id,
359
+ guild_id: server&.id,
360
+ permissions: permission_bits,
361
+ redirect_uri: redirect_uri,
362
+ scope: scopes.join(' ')
363
+ }.compact)
364
+
365
+ "https://discord.com/oauth2/authorize?#{query}"
366
+ end
367
+
368
+ # @return [Hash<Integer => VoiceBot>] the voice connections this bot currently has, by the server ID to which they are connected.
369
+ attr_reader :voices
370
+
371
+ # Gets the voice bot for a particular server or channel. You can connect to a new channel using the {#voice_connect}
372
+ # method.
373
+ # @param thing [Channel, Server, Integer] the server or channel you want to get the voice bot for, or its ID.
374
+ # @return [Voice::VoiceBot, nil] the VoiceBot for the thing you specified, or nil if there is no connection yet
375
+ def voice(thing)
376
+ id = thing.resolve_id
377
+ return @voices[id] if @voices[id]
378
+
379
+ channel = channel(id)
380
+ return nil unless channel
381
+
382
+ server_id = channel.server.id
383
+ return @voices[server_id] if @voices[server_id]
384
+ end
385
+
386
+ # Connects to a voice channel, initializes network connections and returns the {Voice::VoiceBot} over which audio
387
+ # data can then be sent. After connecting, the bot can also be accessed using {#voice}. If the bot is already
388
+ # connected to voice, the existing connection will be terminated - you don't have to call
389
+ # {OnyxCord::Voice::VoiceBot#destroy} before calling this method.
390
+ # @param chan [Channel, String, Integer] The voice channel, or its ID, to connect to.
391
+ # @param encrypted [true, false] Whether voice communication should be encrypted using
392
+ # (uses an XSalsa20 stream cipher for encryption and Poly1305 for authentication)
393
+ # @return [Voice::VoiceBot] the initialized bot over which audio data can then be sent.
394
+ def voice_connect(chan, encrypted = true)
395
+ raise ArgumentError, 'Unencrypted voice connections are no longer supported.' unless encrypted
396
+
397
+ chan = channel(chan.resolve_id)
398
+ server_id = chan.server.id
399
+
400
+ if @voices[chan.id]
401
+ debug('Voice bot exists already! Destroying it')
402
+ @voices[chan.id].destroy
403
+ @voices.delete(chan.id)
404
+ end
405
+
406
+ debug("Got voice channel: #{chan}")
407
+
408
+ @should_connect_to_voice[server_id] = chan
409
+ @gateway.send_voice_state_update(server_id.to_s, chan.id.to_s, false, false)
410
+
411
+ debug('Voice channel init packet sent! Now waiting.')
412
+
413
+ sleep(0.05) until @voices[server_id]
414
+ debug('Voice connect succeeded!')
415
+ @voices[server_id]
416
+ end
417
+
418
+ # Disconnects the client from a specific voice connection given the server ID. Usually it's more convenient to use
419
+ # {OnyxCord::Voice::VoiceBot#destroy} rather than this.
420
+ # @param server [Server, String, Integer] The server, or server ID, the voice connection is on.
421
+ # @param destroy_vws [true, false] Whether or not the VWS should also be destroyed. If you're calling this method
422
+ # directly, you should leave it as true.
423
+ def voice_destroy(server, destroy_vws = true)
424
+ server = server.resolve_id
425
+ @gateway.send_voice_state_update(server.to_s, nil, false, false)
426
+ @voices[server].destroy if @voices[server] && destroy_vws
427
+ @voices.delete(server)
428
+ end
429
+
430
+ # Revokes an invite to a server. Will fail unless you have the *Manage Server* permission.
431
+ # It is recommended that you use {Invite#delete} instead.
432
+ # @param code [String, Invite] The invite to revoke. For possible formats see {#resolve_invite_code}.
433
+ def delete_invite(code)
434
+ invite = resolve_invite_code(code)
435
+ API::Invite.delete(token, invite)
436
+ end
437
+
438
+ # Sends a text message to a channel given its ID and the message's content.
439
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
440
+ # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
441
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
442
+ # @param embeds [Hash, OnyxCord::Webhooks::Embed, Array<Hash>, Array<OnyxCord::Webhooks::Embed> nil] The rich embed(s) to append to this message.
443
+ # @param allowed_mentions [Hash, OnyxCord::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
444
+ # @param message_reference [Message, String, Integer, Hash, nil] The message, or message ID, to reply to if any.
445
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
446
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2), SUPPRESS_NOTIFICATIONS (1 << 12), and IS_COMPONENTS_V2 (1 << 15) can be set.
447
+ # @param nonce [String, nil] A optional nonce in order to verify that a message was sent. Maximum of twenty-five characters.
448
+ # @param enforce_nonce [true, false] Whether the nonce should be enforced and used for message de-duplication.
449
+ # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to this message.
450
+ # @return [Message] The message that was sent.
451
+ def send_message(channel, content, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil)
452
+ channel = channel.resolve_id
453
+ debug("Sending message to #{channel} with content '#{content}'")
454
+ allowed_mentions = { parse: [] } if allowed_mentions == false
455
+ message_reference = { message_id: message_reference.resolve_id } if message_reference.respond_to?(:resolve_id)
456
+ embeds = (embeds.instance_of?(Array) ? embeds.map(&:to_hash) : [embeds&.to_hash]).compact
457
+ flags = OnyxCord::MessageComponents.apply_v2_flag(flags, components)
458
+
459
+ response = API::Channel.create_message(token, channel, content, tts, embeds, nonce, attachments, allowed_mentions&.to_hash, message_reference, components, flags, enforce_nonce, poll&.to_h)
460
+ Message.new(JSON.parse(response), self)
461
+ end
462
+
463
+ # Sends a text message to a channel given its ID and the message's content,
464
+ # then deletes it after the specified timeout in seconds.
465
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
466
+ # @param content [String] The text that should be sent as a message. It is limited to 2000 characters (Discord imposed).
467
+ # @param timeout [Float] The amount of time in seconds after which the message sent will be deleted.
468
+ # @param tts [true, false] Whether or not this message should be sent using Discord text-to-speech.
469
+ # @param embeds [Hash, OnyxCord::Webhooks::Embed, Array<Hash>, Array<OnyxCord::Webhooks::Embed> nil] The rich embed(s) to append to this message.
470
+ # @param attachments [Array<File>] Files that can be referenced in embeds via `attachment://file.png`
471
+ # @param allowed_mentions [Hash, OnyxCord::AllowedMentions, false, nil] Mentions that are allowed to ping on this message. `false` disables all pings
472
+ # @param message_reference [Message, String, Integer, nil] The message, or message ID, to reply to if any.
473
+ # @param components [View, Array<Hash>] Interaction components to associate with this message.
474
+ # @param flags [Integer] Flags for this message. Currently only SUPPRESS_EMBEDS (1 << 2), SUPPRESS_NOTIFICATIONS (1 << 12), and IS_COMPONENTS_V2 (1 << 15) can be set.
475
+ # @param nonce [String, nil] A optional nonce in order to verify that a message was sent. Maximum of twenty-five characters.
476
+ # @param enforce_nonce [true, false] Whether the nonce should be enforced and used for message de-duplication.
477
+ # @param poll [Hash, Poll::Builder, Poll, nil] The poll that should be attached to this message.
478
+ def send_temporary_message(channel, content, timeout, tts = false, embeds = nil, attachments = nil, allowed_mentions = nil, message_reference = nil, components = nil, flags = 0, nonce = nil, enforce_nonce = false, poll = nil)
479
+ Thread.new do
480
+ Thread.current[:onyxcord_name] = "#{@current_thread}-temp-msg"
481
+
482
+ message = send_message(channel, content, tts, embeds, attachments, allowed_mentions, message_reference, components, flags, nonce, enforce_nonce, poll)
483
+ sleep(timeout)
484
+ message.delete
485
+ end
486
+
487
+ nil
488
+ end
489
+
490
+ # Sends a file to a channel. If it is an image, it will automatically be embedded.
491
+ # @note This executes in a blocking way, so if you're sending long files, be wary of delays.
492
+ # @param channel [Channel, String, Integer] The channel, or its ID, to send something to.
493
+ # @param file [File] The file that should be sent.
494
+ # @param caption [string] The caption for the file.
495
+ # @param tts [true, false] Whether or not this file's caption should be sent using Discord text-to-speech.
496
+ # @param filename [String] Overrides the filename of the uploaded file
497
+ # @param spoiler [true, false] Whether or not this file should appear as a spoiler.
498
+ # @example Send a file from disk
499
+ # bot.send_file(83281822225530880, File.open('rubytaco.png', 'r'))
500
+ def send_file(channel, file, caption: nil, tts: false, filename: nil, spoiler: nil)
501
+ if file.respond_to?(:read)
502
+ if spoiler
503
+ filename ||= File.basename(file.path)
504
+ filename = "SPOILER_#{filename}" unless filename.start_with? 'SPOILER_'
505
+ end
506
+ # https://github.com/rest-client/rest-client/blob/v2.0.2/lib/restclient/payload.rb#L160
507
+ file.define_singleton_method(:original_filename) { filename } if filename
508
+ file.define_singleton_method(:path) { filename } if filename
509
+ end
510
+
511
+ channel = channel.resolve_id
512
+ response = API::Channel.upload_file(token, channel, file, caption: caption, tts: tts)
513
+ Message.new(JSON.parse(response), self)
514
+ end
515
+
516
+ # Creates a new application to do OAuth authorization with. This allows you to use OAuth to authorize users using
517
+ # Discord. For information how to use this, see the docs: https://discord.com/developers/docs/topics/oauth2
518
+ # @param name [String] What your application should be called.
519
+ # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
520
+ # @return [Array(String, String)] your applications' client ID and client secret to be used in OAuth authorization.
521
+ def create_oauth_application(name, redirect_uris)
522
+ response = JSON.parse(API.create_oauth_application(@token, name, redirect_uris))
523
+ [response['id'], response['secret']]
524
+ end
525
+
526
+ # Changes information about your OAuth application
527
+ # @param name [String] What your application should be called.
528
+ # @param redirect_uris [Array<String>] URIs that Discord should redirect your users to after authorizing.
529
+ # @param description [String] A string that describes what your application does.
530
+ # @param icon [String, nil] A data URI for your icon image (for example a base 64 encoded image), or nil if no icon
531
+ # should be set or changed.
532
+ def update_oauth_application(name, redirect_uris, description = '', icon = nil)
533
+ API.update_oauth_application(@token, name, redirect_uris, description, icon)
534
+ end
535
+
536
+ # Gets the users, channels, roles and emoji from a string.
537
+ # @param mentions [String] The mentions, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
538
+ # @param server [Server, nil] The server of the associated mentions. (recommended for role parsing, to speed things up)
539
+ # @return [Array<User, Channel, Role, Emoji>] The array of users, channels, roles and emoji identified by the mentions, or `nil` if none exists.
540
+ def parse_mentions(mentions, server = nil)
541
+ array_to_return = []
542
+ # While possible mentions may be in message
543
+ while mentions.include?('<') && mentions.include?('>')
544
+ # Removing all content before the next possible mention
545
+ mentions = mentions.split('<', 2)[1]
546
+ # Locate the first valid mention enclosed in `<...>`, otherwise advance to the next open `<`
547
+ next unless mentions.split('>', 2).first.length < mentions.split('<', 2).first.length
548
+
549
+ # Store the possible mention value to be validated with RegEx
550
+ mention = mentions.split('>', 2).first
551
+ if /@!?(?<id>\d+)/ =~ mention
552
+ array_to_return << user(id) unless user(id).nil?
553
+ elsif /#(?<id>\d+)/ =~ mention
554
+ array_to_return << channel(id, server) unless channel(id, server).nil?
555
+ elsif /@&(?<id>\d+)/ =~ mention
556
+ if server
557
+ array_to_return << server.role(id) unless server.role(id).nil?
558
+ else
559
+ @servers.each_value do |element|
560
+ array_to_return << element.role(id) unless element.role(id).nil?
561
+ end
562
+ end
563
+ elsif /(?<animated>^a|^${0}):(?<name>\w+):(?<id>\d+)/ =~ mention
564
+ array_to_return << (emoji(id) || Emoji.new({ 'animated' => animated != '', 'name' => name, 'id' => id }, self, nil))
565
+ end
566
+ end
567
+ array_to_return
568
+ end
569
+
570
+ # Gets the user, channel, role or emoji from a string.
571
+ # @param mention [String] The mention, which should look like `<@12314873129>`, `<#123456789>`, `<@&123456789>` or `<:name:126328:>`.
572
+ # @param server [Server, nil] The server of the associated mention. (recommended for role parsing, to speed things up)
573
+ # @return [User, Channel, Role, Emoji] The user, channel, role or emoji identified by the mention, or `nil` if none exists.
574
+ def parse_mention(mention, server = nil)
575
+ parse_mentions(mention, server).first
576
+ end
577
+
578
+ # Updates presence status.
579
+ # @param status [String] The status the bot should show up as. Can be `online`, `dnd`, `idle`, or `invisible`
580
+ # @param activity [String, nil] The name of the activity to be played/watched/listened to/stream name on the stream.
581
+ # @param url [String, nil] The Twitch URL to display as a stream. nil for no stream.
582
+ # @param since [Integer] When this status was set.
583
+ # @param afk [true, false] Whether the bot is AFK.
584
+ # @param activity_type [Integer] The type of activity status to display.
585
+ # Can be 0 (Playing), 1 (Streaming), 2 (Listening), 3 (Watching), or 5 (Competing).
586
+ # @see Gateway#send_status_update
587
+ def update_status(status, activity, url, since = 0, afk = false, activity_type = 0)
588
+ gateway_check
589
+
590
+ @activity = activity
591
+ @status = status
592
+ @streamurl = url
593
+ type = url ? 1 : activity_type
594
+
595
+ activity_obj = activity || url ? { 'name' => activity, 'url' => url, 'type' => type } : nil
596
+ @gateway.send_status_update(status, since, activity_obj, afk)
597
+
598
+ # Update the status in the cache
599
+ profile.update_presence('status' => status.to_s, 'activities' => [activity_obj].compact)
600
+ end
601
+
602
+ # Sets the currently playing game to the specified game.
603
+ # @param name [String] The name of the game to be played.
604
+ # @return [String] The game that is being played now.
605
+ def game=(name)
606
+ gateway_check
607
+ update_status(@status, name, nil)
608
+ end
609
+
610
+ alias_method :playing=, :game=
611
+
612
+ # Sets the current listening status to the specified name.
613
+ # @param name [String] The thing to be listened to.
614
+ # @return [String] The thing that is now being listened to.
615
+ def listening=(name)
616
+ gateway_check
617
+ update_status(@status, name, nil, nil, nil, 2)
618
+ end
619
+
620
+ # Sets the current watching status to the specified name.
621
+ # @param name [String] The thing to be watched.
622
+ # @return [String] The thing that is now being watched.
623
+ def watching=(name)
624
+ gateway_check
625
+ update_status(@status, name, nil, nil, nil, 3)
626
+ end
627
+
628
+ # Sets the currently online stream to the specified name and Twitch URL.
629
+ # @param name [String] The name of the stream to display.
630
+ # @param url [String] The url of the current Twitch stream.
631
+ # @return [String] The stream name that is being displayed now.
632
+ def stream(name, url)
633
+ gateway_check
634
+ update_status(@status, name, url)
635
+ name
636
+ end
637
+
638
+ # Sets the currently competing status to the specified name.
639
+ # @param name [String] The name of the game to be competing in.
640
+ # @return [String] The game that is being competed in now.
641
+ def competing=(name)
642
+ gateway_check
643
+ update_status(@status, name, nil, nil, nil, 5)
644
+ end
645
+
646
+ # Sets status to online.
647
+ def online
648
+ gateway_check
649
+ update_status(:online, @activity, @streamurl)
650
+ end
651
+
652
+ alias_method :on, :online
653
+
654
+ # Sets status to idle.
655
+ def idle
656
+ gateway_check
657
+ update_status(:idle, @activity, nil)
658
+ end
659
+
660
+ alias_method :away, :idle
661
+
662
+ # Sets the bot's status to DnD (red icon).
663
+ def dnd
664
+ gateway_check
665
+ update_status(:dnd, @activity, nil)
666
+ end
667
+
668
+ # Sets the bot's status to invisible (appears offline).
669
+ def invisible
670
+ gateway_check
671
+ update_status(:invisible, @activity, nil)
672
+ end
673
+
674
+ # Join a thread
675
+ # @param channel [Channel, Integer, String]
676
+ def join_thread(channel)
677
+ API::Channel.join_thread(@token, channel.resolve_id)
678
+ nil
679
+ end
680
+
681
+ # Leave a thread
682
+ # @param channel [Channel, Integer, String]
683
+ def leave_thread(channel)
684
+ API::Channel.leave_thread(@token, channel.resolve_id)
685
+ nil
686
+ end
687
+
688
+ # Add a member to a thread
689
+ # @param channel [Channel, Integer, String]
690
+ # @param member [Member, Integer, String]
691
+ def add_thread_member(channel, member)
692
+ API::Channel.add_thread_member(@token, channel.resolve_id, member.resolve_id)
693
+ nil
694
+ end
695
+
696
+ # Remove a member from a thread
697
+ # @param channel [Channel, Integer, String]
698
+ # @param member [Member, Integer, String]
699
+ def remove_thread_member(channel, member)
700
+ API::Channel.remove_thread_member(@token, channel.resolve_id, member.resolve_id)
701
+ nil
702
+ end
703
+
704
+ # Sets debug mode. If debug mode is on, many things will be outputted to STDOUT.
705
+ def debug=(new_debug)
706
+ LOGGER.debug = new_debug
707
+ end
708
+
709
+ # Sets the logging mode
710
+ # @see Logger#mode=
711
+ def mode=(new_mode)
712
+ LOGGER.mode = new_mode
713
+ end
714
+
715
+ # Prevents the READY packet from being printed regardless of debug mode.
716
+ def suppress_ready_debug
717
+ @prevent_ready = true
718
+ end
719
+
720
+ # Add an await the bot should listen to. For information on awaits, see {Await}.
721
+ # @param key [Symbol] The key that uniquely identifies the await for {AwaitEvent}s to listen to (see {#await}).
722
+ # @param type [Class] The event class that should be listened for.
723
+ # @param attributes [Hash] The attributes the event should check for. The block will only be executed if all attributes match.
724
+ # @yield Is executed when the await is triggered.
725
+ # @yieldparam event [Event] The event object that was triggered.
726
+ # @return [Await] The await that was created.
727
+ # @deprecated Will be changed to blocking behavior in v4.0. Use {#add_await!} instead.
728
+ def add_await(key, type, attributes = {}, &block)
729
+ raise "You can't await an AwaitEvent!" if type == OnyxCord::Events::AwaitEvent
730
+
731
+ await = Await.new(self, key, type, attributes, block)
732
+ @awaits ||= {}
733
+ @awaits[key] = await
734
+ end
735
+
736
+ # Awaits an event, blocking the current thread until a response is received.
737
+ # @param type [Class] The event class that should be listened for.
738
+ # @option attributes [Numeric] :timeout the amount of time (in seconds) to wait for a response before returning `nil`. Waits forever if omitted.
739
+ # @yield Executed when a matching event is received.
740
+ # @yieldparam event [Event] The event object that was triggered.
741
+ # @yieldreturn [true, false] Whether the event matches extra await criteria described by the block
742
+ # @return [Event, nil] The event object that was triggered, or `nil` if a `timeout` was set and no event was raised in time.
743
+ # @raise [ArgumentError] if `timeout` is given and is not a positive numeric value
744
+ def add_await!(type, attributes = {})
745
+ raise "You can't await an AwaitEvent!" if type == OnyxCord::Events::AwaitEvent
746
+
747
+ timeout = attributes[:timeout]
748
+ raise ArgumentError, 'Timeout must be a number > 0' if timeout.is_a?(Numeric) && !timeout.positive?
749
+
750
+ mutex = Mutex.new
751
+ cv = ConditionVariable.new
752
+ response = nil
753
+ block = lambda do |event|
754
+ mutex.synchronize do
755
+ response = event
756
+ if block_given?
757
+ result = yield(event)
758
+ cv.signal if result.is_a?(TrueClass)
759
+ else
760
+ cv.signal
761
+ end
762
+ end
763
+ end
764
+
765
+ handler = register_event(type, attributes, block)
766
+
767
+ if timeout
768
+ Thread.new do
769
+ sleep timeout
770
+ mutex.synchronize { cv.signal }
771
+ end
772
+ end
773
+
774
+ mutex.synchronize { cv.wait(mutex) }
775
+
776
+ remove_handler(handler)
777
+ raise 'ConditionVariable was signaled without returning an event!' if response.nil? && timeout.nil?
778
+
779
+ response
780
+ end
781
+
782
+ # Add a user to the list of ignored users. Those users will be ignored in message events at event processing level.
783
+ # @note Ignoring a user only prevents any message events (including mentions, commands etc.) from them! Typing and
784
+ # presence and any other events will still be received.
785
+ # @param user [User, String, Integer] The user, or its ID, to be ignored.
786
+ def ignore_user(user)
787
+ @ignored_ids << user.resolve_id
788
+ end
789
+
790
+ # Remove a user from the ignore list.
791
+ # @param user [User, String, Integer] The user, or its ID, to be unignored.
792
+ def unignore_user(user)
793
+ @ignored_ids.delete(user.resolve_id)
794
+ end
795
+
796
+ # Checks whether a user is being ignored.
797
+ # @param user [User, String, Integer] The user, or its ID, to check.
798
+ # @return [true, false] whether or not the user is ignored.
799
+ def ignored?(user)
800
+ @ignored_ids.include?(user.resolve_id)
801
+ end
802
+
803
+ # @see Logger#debug
804
+ def debug(message)
805
+ LOGGER.debug(message)
806
+ end
807
+
808
+ # @see Logger#log_exception
809
+ def log_exception(e)
810
+ LOGGER.log_exception(e)
811
+ end
812
+
813
+ # Dispatches an event to this bot. Called by the gateway connection handler used internally.
814
+ def dispatch(type, data = nil)
815
+ return dispatch_packet(type) if data.nil? && type.is_a?(Hash)
816
+
817
+ handle_dispatch(type, data)
818
+ end
819
+
820
+ # Raises a heartbeat event. Called by the gateway connection handler used internally.
821
+ def raise_heartbeat_event
822
+ raise_event(HeartbeatEvent.new(self))
823
+ end
824
+
825
+ # Makes the bot leave any groups with no recipients remaining
826
+ def prune_empty_groups
827
+ @channels.each_value do |channel|
828
+ channel.leave_group if channel.group? && channel.recipients.empty?
829
+ end
830
+ end
831
+
832
+ # Get all application commands.
833
+ # @param server_id [String, Integer, nil] The ID of the server to get the commands from. Global if `nil`.
834
+ # @return [Array<ApplicationCommand>]
835
+ def get_application_commands(server_id: nil)
836
+ resp = if server_id
837
+ API::Application.get_guild_commands(@token, profile.id, server_id)
838
+ else
839
+ API::Application.get_global_commands(@token, profile.id)
840
+ end
841
+
842
+ JSON.parse(resp).map do |command_data|
843
+ ApplicationCommand.new(command_data, self, server_id)
844
+ end
845
+ end
846
+
847
+ # Get an application command by ID.
848
+ # @param command_id [String, Integer]
849
+ # @param server_id [String, Integer, nil] The ID of the server to get the command from. Global if `nil`.
850
+ def get_application_command(command_id, server_id: nil)
851
+ resp = if server_id
852
+ API::Application.get_guild_command(@token, profile.id, server_id, command_id)
853
+ else
854
+ API::Application.get_global_command(@token, profile.id, command_id)
855
+ end
856
+ ApplicationCommand.new(JSON.parse(resp), self, server_id)
857
+ end
858
+
859
+ # @yieldparam [OptionBuilder]
860
+ # @yieldparam [PermissionBuilder]
861
+ # @example
862
+ # bot.register_application_command(:reddit, 'Reddit Commands') do |cmd|
863
+ # cmd.subcommand_group(:subreddit, 'Subreddit Commands') do |group|
864
+ # group.subcommand(:hot, "What's trending") do |sub|
865
+ # sub.string(:subreddit, 'Subreddit to search')
866
+ # end
867
+ # group.subcommand(:new, "What's new") do |sub|
868
+ # sub.string(:since, 'How long ago', choices: ['this hour', 'today', 'this week', 'this month', 'this year', 'all time'])
869
+ # sub.string(:subreddit, 'Subreddit to search')
870
+ # end
871
+ # end
872
+ # end
873
+ def register_application_command(name, description, server_id: nil, default_permission: nil, type: :chat_input, default_member_permissions: nil, contexts: nil, nsfw: false, integration_types: nil)
874
+ type = ApplicationCommand::TYPES[type] || type
875
+
876
+ contexts = contexts&.map { |context| Interaction::CONTEXTS[context] || context }
877
+ integration_types = integration_types&.map { |type| Interaction::INTEGRATION_TYPES[type] || type }
878
+ default_member_permissions = Permissions.bits(default_member_permissions) if default_member_permissions.is_a?(Array)
879
+
880
+ builder = Interactions::OptionBuilder.new
881
+ permission_builder = Interactions::PermissionBuilder.new
882
+ yield(builder, permission_builder) if block_given?
883
+
884
+ resp = if server_id
885
+ API::Application.create_guild_command(@token, profile.id, server_id, name, description, builder.to_a, default_permission, type, default_member_permissions&.to_s, contexts, nsfw)
886
+ else
887
+ API::Application.create_global_command(@token, profile.id, name, description, builder.to_a, default_permission, type, default_member_permissions&.to_s, contexts, nsfw, integration_types)
888
+ end
889
+ cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
890
+
891
+ if permission_builder.to_a.any?
892
+ raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
893
+
894
+ edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
895
+ end
896
+
897
+ cmd
898
+ end
899
+
900
+ # @yieldparam [OptionBuilder]
901
+ # @yieldparam [PermissionBuilder]
902
+ def edit_application_command(command_id, server_id: nil, name: nil, description: nil, default_permission: nil, type: :chat_input, default_member_permissions: nil, contexts: nil, nsfw: nil, integration_types: nil)
903
+ type = ApplicationCommand::TYPES[type] || type
904
+
905
+ contexts = contexts&.map { |context| Interaction::CONTEXTS[context] || context }
906
+ integration_types = integration_types&.map { |type| Interaction::INTEGRATION_TYPES[type] || type }
907
+ default_member_permissions = Permissions.bits(default_member_permissions) if default_member_permissions.is_a?(Array)
908
+
909
+ builder = Interactions::OptionBuilder.new
910
+ permission_builder = Interactions::PermissionBuilder.new
911
+
912
+ yield(builder, permission_builder) if block_given?
913
+
914
+ resp = if server_id
915
+ API::Application.edit_guild_command(@token, profile.id, server_id, command_id, name, description, builder.to_a, default_permission, type, default_member_permissions&.to_s, contexts, nsfw)
916
+ else
917
+ API::Application.edit_global_command(@token, profile.id, command_id, name, description, builder.to_a, default_permission, type, default_member_permissions&.to_s, contexts, nsfw, integration_types)
918
+ end
919
+ cmd = ApplicationCommand.new(JSON.parse(resp), self, server_id)
920
+
921
+ if permission_builder.to_a.any?
922
+ raise ArgumentError, 'Permissions can only be set for guild commands' unless server_id
923
+
924
+ edit_application_command_permissions(cmd.id, server_id, permission_builder.to_a)
925
+ end
926
+
927
+ cmd
928
+ end
929
+
930
+ # Remove an application command from the commands registered with discord.
931
+ # @param command_id [String, Integer] The ID of the command to remove.
932
+ # @param server_id [String, Integer] The ID of the server to delete this command from, global if `nil`.
933
+ def delete_application_command(command_id, server_id: nil)
934
+ if server_id
935
+ API::Application.delete_guild_command(@token, profile.id, server_id, command_id)
936
+ else
937
+ API::Application.delete_global_command(@token, profile.id, command_id)
938
+ end
939
+ end
940
+
941
+ # @param command_id [Integer, String]
942
+ # @param server_id [Integer, String]
943
+ # @param permissions [Array<Hash>] An array of objects formatted as `{ id: ENTITY_ID, type: 1 or 2, permission: true or false }`
944
+ # @param bearer_token [String] A valid bearer token that has permission to manage the server and its roles.
945
+ def edit_application_command_permissions(command_id, server_id, permissions = [], bearer_token = nil)
946
+ builder = Interactions::PermissionBuilder.new
947
+ yield builder if block_given?
948
+
949
+ raise ArgumentError, 'This method requires a valid bearer token to be provided' unless bearer_token
950
+
951
+ permissions += builder.to_a
952
+ bearer_token = "Bearer #{bearer_token.delete_prefix('Bearer ')}"
953
+ API::Application.edit_guild_command_permissions(bearer_token, profile.id, server_id, command_id, permissions)
954
+ end
955
+
956
+ # Get the permissions for all of the application commands in a specific server.
957
+ # @param server_id [Integer, String, nil] The ID of the server to fetch application command permissions for.
958
+ # @return [Array<ApplicationCommand::Permission>] The permissions for all of the application commands in the given server.
959
+ def application_command_permissions(server_id:)
960
+ response = API::Application.get_guild_application_command_permissions(@token, profile.id, server_id.resolve_id)
961
+ JSON.parse(response).flat_map { |data| data['permissions'].map { |inner| ApplicationCommand::Permission.new(inner, data, self) } }
962
+ end
963
+
964
+ # Fetches all the application emojis that the bot can use.
965
+ # @return [Array<Emoji>] Returns an array of emoji objects.
966
+ def application_emojis
967
+ response = API::Application.list_application_emojis(@token, profile.id)
968
+ JSON.parse(response)['items'].map { |emoji| Emoji.new(emoji, self) }
969
+ end
970
+
971
+ # Fetches a single application emoji from its ID.
972
+ # @param emoji_id [Integer, String] ID of the application emoji.
973
+ # @return [Emoji] The application emoji.
974
+ def application_emoji(emoji_id)
975
+ response = API::Application.get_application_emoji(@token, profile.id, emoji_id.resolve_id)
976
+ Emoji.new(JSON.parse(response), self)
977
+ end
978
+
979
+ # Creates a new custom emoji that can be used by this application.
980
+ # @param name [String] The name of emoji to create.
981
+ # @param image [String, #read] Base64 string with the image data, or an object that responds to #read.
982
+ # @return [Emoji] The emoji that has been created.
983
+ def create_application_emoji(name:, image:)
984
+ image = image.respond_to?(:read) ? OnyxCord.encode64(image) : image
985
+ response = API::Application.create_application_emoji(@token, profile.id, name, image)
986
+ Emoji.new(JSON.parse(response), self)
987
+ end
988
+
989
+ # Edits an existing application emoji.
990
+ # @param emoji_id [Integer, String, Emoji] ID of the application emoji to edit.
991
+ # @param name [String] The new name of the emoji.
992
+ # @return [Emoji] Returns the updated emoji object on success.
993
+ def edit_application_emoji(emoji_id, name:)
994
+ response = API::Application.edit_application_emoji(@token, profile.id, emoji_id.resolve_id, name)
995
+ Emoji.new(JSON.parse(response), self)
996
+ end
997
+
998
+ # Deletes an existing application emoji.
999
+ # @param emoji_id [Integer, String, Emoji] ID of the application emoji to delete.
1000
+ def delete_application_emoji(emoji_id)
1001
+ API::Application.delete_application_emoji(@token, profile.id, emoji_id.resolve_id)
1002
+ end
1003
+
1004
+ # @!visibility private
1005
+ def inspect
1006
+ "<Bot client_id=#{@client_id.inspect} redact_token=#{@redact_token.inspect}>"
1007
+ end
1008
+
1009
+ private
1010
+
1011
+ # Throws a useful exception if there's currently no gateway connection.
1012
+ def gateway_check
1013
+ raise "A gateway connection is necessary to call this method! You'll have to do it inside any event (e.g. `ready`) or after `bot.run :async`." unless connected?
1014
+ end
1015
+
1016
+ # Logs a warning if there are servers which are still unavailable.
1017
+ # e.g. due to a Discord outage or because the servers are large and taking a while to load.
1018
+ def unavailable_servers_check
1019
+ # Return unless there are servers that are unavailable.
1020
+ return unless @unavailable_servers&.positive?
1021
+
1022
+ LOGGER.warn("#{@unavailable_servers} servers haven't been cached yet.")
1023
+ LOGGER.warn('Servers may be unavailable due to an outage, or your bot is on very large servers that are taking a while to load.')
1024
+ end
1025
+
1026
+ ### ## ## ######## ######## ######## ## ## ### ## ######
1027
+ ## ### ## ## ## ## ## ### ## ## ## ## ## ##
1028
+ ## #### ## ## ## ## ## #### ## ## ## ## ##
1029
+ ## ## ## ## ## ###### ######## ## ## ## ## ## ## ######
1030
+ ## ## #### ## ## ## ## ## #### ######### ## ##
1031
+ ## ## ### ## ## ## ## ## ### ## ## ## ## ##
1032
+ ### ## ## ## ######## ## ## ## ## ## ## ######## ######
1033
+
1034
+ # Internal handler for PRESENCE_UPDATE
1035
+ def update_presence(data)
1036
+ # Friends list presences have no server ID so ignore these to not cause an error
1037
+ return unless data['guild_id']
1038
+
1039
+ user_id = data['user']['id'].to_i
1040
+ server_id = data['guild_id'].to_i
1041
+ server = server(server_id)
1042
+ return unless server
1043
+
1044
+ member_is_new = false
1045
+
1046
+ if server.member_cached?(user_id)
1047
+ member = server.member(user_id)
1048
+ else
1049
+ # If the member is not cached yet, it means that it just came online from not being cached at all
1050
+ # due to large_threshold. Fortunately, Discord sends the entire member object in this case, and
1051
+ # not just a part of it - we can just cache this member directly
1052
+ member = Member.new(data, server, self)
1053
+ debug("Implicitly adding presence-obtained member #{user_id} to #{server_id} cache")
1054
+
1055
+ member_is_new = true
1056
+ end
1057
+
1058
+ username = data['user']['username']
1059
+ if username && !member_is_new # Don't set the username for newly-cached members
1060
+ debug "Implicitly updating presence-obtained information username for member #{user_id}"
1061
+ member.update_username(username)
1062
+ end
1063
+
1064
+ global_name = data['user']['global_name']
1065
+ if global_name && !member_is_new # Don't set the global_name for newly-cached members
1066
+ debug "Implicitly updating presence-obtained information global_name for member #{user_id}"
1067
+ member.update_global_name(global_name)
1068
+ end
1069
+
1070
+ member.update_presence(data)
1071
+
1072
+ member.avatar_id = data['user']['avatar'] if data['user']['avatar']
1073
+
1074
+ server.cache_member(member)
1075
+ end
1076
+
1077
+ # Internal handler for VOICE_STATE_UPDATE
1078
+ def update_voice_state(data)
1079
+ @session_id = data['session_id']
1080
+
1081
+ server_id = data['guild_id'].to_i
1082
+ server = server(server_id)
1083
+ return unless server
1084
+
1085
+ user_id = data['user_id'].to_i
1086
+ old_voice_state = server.voice_states[user_id]
1087
+ old_channel_id = old_voice_state.voice_channel&.id if old_voice_state
1088
+
1089
+ server.update_voice_state(data)
1090
+
1091
+ existing_voice = @voices[server_id]
1092
+ if user_id == @profile.id && existing_voice
1093
+ new_channel_id = data['channel_id']
1094
+ if new_channel_id
1095
+ new_channel = channel(new_channel_id)
1096
+ existing_voice.channel = new_channel
1097
+ else
1098
+ voice_destroy(server_id)
1099
+ end
1100
+ end
1101
+
1102
+ old_channel_id
1103
+ end
1104
+
1105
+ # Internal handler for VOICE_SERVER_UPDATE
1106
+ def update_voice_server(data)
1107
+ server_id = data['guild_id'].to_i
1108
+ channel = @should_connect_to_voice[server_id]
1109
+
1110
+ debug("Voice server update received! chan: #{channel.inspect}")
1111
+ return unless channel
1112
+
1113
+ @should_connect_to_voice.delete(server_id)
1114
+ debug('Updating voice server!')
1115
+
1116
+ token = data['token']
1117
+ endpoint = data['endpoint']
1118
+
1119
+ unless endpoint
1120
+ debug('VOICE_SERVER_UPDATE sent with nil endpoint! Ignoring')
1121
+ return
1122
+ end
1123
+
1124
+ debug('Got data, now creating the bot.')
1125
+ @voices[server_id] = OnyxCord::Voice::VoiceBot.new(channel, self, token, @session_id, endpoint)
1126
+ end
1127
+
1128
+ # Internal handler for CHANNEL_CREATE
1129
+ def create_channel(data)
1130
+ channel = data.is_a?(OnyxCord::Channel) ? data : Channel.new(data, self)
1131
+ server = channel.server
1132
+
1133
+ # The last message ID of a forum channel is the most recent post
1134
+ channel.parent.process_last_message_id(channel.id) if channel.parent&.forum? || channel.parent&.media?
1135
+
1136
+ # Handle normal and private channels separately
1137
+ if server
1138
+ server.add_channel(channel)
1139
+ @channels[channel.id] = channel if @channels
1140
+ elsif channel.private?
1141
+ @pm_channels[channel.recipient.id] = channel if @pm_channels
1142
+ elsif channel.group?
1143
+ @channels[channel.id] = channel if @channels
1144
+ end
1145
+ end
1146
+
1147
+ # Internal handler for CHANNEL_UPDATE
1148
+ def update_channel(data)
1149
+ @channels&.[](data['id'].to_i)&.update_data(data)
1150
+ end
1151
+
1152
+ # Internal handler for CHANNEL_DELETE
1153
+ def delete_channel(data)
1154
+ channel = Channel.new(data, self)
1155
+ server = channel.server
1156
+
1157
+ # Handle normal and private channels separately
1158
+ if server
1159
+ @channels&.delete(channel.id)
1160
+ server.delete_channel(channel.id)
1161
+ elsif channel.pm?
1162
+ @pm_channels&.delete(channel.recipient.id)
1163
+ elsif channel.group?
1164
+ @channels&.delete(channel.id)
1165
+ end
1166
+
1167
+ @thread_members&.delete(channel.id) if channel.thread?
1168
+ end
1169
+
1170
+ # Internal handler for GUILD_MEMBER_ADD
1171
+ def add_guild_member(data)
1172
+ server_id = data['guild_id'].to_i
1173
+ server = self.server(server_id)
1174
+
1175
+ member = Member.new(data, server, self)
1176
+ server.add_member(member)
1177
+ end
1178
+
1179
+ # Internal handler for GUILD_MEMBER_UPDATE
1180
+ def update_guild_member(data)
1181
+ server_id = data['guild_id'].to_i
1182
+ server = self.server(server_id)
1183
+
1184
+ # Only attempt to update members that're already cached
1185
+ if (member = server.member(data['user']['id'].to_i, false))
1186
+ member.update_data(data)
1187
+ else
1188
+ ensure_user(data['user'])
1189
+ end
1190
+ end
1191
+
1192
+ # Internal handler for GUILD_MEMBER_DELETE
1193
+ def delete_guild_member(data)
1194
+ server_id = data['guild_id'].to_i
1195
+ server = self.server(server_id)
1196
+ return unless server
1197
+
1198
+ user_id = data['user']['id'].to_i
1199
+ server.delete_member(user_id)
1200
+ rescue OnyxCord::Errors::NoPermission
1201
+ OnyxCord::LOGGER.warn("delete_guild_member attempted to access a server for which the bot doesn't have permission! Not sure what happened here, ignoring")
1202
+ end
1203
+
1204
+ # Internal handler for GUILD_CREATE
1205
+ def create_guild(data)
1206
+ ensure_server(data, true)
1207
+ end
1208
+
1209
+ # Internal handler for GUILD_UPDATE
1210
+ def update_guild(data)
1211
+ @servers[data['id'].to_i].update_data(data)
1212
+ end
1213
+
1214
+ # Internal handler for GUILD_DELETE
1215
+ def delete_guild(data)
1216
+ id = data['id'].to_i
1217
+ @servers.delete(id)
1218
+ end
1219
+
1220
+ # Internal handler for GUILD_ROLE_CREATE and GUILD_ROLE_UPDATE
1221
+ def update_guild_role(data)
1222
+ server = @servers[data['guild_id'].to_i]
1223
+
1224
+ if (role = server&.role(data['role']['id'].to_i))
1225
+ role.update_data(data['role'])
1226
+ else
1227
+ server&.add_role(Role.new(data['role'], self, server))
1228
+ end
1229
+ end
1230
+
1231
+ # Internal handler for GUILD_ROLE_DELETE
1232
+ def delete_guild_role(data)
1233
+ role_id = data['role_id'].to_i
1234
+ server_id = data['guild_id'].to_i
1235
+ server = @servers[server_id]
1236
+ server&.delete_role(role_id)
1237
+ end
1238
+
1239
+ # Internal handler for GUILD_EMOJIS_UPDATE
1240
+ def update_guild_emoji(data)
1241
+ server_id = data['guild_id'].to_i
1242
+ server = @servers[server_id]
1243
+ server&.update_emoji_data(data)
1244
+ end
1245
+
1246
+ # Internal handler for GUILD_SCHEDULED_EVENT_CREATE and GUILD_SCHEDULED_EVENT_UPDATE
1247
+ def update_guild_scheduled_event(data)
1248
+ server = @servers[data['guild_id'].to_i]
1249
+
1250
+ if (event = server&.scheduled_event(data['id'].to_i, request: false))
1251
+ event&.update_data(data)
1252
+ else
1253
+ server&.cache_scheduled_event(ScheduledEvent.new(data, server, self))
1254
+ end
1255
+ end
1256
+
1257
+ # Internal handler for MESSAGE_CREATE
1258
+ def create_message(data); end
1259
+
1260
+ # Internal handler for TYPING_START
1261
+ def start_typing(data); end
1262
+
1263
+ # Internal handler for MESSAGE_UPDATE
1264
+ def update_message(data); end
1265
+
1266
+ # Internal handler for MESSAGE_DELETE
1267
+ def delete_message(data); end
1268
+
1269
+ # Internal handler for MESSAGE_REACTION_ADD
1270
+ def add_message_reaction(data); end
1271
+
1272
+ # Internal handler for MESSAGE_REACTION_REMOVE
1273
+ def remove_message_reaction(data); end
1274
+
1275
+ # Internal handler for MESSAGE_REACTION_REMOVE_ALL
1276
+ def remove_all_message_reactions(data); end
1277
+
1278
+ # Internal handler for GUILD_BAN_ADD
1279
+ def add_user_ban(data); end
1280
+
1281
+ # Internal handler for GUILD_BAN_REMOVE
1282
+ def remove_user_ban(data); end
1283
+
1284
+ ## ####### ###### #### ## ##
1285
+ ## ## ## ## ## ## ### ##
1286
+ ## ## ## ## ## #### ##
1287
+ ## ## ## ## #### ## ## ## ##
1288
+ ## ## ## ## ## ## ## ####
1289
+ ## ## ## ## ## ## ## ###
1290
+ ######## ####### ###### #### ## ##
1291
+
1292
+ def process_token(type, token)
1293
+ # Remove the "Bot " prefix if it exists
1294
+ token = token[4..] if token.start_with? 'Bot '
1295
+
1296
+ token = "Bot #{token}" unless type == :user
1297
+ token
1298
+ end
1299
+
1300
+ def handle_dispatch(type, data)
1301
+ # Check whether there are still unavailable servers and there have been more than 10 seconds since READY
1302
+ if @unavailable_servers&.positive? && (Time.now - @unavailable_timeout_time) > 10 && !(@intents || 0).nobits?(INTENTS[:servers])
1303
+ # The server streaming timed out!
1304
+ LOGGER.debug("Server streaming timed out with #{@unavailable_servers} servers remaining")
1305
+ LOGGER.debug('Calling ready now because server loading is taking a long time. Servers may be unavailable due to an outage, or your bot is on very large servers.')
1306
+
1307
+ # Unset the unavailable server count so this doesn't get triggered again
1308
+ @unavailable_servers = 0
1309
+
1310
+ notify_ready
1311
+ end
1312
+
1313
+ case type
1314
+ when :READY
1315
+ # As READY may be called multiple times over a single process lifetime, we here need to reset the cache entirely
1316
+ # to prevent possible inconsistencies, like objects referencing old versions of other objects which have been
1317
+ # replaced.
1318
+ init_cache
1319
+
1320
+ @profile = Profile.new(data['user'], self)
1321
+
1322
+ @client_id ||= data['application']['id']&.to_i
1323
+
1324
+ # Initialize servers
1325
+ @servers = {}
1326
+
1327
+ # Count unavailable servers
1328
+ @unavailable_servers = 0
1329
+
1330
+ data['guilds'].each do |element|
1331
+ # Check for true specifically because unavailable=false indicates that a previously unavailable server has
1332
+ # come online
1333
+ if element['unavailable']
1334
+ @unavailable_servers += 1
1335
+
1336
+ # Ignore any unavailable servers
1337
+ next
1338
+ end
1339
+
1340
+ ensure_server(element, true)
1341
+ end
1342
+
1343
+ # Don't notify yet if there are unavailable servers because they need to get available before the bot truly has
1344
+ # all the data
1345
+ if @unavailable_servers.zero?
1346
+ # No unavailable servers - we're ready!
1347
+ notify_ready
1348
+ end
1349
+
1350
+ @ready_time = Time.now
1351
+ @unavailable_timeout_time = Time.now
1352
+ when :GUILD_MEMBERS_CHUNK
1353
+ id = data['guild_id'].to_i
1354
+ server = server(id)
1355
+ server.process_chunk(data['members'], data['chunk_index'], data['chunk_count'])
1356
+ when :USER_UPDATE
1357
+ @profile = Profile.new(data, self)
1358
+ when :INVITE_CREATE
1359
+ invite = Invite.new(data, self)
1360
+ raise_event(InviteCreateEvent.new(data, invite, self))
1361
+ when :INVITE_DELETE
1362
+ raise_event(InviteDeleteEvent.new(data, self))
1363
+ when :MESSAGE_CREATE
1364
+ if ignored?(data['author']['id'])
1365
+ debug("Ignored author with ID #{data['author']['id']}")
1366
+ return
1367
+ end
1368
+
1369
+ if @ignore_bots && data['author']['bot']
1370
+ debug("Ignored Bot account with ID #{data['author']['id']}")
1371
+ return
1372
+ end
1373
+
1374
+ if !should_parse_self && profile.id == data['author']['id'].to_i
1375
+ debug('Ignored message from the current bot')
1376
+ return
1377
+ end
1378
+
1379
+ # If create_message is overwritten with a method that returns the parsed message, use that instead, so we don't
1380
+ # parse the message twice (which is just thrown away performance)
1381
+ message = create_message(data)
1382
+ message = Message.new(data, self) unless message.is_a? Message
1383
+
1384
+ # Update the existing member if it exists in the cache.
1385
+ if data['member']
1386
+ member = message.channel.server&.member(data['author']['id'].to_i, false)
1387
+ data['member']['user'] = data['author']
1388
+ member&.update_data(data['member'])
1389
+ end
1390
+
1391
+ # Dispatch a ChannelCreateEvent for channels we don't have cached
1392
+ if message.channel.private? && !@pm_channels&.key?(message.channel.recipient.id)
1393
+ create_channel(message.channel)
1394
+
1395
+ raise_event(ChannelCreateEvent.new(message.channel, self))
1396
+ end
1397
+
1398
+ message.channel.process_last_message_id(message.id)
1399
+
1400
+ event = MessageEvent.new(message, self)
1401
+ raise_event(event)
1402
+
1403
+ # Raise a mention event for any direct mentions.
1404
+ if message.mentions.any? { |user| user.id == profile.id }
1405
+ event = MentionEvent.new(message, self, false)
1406
+ raise_event(event)
1407
+ end
1408
+
1409
+ # Raise a mention event for the current bot's auto-generated role.
1410
+ if message.role_mentions.any? { |role| role.tags&.bot_id == profile.id }
1411
+ event = MentionEvent.new(message, self, true)
1412
+ raise_event(event)
1413
+ end
1414
+
1415
+ if message.channel.private?
1416
+ event = PrivateMessageEvent.new(message, self)
1417
+ raise_event(event)
1418
+ end
1419
+ when :MESSAGE_UPDATE
1420
+ update_message(data)
1421
+
1422
+ if !should_parse_self && profile.id == data['author']['id'].to_i
1423
+ debug('Ignored message from the current bot')
1424
+ return
1425
+ end
1426
+
1427
+ message = Message.new(data, self)
1428
+
1429
+ event = MessageUpdateEvent.new(message, self)
1430
+ raise_event(event)
1431
+
1432
+ if data['author'].nil?
1433
+ LOGGER.debug("Edited a message with nil author! Content: #{message.content.inspect}, channel: #{message.channel.inspect}")
1434
+ return
1435
+ end
1436
+
1437
+ # Update the existing member if it exists in the cache.
1438
+ if data['member']
1439
+ member = message.channel.server&.member(data['author']['id'].to_i, false)
1440
+ data['member']['user'] = data['author']
1441
+ member&.update_data(data['member'])
1442
+ end
1443
+
1444
+ event = MessageEditEvent.new(message, self)
1445
+ raise_event(event)
1446
+ when :MESSAGE_DELETE
1447
+ delete_message(data)
1448
+
1449
+ event = MessageDeleteEvent.new(data, self)
1450
+ raise_event(event)
1451
+ when :MESSAGE_DELETE_BULK
1452
+ debug("MESSAGE_DELETE_BULK will raise #{data['ids'].length} events")
1453
+
1454
+ data['ids'].each do |single_id|
1455
+ # Form a data hash for a single ID so the methods get what they want
1456
+ single_data = {
1457
+ 'id' => single_id,
1458
+ 'channel_id' => data['channel_id']
1459
+ }
1460
+
1461
+ # Raise as normal
1462
+ delete_message(single_data)
1463
+
1464
+ event = MessageDeleteEvent.new(single_data, self)
1465
+ raise_event(event)
1466
+ end
1467
+ when :TYPING_START
1468
+ start_typing(data)
1469
+
1470
+ begin
1471
+ event = TypingEvent.new(data, self)
1472
+ raise_event(event)
1473
+ rescue OnyxCord::Errors::NoPermission
1474
+ debug 'Typing started in channel the bot has no access to, ignoring'
1475
+ end
1476
+ when :MESSAGE_REACTION_ADD
1477
+ add_message_reaction(data)
1478
+
1479
+ return if profile.id == data['user_id'].to_i && !should_parse_self
1480
+
1481
+ if data['member']
1482
+ server = self.server(data['guild_id'].to_i)
1483
+
1484
+ server&.cache_member(Member.new(data['member'], server, self))
1485
+ end
1486
+
1487
+ event = ReactionAddEvent.new(data, self)
1488
+ raise_event(event)
1489
+ when :MESSAGE_REACTION_REMOVE
1490
+ remove_message_reaction(data)
1491
+
1492
+ return if profile.id == data['user_id'].to_i && !should_parse_self
1493
+
1494
+ event = ReactionRemoveEvent.new(data, self)
1495
+ raise_event(event)
1496
+ when :MESSAGE_REACTION_REMOVE_ALL
1497
+ remove_all_message_reactions(data)
1498
+
1499
+ event = ReactionRemoveAllEvent.new(data, self)
1500
+ raise_event(event)
1501
+ when :MESSAGE_REACTION_REMOVE_EMOJI
1502
+
1503
+ event = ReactionRemoveEmojiEvent.new(data, self)
1504
+ raise_event(event)
1505
+ when :PRESENCE_UPDATE
1506
+ # Ignore friends list presences
1507
+ return unless data['guild_id']
1508
+
1509
+ new_activities = (data['activities'] || []).map { |act_data| Activity.new(act_data, self) }
1510
+ presence_user = @users[data['user']['id'].to_i]
1511
+ old_activities = (presence_user&.activities || [])
1512
+ update_presence(data)
1513
+
1514
+ # Starting a new game
1515
+ playing_change = new_activities.reject do |act|
1516
+ old_activities.find { |old| old.name == act.name }
1517
+ end
1518
+
1519
+ # Exiting an existing game
1520
+ playing_change += old_activities.reject do |old|
1521
+ new_activities.find { |act| act.name == old.name }
1522
+ end
1523
+
1524
+ if playing_change.any?
1525
+ playing_change.each do |act|
1526
+ raise_event(PlayingEvent.new(data, act, self))
1527
+ end
1528
+ else
1529
+ raise_event(PresenceEvent.new(data, self))
1530
+ end
1531
+ when :VOICE_STATE_UPDATE
1532
+ old_channel_id = update_voice_state(data)
1533
+
1534
+ event = VoiceStateUpdateEvent.new(data, old_channel_id, self)
1535
+ raise_event(event)
1536
+ when :VOICE_SERVER_UPDATE
1537
+ update_voice_server(data)
1538
+
1539
+ event = VoiceServerUpdateEvent.new(data, self)
1540
+ raise_event(event)
1541
+ when :CHANNEL_CREATE
1542
+ create_channel(data)
1543
+
1544
+ event = ChannelCreateEvent.new(data, self)
1545
+ raise_event(event)
1546
+ when :CHANNEL_UPDATE
1547
+ update_channel(data)
1548
+
1549
+ event = ChannelUpdateEvent.new(data, self)
1550
+ raise_event(event)
1551
+ when :CHANNEL_DELETE
1552
+ delete_channel(data)
1553
+
1554
+ event = ChannelDeleteEvent.new(data, self)
1555
+ raise_event(event)
1556
+ when :CHANNEL_PINS_UPDATE
1557
+ event = ChannelPinsUpdateEvent.new(data, self)
1558
+
1559
+ event.channel.process_last_pin_timestamp(data['last_pin_timestamp']) if data.key?('last_pin_timestamp')
1560
+
1561
+ raise_event(event)
1562
+ when :GUILD_MEMBER_ADD
1563
+ add_guild_member(data)
1564
+
1565
+ event = ServerMemberAddEvent.new(data, self)
1566
+ raise_event(event)
1567
+ when :GUILD_MEMBER_UPDATE
1568
+ update_guild_member(data)
1569
+
1570
+ event = ServerMemberUpdateEvent.new(data, self)
1571
+ raise_event(event)
1572
+ when :GUILD_MEMBER_REMOVE
1573
+ delete_guild_member(data)
1574
+
1575
+ event = ServerMemberDeleteEvent.new(data, self)
1576
+ raise_event(event)
1577
+ when :GUILD_AUDIT_LOG_ENTRY_CREATE
1578
+ event = AuditLogEntryCreateEvent.new(data, self)
1579
+ raise_event(event)
1580
+ when :GUILD_BAN_ADD
1581
+ add_user_ban(data)
1582
+
1583
+ event = UserBanEvent.new(data, self)
1584
+ raise_event(event)
1585
+ when :GUILD_BAN_REMOVE
1586
+ remove_user_ban(data)
1587
+
1588
+ event = UserUnbanEvent.new(data, self)
1589
+ raise_event(event)
1590
+ when :GUILD_ROLE_UPDATE
1591
+ update_guild_role(data)
1592
+
1593
+ event = ServerRoleUpdateEvent.new(data, self)
1594
+ raise_event(event)
1595
+ when :GUILD_ROLE_CREATE
1596
+ update_guild_role(data)
1597
+
1598
+ event = ServerRoleCreateEvent.new(data, self)
1599
+ raise_event(event)
1600
+ when :GUILD_ROLE_DELETE
1601
+ delete_guild_role(data)
1602
+
1603
+ event = ServerRoleDeleteEvent.new(data, self)
1604
+ raise_event(event)
1605
+ when :INTEGRATION_CREATE
1606
+ event = IntegrationCreateEvent.new(data, self)
1607
+ raise_event(event)
1608
+ when :INTEGRATION_UPDATE
1609
+ event = IntegrationUpdateEvent.new(data, self)
1610
+ raise_event(event)
1611
+ when :INTEGRATION_DELETE
1612
+ event = IntegrationDeleteEvent.new(data, self)
1613
+ raise_event(event)
1614
+ when :GUILD_CREATE
1615
+ create_guild(data)
1616
+
1617
+ # Check for false specifically (no data means the server has never been unavailable)
1618
+ if data['unavailable'].is_a? FalseClass
1619
+ @unavailable_servers -= 1 if @unavailable_servers
1620
+ @unavailable_timeout_time = Time.now
1621
+
1622
+ notify_ready if @unavailable_servers.zero?
1623
+
1624
+ # Return here so the event doesn't get triggered
1625
+ return
1626
+ end
1627
+
1628
+ event = ServerCreateEvent.new(data, self)
1629
+ raise_event(event)
1630
+ when :GUILD_UPDATE
1631
+ update_guild(data)
1632
+
1633
+ event = ServerUpdateEvent.new(data, self)
1634
+ raise_event(event)
1635
+ when :GUILD_DELETE
1636
+ delete_guild(data)
1637
+
1638
+ if data['unavailable'].is_a? TrueClass
1639
+ LOGGER.warn("Server #{data['id']} is unavailable due to an outage!")
1640
+ return # Don't raise an event
1641
+ end
1642
+
1643
+ event = ServerDeleteEvent.new(data, self)
1644
+ raise_event(event)
1645
+ when :GUILD_EMOJIS_UPDATE
1646
+ server_id = data['guild_id'].to_i
1647
+ server = @servers[server_id]
1648
+ old_emoji_data = server.emoji.clone
1649
+ update_guild_emoji(data)
1650
+ new_emoji_data = server.emoji
1651
+
1652
+ created_ids = new_emoji_data.keys - old_emoji_data.keys
1653
+ deleted_ids = old_emoji_data.keys - new_emoji_data.keys
1654
+ updated_ids = old_emoji_data.select do |k, v|
1655
+ new_emoji_data[k] && (v.name != new_emoji_data[k].name || v.roles != new_emoji_data[k].roles)
1656
+ end.keys
1657
+
1658
+ event = ServerEmojiChangeEvent.new(server, data, self)
1659
+ raise_event(event)
1660
+
1661
+ created_ids.each do |e|
1662
+ event = ServerEmojiCreateEvent.new(server, new_emoji_data[e], self)
1663
+ raise_event(event)
1664
+ end
1665
+
1666
+ deleted_ids.each do |e|
1667
+ event = ServerEmojiDeleteEvent.new(server, old_emoji_data[e], self)
1668
+ raise_event(event)
1669
+ end
1670
+
1671
+ updated_ids.each do |e|
1672
+ event = ServerEmojiUpdateEvent.new(server, old_emoji_data[e], new_emoji_data[e], self)
1673
+ raise_event(event)
1674
+ end
1675
+ when :APPLICATION_COMMAND_PERMISSIONS_UPDATE
1676
+ event = ApplicationCommandPermissionsUpdateEvent.new(data, self)
1677
+
1678
+ raise_event(event)
1679
+ when :INTERACTION_CREATE
1680
+ OnyxCord::LOGGER.info(">>> INTERACTION_CREATE received inside bot.rb! type: #{data['type']}")
1681
+ event = InteractionCreateEvent.new(data, self)
1682
+ raise_event(event)
1683
+ OnyxCord::LOGGER.info(">>> raised InteractionCreateEvent successfully")
1684
+
1685
+ case data['type']
1686
+ when Interaction::TYPES[:command]
1687
+ OnyxCord::LOGGER.info(">>> creating ApplicationCommandEvent")
1688
+ event = ApplicationCommandEvent.new(data, self)
1689
+ OnyxCord::LOGGER.info(">>> spawned Thread to execute command")
1690
+
1691
+ Thread.new(event) do |evt|
1692
+ Thread.current[:onyxcord_name] = "it-#{evt.interaction.id}"
1693
+
1694
+ begin
1695
+ OnyxCord::LOGGER.info(">>> Executing application command #{evt.command_name}:#{evt.command_id}")
1696
+ handler = @application_commands[evt.command_name]
1697
+ OnyxCord::LOGGER.info(">>> Handler found? #{!handler.nil?}")
1698
+ handler&.call(evt)
1699
+ OnyxCord::LOGGER.info(">>> Handler call finished")
1700
+ rescue Exception => e
1701
+ OnyxCord::LOGGER.error(">>> FATAL EXCEPTION IN THREAD: #{e.class}: #{e.message}")
1702
+ log_exception(e)
1703
+ end
1704
+ end
1705
+ when Interaction::TYPES[:component]
1706
+ case data['data']['component_type']
1707
+ when Webhooks::View::COMPONENT_TYPES[:button]
1708
+ event = ButtonEvent.new(data, self)
1709
+
1710
+ raise_event(event)
1711
+ when Webhooks::View::COMPONENT_TYPES[:string_select]
1712
+ event = StringSelectEvent.new(data, self)
1713
+
1714
+ raise_event(event)
1715
+ when Webhooks::View::COMPONENT_TYPES[:user_select]
1716
+ event = UserSelectEvent.new(data, self)
1717
+
1718
+ raise_event(event)
1719
+ when Webhooks::View::COMPONENT_TYPES[:role_select]
1720
+ event = RoleSelectEvent.new(data, self)
1721
+
1722
+ raise_event(event)
1723
+ when Webhooks::View::COMPONENT_TYPES[:mentionable_select]
1724
+ event = MentionableSelectEvent.new(data, self)
1725
+
1726
+ raise_event(event)
1727
+ when Webhooks::View::COMPONENT_TYPES[:channel_select]
1728
+ event = ChannelSelectEvent.new(data, self)
1729
+
1730
+ raise_event(event)
1731
+ end
1732
+ when Interaction::TYPES[:modal_submit]
1733
+
1734
+ event = ModalSubmitEvent.new(data, self)
1735
+ raise_event(event)
1736
+ when Interaction::TYPES[:autocomplete]
1737
+
1738
+ event = AutocompleteEvent.new(data, self)
1739
+ raise_event(event)
1740
+ end
1741
+ when :WEBHOOKS_UPDATE
1742
+ event = WebhookUpdateEvent.new(data, self)
1743
+ raise_event(event)
1744
+ when :THREAD_CREATE
1745
+ create_channel(data)
1746
+
1747
+ event = ThreadCreateEvent.new(data, self)
1748
+ raise_event(event)
1749
+ when :THREAD_UPDATE
1750
+ update_channel(data)
1751
+
1752
+ event = ThreadUpdateEvent.new(data, self)
1753
+ raise_event(event)
1754
+ when :THREAD_DELETE
1755
+ delete_channel(data)
1756
+ @thread_members.delete(data['id']&.resolve_id)
1757
+
1758
+ # raise ThreadDeleteEvent
1759
+ when :THREAD_LIST_SYNC
1760
+ server_id = data['guild_id'].to_i
1761
+ server = @servers[server_id]
1762
+
1763
+ # The `channel_ids` field has two meanings:
1764
+ #
1765
+ # 1. If the field is not present, the thread list is being synced for the whole server.
1766
+ #
1767
+ # 2. We are syncing the threads for a specific channel. This can happen when gaining access
1768
+ # to a channel.
1769
+ if (ids = data['channel_ids']&.map(&:to_i))
1770
+ @channels.delete_if { |_, channel| channel.thread? && ids.any?(channel.parent&.id) }
1771
+ server&.clear_threads(ids)
1772
+ else
1773
+ @channels.delete_if { |_, channel| channel.server.id == server_id && channel.thread? }
1774
+ server&.clear_threads
1775
+ end
1776
+
1777
+ data['members'].each { |member| ensure_thread_member(member) }
1778
+ data['threads'].each { |channel| ensure_channel(channel) }
1779
+
1780
+ # raise ThreadListSyncEvent?
1781
+ when :THREAD_MEMBER_UPDATE
1782
+ ensure_thread_member(data)
1783
+ when :THREAD_MEMBERS_UPDATE
1784
+ data['added_members']&.each do |added_member|
1785
+ ensure_thread_member(added_member) if added_member['user_id']
1786
+ end
1787
+
1788
+ data['removed_member_ids']&.each do |member_id|
1789
+ @thread_members[data['id']&.resolve_id]&.delete(member_id&.resolve_id)
1790
+ end
1791
+
1792
+ event = ThreadMembersUpdateEvent.new(data, self)
1793
+ raise_event(event)
1794
+ when :MESSAGE_POLL_VOTE_ADD
1795
+ event = PollVoteAddEvent.new(data, self)
1796
+ raise_event(event)
1797
+ when :MESSAGE_POLL_VOTE_REMOVE
1798
+ event = PollVoteRemoveEvent.new(data, self)
1799
+ raise_event(event)
1800
+ when :GUILD_SCHEDULED_EVENT_CREATE
1801
+ update_guild_scheduled_event(data)
1802
+
1803
+ event = ScheduledEventCreateEvent.new(data, self)
1804
+ raise_event(event)
1805
+ when :GUILD_SCHEDULED_EVENT_UPDATE
1806
+ update_guild_scheduled_event(data)
1807
+
1808
+ event = ScheduledEventUpdateEvent.new(data, self)
1809
+ raise_event(event)
1810
+ when :GUILD_SCHEDULED_EVENT_DELETE
1811
+ @servers[data['guild_id'].to_i]&.delete_scheduled_event(data['id'].to_i)
1812
+
1813
+ event = ScheduledEventDeleteEvent.new(data, self)
1814
+ raise_event(event)
1815
+ when :GUILD_SCHEDULED_EVENT_USER_ADD
1816
+ server = @servers[data['guild_id'].to_i]
1817
+ server&.scheduled_event(data['guild_scheduled_event_id'], request: false)&.increment_user_count
1818
+
1819
+ event = ScheduledEventUserAddEvent.new(data, self)
1820
+ raise_event(event)
1821
+ when :GUILD_SCHEDULED_EVENT_USER_REMOVE
1822
+ server = @servers[data['guild_id'].to_i]
1823
+ server&.scheduled_event(data['guild_scheduled_event_id'], request: false)&.deincrement_user_count
1824
+
1825
+ event = ScheduledEventUserRemoveEvent.new(data, self)
1826
+ raise_event(event)
1827
+ else
1828
+ # another event that we don't support yet
1829
+ debug "Event #{type} has been received but is unsupported. Raising UnknownEvent"
1830
+
1831
+ event = UnknownEvent.new(type, data, self)
1832
+ raise_event(event)
1833
+ end
1834
+
1835
+ # The existence of this array is checked before for performance reasons, since this has to be done for *every*
1836
+ # dispatch.
1837
+ if @event_handlers && @event_handlers[RawEvent]
1838
+ event = RawEvent.new(type, data, self)
1839
+ raise_event(event)
1840
+ end
1841
+ rescue Exception => e
1842
+ LOGGER.error('Gateway message error!')
1843
+ log_exception(e)
1844
+ end
1845
+
1846
+ # Notifies everything there is to be notified that the connection is now ready
1847
+ def notify_ready
1848
+ if @mode == :raw
1849
+ notify_raw_ready
1850
+ return
1851
+ end
1852
+
1853
+ # Make sure to raise the event
1854
+ raise_event(ReadyEvent.new(self))
1855
+ LOGGER.good 'Ready'
1856
+
1857
+ @gateway.notify_ready
1858
+ end
1859
+
1860
+ def raise_event(event)
1861
+ debug("Raised a #{event.class}")
1862
+ handle_awaits(event)
1863
+
1864
+ @event_handlers ||= {}
1865
+ handlers = @event_handlers[event.class]
1866
+ return unless handlers
1867
+
1868
+ handlers.dup.each do |handler|
1869
+ call_event(handler, event) if handler.matches?(event)
1870
+ end
1871
+ end
1872
+
1873
+ def call_event(handler, event)
1874
+ @event_executor.post do
1875
+ Thread.current[:onyxcord_name] = next_event_thread_name('et')
1876
+ begin
1877
+ handler.call(event)
1878
+ handler.after_call(event)
1879
+ rescue StandardError => e
1880
+ log_exception(e)
1881
+ end
1882
+ end
1883
+ end
1884
+
1885
+ def handle_awaits(event)
1886
+ @awaits ||= {}
1887
+ @awaits.each_value do |await|
1888
+ key, should_delete = await.match(event)
1889
+ next unless key
1890
+
1891
+ debug("should_delete: #{should_delete}")
1892
+ @awaits.delete(await.key) if should_delete
1893
+
1894
+ await_event = OnyxCord::Events::AwaitEvent.new(await, event, self)
1895
+ raise_event(await_event)
1896
+ end
1897
+ end
1898
+
1899
+ def calculate_intents(intents)
1900
+ intents = [intents] unless intents.is_a? Array
1901
+
1902
+ intents.reduce(0) do |sum, intent|
1903
+ case intent
1904
+ when Symbol
1905
+ intent = INTENT_ALIASES[intent] || intent
1906
+
1907
+ if INTENTS[intent]
1908
+ sum | INTENTS[intent]
1909
+ else
1910
+ LOGGER.warn("Unknown intent: #{intent}")
1911
+ sum
1912
+ end
1913
+ when Integer
1914
+ sum | intent
1915
+ else
1916
+ LOGGER.warn("Invalid intent: #{intent}")
1917
+ sum
1918
+ end
1919
+ end
1920
+ end
1921
+
1922
+ def dispatch_packet(packet)
1923
+ type = packet['t']&.intern
1924
+ data = packet['d']
1925
+
1926
+ case @mode
1927
+ when :raw
1928
+ dispatch_raw_packet(packet)
1929
+ notify_raw_ready if type == :READY
1930
+ when :hybrid
1931
+ dispatch_raw_packet(packet)
1932
+ handle_dispatch(type, data)
1933
+ else
1934
+ handle_dispatch(type, data)
1935
+ end
1936
+ end
1937
+
1938
+ def dispatch_raw_packet(packet)
1939
+ handlers = @raw_handlers
1940
+ return unless handlers
1941
+
1942
+ handlers.dup.each do |handler|
1943
+ call_raw_handler(handler, packet) if handler.matches?(packet)
1944
+ end
1945
+ end
1946
+
1947
+ def call_raw_handler(handler, packet)
1948
+ @event_executor.post do
1949
+ Thread.current[:onyxcord_name] = next_event_thread_name('rt')
1950
+ handler.call(packet)
1951
+ rescue StandardError => e
1952
+ log_exception(e)
1953
+ end
1954
+ end
1955
+
1956
+ def notify_raw_ready
1957
+ return if @raw_ready_notified
1958
+
1959
+ @raw_ready_notified = true
1960
+ LOGGER.good 'Ready'
1961
+ @gateway.notify_ready
1962
+ end
1963
+
1964
+ def next_event_thread_name(prefix)
1965
+ @current_thread_mutex.synchronize do
1966
+ @current_thread += 1
1967
+ "#{prefix}-#{@current_thread}"
1968
+ end
1969
+ end
1970
+ end
1971
+ end