windsor-bot 0.1.16 → 0.2.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/README.md +17 -190
- package/dist/ai.js +10 -91
- package/dist/bot.js +152 -151
- package/dist/index.js +1 -5
- package/dist/server.js +24 -8
- package/dist/web/app.bundle.js +5 -5
- package/dist/web/app.js +5 -5
- package/package.json +2 -3
package/dist/bot.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { ChannelType, Client, GatewayIntentBits, } from 'discord.js';
|
|
2
|
-
import {
|
|
1
|
+
import { ChannelType, Client, GatewayIntentBits, Partials, } from 'discord.js';
|
|
2
|
+
import { generateIcon, isIconCached } from './ai.js';
|
|
3
3
|
import { Commands } from './commands/index.js';
|
|
4
4
|
import { getCurrentConfig, reconcileChannels } from './config.js';
|
|
5
5
|
import { formatTimestamp } from './printer.js';
|
|
@@ -31,6 +31,15 @@ export function extractUrls(text) {
|
|
|
31
31
|
export function stripUrls(text) {
|
|
32
32
|
return text.replace(URL_REGEX, '').replace(/\s+/g, ' ').trim();
|
|
33
33
|
}
|
|
34
|
+
async function removeOwnReactionSafe(reaction) {
|
|
35
|
+
try {
|
|
36
|
+
if (reaction.me)
|
|
37
|
+
await reaction.users.remove();
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
34
43
|
export function replaceUrlsInText(text, urls) {
|
|
35
44
|
if (urls.length === 0)
|
|
36
45
|
return text;
|
|
@@ -83,10 +92,10 @@ async function removeReactionSafe(message, emoji) {
|
|
|
83
92
|
// ignore
|
|
84
93
|
}
|
|
85
94
|
}
|
|
86
|
-
async function reactSafe(message, emoji) {
|
|
95
|
+
async function reactSafe(message, emoji, clearThinking = true) {
|
|
87
96
|
try {
|
|
88
97
|
await message.react(emoji);
|
|
89
|
-
if (emoji !== Reaction.thinking) {
|
|
98
|
+
if (emoji !== Reaction.thinking && clearThinking) {
|
|
90
99
|
await removeReactionSafe(message, Reaction.thinking);
|
|
91
100
|
}
|
|
92
101
|
if (emoji === Reaction.ok) {
|
|
@@ -127,6 +136,7 @@ export function parseCommand(content) {
|
|
|
127
136
|
}
|
|
128
137
|
export function createWindsorBot() {
|
|
129
138
|
const client = new Client({
|
|
139
|
+
partials: [Partials.Message, Partials.Reaction, Partials.User],
|
|
130
140
|
intents: [
|
|
131
141
|
GatewayIntentBits.Guilds,
|
|
132
142
|
GatewayIntentBits.GuildMessages,
|
|
@@ -136,18 +146,18 @@ export function createWindsorBot() {
|
|
|
136
146
|
});
|
|
137
147
|
const pendingPrints = new Map();
|
|
138
148
|
const processingPending = new Set();
|
|
149
|
+
const reusableStates = new Map();
|
|
139
150
|
let pendingPoller = null;
|
|
140
151
|
client.on('clientReady', () => void onReady());
|
|
141
152
|
client.on('messageCreate', (msg) => void onMessage(msg));
|
|
153
|
+
client.on('messageReactionAdd', (reaction, user) => void onReactionAdd(reaction, user));
|
|
154
|
+
client.on('messageReactionRemove', (reaction, user) => void onReactionRemove(reaction, user));
|
|
142
155
|
const bot = {
|
|
143
156
|
start,
|
|
144
157
|
destroy,
|
|
145
158
|
getClient,
|
|
146
159
|
handleImmediatePrint,
|
|
147
160
|
handleAccumulatingPrint,
|
|
148
|
-
handleRecurringSetup,
|
|
149
|
-
executeRecurringTask,
|
|
150
|
-
advanceRecurring,
|
|
151
161
|
handleOnDemand,
|
|
152
162
|
};
|
|
153
163
|
async function start(token) {
|
|
@@ -163,6 +173,74 @@ export function createWindsorBot() {
|
|
|
163
173
|
function getClient() {
|
|
164
174
|
return client;
|
|
165
175
|
}
|
|
176
|
+
function getReusableConfig(message) {
|
|
177
|
+
const config = getCurrentConfig();
|
|
178
|
+
const mapping = config.channels.find(m => m.channelId === message.channelId);
|
|
179
|
+
if (!mapping || mapping.config.type !== 'reusable-list')
|
|
180
|
+
return null;
|
|
181
|
+
if (config.serverId && message.guildId !== config.serverId)
|
|
182
|
+
return null;
|
|
183
|
+
if (message.author.bot || message.reference)
|
|
184
|
+
return null;
|
|
185
|
+
return mapping.config;
|
|
186
|
+
}
|
|
187
|
+
async function hydrateReaction(reaction) {
|
|
188
|
+
const fullReaction = reaction.partial ? await reaction.fetch() : reaction;
|
|
189
|
+
if (fullReaction.message.partial)
|
|
190
|
+
await fullReaction.message.fetch();
|
|
191
|
+
return { reaction: fullReaction, message: fullReaction.message };
|
|
192
|
+
}
|
|
193
|
+
async function findStartupReusableReaction(message) {
|
|
194
|
+
if ([...message.reactions.cache.values()].some(reaction => reaction.me))
|
|
195
|
+
return null;
|
|
196
|
+
for (const reaction of message.reactions.cache.values()) {
|
|
197
|
+
const users = await reaction.users.fetch();
|
|
198
|
+
if ([...users.values()].some(user => !user.bot))
|
|
199
|
+
return reaction;
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
async function onReactionAdd(reactionEvent, user) {
|
|
204
|
+
if (user.bot)
|
|
205
|
+
return;
|
|
206
|
+
try {
|
|
207
|
+
const { reaction, message } = await hydrateReaction(reactionEvent);
|
|
208
|
+
const config = getReusableConfig(message);
|
|
209
|
+
if (!config)
|
|
210
|
+
return;
|
|
211
|
+
if ([...message.reactions.cache.values()].some(candidate => candidate.me))
|
|
212
|
+
return;
|
|
213
|
+
if (reusableStates.has(message.id))
|
|
214
|
+
return;
|
|
215
|
+
const state = { reaction, cancelled: false, status: 'printing' };
|
|
216
|
+
reusableStates.set(message.id, state);
|
|
217
|
+
await handleReusablePrint(message, config, state);
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
logEvent('error', `Reusable List reaction handling failed: ${err}`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function onReactionRemove(reactionEvent, user) {
|
|
224
|
+
if (user.bot)
|
|
225
|
+
return;
|
|
226
|
+
try {
|
|
227
|
+
const { reaction, message } = await hydrateReaction(reactionEvent);
|
|
228
|
+
if (!getReusableConfig(message))
|
|
229
|
+
return;
|
|
230
|
+
const state = reusableStates.get(message.id);
|
|
231
|
+
if (state) {
|
|
232
|
+
state.cancelled = true;
|
|
233
|
+
pendingPrints.delete(message.id);
|
|
234
|
+
await removeReactionSafe(message, Reaction.waiting);
|
|
235
|
+
if (state.status !== 'printing')
|
|
236
|
+
reusableStates.delete(message.id);
|
|
237
|
+
}
|
|
238
|
+
await removeOwnReactionSafe(reaction);
|
|
239
|
+
}
|
|
240
|
+
catch (err) {
|
|
241
|
+
logEvent('error', `Reusable List reaction removal failed: ${err}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
166
244
|
function isTextOnlyChannel(ch) {
|
|
167
245
|
return ch.isTextBased()
|
|
168
246
|
&& ch.type !== ChannelType.GuildVoice
|
|
@@ -244,16 +322,7 @@ export function createWindsorBot() {
|
|
|
244
322
|
if (!mapping)
|
|
245
323
|
continue;
|
|
246
324
|
try {
|
|
247
|
-
|
|
248
|
-
if (mapping.config.type === 'recurring-print') {
|
|
249
|
-
const page1 = await textCh.messages.fetch({ limit: 100 });
|
|
250
|
-
const oldest = [...page1.values()].reduce((a, b) => BigInt(a.id) < BigInt(b.id) ? a : b);
|
|
251
|
-
const page2 = await textCh.messages.fetch({ limit: 100, before: oldest.id });
|
|
252
|
-
fetchedMessages = new Map([...page1, ...page2]);
|
|
253
|
-
}
|
|
254
|
-
else {
|
|
255
|
-
fetchedMessages = await textCh.messages.fetch({ limit: 100 });
|
|
256
|
-
}
|
|
325
|
+
const fetchedMessages = await textCh.messages.fetch({ limit: 100 });
|
|
257
326
|
const sorted = [...fetchedMessages.values()].sort((a, b) => Number(BigInt(a.id) - BigInt(b.id)));
|
|
258
327
|
await processBehaviorStartup(textCh, mapping.config, sorted);
|
|
259
328
|
}
|
|
@@ -309,8 +378,17 @@ export function createWindsorBot() {
|
|
|
309
378
|
}
|
|
310
379
|
break;
|
|
311
380
|
}
|
|
312
|
-
case '
|
|
313
|
-
|
|
381
|
+
case 'reusable-list': {
|
|
382
|
+
for (const msg of messages) {
|
|
383
|
+
if (msg.author.bot || msg.reference || msg.author.id === botId)
|
|
384
|
+
continue;
|
|
385
|
+
const reaction = await findStartupReusableReaction(msg);
|
|
386
|
+
if (!reaction || reusableStates.has(msg.id))
|
|
387
|
+
continue;
|
|
388
|
+
const state = { reaction, cancelled: false, status: 'printing' };
|
|
389
|
+
reusableStates.set(msg.id, state);
|
|
390
|
+
await handleReusablePrint(msg, config, state);
|
|
391
|
+
}
|
|
314
392
|
break;
|
|
315
393
|
}
|
|
316
394
|
case 'on-demand': {
|
|
@@ -333,35 +411,6 @@ export function createWindsorBot() {
|
|
|
333
411
|
}
|
|
334
412
|
}
|
|
335
413
|
}
|
|
336
|
-
async function startupRecurring(channel, config, messages) {
|
|
337
|
-
const botId = client.user?.id;
|
|
338
|
-
const scheduleReplies = messages.filter(m => m.author.id === botId && m.content.startsWith('Got it. I will print ⟪'));
|
|
339
|
-
for (const scheduleReply of scheduleReplies) {
|
|
340
|
-
const parentId = scheduleReply.reference?.messageId;
|
|
341
|
-
if (!parentId)
|
|
342
|
-
continue;
|
|
343
|
-
const statusReplies = messages.filter(m => m.author.id === botId &&
|
|
344
|
-
m.reference?.messageId === scheduleReply.id &&
|
|
345
|
-
(m.content.startsWith('Printed at') || m.content.includes('has expired')));
|
|
346
|
-
const latestStatus = statusReplies[statusReplies.length - 1];
|
|
347
|
-
if (latestStatus?.content.includes('has expired')) {
|
|
348
|
-
continue;
|
|
349
|
-
}
|
|
350
|
-
if (latestStatus && latestStatus.content.startsWith('Printed at') && !await hasReaction(latestStatus, Reaction.ok)) {
|
|
351
|
-
await advanceRecurring(scheduleReply, latestStatus, config);
|
|
352
|
-
continue;
|
|
353
|
-
}
|
|
354
|
-
if (!latestStatus) {
|
|
355
|
-
const match = /at (.+)$/.exec(scheduleReply.content);
|
|
356
|
-
if (!match?.[1])
|
|
357
|
-
continue;
|
|
358
|
-
const nextTime = new Date(match[1]);
|
|
359
|
-
if (isNaN(nextTime.getTime()))
|
|
360
|
-
continue;
|
|
361
|
-
scheduleRecurringTask(scheduleReply, nextTime, config, bot);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
414
|
async function onMessage(message) {
|
|
366
415
|
if (message.author.bot)
|
|
367
416
|
return;
|
|
@@ -394,8 +443,7 @@ export function createWindsorBot() {
|
|
|
394
443
|
}
|
|
395
444
|
break;
|
|
396
445
|
}
|
|
397
|
-
case '
|
|
398
|
-
await handleRecurringSetup(message, mapping.config);
|
|
446
|
+
case 'reusable-list':
|
|
399
447
|
break;
|
|
400
448
|
case 'on-demand':
|
|
401
449
|
await handleOnDemand(message);
|
|
@@ -424,9 +472,12 @@ export function createWindsorBot() {
|
|
|
424
472
|
`${message.author.username} · ${formatTimestamp(message.createdAt)}`,
|
|
425
473
|
];
|
|
426
474
|
}
|
|
475
|
+
let thinkingReacted = false;
|
|
427
476
|
if (config.includeIcon) {
|
|
428
|
-
await reactSafe(message, Reaction.thinking);
|
|
429
477
|
const iconCacheDir = getCurrentConfig().iconCacheDir ?? './icon-cache';
|
|
478
|
+
thinkingReacted = !await isIconCached(strippedText, iconCacheDir);
|
|
479
|
+
if (thinkingReacted)
|
|
480
|
+
await reactSafe(message, Reaction.thinking);
|
|
430
481
|
const iconPath = await generateIcon(strippedText, iconCacheDir);
|
|
431
482
|
if (iconPath)
|
|
432
483
|
job.iconPath = iconPath;
|
|
@@ -435,7 +486,8 @@ export function createWindsorBot() {
|
|
|
435
486
|
if (await hasAnyReaction(message)) {
|
|
436
487
|
const waiting = message.reactions.cache.get(Reaction.waiting)?.me;
|
|
437
488
|
if (!retryPending && (!retryFailed || !message.reactions.cache.get(Reaction.fail)?.me)) {
|
|
438
|
-
|
|
489
|
+
if (thinkingReacted)
|
|
490
|
+
await removeReactionSafe(message, Reaction.thinking);
|
|
439
491
|
return;
|
|
440
492
|
}
|
|
441
493
|
if (retryPending && !waiting)
|
|
@@ -444,21 +496,22 @@ export function createWindsorBot() {
|
|
|
444
496
|
try {
|
|
445
497
|
await printJob(job);
|
|
446
498
|
pendingPrints.delete(message.id);
|
|
447
|
-
await reactSafe(message, Reaction.ok);
|
|
499
|
+
await reactSafe(message, Reaction.ok, thinkingReacted);
|
|
448
500
|
logEvent('print', `Printed immediate message from ${message.author.username}`);
|
|
449
501
|
}
|
|
450
502
|
catch (err) {
|
|
451
503
|
if (isPrinterUnavailableError(err)) {
|
|
452
|
-
await reactSafe(message, Reaction.waiting);
|
|
504
|
+
await reactSafe(message, Reaction.waiting, thinkingReacted);
|
|
453
505
|
enqueuePendingPrint(message, () => handleImmediatePrint(message, config, false, true));
|
|
454
506
|
logEvent('info', `Printer unavailable for immediate message from ${message.author.username}`);
|
|
455
507
|
return;
|
|
456
508
|
}
|
|
457
|
-
await reactSafe(message, Reaction.fail);
|
|
509
|
+
await reactSafe(message, Reaction.fail, thinkingReacted);
|
|
458
510
|
await replySafe(message, `⏸️ Print failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
459
511
|
logEvent('error', `Print failed: ${err}`);
|
|
460
512
|
}
|
|
461
|
-
|
|
513
|
+
if (thinkingReacted)
|
|
514
|
+
await removeReactionSafe(message, Reaction.thinking);
|
|
462
515
|
}
|
|
463
516
|
async function handleAccumulatingPrint(triggerMessage, config, allMessages, retryPending = false) {
|
|
464
517
|
const botId = client.user?.id;
|
|
@@ -529,99 +582,70 @@ export function createWindsorBot() {
|
|
|
529
582
|
logEvent('error', `Accumulating print failed: ${err}`);
|
|
530
583
|
}
|
|
531
584
|
}
|
|
532
|
-
async function
|
|
533
|
-
|
|
585
|
+
async function handleReusablePrint(message, config, state) {
|
|
586
|
+
if (state.cancelled) {
|
|
587
|
+
reusableStates.delete(message.id);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
534
590
|
try {
|
|
535
|
-
|
|
536
|
-
if (!parsed) {
|
|
537
|
-
await reactSafe(message, Reaction.what);
|
|
538
|
-
await replySafe(message, '⁉️ Could not parse a schedule from your message. Please try again with a clearer schedule.');
|
|
539
|
-
return;
|
|
540
|
-
}
|
|
541
|
-
const nextStr = formatScheduleDate(parsed.nextOccurrence);
|
|
542
|
-
await reactSafe(message, Reaction.ok);
|
|
543
|
-
const reply = await replySafe(message, `Got it. I will print ⟪${parsed.message}⟫ at ${nextStr}`);
|
|
544
|
-
if (reply) {
|
|
545
|
-
scheduleRecurringTask(reply, parsed.nextOccurrence, config, bot);
|
|
546
|
-
}
|
|
591
|
+
await message.fetch();
|
|
547
592
|
}
|
|
548
593
|
catch (err) {
|
|
549
|
-
|
|
550
|
-
|
|
594
|
+
reusableStates.delete(message.id);
|
|
595
|
+
pendingPrints.delete(message.id);
|
|
596
|
+
logEvent('info', `Cancelled reusable print for unavailable message ${message.id}: ${err}`);
|
|
597
|
+
return;
|
|
551
598
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
const
|
|
555
|
-
|
|
599
|
+
const rawContent = message.content;
|
|
600
|
+
const urls = extractUrls(rawContent);
|
|
601
|
+
const text = replaceUrlsInText(rawContent, urls);
|
|
602
|
+
const strippedText = stripUrls(rawContent);
|
|
603
|
+
if (strippedText.length > MAX_MESSAGE_CHARS) {
|
|
604
|
+
reusableStates.delete(message.id);
|
|
605
|
+
await removeReactionSafe(message, Reaction.waiting);
|
|
606
|
+
await replySafe(message, '⁉️ This message is too long to print.');
|
|
556
607
|
return;
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
const textWithLinks = replaceUrlsInText(text, urls);
|
|
560
|
-
const strippedText = stripUrls(text);
|
|
561
|
-
const job = {
|
|
562
|
-
lines: [textWithLinks],
|
|
563
|
-
urls,
|
|
564
|
-
};
|
|
608
|
+
}
|
|
609
|
+
const job = { lines: [text], urls };
|
|
565
610
|
if (config.header)
|
|
566
611
|
job.header = config.header;
|
|
567
612
|
if (config.footer)
|
|
568
613
|
job.footer = config.footer;
|
|
569
614
|
if (config.includeMetadata) {
|
|
570
|
-
job.metadataLines = [
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
const iconCacheDir = getCurrentConfig().iconCacheDir ?? './icon-cache';
|
|
574
|
-
const iconPath = await generateIcon(strippedText, iconCacheDir);
|
|
575
|
-
if (iconPath)
|
|
576
|
-
job.iconPath = iconPath;
|
|
615
|
+
job.metadataLines = [
|
|
616
|
+
`${message.author.username} · ${formatTimestamp(message.createdAt)}`,
|
|
617
|
+
];
|
|
577
618
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
await removeReactionSafe(scheduleReply, Reaction.thinking);
|
|
619
|
+
if (state.cancelled) {
|
|
620
|
+
reusableStates.delete(message.id);
|
|
581
621
|
return;
|
|
582
622
|
}
|
|
583
|
-
const printedAt = formatTimestamp(new Date());
|
|
584
623
|
try {
|
|
585
624
|
await printJob(job);
|
|
586
625
|
}
|
|
587
626
|
catch (err) {
|
|
588
|
-
|
|
589
|
-
|
|
627
|
+
if (isPrinterUnavailableError(err)) {
|
|
628
|
+
state.status = 'pending';
|
|
629
|
+
await reactSafe(message, Reaction.waiting);
|
|
630
|
+
enqueuePendingPrint(message, () => handleReusablePrint(message, config, state));
|
|
631
|
+
logEvent('info', `Printer unavailable for reusable message in #${message.channelId}`);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
reusableStates.delete(message.id);
|
|
635
|
+
await removeReactionSafe(message, Reaction.waiting);
|
|
636
|
+
await replySafe(message, `⏸️ Print failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
637
|
+
logEvent('error', `Reusable List print failed: ${err}`);
|
|
590
638
|
return;
|
|
591
639
|
}
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (!nextOccurrence) {
|
|
597
|
-
statusReply = await replySafe(scheduleReply, 'This occurrence has expired and no more prints are scheduled');
|
|
598
|
-
}
|
|
599
|
-
else {
|
|
600
|
-
const nextStr = formatScheduleDate(nextOccurrence);
|
|
601
|
-
statusReply = await replySafe(scheduleReply, `Printed at ${printedAt}. The next print will be at ${nextStr}.`);
|
|
602
|
-
}
|
|
603
|
-
if (statusReply) {
|
|
604
|
-
await reactSafe(statusReply, Reaction.ok);
|
|
605
|
-
}
|
|
606
|
-
await removeReactionSafe(scheduleReply, Reaction.thinking);
|
|
607
|
-
if (nextOccurrence && statusReply) {
|
|
608
|
-
scheduleRecurringTask(scheduleReply, nextOccurrence, config, bot);
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
async function advanceRecurring(scheduleReply, latestStatusReply, config) {
|
|
612
|
-
const match = /⟪(.+?)⟫/.exec(scheduleReply.content);
|
|
613
|
-
if (!match?.[1])
|
|
614
|
-
return;
|
|
615
|
-
const nextOccurrence = await getNextOccurrence(match[1], new Date());
|
|
616
|
-
if (!nextOccurrence) {
|
|
617
|
-
await replySafe(latestStatusReply, 'This occurrence has expired and no more prints are scheduled');
|
|
618
|
-
await removeReactionSafe(latestStatusReply, Reaction.thinking);
|
|
640
|
+
pendingPrints.delete(message.id);
|
|
641
|
+
await removeReactionSafe(message, Reaction.waiting);
|
|
642
|
+
if (state.cancelled) {
|
|
643
|
+
reusableStates.delete(message.id);
|
|
619
644
|
return;
|
|
620
645
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
scheduleRecurringTask(scheduleReply, nextOccurrence, config, bot);
|
|
646
|
+
await message.react(state.reaction.emoji);
|
|
647
|
+
state.status = 'printed';
|
|
648
|
+
logEvent('print', `Printed reusable message from ${message.author.username}`);
|
|
625
649
|
}
|
|
626
650
|
async function retryFailedMessages(message) {
|
|
627
651
|
const mapping = getCurrentConfig().channels.find(m => m.channelId === message.channelId);
|
|
@@ -690,26 +714,3 @@ export function createWindsorBot() {
|
|
|
690
714
|
}
|
|
691
715
|
return bot;
|
|
692
716
|
}
|
|
693
|
-
const scheduledTasks = new Map();
|
|
694
|
-
export function scheduleRecurringTask(scheduleReply, nextOccurrence, config, bot) {
|
|
695
|
-
const key = scheduleReply.id;
|
|
696
|
-
const existing = scheduledTasks.get(key);
|
|
697
|
-
if (existing)
|
|
698
|
-
clearTimeout(existing.timerId);
|
|
699
|
-
const delay = Math.max(0, nextOccurrence.getTime() - Date.now());
|
|
700
|
-
const timerId = setTimeout(() => {
|
|
701
|
-
scheduledTasks.delete(key);
|
|
702
|
-
void bot.executeRecurringTask(scheduleReply, config);
|
|
703
|
-
}, delay);
|
|
704
|
-
scheduledTasks.set(key, { scheduleReply, nextOccurrence, config, bot, timerId });
|
|
705
|
-
}
|
|
706
|
-
export function checkScheduledTasks() {
|
|
707
|
-
const now = new Date();
|
|
708
|
-
for (const [key, task] of scheduledTasks) {
|
|
709
|
-
if (task.nextOccurrence <= now) {
|
|
710
|
-
clearTimeout(task.timerId);
|
|
711
|
-
scheduledTasks.delete(key);
|
|
712
|
-
void task.bot.executeRecurringTask(task.scheduleReply, task.config);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { getCurrentConfig, loadConfig } from './config.js';
|
|
3
3
|
import { logEvent, startDiagnosticsServer, setDiscordChannels, setRestartHandler, setRefreshChannelsHandler } from './server.js';
|
|
4
|
-
import { createWindsorBot
|
|
4
|
+
import { createWindsorBot } from './bot.js';
|
|
5
5
|
import { ChannelType } from 'discord.js';
|
|
6
6
|
const { config } = await loadConfig();
|
|
7
7
|
startDiagnosticsServer(config.diagnosticsPort);
|
|
@@ -39,10 +39,6 @@ export async function restartBot() {
|
|
|
39
39
|
const { config } = await loadConfig();
|
|
40
40
|
await startBot(config);
|
|
41
41
|
}
|
|
42
|
-
// Recurring task checker
|
|
43
|
-
setInterval(() => {
|
|
44
|
-
checkScheduledTasks();
|
|
45
|
-
}, 30_000);
|
|
46
42
|
setRestartHandler(restartBot);
|
|
47
43
|
async function refreshChannels() {
|
|
48
44
|
if (!bot) {
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createServer } from 'http';
|
|
2
2
|
import { execFile } from 'child_process';
|
|
3
3
|
import { build } from 'esbuild';
|
|
4
|
-
import { readFile, stat } from 'fs/promises';
|
|
4
|
+
import { access, readFile, stat } from 'fs/promises';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
6
|
import { spawn } from 'child_process';
|
|
7
7
|
import { dirname, join } from 'path';
|
|
@@ -65,13 +65,29 @@ async function readPackageVersion() {
|
|
|
65
65
|
return packageJson.version;
|
|
66
66
|
}
|
|
67
67
|
async function updateGlobalBot() {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
const npmCliCandidates = [
|
|
69
|
+
process.env['npm_execpath'],
|
|
70
|
+
join(dirname(process.execPath), '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
71
|
+
'/usr/local/lib/node_modules/npm/bin/npm-cli.js',
|
|
72
|
+
'/opt/homebrew/lib/node_modules/npm/bin/npm-cli.js',
|
|
73
|
+
].filter((candidate) => Boolean(candidate));
|
|
74
|
+
let npmCliPath;
|
|
75
|
+
for (const candidate of npmCliCandidates) {
|
|
76
|
+
try {
|
|
77
|
+
await access(candidate);
|
|
78
|
+
npmCliPath = candidate;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
// Try the next known npm installation location.
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!npmCliPath)
|
|
86
|
+
throw new Error('Unable to locate npm');
|
|
87
|
+
const npmCli = npmCliPath;
|
|
88
|
+
const runNpm = (args) => execFileAsync(process.execPath, [npmCli, ...args]);
|
|
89
|
+
await runNpm(['install', '-g', 'windsor-bot@latest']);
|
|
90
|
+
const { stdout } = await runNpm(['root', '-g']);
|
|
75
91
|
const packageJsonPath = join(stdout.trim(), 'windsor-bot', 'package.json');
|
|
76
92
|
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
77
93
|
if (typeof packageJson.version !== 'string')
|