@interactive-inc/flume 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.
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/discord-source-Q4JRIbNs.js +525 -0
- package/dist/discord.d.ts +24 -0
- package/dist/discord.js +2 -0
- package/dist/github-source-D7Z1RUbe.js +280 -0
- package/dist/github.d.ts +19 -0
- package/dist/github.js +2 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +66 -0
- package/dist/logger-B9E8zvgj.js +69 -0
- package/dist/safe-json-parse-D8t_4Vm_.js +81 -0
- package/dist/slack-source-CszepStG.js +400 -0
- package/dist/slack.d.ts +23 -0
- package/dist/slack.js +2 -0
- package/dist/types-BVQSU336.d.ts +112 -0
- package/package.json +77 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import { n as FlumeGatewayMessageSchema, t as FlumeLogger } from "./logger-B9E8zvgj.js";
|
|
2
|
+
import { a as FlumeParseError, i as resolveFlumeReconnectConfig, n as isRecord, o as FlumeConnectionError, r as FlumeReconnector, t as safeJsonParse } from "./safe-json-parse-D8t_4Vm_.js";
|
|
3
|
+
//#region lib/discord/discord-heartbeat.ts
|
|
4
|
+
var FlumeDiscordHeartbeat = class {
|
|
5
|
+
props;
|
|
6
|
+
timer = null;
|
|
7
|
+
ackReceived = true;
|
|
8
|
+
constructor(props) {
|
|
9
|
+
this.props = props;
|
|
10
|
+
}
|
|
11
|
+
start(intervalMs) {
|
|
12
|
+
this.stop();
|
|
13
|
+
this.ackReceived = true;
|
|
14
|
+
this.timer = this.props.deps.setInterval(() => {
|
|
15
|
+
if (!this.ackReceived) {
|
|
16
|
+
this.props.onZombie();
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
this.ackReceived = false;
|
|
20
|
+
this.props.onSend();
|
|
21
|
+
}, intervalMs);
|
|
22
|
+
}
|
|
23
|
+
stop() {
|
|
24
|
+
if (this.timer === null) return;
|
|
25
|
+
this.props.deps.clearInterval(this.timer);
|
|
26
|
+
this.timer = null;
|
|
27
|
+
}
|
|
28
|
+
ack() {
|
|
29
|
+
this.ackReceived = true;
|
|
30
|
+
}
|
|
31
|
+
isRunning() {
|
|
32
|
+
return this.timer !== null;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region lib/discord/discord-gateway-session.ts
|
|
37
|
+
var FlumeDiscordGatewaySession = class FlumeDiscordGatewaySession {
|
|
38
|
+
sessionId;
|
|
39
|
+
resumeUrl;
|
|
40
|
+
seq;
|
|
41
|
+
constructor(props) {
|
|
42
|
+
this.sessionId = props.sessionId;
|
|
43
|
+
this.resumeUrl = props.resumeUrl;
|
|
44
|
+
this.seq = props.seq;
|
|
45
|
+
Object.freeze(this);
|
|
46
|
+
}
|
|
47
|
+
static empty() {
|
|
48
|
+
return new FlumeDiscordGatewaySession({
|
|
49
|
+
sessionId: null,
|
|
50
|
+
resumeUrl: null,
|
|
51
|
+
seq: null
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
canResume() {
|
|
55
|
+
return this.sessionId !== null;
|
|
56
|
+
}
|
|
57
|
+
withSeq(seq) {
|
|
58
|
+
return new FlumeDiscordGatewaySession({
|
|
59
|
+
sessionId: this.sessionId,
|
|
60
|
+
resumeUrl: this.resumeUrl,
|
|
61
|
+
seq
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
withReady(sessionId, resumeUrl) {
|
|
65
|
+
return new FlumeDiscordGatewaySession({
|
|
66
|
+
sessionId,
|
|
67
|
+
resumeUrl,
|
|
68
|
+
seq: this.seq
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
withReset() {
|
|
72
|
+
return FlumeDiscordGatewaySession.empty();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
//#region lib/discord/parse-discord-gateway-message.ts
|
|
77
|
+
function parseDiscordGatewayMessage(raw) {
|
|
78
|
+
const json = safeJsonParse(raw);
|
|
79
|
+
const parsed = FlumeGatewayMessageSchema.safeParse(json);
|
|
80
|
+
if (!parsed.success) return new FlumeParseError(`invalid gateway message: ${raw.slice(0, 200)}`);
|
|
81
|
+
return parsed.data;
|
|
82
|
+
}
|
|
83
|
+
//#endregion
|
|
84
|
+
//#region lib/discord/discord-gateway.ts
|
|
85
|
+
const GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
86
|
+
function framePreview(raw) {
|
|
87
|
+
return raw.length > 200 ? `${raw.slice(0, 200)}... (${raw.length} bytes)` : raw;
|
|
88
|
+
}
|
|
89
|
+
const OP_DISPATCH = 0;
|
|
90
|
+
const OP_HEARTBEAT = 1;
|
|
91
|
+
const OP_IDENTIFY = 2;
|
|
92
|
+
const OP_RESUME = 6;
|
|
93
|
+
const OP_RECONNECT = 7;
|
|
94
|
+
const OP_INVALID_SESSION = 9;
|
|
95
|
+
const OP_HELLO = 10;
|
|
96
|
+
const OP_HEARTBEAT_ACK = 11;
|
|
97
|
+
const OP_NAMES = {
|
|
98
|
+
[OP_DISPATCH]: "DISPATCH",
|
|
99
|
+
[OP_HEARTBEAT]: "HEARTBEAT",
|
|
100
|
+
[OP_IDENTIFY]: "IDENTIFY",
|
|
101
|
+
[OP_RESUME]: "RESUME",
|
|
102
|
+
[OP_RECONNECT]: "RECONNECT",
|
|
103
|
+
[OP_INVALID_SESSION]: "INVALID_SESSION",
|
|
104
|
+
[OP_HELLO]: "HELLO",
|
|
105
|
+
[OP_HEARTBEAT_ACK]: "HEARTBEAT_ACK"
|
|
106
|
+
};
|
|
107
|
+
var FlumeDiscordGateway = class {
|
|
108
|
+
props;
|
|
109
|
+
log;
|
|
110
|
+
ws = null;
|
|
111
|
+
heartbeat = null;
|
|
112
|
+
session = FlumeDiscordGatewaySession.empty();
|
|
113
|
+
stopped = false;
|
|
114
|
+
pendingResolve = null;
|
|
115
|
+
pendingResolved = false;
|
|
116
|
+
constructor(props) {
|
|
117
|
+
this.props = props;
|
|
118
|
+
this.log = new FlumeLogger({
|
|
119
|
+
source: "discord.gateway",
|
|
120
|
+
handler: props.onLog,
|
|
121
|
+
deps: props.deps
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
connect(url) {
|
|
125
|
+
const target = url ?? GATEWAY_URL;
|
|
126
|
+
this.log.info({
|
|
127
|
+
action: "connect.start",
|
|
128
|
+
message: `url=${new URL(target).hostname}`
|
|
129
|
+
});
|
|
130
|
+
this.pendingResolved = false;
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
this.pendingResolve = resolve;
|
|
133
|
+
const socket = new this.props.deps.WebSocket(target);
|
|
134
|
+
this.ws = socket;
|
|
135
|
+
socket.addEventListener("message", (ev) => this.onMessage(String(ev.data), socket));
|
|
136
|
+
socket.addEventListener("close", (ev) => this.onClose(ev));
|
|
137
|
+
socket.addEventListener("error", () => this.onError());
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
disconnect() {
|
|
141
|
+
this.log.info({
|
|
142
|
+
action: "disconnect",
|
|
143
|
+
message: "shutting down gateway"
|
|
144
|
+
});
|
|
145
|
+
this.stopped = true;
|
|
146
|
+
this.heartbeat?.stop();
|
|
147
|
+
if (this.ws) {
|
|
148
|
+
this.ws.close(1e3, "shutdown");
|
|
149
|
+
this.ws = null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
isConnected() {
|
|
153
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
154
|
+
}
|
|
155
|
+
completeConnect(error) {
|
|
156
|
+
if (this.pendingResolved || !this.pendingResolve) return;
|
|
157
|
+
this.pendingResolved = true;
|
|
158
|
+
this.pendingResolve(error);
|
|
159
|
+
}
|
|
160
|
+
onMessage(raw, socket) {
|
|
161
|
+
this.log.debug({
|
|
162
|
+
action: "ws.recv",
|
|
163
|
+
message: framePreview(raw)
|
|
164
|
+
});
|
|
165
|
+
const parsed = parseDiscordGatewayMessage(raw);
|
|
166
|
+
if (parsed instanceof FlumeParseError) {
|
|
167
|
+
this.log.error({
|
|
168
|
+
action: "ws.parse-error",
|
|
169
|
+
message: parsed.message,
|
|
170
|
+
error: parsed
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.log.debug({
|
|
175
|
+
action: "ws.frame",
|
|
176
|
+
message: `op=${OP_NAMES[parsed.op] ?? parsed.op} t=${parsed.t ?? "-"} s=${parsed.s ?? "-"}`,
|
|
177
|
+
detail: {
|
|
178
|
+
op: parsed.op,
|
|
179
|
+
t: parsed.t,
|
|
180
|
+
s: parsed.s
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
if (parsed.s !== null) this.session = this.session.withSeq(parsed.s);
|
|
184
|
+
if (parsed.op === OP_HELLO) return this.onHello(parsed);
|
|
185
|
+
if (parsed.op === OP_HEARTBEAT_ACK) return this.onHeartbeatAck();
|
|
186
|
+
if (parsed.op === OP_HEARTBEAT) return this.onHeartbeatRequest();
|
|
187
|
+
if (parsed.op === OP_RECONNECT) return this.onReconnectRequest(socket);
|
|
188
|
+
if (parsed.op === OP_INVALID_SESSION) return this.onInvalidSession(parsed, socket);
|
|
189
|
+
if (parsed.op === OP_DISPATCH) return this.onDispatch(parsed);
|
|
190
|
+
this.log.warn({
|
|
191
|
+
action: "ws.unknown-op",
|
|
192
|
+
message: `unknown op=${parsed.op}`,
|
|
193
|
+
detail: { op: parsed.op }
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
onHello(msg) {
|
|
197
|
+
const interval = typeof msg.d?.heartbeat_interval === "number" ? msg.d.heartbeat_interval : 0;
|
|
198
|
+
this.log.info({
|
|
199
|
+
action: "hello",
|
|
200
|
+
message: `heartbeat_interval=${interval}ms`
|
|
201
|
+
});
|
|
202
|
+
this.heartbeat = new FlumeDiscordHeartbeat({
|
|
203
|
+
deps: this.props.deps,
|
|
204
|
+
onSend: () => {
|
|
205
|
+
this.log.debug({
|
|
206
|
+
action: "heartbeat.send",
|
|
207
|
+
message: `seq=${this.session.seq}`
|
|
208
|
+
});
|
|
209
|
+
this.send({
|
|
210
|
+
op: OP_HEARTBEAT,
|
|
211
|
+
d: this.session.seq
|
|
212
|
+
});
|
|
213
|
+
},
|
|
214
|
+
onZombie: () => {
|
|
215
|
+
this.log.warn({
|
|
216
|
+
action: "heartbeat.zombie",
|
|
217
|
+
message: "no ACK received, closing connection"
|
|
218
|
+
});
|
|
219
|
+
this.ws?.close(4009, "zombie connection");
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
this.heartbeat.start(interval);
|
|
223
|
+
if (this.session.canResume()) this.sendResume();
|
|
224
|
+
else this.sendIdentify();
|
|
225
|
+
}
|
|
226
|
+
onHeartbeatAck() {
|
|
227
|
+
this.log.debug({
|
|
228
|
+
action: "heartbeat.ack",
|
|
229
|
+
message: "received"
|
|
230
|
+
});
|
|
231
|
+
this.heartbeat?.ack();
|
|
232
|
+
}
|
|
233
|
+
onHeartbeatRequest() {
|
|
234
|
+
this.log.debug({
|
|
235
|
+
action: "heartbeat.requested",
|
|
236
|
+
message: "server requested heartbeat"
|
|
237
|
+
});
|
|
238
|
+
this.send({
|
|
239
|
+
op: OP_HEARTBEAT,
|
|
240
|
+
d: this.session.seq
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
onReconnectRequest(socket) {
|
|
244
|
+
this.log.info({
|
|
245
|
+
action: "reconnect.requested",
|
|
246
|
+
message: "server requested reconnect"
|
|
247
|
+
});
|
|
248
|
+
socket.close(4e3, "reconnect requested");
|
|
249
|
+
}
|
|
250
|
+
onInvalidSession(msg, socket) {
|
|
251
|
+
const resumable = !!msg.d;
|
|
252
|
+
this.log.warn({
|
|
253
|
+
action: "invalid-session",
|
|
254
|
+
message: `resumable=${resumable}`
|
|
255
|
+
});
|
|
256
|
+
this.session = this.session.withReset();
|
|
257
|
+
if (resumable) {
|
|
258
|
+
const delay = 1e3 + this.props.deps.random() * 4e3;
|
|
259
|
+
this.log.info({
|
|
260
|
+
action: "identify.delayed",
|
|
261
|
+
message: `re-identify in ${Math.round(delay)}ms`
|
|
262
|
+
});
|
|
263
|
+
this.props.deps.setTimeout(() => this.sendIdentify(), delay);
|
|
264
|
+
} else socket.close(4e3, "invalid session");
|
|
265
|
+
}
|
|
266
|
+
onDispatch(msg) {
|
|
267
|
+
if (msg.t === "READY" && msg.d) {
|
|
268
|
+
const sessionId = typeof msg.d.session_id === "string" ? msg.d.session_id : "";
|
|
269
|
+
const resumeUrl = typeof msg.d.resume_gateway_url === "string" ? msg.d.resume_gateway_url : "";
|
|
270
|
+
this.session = this.session.withReady(sessionId, resumeUrl);
|
|
271
|
+
this.log.info({
|
|
272
|
+
action: "ready",
|
|
273
|
+
message: `session=${sessionId}`
|
|
274
|
+
});
|
|
275
|
+
this.props.onStatus("connected");
|
|
276
|
+
this.completeConnect(null);
|
|
277
|
+
}
|
|
278
|
+
if (msg.t === "RESUMED") {
|
|
279
|
+
this.log.info({
|
|
280
|
+
action: "resumed",
|
|
281
|
+
message: `session=${this.session.sessionId} seq=${this.session.seq}`
|
|
282
|
+
});
|
|
283
|
+
this.props.onStatus("connected");
|
|
284
|
+
this.completeConnect(null);
|
|
285
|
+
}
|
|
286
|
+
if (msg.t && msg.d) this.props.onDispatch(msg.t, msg.d);
|
|
287
|
+
else if (msg.t) this.props.onDispatch(msg.t, {});
|
|
288
|
+
}
|
|
289
|
+
onClose(ev) {
|
|
290
|
+
this.log.info({
|
|
291
|
+
action: "ws.close",
|
|
292
|
+
message: `code=${ev.code} reason=${ev.reason || "none"}`,
|
|
293
|
+
detail: {
|
|
294
|
+
code: ev.code,
|
|
295
|
+
reason: ev.reason
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
this.ws = null;
|
|
299
|
+
this.heartbeat?.stop();
|
|
300
|
+
this.props.onStatus("disconnected");
|
|
301
|
+
this.completeConnect(new FlumeConnectionError(`WebSocket closed before ready (code=${ev.code})`));
|
|
302
|
+
}
|
|
303
|
+
onError() {
|
|
304
|
+
this.log.error({
|
|
305
|
+
action: "ws.error",
|
|
306
|
+
message: "WebSocket error event"
|
|
307
|
+
});
|
|
308
|
+
this.completeConnect(new FlumeConnectionError("WebSocket connection error"));
|
|
309
|
+
}
|
|
310
|
+
send(input) {
|
|
311
|
+
const payload = JSON.stringify({
|
|
312
|
+
op: input.op,
|
|
313
|
+
d: input.d ?? null
|
|
314
|
+
});
|
|
315
|
+
this.log.debug({
|
|
316
|
+
action: "ws.send",
|
|
317
|
+
message: `op=${OP_NAMES[input.op] ?? input.op}`,
|
|
318
|
+
detail: { op: input.op }
|
|
319
|
+
});
|
|
320
|
+
this.ws?.send(payload);
|
|
321
|
+
this.log.debug({
|
|
322
|
+
action: "ws.sent",
|
|
323
|
+
message: framePreview(payload)
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
sendIdentify() {
|
|
327
|
+
this.log.info({
|
|
328
|
+
action: "identify",
|
|
329
|
+
message: `intents=${this.props.intents}`
|
|
330
|
+
});
|
|
331
|
+
this.send({
|
|
332
|
+
op: OP_IDENTIFY,
|
|
333
|
+
d: {
|
|
334
|
+
token: this.props.token,
|
|
335
|
+
intents: this.props.intents,
|
|
336
|
+
properties: {
|
|
337
|
+
os: "linux",
|
|
338
|
+
browser: "open-flume",
|
|
339
|
+
device: "open-flume"
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
sendResume() {
|
|
345
|
+
this.log.info({
|
|
346
|
+
action: "resume",
|
|
347
|
+
message: `session=${this.session.sessionId} seq=${this.session.seq}`
|
|
348
|
+
});
|
|
349
|
+
this.send({
|
|
350
|
+
op: OP_RESUME,
|
|
351
|
+
d: {
|
|
352
|
+
token: this.props.token,
|
|
353
|
+
session_id: this.session.sessionId,
|
|
354
|
+
seq: this.session.seq
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
};
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region lib/discord/discord-gateway-intents.ts
|
|
361
|
+
const FlumeDiscordGatewayIntents = {
|
|
362
|
+
Guilds: 1,
|
|
363
|
+
GuildMembers: 2,
|
|
364
|
+
GuildModeration: 4,
|
|
365
|
+
GuildExpressions: 8,
|
|
366
|
+
GuildIntegrations: 16,
|
|
367
|
+
GuildWebhooks: 32,
|
|
368
|
+
GuildInvites: 64,
|
|
369
|
+
GuildVoiceStates: 128,
|
|
370
|
+
GuildPresences: 256,
|
|
371
|
+
GuildMessages: 512,
|
|
372
|
+
GuildMessageReactions: 1024,
|
|
373
|
+
GuildMessageTyping: 2048,
|
|
374
|
+
DirectMessages: 4096,
|
|
375
|
+
DirectMessageReactions: 8192,
|
|
376
|
+
DirectMessageTyping: 16384,
|
|
377
|
+
MessageContent: 32768,
|
|
378
|
+
GuildScheduledEvents: 65536,
|
|
379
|
+
AutoModerationConfiguration: 1 << 20,
|
|
380
|
+
AutoModerationExecution: 1 << 21,
|
|
381
|
+
GuildMessagePolls: 1 << 24,
|
|
382
|
+
DirectMessagePolls: 1 << 25
|
|
383
|
+
};
|
|
384
|
+
//#endregion
|
|
385
|
+
//#region lib/discord/discord-source.ts
|
|
386
|
+
const DEFAULT_INTENTS = FlumeDiscordGatewayIntents.Guilds | FlumeDiscordGatewayIntents.GuildMessages | FlumeDiscordGatewayIntents.DirectMessages | FlumeDiscordGatewayIntents.MessageContent;
|
|
387
|
+
var FlumeDiscordSource = class FlumeDiscordSource {
|
|
388
|
+
options;
|
|
389
|
+
gateway = null;
|
|
390
|
+
reconnector = null;
|
|
391
|
+
handler = null;
|
|
392
|
+
currentStatus = "disconnected";
|
|
393
|
+
log;
|
|
394
|
+
deps;
|
|
395
|
+
constructor(options) {
|
|
396
|
+
this.options = options;
|
|
397
|
+
this.deps = options.deps;
|
|
398
|
+
this.log = new FlumeLogger({
|
|
399
|
+
source: "discord",
|
|
400
|
+
handler: options.onLog,
|
|
401
|
+
deps: this.deps
|
|
402
|
+
});
|
|
403
|
+
const rc = resolveFlumeReconnectConfig(options.reconnect);
|
|
404
|
+
if (rc) this.reconnector = new FlumeReconnector({
|
|
405
|
+
...rc,
|
|
406
|
+
deps: this.deps
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
async start(handler) {
|
|
410
|
+
if (this.options.signal?.aborted) return;
|
|
411
|
+
this.options.signal?.addEventListener("abort", () => this.stop(), { once: true });
|
|
412
|
+
this.handler = handler;
|
|
413
|
+
this.log.info({
|
|
414
|
+
action: "start",
|
|
415
|
+
message: "starting Discord source"
|
|
416
|
+
});
|
|
417
|
+
await this.connectInternal();
|
|
418
|
+
}
|
|
419
|
+
async stop() {
|
|
420
|
+
this.log.info({
|
|
421
|
+
action: "stop",
|
|
422
|
+
message: "stopping Discord source"
|
|
423
|
+
});
|
|
424
|
+
if (this.reconnector && !this.reconnector.aborted) this.log.debug({
|
|
425
|
+
action: "reconnect.cancel",
|
|
426
|
+
message: "aborting reconnector"
|
|
427
|
+
});
|
|
428
|
+
this.reconnector?.cancel();
|
|
429
|
+
this.gateway?.disconnect();
|
|
430
|
+
this.gateway = null;
|
|
431
|
+
this.handler = null;
|
|
432
|
+
this.setStatus("disconnected");
|
|
433
|
+
}
|
|
434
|
+
status() {
|
|
435
|
+
return this.currentStatus;
|
|
436
|
+
}
|
|
437
|
+
async connectInternal(resumeUrl) {
|
|
438
|
+
this.setStatus("connecting");
|
|
439
|
+
this.gateway = new FlumeDiscordGateway({
|
|
440
|
+
token: this.options.token,
|
|
441
|
+
intents: this.options.intents ?? DEFAULT_INTENTS,
|
|
442
|
+
onLog: this.options.onLog,
|
|
443
|
+
deps: this.deps,
|
|
444
|
+
onDispatch: (eventName, eventData) => this.handleDispatch(eventName, eventData),
|
|
445
|
+
onStatus: (status) => this.handleGatewayStatus(status)
|
|
446
|
+
});
|
|
447
|
+
const error = await this.gateway.connect(resumeUrl);
|
|
448
|
+
if (error instanceof FlumeConnectionError) {
|
|
449
|
+
this.log.error({
|
|
450
|
+
action: "connect.failed",
|
|
451
|
+
message: error.message,
|
|
452
|
+
error
|
|
453
|
+
});
|
|
454
|
+
this.scheduleReconnect();
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
handleDispatch(eventName, eventData) {
|
|
458
|
+
const event = {
|
|
459
|
+
source: "discord",
|
|
460
|
+
type: eventName,
|
|
461
|
+
data: eventData,
|
|
462
|
+
meta: FlumeDiscordSource.extractMeta(eventName, eventData),
|
|
463
|
+
receivedAt: this.deps.now()
|
|
464
|
+
};
|
|
465
|
+
try {
|
|
466
|
+
this.handler?.(event);
|
|
467
|
+
} catch (err) {
|
|
468
|
+
this.log.error({
|
|
469
|
+
action: "handler.error",
|
|
470
|
+
message: "user handler threw",
|
|
471
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
handleGatewayStatus(status) {
|
|
476
|
+
if (status === "connected") {
|
|
477
|
+
if (this.reconnector && this.reconnector.attempt > 0) this.log.info({
|
|
478
|
+
action: "reconnect.reset",
|
|
479
|
+
message: `cleared ${this.reconnector.attempt} attempts`
|
|
480
|
+
});
|
|
481
|
+
this.reconnector?.reset();
|
|
482
|
+
this.setStatus("connected");
|
|
483
|
+
}
|
|
484
|
+
if (status === "disconnected" && !this.gateway?.stopped) this.scheduleReconnect();
|
|
485
|
+
}
|
|
486
|
+
scheduleReconnect() {
|
|
487
|
+
if (!this.reconnector || this.reconnector.aborted) {
|
|
488
|
+
this.setStatus("disconnected");
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const url = this.gateway?.session.resumeUrl ?? void 0;
|
|
492
|
+
this.setStatus("reconnecting");
|
|
493
|
+
const delay = this.reconnector.schedule(() => {
|
|
494
|
+
this.connectInternal(url);
|
|
495
|
+
});
|
|
496
|
+
if (delay === -1) {
|
|
497
|
+
this.log.error({
|
|
498
|
+
action: "reconnect.exhausted",
|
|
499
|
+
message: `gave up after ${this.reconnector.attempt} attempts`
|
|
500
|
+
});
|
|
501
|
+
this.setStatus("disconnected");
|
|
502
|
+
} else this.log.info({
|
|
503
|
+
action: "reconnect.scheduled",
|
|
504
|
+
message: `next attempt in ${Math.round(delay)}ms`
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
setStatus(next) {
|
|
508
|
+
if (this.currentStatus === next) return;
|
|
509
|
+
this.log.info({
|
|
510
|
+
action: "status",
|
|
511
|
+
message: `${this.currentStatus} → ${next}`
|
|
512
|
+
});
|
|
513
|
+
this.currentStatus = next;
|
|
514
|
+
this.options.onStatus?.(next);
|
|
515
|
+
}
|
|
516
|
+
static extractMeta(eventName, eventData) {
|
|
517
|
+
const meta = { event_type: eventName };
|
|
518
|
+
if (typeof eventData.channel_id === "string") meta.channel_id = eventData.channel_id;
|
|
519
|
+
if (typeof eventData.guild_id === "string") meta.guild_id = eventData.guild_id;
|
|
520
|
+
if (isRecord(eventData.author) && typeof eventData.author.id === "string") meta.user_id = eventData.author.id;
|
|
521
|
+
return meta;
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
//#endregion
|
|
525
|
+
export { FlumeDiscordGatewaySession as a, parseDiscordGatewayMessage as i, FlumeDiscordGatewayIntents as n, FlumeDiscordHeartbeat as o, FlumeDiscordGateway as r, FlumeDiscordSource as t };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { o as FlumeHandler, t as FlumeDiscordSourceOptions, y as FlumeStatus } from "./types-BVQSU336.js";
|
|
2
|
+
|
|
3
|
+
//#region lib/discord/discord-source.d.ts
|
|
4
|
+
declare class FlumeDiscordSource {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private gateway;
|
|
7
|
+
private reconnector;
|
|
8
|
+
private handler;
|
|
9
|
+
private currentStatus;
|
|
10
|
+
private readonly log;
|
|
11
|
+
private readonly deps;
|
|
12
|
+
constructor(options: FlumeDiscordSourceOptions);
|
|
13
|
+
start(handler: FlumeHandler): Promise<void>;
|
|
14
|
+
stop(): Promise<void>;
|
|
15
|
+
status(): FlumeStatus;
|
|
16
|
+
private connectInternal;
|
|
17
|
+
private handleDispatch;
|
|
18
|
+
private handleGatewayStatus;
|
|
19
|
+
private scheduleReconnect;
|
|
20
|
+
private setStatus;
|
|
21
|
+
static extractMeta(eventName: string, eventData: Record<string, unknown>): Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
//#endregion
|
|
24
|
+
export { FlumeDiscordSource };
|
package/dist/discord.js
ADDED