stoatrb 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +95 -0
- data/lib/stoatrb/bot.rb +589 -0
- data/lib/stoatrb/debuglogger.rb +13 -0
- data/lib/stoatrb/request_queue.rb +59 -0
- data/lib/stoatrb/version.rb +6 -0
- data/lib/stoatrb/webhooks.rb +104 -0
- data/lib/stoatrb.rb +14 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d34d4d67e82ebfa41c47ace5c40ed7e07cef241bc66c86da2eae9267840946c3
|
4
|
+
data.tar.gz: 6c7fad3f522ca70369b4d8f292caa6535de5613a509abd6d08986d04315ac23a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7a11ed7db6ed626fe6f587ce08d29ad1f10ca0c69524d4ba335c54f273a89c4eea112bb84d3be81d69a282df9f3c716a7420c18242a0d0ac1933a6c9743cfc94
|
7
|
+
data.tar.gz: 1bd193a7980d36125fc1029fc3617cce06fb1ecd20ae303c9071fbbc39ece814dc183fdb2e86387825acdb84bc2d0fe359fe9a13fb0cbb43a6df3a8814da9023
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Roxanne Wolf
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
# stoatrb
|
2
|
+
|
3
|
+
Stoatrb is a Ruby package (a.k.a. Gem) that allows you to make stoat.chat bots and use stoat.chat webhooks using the Ruby programming language. This package (a.k.a. Gem) is not officially endorsed by stoat.chat amd this is not an official stoat.chat product.
|
4
|
+
|
5
|
+
You need Ruby 3.0 or newer in order to use this package (Ruby 3.2 or newer is recommended)
|
6
|
+
|
7
|
+
> [!NOTE]
|
8
|
+
> This package (a.k.a. Gem) is in an alpha state so expect things to be buggy and/or broken.
|
9
|
+
|
10
|
+
## ToDo
|
11
|
+
|
12
|
+
This list contains a list of things that I know is broken and gotta fix. Contributing will be super helpful.
|
13
|
+
|
14
|
+
- Fix reactions support
|
15
|
+
|
16
|
+
## Setup
|
17
|
+
|
18
|
+
You can install Revoltrb through the following methods:
|
19
|
+
|
20
|
+
#### Method 1: Install from Gemfile
|
21
|
+
|
22
|
+
Add the following to your Gemfile file and run the "[bundle install](https://rubygems.org/gems/stoatrb)" command:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
gem 'revoltrb'
|
26
|
+
```
|
27
|
+
|
28
|
+
or add the following to your Gemfile file.
|
29
|
+
|
30
|
+
```ruby
|
31
|
+
gem 'revoltrb', git: 'https://gitlab.com/roxannewolf/stoatrb'
|
32
|
+
```
|
33
|
+
|
34
|
+
#### Troubleshooting
|
35
|
+
|
36
|
+
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)
|
37
|
+
|
38
|
+
## Usage
|
39
|
+
|
40
|
+
You can make a simple bot like this:
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
require 'stoatrb'
|
44
|
+
|
45
|
+
bot = Stoatrb::StoatBot.new('REVOLT_BOT_TOKEN_HERE')
|
46
|
+
|
47
|
+
bot.on_message do |message|
|
48
|
+
next if message['author'] == bot.user_id
|
49
|
+
content = message['content']&.downcase
|
50
|
+
channel_id = message['channel']
|
51
|
+
|
52
|
+
if content.include?("ping")
|
53
|
+
bot.send_message(channel_id, text: "Pong!")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
bot.run
|
58
|
+
```
|
59
|
+
|
60
|
+
or you can make a bot with full prefix commands like this:
|
61
|
+
|
62
|
+
```ruby
|
63
|
+
require 'stoatrb'
|
64
|
+
|
65
|
+
bot = Stoatrb::StoatBot.new('REVOLT_BOT_TOKEN_HERE', prefix: '!')
|
66
|
+
|
67
|
+
bot.command(:ping) do |message, args|
|
68
|
+
channel_id = message['channel']
|
69
|
+
bot.send_message(channel_id, text: "Pong! You sent: #{args.join(' ')}")
|
70
|
+
end
|
71
|
+
|
72
|
+
bot.run
|
73
|
+
```
|
74
|
+
|
75
|
+
Webhook example:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
require 'stoatrb'
|
79
|
+
# Webhook url should look like this: https://stoat.chat/api/webhooks/<WEBHOOK_ID>/<WEBHOOK_TOKEN>
|
80
|
+
WEBHOOK_ID = ''
|
81
|
+
WEBHOOK_TOKEN = ''
|
82
|
+
webhook = StoatWebhooks::Webhook.new(WEBHOOK_ID, WEBHOOK_TOKEN)
|
83
|
+
|
84
|
+
webhook.send_message(
|
85
|
+
content: "Stoatrb webhook message content",
|
86
|
+
)
|
87
|
+
```
|
88
|
+
|
89
|
+
## Support and Help
|
90
|
+
|
91
|
+
If you need help with this ruby package (a.k.a. Gem), feel free to join the [Roxanne Studios Stoat.chat Server](http://stt.gg/r4Ee2R1Z) and use the STOATRB category to talk about this package.
|
92
|
+
|
93
|
+
## Contributing
|
94
|
+
|
95
|
+
We are working to support more of Stoat.chat'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 Stoat.chat API. Opening issues and Pull requests are welcome at our [Gitlab repo](https://gitlab.com/roxannewolf/stoatrb) and [Codeberg repo](https://codeberg.org/roxannewolf/stoatrb)
|
data/lib/stoatrb/bot.rb
ADDED
@@ -0,0 +1,589 @@
|
|
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
|
+
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.stoat.chat/0.8', websocket_url: 'wss://events.stoat.chat', cdn_url: 'https://cdn.stoatusercontent.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 = Stoatrb::DebugLogger.new(debuglogs)
|
50
|
+
@request_queue = Stoatrb::RequestQueue.new(500)
|
51
|
+
@webhooks = Stoatrb::Webhooks.new(api_url, @logger, token, selfbot)
|
52
|
+
|
53
|
+
@prefix = "!"
|
54
|
+
@prefix = prefix if prefix
|
55
|
+
@selfbot = selfbot
|
56
|
+
@logger.debug "Stoat.chat Bot 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 Stoat.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: Stoat.chat 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
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# lib/stoatrb/request_queue.rb
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
module Stoatrb
|
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
|
14
|
+
@logger = Stoatrb::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
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# lib/stoatrb/webhooks.rb
|
2
|
+
require 'json'
|
3
|
+
require 'net/http'
|
4
|
+
require 'uri'
|
5
|
+
|
6
|
+
module Stoatrb
|
7
|
+
class Webhooks
|
8
|
+
attr_reader :api_url, :logger, :token, :selfbot
|
9
|
+
def initialize(api_url, logger, token, selfbot)
|
10
|
+
@api_url = api_url
|
11
|
+
@logger = logger
|
12
|
+
@token = token
|
13
|
+
@selfbot = selfbot
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_webhook(channel_id, name, avatar_url: nil)
|
17
|
+
@logger.debug "Attempting to create a webhook for channel '#{channel_id}' with name '#{name}'."
|
18
|
+
|
19
|
+
uri = URI("#{@api_url}/channels/#{channel_id}/webhooks")
|
20
|
+
req = Net::HTTP::Post.new(uri)
|
21
|
+
if @selfbot
|
22
|
+
req['Authorization'] = @token
|
23
|
+
else
|
24
|
+
req['x-bot-token'] = @token
|
25
|
+
end
|
26
|
+
req['Content-Type'] = 'application/json'
|
27
|
+
|
28
|
+
payload = {
|
29
|
+
name: name
|
30
|
+
}
|
31
|
+
payload[:avatar] = avatar_url unless avatar_url.nil?
|
32
|
+
req.body = payload.to_json
|
33
|
+
|
34
|
+
res = nil
|
35
|
+
begin
|
36
|
+
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https', read_timeout: 10, open_timeout: 10) do |http|
|
37
|
+
http.request(req)
|
38
|
+
end
|
39
|
+
rescue Net::ReadTimeout => e
|
40
|
+
@logger.debug "AN ERROR HAS OCCURED: Network request timed out while reading: #{e.message}"
|
41
|
+
return nil
|
42
|
+
rescue Net::OpenTimeout => e
|
43
|
+
@logger.debug "AN ERROR HAS OCCURED: Network request timed out while connecting: #{e.message}"
|
44
|
+
return nil
|
45
|
+
rescue => e
|
46
|
+
@logger.debug "AN ERROR HAS OCCURED: An unexpected error occurred while creating the webhook: #{e.message}"
|
47
|
+
return nil
|
48
|
+
end
|
49
|
+
|
50
|
+
if res.is_a?(Net::HTTPSuccess)
|
51
|
+
webhook_data = JSON.parse(res.body)
|
52
|
+
@logger.debug "Webhook created successfully! Details: #{res.body}"
|
53
|
+
@logger.debug "Webhook Name: #{webhook_data['name']} | Webhook ID: #{webhook_data['id']}"
|
54
|
+
@logger.debug "Webhook Channel ID: #{webhook_data['channel_id']}"
|
55
|
+
return webhook_data
|
56
|
+
else
|
57
|
+
@logger.debug "AN ERROR HAS OCCURED: Failed to create webhook. (Code: #{res.code}) Response: #{res.message}"
|
58
|
+
@logger.debug "Response Body: #{res.body}"
|
59
|
+
return nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
module StoatWebhooks
|
66
|
+
class Webhook
|
67
|
+
DEFAULT_API_URL = 'https://stoat.chat/api'.freeze
|
68
|
+
def initialize(webhook_id, webhook_token, api_url: DEFAULT_API_URL)
|
69
|
+
@webhook_id = webhook_id
|
70
|
+
@webhook_token = webhook_token
|
71
|
+
@api_url = api_url
|
72
|
+
@uri = URI("#{@api_url}/webhooks/#{@webhook_id}/#{@webhook_token}")
|
73
|
+
end
|
74
|
+
|
75
|
+
def send_message(content:, embeds: nil)
|
76
|
+
payload = {
|
77
|
+
content: content
|
78
|
+
}
|
79
|
+
|
80
|
+
if embeds
|
81
|
+
payload[:embeds] = Array(embeds)
|
82
|
+
end
|
83
|
+
|
84
|
+
http = Net::HTTP.new(@uri.host, @uri.port)
|
85
|
+
http.use_ssl = true
|
86
|
+
request = Net::HTTP::Post.new(@uri.path, 'Content-Type' => 'application/json')
|
87
|
+
request.body = payload.to_json
|
88
|
+
|
89
|
+
begin
|
90
|
+
response = http.request(request)
|
91
|
+
if response.code.to_i == 204 || response.code.to_i == 200
|
92
|
+
return true
|
93
|
+
else
|
94
|
+
puts "[Stoatrb Webhooks] AN ERROR HAS OCCURED: Status code: #{response.code}"
|
95
|
+
puts "[Stoatrb Webhooks] Response body: #{response.body}"
|
96
|
+
return false
|
97
|
+
end
|
98
|
+
rescue StandardError => e
|
99
|
+
puts "[Stoatrb Webhooks] AN ERROR HAS OCCURED: #{e.message}"
|
100
|
+
return false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
data/lib/stoatrb.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# lib/stoatrb.rb
|
2
|
+
require_relative 'stoatrb/bot'
|
3
|
+
require_relative 'stoatrb/version'
|
4
|
+
require_relative 'stoatrb/debuglogger'
|
5
|
+
require_relative 'stoatrb/request_queue'
|
6
|
+
require_relative 'stoatrb/webhooks'
|
7
|
+
|
8
|
+
module Stoatrb
|
9
|
+
# This module can serve as a namespace for the gem's classes. For now, it's a direct require.
|
10
|
+
end
|
11
|
+
|
12
|
+
module StoatWebhooks
|
13
|
+
# This module can serve as a namespace for the gem's classes. For now, it's a direct require.
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: stoatrb
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
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: 2.13.2
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: 2.13.2
|
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.6.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.6.0
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: websocket-client-simple
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 0.9.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.9.0
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: thread
|
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
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: parser
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: 3.3.9.0
|
75
|
+
type: :runtime
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: 3.3.9.0
|
82
|
+
description: The first Ruby package (a.k.a. gem) to exist for making Stoat.chat bots
|
83
|
+
and using Stoat.chat webhooks. This project is not officially endorsed by stoat.chat
|
84
|
+
email:
|
85
|
+
- ''
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- lib/stoatrb.rb
|
93
|
+
- lib/stoatrb/bot.rb
|
94
|
+
- lib/stoatrb/debuglogger.rb
|
95
|
+
- lib/stoatrb/request_queue.rb
|
96
|
+
- lib/stoatrb/version.rb
|
97
|
+
- lib/stoatrb/webhooks.rb
|
98
|
+
homepage: https://gitlab.com/roxannewolf/stoatrb
|
99
|
+
licenses:
|
100
|
+
- MIT
|
101
|
+
metadata: {}
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubygems_version: 3.6.9
|
117
|
+
specification_version: 4
|
118
|
+
summary: Ruby gem for making stoat.chat bots
|
119
|
+
test_files: []
|