disrb 0.1.2.1 → 0.1.3

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.
data/lib/disrb.rb CHANGED
@@ -5,24 +5,77 @@ require 'async'
5
5
  require 'async/http/endpoint'
6
6
  require 'async/websocket/client'
7
7
  require 'faraday'
8
- require 'disrb/guild'
9
- require 'disrb/logger'
10
- require 'disrb/user'
11
- require 'disrb/message'
8
+ require_relative 'disrb/guild'
9
+ require_relative 'disrb/logger'
10
+ require_relative 'disrb/user'
11
+ require_relative 'disrb/message'
12
+ require_relative 'disrb/application_commands'
13
+
14
+ # Contains functions related to Discord snowflakes.
15
+ class Snowflake
16
+ # @!attribute [r] snowflake
17
+ # @return [String] 64-bit binary representation of the snowflake as a string
18
+ # @!attribute [r] discord_epoch_timestamp
19
+ # @return [String] binary representation of the discord epoch timestamp
20
+ # (milliseconds since the first second of 2015).
21
+ # @!attribute [r] internal_worker_id
22
+ # @return [String] Internal worker ID.
23
+ # @!attribute [r] internal_process_id
24
+ # @return [String] Internal process ID.
25
+ # @!attribute [r] gen_id_on_process
26
+ # @return [String] Nº of the ID generated on the process. This is incremented every time a new snowflake is generated
27
+ # on the same process.
28
+ # @!attribute [r] unix_timestamp
29
+ # @return [Integer] Unix timestamp of the snowflake in milliseconds.
30
+ # @!attribute [r] timestamp
31
+ # @return [Time] Timestamp of the snowflake in UTC as a Time object.
32
+ attr_accessor(:snowflake, :discord_epoch_timestamp, :internal_worker_id, :internal_process_id, :gen_id_on_process,
33
+ :unix_timestamp, :timestamp)
34
+
35
+ # Creates a new Snowflake instance.
36
+ # @param snowflake [Integer] The snowflake to be used.
37
+ # @return [Snowflake] Snowflake instance.
38
+ def initialize(snowflake)
39
+ @snowflake = snowflake.to_s(2).rjust(64, '0')
40
+ @discord_epoch_timestamp = snowflake[0..41]
41
+ @internal_worker_id = snowflake[42..46]
42
+ @internal_process_id = snowflake[47..51]
43
+ @gen_id_on_process = snowflake[52..64]
44
+ @unix_timestamp = snowflake[0..41].to_i(2) + 1_420_070_400_000
45
+ @timestamp = Time.at((snowflake[0..41].to_i(2) + 1_420_070_400_000) / 1000).utc
46
+ end
47
+ end
12
48
 
13
- # DiscordApi
14
- # The class that contains everything that interacts with the Discord API.
49
+ # Class that contains functions that allow interacting with the Discord API.
50
+ # @version 0.1.3
15
51
  class DiscordApi
16
- attr_accessor(:base_url, :authorization_header, :application_id, :interaction_created, :interaction, :logger)
17
-
52
+ # @!attribute [r] base_url
53
+ # @return [String] the base URL that is used to access the Discord API. ex: "https://discord.com/api/v10"
54
+ # @!attribute [r] authorization_header
55
+ # @return [String] the authorization header that is used to authenticate requests to the Discord API.
56
+ # @!attribute [r] application_id
57
+ # @return [Integer] the application ID of the bot that has been assigned to the provided authorization token.
58
+ attr_accessor(:base_url, :authorization_header, :application_id, :logger)
59
+
60
+ # Creates a new DiscordApi instance. (required to use most functions)
61
+ #
62
+ # @param authorization_token_type [String] The type of authorization token provided by Discord, 'Bot' or 'Bearer'.
63
+ # @param authorization_token [String] The value of the authorization token provided by Discord.
64
+ # @param verbosity_level [String, Integer, nil] The verbosity level of the logger.
65
+ # Set verbosity_level to:
66
+ # - 'all' or 5 to log all of the below plus debug messages
67
+ # - 'info', 4 or nil to log all of the below plus info messages [DEFAULT]
68
+ # - 'warning' or 3 to log all of the below plus warning messages
69
+ # - 'error' or 2 to log fatal errors and error messages
70
+ # - 'fatal_error' or 1 to log only fatal errors
71
+ # - 'none' or 0 for no logging
72
+ # @return [DiscordApi] DiscordApi instance.
18
73
  def initialize(authorization_token_type, authorization_token, verbosity_level = nil)
19
74
  @api_version = '10'
