disrb 0.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.
- checksums.yaml +7 -0
- data/LICENSE +7 -0
- data/README.md +7 -0
- data/lib/disrb/guild.rb +743 -0
- data/lib/disrb/logger.rb +65 -0
- data/lib/disrb/message.rb +36 -0
- data/lib/disrb/user.rb +147 -0
- data/lib/disrb.rb +637 -0
- metadata +104 -0
data/lib/disrb.rb
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'async'
|
|
6
|
+
require 'async/http/endpoint'
|
|
7
|
+
require 'async/websocket/client'
|
|
8
|
+
require 'faraday'
|
|
9
|
+
require 'disrb/guild'
|
|
10
|
+
require 'disrb/logger'
|
|
11
|
+
require 'disrb/user'
|
|
12
|
+
require 'disrb/message'
|
|
13
|
+
|
|
14
|
+
# TODO: If there is more than 1 optional parameter in a function, I should change it from setting the default value via
|
|
15
|
+
# = to : so that the user can pass only the parameters they want to set
|
|
16
|
+
|
|
17
|
+
# DiscordApi
|
|
18
|
+
# The class that contains everything that interacts with the Discord API.
|
|
19
|
+
class DiscordApi
|
|
20
|
+
attr_accessor(:base_url, :authorization_header, :application_id, :interaction_created, :interaction, :logger)
|
|
21
|
+
|
|
22
|
+
def initialize(authorization_token_type, authorization_token, verbosity_level = nil)
|
|
23
|
+
@api_version = '10'
|
|
24
|
+
@base_url = "https://discord.com/api/v#{@api_version}"
|
|
25
|
+
@authorization_token_type = authorization_token_type
|
|
26
|
+
@authorization_token = authorization_token
|
|
27
|
+
@authorization_header = "#{authorization_token_type} #{authorization_token}"
|
|
28
|
+
url = URI("#{@base_url}/applications/@me")
|
|
29
|
+
headers = { 'Authorization': @authorization_header }
|
|
30
|
+
@application_id = JSON.parse(Net::HTTP.get(url, headers))['id']
|
|
31
|
+
@interaction_created = false
|
|
32
|
+
@interaction = {}
|
|
33
|
+
if verbosity_level.nil?
|
|
34
|
+
@verbosity_level = 3
|
|
35
|
+
elsif verbosity_level.is_a?(String)
|
|
36
|
+
case verbosity_level.downcase
|
|
37
|
+
when 'all'
|
|
38
|
+
@verbosity_level = 4
|
|
39
|
+
when 'info'
|
|
40
|
+
@verbosity_level = 3
|
|
41
|
+
when 'warning'
|
|
42
|
+
@verbosity_level = 2
|
|
43
|
+
when 'error'
|
|
44
|
+
@verbosity_level = 1
|
|
45
|
+
when 'none'
|
|
46
|
+
@verbosity_level = 0
|
|
47
|
+
else
|
|
48
|
+
Logger2.s_error("Unknown verbosity level: #{verbosity_level}. Defaulting to 'info'.")
|
|
49
|
+
@verbosity_level = 3
|
|
50
|
+
end
|
|
51
|
+
elsif verbosity_level.is_a?(Integer)
|
|
52
|
+
if verbosity_level >= 0 && verbosity_level <= 4
|
|
53
|
+
@verbosity_level = verbosity_level
|
|
54
|
+
else
|
|
55
|
+
Logger2.s_error("Unknown verbosity level: #{verbosity_level}. Defaulting to 'info'.")
|
|
56
|
+
@verbosity_level = 3
|
|
57
|
+
end
|
|
58
|
+
else
|
|
59
|
+
Logger2.s_error("Unknown verbosity level: #{verbosity_level}. Defaulting to 'info'.")
|
|
60
|
+
@verbosity_level = 3
|
|
61
|
+
end
|
|
62
|
+
@logger = Logger2.new(@verbosity_level)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.handle_query_strings(query_string_hash)
|
|
66
|
+
query_string_array = []
|
|
67
|
+
query_string_hash.each do |key, value|
|
|
68
|
+
if value.nil?
|
|
69
|
+
next
|
|
70
|
+
elsif query_string_array.empty?
|
|
71
|
+
query_string_array << "?#{key}=#{value}"
|
|
72
|
+
else
|
|
73
|
+
query_string_array << "&#{key}=#{value}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
query_string_array << '' if query_string_array.empty?
|
|
77
|
+
query_string_array.join
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.handle_snowflake(snowflake)
|
|
81
|
+
snowflake = snowflake.to_s(2).rjust(64, '0')
|
|
82
|
+
{
|
|
83
|
+
discord_epoch_timestamp: snowflake[0..41],
|
|
84
|
+
internal_worker_id: snowflake[42..46],
|
|
85
|
+
internal_process_id: snowflake[47..51],
|
|
86
|
+
gen_id_on_process: snowflake[52..64],
|
|
87
|
+
unix_timestamp: snowflake[0..41].to_i(2) + 1_420_070_400_000,
|
|
88
|
+
timestamp: Time.at((snowflake[0..41].to_i(2) + 1_420_070_400_000) / 1000).utc
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def create_guild_application_command(guild_id, name, name_localizations: nil, description: nil,
|
|
93
|
+
description_localizations: nil, options: nil, default_member_permissions: nil,
|
|
94
|
+
default_permission: true, type: 1, nsfw: false)
|
|
95
|
+
output = {}
|
|
96
|
+
output[:name] = name
|
|
97
|
+
output[:name_localizations] = name_localizations unless name_localizations.nil?
|
|
98
|
+
output[:description] = description unless description.nil?
|
|
99
|
+
output[:description_localizations] = description_localizations unless description_localizations.nil?
|
|
100
|
+
output[:options] = options unless options.nil?
|
|
101
|
+
output[:default_permission] = default_permission
|
|
102
|
+
output[:type] = type
|
|
103
|
+
output[:nsfw] = nsfw
|
|
104
|
+
output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
|
|
105
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands")
|
|
106
|
+
data = JSON.generate(output)
|
|
107
|
+
headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
|
|
108
|
+
Net::HTTP.post(url, data, headers)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def create_guild_application_commands(application_commands_array)
|
|
112
|
+
if application_commands_array.is_a?(Array)
|
|
113
|
+
application_commands_array.each do |parameter_array|
|
|
114
|
+
if parameter_array.is_a?(Array)
|
|
115
|
+
create_guild_application_command(*parameter_array)
|
|
116
|
+
else
|
|
117
|
+
@logger.error("Invalid parameter array: #{parameter_array}. Expected an array of parameters.")
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
else
|
|
121
|
+
@logger.error("Invalid application commands array: #{application_commands_array}. Expected an array of arrays.")
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def create_global_application_command(name, name_localizations: nil, description: nil,
|
|
126
|
+
description_localizations: nil, options: nil,
|
|
127
|
+
default_member_permissions: nil, default_permission: true,
|
|
128
|
+
integration_types: nil, contexts: nil, type: 1, nsfw: false)
|
|
129
|
+
output = {}
|
|
130
|
+
output[:name] = name
|
|
131
|
+
output[:name_localizations] = name_localizations unless name_localizations.nil?
|
|
132
|
+
output[:description] = description unless description.nil?
|
|
133
|
+
output[:description_localizations] = description_localizations unless description_localizations.nil?
|
|
134
|
+
output[:options] = options unless options.nil?
|
|
135
|
+
output[:default_permission] = default_permission
|
|
136
|
+
output[:type] = type
|
|
137
|
+
output[:nsfw] = nsfw
|
|
138
|
+
output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
|
|
139
|
+
output[:integration_types] = integration_types unless integration_types.nil?
|
|
140
|
+
output[:contexts] = contexts unless contexts.nil?
|
|
141
|
+
url = URI("#{@base_url}/applications/#{@application_id}/commands")
|
|
142
|
+
data = JSON.generate(output)
|
|
143
|
+
headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
|
|
144
|
+
Net::HTTP.post(url, data, headers)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def create_global_application_commands(application_commands_array)
|
|
148
|
+
if application_commands_array.is_a?(Array)
|
|
149
|
+
application_commands_array.each do |parameter_array|
|
|
150
|
+
if parameter_array.is_a?(Array)
|
|
151
|
+
create_global_application_command(*parameter_array)
|
|
152
|
+
else
|
|
153
|
+
@logger.error("Invalid parameter array: #{parameter_array}. Expected an array of parameters.")
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
@logger.error("Invalid application commands array: #{application_commands_array}. Expected an array of arrays.")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def edit_global_application_command(command_id, name = nil, name_localizations = nil, description = nil,
|
|
162
|
+
description_localizations = nil, options = nil, default_member_permissions = nil,
|
|
163
|
+
default_permission: true, integration_types: nil, contexts: nil, nsfw: nil)
|
|
164
|
+
output = {}
|
|
165
|
+
output[:name] = name
|
|
166
|
+
output[:name_localizations] = name_localizations unless name_localizations.nil?
|
|
167
|
+
output[:description] = description unless description.nil?
|
|
168
|
+
output[:description_localizations] = description_localizations unless description_localizations.nil?
|
|
169
|
+
output[:options] = options unless options.nil?
|
|
170
|
+
output[:default_permission] = default_permission
|
|
171
|
+
output[:nsfw] = nsfw
|
|
172
|
+
output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
|
|
173
|
+
output[:integration_types] = integration_types unless integration_types.nil?
|
|
174
|
+
output[:contexts] = contexts unless contexts.nil?
|
|
175
|
+
url = URI("#{@base_url}/applications/#{@application_id}/commands/#{command_id}")
|
|
176
|
+
data = JSON.generate(output)
|
|
177
|
+
headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
|
|
178
|
+
Net::HTTP.patch(url, data, headers)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def edit_guild_application_command(guild_id, command_id, name = nil, name_localizations = nil, description = nil,
|
|
182
|
+
description_localizations = nil, options = nil, default_member_permissions = nil,
|
|
183
|
+
default_permission: true, nsfw: nil)
|
|
184
|
+
output = {}
|
|
185
|
+
output[:name] = name
|
|
186
|
+
output[:name_localizations] = name_localizations unless name_localizations.nil?
|
|
187
|
+
output[:description] = description unless description.nil?
|
|
188
|
+
output[:description_localizations] = description_localizations unless description_localizations.nil?
|
|
189
|
+
output[:options] = options unless options.nil?
|
|
190
|
+
output[:default_permission] = default_permission
|
|
191
|
+
output[:nsfw] = nsfw
|
|
192
|
+
output[:default_member_permissions] = default_member_permissions unless default_member_permissions.nil?
|
|
193
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}")
|
|
194
|
+
data = JSON.generate(output)
|
|
195
|
+
headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
|
|
196
|
+
Net::HTTP.patch(url, data, headers)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def delete_global_application_command(command_id)
|
|
200
|
+
url = URI("#{@base_url}/applications/#{@application_id}/commands/#{command_id}")
|
|
201
|
+
headers = { 'Authorization': @authorization_header }
|
|
202
|
+
Net::HTTP.delete(url, headers)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def delete_guild_application_command(guild_id, command_id)
|
|
206
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}")
|
|
207
|
+
headers = { 'Authorization': @authorization_header }
|
|
208
|
+
Net::HTTP.delete(url, headers)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def get_guild_application_commands(guild_id, with_localizations: false)
|
|
212
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands?with_localizations=" \
|
|
213
|
+
"#{with_localizations}")
|
|
214
|
+
headers = { 'Authorization': @authorization_header }
|
|
215
|
+
Net::HTTP.get(url, headers)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def get_global_application_commands(with_localizations: false)
|
|
219
|
+
url = URI("#{@base_url}/applications/#{@application_id}/commands?with_localizations=#{with_localizations}")
|
|
220
|
+
headers = { 'Authorization': @authorization_header }
|
|
221
|
+
Net::HTTP.get(url, headers)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def get_global_application_command(command_id)
|
|
225
|
+
url = URI("#{@base_url}/applications/#{@application_id}/commands/#{command_id}")
|
|
226
|
+
headers = { 'Authorization': @authorization_header }
|
|
227
|
+
Net::HTTP.get(url, headers)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def get_guild_application_command(guild_id, command_id)
|
|
231
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}")
|
|
232
|
+
headers = { 'Authorization': @authorization_header }
|
|
233
|
+
Net::HTTP.get(url, headers)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def bulk_overwrite_global_application_commands(commands)
|
|
237
|
+
url = URI("#{@base_url}/applications/#{@application_id}/commands")
|
|
238
|
+
data = JSON.generate(commands)
|
|
239
|
+
headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
|
|
240
|
+
Net::HTTP.put(url, data, headers)
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def bulk_overwrite_guild_application_commands(guild_id, commands)
|
|
244
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands")
|
|
245
|
+
data = JSON.generate(commands)
|
|
246
|
+
headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
|
|
247
|
+
Net::HTTP.put(url, data, headers)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def get_guild_application_command_permissions(guild_id)
|
|
251
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/permissions")
|
|
252
|
+
headers = { 'Authorization': @authorization_header }
|
|
253
|
+
Net::HTTP.get(url, headers)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def get_application_command_permissions(guild_id, command_id)
|
|
257
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}/permissions")
|
|
258
|
+
headers = { 'Authorization': @authorization_header }
|
|
259
|
+
Net::HTTP.get(url, headers)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def edit_application_command_permissions(guild_id, command_id, permissions)
|
|
263
|
+
url = URI("#{@base_url}/applications/#{@application_id}/guilds/#{guild_id}/commands/#{command_id}/permissions")
|
|
264
|
+
data = JSON.generate(permissions)
|
|
265
|
+
headers = { 'Authorization': @authorization_header, 'Content-Type': 'application/json' }
|
|
266
|
+
Net::HTTP.put(url, data, headers)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def connect_gateway(activities: nil, os: nil, browser: nil, device: nil, intents: nil, presence_since: nil,
|
|
270
|
+
presence_status: nil, presence_afk: nil, &block)
|
|
271
|
+
if @authorization_token_type == 'Bearer'
|
|
272
|
+
acceptable_activities_keys = %w[name type url created_at timestamps application_id details state emoji party
|
|
273
|
+
assets secrets instance flags buttons]
|
|
274
|
+
elsif @authorization_token_type == 'Bot'
|
|
275
|
+
acceptable_activities_keys = %w[name state type url]
|
|
276
|
+
end
|
|
277
|
+
if activities.is_a?(Hash)
|
|
278
|
+
activities.each_key do |key|
|
|
279
|
+
next if acceptable_activities_keys.include?(key.to_s)
|
|
280
|
+
|
|
281
|
+
@logger.error("Unknown activity key: #{key}. Deleting key from hash.")
|
|
282
|
+
activities.delete(key)
|
|
283
|
+
end
|
|
284
|
+
if activities.empty?
|
|
285
|
+
@logger.error('Empty activity hash. No activities will be sent.')
|
|
286
|
+
activities = nil
|
|
287
|
+
else
|
|
288
|
+
activities = [activities]
|
|
289
|
+
end
|
|
290
|
+
elsif activities.is_a?(Array)
|
|
291
|
+
activities.each do |activity|
|
|
292
|
+
if activity.is_a?(Hash)
|
|
293
|
+
activity.each_key do |key|
|
|
294
|
+
next if acceptable_activities_keys.include?(key.to_s)
|
|
295
|
+
|
|
296
|
+
@logger.error("Unknown activity key: #{key}. Deleting key from Hash.")
|
|
297
|
+
activity.delete(key)
|
|
298
|
+
end
|
|
299
|
+
if activity.empty?
|
|
300
|
+
@logger.error('Empty activity hash. Deleting from Array.')
|
|
301
|
+
activities.delete(activity)
|
|
302
|
+
end
|
|
303
|
+
else
|
|
304
|
+
@logger.error("Invalid activity: #{activity}. Expected a Hash. Deleting from Array.")
|
|
305
|
+
activities.delete(activity)
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
if activities.empty?
|
|
309
|
+
@logger.error('Empty activities Array. No activities will be sent.')
|
|
310
|
+
activities = nil
|
|
311
|
+
end
|
|
312
|
+
elsif !activities.nil?
|
|
313
|
+
@logger.error("Invalid activities: #{activities}. Expected a Hash or an Array of Hashes.")
|
|
314
|
+
activities = nil
|
|
315
|
+
end
|
|
316
|
+
unless os.is_a?(String) || os.nil?
|
|
317
|
+
@logger.error("Invalid OS: #{os}. Expected a String. Defaulting to #{RbConfig::CONFIG['host_os']}.")
|
|
318
|
+
os = nil
|
|
319
|
+
end
|
|
320
|
+
unless browser.is_a?(String) || browser.nil?
|
|
321
|
+
@logger.error("Invalid browser: #{browser}. Expected a String. Defaulting to 'discord.rb'.")
|
|
322
|
+
browser = nil
|
|
323
|
+
end
|
|
324
|
+
unless device.is_a?(String) || device.nil?
|
|
325
|
+
@logger.error("Invalid device: #{device}. Expected a String. Defaulting to 'discord.rb'.")
|
|
326
|
+
device = nil
|
|
327
|
+
end
|
|
328
|
+
unless (intents.is_a?(Integer) && intents >= 0 && intents <= 131_071) || intents.nil?
|
|
329
|
+
@logger.error("Invalid intents: #{intents}. Expected an Integer between 0 and 131.071. Defaulting to 513" \
|
|
330
|
+
' (GUILD_MESSAGES, GUILDS).')
|
|
331
|
+
intents = nil
|
|
332
|
+
end
|
|
333
|
+
unless presence_since.is_a?(Integer) || presence_since == true || presence_since.nil?
|
|
334
|
+
@logger.error("Invalid presence since: #{presence_since}. Expected an Integer or true. Defaulting to nil.")
|
|
335
|
+
presence_since = nil
|
|
336
|
+
end
|
|
337
|
+
unless presence_status.is_a?(String) || presence_status.nil?
|
|
338
|
+
@logger.error("Invalid presence status: #{presence_status}. Expected a String. Defaulting to nil.")
|
|
339
|
+
presence_status = nil
|
|
340
|
+
end
|
|
341
|
+
unless [true, false].include?(presence_afk) || presence_afk.nil?
|
|
342
|
+
@logger.error("Invalid presence afk: #{presence_afk}. Expected a Boolean. Defaulting to nil.")
|
|
343
|
+
presence_afk = nil
|
|
344
|
+
end
|
|
345
|
+
Async do |_task|
|
|
346
|
+
rescue_connection, sequence, resume_gateway_url, session_id = nil
|
|
347
|
+
loop do
|
|
348
|
+
url = if rescue_connection.nil?
|
|
349
|
+
"#{JSON.parse(Net::HTTP.get(URI("#{@base_url}/gateway")))['url']}/?v=#{@api_version}&encoding=json"
|
|
350
|
+
else
|
|
351
|
+
"#{rescue_connection[:resume_gateway_url]}/?v=#{@api_version}&encoding=json"
|
|
352
|
+
end
|
|
353
|
+
endpoint = Async::HTTP::Endpoint.parse(url, alpn_protocols: Async::HTTP::Protocol::HTTP11.names)
|
|
354
|
+
Async::WebSocket::Client.connect(endpoint) do |connection|
|
|
355
|
+
connection.write JSON.generate({ op: 1, d: nil })
|
|
356
|
+
connection.flush
|
|
357
|
+
|
|
358
|
+
if rescue_connection.nil?
|
|
359
|
+
identify = {}
|
|
360
|
+
identify[:op] = 2
|
|
361
|
+
identify[:d] = {}
|
|
362
|
+
identify[:d][:token] = @authorization_header
|
|
363
|
+
identify[:d][:intents] = intents || 513
|
|
364
|
+
identify[:d][:properties] = {}
|
|
365
|
+
identify[:d][:properties][:os] = os || RbConfig::CONFIG['host_os']
|
|
366
|
+
identify[:d][:properties][:browser] = browser || 'discord.rb'
|
|
367
|
+
identify[:d][:properties][:device] = device || 'discord.rb'
|
|
368
|
+
if !activities.nil? || !presence_since.nil? || !presence_status.nil? || !presence_afk.nil?
|
|
369
|
+
identify[:d][:presence] = {}
|
|
370
|
+
identify[:d][:presence][:activities] = activities unless activities.nil?
|
|
371
|
+
if presence_since == true
|
|
372
|
+
identify[:d][:presence][:since] = (Time.now.to_f * 1000).floor
|
|
373
|
+
elsif presence_since.is_a?(Integer)
|
|
374
|
+
identify[:d][:presence][:since] = presence_since
|
|
375
|
+
end
|
|
376
|
+
identify[:d][:presence][:status] = presence_status unless presence_status.nil?
|
|
377
|
+
identify[:d][:presence][:afk] = presence_afk unless presence_afk.nil?
|
|
378
|
+
end
|
|
379
|
+
@logger.debug("Identify payload: #{JSON.generate(identify)}")
|
|
380
|
+
connection.write(JSON.generate(identify))
|
|
381
|
+
else
|
|
382
|
+
@logger.info('Resuming connection...')
|
|
383
|
+
resume = {}
|
|
384
|
+
resume[:op] = 6
|
|
385
|
+
resume[:d] = {}
|
|
386
|
+
resume[:d][:token] = @authorization_header
|
|
387
|
+
resume[:d][:session_id] = rescue_connection[:session_id]
|
|
388
|
+
resume[:d][:seq] = rescue_connection[:sequence]
|
|
389
|
+
@logger.debug("Resume payload: #{JSON.generate(resume)}")
|
|
390
|
+
connection.write(JSON.generate(resume))
|
|
391
|
+
rescue_connection, sequence, resume_gateway_url, session_id = nil
|
|
392
|
+
end
|
|
393
|
+
connection.flush
|
|
394
|
+
|
|
395
|
+
loop do
|
|
396
|
+
message = connection.read
|
|
397
|
+
message = JSON.parse(message, symbolize_names: true)
|
|
398
|
+
@logger.debug(message)
|
|
399
|
+
|
|
400
|
+
case message
|
|
401
|
+
in { op: 10 }
|
|
402
|
+
@logger.info('Received Hello')
|
|
403
|
+
@heartbeat_interval = message[:d][:heartbeat_interval]
|
|
404
|
+
in { op: 1 }
|
|
405
|
+
@logger.info('Received Heartbeat Request')
|
|
406
|
+
connection.write JSON.generate({ op: 1, d: nil })
|
|
407
|
+
connection.flush
|
|
408
|
+
in { op: 11 }
|
|
409
|
+
@logger.info('Received Heartbeat ACK')
|
|
410
|
+
in { op: 0, t: 'INTERACTION_CREATE' }
|
|
411
|
+
@logger.info('An interaction was created')
|
|
412
|
+
sequence = message[:s]
|
|
413
|
+
block.call(message)
|
|
414
|
+
in { op: 0, t: 'READY' }
|
|
415
|
+
@logger.info('Recieved Ready')
|
|
416
|
+
session_id = message[:d][:session_id]
|
|
417
|
+
resume_gateway_url = message[:d][:resume_gateway_url]
|
|
418
|
+
sequence = message[:s]
|
|
419
|
+
in { op: 0 }
|
|
420
|
+
@logger.info('An event was dispatched')
|
|
421
|
+
sequence = message[:s]
|
|
422
|
+
in { op: 7 }
|
|
423
|
+
rescue_connection = { type: 'reconnect', session_id: session_id, resume_gateway_url: resume_gateway_url,
|
|
424
|
+
sequence: sequence }
|
|
425
|
+
@logger.warn('Received Reconnect. A rescue will be attempted....')
|
|
426
|
+
else
|
|
427
|
+
@logger.error('Unimplemented event type')
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
rescue Protocol::WebSocket::ClosedError
|
|
432
|
+
@logger.warn('WebSocket connection closed. Attempting rescue.')
|
|
433
|
+
if rescue_connection && rescue_connection[:type] == 'reconnect'
|
|
434
|
+
@logger.info('Rescue possible. Starting...')
|
|
435
|
+
next
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def respond_interaction(interaction, response, with_response: false)
|
|
442
|
+
query_string_hash = {}
|
|
443
|
+
query_string_hash[:with_response] = with_response
|
|
444
|
+
query_string = DiscordApi.handle_query_strings(query_string_hash)
|
|
445
|
+
url = "#{@base_url}/interactions/#{interaction[:d][:id]}/#{interaction[:d][:token]}/callback#{query_string}"
|
|
446
|
+
data = JSON.generate(response)
|
|
447
|
+
headers = { 'content-type': 'application/json' }
|
|
448
|
+
response = DiscordApi.post(url, data, headers)
|
|
449
|
+
return response if (response.status == 204 && !with_response) || (response.status == 200 && with_response)
|
|
450
|
+
|
|
451
|
+
@logger.error("Failed to respond to interaction. Response: #{response.body}")
|
|
452
|
+
response
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def self.calculate_permissions_integer(permissions)
|
|
456
|
+
bitwise_permission_flags = {
|
|
457
|
+
create_instant_invite: 0x0000000000000001,
|
|
458
|
+
kick_members: 0x0000000000000002,
|
|
459
|
+
ban_members: 0x0000000000000004,
|
|
460
|
+
administrator: 0x0000000000000008,
|
|
461
|
+
manage_channels: 0x0000000000000010,
|
|
462
|
+
manage_guild: 0x0000000000000020,
|
|
463
|
+
add_reactions: 0x0000000000000040,
|
|
464
|
+
view_audit_log: 0x0000000000000080,
|
|
465
|
+
priority_speaker: 0x0000000000000100,
|
|
466
|
+
stream: 0x0000000000000200,
|
|
467
|
+
view_channel: 0x0000000000000400,
|
|
468
|
+
send_messages: 0x0000000000000800,
|
|
469
|
+
send_tts_messages: 0x0000000000001000,
|
|
470
|
+
manage_messages: 0x0000000000002000,
|
|
471
|
+
embed_links: 0x0000000000004000,
|
|
472
|
+
attach_files: 0x0000000000008000,
|
|
473
|
+
read_message_history: 0x0000000000010000,
|
|
474
|
+
mention_everyone: 0x0000000000020000,
|
|
475
|
+
use_external_emojis: 0x0000000000040000,
|
|
476
|
+
view_guild_insights: 0x0000000000080000,
|
|
477
|
+
connect: 0x0000000000100000,
|
|
478
|
+
speak: 0x0000000000200000,
|
|
479
|
+
mute_members: 0x0000000000400000,
|
|
480
|
+
deafen_members: 0x0000000000800000,
|
|
481
|
+
move_members: 0x0000000001000000,
|
|
482
|
+
use_vad: 0x0000000002000000,
|
|
483
|
+
change_nickname: 0x0000000004000000,
|
|
484
|
+
manage_nicknames: 0x0000000008000000,
|
|
485
|
+
manage_roles: 0x0000000010000000,
|
|
486
|
+
manage_webhooks: 0x0000000020000000,
|
|
487
|
+
manage_guild_expressions: 0x0000000040000000,
|
|
488
|
+
use_application_commands: 0x0000000080000000,
|
|
489
|
+
request_to_speak: 0x0000000100000000,
|
|
490
|
+
manage_events: 0x0000000200000000,
|
|
491
|
+
manage_threads: 0x0000000400000000,
|
|
492
|
+
create_public_threads: 0x0000000800000000,
|
|
493
|
+
create_private_threads: 0x0000001000000000,
|
|
494
|
+
use_external_stickers: 0x0000002000000000,
|
|
495
|
+
send_messages_in_threads: 0x0000004000000000,
|
|
496
|
+
use_embedded_activities: 0x0000008000000000,
|
|
497
|
+
moderate_members: 0x0000010000000000,
|
|
498
|
+
view_creator_monetization_analytics: 0x0000020000000000,
|
|
499
|
+
use_soundboard: 0x0000040000000000,
|
|
500
|
+
create_guild_expressions: 0x0000080000000000,
|
|
501
|
+
create_events: 0x0000100000000000,
|
|
502
|
+
use_external_sounds: 0x0000200000000000,
|
|
503
|
+
send_voice_messages: 0x0000400000000000,
|
|
504
|
+
send_polls: 0x0002000000000000,
|
|
505
|
+
use_external_apps: 0x0004000000000000
|
|
506
|
+
}
|
|
507
|
+
permissions = permissions.map do |permission|
|
|
508
|
+
bitwise_permission_flags[permission.downcase.to_sym]
|
|
509
|
+
end
|
|
510
|
+
permissions.reduce(0) { |acc, n| acc | n }
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
def self.reverse_permissions_integer(permissions_integer)
|
|
514
|
+
bitwise_permission_flags = {
|
|
515
|
+
create_instant_invite: 0x0000000000000001,
|
|
516
|
+
kick_members: 0x0000000000000002,
|
|
517
|
+
ban_members: 0x0000000000000004,
|
|
518
|
+
administrator: 0x0000000000000008,
|
|
519
|
+
manage_channels: 0x0000000000000010,
|
|
520
|
+
manage_guild: 0x0000000000000020,
|
|
521
|
+
add_reactions: 0x0000000000000040,
|
|
522
|
+
view_audit_log: 0x0000000000000080,
|
|
523
|
+
priority_speaker: 0x0000000000000100,
|
|
524
|
+
stream: 0x0000000000000200,
|
|
525
|
+
view_channel: 0x0000000000000400,
|
|
526
|
+
send_messages: 0x0000000000000800,
|
|
527
|
+
send_tts_messages: 0x0000000000001000,
|
|
528
|
+
manage_messages: 0x0000000000002000,
|
|
529
|
+
embed_links: 0x0000000000004000,
|
|
530
|
+
attach_files: 0x0000000000008000,
|
|
531
|
+
read_message_history: 0x0000000000010000,
|
|
532
|
+
mention_everyone: 0x0000000000020000,
|
|
533
|
+
use_external_emojis: 0x0000000000040000,
|
|
534
|
+
view_guild_insights: 0x0000000000080000,
|
|
535
|
+
connect: 0x0000000000100000,
|
|
536
|
+
speak: 0x0000000000200000,
|
|
537
|
+
mute_members: 0x0000000000400000,
|
|
538
|
+
deafen_members: 0x0000000000800000,
|
|
539
|
+
move_members: 0x0000000001000000,
|
|
540
|
+
use_vad: 0x0000000002000000,
|
|
541
|
+
change_nickname: 0x0000000004000000,
|
|
542
|
+
manage_nicknames: 0x0000000008000000,
|
|
543
|
+
manage_roles: 0x0000000010000000,
|
|
544
|
+
manage_webhooks: 0x0000000020000000,
|
|
545
|
+
manage_guild_expressions: 0x0000000040000000,
|
|
546
|
+
use_application_commands: 0x0000000080000000,
|
|
547
|
+
request_to_speak: 0x0000000100000000,
|
|
548
|
+
manage_events: 0x0000000200000000,
|
|
549
|
+
manage_threads: 0x0000000400000000,
|
|
550
|
+
create_public_threads: 0x0000000800000000,
|
|
551
|
+
create_private_threads: 0x0000001000000000,
|
|
552
|
+
use_external_stickers: 0x0000002000000000,
|
|
553
|
+
send_messages_in_threads: 0x0000004000000000,
|
|
554
|
+
use_embedded_activities: 0x0000008000000000,
|
|
555
|
+
moderate_members: 0x0000010000000000,
|
|
556
|
+
view_creator_monetization_analytics: 0x0000020000000000,
|
|
557
|
+
use_soundboard: 0x0000040000000000,
|
|
558
|
+
create_guild_expressions: 0x0000080000000000,
|
|
559
|
+
create_events: 0x0000100000000000,
|
|
560
|
+
use_external_sounds: 0x0000200000000000,
|
|
561
|
+
send_voice_messages: 0x0000400000000000,
|
|
562
|
+
send_polls: 0x0002000000000000,
|
|
563
|
+
use_external_apps: 0x0004000000000000
|
|
564
|
+
}
|
|
565
|
+
permissions = []
|
|
566
|
+
bitwise_permission_flags.each do |permission, value|
|
|
567
|
+
permissions << permission if (permissions_integer & value) != 0
|
|
568
|
+
end
|
|
569
|
+
permissions
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def self.calculate_gateway_intents(intents)
|
|
573
|
+
bitwise_intent_flags = {
|
|
574
|
+
guilds: 1 << 0,
|
|
575
|
+
guild_members: 1 << 1,
|
|
576
|
+
guild_bans: 1 << 2,
|
|
577
|
+
guild_emojis_and_stickers: 1 << 3,
|
|
578
|
+
guild_integrations: 1 << 4,
|
|
579
|
+
guild_webhooks: 1 << 5,
|
|
580
|
+
guild_invites: 1 << 6,
|
|
581
|
+
guild_voice_states: 1 << 7,
|
|
582
|
+
guild_presences: 1 << 8,
|
|
583
|
+
guild_messages: 1 << 9,
|
|
584
|
+
guild_message_reactions: 1 << 10,
|
|
585
|
+
guild_message_typing: 1 << 11,
|
|
586
|
+
direct_messages: 1 << 12,
|
|
587
|
+
direct_message_reactions: 1 << 13,
|
|
588
|
+
direct_message_typing: 1 << 14,
|
|
589
|
+
message_content: 1 << 15,
|
|
590
|
+
guild_scheduled_events: 1 << 16
|
|
591
|
+
}
|
|
592
|
+
intents = intents.map do |intent|
|
|
593
|
+
bitwise_intent_flags[intent.downcase.to_sym]
|
|
594
|
+
end
|
|
595
|
+
intents.reduce(0) { |acc, n| acc | n }
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# TODO: Transition from Net::HTTP to below wrapper methods
|
|
599
|
+
def self.get(url, headers = nil)
|
|
600
|
+
split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
|
|
601
|
+
@logger.error("Empty/invalid URL provided: #{url}. Cannot perform GET request.") if split_url.empty?
|
|
602
|
+
host = split_url[0]
|
|
603
|
+
path = split_url[1] if split_url[1]
|
|
604
|
+
conn = Faraday.new(url: host, headers: headers)
|
|
605
|
+
if path
|
|
606
|
+
conn.get(path)
|
|
607
|
+
else
|
|
608
|
+
conn.get
|
|
609
|
+
end
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def self.delete(url, headers = nil)
|
|
613
|
+
split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
|
|
614
|
+
@logger.error("Empty/invalid URL provided: #{url}. Cannot perform DELETE request.") if split_url.empty?
|
|
615
|
+
host = split_url[0]
|
|
616
|
+
path = split_url[1] if split_url[1]
|
|
617
|
+
conn = Faraday.new(url: host, headers: headers)
|
|
618
|
+
if path
|
|
619
|
+
conn.delete(path)
|
|
620
|
+
else
|
|
621
|
+
conn.delete
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def self.post(url, data, headers = nil)
|
|
626
|
+
split_url = url.split(%r{(http[^/]+)(/.*)}).reject(&:empty?)
|
|
627
|
+
@logger.error("Empty/invalid URL provided: #{url}. Cannot perform POST request.") if split_url.empty?
|
|
628
|
+
host = split_url[0]
|
|
629
|
+
path = split_url[1] if split_url[1]
|
|
630
|
+
conn = Faraday.new(url: host, headers: headers)
|
|
631
|
+
if path
|
|
632
|
+
conn.post(path, data)
|
|
633
|
+
else
|
|
634
|
+
conn.post('', data)
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
end
|