revoltrb 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +114 -0
- data/lib/revoltrb/bot.rb +488 -0
- data/lib/revoltrb/debuglogger.rb +13 -0
- data/lib/revoltrb/request_queue.rb +59 -0
- data/lib/revoltrb/version.rb +6 -0
- data/lib/revoltrb.rb +8 -0
- metadata +103 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: be934ade9a4122fe6ea640bf5aafe2930eb1a11ee2a8f4340d282b3fa99f163c
|
|
4
|
+
data.tar.gz: 6e384401d88c604fa0bfa05b1a840697c0382a288683ac27db23d98e425b4a25
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e36bad7e4adec3333f2341b6d1e85aadc1cbab691552d69a1f9e777a7df5722b2fc048e1e98be0e361a4a1447d2597e65f3cc8888d7afb9fb589e9dfa4f79b27
|
|
7
|
+
data.tar.gz: db5fa947e54394afb19495145a3b3beb8ef50bf6bc17c584fc8369659a281603c48f522fe7a7c0ea4dcf56c7a1f1a35185d5a593ffaea5586951862a19a40535
|
data/README.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# revoltrb
|
|
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.
|
|
6
|
+
|
|
7
|
+
You need Ruby 3.0 or newer in order to use this package (Ruby 3.2 or newer is recommended)
|
|
8
|
+
|
|
9
|
+
## ToDo
|
|
10
|
+
|
|
11
|
+
This list contains a list of things that I know is broken and gotta fix. Contributing will be super helpful.
|
|
12
|
+
|
|
13
|
+
- Fix reactions support
|
|
14
|
+
- Fix obtaining server information
|
|
15
|
+
- Fix not being able to have an embed color
|
|
16
|
+
|
|
17
|
+
## Setup
|
|
18
|
+
|
|
19
|
+
You can install Revoltrb through several methods:
|
|
20
|
+
|
|
21
|
+
#### Method 1: Install from rubygems.org (Bundler)
|
|
22
|
+
|
|
23
|
+
Add the following to your Gemfile file and run the "bundle install" command:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem 'discordrb'
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
#### Method 2: Install via Git
|
|
30
|
+
|
|
31
|
+
Add the following to your Gemfile file and run the "bundle install" command:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
gem 'revoltrb', git: 'https://gitlab.com/roxannewolf/revoltrb'
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
#### Troubleshooting
|
|
38
|
+
|
|
39
|
+
If you encounter the "Exited with code: 16 output:Ignoring debug-1.7.1 because its extensions are not built." error, most likely, there is something wrong with the parser package. This can be fixed by installing parser manually (gem install parser)
|
|
40
|
+
|
|
41
|
+
## Usage
|
|
42
|
+
|
|
43
|
+
You can make a simple bot like this:
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
require 'revoltrb'
|
|
47
|
+
|
|
48
|
+
bot = Revoltrb::RevoltBot.new('REVOLT_BOT_TOKEN_HERE')
|
|
49
|
+
|
|
50
|
+
bot.on_message do |message|
|
|
51
|
+
next if message['author'] == bot.user_id
|
|
52
|
+
content = message['content']&.downcase
|
|
53
|
+
channel_id = message['channel']
|
|
54
|
+
|
|
55
|
+
if content.include?("ping")
|
|
56
|
+
bot.send_message(channel_id, text: "Pong!")
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
begin
|
|
61
|
+
unless bot.login
|
|
62
|
+
puts "Bot failed to log in. Exiting."
|
|
63
|
+
exit(1)
|
|
64
|
+
end
|
|
65
|
+
puts "Bot is ONLINE and READY! Press Ctrl+C to stop."
|
|
66
|
+
bot.instance_variable_get(:@websocket_thread).join
|
|
67
|
+
rescue Interrupt
|
|
68
|
+
puts "\nCtrl+C detected. Shutting down bot"
|
|
69
|
+
rescue => e
|
|
70
|
+
puts "An unhandled error occurred in the main script loop: #{e.message}"
|
|
71
|
+
puts e.backtrace.join("\n")
|
|
72
|
+
ensure
|
|
73
|
+
bot.stop
|
|
74
|
+
puts "Bot process ended."
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
or you can make a bot with full prefix commands like this:
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
require 'revoltrb'
|
|
82
|
+
|
|
83
|
+
bot = Revoltrb::RevoltBot.new('REVOLT_BOT_TOKEN_HERE', prefix: '!')
|
|
84
|
+
|
|
85
|
+
bot.command(:ping) do |message, args|
|
|
86
|
+
channel_id = message['channel']
|
|
87
|
+
bot.send_message(channel_id, text: "Pong! You sent: #{args.join(' ')}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
begin
|
|
91
|
+
unless bot.login
|
|
92
|
+
puts "Bot failed to log in. Exiting."
|
|
93
|
+
exit(1)
|
|
94
|
+
end
|
|
95
|
+
puts "Bot is online and running. Press Ctrl+C to stop."
|
|
96
|
+
bot.instance_variable_get(:@websocket_thread).join
|
|
97
|
+
rescue Interrupt
|
|
98
|
+
puts "\nCtrl+C detected. Shutting down bot gracefully..."
|
|
99
|
+
rescue => e
|
|
100
|
+
puts "An unhandled error occurred in the main script loop: #{e.message}"
|
|
101
|
+
puts e.backtrace.join("\n")
|
|
102
|
+
ensure
|
|
103
|
+
bot.stop
|
|
104
|
+
puts "Bot process ended."
|
|
105
|
+
end
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Support and Help
|
|
109
|
+
|
|
110
|
+
If you need help with this ruby package (a.k.a. Gem), feel free to join the [Roxanne Studios Revolt Server](https://rvlt.gg/r4Ee2R1Z) and use the REVOLTRB category to talk about this package.
|
|
111
|
+
|
|
112
|
+
## Contributing
|
|
113
|
+
|
|
114
|
+
We are working to support more of Revolt's API. Remember, the creator of this package is only a Ruby beginner so contributing to this project will mean a lot and can help with more coverage with the Revolt.chat API. Opening issues and Pull requests are welcome at our [Gitlab repo](https://gitlab.com/roxannewolf/revoltrb) and [Codeberg repo](https://codeberg.org/roxannewolf/revoltrb)
|
data/lib/revoltrb/bot.rb
ADDED
|
@@ -0,0 +1,488 @@
|
|
|
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
|
+
attr_reader :token, :user_id, :bot_name, :servers, :prefix, :bot_owner_id, :bot_discriminator, :bot_discoverable, :bot_creation_date
|
|
14
|
+
attr_accessor :websocket_url, :api_url, :cdn_url
|
|
15
|
+
# Initializes the bot with the provided token, API endpoints, and configuration.
|
|
16
|
+
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)
|
|
17
|
+
@token = token
|
|
18
|
+
@api_url = api_url
|
|
19
|
+
@websocket_url = websocket_url
|
|
20
|
+
@cdn_url = cdn_url
|
|
21
|
+
|
|
22
|
+
@user_id = nil
|
|
23
|
+
@bot_name = nil
|
|
24
|
+
@servers = {}
|
|
25
|
+
@commands = {}
|
|
26
|
+
@message_handlers = []
|
|
27
|
+
|
|
28
|
+
@websocket = nil
|
|
29
|
+
@websocket_thread = nil
|
|
30
|
+
@heartbeat_interval = 30 # Default heartbeat interval in seconds
|
|
31
|
+
@last_heartbeat_sent = Time.now.to_i
|
|
32
|
+
@running = false
|
|
33
|
+
@ready_event_received = false
|
|
34
|
+
@logger = Revoltrb::DebugLogger.new(debuglogs)
|
|
35
|
+
@request_queue = Revoltrb::RequestQueue.new(500)
|
|
36
|
+
|
|
37
|
+
@prefix = "!"
|
|
38
|
+
@prefix = prefix if prefix
|
|
39
|
+
@selfbot = selfbot
|
|
40
|
+
@logger.debug "RevoltBot initialized. API: #{@api_url}, WS: #{@websocket_url}, CDN: #{@cdn_url}, Prefix: #{@prefix}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def get_botinfo
|
|
44
|
+
{
|
|
45
|
+
'bot_id' => @user_id,
|
|
46
|
+
'bot_name' => @bot_name,
|
|
47
|
+
'bot_discriminator' => @bot_discriminator,
|
|
48
|
+
'bot_ownerid' => @bot_owner_id,
|
|
49
|
+
'bot_discoverable' => @bot_discoverable,
|
|
50
|
+
'bot_creationdate' => @bot_creation_date,
|
|
51
|
+
'bot_prefix' => @prefix,
|
|
52
|
+
'bot_token' => @token
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Log into the Revolt.chat bot
|
|
57
|
+
def login
|
|
58
|
+
@logger.debug "BOT: Bot attempting to start...."
|
|
59
|
+
# 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.
|
|
60
|
+
uri = URI("#{@api_url}/users/@me")
|
|
61
|
+
req = Net::HTTP::Get.new(uri)
|
|
62
|
+
_add_auth_header(req)
|
|
63
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
64
|
+
http.request(req)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
68
|
+
bot_user_data = JSON.parse(res.body)
|
|
69
|
+
@logger.debug "Response Body (successful login attempt from /users/@me): #{res.body}"
|
|
70
|
+
@user_id = bot_user_data&.[]('_id')
|
|
71
|
+
@bot_name = bot_user_data&.[]('username')
|
|
72
|
+
@bot_discriminator = bot_user_data&.[]('discriminator')
|
|
73
|
+
bot_specific_info = bot_user_data&.[]('bot')
|
|
74
|
+
@bot_owner_id = bot_specific_info&.[]('owner')
|
|
75
|
+
@bot_discoverable = nil
|
|
76
|
+
@bot_creation_date = Time.at(bot_user_data&.[]('created_at').to_i / 1000) rescue nil
|
|
77
|
+
if @user_id.nil? || @bot_name.nil?
|
|
78
|
+
@logger.debug "Error: Essential properties (_id, username) missing or nil in API response from /users/@me."
|
|
79
|
+
@logger.debug "Please inspect the 'Response Body' above for unexpected format."
|
|
80
|
+
return false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
@logger.debug "Successfully identified as #{@bot_name} (ID: #{@user_id}) via REST API."
|
|
84
|
+
@logger.debug "Bot owner ID after parsing: #{@bot_owner_id}"
|
|
85
|
+
@logger.debug "Owner ID: #{@bot_owner_id}, Discoverable: #{@bot_discoverable.inspect}, Created: #{@bot_creation_date}"
|
|
86
|
+
else
|
|
87
|
+
@logger.debug "Initial REST API call failed: #{res.message} (Code: #{res.code})"
|
|
88
|
+
@logger.debug "Response Body: #{res.body}"
|
|
89
|
+
@logger.debug "Please check your bot token. Cannot proceed with WebSocket connection."
|
|
90
|
+
return false
|
|
91
|
+
end
|
|
92
|
+
# Step 2: Connect to the WebSocket
|
|
93
|
+
@running = true
|
|
94
|
+
connect_websocket
|
|
95
|
+
@request_queue.start_processing
|
|
96
|
+
true
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def connect_websocket
|
|
100
|
+
if @websocket && @websocket.open? && @running
|
|
101
|
+
@logger.debug "WebSocket already open and running."
|
|
102
|
+
return
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
@logger.debug "Connecting to WebSocket: #{@websocket_url}"
|
|
106
|
+
ws_url_with_token = "#{@websocket_url}?token=#{@token}"
|
|
107
|
+
bot_instance = self
|
|
108
|
+
thread_logger = @logger
|
|
109
|
+
|
|
110
|
+
@websocket_thread = Thread.new do
|
|
111
|
+
begin
|
|
112
|
+
@websocket = WebSocket::Client::Simple.connect ws_url_with_token
|
|
113
|
+
@websocket.on :open do
|
|
114
|
+
thread_logger.debug "WebSocket connection opened!"
|
|
115
|
+
end
|
|
116
|
+
@websocket.on :message do |msg|
|
|
117
|
+
bot_instance.handle_websocket_message(msg.data)
|
|
118
|
+
end
|
|
119
|
+
@websocket.on :close do |e|
|
|
120
|
+
close_code = e&.code || 'N/A'
|
|
121
|
+
close_reason = e&.reason || 'No reason provided'
|
|
122
|
+
thread_logger.debug "WebSocket closed: #{close_code} - #{close_reason}."
|
|
123
|
+
bot_instance.instance_variable_set(:@websocket, nil)
|
|
124
|
+
if bot_instance.instance_variable_get(:@running)
|
|
125
|
+
thread_logger.debug "Attempting to reconnect in 5 seconds..."
|
|
126
|
+
sleep 5
|
|
127
|
+
bot_instance.connect_websocket
|
|
128
|
+
else
|
|
129
|
+
thread_logger.debug "BOT: Bot has stopped and will not try to reconnect" # Use local thread_logger
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
@websocket.on :error do |e|
|
|
133
|
+
error_message = e&.message || 'Unknown error'
|
|
134
|
+
thread_logger.debug "WebSocket error: #{error_message}"
|
|
135
|
+
@websocket.close if @websocket&.open?
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
while bot_instance.instance_variable_get(:@running)
|
|
139
|
+
if Time.now.to_i - @last_heartbeat_sent > @heartbeat_interval
|
|
140
|
+
bot_instance.send_heartbeat
|
|
141
|
+
end
|
|
142
|
+
sleep 1
|
|
143
|
+
end
|
|
144
|
+
thread_logger.debug "WebSocket thread loop finished."
|
|
145
|
+
rescue => e
|
|
146
|
+
thread_logger.debug "WebSocket thread unhandled exception: #{e.message}"
|
|
147
|
+
thread_logger.debug e.backtrace.join("\n")
|
|
148
|
+
bot_instance.instance_variable_set(:@websocket, nil)
|
|
149
|
+
if bot_instance.instance_variable_get(:@running)
|
|
150
|
+
thread_logger.debug "Attempting to reconnect in 5 seconds due to unhandled error..."
|
|
151
|
+
sleep 5
|
|
152
|
+
bot_instance.connect_websocket
|
|
153
|
+
else
|
|
154
|
+
thread_logger.debug "Bot is stopped, not attempting to reconnect after unhandled error."
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
sleep 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def send_heartbeat
|
|
162
|
+
if @websocket && @websocket.open?
|
|
163
|
+
payload = { type: 'Ping', data: Time.now.to_i }
|
|
164
|
+
@websocket.send(payload.to_json)
|
|
165
|
+
@last_heartbeat_sent = Time.now.to_i
|
|
166
|
+
end
|
|
167
|
+
rescue OpenSSL::SSL::SSLError => e
|
|
168
|
+
@logger.debug "AN ERROR HAS OCCURED: Error sending heartbeat (SSL): #{e.message}"
|
|
169
|
+
@websocket&.close
|
|
170
|
+
rescue => e
|
|
171
|
+
@logger.debug "AN ERROR HAS OCCURED: Error sending heartbeat: #{e.message}"
|
|
172
|
+
@websocket&.close
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def handle_websocket_message(raw_data)
|
|
176
|
+
begin
|
|
177
|
+
event = JSON.parse(raw_data)
|
|
178
|
+
event_type = event['type']
|
|
179
|
+
case event_type
|
|
180
|
+
when 'Ready'
|
|
181
|
+
@logger.debug "Received 'Ready' event. Populating initial data..."
|
|
182
|
+
if event['servers']
|
|
183
|
+
event['servers'].each do |server_data|
|
|
184
|
+
@servers[server_data['_id']] = {
|
|
185
|
+
'name' => server_data['name'],
|
|
186
|
+
'id' => server_data['_id']
|
|
187
|
+
}
|
|
188
|
+
@logger.debug "Stored server ID from Ready event: #{server_data['_id'].inspect}" # New debug
|
|
189
|
+
end
|
|
190
|
+
@logger.debug "Loaded #{event['servers'].count} real servers from 'Ready' event."
|
|
191
|
+
else
|
|
192
|
+
@logger.debug "'Ready' event received but no 'servers' array found."
|
|
193
|
+
end
|
|
194
|
+
@ready_event_received = true
|
|
195
|
+
@logger.debug "@ready_event_received set to true."
|
|
196
|
+
when 'Message'
|
|
197
|
+
unless event['author'] == @user_id
|
|
198
|
+
process_message(event)
|
|
199
|
+
end
|
|
200
|
+
when 'Authenticated'
|
|
201
|
+
@logger.debug "Successfully authenticated with WebSocket."
|
|
202
|
+
when 'Pong'
|
|
203
|
+
# @logger.debug "Received Pong response."
|
|
204
|
+
when 'Error'
|
|
205
|
+
@logger.debug "AN ERROR HAS OCCURED: Revolt API Error received via WebSocket: #{event['error']}"
|
|
206
|
+
else
|
|
207
|
+
# @logger.debug "AN ERROR HAS OCCURED: Unhandled WebSocket event type: #{event_type}"
|
|
208
|
+
end
|
|
209
|
+
rescue JSON::ParserError => e
|
|
210
|
+
@logger.debug "Failed to parse WebSocket message as JSON: #{e.message}"
|
|
211
|
+
@logger.debug "Raw message: #{raw_data}"
|
|
212
|
+
rescue => e
|
|
213
|
+
@logger.debug "Error processing WebSocket message: #{e.message}"
|
|
214
|
+
@logger.debug e.backtrace.join("\n")
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def on_message(&block)
|
|
219
|
+
@message_handlers << block
|
|
220
|
+
end
|
|
221
|
+
def command(command_name, required_permissions: [], nsfw_channel_required: false, &block)
|
|
222
|
+
cmd_key = command_name.to_s.downcase
|
|
223
|
+
@commands[cmd_key] = {
|
|
224
|
+
'block' => block,
|
|
225
|
+
'permissions' => required_permissions,
|
|
226
|
+
'nsfw_cmd' => nsfw_channel_required
|
|
227
|
+
}
|
|
228
|
+
@logger.debug "Command '#{command_name}' registered with permissions: #{required_permissions.inspect}, NSFW required: #{nsfw_channel_required}."
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def check_permissions(message, required_permissions, nsfw_channel_required)
|
|
232
|
+
@logger.debug "check_permissions called with required_permissions: #{required_permissions.inspect}, NSFW required: #{nsfw_channel_required}."
|
|
233
|
+
@logger.debug "message['channel'] value: #{message['channel'].inspect}"
|
|
234
|
+
|
|
235
|
+
server_id_from_message = message['member']&.[]('_id')&.[]('server')
|
|
236
|
+
@logger.debug "message['member']['_id']['server'] value: #{server_id_from_message.inspect}"
|
|
237
|
+
|
|
238
|
+
if required_permissions.empty? && !nsfw_channel_required
|
|
239
|
+
@logger.debug "Permission Check: No specific permissions or NSFW requirement. Allowing command."
|
|
240
|
+
return true
|
|
241
|
+
end
|
|
242
|
+
user_id = message['author']
|
|
243
|
+
|
|
244
|
+
@logger.debug "\n--- PERMISSION DEBUG START ---"
|
|
245
|
+
@logger.debug "User ID from message (user_id): '#{user_id}' | Bot Owner ID stored (@bot_owner_id): '#{@bot_owner_id}'"
|
|
246
|
+
@logger.debug "Are they equal? (user_id == @bot_owner_id): #{user_id == @bot_owner_id}"
|
|
247
|
+
# @logger.debug "User ID char codes: #{user_id.each_char.map(&:ord).join(', ')}"
|
|
248
|
+
# @logger.debug "Bot Owner ID char codes: #{@bot_owner_id.each_char.map(&:ord).join(', ')}"
|
|
249
|
+
@logger.debug "--- PERMISSION DEBUG END ---\n"
|
|
250
|
+
# --- BotOwner Check ---
|
|
251
|
+
if required_permissions.include?('BotOwner')
|
|
252
|
+
if user_id == @bot_owner_id
|
|
253
|
+
@logger.debug "Permission Check: User is the bot owner. Allowing command."
|
|
254
|
+
return true
|
|
255
|
+
else
|
|
256
|
+
@logger.debug "Permission Check: User is NOT the bot owner. Denying command."
|
|
257
|
+
return false
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
# --- NSFW Channel Check ---
|
|
261
|
+
channel_id = message['channel']
|
|
262
|
+
if server_id_from_message.nil?
|
|
263
|
+
if nsfw_channel_required
|
|
264
|
+
@logger.debug "Permission Check: Command requires NSFW channel, but message is in DM. Denying command."
|
|
265
|
+
return false
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
channel_details = get_channel_details(channel_id)
|
|
269
|
+
if channel_details.nil?
|
|
270
|
+
@logger.debug "Permission Check: Command requires NSFW channel but could not retrieve channel details. Denying command to be safe."
|
|
271
|
+
return false
|
|
272
|
+
end
|
|
273
|
+
is_channel_nsfw = channel_details['nsfw'] || false
|
|
274
|
+
if nsfw_channel_required && !is_channel_nsfw
|
|
275
|
+
@logger.debug "Permission Check: Command requires NSFW channel, but current channel is NOT NSFW marked. Denying."
|
|
276
|
+
return false
|
|
277
|
+
elsif !nsfw_channel_required && is_channel_nsfw
|
|
278
|
+
@logger.debug "Permission Check: Command does not require NSFW channel, but is in NSFW marked channel. Allowing."
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
if !required_permissions.empty?
|
|
283
|
+
@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)."
|
|
284
|
+
return false
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
@logger.debug "Permission Check: All checks passed. Allowing command."
|
|
288
|
+
true
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def process_message(message)
|
|
292
|
+
unless @ready_event_received
|
|
293
|
+
@logger.debug "AN ERROR HAS OCCURED: Bot is not ready for commands"
|
|
294
|
+
return
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
unless @selfbot
|
|
298
|
+
return if message['author'] == @user_id
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
content = message['content']&.strip
|
|
302
|
+
return if content.nil? || content.empty?
|
|
303
|
+
@logger.debug "Full Message object received in process_message: #{message.inspect}"
|
|
304
|
+
|
|
305
|
+
@commands.each do |cmd_name, cmd_data|
|
|
306
|
+
command_full_string = "#{@prefix}#{cmd_name}"
|
|
307
|
+
if content.downcase.start_with?(command_full_string.downcase)
|
|
308
|
+
args_string = content[command_full_string.length..]&.strip
|
|
309
|
+
args = args_string.to_s.split(/\s+/)
|
|
310
|
+
args = [] if args == ['']
|
|
311
|
+
# --- Permission Check (Pass both permissions and nsfw_channel_required to check_permissions) ---
|
|
312
|
+
if check_permissions(message, cmd_data['permissions'], cmd_data['nsfw_cmd'])
|
|
313
|
+
@logger.debug "Executing command: '#{cmd_name}' with args: #{args.inspect}"
|
|
314
|
+
cmd_data['block'].call(message, args)
|
|
315
|
+
else
|
|
316
|
+
channel_id = message['channel']
|
|
317
|
+
if cmd_data['nsfw_cmd'] && message['member']
|
|
318
|
+
channel_details = get_channel_details(channel_id)
|
|
319
|
+
if channel_details && !channel_details['nsfw']
|
|
320
|
+
self.send_message(channel_id, text: "⛔ A NSFW marked channel is required to use the following command: `#{cmd_name}`")
|
|
321
|
+
else
|
|
322
|
+
self.send_message(channel_id, text: "⛔ You don't have permission to use the `#{cmd_name}` command.")
|
|
323
|
+
end
|
|
324
|
+
else
|
|
325
|
+
self.send_message(channel_id, text: "⛔ You don't have permission to use the `#{cmd_name}` command.")
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
return
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
@message_handlers.each do |handler|
|
|
333
|
+
@logger.debug "Calling general message handler for: '#{content}'"
|
|
334
|
+
handler.call(message)
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def send_message(channel_id, text: nil, embeds: nil, masquerade_name: nil, masquerade_avatar_url: nil)
|
|
339
|
+
if text.nil? && (embeds.nil? || embeds.empty?)
|
|
340
|
+
@logger.debug "AN ERROR HAS OCCURED: Cannot send empty message or embeds."
|
|
341
|
+
return
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
payload = {}
|
|
345
|
+
payload[:content] = text if text
|
|
346
|
+
|
|
347
|
+
if embeds && !embeds.empty?
|
|
348
|
+
filtered_embeds = embeds.map do |embed|
|
|
349
|
+
supported_keys = ['title', 'description', 'color', 'url', 'image']
|
|
350
|
+
embed.select { |k, v| supported_keys.include?(k.to_s) }
|
|
351
|
+
end
|
|
352
|
+
payload[:embeds] = filtered_embeds
|
|
353
|
+
end
|
|
354
|
+
if masquerade_name || masquerade_avatar_url
|
|
355
|
+
payload[:masquerade] = {}
|
|
356
|
+
payload[:masquerade][:name] = masquerade_name if masquerade_name
|
|
357
|
+
payload[:masquerade][:avatar] = masquerade_avatar_url if masquerade_avatar_url
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
@request_queue.enqueue do
|
|
361
|
+
@logger.debug "Attempting to send message to channel '#{channel_id}' via queue..."
|
|
362
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages")
|
|
363
|
+
req = Net::HTTP::Post.new(uri)
|
|
364
|
+
_add_auth_header(req)
|
|
365
|
+
req['Content-Type'] = 'application/json'
|
|
366
|
+
req.body = payload.to_json
|
|
367
|
+
|
|
368
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
369
|
+
http.request(req)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
373
|
+
@logger.debug "Message sent successfully!"
|
|
374
|
+
else
|
|
375
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to send message: #{res.message} (Code: #{res.code})"
|
|
376
|
+
@logger.debug "Response Body: #{res.body}"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
rescue => e
|
|
380
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing message: #{e.message}"
|
|
381
|
+
@logger.debug e.backtrace.join("\n")
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def add_reaction(channel_id, message_id, emoji_id)
|
|
386
|
+
@request_queue.enqueue do
|
|
387
|
+
@logger.debug "Attempting to add reaction '#{emoji_id}' to message '#{message_id}' in channel '#{channel_id}' via queue."
|
|
388
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages/#{message_id}/reactions/#{emoji_id}")
|
|
389
|
+
req = Net::HTTP::Put.new(uri) # Reactions use PUT
|
|
390
|
+
req['x-bot-token'] = @token
|
|
391
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
392
|
+
http.request(req)
|
|
393
|
+
end
|
|
394
|
+
if res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
395
|
+
@logger.debug "Reaction has been added successfully!"
|
|
396
|
+
else
|
|
397
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to add reaction: #{res.message} (Code: #{res.code})"
|
|
398
|
+
@logger.debug "Response Body: #{res.body}"
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
rescue => e
|
|
402
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing add reaction: #{e.message}"
|
|
403
|
+
@logger.debug e.backtrace.join("\n")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def remove_reaction(channel_id, message_id, emoji_id, user_id: nil)
|
|
407
|
+
target_user_id = user_id || @user_id # Default to bot's own ID
|
|
408
|
+
@request_queue.enqueue do
|
|
409
|
+
@logger.debug "Attempting to remove reaction '#{emoji_id}' from message '#{message_id}' by user '#{target_user_id}' in channel '#{channel_id}' via queue."
|
|
410
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/messages/#{message_id}/reactions/#{emoji_id}?user_id=#{target_user_id}")
|
|
411
|
+
req = Net::HTTP::Delete.new(uri)
|
|
412
|
+
req['x-bot-token'] = @token
|
|
413
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
414
|
+
http.request(req)
|
|
415
|
+
end
|
|
416
|
+
if res.is_a?(Net::HTTPSuccess) || res.code == '204'
|
|
417
|
+
@logger.debug "Reaction has been removed successfully!"
|
|
418
|
+
else
|
|
419
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to remove reaction: #{res.message} (Code: #{res.code})"
|
|
420
|
+
@logger.debug "Response Body: #{res.body}"
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
rescue => e
|
|
424
|
+
@logger.debug "AN ERROR HAS OCCURED: Error enqueuing remove reaction: #{e.message}"
|
|
425
|
+
@logger.debug e.backtrace.join("\n")
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def get_channel_details(channel_id)
|
|
429
|
+
@logger.debug "Fetching channel details for ID: #{channel_id} (direct API call for permission check)."
|
|
430
|
+
uri = URI("#{@api_url}/channels/#{channel_id}")
|
|
431
|
+
req = Net::HTTP::Get.new(uri)
|
|
432
|
+
req['x-bot-token'] = @token
|
|
433
|
+
|
|
434
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
435
|
+
http.request(req)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
if res.is_a?(Net::HTTPSuccess)
|
|
439
|
+
JSON.parse(res.body)
|
|
440
|
+
else
|
|
441
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to fetch channel details for #{channel_id}: #{res.message} (Code: #{res.code})"
|
|
442
|
+
nil
|
|
443
|
+
end
|
|
444
|
+
rescue => e
|
|
445
|
+
@logger.debug "AN ERROR HAS OCCURED: Error fetching channel details: #{e.message}"
|
|
446
|
+
nil
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def get_server_info(server_id)
|
|
450
|
+
@logger.debug "get_server_info called with server_id: '#{server_id}' (Type: #{server_id.class}, Length: #{server_id.length})"
|
|
451
|
+
@logger.debug "Available server IDs in cache (@servers.keys): #{@servers.keys.inspect}"
|
|
452
|
+
found_server = @servers[server_id]
|
|
453
|
+
@logger.debug "Result of @servers[server_id]: #{found_server.inspect}"
|
|
454
|
+
found_server
|
|
455
|
+
end
|
|
456
|
+
def get_server_name(server_id)
|
|
457
|
+
@servers[server_id]&.[]('name')
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def stop
|
|
461
|
+
@logger.debug "Stopping bot..."
|
|
462
|
+
@running = false
|
|
463
|
+
if @websocket_thread && @websocket_thread.alive?
|
|
464
|
+
unless @websocket_thread.join(5)
|
|
465
|
+
@logger.debug "WebSocket thread did not terminate gracefully, forcing kill."
|
|
466
|
+
@websocket_thread.kill
|
|
467
|
+
end
|
|
468
|
+
@logger.debug "WebSocket thread terminated."
|
|
469
|
+
end
|
|
470
|
+
if @websocket && @websocket.open?
|
|
471
|
+
@websocket.close
|
|
472
|
+
@logger.debug "WebSocket closed."
|
|
473
|
+
end
|
|
474
|
+
@request_queue.stop_processing
|
|
475
|
+
@logger.debug "Bot stopped."
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
private
|
|
479
|
+
|
|
480
|
+
def _add_auth_header(request)
|
|
481
|
+
if @selfbot
|
|
482
|
+
request['x-session-token'] = @token
|
|
483
|
+
else
|
|
484
|
+
request['x-bot-token'] = @token
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# lib/revoltrb/request_queue.rb
|
|
2
|
+
|
|
3
|
+
require 'thread'
|
|
4
|
+
|
|
5
|
+
module Revoltrb
|
|
6
|
+
class RequestQueue
|
|
7
|
+
def initialize(delay_between_requests_ms = 100)
|
|
8
|
+
@queue = []
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
@condition = ConditionVariable.new
|
|
11
|
+
@processing_thread = nil
|
|
12
|
+
@running = false
|
|
13
|
+
@delay_between_requests = delay_between_requests_ms / 1000.0 # Convert ms to seconds
|
|
14
|
+
@logger = Revoltrb::DebugLogger.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def enqueue(&block)
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
@queue << block
|
|
20
|
+
@condition.signal
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def start_processing
|
|
25
|
+
return if @running
|
|
26
|
+
|
|
27
|
+
@running = true
|
|
28
|
+
@processing_thread = Thread.new do
|
|
29
|
+
@logger.debug "RequestQueue processing thread started."
|
|
30
|
+
while @running
|
|
31
|
+
request = nil
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@condition.wait(@mutex) if @queue.empty?
|
|
34
|
+
request = @queue.shift unless @queue.empty?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
if request
|
|
38
|
+
begin
|
|
39
|
+
request.call
|
|
40
|
+
sleep @delay_between_requests
|
|
41
|
+
rescue => e
|
|
42
|
+
@logger.debug "AN ERROR HAS OCCURED: Error processing queued request: #{e.message}"
|
|
43
|
+
@logger.debug e.backtrace.join("\n")
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
@logger.debug "RequestQueue processing thread stopped."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def stop_processing
|
|
52
|
+
@running = false
|
|
53
|
+
@mutex.synchronize do
|
|
54
|
+
@condition.signal
|
|
55
|
+
end
|
|
56
|
+
@processing_thread.join if @processing_thread&.alive?
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/revoltrb.rb
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
require_relative 'revoltrb/bot'
|
|
2
|
+
require_relative 'revoltrb/version'
|
|
3
|
+
require_relative 'revoltrb/debuglogger'
|
|
4
|
+
require_relative 'revoltrb/request_queue'
|
|
5
|
+
|
|
6
|
+
module Revoltrb
|
|
7
|
+
# This module can serve as a namespace for the gem's classes. For now, it's a direct require.
|
|
8
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: revoltrb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Roxanne Studios
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: net-http
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: thread
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: parser
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
description: The first Ruby package (a.k.a. gem) to exist for making Revolt.chat bots.
|
|
69
|
+
This project is not officially endorsed by revolt.chat
|
|
70
|
+
email:
|
|
71
|
+
- ''
|
|
72
|
+
executables: []
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- README.md
|
|
77
|
+
- lib/revoltrb.rb
|
|
78
|
+
- lib/revoltrb/bot.rb
|
|
79
|
+
- lib/revoltrb/debuglogger.rb
|
|
80
|
+
- lib/revoltrb/request_queue.rb
|
|
81
|
+
- lib/revoltrb/version.rb
|
|
82
|
+
homepage: https://gitlab.com/roxannewolf/revoltrb
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
85
|
+
metadata: {}
|
|
86
|
+
rdoc_options: []
|
|
87
|
+
require_paths:
|
|
88
|
+
- lib
|
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '3.0'
|
|
94
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
95
|
+
requirements:
|
|
96
|
+
- - ">="
|
|
97
|
+
- !ruby/object:Gem::Version
|
|
98
|
+
version: '0'
|
|
99
|
+
requirements: []
|
|
100
|
+
rubygems_version: 3.6.9
|
|
101
|
+
specification_version: 4
|
|
102
|
+
summary: Ruby gem for Revolt.chat bots
|
|
103
|
+
test_files: []
|