discord-html-transcripts-fix 2.2.0 → 2.3.1

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.
@@ -9,13 +9,49 @@ exports.parseDiscordEmoji = parseDiscordEmoji;
9
9
  exports.streamToString = streamToString;
10
10
  exports.safeJsonForScript = safeJsonForScript;
11
11
  exports.safeHref = safeHref;
12
+ exports.safeImageSrc = safeImageSrc;
13
+ exports.safeLinkHref = safeLinkHref;
14
+ exports.describeError = describeError;
15
+ exports.truncateText = truncateText;
12
16
  exports.safeColor = safeColor;
13
17
  exports.safeImageMime = safeImageMime;
14
18
  exports.escapeHtml = escapeHtml;
15
19
  exports.resolveTimestampFormat = resolveTimestampFormat;
16
20
  exports.formatMessageTimestamp = formatMessageTimestamp;
21
+ exports.isForwardReference = isForwardReference;
22
+ exports.isForwardMessage = isForwardMessage;
23
+ exports.isLibraryStructure = isLibraryStructure;
24
+ const discord_js_1 = require("discord.js");
17
25
  const twemoji_1 = __importDefault(require("twemoji"));
18
26
 
27
+ // MessageReferenceType.Forward. Spelled as the API value on purpose: the enum only
28
+ // exists from discord.js 14.16, and reading it on an older 14.x throws for every
29
+ // message that has a reference.
30
+ const MESSAGE_REFERENCE_TYPE_FORWARD = 1;
31
+ // MessageFlags.HasSnapshot, for the same reason.
32
+ const MESSAGE_FLAG_HAS_SNAPSHOT = 1 << 14;
33
+
34
+ function isForwardReference(reference) {
35
+ return !!reference && reference.type === MESSAGE_REFERENCE_TYPE_FORWARD;
36
+ }
37
+
38
+ // Before 14.16, discord.js dropped the reference type and the snapshots, so a
39
+ // forward looked exactly like a reply. The message flags are kept raw by every
40
+ // 14.x and still tell the two apart.
41
+ function isForwardMessage(message) {
42
+ if (!message) return false;
43
+ if (isForwardReference(message.reference)) return true;
44
+ const flags = typeof message.flags === 'number' ? message.flags : message.flags?.bitfield;
45
+ return typeof flags === 'number' && (flags & MESSAGE_FLAG_HAS_SNAPSHOT) !== 0;
46
+ }
47
+
48
+ // True for anything discord.js built, as opposed to plain objects a caller passed
49
+ // in. Every structure carries its client; the second check also holds when a
50
+ // second copy of discord.js is installed and `instanceof` fails.
51
+ function isLibraryStructure(value) {
52
+ return value instanceof discord_js_1.Base || (value !== null && typeof value === 'object' && 'client' in value);
53
+ }
54
+
19
55
  const DEFAULT_TIMESTAMP_FORMAT = { dateFormat: 'dd/mm/yyyy', timeFormat: '24h' };
20
56
 
