windsor-bot 0.1.11 → 0.1.13
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/assets/poker/README.txt +6 -0
- package/assets/poker/club.svg +11 -0
- package/assets/poker/diamond.svg +8 -0
- package/assets/poker/heart.svg +9 -0
- package/assets/poker/spade.svg +9 -0
- package/dist/bot.js +110 -17
- package/dist/commands/index.js +2 -0
- package/dist/commands/poker.js +139 -0
- package/dist/index.js +9 -17
- package/dist/printing/index.js +39 -13
- package/dist/reactions.js +2 -1
- package/dist/server.js +52 -0
- package/dist/tests/config.test.js +8 -1
- package/dist/tests/poker.test.js +12 -0
- package/dist/tests/urls.test.js +11 -1
- package/dist/web/app.bundle.js +42 -2
- package/dist/web/app.js +41 -1
- package/package.json +2 -1
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
These suit SVGs are CC0 assets from Wikimedia Commons:
|
|
2
|
+
|
|
3
|
+
- https://commons.wikimedia.org/wiki/File:Playing_Card_Suit_-_Swiss-French_Club.svg
|
|
4
|
+
- https://commons.wikimedia.org/wiki/File:Playing_Card_Suit_-_Swiss-French_Diamond.svg
|
|
5
|
+
- https://commons.wikimedia.org/wiki/File:Playing_Card_Suit_-_Swiss-French_Heart.svg
|
|
6
|
+
- https://commons.wikimedia.org/wiki/File:Playing_Card_Suit_-_Swiss-French_Spade.svg
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
5
|
+
width="72px" height="72px" viewBox="0 0 72 72" enable-background="new 0 0 72 72" xml:space="preserve">
|
|
6
|
+
<path fill="#0A0B0B" d="M19.21,28.22c-12.744,0-12.976,16.22,0,16.22c7.647,0,11.818-7.491,14.367-4.865
|
|
7
|
+
c2.395,2.472-1.391,15.911-1.699,18.073c-0.464,2.936,2.008,5.253,4.712,5.253c2.857,0,4.865-2.317,4.325-5.253
|
|
8
|
+
c-0.387-2.162-3.785-15.524-1.313-18.073c2.395-2.472,7.029,4.942,15.217,4.865c11.662-0.076,11.662-16.22,0-16.22
|
|
9
|
+
c-8.033,0-12.745,7.415-15.217,4.943c-2.317-2.317,4.943-5.021,4.943-12.899c0-11.432-16.065-11.432-16.065-0.077
|
|
10
|
+
c0,7.956,7.492,10.736,5.098,12.976C30.951,35.635,26.857,28.22,19.21,28.22z"/>
|
|
11
|
+
</svg>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
5
|
+
width="72px" height="72px" viewBox="0 0 72 72" enable-background="new 0 0 72 72" xml:space="preserve">
|
|
6
|
+
<path fill="#ED2224" d="M36.143,9.601c-5.631,9.257-16.662,20.982-25.688,27.154c9.026,5.708,20.057,17.896,25.688,26.846
|
|
7
|
+
c5.632-8.949,17.28-21.138,25.998-27.077C53.423,30.583,41.774,18.857,36.143,9.601z"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
5
|
+
width="72px" height="72px" viewBox="0 0 72 72" enable-background="new 0 0 72 72" xml:space="preserve">
|
|
6
|
+
<path fill="#ED2224" d="M35.84,21.358c-11.368-11.92-27-6-27,9c0,14.684,14.842,18.631,21.158,24.947
|
|
7
|
+
c0.947,0.947,3.711,3.474,5.921,7.895c2.289-4.421,4.974-6.947,5.921-7.895c6-6.316,21-10.264,21-24.632
|
|
8
|
+
C62.84,15.358,47.051,9.438,35.84,21.358z"/>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Generator: Adobe Illustrator 15.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
3
|
+
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
4
|
+
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
5
|
+
width="72px" height="72px" viewBox="0 0 72 72" enable-background="new 0 0 72 72" xml:space="preserve">
|
|
6
|
+
<path fill="#010101" d="M36.067,10.646C29.462,25.327,11.332,27.309,9.35,39.199C7.441,51.017,22.636,58.356,33.572,49.988
|
|
7
|
+
c1.468,3.084-0.367,8.001-3.377,12.111c1.541-0.513,4.624-0.733,5.872-0.733s4.33,0.221,5.872,0.733
|
|
8
|
+
c-3.01-4.11-4.771-9.027-3.377-12.111c12.772,7.928,26.352,0.882,24.443-10.789C60.95,26.648,42.674,25.327,36.067,10.646z"/>
|
|
9
|
+
</svg>
|
package/dist/bot.js
CHANGED
|
@@ -3,11 +3,12 @@ import { formatScheduleDate, generateIcon, getNextOccurrence, parseRecurringSche
|
|
|
3
3
|
import { Commands } from './commands/index.js';
|
|
4
4
|
import { getCurrentConfig, reconcileChannels } from './config.js';
|
|
5
5
|
import { formatTimestamp } from './printer.js';
|
|
6
|
-
import { printJob } from './printing/index.js';
|
|
6
|
+
import { printJob, PrinterUnavailableError } from './printing/index.js';
|
|
7
7
|
import { logEvent, status } from './server.js';
|
|
8
8
|
import { Reaction } from './reactions.js';
|
|
9
9
|
const MAX_MESSAGE_CHARS = 800;
|
|
10
10
|
const MAX_URLS = 5;
|
|
11
|
+
const PRINTER_POLL_INTERVAL_MS = 3_000;
|
|
11
12
|
const URL_REGEX = /https:\/\/[^\s<>"']+/g;
|
|
12
13
|
const TRAILING_PUNCT = /[.,;:!?)\]}>'"]+$/;
|
|
13
14
|
export function extractUrls(text) {
|
|
@@ -90,6 +91,10 @@ async function reactSafe(message, emoji) {
|
|
|
90
91
|
}
|
|
91
92
|
if (emoji === Reaction.ok) {
|
|
92
93
|
await removeReactionSafe(message, Reaction.fail);
|
|
94
|
+
await removeReactionSafe(message, Reaction.waiting);
|
|
95
|
+
}
|
|
96
|
+
if (emoji === Reaction.waiting) {
|
|
97
|
+
await removeReactionSafe(message, Reaction.fail);
|
|
93
98
|
}
|
|
94
99
|
}
|
|
95
100
|
catch {
|
|
@@ -111,6 +116,15 @@ function isCommandMessage(content) {
|
|
|
111
116
|
const trimmed = content.trim();
|
|
112
117
|
return trimmed.startsWith('!') || trimmed.startsWith('/');
|
|
113
118
|
}
|
|
119
|
+
export function parseCommand(content) {
|
|
120
|
+
const match = content.trim().match(/^[!/]\s*(\S+)(?:\s+(.*))?$/);
|
|
121
|
+
if (!match?.[1])
|
|
122
|
+
return null;
|
|
123
|
+
return {
|
|
124
|
+
name: match[1].toLowerCase(),
|
|
125
|
+
args: match[2]?.trim() ?? '',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
114
128
|
export function createWindsorBot() {
|
|
115
129
|
const client = new Client({
|
|
116
130
|
intents: [
|
|
@@ -120,6 +134,9 @@ export function createWindsorBot() {
|
|
|
120
134
|
GatewayIntentBits.GuildMessageReactions,
|
|
121
135
|
],
|
|
122
136
|
});
|
|
137
|
+
const pendingPrints = new Map();
|
|
138
|
+
const processingPending = new Set();
|
|
139
|
+
let pendingPoller = null;
|
|
123
140
|
client.on('clientReady', () => void onReady());
|
|
124
141
|
client.on('messageCreate', (msg) => void onMessage(msg));
|
|
125
142
|
const bot = {
|
|
@@ -137,6 +154,10 @@ export function createWindsorBot() {
|
|
|
137
154
|
await client.login(token);
|
|
138
155
|
}
|
|
139
156
|
function destroy() {
|
|
157
|
+
if (pendingPoller) {
|
|
158
|
+
clearInterval(pendingPoller);
|
|
159
|
+
pendingPoller = null;
|
|
160
|
+
}
|
|
140
161
|
client.destroy();
|
|
141
162
|
}
|
|
142
163
|
function getClient() {
|
|
@@ -149,6 +170,37 @@ export function createWindsorBot() {
|
|
|
149
170
|
&& ch.type !== ChannelType.GuildForum
|
|
150
171
|
&& ch.type !== ChannelType.GuildMedia;
|
|
151
172
|
}
|
|
173
|
+
function enqueuePendingPrint(message, retry) {
|
|
174
|
+
pendingPrints.set(message.id, { message, retry });
|
|
175
|
+
if (pendingPoller)
|
|
176
|
+
return;
|
|
177
|
+
pendingPoller = setInterval(() => void pollPendingPrints(), PRINTER_POLL_INTERVAL_MS);
|
|
178
|
+
logEvent('info', `Printer unavailable; polling for ${pendingPrints.size} pending print(s)`);
|
|
179
|
+
}
|
|
180
|
+
async function pollPendingPrints() {
|
|
181
|
+
for (const [messageId, pending] of pendingPrints) {
|
|
182
|
+
if (processingPending.has(messageId))
|
|
183
|
+
continue;
|
|
184
|
+
processingPending.add(messageId);
|
|
185
|
+
try {
|
|
186
|
+
await pending.retry();
|
|
187
|
+
await refreshMessage(pending.message);
|
|
188
|
+
if (!pending.message.reactions.cache.get(Reaction.waiting)?.me) {
|
|
189
|
+
pendingPrints.delete(messageId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
logEvent('error', `Pending print retry failed: ${err}`);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
processingPending.delete(messageId);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (pendingPrints.size === 0 && pendingPoller) {
|
|
200
|
+
clearInterval(pendingPoller);
|
|
201
|
+
pendingPoller = null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
152
204
|
async function onReady() {
|
|
153
205
|
const botUser = client.user;
|
|
154
206
|
status.connected = true;
|
|
@@ -224,7 +276,11 @@ export function createWindsorBot() {
|
|
|
224
276
|
if (msg.author.id === botId)
|
|
225
277
|
continue;
|
|
226
278
|
if (isCommandMessage(msg.content)) {
|
|
227
|
-
await handleOnDemand(msg);
|
|
279
|
+
await handleOnDemand(msg, await hasReaction(msg, Reaction.waiting));
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (await hasReaction(msg, Reaction.waiting)) {
|
|
283
|
+
await handleImmediatePrint(msg, config, false, true);
|
|
228
284
|
continue;
|
|
229
285
|
}
|
|
230
286
|
if (await hasAnyReaction(msg))
|
|
@@ -243,6 +299,10 @@ export function createWindsorBot() {
|
|
|
243
299
|
continue;
|
|
244
300
|
if (!isPrintTrigger(msg.content))
|
|
245
301
|
continue;
|
|
302
|
+
if (await hasReaction(msg, Reaction.waiting)) {
|
|
303
|
+
await handleAccumulatingPrint(msg, config, messages, true);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
246
306
|
if (await hasAnyReaction(msg))
|
|
247
307
|
continue;
|
|
248
308
|
await handleAccumulatingPrint(msg, config, messages);
|
|
@@ -261,6 +321,10 @@ export function createWindsorBot() {
|
|
|
261
321
|
continue;
|
|
262
322
|
if (msg.author.id === botId)
|
|
263
323
|
continue;
|
|
324
|
+
if (await hasReaction(msg, Reaction.waiting)) {
|
|
325
|
+
await handleOnDemand(msg, true);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
264
328
|
if (await hasAnyReaction(msg))
|
|
265
329
|
continue;
|
|
266
330
|
await handleOnDemand(msg);
|
|
@@ -338,7 +402,7 @@ export function createWindsorBot() {
|
|
|
338
402
|
break;
|
|
339
403
|
}
|
|
340
404
|
}
|
|
341
|
-
async function handleImmediatePrint(message, config, retryFailed = false) {
|
|
405
|
+
async function handleImmediatePrint(message, config, retryFailed = false, retryPending = false) {
|
|
342
406
|
const rawContent = message.content;
|
|
343
407
|
const urls = extractUrls(rawContent);
|
|
344
408
|
const textWithLinkLabels = replaceUrlsInText(rawContent, urls);
|
|
@@ -361,7 +425,7 @@ export function createWindsorBot() {
|
|
|
361
425
|
];
|
|
362
426
|
}
|
|
363
427
|
if (config.includeIcon) {
|
|
364
|
-
await
|
|
428
|
+
await reactSafe(message, Reaction.thinking);
|
|
365
429
|
const iconCacheDir = getCurrentConfig().iconCacheDir ?? './icon-cache';
|
|
366
430
|
const iconPath = await generateIcon(strippedText, iconCacheDir);
|
|
367
431
|
if (iconPath)
|
|
@@ -369,24 +433,34 @@ export function createWindsorBot() {
|
|
|
369
433
|
}
|
|
370
434
|
await refreshMessage(message);
|
|
371
435
|
if (await hasAnyReaction(message)) {
|
|
372
|
-
|
|
436
|
+
const waiting = message.reactions.cache.get(Reaction.waiting)?.me;
|
|
437
|
+
if (!retryPending && (!retryFailed || !message.reactions.cache.get(Reaction.fail)?.me)) {
|
|
373
438
|
await removeReactionSafe(message, Reaction.thinking);
|
|
374
439
|
return;
|
|
375
440
|
}
|
|
441
|
+
if (retryPending && !waiting)
|
|
442
|
+
return;
|
|
376
443
|
}
|
|
377
444
|
try {
|
|
378
445
|
await printJob(job);
|
|
446
|
+
pendingPrints.delete(message.id);
|
|
379
447
|
await reactSafe(message, Reaction.ok);
|
|
380
448
|
logEvent('print', `Printed immediate message from ${message.author.username}`);
|
|
381
449
|
}
|
|
382
450
|
catch (err) {
|
|
451
|
+
if (err instanceof PrinterUnavailableError) {
|
|
452
|
+
await reactSafe(message, Reaction.waiting);
|
|
453
|
+
enqueuePendingPrint(message, () => handleImmediatePrint(message, config, false, true));
|
|
454
|
+
logEvent('info', `Printer unavailable for immediate message from ${message.author.username}`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
383
457
|
await reactSafe(message, Reaction.fail);
|
|
384
458
|
await replySafe(message, `⏸️ Print failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
385
459
|
logEvent('error', `Print failed: ${err}`);
|
|
386
460
|
}
|
|
387
461
|
await removeReactionSafe(message, Reaction.thinking);
|
|
388
462
|
}
|
|
389
|
-
async function handleAccumulatingPrint(triggerMessage, config, allMessages) {
|
|
463
|
+
async function handleAccumulatingPrint(triggerMessage, config, allMessages, retryPending = false) {
|
|
390
464
|
const botId = client.user?.id;
|
|
391
465
|
const triggerIdx = allMessages.findIndex(m => m.id === triggerMessage.id);
|
|
392
466
|
const prior = allMessages.slice(0, triggerIdx).reverse();
|
|
@@ -433,14 +507,23 @@ export function createWindsorBot() {
|
|
|
433
507
|
job.metadataLines = [`Printed at ${formatTimestamp(new Date())}`];
|
|
434
508
|
}
|
|
435
509
|
await refreshMessage(triggerMessage);
|
|
436
|
-
if (await hasAnyReaction(triggerMessage))
|
|
437
|
-
|
|
510
|
+
if (await hasAnyReaction(triggerMessage)) {
|
|
511
|
+
if (!retryPending || !triggerMessage.reactions.cache.get(Reaction.waiting)?.me)
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
438
514
|
try {
|
|
439
515
|
await printJob(job);
|
|
516
|
+
pendingPrints.delete(triggerMessage.id);
|
|
440
517
|
await reactSafe(triggerMessage, Reaction.ok);
|
|
441
518
|
logEvent('print', `Printed accumulating list (${lines.length} items)`);
|
|
442
519
|
}
|
|
443
520
|
catch (err) {
|
|
521
|
+
if (err instanceof PrinterUnavailableError) {
|
|
522
|
+
await reactSafe(triggerMessage, Reaction.waiting);
|
|
523
|
+
enqueuePendingPrint(triggerMessage, () => handleAccumulatingPrint(triggerMessage, config, allMessages, true));
|
|
524
|
+
logEvent('info', `Printer unavailable for accumulating list in #${triggerMessage.channelId}`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
444
527
|
await reactSafe(triggerMessage, Reaction.fail);
|
|
445
528
|
await replySafe(triggerMessage, `⏸️ Print failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
446
529
|
logEvent('error', `Accumulating print failed: ${err}`);
|
|
@@ -556,28 +639,38 @@ export function createWindsorBot() {
|
|
|
556
639
|
}
|
|
557
640
|
return failed.length;
|
|
558
641
|
}
|
|
559
|
-
async function handleOnDemand(message) {
|
|
560
|
-
const
|
|
561
|
-
if (!
|
|
642
|
+
async function handleOnDemand(message, retryPending = false) {
|
|
643
|
+
const parsed = parseCommand(message.content);
|
|
644
|
+
if (!parsed)
|
|
562
645
|
return;
|
|
563
|
-
const
|
|
564
|
-
const spaceIdx = afterPrefix.indexOf(' ');
|
|
565
|
-
const commandName = (spaceIdx === -1 ? afterPrefix : afterPrefix.slice(0, spaceIdx)).toLowerCase();
|
|
566
|
-
const args = spaceIdx === -1 ? '' : afterPrefix.slice(spaceIdx + 1).trim();
|
|
646
|
+
const { name: commandName, args } = parsed;
|
|
567
647
|
let foundCommand = false;
|
|
568
648
|
for (const cmd of Commands) {
|
|
569
649
|
if (cmd.aliases.some(a => commandName.localeCompare(a, undefined, { sensitivity: "base" }) == 0)) {
|
|
570
650
|
foundCommand = true;
|
|
571
651
|
logEvent('command', `Command '${commandName}' invoked by ${message.author.username}`);
|
|
572
652
|
await refreshMessage(message);
|
|
573
|
-
if (await hasAnyReaction(message))
|
|
653
|
+
if (await hasAnyReaction(message) && !retryPending)
|
|
574
654
|
return;
|
|
575
655
|
const ctx = {
|
|
576
656
|
printJob,
|
|
577
657
|
retryFailedMessages: () => retryFailedMessages(message),
|
|
578
658
|
};
|
|
579
|
-
|
|
659
|
+
let result;
|
|
660
|
+
try {
|
|
661
|
+
result = await cmd.invoke(args, ctx);
|
|
662
|
+
}
|
|
663
|
+
catch (err) {
|
|
664
|
+
if (err instanceof PrinterUnavailableError) {
|
|
665
|
+
await reactSafe(message, Reaction.waiting);
|
|
666
|
+
enqueuePendingPrint(message, () => handleOnDemand(message, true));
|
|
667
|
+
logEvent('info', `Printer unavailable for command '${commandName}'`);
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
throw err;
|
|
671
|
+
}
|
|
580
672
|
if (result.kind === 'pass') {
|
|
673
|
+
pendingPrints.delete(message.id);
|
|
581
674
|
await reactSafe(message, "✅");
|
|
582
675
|
if (result.reply) {
|
|
583
676
|
await replySafe(message, result.reply);
|
package/dist/commands/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { debugFail } from "./debug-fail.js";
|
|
2
2
|
import { helloWorld } from "./hello-world.js";
|
|
3
3
|
import { printPokemon } from "./pokemon.js";
|
|
4
|
+
import { printPoker } from "./poker.js";
|
|
4
5
|
import { retry } from "./retry.js";
|
|
5
6
|
import { printSudoku } from "./sudoku.js";
|
|
6
7
|
import { printWordsearch } from "./wordsearch.js";
|
|
@@ -9,6 +10,7 @@ export const Commands = [
|
|
|
9
10
|
printSudoku,
|
|
10
11
|
printWordsearch,
|
|
11
12
|
printPokemon,
|
|
13
|
+
printPoker,
|
|
12
14
|
retry,
|
|
13
15
|
debugFail
|
|
14
16
|
];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { readFile, unlink, writeFile } from 'fs/promises';
|
|
2
|
+
import { tmpdir } from 'os';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { Jimp, JimpMime, loadFont, measureText, ResizeStrategy } from 'jimp';
|
|
5
|
+
import { Resvg } from '@resvg/resvg-js';
|
|
6
|
+
import { tryExecCommandFunction } from './util.js';
|
|
7
|
+
const FONT_DIR = new URL('../../node_modules/@jimp/plugin-print/dist/fonts/open-sans/', import.meta.url).pathname;
|
|
8
|
+
const FONT_64 = `${FONT_DIR}open-sans-64-black/open-sans-64-black.fnt`;
|
|
9
|
+
const FONT_128 = `${FONT_DIR}open-sans-128-black/open-sans-128-black.fnt`;
|
|
10
|
+
const IMAGE_WIDTH = 560;
|
|
11
|
+
const MARGIN = 24;
|
|
12
|
+
const CARD_WIDTH = 88;
|
|
13
|
+
const CARD_HEIGHT = 118;
|
|
14
|
+
const CARD_GAP = 8;
|
|
15
|
+
const HAND_GAP = 12;
|
|
16
|
+
const TITLE_HEIGHT = 48;
|
|
17
|
+
const SUIT_MARGIN = 8;
|
|
18
|
+
const RENDER_SCALE = 3;
|
|
19
|
+
const RANKS = ['2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K', 'A'];
|
|
20
|
+
const SUITS = ['clubs', 'diamonds', 'hearts', 'spades'];
|
|
21
|
+
const SUIT_FILES = {
|
|
22
|
+
clubs: new URL('../../assets/poker/club.svg', import.meta.url),
|
|
23
|
+
diamonds: new URL('../../assets/poker/diamond.svg', import.meta.url),
|
|
24
|
+
hearts: new URL('../../assets/poker/heart.svg', import.meta.url),
|
|
25
|
+
spades: new URL('../../assets/poker/spade.svg', import.meta.url),
|
|
26
|
+
};
|
|
27
|
+
export function dealPokerHand(random = Math.random) {
|
|
28
|
+
const deck = SUITS.flatMap(suit => RANKS.map(rank => ({ rank, suit })));
|
|
29
|
+
for (let i = deck.length - 1; i > 0; i--) {
|
|
30
|
+
const j = Math.floor(random() * (i + 1));
|
|
31
|
+
[deck[i], deck[j]] = [deck[j], deck[i]];
|
|
32
|
+
}
|
|
33
|
+
const board = deck.slice(0, 5);
|
|
34
|
+
const hands = Array.from({ length: 4 }, (_, index) => {
|
|
35
|
+
const offset = 5 + index * 2;
|
|
36
|
+
return [deck[offset], deck[offset + 1]];
|
|
37
|
+
});
|
|
38
|
+
return { board, hands };
|
|
39
|
+
}
|
|
40
|
+
function drawBox(image, left, top) {
|
|
41
|
+
left *= RENDER_SCALE;
|
|
42
|
+
top *= RENDER_SCALE;
|
|
43
|
+
const right = left + CARD_WIDTH * RENDER_SCALE;
|
|
44
|
+
const bottom = top + CARD_HEIGHT * RENDER_SCALE;
|
|
45
|
+
for (let x = left; x <= right; x++) {
|
|
46
|
+
image.setPixelColor(0x000000ff, x, top);
|
|
47
|
+
image.setPixelColor(0x000000ff, x, bottom);
|
|
48
|
+
}
|
|
49
|
+
for (let y = top; y <= bottom; y++) {
|
|
50
|
+
image.setPixelColor(0x000000ff, left, y);
|
|
51
|
+
image.setPixelColor(0x000000ff, right, y);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function loadSuitImages() {
|
|
55
|
+
const entries = await Promise.all(Object.entries(SUIT_FILES).map(async ([suit, path]) => {
|
|
56
|
+
const svg = await readFile(path, 'utf8');
|
|
57
|
+
const png = new Resvg(svg, {
|
|
58
|
+
fitTo: { mode: 'width', value: 48 * RENDER_SCALE },
|
|
59
|
+
}).render().asPng();
|
|
60
|
+
return [suit, await Jimp.fromBuffer(png)];
|
|
61
|
+
}));
|
|
62
|
+
return Object.fromEntries(entries);
|
|
63
|
+
}
|
|
64
|
+
function drawCard(image, card, left, top, rankFont, suitImages) {
|
|
65
|
+
drawBox(image, left, top);
|
|
66
|
+
image.print({
|
|
67
|
+
font: rankFont,
|
|
68
|
+
x: (left + 8) * RENDER_SCALE,
|
|
69
|
+
y: (top + 7) * RENDER_SCALE,
|
|
70
|
+
text: card.rank,
|
|
71
|
+
});
|
|
72
|
+
const suit = suitImages[card.suit];
|
|
73
|
+
image.composite(suit, (left + CARD_WIDTH - SUIT_MARGIN) * RENDER_SCALE - suit.width, (top + 58) * RENDER_SCALE);
|
|
74
|
+
}
|
|
75
|
+
async function renderPokerImage(deal) {
|
|
76
|
+
const rankFont = await loadFont(FONT_128);
|
|
77
|
+
const titleFont = await loadFont(FONT_64);
|
|
78
|
+
const suitImages = await loadSuitImages();
|
|
79
|
+
const boardWidth = CARD_WIDTH * 5 + CARD_GAP * 4;
|
|
80
|
+
const handsTop = MARGIN + TITLE_HEIGHT + CARD_HEIGHT + 24;
|
|
81
|
+
const imageHeight = handsTop + CARD_HEIGHT * 4 + HAND_GAP * 3 + MARGIN;
|
|
82
|
+
const image = new Jimp({
|
|
83
|
+
width: IMAGE_WIDTH * RENDER_SCALE,
|
|
84
|
+
height: imageHeight * RENDER_SCALE,
|
|
85
|
+
color: 0xffffffff,
|
|
86
|
+
});
|
|
87
|
+
const title = "Texas Hold 'Em";
|
|
88
|
+
image.print({
|
|
89
|
+
font: titleFont,
|
|
90
|
+
x: (IMAGE_WIDTH * RENDER_SCALE - measureText(titleFont, title)) / 2,
|
|
91
|
+
y: MARGIN * RENDER_SCALE,
|
|
92
|
+
text: title,
|
|
93
|
+
});
|
|
94
|
+
const boardLeft = (IMAGE_WIDTH - boardWidth) / 2;
|
|
95
|
+
for (const [index, card] of deal.board.entries()) {
|
|
96
|
+
drawCard(image, card, boardLeft + index * (CARD_WIDTH + CARD_GAP), MARGIN + TITLE_HEIGHT, rankFont, suitImages);
|
|
97
|
+
}
|
|
98
|
+
const handCardsWidth = CARD_WIDTH * 2 + CARD_GAP;
|
|
99
|
+
const lineLeft = MARGIN + handCardsWidth + 28;
|
|
100
|
+
const lineRight = IMAGE_WIDTH - MARGIN;
|
|
101
|
+
for (const [index, hand] of deal.hands.entries()) {
|
|
102
|
+
const top = handsTop + index * (CARD_HEIGHT + HAND_GAP);
|
|
103
|
+
drawCard(image, hand[0], MARGIN, top, rankFont, suitImages);
|
|
104
|
+
drawCard(image, hand[1], MARGIN + CARD_WIDTH + CARD_GAP, top, rankFont, suitImages);
|
|
105
|
+
const lineY = top + CARD_HEIGHT;
|
|
106
|
+
for (let x = lineLeft * RENDER_SCALE; x <= lineRight * RENDER_SCALE; x++) {
|
|
107
|
+
for (let offset = 0; offset < RENDER_SCALE; offset++) {
|
|
108
|
+
image.setPixelColor(0x000000ff, x, lineY * RENDER_SCALE + offset);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
image.resize({
|
|
113
|
+
w: IMAGE_WIDTH,
|
|
114
|
+
h: imageHeight,
|
|
115
|
+
mode: ResizeStrategy.BICUBIC,
|
|
116
|
+
});
|
|
117
|
+
const imagePath = join(tmpdir(), `windsor-poker-${process.pid}-${Date.now()}.png`);
|
|
118
|
+
await writeFile(imagePath, await image.getBuffer(JimpMime.png));
|
|
119
|
+
return imagePath;
|
|
120
|
+
}
|
|
121
|
+
async function pokerWorker(_args, ctx) {
|
|
122
|
+
const imagePath = await renderPokerImage(dealPokerHand());
|
|
123
|
+
try {
|
|
124
|
+
const job = {
|
|
125
|
+
lines: [],
|
|
126
|
+
urls: [],
|
|
127
|
+
iconPath: imagePath,
|
|
128
|
+
};
|
|
129
|
+
await ctx.printJob(job);
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
await unlink(imagePath);
|
|
133
|
+
}
|
|
134
|
+
return { kind: 'pass' };
|
|
135
|
+
}
|
|
136
|
+
export const printPoker = {
|
|
137
|
+
aliases: ['poker'],
|
|
138
|
+
invoke: tryExecCommandFunction(pokerWorker),
|
|
139
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { loadConfig } from './config.js';
|
|
2
|
+
import { getCurrentConfig, loadConfig } from './config.js';
|
|
3
3
|
import { logEvent, startDiagnosticsServer, setDiscordChannels, setRestartHandler, setRefreshChannelsHandler } from './server.js';
|
|
4
4
|
import { createWindsorBot, checkScheduledTasks } from './bot.js';
|
|
5
5
|
import { ChannelType } from 'discord.js';
|
|
@@ -19,21 +19,7 @@ async function startBot(cfg) {
|
|
|
19
19
|
try {
|
|
20
20
|
await nextBot.start(cfg.discordToken);
|
|
21
21
|
bot = nextBot;
|
|
22
|
-
|
|
23
|
-
const guilds = [...nextBot.getClient().guilds.cache.values()];
|
|
24
|
-
const channels = [];
|
|
25
|
-
for (const guild of guilds) {
|
|
26
|
-
const fetched = await guild.channels.fetch();
|
|
27
|
-
for (const ch of fetched.values()) {
|
|
28
|
-
if (ch?.isTextBased() &&
|
|
29
|
-
'name' in ch &&
|
|
30
|
-
ch.type !== ChannelType.GuildVoice &&
|
|
31
|
-
ch.type !== ChannelType.GuildStageVoice) {
|
|
32
|
-
channels.push({ id: ch.id, name: ch.name });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
setDiscordChannels(channels);
|
|
22
|
+
await refreshChannels();
|
|
37
23
|
logEvent('startup', 'Bot started successfully');
|
|
38
24
|
}
|
|
39
25
|
catch (err) {
|
|
@@ -64,7 +50,13 @@ async function refreshChannels() {
|
|
|
64
50
|
return;
|
|
65
51
|
}
|
|
66
52
|
logEvent('info', 'Refreshing Discord channels…');
|
|
67
|
-
const
|
|
53
|
+
const serverId = getCurrentConfig().serverId;
|
|
54
|
+
const guilds = serverId
|
|
55
|
+
? [bot.getClient().guilds.cache.get(serverId)].filter((guild) => guild !== undefined)
|
|
56
|
+
: [...bot.getClient().guilds.cache.values()];
|
|
57
|
+
if (serverId && guilds.length === 0) {
|
|
58
|
+
throw new Error(`Configured serverId ${serverId} not found`);
|
|
59
|
+
}
|
|
68
60
|
const channels = [];
|
|
69
61
|
for (const guild of guilds) {
|
|
70
62
|
const fetched = await guild.channels.fetch();
|
package/dist/printing/index.js
CHANGED
|
@@ -2,26 +2,52 @@ import { getCurrentConfig } from '../config.js';
|
|
|
2
2
|
import { printJobEscp } from './escp.js';
|
|
3
3
|
import { printJobCups } from './cups.js';
|
|
4
4
|
import { printJobImageFeed, printTestJobImageFeed } from './imagefeed.js';
|
|
5
|
+
export class PrinterUnavailableError extends Error {
|
|
6
|
+
constructor(message = 'Printer is unavailable') {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'PrinterUnavailableError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function isUnavailableError(error) {
|
|
12
|
+
if (!(error instanceof Error))
|
|
13
|
+
return false;
|
|
14
|
+
const code = 'code' in error ? error.code : undefined;
|
|
15
|
+
const message = error.message.toLowerCase();
|
|
16
|
+
return code === 'ENOENT' ||
|
|
17
|
+
code === 'ENODEV' ||
|
|
18
|
+
code === 'ENXIO' ||
|
|
19
|
+
message.includes('printer or class does not exist') ||
|
|
20
|
+
message.includes('no printer') ||
|
|
21
|
+
message.includes('device or resource busy');
|
|
22
|
+
}
|
|
5
23
|
export async function printJob(job) {
|
|
6
24
|
const cfg = getCurrentConfig();
|
|
7
25
|
const printConfig = cfg.printConfig;
|
|
8
26
|
if (!printConfig) {
|
|
9
27
|
throw new Error('No print mode configured. Set a print mode in the Control Panel.');
|
|
10
28
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
try {
|
|
30
|
+
switch (printConfig.mode) {
|
|
31
|
+
case 'escp':
|
|
32
|
+
await printJobEscp(job, printConfig.serialPort);
|
|
33
|
+
break;
|
|
34
|
+
case 'cups':
|
|
35
|
+
await printJobCups(job, printConfig);
|
|
36
|
+
break;
|
|
37
|
+
case 'imagefeed':
|
|
38
|
+
await printJobImageFeed(job);
|
|
39
|
+
break;
|
|
40
|
+
default: {
|
|
41
|
+
const never = printConfig;
|
|
42
|
+
throw new Error(`Unknown print mode: ${never.mode}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isUnavailableError(error)) {
|
|
48
|
+
throw new PrinterUnavailableError(error instanceof Error ? error.message : String(error));
|
|
24
49
|
}
|
|
50
|
+
throw error;
|
|
25
51
|
}
|
|
26
52
|
}
|
|
27
53
|
export async function printTestPage() {
|