20
75
  @base_url = "https://discord.com/api/v#{@api_version}"
21
76
  @authorization_token_type = authorization_token_type
22
77
  @authorization_token = authorization_token
23
78
  @authorization_header = "#{authorization_token_type} #{authorization_token}"
24
- @interaction_created = false
25
- @interaction = {}
26
79
  if verbosity_level.nil?
27
80
  @verbosity_level = 4
28
81
  elsif verbosity_level.is_a?(String)
@@ -66,6 +119,12 @@ class DiscordApi
66
119
  end
67
120
  end
68
121
 
122
+ # Converts a hash into a valid query string.
123
+ # @example Convert a hash into a query string
124
+ # DiscordApi.handle_query_strings({'key1' => 'value1', 'key2' => 'value2'}) #=> "?key1=value1&key2=value2"
125
+ # If the hash is empty, it returns an empty string.
126
+ # @param query_string_hash [Hash] The hash to convert into a query string.
127
+ # @return [String] The query string.
69
128
  def self.handle_query_strings(query_string_hash)
70
129
  query_string_array = []
71
130
  query_string_hash.each do |key, value|
@@ -81,272 +140,22 @@ class DiscordApi
81
140
  query_string_array.join
82
141
  end
83
142
 
84
- def self.handle_snowflake(snowflake)
85
- snowflake = snowflake.to_s(2).rjust(64, '0')
86
- {
87
- discord_epoch_timestamp: snowflake[0..41],
88
- internal_worker_id: snowflake[42..46],
89
- internal_process_id: snowflake[47..51],
90
- gen_id_on_process: snowflake[52..64],
91
- unix_timestamp: snowflake[0..41].to_i(2) + 1_420_070_400_000,
92
- timestamp: Time.at((snowflake[0..41].to_i(2) + 1_420_070_400_000) / 1000).utc
93
- }
94
- end
95
-
96
- def create_guild_application_command(guild_id, name, name_localizations: nil, description: nil,
97
- description_localizations: nil, options: nil, default_member_permissions: nil,
98
- default_permission: nil, type: nil, nsfw: nil)
99
- output = {}
100
- output[:name] = name
101
- output[:name_localizations] = name_localizations unless name_localizations.nil?
102
- output[:description] = description unless description.nil?
103
- output[:description_localizations] = description_localizations unless description_localizations.nil?
104
- output[:options] = options unless options.nil?
105
- output[:default_permission] = default_permission unless default_permission.nil?
106
- output[:type] = type unless type.nil?
107
- output[:nsfw] = nsfw unless nsfw.nil?
108
- output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
109
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands"
110
- data = JSON.generate(output)
111
- headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
112
- response = DiscordApi.post(url, data, headers)
113
- return response unless response.status != 201 || response.status != 200
114
-
115
- @logger.error("Failed to create guild application command in guild with ID #{guild_id}. Response: #{response.body}")
116
- response
117
- end
118
-
119
- def create_guild_application_commands(application_commands_array)
120
- if application_commands_array.is_a?(Array)
121
- application_commands_array.each do |parameter_array|
122
- if parameter_array.is_a?(Array)
123
- create_guild_application_command(*parameter_array)
124
- else
125
- @logger.error("Invalid parameter array: #{parameter_array}. Expected an array of parameters.")
126
- end
127
- end
128
- else
129
- @logger.error("Invalid application commands array: #{application_commands_array}. Expected an array of arrays.")
130
- end
131
- end
132
-
133
- def create_global_application_command(name, name_localizations: nil, description: nil,
134
- description_localizations: nil, options: nil,
135
- default_member_permissions: nil, default_permission: nil,
136
- integration_types: nil, contexts: nil, type: nil, nsfw: nil)
137
- output = {}
138
- output[:name] = name
139
- output[:name_localizations] = name_localizations unless name_localizations.nil?
140
- output[:description] = description unless description.nil?
141
- output[:description_localizations] = description_localizations unless description_localizations.nil?
142
- output[:options] = options unless options.nil?
143
- output[:default_permission] = default_permission unless default_permission.nil?
144
- output[:type] = type unless type.nil?
145
- output[:nsfw] = nsfw unless nsfw.nil?
146
- output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
147
- output[:integration_types] = integration_types unless integration_types.nil?
148
- output[:contexts] = contexts unless contexts.nil?
149
- url = "#{@base_url}/applications/#{@application_id}/commands"
150
- data = JSON.generate(output)
151
- headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
152
- response = DiscordApi.post(url, data, headers)
153
- return response unless response.status != 201 || response.status != 200
154
-
155
- @logger.error("Failed to create global application command. Response: #{response.body}")
156
- response
157
- end
158
-
159
- def create_global_application_commands(application_commands_array)
160
- if application_commands_array.is_a?(Array)
161
- application_commands_array.each do |parameter_array|
162
- if parameter_array.is_a?(Array)
163
- create_global_application_command(*parameter_array)
164
- else
165
- @logger.error("Invalid parameter array: #{parameter_array}. Expected an array of parameters.")
166
- end
167
- end
168
- else
169
- @logger.error("Invalid application commands array: #{application_commands_array}. Expected an array of arrays.")
170
- end
171
- end
172
-
173
- def edit_global_application_command(command_id, name: nil, name_localizations: nil, description: nil,
174
- description_localizations: nil, options: nil, default_member_permissions: nil,
175
- default_permission: nil, integration_types: nil, contexts: nil, nsfw: nil)
176
- if args[1..].all?(&:nil?)
177
- @logger.warn("No modifications provided for global application command with ID #{command_id}. Skipping.")
178
- return nil
179
- end
180
- output = {}
181
- output[:name] = name
182
- output[:name_localizations] = name_localizations unless name_localizations.nil?
183
- output[:description] = description unless description.nil?
184
- output[:description_localizations] = description_localizations unless description_localizations.nil?
185
- output[:options] = options unless options.nil?
186
- output[:default_permission] = default_permission unless default_permission.nil?
187
- output[:nsfw] = nsfw unless nsfw.nil?
188
- output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
189
- output[:integration_types] = integration_types unless integration_types.nil?
190
- output[:contexts] = contexts unless contexts.nil?
191
- url = "#{@base_url}/applications/#{@application_id}/commands/#{command_id}"
192
- data = JSON.generate(output)
193
- headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
194
- response = DiscordApi.patch(url, data, headers)
195
- return response unless response.status != 200
196
-
197
- @logger.error("Failed to edit global application command with ID #{command_id}. Response: #{response.body}")
198
- response
199
- end
200
-
201
- def edit_guild_application_command(guild_id, command_id, name: nil, name_localizations: nil, description: nil,
202
- description_localizations: nil, options: nil, default_member_permissions: nil,
203
- default_permission: nil, nsfw: nil)
204
- if args[2..].all?(&:nil?)
205
- @logger.warn("No modifications provided for guild application command with command ID #{command_id}. Skipping.")
206
- return nil
207
- end
208
- output = {}
209
- output[:name] = name
210
- output[:name_localizations] = name_localizations unless name_localizations.nil?
211
- output[:description] = description unless description.nil?
212
- output[:description_localizations] = description_localizations unless description_localizations.nil?
213
- output[:options] = options unless options.nil?
214
- output[:default_permission] = default_permission unless default_permission.nil?
215
- output[:nsfw] = nsfw unless nsfw.nil?
216
- output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
217
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}"
218
- data = JSON.generate(output)
219
- headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
220
- response = DiscordApi.patch(url, data, headers)
221
- return response unless response.status != 200
222
-
223
- @logger.error("Failed to edit guild application command with ID #{command_id}. Response: #{response.body}")
224
- response
225
- end
226
-
227
- def delete_global_application_command(command_id)
228
- url = "#{@base_url}/applications/#{@application_id}/commands/#{command_id}"
229
- headers = { 'Authorization': @authorization_header }
230
- response = DiscordApi.delete(url, headers)
231
- return response unless response.status != 204
232
-
233
- @logger.error("Failed to delete global application command with ID #{command_id}. Response: #{response.body}")
234
- response
235
- end
236
-
237
- def delete_guild_application_command(guild_id, command_id)
238
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}"
239
- headers = { 'Authorization': @authorization_header }
240
- response = DiscordApi.delete(url, headers)
241
- return response unless response.status != 204
242
-
243
- @logger.error("Failed to delete guild application command with ID #{command_id} in guild with ID #{guild_id}. " \
244
- "Response: #{response.body}")
245
- end
246
-
247
- def get_guild_application_commands(guild_id, with_localizations: nil)
248
- query_string_hash = {}
249
- query_string_hash[:with_localizations] = with_localizations unless with_localizations.nil?
250
- query_string = DiscordApi.handle_query_strings(query_string_hash)
251
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands#{query_string}"
252
- headers = { 'Authorization': @authorization_header }
253
- response = DiscordApi.get(url, headers)
254
- return response unless response.status != 200
255
-
256
- @logger.error("Failed to get guild application commands for guild with ID #{guild_id}. Response: #{response.body}")
257
- response
258
- end
259
-
260
- def get_global_application_commands(with_localizations: false)
261
- query_string_hash = {}
262
- query_string_hash[:with_localizations] = with_localizations unless with_localizations.nil?
263
- query_string = DiscordApi.handle_query_strings(query_string_hash)
264
- url = "#{@base_url}/applications/#{@application_id}/commands#{query_string}"
265
- headers = { 'Authorization': @authorization_header }
266
- response = DiscordApi.get(url, headers)
267
- return response unless response.status != 200
268
-
269
- @logger.error("Failed to get global application commands. Response: #{response.body}")
270
- response
271
- end
272
-
273
- def get_global_application_command(command_id)
274
- url = "#{@base_url}/applications/#{@application_id}/commands/#{command_id}"
275
- headers = { 'Authorization': @authorization_header }
276
- response = DiscordApi.get(url, headers)
277
- return response unless response.status != 200
278
-
279
- @logger.error("Failed to get global application command with ID #{command_id}. Response: #{response.body}")
280
- response
281
- end
282
-
283
- def get_guild_application_command(guild_id, command_id)
284
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}"
285
- headers = { 'Authorization': @authorization_header }
286
- response = DiscordApi.get(url, headers)
287
- return response unless response.status != 200
288
-
289
- @logger.error("Failed to get guild application command with ID #{command_id}. Response: #{response.body}")
290
- response
291
- end
292
-
293
- def bulk_overwrite_global_application_commands(commands)
294
- url = "#{@base_url}/applications/#{@application_id}/commands"
295
- data = JSON.generate(commands)
296
- headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
297
- response = DiscordApi.put(url, data, headers)
298
- return response unless response.status != 200
299
-
300
- @logger.error("Failed to bulk overwrite global application commands. Response: #{response.body}")
301
- response
302
- end
303
-
304
- def bulk_overwrite_guild_application_commands(guild_id, commands)
305
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands"
306
- data = JSON.generate(commands)
307
- headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
308
- response = DiscordApi.put(url, data, headers)
309
- return response unless response.status != 200
310
-
311
- @logger.error("Failed to bulk overwrite guild application commands in guild with ID #{guild_id}. " \
312
- "Response: #{response.body}")
313
- response
314
- end
315
-
316
- def get_guild_application_command_permissions(guild_id)
317
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/permissions"
318
- headers = { 'Authorization': @authorization_header }
319
- response = DiscordApi.get(url, headers)
320
- return response unless response.status != 200
321
-
322
- @logger.error("Failed to get guild application command permissions for guild with ID #{guild_id}. " \
323
- "Response: #{response.body}")
324
- response
325
- end
326
-
327
- def get_application_command_permissions(guild_id, command_id)
328
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}/permissions"
329
- headers = { 'Authorization': @authorization_header }
330
- response = DiscordApi.get(url, headers)
331
- return response unless response.status != 200
332
-
333
- @logger.error("Failed to get appliaction command permissions for command with ID #{command_id} in guild with ID " \
334
- "#{guild_id}. Response: #{response.body}")
335
- response
336
- end
337
-
338
- def edit_application_command_permissions(guild_id, command_id, permissions)
339
- url = "#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}/permissions"
340
- data = JSON.generate(permissions)
341
- headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
342
- response = DiscordApi.put(url, data, headers)
343
- return response unless response.status != 200
344
-
345
- @logger.error("Failed to edit application command permissions for command with ID #{command_id} in guild with ID " \
346
- "#{guild_id}. Response: #{response.body}")
347
- response
348
- end
349
-
143
+ # Connects to the Discord Gateway and identifies/resumes the session.
144
+ # This establishes a WebSocket connection, performs Identify/Resume flows, sends/receives heartbeats,
145
+ # and yields gateway events to the provided block.
146
+ # See https://discord.com/developers/docs/topics/gateway and
147
+ # https://discord.com/developers/docs/topics/gateway#identify and
148
+ # https://discord.com/developers/docs/topics/gateway#resume
149
+ # @param activities [Hash, Array, nil] Activity or list of activities to set in presence.
150
+ # @param os [String, nil] OS name reported to the Gateway. Host OS if nil.
151
+ # @param browser [String, nil] Browser/client name reported to the Gateway. "discord.rb" if nil.
152
+ # @param device [String, nil] Device name reported to the Gateway. "discord.rb" if nil
153
+ # @param intents [Integer, nil] Bitwise Gateway intents integer.
154
+ # @param presence_since [Integer, TrueClass, nil] Unix ms timestamp or true for since value in presence.
155
+ # @param presence_status [String, nil] Presence status (e.g., "online", "idle", "dnd").
156
+ # @param presence_afk [TrueClass, FalseClass, nil] Whether the client is AFK.
157
+ # @yield [event] Yields parsed Gateway events to the block if provided.
158
+ # @return [void]
350
159
  def connect_gateway(activities: nil, os: nil, browser: nil, device: nil, intents: nil, presence_since: nil,
351
160
  presence_status: nil, presence_afk: nil, &block)
