disrb 0.1.2.2 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +88 -0
- data/README.md +16 -5
- data/lib/disrb/application_commands.rb +401 -0
- data/lib/disrb/guild.rb +439 -59
- data/lib/disrb/logger.rb +69 -32
- data/lib/disrb/message.rb +157 -27
- data/lib/disrb/user.rb +74 -7
- data/lib/disrb.rb +218 -397
- metadata +17 -1
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
#
|
|
14
|
-
#
|
|
49
|
+
# Class that contains functions that allow interacting with the Discord API.
|
|
50
|
+
# @version 0.1.3
|
|
15
51
|
class DiscordApi
|
|
16
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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 if 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:
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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(
|
|
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
|
|
521
|
-
@logger.info('Rescue possible.
|
|
522
|
-
|
|
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
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
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?
|