@serverkgg/bridge 0.1.8 → 0.1.10

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.
@@ -0,0 +1,35 @@
1
+ {
2
+ "source": {
3
+ "url": "https://github.com/mruniquehacker/Knightbot-MD",
4
+ "branch": "main",
5
+ "commit": "871bce832169c3a52a632dc100c1f235cb35977c",
6
+ "stars": 8128
7
+ },
8
+ "files": [
9
+ ".gitignore",
10
+ "README.md",
11
+ "config.js",
12
+ "index.js",
13
+ "main.js",
14
+ "package.json",
15
+ "settings.js"
16
+ ],
17
+ "contents": {
18
+ "package.json": "{\n \"name\": \"knightbot\",\n \"version\": \"1.0.0\",\n \"description\": \"WhatsApp Bot\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"start\": \"node index.js\",\n \"start:optimized\": \"node --max-old-space-size=512 --optimize-for-size --gc-interval=100 index.js\",\n \"cleanup\": \"node cleanup.js\",\n \"reset-session\": \"node reset-session.js\",\n \"start:clean\": \"npm run cleanup && npm run start:optimized\",\n \"start:fresh\": \"npm run reset-session && npm start\",\n \"install:panel\": \"npm install --legacy-peer-deps\",\n \"install:force\": \"npm install --force\",\n \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\",\n \"docker:build\": \"docker build -t docker run -e SESSION_ID=$SESSION_ID knightbot\"\n },\n \"keywords\": [\n \"whatsapp-bot\"\n ],\n \"author\": \"\",\n \"license\": \"ISC\",\n \"dependencies\": {\n \"@adiwajshing/keyed-db\": \"^0.2.4\",\n \"@ffmpeg/ffmpeg\": \"^0.12.15\",\n \"@hapi/boom\": \"^10.0.1\",\n \"@types/node\": \"^18.0.6\",\n \"@whiskeysockets/baileys\": \"^v7.0.0-rc.9\",\n \"awesome-phonenumber\": \"^5.9.0\",\n \"axios\": \"^1.8.4\",\n \"chalk\": \"^4.1.2\",\n \"cheerio\": \"^1.0.0-rc.12\",\n \"cookie\": \"^0.5.0\",\n \"dotenv\": \"^16.4.5\",\n \"events\": \"^3.3.0\",\n \"file-type\": \"^16.5.4\",\n \"fluent-ffmpeg\": \"^2.1.3\",\n \"form-data\": \"^4.0.1\",\n \"fs-extra\": \"^11.2.0\",\n \"gtts\": \"^0.2.1\",\n \"human-readable\": \"^0.2.1\",\n \"jimp\": \"^1.6.0\",\n \"jsdom\": \"^22.1.0\",\n \"libphonenumber-js\": \"^1.11.18\",\n \"libsignal\": \"^2.0.1\",\n \"link-preview-js\": \"^3.0.5\",\n \"moment-timezone\": \"^0.5.43\",\n \"mumaker\": \"^2.0.0\",\n \"node-cache\": \"^5.1.2\",\n \"node-fetch\": \"^2.7.0\",\n \"node-id3\": \"^0.2.3\",\n \"node-webpmux\": \"^3.1.0\",\n \"node-youtube-music\": \"^0.8.3\",\n \"performance-now\": \"^2.1.0\",\n \"phin\": \"^3.7.1\",\n \"pino\": \"^8.21.0\",\n \"qrcode\": \"^1.5.4\",\n \"qrcode-reader\": \"^1.0.4\",\n \"qrcode-terminal\": \"^0.12.0\",\n \"request\": \"^2.88.2\",\n \"ruhend-scraper\": \"^8.3.0\",\n \"safe-stable-stringify\": \"^2.5.0\",\n \"set-cookie\": \"^0.0.4\",\n \"sharp\": \"^0.32.6\",\n \"tough-cookie\": \"^5.0.0\",\n \"translate-google-api\": \"^1.0.4\",\n \"ws\": \"^8.17.1\",\n \"yargs\": \"^17.6.0\",\n \"yargs-parser\": \"^21.1.1\",\n \"youtube-yts\": \"^2.0.0\",\n \"youtubedl-core\": \"^4.11.7\",\n \"yt-search\": \"^2.12.1\",\n \"ytdl-core\": \"^4.11.5\"\n },\n \"overrides\": {\n \"jimp\": \"^1.6.0\"\n },\n \"engines\": {\n \"node\": \">=18.0.0\"\n }\n}\n"
19
+ },
20
+ "expected": {
21
+ "runtime": "node",
22
+ "packageManager": "npm",
23
+ "framework": null,
24
+ "platform": null,
25
+ "install": "npm install",
26
+ "build": null,
27
+ "start": "node index.js",
28
+ "nodeVersion": "18",
29
+ "pythonVersion": null,
30
+ "tokenKey": null,
31
+ "warnings": [
32
+ "no-lockfile"
33
+ ]
34
+ }
35
+ }
@@ -678,6 +678,28 @@ describe("detect warnings", () => {
678
678
 
679
679
  expect(quiet.warnings).not.toContain(BotInspectWarning.ReservedEnvKey);
680
680
  });
