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.
- package/dist/downloader/images.js +11 -0
- package/dist/generator/index.js +45 -8
- package/dist/generator/renderers/components/Container.js +10 -4
- package/dist/generator/renderers/components/Media Gallery.js +5 -5
- package/dist/generator/renderers/components/Spacing.js +18 -6
- package/dist/generator/renderers/components/section/Section.js +13 -3
- package/dist/generator/renderers/components/section/SectionAccessory.js +8 -7
- package/dist/generator/renderers/components/section/SectionContent.js +9 -4
- package/dist/generator/renderers/content.js +219 -42
- package/dist/generator/renderers/embed.js +31 -7
- package/dist/generator/renderers/message.js +211 -30
- package/dist/generator/renderers/reply.js +14 -3
- package/dist/generator/renderers/systemMessage.js +62 -9
- package/dist/index.js +25 -3
- package/dist/static/client.js +496 -25
- package/dist/utils/buildProfiles.js +19 -1
- package/dist/utils/utils.js +1 -1
- package/package.json +1 -1
|
@@ -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')));
|
package/dist/generator/index.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
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", {
|
|
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", {
|
|
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", {
|
|
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
|
-
|
|
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:#
|
|
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
|
-
|
|
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: '
|
|
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
|
|
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
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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", {
|
|
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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
5
|
+
if (!children) return null;
|
|
6
|
+
return ((0, jsx_runtime_1.jsx)("div", {
|
|
7
|
+
style: {
|
|
8
8
|
display: 'flex',
|
|
9
|
-
|
|
10
|
-
maxWidth: '500px',
|
|
9
|
+
flex: '0 0 auto',
|
|
11
10
|
justifyContent: 'flex-end',
|
|
12
11
|
alignItems: 'center',
|
|
13
|
-
},
|
|
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", {
|
|
5
|
+
return ((0, jsx_runtime_1.jsx)("div", {
|
|
6
|
+
style: {
|
|
6
7
|
display: 'flex',
|
|
7
8
|
flexDirection: 'column',
|
|
8
|
-
|
|
9
|
-
|
|
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
|