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.
@@ -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