681
+
682
+ it("keeps quiet about the web address and raw port variables an example file lists, and warns about the switches", () => {
683
+ const quiet = project({
684
+ ...DISCORD_NODE,
685
+ ".env.example": "DISCORD_TOKEN=\nSERVERK_WEB_URL=\nSERVERK_WEB_HOST=\nSERVERK_PORT_1=\nSERVERK_PORT_2=\n",
686
+ });
687
+
688
+ expect(quiet.env.map((entry) => entry.key)).toEqual([
689
+ "DISCORD_TOKEN",
690
+ ]);
691
+ expect(quiet.warnings).not.toContain(BotInspectWarning.ReservedEnvKey);
692
+
693
+ const loud = project({
694
+ ...DISCORD_NODE,
695
+ ".env.example": "DISCORD_TOKEN=\nBOT_WEB=on\n",
696
+ });
697
+
698
+ expect(loud.env.map((entry) => entry.key)).toEqual([
699
+ "DISCORD_TOKEN",
700
+ ]);
701
+ expect(loud.warnings).toContain(BotInspectWarning.ReservedEnvKey);
702
+ });
681
703
  });
682
704
 
683
705
  describe("detect with serverk.json", () => {
@@ -3,9 +3,21 @@ import {
3
3
  BOT_AMBIENT_KEYS,
4
4
  BOT_PLATFORM_KEYS,
5
5
  BOT_PLATFORM_SECRET_KEYS,
6
+ BOT_PORT_1,
7
+ BOT_PORT_1_KEY,
8
+ BOT_PORT_2,
9
+ BOT_PORT_2_KEY,
6
10
  BOT_PROTECTED_KEYS,
7
11
  BOT_PROVIDED_KEYS,
12
+ BOT_RAW_PORT_KEYS,
13
+ BOT_WEB,
14
+ BOT_WEB_PORT_KEY,
8
15
  isReservedBotKey,
16
+ PORT,
17
+ SERVERK_PORT_1,
18
+ SERVERK_PORT_2,
19
+ SERVERK_WEB_HOST,
20
+ SERVERK_WEB_URL,
9
21
  } from "./keys";
10
22
 
11
23
  describe("isReservedBotKey", () => {
@@ -21,6 +33,21 @@ describe("isReservedBotKey", () => {
21
33
  }
22
34
  });
23
35
 
36
+ it("reserves the web and port switches the platform sets", () => {
37
+ for (const key of [
38
+ BOT_WEB,
39
+ BOT_PORT_1,
40
+ BOT_PORT_2,
41
+ ]) {
42
+ expect(BOT_PLATFORM_KEYS).toContain(key);
43
+ expect(isReservedBotKey(key)).toBe(true);
44
+ }
45
+ });
46
+
47
+ it("leaves PORT to the customer, so a bot without the web port keeps its own", () => {
48
+ expect(isReservedBotKey(PORT)).toBe(false);
49
+ });
50
+
24
51
  it("leaves the customer's own keys alone, case included", () => {
25
52
  for (const key of [
26
53
  "DISCORD_TOKEN",
@@ -47,6 +74,19 @@ describe("BOT_AMBIENT_KEYS", () => {
47
74
  }
48
75
  });
49
76
 
77
+ it("holds the web address and raw port variables, so detection never asks for them", () => {
78
+ for (const key of [
79
+ SERVERK_WEB_URL,
80
+ SERVERK_WEB_HOST,
81
+ SERVERK_PORT_1,
82
+ SERVERK_PORT_2,
83
+ ]) {
84
+ expect(BOT_PROVIDED_KEYS).toContain(key);
85
+ expect(BOT_AMBIENT_KEYS).toContain(key);
86
+ expect(isReservedBotKey(key)).toBe(true);
87
+ }
88
+ });
89
+
50
90
  it("leaves a bot's own keys out", () => {
51
91
  for (const key of [
52
92
  "DISCORD_TOKEN",
@@ -57,3 +97,13 @@ describe("BOT_AMBIENT_KEYS", () => {
57
97
  }
58
98
  });
59
99
  });
100
+
101
+ describe("BOT_RAW_PORT_KEYS", () => {
102
+ it("names the two raw ports apart from the web port", () => {
103
+ expect(BOT_RAW_PORT_KEYS).toEqual([
104
+ BOT_PORT_1_KEY,
105
+ BOT_PORT_2_KEY,
106
+ ]);
107
+ expect(BOT_RAW_PORT_KEYS).not.toContain(BOT_WEB_PORT_KEY);
108
+ });
109
+ });
@@ -18,12 +18,49 @@ export const SNAPSHOT_ID = "SNAPSHOT_ID";
18
18
 
19
19
  export const BOT_PLATFORM = "BOT_PLATFORM";
20
20
 
21
+ export const BOT_WEB = "BOT_WEB";
22
+
23
+ export const BOT_PORT_1 = "BOT_PORT_1";
24
+
25
+ export const BOT_PORT_2 = "BOT_PORT_2";
26
+
27
+ export const BOT_SWITCH_ON = "on";
28
+
21
29
  export const GIT_TOKEN = "GIT_TOKEN";
22
30
 
23
31
  export const SERVERK_BOT = "SERVERK_BOT";
24
32
 
25
33
  export const SERVERK_SERVER_CODE = "SERVERK_SERVER_CODE";
26
34
 
35
+ export const SERVERK_WEB_URL = "SERVERK_WEB_URL";
36
+
37
+ export const SERVERK_WEB_HOST = "SERVERK_WEB_HOST";
38
+
39
+ export const SERVERK_PORT_1 = "SERVERK_PORT_1";
40
+
41
+ export const SERVERK_PORT_2 = "SERVERK_PORT_2";
42
+
43
+ export const PORT = "PORT";
44
+
45
+ export const BOT_WEB_PORT_KEY = "web";
46
+
47
+ export const BOT_PORT_1_KEY = "port1";
48
+
49
+ export const BOT_PORT_2_KEY = "port2";
50
+
51
+ export const BOT_RAW_PORT_KEYS = [
52
+ BOT_PORT_1_KEY,
53
+ BOT_PORT_2_KEY,
54
+ ] as const;
55
+
56
+ export type BotRawPortKey = (typeof BOT_RAW_PORT_KEYS)[number];
57
+
58
+ export const BOT_PORT_SWITCHES = {
59
+ [BOT_WEB_PORT_KEY]: BOT_WEB,
60
+ [BOT_PORT_1_KEY]: BOT_PORT_1,
61
+ [BOT_PORT_2_KEY]: BOT_PORT_2,
62
+ } as const;
63
+
27
64
  export const BOT_PLATFORM_KEYS: readonly string[] = [
28
65
  REPO_URL,
29
66
  REPO_REF,
@@ -34,6 +71,9 @@ export const BOT_PLATFORM_KEYS: readonly string[] = [
34
71
  DEPLOYMENT_ID,
35
72
  SNAPSHOT_ID,
36
73
  BOT_PLATFORM,
74
+ BOT_WEB,
75
+ BOT_PORT_1,
76
+ BOT_PORT_2,
37
77
  ];
38
78
 
39
79
  export const BOT_PLATFORM_SECRET_KEYS: readonly string[] = [
@@ -52,6 +92,10 @@ export const BOT_PROVIDED_KEYS: readonly string[] = [
52
92
  SERVERK_BOT,
53
93
  SERVERK_SERVER_CODE,
54
94
  BOT_PLATFORM,
95
+ SERVERK_WEB_URL,
96
+ SERVERK_WEB_HOST,
97
+ SERVERK_PORT_1,
98
+ SERVERK_PORT_2,
55
99
  ];
56
100
 
57
101
  export const BOT_AMBIENT_KEYS: readonly string[] = [
@@ -59,7 +103,7 @@ export const BOT_AMBIENT_KEYS: readonly string[] = [
59
103
  ...BOT_PROVIDED_KEYS,
60
104
  "NODE_ENV",
61
105
  "NODE_OPTIONS",
62
- "PORT",
106
+ PORT,
63
107
  "HOST",
64
108
  "HOSTNAME",
65
109
  "DEBUG",
@@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
2
2
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { renderDetectTable } from "../bot/detect";
5
+ import { BOT_SWITCH_ON, BOT_WEB, BOT_WEB_PORT_KEY, renderDetectTable } from "../bot/detect";
6
6
  import { BridgeKind, BridgeSetupStepKind } from "../core";
7
7
  import { GuideOpenTab, type GuidePack, type ParsedBlock, type ParsedLeafBlock } from "../guides";
8
8
  import { MANIFEST_SCHEMA_VERSION, PackageKind, type RunnableManifest } from "../manifest";
@@ -600,7 +600,7 @@ describe("validating a bot package", () => {
600
600
  );
601
601
  });
602
602
 
603
- it("warns about a port on a bot but still validates it", async () => {
603
+ it("warns about a bot port with no activeWhen but still validates it", async () => {
604
604
  writeBot(
605
605
  botManifest({
606
606
  container: {
@@ -616,10 +616,40 @@ describe("validating a bot package", () => {
616
616
 
617
617
  expect(lines).toContain("bot-node is valid");
618
618
  expect(lines).toContain(
619
- " warning: a bot package declares 1 port(s); bots need none — remove them unless a webhook needs a public port",
619
+ " warning: a bot package declares 1 port(s) with no activeWhen; a bot publishes a port only while its author turns it on",
620
620
  );
621
621
  });
622
622
 
623
+ it("validates a bot web port its author turns on without a warning", async () => {
624
+ writeBot(
625
+ botManifest({
626
+ container: {
627
+ image: "serverkgg/bot-node",
628
+ ports: [
629
+ {
630
+ ...WEBHOOK_PORT,
631
+ key: BOT_WEB_PORT_KEY,
632
+ web: true,
633
+ activeWhen: [
634
+ {
635
+ variable: BOT_WEB,
636
+ values: [
637
+ BOT_SWITCH_ON,
638
+ ],
639
+ },
640
+ ],
641
+ },
642
+ ],
643
+ },
644
+ }),
645
+ );
646
+
647
+ await validate();
648
+
649
+ expect(lines).toContain("bot-node is valid");
650
+ expect(lines.some((line) => line.includes("warning:"))).toBe(false);
651
+ });
652
+
623
653
  it("fails a bot that declares platform secrets", async () => {
624
654
  writeBot(
625
655
  botManifest({
@@ -4,7 +4,7 @@ import { pathToFileURL } from "node:url";
4
4
  import { parseArgs } from "node:util";
5
5
  import { z } from "zod";
6
6
  import { type ArticlePack, compileArticles } from "../blog";
7
- import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
7
+ import { BOT_PORT_SWITCHES, DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
8
8
  import { BridgeKind, PortProtocol } from "../core";
9
9
  import {
10
10
  GuideOpenTab,
@@ -599,14 +599,18 @@ export const setupReportLines = (setup: BridgeSetupModule | undefined): string[]
599
599
  };
600
600
 
601
601
  export const botPortLines = (manifest: RunnableManifest): string[] => {
602
- const ports = manifest.container.ports.length;
602
+ if (!isBotManifest(manifest)) {
603
+ return [];
604
+ }
603
605
 
604
- if (!isBotManifest(manifest) || ports === 0) {
606
+ const ungated = manifest.container.ports.filter((port) => port.activeWhen.length === 0).length;
607
+
608
+ if (ungated === 0) {
605
609
  return [];
606
610
  }
607
611
 
608
612
  return [
609
- ` warning: a bot package declares ${ports} port(s); bots need none — remove them unless a webhook needs a public port`,
613
+ ` warning: a bot package declares ${ungated} port(s) with no activeWhen; a bot publishes a port only while its author turns it on`,
610
614
  ];
611
615
  };
612
616
 
@@ -680,7 +684,11 @@ const validateRunnablePackage = async (
680
684
  }
681
685
 
682
686
  validateDriver(driver);
683
- validatePortActivation(manifest.container.ports, driver);
687
+ validatePortActivation(
688
+ manifest.container.ports,
689
+ driver,
690
+ isBotManifest(manifest) ? Object.values(BOT_PORT_SWITCHES) : [],
691
+ );
684
692
 
685
693
  const confirmLines = confirmReportLines(driver);
686
694
 
@@ -390,3 +390,121 @@ describe("conditionals", () => {
390
390
  expect(() => parse(":::if server.address")).toThrow(/conditionals open with :::when/);
391
391
  });
392
392
  });
393
+
394
+ describe("heading anchors", () => {
395
+ it("takes the id after a heading and keeps it out of the heading text", () => {
396
+ const { blocks } = parse(
397
+ [
398
+ "## The token is invalid {#token-invalid}",
399
+ "",
400
+ "### التوكن غير صحيح {#token-invalid-2}",
401
+ ].join("\n"),
402
+ );
403
+
404
+ expect(blocks).toEqual([
405
+ {
406
+ type: "heading",
407
+ level: 2,
408
+ id: "token-invalid",
409
+ children: [
410
+ {
411
+ type: "text",
412
+ text: "The token is invalid",
413
+ },
414
+ ],
415
+ },
416
+ {
417
+ type: "heading",
418
+ level: 3,
419
+ id: "token-invalid-2",
420
+ children: [
421
+ {
422
+ type: "text",
423
+ text: "التوكن غير صحيح",
424
+ },
425
+ ],
426
+ },
427
+ ]);
428
+ });
429
+
430
+ it("keeps inline markup before the anchor", () => {
431
+ const { blocks } = parse("## `ffmpeg` is missing {#ffmpeg-missing}");
432
+
433
+ expect(blocks[0]).toEqual({
434
+ type: "heading",
435
+ level: 2,
436
+ id: "ffmpeg-missing",
437
+ children: [
438
+ {
439
+ type: "code",
440
+ text: "ffmpeg",
441
+ },
442
+ {
443
+ type: "text",
444
+ text: " is missing",
445
+ },
446
+ ],
447
+ });
448
+ });
449
+
450
+ it("still slugs a heading without an anchor", () => {
451
+ const { blocks } = parse("## Find your line");
452
+
453
+ expect(blocks[0]).toMatchObject({
454
+ id: "find-your-line",
455
+ });
456
+ });
457
+
458
+ it("moves a later slug aside instead of taking an anchored id", () => {
459
+ const { blocks } = parse(
460
+ [
461
+ "## Rate limits {#rate-limited}",
462
+ "",
463
+ "## Rate limited",
464
+ ].join("\n"),
465
+ );
466
+
467
+ expect(blocks.map((block) => (block.type === "heading" ? block.id : null))).toEqual([
468
+ "rate-limited",
469
+ "rate-limited-2",
470
+ ]);
471
+ });
472
+
473
+ it("rejects an anchored id that is already used", () => {
474
+ expect(() =>
475
+ parse(
476
+ [
477
+ "## Rate limited",
478
+ "",
479
+ "## Too many requests {#rate-limited}",
480
+ ].join("\n"),
481
+ ),
482
+ ).toThrow(/heading id "rate-limited" is already used/);
483
+ expect(() =>
484
+ parse(
485
+ [
486
+ "## One {#same}",
487
+ "",
488
+ "## Two {#same}",
489
+ ].join("\n"),
490
+ ),
491
+ ).toThrow(/heading id "same" is already used/);
492
+ });
493
+
494
+ it("rejects an id that is not lowercase letters, digits and hyphens", () => {
495
+ for (const id of [
496
+ "Token-Invalid",
497
+ "token_invalid",
498
+ "token invalid",
499
+ "-token",
500
+ "token--invalid",
501
+ "",
502
+ ]) {
503
+ expect(() => parse(`## The token is invalid {#${id}}`)).toThrow(/must be lowercase letters, digits/);
504
+ }
505
+ });
506
+
507
+ it("rejects an anchor with no heading text", () => {
508
+ expect(() => parse("## {#token-invalid}")).toThrow(/empty text run/);
509
+ });
510
+ });
@@ -52,6 +52,10 @@ const HEADING_2_PATTERN = /^##\s+(.+)$/;
52
52
 
53
53
  const HEADING_3_PATTERN = /^###\s+(.+)$/;
54
54
 
55
+ const HEADING_ANCHOR_PATTERN = /^(.*?)\s*\{#([^}]*)\}$/;
56
+
57
+ const HEADING_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
58
+
55
59
  const CALLOUT_OPEN_PATTERN = /^>\s*\[!(note|warning|danger)\]\s*(.*)$/;
56
60
 
57
61
  const IMAGE_LINE_PATTERN = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
@@ -285,6 +289,20 @@ const headingId = (inlines: GuideInline[], used: Map<string, number>, fallback:
285
289
  return count === 0 ? base : `${base}-${count + 1}`;
286
290
  };
287
291
 
292
+ const anchoredHeadingId = (path: string, line: number, id: string, used: Map<string, number>) => {
293
+ if (!HEADING_ID_PATTERN.test(id)) {
294
+ fail(path, line, `heading id "${id}" must be lowercase letters, digits and single hyphens`);
295
+ }
296
+
297
+ if (used.has(id)) {
298
+ fail(path, line, `heading id "${id}" is already used in this guide`);
299
+ }
300
+
301
+ used.set(id, 1);
302
+
303
+ return id;
304
+ };
305
+
288
306
  const parseOpenTarget = (path: string, line: number, value: string): GuideOpenTarget => {
289
307
  const separator = value.indexOf(":");
290
308
  const tab = separator === -1 ? value : value.slice(0, separator);
@@ -459,12 +477,16 @@ const scanLeafBlocks = (scanner: Scanner, nested: boolean): ParsedLeafBlock[] =>
459
477
  fail(path, lineNumber, "unsupported heading depth; use ## or ###");
460
478
  }
461
479
 
462
- const children = parseInlines(path, lineNumber, match[1].trim());
480
+ const text = match[1].trim();
481
+ const anchor = text.match(HEADING_ANCHOR_PATTERN);
482
+ const children = parseInlines(path, lineNumber, anchor ? (anchor[1] ?? "") : text);
463
483
 
464
484
  blocks.push({
465
485
  type: "heading",
466
486
  level: h3 ? 3 : 2,
467
- id: headingId(children, scanner.headingIds, `section-${scanner.headingIds.size + 1}`),
487
+ id: anchor
488
+ ? anchoredHeadingId(path, lineNumber, anchor[2] ?? "", scanner.headingIds)
489
+ : headingId(children, scanner.headingIds, `section-${scanner.headingIds.size + 1}`),
468
490
  children,
469
491
  });
470
492