discord-html-transcripts-fix 1.2.0 → 1.4.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.
@@ -67,10 +67,21 @@ class TranscriptImageDownloader {
67
67
  return null;
68
68
  });
69
69
  if (!response) return undefined;
70
+ // SECURITY: only accept success responses — avoid embedding HTML/error bodies as images.
71
+ if (response.statusCode < 200 || response.statusCode >= 300) {
72
+ this.log('non-2xx status for %s: %d', attachment.id, response.statusCode);
73
+ try { await response.body.arrayBuffer(); } catch (_e) {}
74
+ return undefined;
75
+ }
70
76
  const rawMime = response.headers['content-type'];
71
77
  const mime = (0, utils_1.safeImageMime)(typeof rawMime === 'string' ? rawMime : '', 'image/png');
72
78
  const arr = await response.body.arrayBuffer();
73
79
  const buffer = Buffer.from(arr);
80
+ // SECURITY: enforce post-download size against the configured cap (metadata-only check is insufficient).
81
+ if (this.maxFileSize && buffer.length > this.maxFileSize * 1024) {
82
+ this.log('payload exceeded cap for %s (%d > %d)', attachment.id, buffer.length, this.maxFileSize * 1024);
83
+ return undefined;
84
+ }
74
85
  this.log('fetched %s (%d bytes, mime=%s)', attachment.id, buffer.length, mime);
75
86
  if (this.compression) {
76
87
  const sharp = this._sharpModule || (this._sharpModule = await __importStar(require('sharp')));
@@ -40,6 +40,7 @@ try {
40
40
  function computeStats(messages) {
41
41
  let images = 0;
42
42
  let humans = new Set();
43
+ let firstTs = null, lastTs = null;
43
44
  for (const m of messages) {
44
45
  if (m.attachments && m.attachments.forEach) {
45
46
  m.attachments.forEach((a) => {
@@ -47,8 +48,20 @@ function computeStats(messages) {
47
48
  });
48
49
  }
49
50
  if (m.author && !m.author.bot) humans.add(m.author.id);
51
+ const ts = m.createdAt instanceof Date ? m.createdAt.getTime() : (snowflakeToDate(m.id)?.getTime() || null);
52
+ if (ts) {
53
+ if (firstTs === null || ts < firstTs) firstTs = ts;
54
+ if (lastTs === null || ts > lastTs) lastTs = ts;
55
+ }
50
56
  }
51
- return { messageCount: messages.length, participantCount: humans.size, imageCount: images };
57
+ return {
58
+ messageCount: messages.length,
59
+ participantCount: humans.size,
60
+ imageCount: images,
61
+ firstTs,
62
+ lastTs,
63
+ spanMs: firstTs && lastTs ? lastTs - firstTs : null,
64
+ };
52
65
  }
53
66
 
54
67
  function snowflakeToDate(id) {
@@ -60,15 +73,16 @@ function snowflakeToDate(id) {
60
73
  }
61
74
 
62
75
  function buildDateSeparators(messages, lang) {
63
- // Returns an array same length as messages, each entry: separator string or null
64
76
  const seps = new Array(messages.length).fill(null);
65
77
  let lastYmd = null;
78
+ // FIX: use local date (matches what the formatter shows). UTC keys were mismatched with the
79
+ // local-time formatter output, causing the wrong day label near midnight UTC.
66
80
  const fmt = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' });
67
81
  for (let i = 0; i < messages.length; i++) {
68
82
  const m = messages[i];
69
83
  const d = m.createdAt instanceof Date ? m.createdAt : snowflakeToDate(m.id);
70
84
  if (!d) continue;
71
- const ymd = `${d.getUTCFullYear()}-${d.getUTCMonth()}-${d.getUTCDate()}`;
85
+ const ymd = `${d.getFullYear()}-${d.getMonth()}-${d.getDate()}`;
72
86
  if (ymd !== lastYmd) {
73
87
  seps[i] = fmt.format(d);
74
88
  lastYmd = ymd;
@@ -122,6 +136,8 @@ async function render(_a) {
122
136
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.tocStyles } }),
123
137
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.i18nStyles } }),
124
138
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.dateSeparatorStyles } }),
139
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.markdownStyles } }),
140
+ (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.filterPanelStyles } }),
125
141
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordImage_1.DiscordAttachmentStyles } }),
126
142
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordHighlightedCode_1.DiscordHighlightStyles } }),
127
143
  (0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: extraStyles } }),
