jeopi-cli 16.6.13 → 16.6.17
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/CHANGELOG.md +30 -0
- package/README.md +3 -1
- package/dist/cli.js +4158 -4105
- package/dist/types/cli/messenger-cli.d.ts +14 -3
- package/dist/types/commands/messenger.d.ts +21 -1
- package/dist/types/messenger/discord-setup.d.ts +48 -0
- package/dist/types/messenger/discord.d.ts +3 -0
- package/dist/types/messenger/gateway.d.ts +7 -0
- package/dist/types/messenger/service.d.ts +19 -0
- package/dist/types/messenger/session-pool.d.ts +38 -0
- package/dist/types/messenger/session-worker.d.ts +49 -0
- package/dist/types/messenger/types.d.ts +4 -0
- package/package.json +12 -12
- package/src/cli/messenger-cli.ts +99 -26
- package/src/cli.ts +13 -0
- package/src/commands/messenger.ts +26 -2
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/messenger/discord-setup.ts +563 -0
- package/src/messenger/discord.ts +213 -54
- package/src/messenger/gateway.ts +170 -91
- package/src/messenger/service.ts +122 -0
- package/src/messenger/session-pool.ts +366 -0
- package/src/messenger/session-worker.ts +79 -0
- package/src/messenger/session.ts +5 -4
- package/src/messenger/types.ts +4 -0
package/src/messenger/discord.ts
CHANGED
|
@@ -6,9 +6,10 @@ import type { MessengerAdapter, MessengerMessage, MessengerPlatformConfig, Messe
|
|
|
6
6
|
// Protocol: https://docs.discord.com/developers/events/gateway
|
|
7
7
|
// Close codes: https://docs.discord.com/developers/topics/opcodes-and-status-codes
|
|
8
8
|
// REST messages: https://docs.discord.com/developers/resources/message#create-message
|
|
9
|
-
const
|
|
10
|
-
const
|
|
9
|
+
export const DISCORD_API = "https://discord.com/api/v10";
|
|
10
|
+
export const DISCORD_GATEWAY_INTENTS = (1 << 9) | (1 << 12) | (1 << 15);
|
|
11
11
|
const PERMANENT_CLOSE_CODES = new Set([4004, 4010, 4011, 4012, 4013, 4014]);
|
|
12
|
+
const MAX_CLASSIFICATION_PENDING = 100;
|
|
12
13
|
|
|
13
14
|
// Discord strips boundary whitespace. Invisible guards preserve it in message content;
|
|
14
15
|
// they are part of the delivered text, and consume one UTF-16 unit each.
|
|
@@ -82,7 +83,7 @@ interface DiscordSession {
|
|
|
82
83
|
sequence: number | null;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
|
-
function
|
|
86
|
+
export function discordGatewayUrl(value: unknown): string {
|
|
86
87
|
let url: URL;
|
|
87
88
|
try {
|
|
88
89
|
if (typeof value !== "string") throw new Error();
|
|
@@ -158,7 +159,7 @@ function connectGateway(
|
|
|
158
159
|
token: string,
|
|
159
160
|
accountId: string,
|
|
160
161
|
session: DiscordSession,
|
|
161
|
-
onMessage: (message: MessengerMessage) => Promise<void>,
|
|
162
|
+
onMessage: (message: MessengerMessage, raw: unknown) => Promise<void>,
|
|
162
163
|
signal: AbortSignal,
|
|
163
164
|
WebSocketImpl: typeof WebSocket,
|
|
164
165
|
clock: DiscordClock,
|
|
@@ -267,7 +268,7 @@ function connectGateway(
|
|
|
267
268
|
} else {
|
|
268
269
|
send(2, {
|
|
269
270
|
token,
|
|
270
|
-
intents:
|
|
271
|
+
intents: DISCORD_GATEWAY_INTENTS,
|
|
271
272
|
properties: { os: process.platform, browser: "jeopi", device: "jeopi" },
|
|
272
273
|
});
|
|
273
274
|
}
|
|
@@ -287,7 +288,7 @@ function connectGateway(
|
|
|
287
288
|
return;
|
|
288
289
|
}
|
|
289
290
|
try {
|
|
290
|
-
session.url =
|
|
291
|
+
session.url = discordGatewayUrl(packet.d.resume_gateway_url);
|
|
291
292
|
} catch {
|
|
292
293
|
finish(new DiscordConfigurationError("Discord returned an untrusted resume Gateway URL"));
|
|
293
294
|
return;
|
|
@@ -304,7 +305,7 @@ function connectGateway(
|
|
|
304
305
|
// The gateway owns authorization, deduplication and its bounded work queue.
|
|
305
306
|
void Promise.resolve()
|
|
306
307
|
.then(() => {
|
|
307
|
-
if (!signal.aborted) return onMessage(message);
|
|
308
|
+
if (!signal.aborted) return onMessage(message, packet.d);
|
|
308
309
|
})
|
|
309
310
|
.catch(() => logger.warn("Discord message handler failed"));
|
|
310
311
|
}
|
|
@@ -328,6 +329,9 @@ export function createDiscordAdapter(
|
|
|
328
329
|
const token = env[config.tokenEnv]?.trim();
|
|
329
330
|
const clock = deps.clock ?? defaultClock;
|
|
330
331
|
const createdThreads = new Map<string, { parentChannelId: string; senderId: string }>();
|
|
332
|
+
const classificationTails = new Map<string, Promise<void>>();
|
|
333
|
+
const messageClassifications = new Map<string, Promise<void>>();
|
|
334
|
+
const pendingClassifications = new Set<string>();
|
|
331
335
|
const rememberThread = (threadId: string, parentChannelId: string, senderId: string): void => {
|
|
332
336
|
createdThreads.set(threadId, { parentChannelId, senderId });
|
|
333
337
|
if (createdThreads.size > 10_000) {
|
|
@@ -351,7 +355,12 @@ export function createDiscordAdapter(
|
|
|
351
355
|
let retryDelay = delay;
|
|
352
356
|
try {
|
|
353
357
|
if (!accountId) {
|
|
354
|
-
const user = await messengerFetchJson(
|
|
358
|
+
const user = await messengerFetchJson(
|
|
359
|
+
`${DISCORD_API}/users/@me`,
|
|
360
|
+
{ headers: headers() },
|
|
361
|
+
signal,
|
|
362
|
+
deps.fetch,
|
|
363
|
+
);
|
|
355
364
|
if (signal.aborted) return;
|
|
356
365
|
if (!isRecord(user) || typeof user.id !== "string" || !user.id || user.bot !== true) {
|
|
357
366
|
throw new DiscordConfigurationError("Discord credentials must identify a bot account");
|
|
@@ -360,7 +369,7 @@ export function createDiscordAdapter(
|
|
|
360
369
|
}
|
|
361
370
|
if (!initialUrl) {
|
|
362
371
|
const gateway = await messengerFetchJson(
|
|
363
|
-
`${
|
|
372
|
+
`${DISCORD_API}/gateway/bot`,
|
|
364
373
|
{ headers: headers() },
|
|
365
374
|
signal,
|
|
366
375
|
deps.fetch,
|
|
@@ -370,49 +379,159 @@ export function createDiscordAdapter(
|
|
|
370
379
|
if (isRecord(gateway.session_start_limit) && gateway.session_start_limit.remaining === 0) {
|
|
371
380
|
throw new DiscordConfigurationError("Discord Gateway session start limit is exhausted");
|
|
372
381
|
}
|
|
373
|
-
initialUrl =
|
|
382
|
+
initialUrl = discordGatewayUrl(gateway.url);
|
|
374
383
|
}
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
if (thread && thread.senderId === message.senderId) {
|
|
378
|
-
await onMessage({
|
|
379
|
-
...message,
|
|
380
|
-
threadId: message.channelId,
|
|
381
|
-
parentChannelId: thread.parentChannelId,
|
|
382
|
-
});
|
|
383
|
-
} else if (
|
|
384
|
-
!message.direct &&
|
|
385
|
-
message.mentioned &&
|
|
386
|
-
config.allowedUserIds.includes(message.senderId) &&
|
|
387
|
-
config.allowedChannelIds &&
|
|
388
|
-
!config.allowedChannelIds.includes(message.channelId)
|
|
389
|
-
) {
|
|
384
|
+
const lookup = async (url: string): Promise<unknown> => {
|
|
385
|
+
for (let attempt = 0; ; attempt++) {
|
|
390
386
|
try {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
{ headers: headers() },
|
|
394
|
-
signal,
|
|
395
|
-
deps.fetch,
|
|
396
|
-
);
|
|
387
|
+
return await messengerFetchJson(url, { headers: headers() }, signal, deps.fetch);
|
|
388
|
+
} catch (error) {
|
|
397
389
|
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
channel.parent_id
|
|
390
|
+
!(error instanceof MessengerHttpError) ||
|
|
391
|
+
error.status !== 429 ||
|
|
392
|
+
attempt >= 3 ||
|
|
393
|
+
signal.aborted
|
|
403
394
|
) {
|
|
404
|
-
|
|
405
|
-
...message,
|
|
406
|
-
threadId: message.channelId,
|
|
407
|
-
parentChannelId: channel.parent_id,
|
|
408
|
-
});
|
|
395
|
+
throw error;
|
|
409
396
|
}
|
|
397
|
+
await waitForReconnect(error.retryAfterMs ?? 1_000, signal, clock);
|
|
398
|
+
if (signal.aborted) throw error;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const isBotReply = async (value: unknown, channelId: string): Promise<boolean> => {
|
|
403
|
+
if (!isRecord(value) || value.type !== 19 || !isRecord(value.message_reference)) return false;
|
|
404
|
+
const reference = value.message_reference;
|
|
405
|
+
if (
|
|
406
|
+
typeof reference.message_id !== "string" ||
|
|
407
|
+
(reference.channel_id && reference.channel_id !== channelId)
|
|
408
|
+
)
|
|
409
|
+
return false;
|
|
410
|
+
let original = value.referenced_message;
|
|
411
|
+
if (original === undefined) {
|
|
412
|
+
try {
|
|
413
|
+
original = await lookup(
|
|
414
|
+
`${DISCORD_API}/channels/${encodeURIComponent(channelId)}/messages/${encodeURIComponent(reference.message_id)}`,
|
|
415
|
+
);
|
|
410
416
|
} catch {
|
|
411
|
-
if (!signal.aborted) logger.warn("Discord
|
|
417
|
+
if (!signal.aborted) logger.warn("Discord reply lookup failed");
|
|
418
|
+
return false;
|
|
412
419
|
}
|
|
413
|
-
} else {
|
|
414
|
-
await onMessage(message);
|
|
415
420
|
}
|
|
421
|
+
return (
|
|
422
|
+
isRecord(original) &&
|
|
423
|
+
original.id === reference.message_id &&
|
|
424
|
+
isRecord(original.author) &&
|
|
425
|
+
original.author.id === accountId
|
|
426
|
+
);
|
|
427
|
+
};
|
|
428
|
+
const forward = (message: MessengerMessage): void => {
|
|
429
|
+
if (signal.aborted) return;
|
|
430
|
+
try {
|
|
431
|
+
void onMessage(message).catch(() => logger.warn("Discord message handler failed"));
|
|
432
|
+
} catch {
|
|
433
|
+
logger.warn("Discord message handler failed");
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const classify = async (message: MessengerMessage, raw: unknown): Promise<void> => {
|
|
437
|
+
if (message.direct || !config.allowedUserIds.includes(message.senderId)) {
|
|
438
|
+
forward(message);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!message.mentioned && (await isBotReply(raw, message.channelId))) {
|
|
442
|
+
message = { ...message, mentioned: true };
|
|
443
|
+
}
|
|
444
|
+
const thread = createdThreads.get(message.channelId);
|
|
445
|
+
if (thread && thread.senderId === message.senderId) {
|
|
446
|
+
forward({ ...message, threadId: message.channelId, parentChannelId: thread.parentChannelId });
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const channel = await lookup(`${DISCORD_API}/channels/${encodeURIComponent(message.channelId)}`);
|
|
451
|
+
if (
|
|
452
|
+
!isRecord(channel) ||
|
|
453
|
+
typeof channel.type !== "number" ||
|
|
454
|
+
![10, 11, 12].includes(channel.type) ||
|
|
455
|
+
typeof channel.parent_id !== "string" ||
|
|
456
|
+
!channel.parent_id ||
|
|
457
|
+
(config.allowedChannelIds &&
|
|
458
|
+
!config.allowedChannelIds.includes(message.channelId) &&
|
|
459
|
+
!config.allowedChannelIds.includes(channel.parent_id))
|
|
460
|
+
) {
|
|
461
|
+
forward(message);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
if (!message.mentioned && channel.owner_id !== accountId) {
|
|
465
|
+
let before = message.messageId;
|
|
466
|
+
for (;;) {
|
|
467
|
+
const history = await lookup(
|
|
468
|
+
`${DISCORD_API}/channels/${encodeURIComponent(message.channelId)}/messages?limit=100&before=${encodeURIComponent(before)}`,
|
|
469
|
+
);
|
|
470
|
+
if (!Array.isArray(history)) return;
|
|
471
|
+
let invoked = false;
|
|
472
|
+
for (const entry of history) {
|
|
473
|
+
if (
|
|
474
|
+
!isRecord(entry) ||
|
|
475
|
+
!isRecord(entry.author) ||
|
|
476
|
+
entry.author.bot ||
|
|
477
|
+
entry.webhook_id ||
|
|
478
|
+
typeof entry.author.id !== "string" ||
|
|
479
|
+
!config.allowedUserIds.includes(entry.author.id)
|
|
480
|
+
)
|
|
481
|
+
continue;
|
|
482
|
+
if (
|
|
483
|
+
(Array.isArray(entry.mentions) &&
|
|
484
|
+
entry.mentions.some(user => isRecord(user) && user.id === accountId)) ||
|
|
485
|
+
(await isBotReply(entry, message.channelId))
|
|
486
|
+
) {
|
|
487
|
+
invoked = true;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (invoked) break;
|
|
492
|
+
if (history.length < 100) {
|
|
493
|
+
forward(message);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const last = history[history.length - 1];
|
|
497
|
+
if (!isRecord(last) || typeof last.id !== "string" || !last.id || last.id === before) return;
|
|
498
|
+
before = last.id;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
forward({ ...message, threadId: message.channelId, parentChannelId: channel.parent_id });
|
|
502
|
+
} catch {
|
|
503
|
+
if (!signal.aborted) logger.warn("Discord thread lookup failed");
|
|
504
|
+
}
|
|
505
|
+
return;
|
|
506
|
+
};
|
|
507
|
+
const deliver = (message: MessengerMessage, raw: unknown): Promise<void> => {
|
|
508
|
+
if (signal.aborted) return Promise.resolve();
|
|
509
|
+
if (message.direct || !config.allowedUserIds.includes(message.senderId)) {
|
|
510
|
+
forward(message);
|
|
511
|
+
return Promise.resolve();
|
|
512
|
+
}
|
|
513
|
+
const key = `${message.channelId}:${message.messageId}`;
|
|
514
|
+
if (pendingClassifications.has(key) || pendingClassifications.size >= MAX_CLASSIFICATION_PENDING) {
|
|
515
|
+
return Promise.resolve();
|
|
516
|
+
}
|
|
517
|
+
pendingClassifications.add(key);
|
|
518
|
+
const previous = Promise.all([
|
|
519
|
+
classificationTails.get(message.channelId),
|
|
520
|
+
messageClassifications.get(message.channelId),
|
|
521
|
+
]);
|
|
522
|
+
const current = previous
|
|
523
|
+
.then(() => (signal.aborted ? undefined : classify(message, raw)))
|
|
524
|
+
.catch(() => logger.warn("Discord classification failed"));
|
|
525
|
+
classificationTails.set(message.channelId, current);
|
|
526
|
+
messageClassifications.set(message.messageId, current);
|
|
527
|
+
void current.then(() => {
|
|
528
|
+
pendingClassifications.delete(key);
|
|
529
|
+
if (messageClassifications.get(message.messageId) === current)
|
|
530
|
+
messageClassifications.delete(message.messageId);
|
|
531
|
+
if (classificationTails.get(message.channelId) === current)
|
|
532
|
+
classificationTails.delete(message.channelId);
|
|
533
|
+
});
|
|
534
|
+
return current;
|
|
416
535
|
};
|
|
417
536
|
await connectGateway(
|
|
418
537
|
session.url ?? initialUrl,
|
|
@@ -459,7 +578,7 @@ export function createDiscordAdapter(
|
|
|
459
578
|
return message;
|
|
460
579
|
}
|
|
461
580
|
const channel = await messengerFetchJson(
|
|
462
|
-
`${
|
|
581
|
+
`${DISCORD_API}/channels/${encodeURIComponent(message.channelId)}`,
|
|
463
582
|
{ headers: headers() },
|
|
464
583
|
signal,
|
|
465
584
|
deps.fetch,
|
|
@@ -474,7 +593,7 @@ export function createDiscordAdapter(
|
|
|
474
593
|
}
|
|
475
594
|
if (channel.type !== 0 && channel.type !== 5) throw new Error("Discord channel cannot host a reply thread");
|
|
476
595
|
const thread = await messengerFetchJson(
|
|
477
|
-
`${
|
|
596
|
+
`${DISCORD_API}/channels/${encodeURIComponent(message.channelId)}/messages/${encodeURIComponent(message.messageId)}/threads`,
|
|
478
597
|
{ method: "POST", headers: headers(), body: JSON.stringify({ name: `Request ${message.messageId}` }) },
|
|
479
598
|
signal,
|
|
480
599
|
deps.fetch,
|
|
@@ -485,7 +604,7 @@ export function createDiscordAdapter(
|
|
|
485
604
|
return { ...message, threadId: thread.id };
|
|
486
605
|
},
|
|
487
606
|
async status(message, state, signal) {
|
|
488
|
-
const base = `${
|
|
607
|
+
const base = `${DISCORD_API}/channels/${encodeURIComponent(message.channelId)}/messages/${encodeURIComponent(message.messageId)}/reactions`;
|
|
489
608
|
const processing = encodeURIComponent("⏳");
|
|
490
609
|
if (state !== "processing") {
|
|
491
610
|
try {
|
|
@@ -500,17 +619,44 @@ export function createDiscordAdapter(
|
|
|
500
619
|
}
|
|
501
620
|
}
|
|
502
621
|
const emoji = state === "processing" ? "⏳" : state === "success" ? "✅" : "❌";
|
|
622
|
+
try {
|
|
623
|
+
await messengerFetchJson(
|
|
624
|
+
`${base}/${encodeURIComponent(emoji)}/@me`,
|
|
625
|
+
{ method: "PUT", headers: headers() },
|
|
626
|
+
signal,
|
|
627
|
+
deps.fetch,
|
|
628
|
+
);
|
|
629
|
+
} finally {
|
|
630
|
+
if (state === "processing" && signal?.aborted) {
|
|
631
|
+
try {
|
|
632
|
+
await messengerFetchJson(
|
|
633
|
+
`${base}/${processing}/@me`,
|
|
634
|
+
{ method: "DELETE", headers: headers() },
|
|
635
|
+
AbortSignal.timeout(3_000),
|
|
636
|
+
deps.fetch,
|
|
637
|
+
);
|
|
638
|
+
} catch {
|
|
639
|
+
logger.warn("Discord late processing reaction removal failed");
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
},
|
|
644
|
+
async clearStatus(message, signal) {
|
|
645
|
+
const base = `${DISCORD_API}/channels/${encodeURIComponent(message.channelId)}/messages/${encodeURIComponent(message.messageId)}/reactions`;
|
|
503
646
|
await messengerFetchJson(
|
|
504
|
-
`${base}/${encodeURIComponent(
|
|
505
|
-
{ method: "
|
|
647
|
+
`${base}/${encodeURIComponent("⏳")}/@me`,
|
|
648
|
+
{ method: "DELETE", headers: headers() },
|
|
506
649
|
signal,
|
|
507
650
|
deps.fetch,
|
|
508
651
|
);
|
|
509
652
|
},
|
|
653
|
+
releaseActivity(message) {
|
|
654
|
+
activityMessages.delete(JSON.stringify([message.threadId ?? message.channelId, message.messageId]));
|
|
655
|
+
},
|
|
510
656
|
async activity(message, text, signal) {
|
|
511
657
|
const channelId = message.threadId ?? message.channelId;
|
|
512
658
|
const key = JSON.stringify([channelId, message.messageId]);
|
|
513
|
-
const messages = `${
|
|
659
|
+
const messages = `${DISCORD_API}/channels/${encodeURIComponent(channelId)}/messages`;
|
|
514
660
|
const statusId = activityMessages.get(key);
|
|
515
661
|
if (text === undefined) {
|
|
516
662
|
if (!statusId) return;
|
|
@@ -560,6 +706,19 @@ export function createDiscordAdapter(
|
|
|
560
706
|
);
|
|
561
707
|
if (!isRecord(created) || typeof created.id !== "string" || !created.id)
|
|
562
708
|
throw new Error("Discord activity message returned no ID");
|
|
709
|
+
if (signal?.aborted) {
|
|
710
|
+
try {
|
|
711
|
+
await messengerFetchJson(
|
|
712
|
+
`${messages}/${encodeURIComponent(created.id)}`,
|
|
713
|
+
{ method: "DELETE", headers: headers() },
|
|
714
|
+
AbortSignal.timeout(3_000),
|
|
715
|
+
deps.fetch,
|
|
716
|
+
);
|
|
717
|
+
} catch {
|
|
718
|
+
logger.warn("Discord late activity removal failed");
|
|
719
|
+
}
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
563
722
|
activityMessages.set(key, created.id);
|
|
564
723
|
},
|
|
565
724
|
async send(message, text, signal) {
|
|
@@ -571,7 +730,7 @@ export function createDiscordAdapter(
|
|
|
571
730
|
files = result.files;
|
|
572
731
|
} catch {
|
|
573
732
|
await messengerFetchJson(
|
|
574
|
-
`${
|
|
733
|
+
`${DISCORD_API}/channels/${encodeURIComponent(message.threadId ?? message.channelId)}/messages`,
|
|
575
734
|
{
|
|
576
735
|
method: "POST",
|
|
577
736
|
headers: headers(),
|
|
@@ -614,7 +773,7 @@ export function createDiscordAdapter(
|
|
|
614
773
|
});
|
|
615
774
|
}
|
|
616
775
|
await messengerFetchJson(
|
|
617
|
-
`${
|
|
776
|
+
`${DISCORD_API}/channels/${encodeURIComponent(message.threadId ?? message.channelId)}/messages`,
|
|
618
777
|
{
|
|
619
778
|
method: "POST",
|
|
620
779
|
headers: requestHeaders,
|
|
@@ -655,7 +814,7 @@ export function createDiscordAdapter(
|
|
|
655
814
|
for (let attempt = 0; ; attempt++) {
|
|
656
815
|
try {
|
|
657
816
|
await messengerFetchJson(
|
|
658
|
-
`${
|
|
817
|
+
`${DISCORD_API}/channels/${encodeURIComponent(message.threadId ?? message.channelId)}/messages`,
|
|
659
818
|
{ method: "POST", headers: requestHeaders, body: form },
|
|
660
819
|
signal,
|
|
661
820
|
deps.fetch,
|
|
@@ -668,7 +827,7 @@ export function createDiscordAdapter(
|
|
|
668
827
|
}
|
|
669
828
|
if (!signal?.aborted) {
|
|
670
829
|
await messengerFetchJson(
|
|
671
|
-
`${
|
|
830
|
+
`${DISCORD_API}/channels/${encodeURIComponent(message.threadId ?? message.channelId)}/messages`,
|
|
672
831
|
{
|
|
673
832
|
method: "POST",
|
|
674
833
|
headers: headers(),
|