stoatrb 0.1.0 → 0.2.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 +4 -4
- data/README.md +2 -2
- data/lib/stoatrb/bot.rb +677 -588
- data/lib/stoatrb/version.rb +1 -1
- metadata +1 -1
data/lib/stoatrb/bot.rb
CHANGED
|
@@ -1,589 +1,678 @@
|
|
|
1
|
-
# lib/stoatrb/bot.rb
|
|
2
|
-
require 'json'
|
|
3
|
-
require 'net/http'
|
|
4
|
-
require 'websocket-client-simple'
|
|
5
|
-
require 'thread'
|
|
6
|
-
require 'time'
|
|
7
|
-
require_relative 'debuglogger'
|
|
8
|
-
require_relative 'request_queue'
|
|
9
|
-
require_relative 'webhooks'
|
|
10
|
-
|
|
11
|
-
module Stoatrb
|
|
12
|
-
class StoatBot
|
|
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
|
-
|
|
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
|
-
@prefix =
|
|
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
|
-
req
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@
|
|
89
|
-
@
|
|
90
|
-
@
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
@
|
|
94
|
-
@
|
|
95
|
-
@
|
|
96
|
-
@
|
|
97
|
-
@
|
|
98
|
-
@
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
@logger.debug "
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@logger.debug "
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
puts
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
valid_statuses
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
req
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
@logger.debug "
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
@logger.debug
|
|
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
|
-
thread_logger.debug "WebSocket thread
|
|
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
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
rescue
|
|
283
|
-
@logger.debug "AN ERROR HAS OCCURED:
|
|
284
|
-
@logger.debug "
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
def
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
@logger.debug "
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
@logger.debug "Permission Check:
|
|
329
|
-
return false
|
|
330
|
-
end
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
end
|
|
454
|
-
|
|
455
|
-
def
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
@logger.debug "
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
@logger.debug "
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
nil
|
|
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
|
-
end
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
1
|
+
# lib/stoatrb/bot.rb
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'websocket-client-simple'
|
|
5
|
+
require 'thread'
|
|
6
|
+
require 'time'
|
|
7
|
+
require_relative 'debuglogger'
|
|
8
|
+
require_relative 'request_queue'
|
|
9
|
+
require_relative 'webhooks'
|
|
10
|
+
|
|
11
|
+
module Stoatrb
|
|
12
|
+
class StoatBot
|
|
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
|
+
attr_reader :token, :user_id, :bot_name, :servers, :prefix, :bot_owner_id, :bot_discriminator, :bot_discoverable, :bot_creation_date, :webhooks
|
|
28
|
+
attr_accessor :websocket_url, :api_url, :cdn_url
|
|
29
|
+
# Initializes the bot with the provided token and configuration.
|
|
30
|
+
def initialize(token, api_url: 'https://api.stoat.chat/0.8', websocket_url: 'wss://events.stoat.chat', cdn_url: 'https://cdn.stoatusercontent.com', prefix: nil, debuglogs: false, selfbot: false)
|
|
31
|
+
@token = token
|
|
32
|
+
@api_url = api_url
|
|
33
|
+
@websocket_url = websocket_url
|
|
34
|
+
@cdn_url = cdn_url
|
|
35
|
+
|
|
36
|
+
@user_id = nil
|
|
37
|
+
@bot_name = nil
|
|
38
|
+
@servers = {}
|
|
39
|
+
@commands = {}
|
|
40
|
+
@message_handlers = []
|
|
41
|
+
|
|
42
|
+
@websocket = nil
|
|
43
|
+
@websocket_thread = nil
|
|
44
|
+
@heartbeat_interval = 30
|
|
45
|
+
@last_heartbeat_sent = Time.now.to_i
|
|
46
|
+
@running = false
|
|
47
|
+
@ready_event_received = false
|
|
48
|
+
@logger = Stoatrb::DebugLogger.new(debuglogs)
|
|
49
|
+
@request_queue = Stoatrb::RequestQueue.new(500)
|
|
50
|
+
@webhooks = Stoatrb::Webhooks.new(api_url, @logger, token, selfbot)
|
|
51
|
+
|
|
52
|
+
@prefix = "!"
|
|
53
|
+
@prefix = prefix if prefix
|
|
54
|
+
@selfbot = selfbot
|
|
55
|
+
@logger.debug "Stoat.chat Bot 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 Stoat.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 "AN ERROR HAS OCCURED: Essential properties (_id, username) missing or nil in API response from /users/@me. Please inspect the 'Response Body' above for unexpected format."
|
|
100
|
+
return false
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
@logger.debug "Successfully identified as #{@bot_name} (ID: #{@user_id}) via REST API."
|
|
104
|
+
@logger.debug "Bot Owner ID: #{@bot_owner_id}, Owner ID: #{@bot_owner_id}, Discoverable: #{@bot_discoverable.inspect}, Created: #{@bot_creation_date}"
|
|
105
|
+
else
|
|
106
|
+
@logger.debug "AN ERROR HAS OCCURED: Initial REST API call failed.... #{res.message} (Code: #{res.code})"
|
|
107
|
+
@logger.debug "Please check your bot token. Cannot proceed with WebSocket connection. Response Body: #{res.body}"
|
|
108
|
+
return false
|
|
109
|
+
end
|
|
110
|
+
# Step 2: Connect to the WebSocket
|
|
111
|
+
@running = true
|
|
112
|
+
connect_websocket
|
|
113
|
+
@request_queue.start_processing
|
|
114
|
+
true
|
|
115
|
+
end
|
|
116
|
+
def run
|
|
117
|
+
begin
|
|
118
|
+
unless login
|
|
119
|
+
puts "Bot failed to log in. Exiting."
|
|
120
|
+
exit(1)
|
|
121
|
+
end
|
|
122
|
+
puts "Bot is online and running. Press Ctrl+C to stop."
|
|
123
|
+
@websocket_thread.join
|
|
124
|
+
rescue Interrupt
|
|
125
|
+
puts "\nCtrl+C detected. Shutting down bot gracefully..."
|
|
126
|
+
rescue => e
|
|
127
|
+
puts "An unhandled error occurred in the main script loop: #{e.message}"
|
|
128
|
+
puts e.backtrace.join("\n")
|
|
129
|
+
ensure
|
|
130
|
+
stop
|
|
131
|
+
puts "Bot process ended."
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def set_presence(status)
|
|
136
|
+
valid_statuses = ['Online', 'Idle', 'Dnd', 'Focus', 'Invisible']
|
|
137
|
+
unless valid_statuses.include?(status)
|
|
138
|
+
@logger.debug "AN ERROR HAS OCCURED: Invalid status '#{status}'. Must be one of the following choices: #{valid_statuses.join(', ')}."
|
|
139
|
+
return false
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
@logger.debug "Attempting to set bot presence to '#{status}'."
|
|
143
|
+
uri = URI("#{@api_url}/users/@me")
|
|
144
|
+
req = Net::HTTP::Patch.new(uri)
|
|
145
|
+
_add_auth_header(req)
|
|
146
|
+
req['Content-Type'] = 'application/json'
|
|
147
|
+
payload = {
|
|
148
|
+
status: {
|
|
149
|
+
presence: status
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
req.body = payload.to_json
|
|
153
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
154
|
+
http.request(req)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
158
|
+
@logger.debug "BOT: Presence has been changed to '#{status}'."
|
|
159
|
+
true
|
|
160
|
+
else
|
|
161
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to update presence. Response: #{res.message} (Code: #{res.code})"
|
|
162
|
+
@logger.debug "Response Body: #{res.body}"
|
|
163
|
+
false
|
|
164
|
+
end
|
|
165
|
+
rescue => e
|
|
166
|
+
@logger.debug "AN ERROR HAS OCCURED: Error setting presence: #{e.message}"
|
|
167
|
+
@logger.debug e.backtrace.join("\n")
|
|
168
|
+
false
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def connect_websocket
|
|
172
|
+
if @websocket && @websocket.open? && @running
|
|
173
|
+
@logger.debug "WebSocket already open and running."
|
|
174
|
+
return
|
|
175
|
+
end
|
|
176
|
+
@logger.debug "Connecting to WebSocket: #{@websocket_url}"
|
|
177
|
+
ws_url_with_token = "#{@websocket_url}?token=#{@token}"
|
|
178
|
+
bot_instance = self
|
|
179
|
+
thread_logger = @logger
|
|
180
|
+
|
|
181
|
+
@websocket_thread = Thread.new do
|
|
182
|
+
begin
|
|
183
|
+
@websocket = WebSocket::Client::Simple.connect ws_url_with_token
|
|
184
|
+
@websocket.on :open do
|
|
185
|
+
thread_logger.debug "WebSocket connection opened!"
|
|
186
|
+
end
|
|
187
|
+
@websocket.on :message do |msg|
|
|
188
|
+
bot_instance.handle_websocket_message(msg.data)
|
|
189
|
+
end
|
|
190
|
+
@websocket.on :close do |e|
|
|
191
|
+
close_code = e&.code || 'N/A'
|
|
192
|
+
close_reason = e&.reason || 'No reason provided'
|
|
193
|
+
thread_logger.debug "WebSocket closed: #{close_code} - #{close_reason}."
|
|
194
|
+
bot_instance.instance_variable_set(:@websocket, nil)
|
|
195
|
+
if bot_instance.instance_variable_get(:@running)
|
|
196
|
+
thread_logger.debug "Attempting to reconnect in 5 seconds..."
|
|
197
|
+
sleep 5
|
|
198
|
+
bot_instance.connect_websocket
|
|
199
|
+
else
|
|
200
|
+
thread_logger.debug "BOT: Bot has stopped and will not try to reconnect" # Use local thread_logger
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
@websocket.on :error do |e|
|
|
204
|
+
error_message = e&.message || 'Unknown error'
|
|
205
|
+
thread_logger.debug "WebSocket error: #{error_message}"
|
|
206
|
+
@websocket.close if @websocket&.open?
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
while bot_instance.instance_variable_get(:@running)
|
|
210
|
+
if Time.now.to_i - @last_heartbeat_sent > @heartbeat_interval
|
|
211
|
+
bot_instance.send_heartbeat
|
|
212
|
+
end
|
|
213
|
+
sleep 1
|
|
214
|
+
end
|
|
215
|
+
thread_logger.debug "WebSocket thread loop finished."
|
|
216
|
+
rescue => e
|
|
217
|
+
thread_logger.debug "WebSocket thread unhandled exception: #{e.message}"
|
|
218
|
+
thread_logger.debug e.backtrace.join("\n")
|
|
219
|
+
bot_instance.instance_variable_set(:@websocket, nil)
|
|
220
|
+
if bot_instance.instance_variable_get(:@running)
|
|
221
|
+
thread_logger.debug "Attempting to reconnect in 5 seconds due to unhandled error..."
|
|
222
|
+
sleep 5
|
|
223
|
+
bot_instance.connect_websocket
|
|
224
|
+
else
|
|
225
|
+
thread_logger.debug "Bot is stopped, not attempting to reconnect after unhandled error."
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
sleep 1
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def send_heartbeat
|
|
233
|
+
if @websocket && @websocket.open?
|
|
234
|
+
payload = { type: 'Ping', data: Time.now.to_i }
|
|
235
|
+
@websocket.send(payload.to_json)
|
|
236
|
+
@last_heartbeat_sent = Time.now.to_i
|
|
237
|
+
end
|
|
238
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
239
|
+
@logger.debug "AN ERROR HAS OCCURED: Error sending heartbeat (SSL): #{e.message}"
|
|
240
|
+
@websocket&.close
|
|
241
|
+
rescue => e
|
|
242
|
+
@logger.debug "AN ERROR HAS OCCURED: Error sending heartbeat: #{e.message}"
|
|
243
|
+
@websocket&.close
|
|
244
|
+
end
|
|
245
|
+
def handle_websocket_message(raw_data)
|
|
246
|
+
begin
|
|
247
|
+
event = JSON.parse(raw_data)
|
|
248
|
+
event_type = event['type']
|
|
249
|
+
case event_type
|
|
250
|
+
when 'Ready'
|
|
251
|
+
@logger.debug "Received 'Ready' event. Populating initial data..."
|
|
252
|
+
if event['servers']
|
|
253
|
+
event['servers'].each do |server_data|
|
|
254
|
+
@servers[server_data['_id']] = {
|
|
255
|
+
'name' => server_data['name'],
|
|
256
|
+
'id' => server_data['_id']
|
|
257
|
+
}
|
|
258
|
+
@logger.debug "Stored server ID from Ready event: #{server_data['_id'].inspect}" # New debug
|
|
259
|
+
end
|
|
260
|
+
@logger.debug "Loaded #{event['servers'].count} real servers from 'Ready' event."
|
|
261
|
+
else
|
|
262
|
+
@logger.debug "'Ready' event received but no 'servers' array found."
|
|
263
|
+
end
|
|
264
|
+
@ready_event_received = true
|
|
265
|
+
@logger.debug "@ready_event_received set to true."
|
|
266
|
+
when 'Message'
|
|
267
|
+
unless event['author'] == @user_id
|
|
268
|
+
process_message(event)
|
|
269
|
+
end
|
|
270
|
+
when 'Authenticated'
|
|
271
|
+
@logger.debug "Successfully authenticated with WebSocket."
|
|
272
|
+
when 'Pong'
|
|
273
|
+
# @logger.debug "Received Pong response."
|
|
274
|
+
when 'Error'
|
|
275
|
+
@logger.debug "AN ERROR HAS OCCURED: Stoat.chat API Error received via WebSocket: #{event['error']}"
|
|
276
|
+
else
|
|
277
|
+
# @logger.debug "AN ERROR HAS OCCURED: Unhandled WebSocket event type: #{event_type}"
|
|
278
|
+
end
|
|
279
|
+
rescue JSON::ParserError => e
|
|
280
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to parse WebSocket message as JSON: #{e.message}"
|
|
281
|
+
@logger.debug "Raw message: #{raw_data}"
|
|
282
|
+
rescue => e
|
|
283
|
+
@logger.debug "AN ERROR HAS OCCURED: Error processing WebSocket message: #{e.message}"
|
|
284
|
+
@logger.debug e.backtrace.join("\n")
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def on_message(&block)
|
|
289
|
+
@message_handlers << block
|
|
290
|
+
end
|
|
291
|
+
def command(command_name, required_permissions: [], nsfw_channel_required: false, &block)
|
|
292
|
+
cmd_key = command_name.to_s.downcase
|
|
293
|
+
@commands[cmd_key] = {
|
|
294
|
+
'block' => block,
|
|
295
|
+
'permissions' => required_permissions,
|
|
296
|
+
'nsfw_cmd' => nsfw_channel_required
|
|
297
|
+
}
|
|
298
|
+
@logger.debug "Command '#{command_name}' registered! Permissions: #{required_permissions.inspect}, NSFW required: #{nsfw_channel_required}."
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def check_permissions(message, required_permissions, nsfw_channel_required)
|
|
302
|
+
@logger.debug "check_permissions called with required_permissions: #{required_permissions.inspect}, NSFW required: #{nsfw_channel_required}."
|
|
303
|
+
@logger.debug "message['channel'] value: #{message['channel'].inspect}"
|
|
304
|
+
server_id_from_message = message['member']&.[]('_id')&.[]('server')
|
|
305
|
+
@logger.debug "message['member']['_id']['server'] value: #{server_id_from_message.inspect}"
|
|
306
|
+
if required_permissions.empty? && !nsfw_channel_required
|
|
307
|
+
@logger.debug "Permission Check: No specific permissions or NSFW requirement. Allowing command."
|
|
308
|
+
return true
|
|
309
|
+
end
|
|
310
|
+
user_id = message['author']
|
|
311
|
+
|
|
312
|
+
# --- PERMISSION CHECK SYSTEM ---
|
|
313
|
+
# --- BotOwner ---
|
|
314
|
+
if required_permissions.include?('BotOwner')
|
|
315
|
+
if user_id == @bot_owner_id
|
|
316
|
+
@logger.debug "Permission Check: User is the bot owner. Allowing command."
|
|
317
|
+
return true
|
|
318
|
+
else
|
|
319
|
+
@logger.debug "Permission Check: User is NOT the bot owner. Denying command."
|
|
320
|
+
return false
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
# --- ServerOwner ---
|
|
324
|
+
if required_permissions.include?('ServerOwner')
|
|
325
|
+
server_id = message['member']&.[]('_id')&.[]('server')
|
|
326
|
+
|
|
327
|
+
if server_id.nil?
|
|
328
|
+
@logger.debug "Permission Check: Command requires ServerOwner, but message is not in a server (DM). Denying command."
|
|
329
|
+
return false
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
owner_id = get_server_owner_id(server_id)
|
|
333
|
+
if owner_id && user_id == owner_id
|
|
334
|
+
@logger.debug "Permission Check: User is the server owner (#{owner_id}). Allowing command."
|
|
335
|
+
return true
|
|
336
|
+
else
|
|
337
|
+
@logger.debug "Permission Check: User is NOT the server owner (#{owner_id.inspect}). Denying command."
|
|
338
|
+
return false
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
# --- END PERMISSION CHECK SYSTEM ---
|
|
342
|
+
|
|
343
|
+
channel_id = message['channel']
|
|
344
|
+
if server_id_from_message.nil?
|
|
345
|
+
if nsfw_channel_required
|
|
346
|
+
@logger.debug "Permission Check: Command requires NSFW channel, but message is in DM. Denying command."
|
|
347
|
+
return false
|
|
348
|
+
end
|
|
349
|
+
else
|
|
350
|
+
channel_details = get_channel_details(channel_id)
|
|
351
|
+
if channel_details.nil?
|
|
352
|
+
@logger.debug "Permission Check: Command requires NSFW channel but could not retrieve channel details. Denying command."
|
|
353
|
+
return false
|
|
354
|
+
end
|
|
355
|
+
is_channel_nsfw = channel_details['nsfw'] || false
|
|
356
|
+
if nsfw_channel_required && !is_channel_nsfw
|
|
357
|
+
@logger.debug "Permission Check: Command requires NSFW channel, but current channel is NOT NSFW marked. Denying."
|
|
358
|
+
return false
|
|
359
|
+
elsif !nsfw_channel_required && is_channel_nsfw
|
|
360
|
+
@logger.debug "Permission Check: Command does not require NSFW channel, but is in NSFW marked channel. Allowing."
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
if !required_permissions.empty?
|
|
365
|
+
@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)."
|
|
366
|
+
return false
|
|
367
|
+
end
|
|
368
|
+
@logger.debug "Permission Check: All checks passed. Allowing command."
|
|
369
|
+
true
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def edit_message(channel_id, message_id, text: nil, embeds: nil)
|
|
373
|
+
if text.nil? && (embeds.nil? || embeds.empty?)
|
|
374
|
+
@logger.debug "AN ERROR HAS OCCURED: Cannot edit message with empty content or embeds."
|
|
375
|
+
return
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
payload = {}
|
|
379
|
+
payload[:content] = text if text
|
|
380
|
+
|
|
381
|
+
if embeds && !embeds.empty?
|
|
382
|
+
filtered_embeds = embeds.map do |embed|
|
|
383
|
+
supported_keys = ['title', 'description', 'colour', 'url', 'icon_url', 'media']
|
|
384
|
+
embed.select { |k, v| supported_keys.include?(k.to_s) }
|
|
385
|
+
end
|
|
386
|
+
payload[:embeds] = filtered_embeds
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
@request_queue.enqueue do
|
|
390
|
+
@logger.debug "Attempting to edit message '#{message_id}' in channel '#{channel_id}' via queue..."
|
|
391
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages/#{message_id}")
|
|
392
|
+
req = Net::HTTP::Patch.new(uri)
|
|
393
|
+
_add_auth_header(req)
|
|
394
|
+
req['Content-Type'] = 'application/json'
|
|
395
|
+
req.body = payload.to_json
|
|
396
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
397
|
+
http.request(req)
|
|
398
|
+
end
|
|
399
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
400
|
+
@logger.debug "Message edited successfully!"
|
|
401
|
+
else
|
|
402
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to edit message: #{res.message} (Code: #{res.code})"
|
|
403
|
+
@logger.debug "Response Body: #{res.body}"
|
|
404
|
+
end
|
|
405
|
+
end
|
|
406
|
+
rescue => e
|
|
407
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing message edit: #{e.message}"
|
|
408
|
+
@logger.debug e.backtrace.join("\n")
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
def process_message(message)
|
|
412
|
+
unless @ready_event_received
|
|
413
|
+
@logger.debug "AN ERROR HAS OCCURED: Bot is not ready for commands"
|
|
414
|
+
return
|
|
415
|
+
end
|
|
416
|
+
unless @selfbot
|
|
417
|
+
return if message['author'] == @user_id
|
|
418
|
+
end
|
|
419
|
+
content = message['content']&.strip
|
|
420
|
+
return if content.nil? || content.empty?
|
|
421
|
+
@logger.debug "Full Message object received in process_message: #{message.inspect}"
|
|
422
|
+
|
|
423
|
+
@commands.each do |cmd_name, cmd_data|
|
|
424
|
+
command_full_string = "#{@prefix}#{cmd_name}"
|
|
425
|
+
if content.downcase.start_with?(command_full_string.downcase)
|
|
426
|
+
args_string = content[command_full_string.length..]&.strip
|
|
427
|
+
args = args_string.to_s.split(/\s+/)
|
|
428
|
+
args = [] if args == ['']
|
|
429
|
+
# --- Permission Check (Pass both permissions and nsfw_channel_required to check_permissions) ---
|
|
430
|
+
if check_permissions(message, cmd_data['permissions'], cmd_data['nsfw_cmd'])
|
|
431
|
+
@logger.debug "Executing command: '#{cmd_name}' with args: #{args.inspect}"
|
|
432
|
+
cmd_data['block'].call(message, args)
|
|
433
|
+
else
|
|
434
|
+
channel_id = message['channel']
|
|
435
|
+
permission_denial = "⛔ You don't have permission to use the `#{cmd_name}` command."
|
|
436
|
+
if cmd_data['nsfw_cmd'] && message['member']
|
|
437
|
+
channel_details = get_channel_details(channel_id)
|
|
438
|
+
if channel_details && !channel_details['nsfw']
|
|
439
|
+
permission_denial = "⛔ A NSFW marked channel is required to use the following command: `#{cmd_name}`"
|
|
440
|
+
end
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
self.send_message(channel_id, text: permission_denial)
|
|
444
|
+
end
|
|
445
|
+
return
|
|
446
|
+
end
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
@message_handlers.each do |handler|
|
|
450
|
+
@logger.debug "Calling general message handler for: '#{content}'"
|
|
451
|
+
handler.call(message)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def send_message(channel_id, text: nil, embeds: nil, masquerade_name: nil, masquerade_avatar_url: nil)
|
|
456
|
+
if text.nil? && (embeds.nil? || embeds.empty?)
|
|
457
|
+
@logger.debug "AN ERROR HAS OCCURED: Cannot send empty message or embeds."
|
|
458
|
+
return
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
payload = {}
|
|
462
|
+
payload[:content] = text if text
|
|
463
|
+
|
|
464
|
+
if embeds && !embeds.empty?
|
|
465
|
+
filtered_embeds = embeds.map do |embed|
|
|
466
|
+
supported_keys = ['title', 'description', 'colour', 'url', 'icon_url', 'media']
|
|
467
|
+
embed.select { |k, v| supported_keys.include?(k.to_s) }
|
|
468
|
+
end
|
|
469
|
+
payload[:embeds] = filtered_embeds
|
|
470
|
+
end
|
|
471
|
+
if masquerade_name || masquerade_avatar_url
|
|
472
|
+
payload[:masquerade] = {}
|
|
473
|
+
payload[:masquerade][:name] = masquerade_name if masquerade_name
|
|
474
|
+
payload[:masquerade][:avatar] = masquerade_avatar_url if masquerade_avatar_url
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
@request_queue.enqueue do
|
|
478
|
+
@logger.debug "Attempting to send message to channel '#{channel_id}' via queue..."
|
|
479
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages")
|
|
480
|
+
req = Net::HTTP::Post.new(uri)
|
|
481
|
+
_add_auth_header(req)
|
|
482
|
+
req['Content-Type'] = 'application/json'
|
|
483
|
+
req.body = payload.to_json
|
|
484
|
+
|
|
485
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
486
|
+
http.request(req)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
490
|
+
@logger.debug "Message sent successfully!"
|
|
491
|
+
else
|
|
492
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to send message: #{res.message} (Code: #{res.code})"
|
|
493
|
+
@logger.debug "Response Body: #{res.body}"
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
rescue => e
|
|
497
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing message: #{e.message}"
|
|
498
|
+
@logger.debug e.backtrace.join("\n")
|
|
499
|
+
end
|
|
500
|
+
def send_message_sync(channel_id, text: nil)
|
|
501
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages")
|
|
502
|
+
req = Net::HTTP::Post.new(uri)
|
|
503
|
+
_add_auth_header(req)
|
|
504
|
+
req['Content-Type'] = 'application/json'
|
|
505
|
+
payload = { content: text }
|
|
506
|
+
req.body = payload.to_json
|
|
507
|
+
@logger.debug "DEBUG: Sync POST URL: #{uri}"
|
|
508
|
+
@logger.debug "DEBUG: Sync POST Payload: #{req.body}"
|
|
509
|
+
begin
|
|
510
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 10) do |http|
|
|
511
|
+
http.request(req)
|
|
512
|
+
end
|
|
513
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
514
|
+
response_data = JSON.parse(res.body)
|
|
515
|
+
@logger.debug "Sync message sent successfully. ID: #{response_data['_id']}"
|
|
516
|
+
return response_data['_id']
|
|
517
|
+
else
|
|
518
|
+
@logger.debug "ERROR: Sync send failed. Code: #{res.code}. Response: #{res.body.slice(0, 150)}..."
|
|
519
|
+
return nil
|
|
520
|
+
end
|
|
521
|
+
rescue => e
|
|
522
|
+
@logger.debug "ERROR: Synchronous HTTP request failed: #{e.message}"
|
|
523
|
+
return nil
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def find_unicode_emoji(shortcode)
|
|
528
|
+
EMOJI_MAP[shortcode]
|
|
529
|
+
end
|
|
530
|
+
def add_reaction(channel_id, message_id, emoji_id)
|
|
531
|
+
if emoji_id.start_with?(':') && emoji_id.end_with?(':')
|
|
532
|
+
@logger.debug "AN ERROR HAS OCCURED: Cannot add reaction with shortcode '#{emoji_id}'. Please use the actual Unicode emoji or a custom emoji ID."
|
|
533
|
+
return
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
@request_queue.enqueue do
|
|
537
|
+
@logger.debug "Attempting to add reaction '#{emoji_id}' to message '#{message_id}' in channel '#{channel_id}' via queue."
|
|
538
|
+
encoded_emoji_id = CGI.escape(emoji_id)
|
|
539
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages/#{message_id}/reactions/#{encoded_emoji_id}")
|
|
540
|
+
req = Net::HTTP::Put.new(uri)
|
|
541
|
+
_add_auth_header(req)
|
|
542
|
+
|
|
543
|
+
res = nil
|
|
544
|
+
begin
|
|
545
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 10, open_timeout: 10) do |http|
|
|
546
|
+
http.request(req)
|
|
547
|
+
end
|
|
548
|
+
rescue Net::ReadTimeout => e
|
|
549
|
+
@logger.debug "AN ERROR HAS OCCURED: The network request timed out while waiting for a response after 10 seconds. Error: #{e.message}"
|
|
550
|
+
return
|
|
551
|
+
rescue Net::OpenTimeout => e
|
|
552
|
+
@logger.debug "AN ERROR HAS OCCURED: The network request timed out while trying to open a connection after 10 seconds. Error: #{e.message}"
|
|
553
|
+
return
|
|
554
|
+
rescue => e
|
|
555
|
+
@logger.debug "AN ERROR HAS OCCURED: An unexpected error occurred during the network request. Error: #{e.message}"
|
|
556
|
+
return
|
|
557
|
+
ensure
|
|
558
|
+
@logger.debug "Network request to add reaction finished."
|
|
559
|
+
end
|
|
560
|
+
@logger.debug "Received response with code: #{res.code}"
|
|
561
|
+
if res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
562
|
+
@logger.debug "Reaction added successfully!"
|
|
563
|
+
else
|
|
564
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to add reaction: #{res.message} (Code: #{res.code})"
|
|
565
|
+
@logger.debug "Response Body: #{res.body}"
|
|
566
|
+
if res.code == '403'
|
|
567
|
+
@logger.debug "HINT: The bot may be missing the 'AddReactions' permission in this channel or server."
|
|
568
|
+
end
|
|
569
|
+
end
|
|
570
|
+
end
|
|
571
|
+
end
|
|
572
|
+
def remove_reaction(channel_id, message_id, emoji_id, user_id: nil)
|
|
573
|
+
target_user_id = user_id || @user_id
|
|
574
|
+
@request_queue.enqueue do
|
|
575
|
+
@logger.debug "Attempting to remove reaction '#{emoji_id}' from message '#{message_id}' by user '#{target_user_id}' in channel '#{channel_id}' via queue."
|
|
576
|
+
encoded_emoji_id = CGI.escape(emoji_id)
|
|
577
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages/#{message_id}/reactions/#{encoded_emoji_id}?user_id=#{target_user_id}")
|
|
578
|
+
req = Net::HTTP::Delete.new(uri)
|
|
579
|
+
_add_auth_header(req)
|
|
580
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
581
|
+
http.request(req)
|
|
582
|
+
end
|
|
583
|
+
if res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
584
|
+
@logger.debug "Reaction removed successfully!"
|
|
585
|
+
else
|
|
586
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to remove reaction: #{res.message} (Code: #{res.code})"
|
|
587
|
+
@logger.debug "Response Body: #{res.body}"
|
|
588
|
+
end
|
|
589
|
+
end
|
|
590
|
+
rescue => e
|
|
591
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing remove reaction: #{e.message}"
|
|
592
|
+
@logger.debug e.backtrace.join("\n")
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def get_server_owner_id(server_id)
|
|
596
|
+
return nil unless server_id
|
|
597
|
+
@logger.debug "Fetching owner ID for server #{server_id}."
|
|
598
|
+
uri = URI("#{@api_url}/servers/#{server_id}")
|
|
599
|
+
req = Net::HTTP::Get.new(uri)
|
|
600
|
+
_add_auth_header(req)
|
|
601
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
602
|
+
http.request(req)
|
|
603
|
+
end
|
|
604
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
605
|
+
server_data = JSON.parse(res.body)
|
|
606
|
+
owner_id = server_data&.[]('owner')
|
|
607
|
+
@logger.debug "Successfully retrieved owner ID: #{owner_id} for server #{server_id}."
|
|
608
|
+
owner_id
|
|
609
|
+
else
|
|
610
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to fetch server details for #{server_id}: #{res.message} (Code: #{res.code})"
|
|
611
|
+
nil
|
|
612
|
+
end
|
|
613
|
+
rescue => e
|
|
614
|
+
@logger.debug "AN ERROR HAS OCCURED: Error fetching server owner ID: #{e.message}"
|
|
615
|
+
nil
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def get_channel_details(channel_id)
|
|
619
|
+
@logger.debug "Fetching channel details for ID: #{channel_id} (direct API call for permission check)."
|
|
620
|
+
uri = URI("#{@api_url}/channels/#{channel_id}")
|
|
621
|
+
req = Net::HTTP::Get.new(uri)
|
|
622
|
+
req['x-bot-token'] = @token
|
|
623
|
+
|
|
624
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
625
|
+
http.request(req)
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
629
|
+
JSON.parse(res.body)
|
|
630
|
+
else
|
|
631
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to fetch channel details for #{channel_id}: #{res.message} (Code: #{res.code})"
|
|
632
|
+
nil
|
|
633
|
+
end
|
|
634
|
+
rescue => e
|
|
635
|
+
@logger.debug "AN ERROR HAS OCCURED: Error fetching channel details: #{e.message}"
|
|
636
|
+
nil
|
|
637
|
+
end
|
|
638
|
+
|
|
639
|
+
def get_server_info(server_id)
|
|
640
|
+
@logger.debug "get_server_info called with server_id: '#{server_id}' (Type: #{server_id.class}, Length: #{server_id.length})"
|
|
641
|
+
@logger.debug "Available server IDs in cache (@servers.keys): #{@servers.keys.inspect}"
|
|
642
|
+
found_server = @servers[server_id]
|
|
643
|
+
@logger.debug "Result of @servers[server_id]: #{found_server.inspect}"
|
|
644
|
+
found_server
|
|
645
|
+
end
|
|
646
|
+
def get_server_name(server_id)
|
|
647
|
+
@servers[server_id]&.[]('name')
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def stop
|
|
651
|
+
@logger.debug "Stopping bot..."
|
|
652
|
+
@running = false
|
|
653
|
+
if @websocket_thread && @websocket_thread.alive?
|
|
654
|
+
unless @websocket_thread.join(5)
|
|
655
|
+
@logger.debug "WebSocket thread did not terminate gracefully, forcing kill."
|
|
656
|
+
@websocket_thread.kill
|
|
657
|
+
end
|
|
658
|
+
@logger.debug "WebSocket thread terminated."
|
|
659
|
+
end
|
|
660
|
+
if @websocket && @websocket.open?
|
|
661
|
+
@websocket.close
|
|
662
|
+
@logger.debug "WebSocket closed."
|
|
663
|
+
end
|
|
664
|
+
@request_queue.stop_processing
|
|
665
|
+
@logger.debug "Bot stopped."
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
private
|
|
669
|
+
|
|
670
|
+
def _add_auth_header(request)
|
|
671
|
+
if @selfbot
|
|
672
|
+
request['x-session-token'] = @token
|
|
673
|
+
else
|
|
674
|
+
request['x-bot-token'] = @token
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|
|
589
678
|
end
|