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