21
57
  function pad2(n) {
@@ -25,11 +61,27 @@ function pad2(n) {
25
61
  // Normalizes the user-facing options once, at render start, so every renderer
26
62
  // shares one reference "now" — otherwise a transcript rendered across midnight
27
63
  // could label the same day both "today" and with a full date.
64
+ // Accepts the documented values case-insensitively ('12H', 'MM/DD/YYYY'); anything
65
+ // else falls back to the default and is reported once per value instead of silently.
66
+ const warnedChoices = new Set();
67
+ function resolveChoice(name, value, allowed, fallback) {
68
+ if (value === undefined || value === null) return fallback;
69
+ const normalized = typeof value === 'string' ? value.trim().toLowerCase() : value;
70
+ if (allowed.includes(normalized)) return normalized;
71
+ const key = name + ':' + String(value);
72
+ if (!warnedChoices.has(key)) {
73
+ warnedChoices.add(key);
74
+ console.warn(`[discord-html-transcripts-fix] ${name} must be one of ${allowed.map((a) => `'${a}'`).join(', ')} ` +
75
+ `(got ${typeof value === 'string' ? JSON.stringify(value) : String(value)}); using '${fallback}'.`);
76
+ }
77
+ return fallback;
78
+ }
79
+
28
80
  function resolveTimestampFormat(options) {
29
81
  const o = options || {};
30
82
  return {
31
- dateFormat: o.dateFormat === 'mm/dd/yyyy' ? 'mm/dd/yyyy' : DEFAULT_TIMESTAMP_FORMAT.dateFormat,
32
- timeFormat: o.timeFormat === '12h' ? '12h' : DEFAULT_TIMESTAMP_FORMAT.timeFormat,
83
+ dateFormat: resolveChoice('dateFormat', o.dateFormat, ['dd/mm/yyyy', 'mm/dd/yyyy'], DEFAULT_TIMESTAMP_FORMAT.dateFormat),
84
+ timeFormat: resolveChoice('timeFormat', o.timeFormat, ['24h', '12h'], DEFAULT_TIMESTAMP_FORMAT.timeFormat),
33
85
  now: o.now instanceof Date ? o.now : new Date(),
34
86
  };
35
87
  }
@@ -144,6 +196,59 @@ function safeHref(url) {
144
196
  }
145
197
  }
146
198
 
199
+ // For URLs that end up as an image source or a file link inside a component, which
200
+ // renders them unchecked: web URLs, inline images, and Discord's attachment://
201
+ // references. Anything else — javascript:, other data: — becomes undefined.
202
+ function safeImageSrc(url) {
203
+ if (typeof url !== 'string' || !url) return undefined;
204
+ const trimmed = url.trim();
205
+ if (/^data:image\//i.test(trimmed) || /^attachment:\/\//i.test(trimmed)) return url;
206
+ try {
207
+ const proto = new URL(trimmed, 'https://example.invalid').protocol.toLowerCase();
208
+ return proto === 'http:' || proto === 'https:' ? url : undefined;
209
+ } catch (_e) {
210
+ return undefined;
211
+ }
212
+ }
213
+
214
+ // For links a component renders without checking them: what safeHref allows, plus
215
+ // the two Discord schemes such links legitimately carry — discord: (link buttons into
216
+ // the client) and attachment:// (file components). Neither runs anything.
217
+ function safeLinkHref(url) {
218
+ if (typeof url !== 'string' || !url) return undefined;
219
+ const trimmed = url.trim();
220
+ if (/^attachment:\/\//i.test(trimmed)) return url;
221
+ if (/^discord:/i.test(trimmed)) {
222
+ try {
223
+ new URL(trimmed);
224
+ return url;
225
+ } catch (_e) {
226
+ return '#';
227
+ }
228
+ }
229
+ return safeHref(url);
230
+ }
231
+
232
+ // Cuts `text` to at most `max` UTF-16 code units without cutting through a surrogate
233
+ // pair. Half of a pair cannot be encoded: it ends up as U+FFFD in the page.
234
+ function truncateText(text, max) {
235
+ if (typeof text !== 'string' || text.length <= max) return text;
236
+ let end = max;
237
+ const last = text.charCodeAt(end - 1);
238
+ if (last >= 0xd800 && last <= 0xdbff) end--; // a first half: leave it out with its second half
239
+ return text.slice(0, end);
240
+ }
241
+
242
+ // An error for a log line: its message and stack only. A whole error object can
243
+ // carry what a caller's HTTP client attached to it, request headers included.
244
+ function describeError(err) {
245
+ try {
246
+ return err instanceof Error ? (err.stack || err.message) : String(err);
247
+ } catch (_e) {
248
+ return 'an error that cannot be printed';
249
+ }
250
+ }
251
+
147
252
  function safeColor(c, fallback = '#5865F2') {
148
253
  if (typeof c !== 'string') return fallback;
149
254
  return /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : fallback;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discord-html-transcripts-fix",
3
- "version": "2.2.0",
3
+ "version": "2.3.1",
4
4
  "description": "A nicely formatted html transcript generator for discord.js. Bugfix fork with support for the latest discord.js and Components v2.",
5
5
  "main": "dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -31,7 +31,7 @@
31
31
  "LICENSE"
32
32
  ],
33
33
  "dependencies": {
34
- "@skyra/discord-components-core": "^4.0.2",
34
+ "@skyra/discord-components-core": "4.0.2",
35
35
  "debug": "^4.4.3",
36
36
  "discord-markdown-parser": "~1.3.0",
37
37
  "highlight.js": "^11.11.1",
@@ -39,7 +39,10 @@
39
39
  "react-dom": "^19.2.3",
40
40
  "simple-markdown": "^0.7.3",
41
41
  "twemoji": "^14.0.2",
42
- "undici": "^7.29.0"
42
+ "undici": "^7.29.1"
43
+ },
44
+ "devDependencies": {
45
+ "discord.js": "14.27.0"
43
46
  },
44
47
  "peerDependencies": {
45
48
  "discord.js": "^14.0.0 || ^15.0.0",
@@ -54,6 +57,8 @@
54
57
  "node": ">=20"
55
58
  },
56
59
  "scripts": {
60
+ "test": "node --test",
61
+ "test:update": "node --test tests/golden.test.js",
57
62
  "prepublishOnly": "node -e \"require('fs').accessSync('dist/index.js'); require('fs').accessSync('dist/index.d.ts');\" || (echo 'dist/index.js or dist/index.d.ts missing — refusing to publish' && exit 1)"
58
63
  },
59
64
  "publishConfig": {