@@ -146,6 +162,9 @@ async function render(_a) {
146
162
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.lightbox } }),
147
163
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.toc } }),
148
164
  (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.i18nSwitcher } }),
165
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.filterPanel } }),
166
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.codeCopyButton } }),
167
+ (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.permalinkButton } }),
149
168
  options.hydrate && (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.revealSpoiler } }),
150
169
  ] })
151
170
  ] });
@@ -171,19 +190,37 @@ async function renderToStream(args) {
171
190
  return render(Object.assign({}, args, { returnType: 'stream' }));
172
191
  }
173
192
 
193
+ function formatSpan(ms, lang) {
194
+ if (!ms || ms < 0) return '';
195
+ const days = Math.floor(ms / 86400000);
196
+ const hours = Math.floor((ms % 86400000) / 3600000);
197
+ const mins = Math.floor((ms % 3600000) / 60000);
198
+ if (days > 0) return `${days}d ${hours}h`;
199
+ if (hours > 0) return `${hours}h ${mins}m`;
200
+ if (mins > 0) return `${mins}m`;
201
+ return '<1m';
202
+ }
174
203
  function StatsFooter({ stats, lang, i18n }) {
175
204
  const dict = (i18n && i18n[lang]) || (i18n && i18n.en) || {};
205
+ const locale = lang === 'de' ? 'de-DE' : 'en-US';
206
+ const fmtDate = (ms) => new Date(ms).toLocaleString(locale, { dateStyle: 'medium', timeStyle: 'short' });
176
207
  return (0, jsx_runtime_1.jsxs)("footer", { className: "dht-stats", children: [
177
- (0, jsx_runtime_1.jsx)("span", { "data-i18n": "statsMessages", "data-i18n-params": JSON.stringify({ n: stats.messageCount }), children: (dict.statsMessages || '{n} messages').replace('{n}', stats.messageCount) }),
208
+ (0, jsx_runtime_1.jsx)("span", { children: (dict.statsMessages || '{n} messages').replace('{n}', stats.messageCount) }),
178
209
  ' · ',
179
- (0, jsx_runtime_1.jsx)("span", { "data-i18n": "statsParticipants", "data-i18n-params": JSON.stringify({ n: stats.participantCount }), children: (dict.statsParticipants || '{n} participants').replace('{n}', stats.participantCount) }),
210
+ (0, jsx_runtime_1.jsx)("span", { children: (dict.statsParticipants || '{n} participants').replace('{n}', stats.participantCount) }),
180
211
  ' · ',
181
- (0, jsx_runtime_1.jsx)("span", { "data-i18n": "statsImages", "data-i18n-params": JSON.stringify({ n: stats.imageCount }), children: (dict.statsImages || '{n} images').replace('{n}', stats.imageCount) }),
212
+ (0, jsx_runtime_1.jsx)("span", { children: (dict.statsImages || '{n} images').replace('{n}', stats.imageCount) }),
213
+ stats.firstTs && stats.lastTs && (0, jsx_runtime_1.jsxs)("span", { children: [' · ', fmtDate(stats.firstTs), ' → ', fmtDate(stats.lastTs)] }),
214
+ stats.spanMs && (0, jsx_runtime_1.jsxs)("span", { children: [' · ', formatSpan(stats.spanMs)] }),
182
215
  ] });
183
216
  }
184
217
 
185
218
  const extraStyles = `
186
- .dht-pinned-indicator{position:absolute;top:6px;right:8px;font-size:12px;opacity:.6}
219
+ /* Ensure the host establishes a positioning context for the absolute pin indicator,
220
+ and clamp it inside the message box on narrow viewports. */
221
+ .dht-msg-pinned{position:relative}
222
+ .dht-pinned-indicator{position:absolute;top:4px;left:4px;font-size:12px;opacity:.75;pointer-events:none}
223
+ @media (max-width:640px){.dht-pinned-indicator{position:static;display:inline-block;margin-right:4px}}
187
224
  .dht-app-badge,.dht-popup-badge{background:#5865F2;color:#fff;font-size:10px;padding:1px 6px;border-radius:4px;margin-left:6px;vertical-align:middle;font-weight:600}
188
225
  .dht-edit-marker{color:#949ba4;font-size:11px;margin-left:4px;cursor:help}
189
226
  .dht-suppressed{color:#6e727a;font-size:11px;font-style:italic}
@@ -211,7 +248,7 @@ const extraStyles = `
211
248
  .dht-edit-history li{margin-top:4px}
