revoltrb 0.0.1 → 0.0.2
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/README.md +5 -5
- data/lib/revoltrb/bot.rb +537 -487
- data/lib/revoltrb/version.rb +1 -1
- metadata +9 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a423864396d60b962eebc129b294b03f79e812d9b95eb2511f7c4bb2f07cefc
|
|
4
|
+
data.tar.gz: 142aba7edfce9961c36ff0ab2eaa70eced3e1a55ec64e2f2ae32db345fc30347
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1c9fb2983e8cba19aa607218619f5b1c7fc8bc712bba8a3030fb6e460060d60ed89a0493c35329668148542156f0591a5d2bbdf6ed0e1d1f5cd799b547293972
|
|
7
|
+
data.tar.gz: 8bf120f58d994b0a9617844acbf0b86e0491b85965a228e50e6430ba5402c80ef1c0f22585e9d22558711f4b92255942ca0bca89db9411da924d7523cb4a43f7
|
data/README.md
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
# revoltrb
|
|
2
2
|
|
|
3
|
-
Revoltrb is a Ruby package (a.k.a. Gem) that allows you to make Revolt.chat bots using the Ruby programming language.
|
|
4
|
-
|
|
5
|
-
This package (a.k.a. Gem) is not officially endorsed by Revolt.chat amd this is not an official Revolt.chat product.
|
|
3
|
+
Revoltrb is a Ruby package (a.k.a. Gem) that allows you to make Revolt.chat bots using the Ruby programming language. This package (a.k.a. Gem) is not officially endorsed by Revolt.chat amd this is not an official Revolt.chat product.
|
|
6
4
|
|
|
7
5
|
You need Ruby 3.0 or newer in order to use this package (Ruby 3.2 or newer is recommended)
|
|
8
6
|
|
|
7
|
+
> [!NOTE]
|
|
8
|
+
> This package (a.k.a. Gem) is in a early alpha state so expect things to be buggy and/or broken.
|
|
9
|
+
|
|
9
10
|
## ToDo
|
|
10
11
|
|
|
11
12
|
This list contains a list of things that I know is broken and gotta fix. Contributing will be super helpful.
|
|
12
13
|
|
|
13
|
-
- Fix reactions support
|
|
14
14
|
- Fix obtaining server information
|
|
15
|
-
- Fix
|
|
15
|
+
- Fix reactions support
|
|
16
16
|
|
|
17
17
|
## Setup
|
|
18
18
|
|
data/lib/revoltrb/bot.rb
CHANGED
|
@@ -1,488 +1,538 @@
|
|
|
1
|
-
# lib/revoltrb/bot.rb
|
|
2
|
-
|
|
3
|
-
require 'json'
|
|
4
|
-
require 'net/http'
|
|
5
|
-
require 'websocket-client-simple' # This is required for WebSocket communication
|
|
6
|
-
require 'thread'
|
|
7
|
-
require 'time'
|
|
8
|
-
require_relative 'debuglogger'
|
|
9
|
-
require_relative 'request_queue'
|
|
10
|
-
|
|
11
|
-
module Revoltrb
|
|
12
|
-
class RevoltBot
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@
|
|
33
|
-
@
|
|
34
|
-
@
|
|
35
|
-
@
|
|
36
|
-
|
|
37
|
-
@
|
|
38
|
-
@
|
|
39
|
-
@
|
|
40
|
-
@
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
@logger.debug "
|
|
88
|
-
@
|
|
89
|
-
@
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
'
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
@
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
@logger.debug "
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
end
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
@
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
@logger.debug "
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
@
|
|
475
|
-
@logger.debug
|
|
476
|
-
end
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
1
|
+
# lib/revoltrb/bot.rb
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'websocket-client-simple' # This is required for WebSocket communication
|
|
6
|
+
require 'thread'
|
|
7
|
+
require 'time'
|
|
8
|
+
require_relative 'debuglogger'
|
|
9
|
+
require_relative 'request_queue'
|
|
10
|
+
|
|
11
|
+
module Revoltrb
|
|
12
|
+
class RevoltBot
|
|
13
|
+
EMOJI_MAP = {
|
|
14
|
+
':grinning:' => '😀',
|
|
15
|
+
':heart:' => '❤️',
|
|
16
|
+
':joy:' => '😂',
|
|
17
|
+
':unamused:' => '😒',
|
|
18
|
+
':sunglasses:' => '😎',
|
|
19
|
+
':thinking:' => '🤔',
|
|
20
|
+
':clap:' => '👏',
|
|
21
|
+
':thumbsup:' => '👍',
|
|
22
|
+
':thumbsdown:' => '👎',
|
|
23
|
+
':point_up:' => '☝️',
|
|
24
|
+
':+1:' => '👍',
|
|
25
|
+
':-1:' => '👎'
|
|
26
|
+
}.freeze
|
|
27
|
+
|
|
28
|
+
attr_reader :token, :user_id, :bot_name, :servers, :prefix, :bot_owner_id, :bot_discriminator, :bot_discoverable, :bot_creation_date
|
|
29
|
+
attr_accessor :websocket_url, :api_url, :cdn_url
|
|
30
|
+
# Initializes the bot with the provided token, API endpoints, and configuration.
|
|
31
|
+
def initialize(token, api_url: 'https://api.revolt.chat', websocket_url: 'wss://app.revolt.chat/events', cdn_url: 'https://cdn.revoltusercontent.com', prefix: nil, debuglogs: false, selfbot: false)
|
|
32
|
+
@token = token
|
|
33
|
+
@api_url = api_url
|
|
34
|
+
@websocket_url = websocket_url
|
|
35
|
+
@cdn_url = cdn_url
|
|
36
|
+
|
|
37
|
+
@user_id = nil
|
|
38
|
+
@bot_name = nil
|
|
39
|
+
@servers = {}
|
|
40
|
+
@commands = {}
|
|
41
|
+
@message_handlers = []
|
|
42
|
+
|
|
43
|
+
@websocket = nil
|
|
44
|
+
@websocket_thread = nil
|
|
45
|
+
@heartbeat_interval = 30 # Default heartbeat interval in seconds
|
|
46
|
+
@last_heartbeat_sent = Time.now.to_i
|
|
47
|
+
@running = false
|
|
48
|
+
@ready_event_received = false
|
|
49
|
+
@logger = Revoltrb::DebugLogger.new(debuglogs)
|
|
50
|
+
@request_queue = Revoltrb::RequestQueue.new(500)
|
|
51
|
+
|
|
52
|
+
@prefix = "!"
|
|
53
|
+
@prefix = prefix if prefix
|
|
54
|
+
@selfbot = selfbot
|
|
55
|
+
@logger.debug "RevoltBot initialized. API: #{@api_url}, WS: #{@websocket_url}, CDN: #{@cdn_url}, Prefix: #{@prefix}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def get_botinfo
|
|
59
|
+
{
|
|
60
|
+
'bot_id' => @user_id,
|
|
61
|
+
'bot_name' => @bot_name,
|
|
62
|
+
'bot_discriminator' => @bot_discriminator,
|
|
63
|
+
'bot_ownerid' => @bot_owner_id,
|
|
64
|
+
'bot_creationdate' => @bot_creation_date,
|
|
65
|
+
'bot_flags' => @bot_flags,
|
|
66
|
+
'bot_discoverable' => @bot_discoverable,
|
|
67
|
+
'bot_public' => @bot_public,
|
|
68
|
+
'bot_analytics' => @bot_analytics,
|
|
69
|
+
'bot_prefix' => @prefix,
|
|
70
|
+
'bot_token' => @token
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Log into the Revolt.chat bot
|
|
75
|
+
def login
|
|
76
|
+
@logger.debug "BOT: Bot attempting to start...."
|
|
77
|
+
# Step 1: Fetch initial bot user details via REST API using /users/@me with X-Bot-Token | As confirmed, /users/@me returns the bot's user object directly.
|
|
78
|
+
uri = URI("#{@api_url}/users/@me")
|
|
79
|
+
req = Net::HTTP::Get.new(uri)
|
|
80
|
+
_add_auth_header(req)
|
|
81
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
82
|
+
http.request(req)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
86
|
+
bot_user_data = JSON.parse(res.body)
|
|
87
|
+
@logger.debug "Response Body (successful login attempt from /users/@me): #{res.body}"
|
|
88
|
+
@user_id = bot_user_data&.[]('_id')
|
|
89
|
+
@bot_name = bot_user_data&.[]('username')
|
|
90
|
+
@bot_discriminator = bot_user_data&.[]('discriminator')
|
|
91
|
+
bot_specific_info = bot_user_data&.[]('bot')
|
|
92
|
+
@bot_owner_id = bot_specific_info&.[]('owner')
|
|
93
|
+
@bot_flags = bot_specific_info&.[]('flags')
|
|
94
|
+
@bot_discoverable = nil
|
|
95
|
+
@bot_public = nil
|
|
96
|
+
@bot_analytics = nil
|
|
97
|
+
@bot_creation_date = Time.at(bot_user_data&.[]('created_at').to_i / 1000) rescue nil
|
|
98
|
+
if @user_id.nil? || @bot_name.nil?
|
|
99
|
+
@logger.debug "Error: Essential properties (_id, username) missing or nil in API response from /users/@me."
|
|
100
|
+
@logger.debug "Please inspect the 'Response Body' above for unexpected format."
|
|
101
|
+
return false
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
@logger.debug "Successfully identified as #{@bot_name} (ID: #{@user_id}) via REST API."
|
|
105
|
+
@logger.debug "Bot owner ID after parsing: #{@bot_owner_id}"
|
|
106
|
+
@logger.debug "Owner ID: #{@bot_owner_id}, Discoverable: #{@bot_discoverable.inspect}, Created: #{@bot_creation_date}"
|
|
107
|
+
else
|
|
108
|
+
@logger.debug "Initial REST API call failed: #{res.message} (Code: #{res.code})"
|
|
109
|
+
@logger.debug "Response Body: #{res.body}"
|
|
110
|
+
@logger.debug "Please check your bot token. Cannot proceed with WebSocket connection."
|
|
111
|
+
return false
|
|
112
|
+
end
|
|
113
|
+
# Step 2: Connect to the WebSocket
|
|
114
|
+
@running = true
|
|
115
|
+
connect_websocket
|
|
116
|
+
@request_queue.start_processing
|
|
117
|
+
true
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def connect_websocket
|
|
121
|
+
if @websocket && @websocket.open? && @running
|
|
122
|
+
@logger.debug "WebSocket already open and running."
|
|
123
|
+
return
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
@logger.debug "Connecting to WebSocket: #{@websocket_url}"
|
|
127
|
+
ws_url_with_token = "#{@websocket_url}?token=#{@token}"
|
|
128
|
+
bot_instance = self
|
|
129
|
+
thread_logger = @logger
|
|
130
|
+
|
|
131
|
+
@websocket_thread = Thread.new do
|
|
132
|
+
begin
|
|
133
|
+
@websocket = WebSocket::Client::Simple.connect ws_url_with_token
|
|
134
|
+
@websocket.on :open do
|
|
135
|
+
thread_logger.debug "WebSocket connection opened!"
|
|
136
|
+
end
|
|
137
|
+
@websocket.on :message do |msg|
|
|
138
|
+
bot_instance.handle_websocket_message(msg.data)
|
|
139
|
+
end
|
|
140
|
+
@websocket.on :close do |e|
|
|
141
|
+
close_code = e&.code || 'N/A'
|
|
142
|
+
close_reason = e&.reason || 'No reason provided'
|
|
143
|
+
thread_logger.debug "WebSocket closed: #{close_code} - #{close_reason}."
|
|
144
|
+
bot_instance.instance_variable_set(:@websocket, nil)
|
|
145
|
+
if bot_instance.instance_variable_get(:@running)
|
|
146
|
+
thread_logger.debug "Attempting to reconnect in 5 seconds..."
|
|
147
|
+
sleep 5
|
|
148
|
+
bot_instance.connect_websocket
|
|
149
|
+
else
|
|
150
|
+
thread_logger.debug "BOT: Bot has stopped and will not try to reconnect" # Use local thread_logger
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
@websocket.on :error do |e|
|
|
154
|
+
error_message = e&.message || 'Unknown error'
|
|
155
|
+
thread_logger.debug "WebSocket error: #{error_message}"
|
|
156
|
+
@websocket.close if @websocket&.open?
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
while bot_instance.instance_variable_get(:@running)
|
|
160
|
+
if Time.now.to_i - @last_heartbeat_sent > @heartbeat_interval
|
|
161
|
+
bot_instance.send_heartbeat
|
|
162
|
+
end
|
|
163
|
+
sleep 1
|
|
164
|
+
end
|
|
165
|
+
thread_logger.debug "WebSocket thread loop finished."
|
|
166
|
+
rescue => e
|
|
167
|
+
thread_logger.debug "WebSocket thread unhandled exception: #{e.message}"
|
|
168
|
+
thread_logger.debug e.backtrace.join("\n")
|
|
169
|
+
bot_instance.instance_variable_set(:@websocket, nil)
|
|
170
|
+
if bot_instance.instance_variable_get(:@running)
|
|
171
|
+
thread_logger.debug "Attempting to reconnect in 5 seconds due to unhandled error..."
|
|
172
|
+
sleep 5
|
|
173
|
+
bot_instance.connect_websocket
|
|
174
|
+
else
|
|
175
|
+
thread_logger.debug "Bot is stopped, not attempting to reconnect after unhandled error."
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
sleep 1
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def send_heartbeat
|
|
183
|
+
if @websocket && @websocket.open?
|
|
184
|
+
payload = { type: 'Ping', data: Time.now.to_i }
|
|
185
|
+
@websocket.send(payload.to_json)
|
|
186
|
+
@last_heartbeat_sent = Time.now.to_i
|
|
187
|
+
end
|
|
188
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
189
|
+
@logger.debug "AN ERROR HAS OCCURED: Error sending heartbeat (SSL): #{e.message}"
|
|
190
|
+
@websocket&.close
|
|
191
|
+
rescue => e
|
|
192
|
+
@logger.debug "AN ERROR HAS OCCURED: Error sending heartbeat: #{e.message}"
|
|
193
|
+
@websocket&.close
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def handle_websocket_message(raw_data)
|
|
197
|
+
begin
|
|
198
|
+
event = JSON.parse(raw_data)
|
|
199
|
+
event_type = event['type']
|
|
200
|
+
case event_type
|
|
201
|
+
when 'Ready'
|
|
202
|
+
@logger.debug "Received 'Ready' event. Populating initial data..."
|
|
203
|
+
if event['servers']
|
|
204
|
+
event['servers'].each do |server_data|
|
|
205
|
+
@servers[server_data['_id']] = {
|
|
206
|
+
'name' => server_data['name'],
|
|
207
|
+
'id' => server_data['_id']
|
|
208
|
+
}
|
|
209
|
+
@logger.debug "Stored server ID from Ready event: #{server_data['_id'].inspect}" # New debug
|
|
210
|
+
end
|
|
211
|
+
@logger.debug "Loaded #{event['servers'].count} real servers from 'Ready' event."
|
|
212
|
+
else
|
|
213
|
+
@logger.debug "'Ready' event received but no 'servers' array found."
|
|
214
|
+
end
|
|
215
|
+
@ready_event_received = true
|
|
216
|
+
@logger.debug "@ready_event_received set to true."
|
|
217
|
+
when 'Message'
|
|
218
|
+
unless event['author'] == @user_id
|
|
219
|
+
process_message(event)
|
|
220
|
+
end
|
|
221
|
+
when 'Authenticated'
|
|
222
|
+
@logger.debug "Successfully authenticated with WebSocket."
|
|
223
|
+
when 'Pong'
|
|
224
|
+
# @logger.debug "Received Pong response."
|
|
225
|
+
when 'Error'
|
|
226
|
+
@logger.debug "AN ERROR HAS OCCURED: Revolt API Error received via WebSocket: #{event['error']}"
|
|
227
|
+
else
|
|
228
|
+
# @logger.debug "AN ERROR HAS OCCURED: Unhandled WebSocket event type: #{event_type}"
|
|
229
|
+
end
|
|
230
|
+
rescue JSON::ParserError => e
|
|
231
|
+
@logger.debug "Failed to parse WebSocket message as JSON: #{e.message}"
|
|
232
|
+
@logger.debug "Raw message: #{raw_data}"
|
|
233
|
+
rescue => e
|
|
234
|
+
@logger.debug "Error processing WebSocket message: #{e.message}"
|
|
235
|
+
@logger.debug e.backtrace.join("\n")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def on_message(&block)
|
|
240
|
+
@message_handlers << block
|
|
241
|
+
end
|
|
242
|
+
def command(command_name, required_permissions: [], nsfw_channel_required: false, &block)
|
|
243
|
+
cmd_key = command_name.to_s.downcase
|
|
244
|
+
@commands[cmd_key] = {
|
|
245
|
+
'block' => block,
|
|
246
|
+
'permissions' => required_permissions,
|
|
247
|
+
'nsfw_cmd' => nsfw_channel_required
|
|
248
|
+
}
|
|
249
|
+
@logger.debug "Command '#{command_name}' registered with permissions: #{required_permissions.inspect}, NSFW required: #{nsfw_channel_required}."
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def check_permissions(message, required_permissions, nsfw_channel_required)
|
|
253
|
+
@logger.debug "check_permissions called with required_permissions: #{required_permissions.inspect}, NSFW required: #{nsfw_channel_required}."
|
|
254
|
+
@logger.debug "message['channel'] value: #{message['channel'].inspect}"
|
|
255
|
+
|
|
256
|
+
server_id_from_message = message['member']&.[]('_id')&.[]('server')
|
|
257
|
+
@logger.debug "message['member']['_id']['server'] value: #{server_id_from_message.inspect}"
|
|
258
|
+
|
|
259
|
+
if required_permissions.empty? && !nsfw_channel_required
|
|
260
|
+
@logger.debug "Permission Check: No specific permissions or NSFW requirement. Allowing command."
|
|
261
|
+
return true
|
|
262
|
+
end
|
|
263
|
+
user_id = message['author']
|
|
264
|
+
|
|
265
|
+
@logger.debug "\n--- PERMISSION DEBUG START ---"
|
|
266
|
+
@logger.debug "User ID from message (user_id): '#{user_id}' | Bot Owner ID stored (@bot_owner_id): '#{@bot_owner_id}'"
|
|
267
|
+
@logger.debug "Are they equal? (user_id == @bot_owner_id): #{user_id == @bot_owner_id}"
|
|
268
|
+
# @logger.debug "User ID char codes: #{user_id.each_char.map(&:ord).join(', ')}"
|
|
269
|
+
# @logger.debug "Bot Owner ID char codes: #{@bot_owner_id.each_char.map(&:ord).join(', ')}"
|
|
270
|
+
@logger.debug "--- PERMISSION DEBUG END ---\n"
|
|
271
|
+
# --- BotOwner Check ---
|
|
272
|
+
if required_permissions.include?('BotOwner')
|
|
273
|
+
if user_id == @bot_owner_id
|
|
274
|
+
@logger.debug "Permission Check: User is the bot owner. Allowing command."
|
|
275
|
+
return true
|
|
276
|
+
else
|
|
277
|
+
@logger.debug "Permission Check: User is NOT the bot owner. Denying command."
|
|
278
|
+
return false
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
# --- NSFW Channel Check ---
|
|
282
|
+
channel_id = message['channel']
|
|
283
|
+
if server_id_from_message.nil?
|
|
284
|
+
if nsfw_channel_required
|
|
285
|
+
@logger.debug "Permission Check: Command requires NSFW channel, but message is in DM. Denying command."
|
|
286
|
+
return false
|
|
287
|
+
end
|
|
288
|
+
else
|
|
289
|
+
channel_details = get_channel_details(channel_id)
|
|
290
|
+
if channel_details.nil?
|
|
291
|
+
@logger.debug "Permission Check: Command requires NSFW channel but could not retrieve channel details. Denying command to be safe."
|
|
292
|
+
return false
|
|
293
|
+
end
|
|
294
|
+
is_channel_nsfw = channel_details['nsfw'] || false
|
|
295
|
+
if nsfw_channel_required && !is_channel_nsfw
|
|
296
|
+
@logger.debug "Permission Check: Command requires NSFW channel, but current channel is NOT NSFW marked. Denying."
|
|
297
|
+
return false
|
|
298
|
+
elsif !nsfw_channel_required && is_channel_nsfw
|
|
299
|
+
@logger.debug "Permission Check: Command does not require NSFW channel, but is in NSFW marked channel. Allowing."
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
if !required_permissions.empty?
|
|
304
|
+
@logger.debug "User #{user_id} is not the bot owner. Requires permissions: #{required_permissions.inspect}. (Full permission check for non-owner, server-specific permissions not implemented)."
|
|
305
|
+
return false
|
|
306
|
+
end
|
|
307
|
+
@logger.debug "Permission Check: All checks passed. Allowing command."
|
|
308
|
+
true
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def process_message(message)
|
|
312
|
+
unless @ready_event_received
|
|
313
|
+
@logger.debug "AN ERROR HAS OCCURED: Bot is not ready for commands"
|
|
314
|
+
return
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
unless @selfbot
|
|
318
|
+
return if message['author'] == @user_id
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
content = message['content']&.strip
|
|
322
|
+
return if content.nil? || content.empty?
|
|
323
|
+
@logger.debug "Full Message object received in process_message: #{message.inspect}"
|
|
324
|
+
|
|
325
|
+
@commands.each do |cmd_name, cmd_data|
|
|
326
|
+
command_full_string = "#{@prefix}#{cmd_name}"
|
|
327
|
+
if content.downcase.start_with?(command_full_string.downcase)
|
|
328
|
+
args_string = content[command_full_string.length..]&.strip
|
|
329
|
+
args = args_string.to_s.split(/\s+/)
|
|
330
|
+
args = [] if args == ['']
|
|
331
|
+
# --- Permission Check (Pass both permissions and nsfw_channel_required to check_permissions) ---
|
|
332
|
+
if check_permissions(message, cmd_data['permissions'], cmd_data['nsfw_cmd'])
|
|
333
|
+
@logger.debug "Executing command: '#{cmd_name}' with args: #{args.inspect}"
|
|
334
|
+
cmd_data['block'].call(message, args)
|
|
335
|
+
else
|
|
336
|
+
channel_id = message['channel']
|
|
337
|
+
if cmd_data['nsfw_cmd'] && message['member']
|
|
338
|
+
channel_details = get_channel_details(channel_id)
|
|
339
|
+
if channel_details && !channel_details['nsfw']
|
|
340
|
+
self.send_message(channel_id, text: "⛔ A NSFW marked channel is required to use the following command: `#{cmd_name}`")
|
|
341
|
+
else
|
|
342
|
+
self.send_message(channel_id, text: "⛔ You don't have permission to use the `#{cmd_name}` command.")
|
|
343
|
+
end
|
|
344
|
+
else
|
|
345
|
+
self.send_message(channel_id, text: "⛔ You don't have permission to use the `#{cmd_name}` command.")
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
return
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
@message_handlers.each do |handler|
|
|
353
|
+
@logger.debug "Calling general message handler for: '#{content}'"
|
|
354
|
+
handler.call(message)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def send_message(channel_id, text: nil, embeds: nil, masquerade_name: nil, masquerade_avatar_url: nil)
|
|
359
|
+
if text.nil? && (embeds.nil? || embeds.empty?)
|
|
360
|
+
@logger.debug "AN ERROR HAS OCCURED: Cannot send empty message or embeds."
|
|
361
|
+
return
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
payload = {}
|
|
365
|
+
payload[:content] = text if text
|
|
366
|
+
|
|
367
|
+
if embeds && !embeds.empty?
|
|
368
|
+
filtered_embeds = embeds.map do |embed|
|
|
369
|
+
supported_keys = ['title', 'description', 'colour', 'url', 'icon_url', 'media']
|
|
370
|
+
embed.select { |k, v| supported_keys.include?(k.to_s) }
|
|
371
|
+
end
|
|
372
|
+
payload[:embeds] = filtered_embeds
|
|
373
|
+
end
|
|
374
|
+
if masquerade_name || masquerade_avatar_url
|
|
375
|
+
payload[:masquerade] = {}
|
|
376
|
+
payload[:masquerade][:name] = masquerade_name if masquerade_name
|
|
377
|
+
payload[:masquerade][:avatar] = masquerade_avatar_url if masquerade_avatar_url
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
@request_queue.enqueue do
|
|
381
|
+
@logger.debug "Attempting to send message to channel '#{channel_id}' via queue..."
|
|
382
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages")
|
|
383
|
+
req = Net::HTTP::Post.new(uri)
|
|
384
|
+
_add_auth_header(req)
|
|
385
|
+
req['Content-Type'] = 'application/json'
|
|
386
|
+
req.body = payload.to_json
|
|
387
|
+
|
|
388
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
389
|
+
http.request(req)
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
393
|
+
@logger.debug "Message sent successfully!"
|
|
394
|
+
else
|
|
395
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to send message: #{res.message} (Code: #{res.code})"
|
|
396
|
+
@logger.debug "Response Body: #{res.body}"
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
rescue => e
|
|
400
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing message: #{e.message}"
|
|
401
|
+
@logger.debug e.backtrace.join("\n")
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def find_unicode_emoji(shortcode)
|
|
405
|
+
EMOJI_MAP[shortcode]
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def add_reaction(channel_id, message_id, emoji_id)
|
|
409
|
+
if emoji_id.start_with?(':') && emoji_id.end_with?(':')
|
|
410
|
+
@logger.debug "AN ERROR HAS OCCURED: Cannot add reaction with shortcode '#{emoji_id}'. Please use the actual Unicode emoji or a custom emoji ID."
|
|
411
|
+
return
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
@request_queue.enqueue do
|
|
415
|
+
@logger.debug "Attempting to add reaction '#{emoji_id}' to message '#{message_id}' in channel '#{channel_id}' via queue."
|
|
416
|
+
encoded_emoji_id = CGI.escape(emoji_id)
|
|
417
|
+
|
|
418
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages/#{message_id}/reactions/#{encoded_emoji_id}")
|
|
419
|
+
req = Net::HTTP::Put.new(uri)
|
|
420
|
+
_add_auth_header(req)
|
|
421
|
+
|
|
422
|
+
res = nil
|
|
423
|
+
begin
|
|
424
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 10, open_timeout: 10) do |http|
|
|
425
|
+
http.request(req)
|
|
426
|
+
end
|
|
427
|
+
rescue Net::ReadTimeout => e
|
|
428
|
+
@logger.debug "AN ERROR HAS OCCURED: The network request timed out while waiting for a response after 10 seconds. Error: #{e.message}"
|
|
429
|
+
return
|
|
430
|
+
rescue Net::OpenTimeout => e
|
|
431
|
+
@logger.debug "AN ERROR HAS OCCURED: The network request timed out while trying to open a connection after 10 seconds. Error: #{e.message}"
|
|
432
|
+
return
|
|
433
|
+
rescue => e
|
|
434
|
+
@logger.debug "AN ERROR HAS OCCURED: An unexpected error occurred during the network request. Error: #{e.message}"
|
|
435
|
+
return
|
|
436
|
+
ensure
|
|
437
|
+
@logger.debug "Network request to add reaction finished."
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
@logger.debug "Received response with code: #{res.code}"
|
|
441
|
+
if res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
442
|
+
@logger.debug "Reaction added successfully!"
|
|
443
|
+
else
|
|
444
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to add reaction: #{res.message} (Code: #{res.code})"
|
|
445
|
+
@logger.debug "Response Body: #{res.body}"
|
|
446
|
+
if res.code == '403'
|
|
447
|
+
@logger.debug "HINT: The bot may be missing the 'AddReactions' permission in this channel or server."
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def remove_reaction(channel_id, message_id, emoji_id, user_id: nil)
|
|
454
|
+
target_user_id = user_id || @user_id
|
|
455
|
+
@request_queue.enqueue do
|
|
456
|
+
@logger.debug "Attempting to remove reaction '#{emoji_id}' from message '#{message_id}' by user '#{target_user_id}' in channel '#{channel_id}' via queue."
|
|
457
|
+
|
|
458
|
+
encoded_emoji_id = CGI.escape(emoji_id)
|
|
459
|
+
|
|
460
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages/#{message_id}/reactions/#{encoded_emoji_id}?user_id=#{target_user_id}")
|
|
461
|
+
req = Net::HTTP::Delete.new(uri)
|
|
462
|
+
_add_auth_header(req)
|
|
463
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
464
|
+
http.request(req)
|
|
465
|
+
end
|
|
466
|
+
if res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
467
|
+
@logger.debug "Reaction removed successfully!"
|
|
468
|
+
else
|
|
469
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to remove reaction: #{res.message} (Code: #{res.code})"
|
|
470
|
+
@logger.debug "Response Body: #{res.body}"
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
rescue => e
|
|
474
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing remove reaction: #{e.message}"
|
|
475
|
+
@logger.debug e.backtrace.join("\n")
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def get_channel_details(channel_id)
|
|
479
|
+
@logger.debug "Fetching channel details for ID: #{channel_id} (direct API call for permission check)."
|
|
480
|
+
uri = URI("#{@api_url}/channels/#{channel_id}")
|
|
481
|
+
req = Net::HTTP::Get.new(uri)
|
|
482
|
+
req['x-bot-token'] = @token
|
|
483
|
+
|
|
484
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
485
|
+
http.request(req)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
489
|
+
JSON.parse(res.body)
|
|
490
|
+
else
|
|
491
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to fetch channel details for #{channel_id}: #{res.message} (Code: #{res.code})"
|
|
492
|
+
nil
|
|
493
|
+
end
|
|
494
|
+
rescue => e
|
|
495
|
+
@logger.debug "AN ERROR HAS OCCURED: Error fetching channel details: #{e.message}"
|
|
496
|
+
nil
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def get_server_info(server_id)
|
|
500
|
+
@logger.debug "get_server_info called with server_id: '#{server_id}' (Type: #{server_id.class}, Length: #{server_id.length})"
|
|
501
|
+
@logger.debug "Available server IDs in cache (@servers.keys): #{@servers.keys.inspect}"
|
|
502
|
+
found_server = @servers[server_id]
|
|
503
|
+
@logger.debug "Result of @servers[server_id]: #{found_server.inspect}"
|
|
504
|
+
found_server
|
|
505
|
+
end
|
|
506
|
+
def get_server_name(server_id)
|
|
507
|
+
@servers[server_id]&.[]('name')
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def stop
|
|
511
|
+
@logger.debug "Stopping bot..."
|
|
512
|
+
@running = false
|
|
513
|
+
if @websocket_thread && @websocket_thread.alive?
|
|
514
|
+
unless @websocket_thread.join(5)
|
|
515
|
+
@logger.debug "WebSocket thread did not terminate gracefully, forcing kill."
|
|
516
|
+
@websocket_thread.kill
|
|
517
|
+
end
|
|
518
|
+
@logger.debug "WebSocket thread terminated."
|
|
519
|
+
end
|
|
520
|
+
if @websocket && @websocket.open?
|
|
521
|
+
@websocket.close
|
|
522
|
+
@logger.debug "WebSocket closed."
|
|
523
|
+
end
|
|
524
|
+
@request_queue.stop_processing
|
|
525
|
+
@logger.debug "Bot stopped."
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
private
|
|
529
|
+
|
|
530
|
+
def _add_auth_header(request)
|
|
531
|
+
if @selfbot
|
|
532
|
+
request['x-session-token'] = @token
|
|
533
|
+
else
|
|
534
|
+
request['x-bot-token'] = @token
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
end
|
|
488
538
|
end
|
data/lib/revoltrb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: revoltrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Roxanne Studios
|
|
@@ -13,30 +13,30 @@ dependencies:
|
|
|
13
13
|
name: json
|
|
14
14
|
requirement: !ruby/object:Gem::Requirement
|
|
15
15
|
requirements:
|
|
16
|
-
- - "
|
|
16
|
+
- - "~>"
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version:
|
|
18
|
+
version: 2.13.2
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
|
-
- - "
|
|
23
|
+
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version:
|
|
25
|
+
version: 2.13.2
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: net-http
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version:
|
|
32
|
+
version: 0.6.0
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version:
|
|
39
|
+
version: 0.6.0
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: thread
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|