352
161
  if @authorization_token_type == 'Bearer'
@@ -426,6 +235,7 @@ class DiscordApi
426
235
  Async do |_task|
427
236
  rescue_connection, sequence, resume_gateway_url, session_id = nil
428
237
  loop do
238
+ recieved_ready = false
429
239
  url = if rescue_connection.nil?
430
240
  response = DiscordApi.get("#{@base_url}/gateway")
431
241
  if response.status == 200
@@ -439,9 +249,6 @@ class DiscordApi
439
249
  end
440
250
  endpoint = Async::HTTP::Endpoint.parse(url, alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
441
251
  Async::WebSocket::Client.connect(endpoint) do |connection|
442
- connection.write JSON.generate({ op: 1, d: nil })
443
- connection.flush
444
-
445
252
  if rescue_connection.nil?
446
253
  identify = {}
447
254
  identify[:op] = 2
@@ -481,50 +288,72 @@ class DiscordApi
481
288
 
482
289
  loop do
483
290
  message = connection.read
291
+ next if message.nil?
292
+
293
+ @logger.debug("Raw gateway message: #{message.buffer}")
484
294
  message = JSON.parse(message, symbolize_names: true)
485
- @logger.debug(message)
295
+ @logger.debug("JSON parsed gateway message: #{message}")
486
296
 
297
+ block.call(message)
487
298
  case message
488
299
  in { op: 10 }
489
300
  @logger.info('Received Hello')
490
301
  @heartbeat_interval = message[:d][:heartbeat_interval]
491
- in { op: 1 }
302
+ in { op: 1 }
492
303
  @logger.info('Received Heartbeat Request')
493
304
  connection.write JSON.generate({ op: 1, d: nil })
494
305
  connection.flush
495
306
  in { op: 11 }
496
307
  @logger.info('Received Heartbeat ACK')
497
- in { op: 0, t: 'INTERACTION_CREATE' }
498
- @logger.info('An interaction was created')
499
- sequence = message[:s]
500
- block.call(message)
501
308
  in { op: 0, t: 'READY' }
502
309
  @logger.info('Recieved Ready')
503
310
  session_id = message[:d][:session_id]
504
311
  resume_gateway_url = message[:d][:resume_gateway_url]
505
312
  sequence = message[:s]
313
+ recieved_ready = true
506
314
  in { op: 0 }
507
315
  @logger.info('An event was dispatched')
508
316
  sequence = message[:s]
509
317
  in { op: 7 }
510
- rescue_connection = { type: 'reconnect', session_id: session_id, resume_gateway_url: resume_gateway_url,
511
- sequence: sequence }
512
- @logger.warn('Received Reconnect. A rescue will be attempted....')
318
+ if recieved_ready
319
+ rescue_connection = { session_id: session_id, resume_gateway_url: resume_gateway_url,
320
+ sequence: sequence }
321
+ @logger.warn('Received Reconnect. A rescue will be attempted....')
322
+ else
323
+ @logger.warn('Received Reconnect. A rescue cannot be attempted.')
324
+ end
325
+ in { op: 9 }
326
+ if message[:d] && recieved_ready
327
+ rescue_connection = { session_id: session_id, resume_gateway_url: resume_gateway_url,
328
+ sequence: sequence }
329
+ @logger.warn('Recieved Invalid Session. A rescue will be attempted...')
330
+ else
331
+ @logger.warn('Recieved Invalid Session. A rescue cannot be attempted.')
332
+ end
513
333
  else
514
- @logger.error('Unimplemented event type')
334
+ @logger.error("Unimplemented event type with opcode #{message[:op]}")
515
335
  end
516
336
  end
517
337
  end
518
338
  rescue Protocol::WebSocket::ClosedError
519
- @logger.warn('WebSocket connection closed. Attempting rescue.')
520
- if rescue_connection && rescue_connection[:type] == 'reconnect'
521
- @logger.info('Rescue possible. Starting...')
522
- next
339
+ @logger.warn('WebSocket connection closed. Attempting reconnect and rescue.')
340
+ if rescue_connection
341
+ @logger.info('Rescue possible. Reconnecting and rescuing...')
342
+ else
343
+ @logger.info('Rescue not possible. Reconnecting...')
523
344
  end
345
+ next
524
346
  end
525
347
  end
526
348
  end
527
349
 
350
+ # Creates a response to an interaction. Returns 204 No Content by default, or 200 OK with the created message
351
+ # if `with_response` is true and the response type expects it.
352
+ # See https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response
353
+ # @param interaction [Hash] The interaction payload received from the Gateway.
354
+ # @param response [Hash] The interaction response payload.
355
+ # @param with_response [TrueClass, FalseClass] Whether to request the created message in the response.
356
+ # @return [Faraday::Response] The response from the Discord API.
528
357
  def respond_interaction(interaction, response, with_response: false)
529
358
  query_string_hash = {}
530
359
  query_string_hash[:with_response] = with_response
@@ -539,123 +368,92 @@ class DiscordApi
539
368
  response
540
369
  end
541
370
 
542
- def self.calculate_permissions_integer(permissions)
543
- bitwise_permission_flags = {
544
- create_instant_invite: 0x0000000000000001,
545
- kick_members: 0x0000000000000002,
546
- ban_members: 0x0000000000000004,
547
- administrator: 0x0000000000000008,
548
- manage_channels: 0x0000000000000010,
549
- manage_guild: 0x0000000000000020,
550
- add_reactions: 0x0000000000000040,
551
- view_audit_log: 0x0000000000000080,
552
- priority_speaker: 0x0000000000000100,
553
- stream: 0x0000000000000200,
554
- view_channel: 0x0000000000000400,
555
- send_messages: 0x0000000000000800,
556
- send_tts_messages: 0x0000000000001000,
557
- manage_messages: 0x0000000000002000,
558
- embed_links: 0x0000000000004000,
559
- attach_files: 0x0000000000008000,
560
- read_message_history: 0x0000000000010000,
561
- mention_everyone: 0x0000000000020000,
562
- use_external_emojis: 0x0000000000040000,
563
- view_guild_insights: 0x0000000000080000,
564
- connect: 0x0000000000100000,
565
- speak: 0x0000000000200000,
566
- mute_members: 0x0000000000400000,
567
- deafen_members: 0x0000000000800000,
568
- move_members: 0x0000000001000000,
569
- use_vad: 0x0000000002000000,
570
- change_nickname: 0x0000000004000000,
571
- manage_nicknames: 0x0000000008000000,
572
- manage_roles: 0x0000000010000000,
573
- manage_webhooks: 0x0000000020000000,
574
- manage_guild_expressions: 0x0000000040000000,
575
- use_application_commands: 0x0000000080000000,
576
- request_to_speak: 0x0000000100000000,
577
- manage_events: 0x0000000200000000,
578
- manage_threads: 0x0000000400000000,
579
- create_public_threads: 0x0000000800000000,
580
- create_private_threads: 0x0000001000000000,
581
- use_external_stickers: 0x0000002000000000,
582
- send_messages_in_threads: 0x0000004000000000,
583
- use_embedded_activities: 0x0000008000000000,
584
- moderate_members: 0x0000010000000000,
585
- view_creator_monetization_analytics: 0x0000020000000000,
586
- use_soundboard: 0x0000040000000000,
587
- create_guild_expressions: 0x0000080000000000,
588
- create_events: 0x0000100000000000,
589
- use_external_sounds: 0x0000200000000000,
590
- send_voice_messages: 0x0000400000000000,
591
- send_polls: 0x0002000000000000,
592
- use_external_apps: 0x0004000000000000
371
+ # Returns a hash of permission names and their corresponding bitwise values.
372
+ # See https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
373
+ def self.bitwise_permission_flags
374
+ {
375
+ create_instant_invite: 1 << 0,
376
+ kick_members: 1 << 1,
377
+ ban_members: 1 << 2,
378
+ administrator: 1 << 3,
379
+ manage_channels: 1 << 4,
380
+ manage_guild: 1 << 5,
381
+ add_reactions: 1 << 6,
382
+ view_audit_log: 1 << 7,
383
+ priority_speaker: 1 << 8,
384
+ stream: 1 << 9,
385
+ view_channel: 1 << 10,
386
+ send_messages: 1 << 11,
387
+ send_tts_messages: 1 << 12,
388
+ manage_messages: 1 << 13,
389
+ embed_links: 1 << 14,
390
+ attach_files: 1 << 15,
391
+ read_message_history: 1 << 16,
392
+ mention_everyone: 1 << 17,
393
+ use_external_emojis: 1 << 18,
394
+ view_guild_insights: 1 << 19,
395
+ connect: 1 << 20,
396
+ speak: 1 << 21,
397
+ mute_members: 1 << 22,
398
+ deafen_members: 1 << 23,
399
+ move_members: 1 << 24,
400
+ use_vad: 1 << 25,
401
+ change_nickname: 1 << 26,
402
+ manage_nicknames: 1 << 27,
403
+ manage_roles: 1 << 28,
404
+ manage_webhooks: 1 << 29,
405
+ manage_guild_expressions: 1 << 30,
406
+ use_application_commands: 1 << 31,
407
+ request_to_speak: 1 << 32,
408
+ manage_events: 1 << 33,
409
+ manage_threads: 1 << 34,
410
+ create_public_threads: 1 << 35,
411
+ create_private_threads: 1 << 36,
412
+ use_external_stickers: 1 << 37,
413
+ send_messages_in_threads: 1 << 38,
414
+ use_embedded_activities: 1 << 39,
415
+ moderate_members: 1 << 40,
416
+ view_creator_monetization_analytics: 1 << 41,
417
+ use_soundboard: 1 << 42,
418
+ create_guild_expressions: 1 << 43,
419
+ create_events: 1 << 44,
420
+ use_external_sounds: 1 << 45,
421
+ send_voice_messages: 1 << 46,
422
+ send_polls: 1 << 49,
423
+ use_external_apps: 1 << 50,
424
+ pin_messages: 1 << 51
593
425
  }
426
+ end
427
+
428
+ # Calculates a permissions integer from an array of permission names.
429
+ # See https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
430
+ # @param permissions [Array] Array of permission names as strings or symbols, case insensitive, use underscores
431
+ # between spaces.
432
+ # @return [Integer] Bitwise OR of all permission flags.
433
+ def self.calculate_permissions_integer(permissions)
594
434
  permissions = permissions.map do |permission|
595
- bitwise_permission_flags[permission.downcase.to_sym]
435
+ DiscordApi.bitwise_permission_flags[permission.downcase.to_sym]
596
436
  end
597
437
  permissions.reduce(0) { |acc, n| acc | n }
598
438
  end
599
439
 
440
+ # Reverses a permissions integer back into an array of permission names.
441
+ # See https://discord.com/developers/docs/topics/permissions#permissions-bitwise-permission-flags
442
+ # @param permissions_integer [Integer] Bitwise permissions integer.
443
+ # @return [Array] Array of permission names present (as symbols) in the integer.
600
444
  def self.reverse_permissions_integer(permissions_integer)
601
- bitwise_permission_flags = {
602
- create_instant_invite: 0x0000000000000001,
603
- kick_members: 0x0000000000000002,
604
- ban_members: 0x0000000000000004,
605
- administrator: 0x0000000000000008,
606
- manage_channels: 0x0000000000000010,
607
- manage_guild: 0x0000000000000020,
608
- add_reactions: 0x0000000000000040,
609
- view_audit_log: 0x0000000000000080,
610
- priority_speaker: 0x0000000000000100,
611
- stream: 0x0000000000000200,
612
- view_channel: 0x0000000000000400,
613
- send_messages: 0x0000000000000800,
614
- send_tts_messages: 0x0000000000001000,
615
- manage_messages: 0x0000000000002000,
616
- embed_links: 0x0000000000004000,
617
- attach_files: 0x0000000000008000,
618
- read_message_history: 0x0000000000010000,
619
- mention_everyone: 0x0000000000020000,
620
- use_external_emojis: 0x0000000000040000,
621
- view_guild_insights: 0x0000000000080000,
622
- connect: 0x0000000000100000,
623
- speak: 0x0000000000200000,
624
- mute_members: 0x0000000000400000,
625
- deafen_members: 0x0000000000800000,
626
- move_members: 0x0000000001000000,
627
- use_vad: 0x0000000002000000,
628
- change_nickname: 0x0000000004000000,
629
- manage_nicknames: 0x0000000008000000,
630
- manage_roles: 0x0000000010000000,
631
- manage_webhooks: 0x0000000020000000,
632
- manage_guild_expressions: 0x0000000040000000,
633
- use_application_commands: 0x0000000080000000,
634
- request_to_speak: 0x0000000100000000,
635
- manage_events: 0x0000000200000000,
636
- manage_threads: 0x0000000400000000,
637
- create_public_threads: 0x0000000800000000,
638
- create_private_threads: 0x0000001000000000,
639
- use_external_stickers: 0x0000002000000000,
640
- send_messages_in_threads: 0x0000004000000000,
641
- use_embedded_activities: 0x0000008000000000,
642
- moderate_members: 0x0000010000000000,
643
- view_creator_monetization_analytics: 0x0000020000000000,
644
- use_soundboard: 0x0000040000000000,
645
- create_guild_expressions: 0x0000080000000000,
646
- create_events: 0x0000100000000000,
647
- use_external_sounds: 0x0000200000000000,
648
- send_voice_messages: 0x0000400000000000,
649
- send_polls: 0x0002000000000000,
650
- use_external_apps: 0x0004000000000000
651
- }
652
445
  permissions = []
653
- bitwise_permission_flags.each do |permission, value|
446
+ DiscordApi.bitwise_permission_flags.each do |permission, value|
654
447
  permissions << permission if (permissions_integer & value) != 0
655
448
  end
656
449
  permissions
657
450
  end
658
451
 
452
+ # Calculates a gateway intents integer from an array of intent names.
453
+ # See https://discord.com/developers/docs/topics/gateway#gateway-intents
454
+ # @param intents [Array] Array of gateway intent names as strings or symbols, case insensitive, use underscores
455
+ # between spaces.
456
+ # @return [Integer] Bitwise OR of all intents flags.
659
457
  def self.calculate_gateway_intents(intents)
660
458
  bitwise_intent_flags = {
661
459
  guilds: 1 << 0,
@@ -682,6 +480,10 @@ class DiscordApi
682
480
  intents.reduce(0) { |acc, n| acc | n }
683
481
  end
684
482
 
483
+ # Performs an HTTP GET request using Faraday.
484
+ # @param url [String] Full URL including scheme and host; path may be included.
485
+ # @param headers [Hash, nil] Optional request headers.
486
+ # @return [Faraday::Response] The Faraday response object.
685
487
  def self.get(url, headers = nil)
686
488
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
687
489
  @logger.error("Empty/invalid URL provided: #{url}. Cannot perform GET request.") if split_url.empty?
@@ -695,6 +497,10 @@ class DiscordApi
695
497
  end
696
498
  end
697
499
 
500
+ # Performs an HTTP DELETE request using Faraday.
501
+ # @param url [String] Full URL including scheme and host; path may be included.
502
+ # @param headers [Hash, nil] Optional request headers.
503
+ # @return [Faraday::Response] The Faraday response object.
698
504
  def self.delete(url, headers = nil)
699
505
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
700
506
  @logger.error("Empty/invalid URL provided: #{url}. Cannot perform DELETE request.") if split_url.empty?
@@ -708,6 +514,11 @@ class DiscordApi
708
514
  end
709
515
  end
710
516
 
517
+ # Performs an HTTP POST request using Faraday.
518
+ # @param url [String] Full URL including scheme and host; path may be included.
519
+ # @param data [String] Serialized request body (e.g., JSON string).
520
+ # @param headers [Hash, nil] Optional request headers.
521
+ # @return [Faraday::Response] The Faraday response object.
711
522
  def self.post(url, data, headers = nil)
712
523
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
713
524
  @logger.error("Empty/invalid URL provided: #{url}. Cannot perform POST request.") if split_url.empty?
@@ -721,6 +532,11 @@ class DiscordApi
721
532
  end
722
533
  end
723
534
 
535
+ # Performs an HTTP PATCH request using Faraday.
536
+ # @param url [String] Full URL including scheme and host; path may be included.
537
+ # @param data [String] Serialized request body (e.g., JSON string).
538
+ # @param headers [Hash, nil] Optional request headers.
539
+ # @return [Faraday::Response] The Faraday response object.
724
540
  def self.patch(url, data, headers = nil)
725
541
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
726
542
  @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PATCH request.") if split_url.empty?
@@ -734,6 +550,11 @@ class DiscordApi
734
550
  end
735
551
  end
736
552
 
553
+ # Performs an HTTP PUT request using Faraday.
554
+ # @param url [String] Full URL including scheme and host; path may be included.
555
+ # @param data [String] Serialized request body (e.g., JSON string).
556
+ # @param headers [Hash, nil] Optional request headers.
557
+ # @return [Faraday::Response] The Faraday response object.
737
558
  def self.put(url, data, headers = nil)
738
559
  split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
739
560
  @logger.error("Empty/invalid URL provided: #{url}. Cannot perform PUT request.") if split_url.empty?