212
249
  .dht-edit-history time{display:block;color:#6e727a;font-size:10px;margin-bottom:2px}
213
250
  .dht-thread-badge{display:inline-block;background:#3f4248;color:#dbdee1;font-size:10px;padding:1px 6px;border-radius:4px;margin-left:6px}
214
- .dht-thread-badge--locked{background:#dc3545}
251
+ .dht-thread-badge--locked{background:#ED4245}
215
252
  .dht-thread-badge--archived{background:#6c757d}
216
253
  .dht-gifv{max-width:400px;max-height:300px;border-radius:6px}
217
254
  .dht-stats{margin:24px auto;padding:12px;text-align:center;color:#b5bac1;font-size:13px;font-family:"gg sans",sans-serif;border-top:1px solid #2b2d31;max-width:800px}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
+ const utils_1 = require("../../../utils/utils");
4
5
 
5
6
  function colorToHex(color) {
6
7
  if (color == null) return null;
@@ -14,14 +15,18 @@ function colorToHex(color) {
14
15
  }
15
16
 
16
17
  function DiscordContainer({ children, accentColor, spoiler }) {
17
- const hex = colorToHex(accentColor);
18
+ // SECURITY: route accentColor through safeColor so an attacker-controlled
19
+ // string can't smuggle CSS into the inline borderLeft.
20
+ const raw = colorToHex(accentColor);
21
+ const hex = raw ? (0, utils_1.safeColor)(raw, null) : null;
18
22
  const style = {
19
23
  display: 'flex',
20
- width: '500px',
24
+ width: '100%',
25
+ maxWidth: '500px',
26
+ minWidth: 0,
21
27
  flexDirection: 'column',
22
28
  backgroundColor: '#3f4248',
23
29
  padding: '16px',
24
- // Use accent color as a left border (Discord's V2 container style)
25
30
  borderLeft: hex ? `4px solid ${hex}` : '1px solid #4f5359',
26
31
  borderTop: '1px solid #4f5359',
27
32
  borderRight: '1px solid #4f5359',
@@ -30,8 +35,9 @@ function DiscordContainer({ children, accentColor, spoiler }) {
30
35
  marginBottom: '2px',
31
36
  borderRadius: '10px',
32
37
  gap: '8px',
38
+ boxSizing: 'border-box',
33
39
  };
34
- const node = (0, jsx_runtime_1.jsx)("div", { style: style, children: children });
40
+ const node = (0, jsx_runtime_1.jsx)("div", { style, children });
35
41
  if (spoiler) {
36
42
  return (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", children: node });
37
43
  }
@@ -10,11 +10,11 @@ function DiscordMediaGallery({ component }) {
10
10
  const imagesToShow = component.items.slice(0, 10);
11
11
  const hasMore = component.items.length > 10;
12
12
  return ((0, jsx_runtime_1.jsx)("div", { style: (0, utils_1.getGalleryLayout)(count), children: imagesToShow.map((media, idx) => {
13
- const img = (0, jsx_runtime_1.jsx)("img", { src: media.media.url, alt: media.description || 'Media content', style: {
14
- width: '100%',
15
- height: '100%',
16
- objectFit: 'cover',
17
- } });
13
+ const url = media.media.url;
14
+ const isVideo = typeof url === 'string' && /\.(mp4|webm|mov)(\?|$)/i.test(url);
15
+ const img = isVideo
16
+ ? (0, jsx_runtime_1.jsx)("video", { src: url, autoPlay: true, loop: true, muted: true, playsInline: true, style: { width: '100%', height: '100%', objectFit: 'cover' } })
17
+ : (0, jsx_runtime_1.jsx)("img", { src: url, alt: media.description || 'Media content', loading: 'lazy', style: { width: '100%', height: '100%', objectFit: 'cover' } });
18
18
  const wrapped = media.spoiler
19
19
  ? (0, jsx_runtime_1.jsx)("span", { className: "discord-spoiler", style: { display: 'block', width: '100%', height: '100%' }, children: img })
20
20
  : img;
@@ -2,13 +2,25 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  const discord_js_1 = require("discord.js");
5
+
6
+ /**
7
+ * V2 Separator component. `spacing` is Small (1) or Large (2). `divider` toggles the visual line.
8
+ * In Discord's UI: Small ≈ 8px, Large ≈ 16px vertical padding. Divider draws a 1px line.
9
+ */
5
10
  function DiscordSeparator({ divider, spacing }) {
6
- return ((0, jsx_runtime_1.jsx)("div", { style: {
11
+ const isLarge = spacing === discord_js_1.SeparatorSpacingSize?.Large || spacing === 2;
12
+ const pad = isLarge ? 16 : 8;
13
+ const showLine = divider !== false; // default true in Discord — only false when explicitly set
14
+ return (0, jsx_runtime_1.jsx)("div", {
15
+ className: "dht-v2-separator",
16
+ style: {
7
17
  width: '100%',
8
- height: divider ? '1px' : '0px',
9
- backgroundColor: '#4f5359',
10
- margin: spacing === discord_js_1.SeparatorSpacingSize.Large ? '8px 0' : '0',
11
- } }));
18
+ padding: `${pad}px 0`,
19
+ display: 'flex',
20
+ alignItems: 'center',
21
+ },
22
+ children: showLine ? (0, jsx_runtime_1.jsx)("div", { style: { width: '100%', height: '1px', backgroundColor: 'rgba(79,84,89,0.65)' } }) : null
23
+ });
12
24
  }
13
25
  exports.default = DiscordSeparator;
14
- //# sourceMappingURL=Spacing.js.map
26
+ //# sourceMappingURL=Spacing.js.map
@@ -8,12 +8,22 @@ const components_1 = require("../../components");
8
8
  const SectionContent_1 = __importDefault(require("./SectionContent"));
9
9
  const SectionAccessory_1 = __importDefault(require("./SectionAccessory"));
10
10
  function DiscordSection({ children, accessory, id }) {
11
- return ((0, jsx_runtime_1.jsxs)("div", { style: {
11
+ return ((0, jsx_runtime_1.jsxs)("div", {
12
+ style: {
12
13
  display: 'flex',
13
14
  flexDirection: 'row',
14
15
  width: '100%',
15
16
  maxWidth: '500px',
16
- }, children: [(0, jsx_runtime_1.jsx)(SectionContent_1.default, { children: children }), (0, jsx_runtime_1.jsx)(SectionAccessory_1.default, { children: accessory && (0, jsx_runtime_1.jsx)(components_1.Component, { component: accessory, id: id }) })] }));
17
+ minWidth: 0,
18
+ gap: '12px',
19
+ alignItems: 'flex-start',
20
+ boxSizing: 'border-box',
21
+ },
22
+ children: [
23
+ (0, jsx_runtime_1.jsx)(SectionContent_1.default, { children }),
24
+ accessory ? (0, jsx_runtime_1.jsx)(SectionAccessory_1.default, { children: (0, jsx_runtime_1.jsx)(components_1.Component, { component: accessory, id }) }) : null,
25
+ ]
26
+ }));
17
27
  }
18
28
  exports.default = DiscordSection;
19
- //# sourceMappingURL=Section.js.map
29
+ //# sourceMappingURL=Section.js.map
@@ -2,15 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  function SectionAccessory({ children }) {
5
- if (!children)
6
- return null;
7
- return ((0, jsx_runtime_1.jsx)("div", { style: {
5
+ if (!children) return null;
6
+ return ((0, jsx_runtime_1.jsx)("div", {
7
+ style: {
8
8
  display: 'flex',
9
- width: '100%',
10
- maxWidth: '500px',
9
+ flex: '0 0 auto',
11
10
  justifyContent: 'flex-end',
12
11
  alignItems: 'center',
13
- }, children: children }));
12
+ },
13
+ children
14
+ }));
14
15
  }
15
16
  exports.default = SectionAccessory;
16
- //# sourceMappingURL=SectionAccessory.js.map
17
+ //# sourceMappingURL=SectionAccessory.js.map
@@ -2,11 +2,16 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const jsx_runtime_1 = require("react/jsx-runtime");
4
4
  function SectionContent({ children }) {
5
- return ((0, jsx_runtime_1.jsx)("div", { style: {
5
+ return ((0, jsx_runtime_1.jsx)("div", {
6
+ style: {
6
7
  display: 'flex',
7
8
  flexDirection: 'column',
8
- width: '100%',
9
- }, children: children }));
9
+ flex: 1,
10
+ minWidth: 0, // allow text to shrink/wrap inside flex parent
11
+ overflowWrap: 'anywhere',
12
+ },
13
+ children
14
+ }));
10
15
  }
11
16
  exports.default = SectionContent;
12
- //# sourceMappingURL=SectionContent.js.map
17
+ //# sourceMappingURL=SectionContent.js.map