brainiac 0.0.9 → 0.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +2 -11
- data/bin/brainiac +1039 -316
- data/bin/brainiac-completion.bash +93 -18
- data/brainiac.gemspec +2 -2
- data/lib/brainiac/config.rb +9 -21
- data/lib/brainiac/cron.rb +41 -26
- data/lib/brainiac/handlers/github.rb +3 -28
- data/lib/brainiac/handlers/zoho.rb +25 -33
- data/lib/brainiac/helpers.rb +6 -16
- data/lib/brainiac/notifications.rb +96 -0
- data/lib/brainiac/prompts.rb +2 -102
- data/lib/brainiac/restart.rb +2 -10
- data/lib/brainiac/routes/api.rb +2 -14
- data/lib/brainiac/version.rb +1 -1
- data/receiver.rb +9 -60
- data/skills/brainiac-plugins/SKILL.md +220 -0
- metadata +5 -25
- data/lib/brainiac/handlers/discord/api.rb +0 -196
- data/lib/brainiac/handlers/discord/config.rb +0 -134
- data/lib/brainiac/handlers/discord/delivery.rb +0 -196
- data/lib/brainiac/handlers/discord/gateway.rb +0 -212
- data/lib/brainiac/handlers/discord/message.rb +0 -921
- data/lib/brainiac/handlers/discord/reactions.rb +0 -215
- data/lib/brainiac/handlers/discord.rb +0 -24
- data/templates/discord.json.example +0 -17
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Discord WebSocket gateway connections.
|
|
4
|
-
#
|
|
5
|
-
# Each agent with a DISCORD_BOT_TOKEN gets its own persistent WebSocket
|
|
6
|
-
# connection. The gateway dispatches MESSAGE_CREATE, MESSAGE_UPDATE,
|
|
7
|
-
# and MESSAGE_REACTION_ADD events to handler functions.
|
|
8
|
-
|
|
9
|
-
DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json"
|
|
10
|
-
|
|
11
|
-
# Per-bot state: { agent_key => { token:, user_id:, status:, thread: } }
|
|
12
|
-
DISCORD_BOTS = {}
|
|
13
|
-
DISCORD_BOTS_MUTEX = Mutex.new
|
|
14
|
-
DISCORD_ALL_READY_LOGGED = { done: false }
|
|
15
|
-
|
|
16
|
-
def start_discord_gateway_for(agent_key, bot_token)
|
|
17
|
-
Thread.new do
|
|
18
|
-
agent_display = agent_display_name(agent_key) || agent_key.capitalize
|
|
19
|
-
bot_user_id = nil
|
|
20
|
-
|
|
21
|
-
loop do
|
|
22
|
-
bot_user_id = run_gateway_connection(agent_key, agent_display, bot_token, bot_user_id)
|
|
23
|
-
rescue StandardError => e
|
|
24
|
-
DISCORD_BOTS_MUTEX.synchronize do
|
|
25
|
-
DISCORD_BOTS[agent_key][:status] = "error" if DISCORD_BOTS[agent_key]
|
|
26
|
-
end
|
|
27
|
-
LOG.error "[Discord:#{agent_display}] Gateway error: #{e.message}, reconnecting in 5s..."
|
|
28
|
-
sleep 5
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def run_gateway_connection(agent_key, agent_display, bot_token, bot_user_id)
|
|
34
|
-
DISCORD_BOTS_MUTEX.synchronize do
|
|
35
|
-
DISCORD_BOTS[agent_key] ||= {}
|
|
36
|
-
DISCORD_BOTS[agent_key][:status] = "connecting"
|
|
37
|
-
DISCORD_BOTS[agent_key][:token] = bot_token
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
LOG.debug "[Discord:#{agent_display}] Connecting to Gateway..."
|
|
41
|
-
heartbeat_thread = nil
|
|
42
|
-
last_sequence = nil
|
|
43
|
-
ws = WebSocket::Client::Simple.connect(DISCORD_GATEWAY_URL)
|
|
44
|
-
|
|
45
|
-
ws.on :message do |msg|
|
|
46
|
-
next if msg.data.nil? || msg.data.empty?
|
|
47
|
-
|
|
48
|
-
payload = JSON.parse(msg.data)
|
|
49
|
-
last_sequence = payload["s"] if payload["s"]
|
|
50
|
-
heartbeat_thread, bot_user_id = handle_gateway_op(
|
|
51
|
-
ws, payload, agent_key, agent_display, bot_token, bot_user_id, heartbeat_thread, last_sequence
|
|
52
|
-
)
|
|
53
|
-
rescue StandardError => e
|
|
54
|
-
LOG.error "[Discord:#{agent_display}] Gateway message error: #{e.message}"
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
ws.on :open do
|
|
58
|
-
LOG.debug "[Discord:#{agent_display}] WebSocket connected"
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
ws.on :close do |e|
|
|
62
|
-
DISCORD_BOTS_MUTEX.synchronize do
|
|
63
|
-
DISCORD_BOTS[agent_key][:status] = "disconnected" if DISCORD_BOTS[agent_key]
|
|
64
|
-
end
|
|
65
|
-
LOG.warn "[Discord:#{agent_display}] WebSocket closed: #{e&.inspect}"
|
|
66
|
-
heartbeat_thread&.kill
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
ws.on :error do |e|
|
|
70
|
-
LOG.error "[Discord:#{agent_display}] WebSocket error: #{e.message}"
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
wait_for_disconnect(ws, agent_display)
|
|
74
|
-
bot_user_id
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def wait_for_disconnect(websocket, agent_display)
|
|
78
|
-
loop do
|
|
79
|
-
sleep 1
|
|
80
|
-
next if websocket.open?
|
|
81
|
-
|
|
82
|
-
LOG.info "[Discord:#{agent_display}] Connection lost, reconnecting in 5s..."
|
|
83
|
-
sleep 5
|
|
84
|
-
break
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Handle a single gateway opcode. Returns [heartbeat_thread, bot_user_id].
|
|
89
|
-
def handle_gateway_op(websocket, payload, agent_key, agent_display, bot_token, bot_user_id, heartbeat_thread, last_sequence)
|
|
90
|
-
op = payload["op"]
|
|
91
|
-
data = payload["d"]
|
|
92
|
-
|
|
93
|
-
case op
|
|
94
|
-
when 10
|
|
95
|
-
heartbeat_thread = start_gateway_heartbeat(websocket, data["heartbeat_interval"], agent_display, last_sequence)
|
|
96
|
-
send_identify(websocket, bot_token, agent_display)
|
|
97
|
-
when 0
|
|
98
|
-
bot_user_id = handle_gateway_dispatch(payload, data, agent_key, agent_display, bot_token, bot_user_id)
|
|
99
|
-
when 1
|
|
100
|
-
websocket.send({ op: 1, d: last_sequence }.to_json)
|
|
101
|
-
when 7
|
|
102
|
-
LOG.info "[Discord:#{agent_display}] Reconnect requested"
|
|
103
|
-
websocket.close
|
|
104
|
-
when 9
|
|
105
|
-
LOG.warn "[Discord:#{agent_display}] Invalid session, re-identifying in 5s"
|
|
106
|
-
sleep 5
|
|
107
|
-
send_identify(websocket, bot_token, agent_display)
|
|
108
|
-
when 11 then nil
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
[heartbeat_thread, bot_user_id]
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def start_gateway_heartbeat(websocket, interval_ms, agent_display, last_sequence)
|
|
115
|
-
LOG.debug "[Discord:#{agent_display}] Gateway connected, heartbeat: #{interval_ms}ms"
|
|
116
|
-
Thread.new do
|
|
117
|
-
loop do
|
|
118
|
-
sleep(interval_ms / 1000.0)
|
|
119
|
-
websocket.send({ op: 1, d: last_sequence }.to_json)
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def send_identify(websocket, bot_token, agent_display)
|
|
125
|
-
LOG.debug "[Discord:#{agent_display}] Sending IDENTIFY"
|
|
126
|
-
websocket.send({
|
|
127
|
-
op: 2,
|
|
128
|
-
d: {
|
|
129
|
-
token: bot_token,
|
|
130
|
-
intents: 46_593,
|
|
131
|
-
properties: { os: RUBY_PLATFORM, browser: "brainiac", device: "brainiac" }
|
|
132
|
-
}
|
|
133
|
-
}.to_json)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
def handle_gateway_dispatch(payload, data, agent_key, agent_display, bot_token, bot_user_id)
|
|
137
|
-
case payload["t"]
|
|
138
|
-
when "READY"
|
|
139
|
-
bot_user_id = data.dig("user", "id")
|
|
140
|
-
mark_bot_ready(agent_key, agent_display, bot_user_id, data)
|
|
141
|
-
when "MESSAGE_CREATE"
|
|
142
|
-
Thread.new do
|
|
143
|
-
handle_discord_message(data, agent_key, bot_token, bot_user_id)
|
|
144
|
-
rescue StandardError => e
|
|
145
|
-
LOG.error "[Discord:#{agent_display}] Error handling message: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
|
|
146
|
-
end
|
|
147
|
-
when "MESSAGE_UPDATE"
|
|
148
|
-
if data["edited_timestamp"]
|
|
149
|
-
Thread.new do
|
|
150
|
-
handle_discord_message(data, agent_key, bot_token, bot_user_id)
|
|
151
|
-
rescue StandardError => e
|
|
152
|
-
LOG.error "[Discord:#{agent_display}] Error handling message update: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
when "MESSAGE_REACTION_ADD"
|
|
156
|
-
Thread.new do
|
|
157
|
-
handle_discord_reaction(data, agent_key, bot_token, bot_user_id)
|
|
158
|
-
rescue StandardError => e
|
|
159
|
-
LOG.error "[Discord:#{agent_display}] Error handling reaction: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
bot_user_id
|
|
164
|
-
end
|
|
165
|
-
|
|
166
|
-
def mark_bot_ready(agent_key, agent_display, bot_user_id, data)
|
|
167
|
-
DISCORD_BOTS_MUTEX.synchronize do
|
|
168
|
-
DISCORD_BOTS[agent_key][:user_id] = bot_user_id
|
|
169
|
-
DISCORD_BOTS[agent_key][:status] = "ready"
|
|
170
|
-
end
|
|
171
|
-
guild_count = data["guilds"]&.size || 0
|
|
172
|
-
LOG.info "[Discord] #{agent_display} ready (#{guild_count} #{guild_count == 1 ? "guild" : "guilds"})"
|
|
173
|
-
LOG.debug "[Discord:#{agent_display}] user_id=#{bot_user_id}"
|
|
174
|
-
|
|
175
|
-
DISCORD_BOTS_MUTEX.synchronize do
|
|
176
|
-
if !DISCORD_ALL_READY_LOGGED[:done] && DISCORD_BOTS.all? { |_, info| info[:status] == "ready" }
|
|
177
|
-
DISCORD_ALL_READY_LOGGED[:done] = true
|
|
178
|
-
LOG.info "[Discord] All bots connected."
|
|
179
|
-
end
|
|
180
|
-
end
|
|
181
|
-
end
|
|
182
|
-
|
|
183
|
-
# Start all per-agent Discord bots.
|
|
184
|
-
def start_all_discord_gateways
|
|
185
|
-
tokens = discord_bot_tokens
|
|
186
|
-
if tokens.empty?
|
|
187
|
-
LOG.info "[Discord] No agents have DISCORD_BOT_TOKEN configured — Discord disabled"
|
|
188
|
-
return
|
|
189
|
-
end
|
|
190
|
-
|
|
191
|
-
LOG.info "[Discord] Starting #{tokens.size} bot(s): #{tokens.keys.join(", ")}"
|
|
192
|
-
|
|
193
|
-
DISCORD_BOTS_MUTEX.synchronize do
|
|
194
|
-
tokens.each do |agent_key, token|
|
|
195
|
-
DISCORD_BOTS[agent_key] = { token: token, status: "starting", user_id: nil }
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
|
|
199
|
-
tokens.each do |agent_key, token|
|
|
200
|
-
start_discord_gateway_for(agent_key, token)
|
|
201
|
-
sleep 1 # Stagger connections to avoid rate limits
|
|
202
|
-
end
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Summary of all bot statuses for the API endpoint.
|
|
206
|
-
def discord_bots_status
|
|
207
|
-
DISCORD_BOTS_MUTEX.synchronize do
|
|
208
|
-
DISCORD_BOTS.transform_values do |info|
|
|
209
|
-
{ status: info[:status], user_id: info[:user_id] }
|
|
210
|
-
end
|
|
211
|
-
end
|
|
212
|
-
end
|