discord-html-transcripts-fix 1.0.0 → 1.2.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/README.md +87 -66
- package/dist/downloader/images.js +60 -85
- package/dist/generator/index.js +197 -36
- package/dist/generator/renderers/attachment.js +27 -36
- package/dist/generator/renderers/components/Container.js +36 -14
- package/dist/generator/renderers/components/Media Gallery.js +24 -18
- package/dist/generator/renderers/components.js +7 -2
- package/dist/generator/renderers/content.js +68 -39
- package/dist/generator/renderers/embed.js +55 -34
- package/dist/generator/renderers/message.js +201 -12
- package/dist/generator/renderers/reply.js +47 -39
- package/dist/generator/renderers/systemMessage.js +138 -43
- package/dist/generator/transcript.js +42 -56
- package/dist/index.js +59 -86
- package/dist/static/client.js +229 -28
- package/dist/types.js +1 -0
- package/dist/utils/buildProfiles.js +141 -29
- package/dist/utils/utils.js +74 -19
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -1,92 +1,113 @@
|
|
|
1
|
-
# `discord-html-transcripts`
|
|
1
|
+
# `discord-html-transcripts-fix`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A nicely formatted HTML transcript generator for discord.js with full **Components V2** support, an **interactive viewer** (search, lightbox, mention popups, i18n) and **hardened security**.
|
|
4
|
+
|
|
5
|
+
Forked from [discord-html-transcripts](https://github.com/ItzDerock/discord-html-transcripts).
|
|
4
6
|
|
|
5
7
|
## Changes compared to the original
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
-
|
|
9
|
+
### Security
|
|
10
|
+
- **Critical fix**: `</script>` breakout via inlined JSON is prevented (`<`, `>`, `&`, U+2028/2029 are escaped)
|
|
11
|
+
- **Critical fix**: markdown links with `javascript:`, `data:`, `vbscript:` and similar dangerous URI schemes are rewritten to `#`
|
|
12
|
+
- **Hardened**: inline `style="color:…"` sinks in the mention popup are hex-validated to block CSS injection
|
|
13
|
+
- **Hardened**: `data:` URI MIME types from the image downloader are restricted to image types only (no `text/html` smuggling)
|
|
14
|
+
- **Fixed**: `process.exit(1)` on discord.js version mismatch removed — library no longer kills the host bot
|
|
15
|
+
|
|
16
|
+
### Robustness
|
|
17
|
+
- **Fix**: invalid Discord timestamp markers (`<t:abc:F>`, oversized values) no longer abort the transcript with `RangeError`
|
|
18
|
+
- **Fix**: per-AST-node error boundary in `MessageSingleASTNode` and per-message error boundary in `DiscordMessage` — one broken message can never kill the whole render any more
|
|
19
|
+
- **Fix**: `parseDiscordEmoji` no longer throws on deleted reactions with `emoji.name === null`
|
|
20
|
+
- **Fix**: `formatBytes(null/undefined/NaN)` no longer returns `NaN undefined`
|
|
21
|
+
- **Fix**: `createTranscript` slice uses the resolved limit instead of the raw `limit`
|
|
22
|
+
- **Fix**: embed fields are rendered through a proper async component (was an inline `async` arrow inside `.map()` — undefined React behaviour)
|
|
23
|
+
- **Fix**: `JoinMessage` text is now deterministic per message ID — re-rendering the same channel always yields the same join line
|
|
24
|
+
- **Fix**: random `console.log` calls in production paths replaced by the `debug` namespace
|
|
25
|
+
|
|
26
|
+
### New: interactive viewer
|
|
27
|
+
- **Clickable mentions** — clicking a user / role / channel mention opens a Discord-style popup with avatar, display name, username, all roles (colored pills), server-since, account-since, color, member count, channel topic, etc.
|
|
28
|
+
- **Search bar** — Ctrl+F is intercepted, live highlighting with "x of n matches", prev / next, Enter / Shift+Enter to navigate, Escape to close
|
|
29
|
+
- **Image lightbox** — click any image to open fullscreen, arrow keys to nav, Escape to close
|
|
30
|
+
- **Participant sidebar (TOC)** — toggle button (top-left), sorted list of every participant, click to jump to their first message
|
|
31
|
+
- **Back-to-top** floating button fades in after scrolling
|
|
32
|
+
- **Date separators** between messages — `— Tuesday, May 13 2026 —` style, automatically inserted on day boundaries
|
|
33
|
+
- **Stats footer** — `324 messages · 12 participants · 5 images` (auto-computed)
|
|
34
|
+
- **Language switcher** (i18n) — built-in English and German dictionaries, switch live in the viewer. Pass your own via `options.i18n: { de: { … } }` to extend
|
|
35
|
+
|
|
36
|
+
### New: more Discord content rendered
|
|
37
|
+
- **Stickers** — image, name, alt
|
|
38
|
+
- **Polls** — question, answer bars with vote counts and percentages, expiry
|
|
39
|
+
- **Forwarded messages (`messageSnapshots`)** — quoted-block style with original author
|
|
40
|
+
- **Voice messages** — `🎤` indicator, inline SVG waveform from `attachment.waveform`, duration
|
|
41
|
+
- **Edit timestamp tooltip** on `(edited)` marker
|
|
42
|
+
- **Optional edit history** — pass `message.editHistory = [{content, editedAt}, …]` and the viewer shows a collapsible `<details>` block
|
|
43
|
+
- **Pinned indicator** (`📌` badge on pinned messages)
|
|
44
|
+
- **App badge** for application/bot messages with `applicationId`
|
|
45
|
+
- **Suppressed embeds flag** is honored — when set, embeds aren't rendered (and a small `(embeds hidden)` note is shown instead)
|
|
46
|
+
- **Cross-guild replies** are no longer silently dropped — they show a "Message from another server" pill
|
|
47
|
+
- **Burst / super-reactions** are flagged
|
|
48
|
+
- **Thread state badges** — `Archived`, `Locked`
|
|
49
|
+
- **GIFV / animated GIFs** are rendered with `<video autoplay loop muted>` like in Discord
|
|
50
|
+
- **Attachment description (alt text)** is used as `alt`/`title`
|
|
51
|
+
- **`<id:guide>`, `<id:browse>`, `<id:customize>`** etc. pseudo-channels show as styled pills with proper labels
|
|
52
|
+
- **`</cmd:id>` slash command mentions** render as blue pills
|
|
53
|
+
- **Embed video** link and **embed provider** ("YouTube" etc.) are shown
|
|
54
|
+
- **`RoleSubscriptionPurchase`** system message renders properly
|
|
55
|
+
- **`AutoModerationAction`** shows the matched rule name and blocked content excerpt
|
|
56
|
+
- **Container component** with `accent_color` (left border) and `spoiler` support
|
|
57
|
+
- **Media gallery** per-item spoiler support
|
|
58
|
+
- Many additional system message types (`ChannelNameChange`, `ChannelIconChange`, `ThreadCreated`, `ChatInputCommand`, `ContextMenuCommand`, `Call`, `ChannelFollowAdd`, `RecipientRemove`, guild incident reports, poll result)
|
|
59
|
+
|
|
60
|
+
### Performance
|
|
61
|
+
- **Single-pass profile collector** — `buildAllContext` builds profiles + extended dicts in one walk over the message list
|
|
62
|
+
- **Image downloader is concurrent** — bounded pool (default 6, configurable via `withConcurrency`) instead of sequential
|
|
63
|
+
- **`@skyra/discord-components-core` version is pinned** to an exact resolved version → CDN cacheable
|
|
64
|
+
- **Inline JSON** is shipped via `<script type="application/json">` so it doesn't block HTML parse
|
|
65
|
+
- **Emoji URL resolution** is memoized
|
|
66
|
+
- **`returnType: 'stream'`** option streams the rendered HTML out instead of buffering — usable for 5,000+ message tickets
|
|
67
|
+
|
|
68
|
+
### DX
|
|
69
|
+
- **`react`, `react-dom`, `debug` moved into regular dependencies** so users don't install them manually (`debug` was actually a missing runtime dep in the original — `images.js` requires it)
|
|
70
|
+
- TypeScript build target unchanged; only `discord.js` remains a peer dependency
|
|
9
71
|
|
|
10
72
|
## Requirements
|
|
11
73
|
|
|
12
|
-
|
|
74
|
+
Only `discord.js` as a peer dependency. Everything else is auto-installed:
|
|
13
75
|
|
|
14
76
|
```bash
|
|
15
|
-
npm install
|
|
77
|
+
npm install discord-html-transcripts-fix
|
|
16
78
|
```
|
|
17
79
|
|
|
18
80
|
## Usage
|
|
19
81
|
|
|
20
|
-
### Example usage using the built in message fetcher.
|
|
21
|
-
|
|
22
82
|
```js
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
83
|
+
const { createTranscript } = require('discord-html-transcripts-fix');
|
|
84
|
+
|
|
85
|
+
const attachment = await createTranscript(channel, {
|
|
86
|
+
// existing options
|
|
87
|
+
limit: -1,
|
|
88
|
+
saveImages: false,
|
|
89
|
+
poweredBy: true,
|
|
90
|
+
// new options
|
|
91
|
+
language: 'de', // 'en' (default) or 'de' — also exposed live in the viewer
|
|
92
|
+
i18n: { de: { joined: 'kam an' } }, // optional overrides per language
|
|
93
|
+
returnType: 'stream', // 'buffer' | 'string' | 'attachment' (default) | 'stream'
|
|
33
94
|
});
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
### Or if you prefer, you can pass in your own messages.
|
|
37
|
-
|
|
38
|
-
```js
|
|
39
|
-
const discordTranscripts = require('discord-html-transcripts');
|
|
40
|
-
// or (if using typescript) import * as discordTranscripts from 'discord-html-transcripts';
|
|
41
95
|
|
|
42
|
-
|
|
43
|
-
const channel = someWayToGetChannel(); // Used for ticket name, guild icon, and guild name
|
|
44
|
-
|
|
45
|
-
// Must be awaited
|
|
46
|
-
const attachment = await discordTranscripts.generateFromMessages(messages, channel);
|
|
47
|
-
|
|
48
|
-
channel.send({
|
|
49
|
-
files: [attachment],
|
|
50
|
-
});
|
|
96
|
+
channel.send({ files: [attachment] });
|
|
51
97
|
```
|
|
52
98
|
|
|
53
|
-
|
|
99
|
+
### Hooking edit history (optional)
|
|
54
100
|
|
|
55
|
-
|
|
56
|
-
**All configuration options are optional!**
|
|
57
|
-
|
|
58
|
-
### Built in Message Fetcher
|
|
101
|
+
If your bot tracks message edits, attach the history to the message before passing it to the renderer:
|
|
59
102
|
|
|
60
103
|
```js
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
saveImages: false, // Download all images and include the image data in the HTML (allows viewing the image even after it has been deleted) (! WILL INCREASE FILE SIZE !)
|
|
66
|
-
footerText: "Exported {number} message{s}", // Change text at footer, don't forget to put {number} to show how much messages got exported, and {s} for plural
|
|
67
|
-
callbacks: {
|
|
68
|
-
// register custom callbacks for the following:
|
|
69
|
-
resolveChannel: (channelId: string) => Awaitable<Channel | null>,
|
|
70
|
-
resolveUser: (userId: string) => Awaitable<User | null>,
|
|
71
|
-
resolveRole: (roleId: string) => Awaitable<Role | null>,
|
|
72
|
-
resolveImageSrc: (
|
|
73
|
-
attachment: APIAttachment,
|
|
74
|
-
message: APIMessage
|
|
75
|
-
) => Awaitable<string | null | undefined>
|
|
76
|
-
},
|
|
77
|
-
poweredBy: true, // Whether to include the "Powered by discord-html-transcripts" footer
|
|
78
|
-
hydrate: true, // Whether to hydrate the html server-side
|
|
79
|
-
filter: (message) => true // Filter messages, e.g. (message) => !message.author.bot
|
|
80
|
-
});
|
|
104
|
+
message.editHistory = [
|
|
105
|
+
{ content: 'first version', editedAt: new Date('2025-05-13T11:02Z') },
|
|
106
|
+
{ content: 'corrected version', editedAt: new Date('2025-05-13T11:05Z') },
|
|
107
|
+
];
|
|
81
108
|
```
|
|
82
109
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```js
|
|
86
|
-
const attachment = await discordTranscripts.generateFromMessages(messages, channel, {
|
|
87
|
-
// Same as createTranscript, except no limit or filter
|
|
88
|
-
});
|
|
89
|
-
```
|
|
110
|
+
The viewer will then render a collapsible `<details>` block next to the `(edited)` marker.
|
|
90
111
|
|
|
91
112
|
## Credits
|
|
92
113
|
|
|
@@ -1,36 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
2
|
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
3
|
+
var ownKeys = function(o){ownKeys = Object.getOwnPropertyNames || function (o){var ar=[];for (var k in o) if (Object.prototype.hasOwnProperty.call(o,k)) ar[ar.length]=k;return ar;};return ownKeys(o);};
|
|
4
|
+
return function (mod){if (mod && mod.__esModule) return mod;var result={};if (mod != null) for (var k=ownKeys(mod), i=0; i<k.length; i++) if (k[i] !== "default") Object.defineProperty(result,k[i],{get:function(){return mod[k[i]];}});Object.defineProperty(result,"default",{value:mod,enumerable:true});return result;};
|
|
34
5
|
})();
|
|
35
6
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
7
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
@@ -39,76 +10,80 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
10
|
exports.TranscriptImageDownloader = void 0;
|
|
40
11
|
const undici_1 = require("undici");
|
|
41
12
|
const debug_1 = __importDefault(require("debug"));
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
13
|
+
const utils_1 = require("../utils/utils");
|
|
14
|
+
|
|
15
|
+
// Small inline concurrency limiter — no extra dep
|
|
16
|
+
function createLimiter(max) {
|
|
17
|
+
let active = 0;
|
|
18
|
+
const queue = [];
|
|
19
|
+
const next = () => {
|
|
20
|
+
if (active >= max || queue.length === 0) return;
|
|
21
|
+
active++;
|
|
22
|
+
const { fn, resolve, reject } = queue.shift();
|
|
23
|
+
fn().then((v) => { active--; resolve(v); next(); }, (e) => { active--; reject(e); next(); });
|
|
24
|
+
};
|
|
25
|
+
return (fn) => new Promise((resolve, reject) => { queue.push({ fn, resolve, reject }); next(); });
|
|
26
|
+
}
|
|
27
|
+
|
|
45
28
|
class TranscriptImageDownloader {
|
|
46
29
|
constructor() {
|
|
47
30
|
this.log = TranscriptImageDownloader.log;
|
|
31
|
+
this.maxConcurrent = 6;
|
|
32
|
+
this._sharpModule = null;
|
|
33
|
+
this._limit = null;
|
|
48
34
|
}
|
|
49
|
-
/**
|
|
50
|
-
* Sets the maximum file size for *each* individual image.
|
|
51
|
-
* @param size The maximum file size in kilobytes
|
|
52
|
-
*/
|
|
53
35
|
withMaxSize(size) {
|
|
54
36
|
this.maxFileSize = size;
|
|
55
37
|
return this;
|
|
56
38
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* @param convertToWebP Whether to convert the image to WebP format
|
|
62
|
-
*/
|
|
39
|
+
withConcurrency(n) {
|
|
40
|
+
this.maxConcurrent = Math.max(1, Number(n) || 1);
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
63
43
|
withCompression(quality = 80, convertToWebP = false, options = {}) {
|
|
64
|
-
if (quality < 1 || quality > 100)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
44
|
+
if (quality < 1 || quality > 100) throw new Error('Quality must be between 1 and 100');
|
|
45
|
+
// Hoist the sharp import so we don't pay it per image
|
|
46
|
+
Promise.resolve()
|
|
47
|
+
.then(() => __importStar(require('sharp')))
|
|
48
|
+
.then((s) => { this._sharpModule = s; })
|
|
49
|
+
.catch((err) => {
|
|
50
|
+
this.log('sharp import failed: %s', err && err.message ? err.message : err);
|
|
51
|
+
});
|
|
71
52
|
this.compression = { quality, convertToWebP, options };
|
|
72
53
|
return this;
|
|
73
54
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Builds the image saving callback.
|
|
76
|
-
*/
|
|
77
55
|
build() {
|
|
56
|
+
if (!this._limit) this._limit = createLimiter(this.maxConcurrent);
|
|
78
57
|
return async (attachment) => {
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
// if the max file size is set, check if the file size is within the limit
|
|
83
|
-
if (this.maxFileSize && attachment.size > this.maxFileSize * 1024)
|
|
84
|
-
return undefined;
|
|
85
|
-
// fetch the image
|
|
86
|
-
this.log(`Fetching attachment ${attachment.id}: ${attachment.url}`);
|
|
87
|
-
const response = await (0, undici_1.request)(attachment.url).catch((err) => {
|
|
88
|
-
console.error(`[discord-html-transcripts] Failed to download image for transcript: `, err);
|
|
89
|
-
return null;
|
|
90
|
-
});
|
|
91
|
-
if (!response)
|
|
92
|
-
return undefined;
|
|
93
|
-
const mimetype = response.headers['content-type'];
|
|
94
|
-
const buffer = await response.body.arrayBuffer().then((res) => Buffer.from(res));
|
|
95
|
-
this.log(`Finished fetching ${attachment.id} (${buffer.length} bytes)`);
|
|
96
|
-
// if the compression options are set, compress the image
|
|
97
|
-
if (this.compression) {
|
|
98
|
-
const sharp = await Promise.resolve().then(() => __importStar(require('sharp')));
|
|
99
|
-
this.log(`Compressing ${attachment.id} with 'sharp'`);
|
|
100
|
-
const sharpbuf = await sharp
|
|
101
|
-
.default(buffer)
|
|
102
|
-
.webp(Object.assign({ quality: this.compression.quality, force: this.compression.convertToWebP, effort: 2 }, this.compression.options))
|
|
103
|
-
.toBuffer({ resolveWithObject: true });
|
|
104
|
-
this.log(`Finished compressing ${attachment.id} (${sharpbuf.info.size} bytes)`);
|
|
105
|
-
return `data:image/${sharpbuf.info.format};base64,${sharpbuf.data.toString('base64')}`;
|
|
106
|
-
}
|
|
107
|
-
// return the base64 string
|
|
108
|
-
return `data:${mimetype};base64,${buffer.toString('base64')}`;
|
|
58
|
+
if (!attachment.width || !attachment.height) return undefined;
|
|
59
|
+
if (this.maxFileSize && attachment.size > this.maxFileSize * 1024) return undefined;
|
|
60
|
+
return this._limit(() => this._fetchOne(attachment));
|
|
109
61
|
};
|
|
110
62
|
}
|
|
63
|
+
async _fetchOne(attachment) {
|
|
64
|
+
this.log('fetch %s %s', attachment.id, attachment.url);
|
|
65
|
+
const response = await (0, undici_1.request)(attachment.url).catch((err) => {
|
|
66
|
+
this.log('download failed for %s: %s', attachment.id, err && err.message ? err.message : err);
|
|
67
|
+
return null;
|
|
68
|
+
});
|
|
69
|
+
if (!response) return undefined;
|
|
70
|
+
const rawMime = response.headers['content-type'];
|
|
71
|
+
const mime = (0, utils_1.safeImageMime)(typeof rawMime === 'string' ? rawMime : '', 'image/png');
|
|
72
|
+
const arr = await response.body.arrayBuffer();
|
|
73
|
+
const buffer = Buffer.from(arr);
|
|
74
|
+
this.log('fetched %s (%d bytes, mime=%s)', attachment.id, buffer.length, mime);
|
|
75
|
+
if (this.compression) {
|
|
76
|
+
const sharp = this._sharpModule || (this._sharpModule = await __importStar(require('sharp')));
|
|
77
|
+
this.log('compressing %s with sharp', attachment.id);
|
|
78
|
+
const sharpbuf = await sharp
|
|
79
|
+
.default(buffer)
|
|
80
|
+
.webp(Object.assign({ quality: this.compression.quality, force: this.compression.convertToWebP, effort: 2 }, this.compression.options))
|
|
81
|
+
.toBuffer({ resolveWithObject: true });
|
|
82
|
+
return `data:image/${sharpbuf.info.format};base64,${sharpbuf.data.toString('base64')}`;
|
|
83
|
+
}
|
|
84
|
+
return `data:${mime};base64,${buffer.toString('base64')}`;
|
|
85
|
+
}
|
|
111
86
|
}
|
|
112
87
|
exports.TranscriptImageDownloader = TranscriptImageDownloader;
|
|
113
88
|
TranscriptImageDownloader.log = (0, debug_1.default)('discord-html-transcripts:TranscriptImageDownloader');
|
|
114
|
-
//# sourceMappingURL=images.js.map
|
|
89
|
+
//# sourceMappingURL=images.js.map
|
package/dist/generator/index.js
CHANGED
|
@@ -1,66 +1,227 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
var __rest = (this && this.__rest) || function (s, e) {
|
|
3
6
|
var t = {};
|
|
4
|
-
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
5
|
-
t[p] = s[p];
|
|
7
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p];
|
|
6
8
|
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
7
9
|
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
8
|
-
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
9
|
-
t[p[i]] = s[p[i]];
|
|
10
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]];
|
|
10
11
|
}
|
|
11
12
|
return t;
|
|
12
13
|
};
|
|
13
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
-
};
|
|
16
14
|
var _a;
|
|
17
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
16
|
exports.default = render;
|
|
17
|
+
exports.renderToStream = renderToStream;
|
|
19
18
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
20
19
|
const static_1 = require("react-dom/static");
|
|
21
20
|
const buildProfiles_1 = require("../utils/buildProfiles");
|
|
22
21
|
const client_1 = require("../static/client");
|
|
23
22
|
const fs_1 = require("fs");
|
|
24
23
|
const path_1 = __importDefault(require("path"));
|
|
25
|
-
//const ssr_1 = require("@lit-labs/ssr");
|
|
26
24
|
const transcript_1 = __importDefault(require("./transcript"));
|
|
27
25
|
const utils_1 = require("../utils/utils");
|
|
28
|
-
//const render_result_1 = require("@lit-labs/ssr/lib/render-result");
|
|
29
26
|
const styles_1 = require("./renderers/components/styles");
|
|
30
27
|
const DiscordImage_1 = require("./renderers/components/DiscordImage");
|
|
31
28
|
const DiscordHighlightedCode_1 = require("./renderers/components/DiscordHighlightedCode");
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
|
|
30
|
+
// Lock to an exact resolved version — semver ranges in src= aren't cacheable by CDNs.
|
|
31
|
+
let discordComponentsVersion = '4.0.2';
|
|
34
32
|
try {
|
|
35
33
|
const packagePath = path_1.default.join(__dirname, '..', '..', 'package.json');
|
|
36
34
|
const packageJSON = JSON.parse((0, fs_1.readFileSync)(packagePath, 'utf8'));
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
const v = (_a = packageJSON.dependencies['@skyra/discord-components-core']) !== null && _a !== void 0 ? _a : discordComponentsVersion;
|
|
36
|
+
const cleaned = String(v).replace(/^[\^~]/, '');
|
|
37
|
+
if (/^\d+\.\d+\.\d+/.test(cleaned)) discordComponentsVersion = cleaned;
|
|
38
|
+
} catch (_b) { /* ignore */ }
|
|
39
|
+
|
|
40
|
+
function computeStats(messages) {
|
|
41
|
+
let images = 0;
|
|
42
|
+
let humans = new Set();
|
|
43
|
+
for (const m of messages) {
|
|
44
|
+
if (m.attachments && m.attachments.forEach) {
|
|
45
|
+
m.attachments.forEach((a) => {
|
|
46
|
+
if (typeof a.contentType === 'string' && a.contentType.startsWith('image/')) images++;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
if (m.author && !m.author.bot) humans.add(m.author.id);
|
|
50
|
+
}
|
|
51
|
+
return { messageCount: messages.length, participantCount: humans.size, imageCount: images };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function snowflakeToDate(id) {
|
|
55
|
+
try {
|
|
56
|
+
const DISCORD_EPOCH = 1420070400000n;
|
|
57
|
+
const ms = (BigInt(id) >> 22n) + DISCORD_EPOCH;
|
|
58
|
+
return new Date(Number(ms));
|
|
59
|
+
} catch (_e) { return null; }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildDateSeparators(messages, lang) {
|
|
63
|
+
// Returns an array same length as messages, each entry: separator string or null
|
|
64
|
+
const seps = new Array(messages.length).fill(null);
|
|
65
|
+
let lastYmd = null;
|
|
66
|
+
const fmt = new Intl.DateTimeFormat(lang === 'de' ? 'de-DE' : 'en-US', { weekday: 'long', day: '2-digit', month: 'long', year: 'numeric' });
|
|
67
|
+
for (let i = 0; i < messages.length; i++) {
|
|
68
|
+
const m = messages[i];
|
|
69
|
+
const d = m.createdAt instanceof Date ? m.createdAt : snowflakeToDate(m.id);
|
|
70
|
+
if (!d) continue;
|
|
71
|
+
const ymd = `${d.getUTCFullYear()}-${d.getUTCMonth()}-${d.getUTCDate()}`;
|
|
72
|
+
if (ymd !== lastYmd) {
|
|
73
|
+
seps[i] = fmt.format(d);
|
|
74
|
+
lastYmd = ymd;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return seps;
|
|
39
78
|
}
|
|
40
|
-
|
|
79
|
+
|
|
41
80
|
async function render(_a) {
|
|
42
81
|
var _b;
|
|
43
82
|
var { messages, channel, callbacks } = _a, options = __rest(_a, ["messages", "channel", "callbacks"]);
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
83
|
+
|
|
84
|
+
const lang = options.language && client_1.defaultDicts[options.language] ? options.language : 'en';
|
|
85
|
+
const i18n = options.i18n
|
|
86
|
+
? Object.fromEntries(Object.keys(client_1.defaultDicts).map((k) => [k, Object.assign({}, client_1.defaultDicts[k], options.i18n[k] || {})]))
|
|
87
|
+
: client_1.defaultDicts;
|
|
88
|
+
|
|
89
|
+
// Single-pass collector (profiles + extended in one walk)
|
|
90
|
+
const allCtx = await (0, buildProfiles_1.buildAllContext)(messages, channel).catch((e) => {
|
|
91
|
+
console.warn('[discord-html-transcripts-fix] buildAllContext failed:', e?.message || e);
|
|
92
|
+
return { profiles: {}, users: {}, roles: {}, channels: {} };
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const stats = computeStats(messages);
|
|
96
|
+
const dateSeps = buildDateSeparators(messages, lang);
|
|
97
|
+
|
|
98
|
+
const globalData = {
|
|
99
|
+
profiles: allCtx.profiles,
|
|
100
|
+
users: allCtx.users,
|
|
101
|
+
roles: allCtx.roles,
|
|
102
|
+
channels: allCtx.channels,
|
|
103
|
+
lang,
|
|
104
|
+
i18n,
|
|
105
|
+
stats,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const renderContext = Object.assign({ lang, i18n, dateSeps, stats }, callbacks ? { callbacks } : {});
|
|
109
|
+
|
|
110
|
+
const docTree = (0, jsx_runtime_1.jsxs)("html", { lang, children: [
|
|
111
|
+
(0, jsx_runtime_1.jsxs)("head", { children: [
|
|
112
|
+
(0, jsx_runtime_1.jsx)("meta", { charSet: "utf-8" }),
|
|
113
|
+
(0, jsx_runtime_1.jsx)("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
|
|
114
|
+
(0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.jsdelivr.net/" }),
|
|
115
|
+
(0, jsx_runtime_1.jsx)("link", { rel: "preconnect", href: "https://cdn.discordapp.com" }),
|
|
116
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.ggSansFont } }),
|
|
117
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: styles_1.globalStyles } }),
|
|
118
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.mentionPopupStyles } }),
|
|
119
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.transcriptUtils } }),
|
|
120
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.searchBarStyles } }),
|
|
121
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.lightboxStyles } }),
|
|
122
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.tocStyles } }),
|
|
123
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.i18nStyles } }),
|
|
124
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: client_1.dateSeparatorStyles } }),
|
|
125
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordImage_1.DiscordAttachmentStyles } }),
|
|
126
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: DiscordHighlightedCode_1.DiscordHighlightStyles } }),
|
|
127
|
+
(0, jsx_runtime_1.jsx)("style", { dangerouslySetInnerHTML: { __html: extraStyles } }),
|
|
128
|
+
(0, jsx_runtime_1.jsx)("link", { rel: "icon", type: "image/png", href: options.favicon === 'guild'
|
|
129
|
+
? channel.isDMBased()
|
|
130
|
+
? undefined
|
|
131
|
+
: ((_b = channel.guild.iconURL({ size: 16, extension: 'png' })) !== null && _b !== void 0 ? _b : undefined)
|
|
132
|
+
: options.favicon }),
|
|
133
|
+
(0, jsx_runtime_1.jsx)("title", { children: channel.isDMBased() ? 'Direct Messages' : channel.name }),
|
|
134
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.scrollToMessage } }),
|
|
135
|
+
// Non-blocking inline JSON: type="application/json" so the parser doesn't pause
|
|
136
|
+
(0, jsx_runtime_1.jsx)("script", { id: "dht-data", type: "application/json", dangerouslySetInnerHTML: { __html: (0, utils_1.safeJsonForScript)(globalData) } }),
|
|
137
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: 'try{globalThis.$discordMessage=JSON.parse(document.getElementById("dht-data").textContent);}catch(e){globalThis.$discordMessage={};}' } }),
|
|
138
|
+
!options.hydrate && (0, jsx_runtime_1.jsx)("script", { type: "module", src: `https://cdn.jsdelivr.net/npm/@skyra/discord-components-core@${discordComponentsVersion}/+esm`, crossOrigin: "anonymous" }),
|
|
139
|
+
] }),
|
|
140
|
+
(0, jsx_runtime_1.jsxs)("body", { style: { margin: 0, minHeight: '100vh' }, children: [
|
|
141
|
+
(0, jsx_runtime_1.jsx)(transcript_1.default, Object.assign({ messages, channel, callbacks }, options, renderContext)),
|
|
142
|
+
(0, jsx_runtime_1.jsx)(StatsFooter, { stats, lang, i18n }),
|
|
143
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.mentionPopup } }),
|
|
144
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.backToTop } }),
|
|
145
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.searchBar } }),
|
|
146
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.lightbox } }),
|
|
147
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.toc } }),
|
|
148
|
+
(0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.i18nSwitcher } }),
|
|
149
|
+
options.hydrate && (0, jsx_runtime_1.jsx)("script", { dangerouslySetInnerHTML: { __html: client_1.revealSpoiler } }),
|
|
150
|
+
] })
|
|
151
|
+
] });
|
|
152
|
+
|
|
153
|
+
const { prelude } = await (0, static_1.prerenderToNodeStream)(docTree);
|
|
154
|
+
|
|
155
|
+
if (options.hydrate) {
|
|
156
|
+
const markup = await (0, utils_1.streamToString)(prelude);
|
|
157
|
+
const { render: renderLit } = await import('@lit-labs/ssr');
|
|
158
|
+
const result = renderLit(markup);
|
|
159
|
+
const { collectResult } = await import('@lit-labs/ssr/lib/render-result.js');
|
|
160
|
+
return await collectResult(result);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (options.returnType === 'stream' || options.stream) {
|
|
164
|
+
// Return the underlying Node stream — caller is responsible for piping.
|
|
165
|
+
return prelude;
|
|
166
|
+
}
|
|
167
|
+
return await (0, utils_1.streamToString)(prelude);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function renderToStream(args) {
|
|
171
|
+
return render(Object.assign({}, args, { returnType: 'stream' }));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function StatsFooter({ stats, lang, i18n }) {
|
|
175
|
+
const dict = (i18n && i18n[lang]) || (i18n && i18n.en) || {};
|
|
176
|
+
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) }),
|
|
178
|
+
' · ',
|
|
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) }),
|
|
180
|
+
' · ',
|
|
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) }),
|
|
182
|
+
] });
|
|
65
183
|
}
|
|
66
|
-
|
|
184
|
+
|
|
185
|
+
const extraStyles = `
|
|
186
|
+
.dht-pinned-indicator{position:absolute;top:6px;right:8px;font-size:12px;opacity:.6}
|
|
187
|
+
.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
|
+
.dht-edit-marker{color:#949ba4;font-size:11px;margin-left:4px;cursor:help}
|
|
189
|
+
.dht-suppressed{color:#6e727a;font-size:11px;font-style:italic}
|
|
190
|
+
.dht-stickers{display:flex;gap:8px;flex-wrap:wrap;margin-top:4px}
|
|
191
|
+
.dht-sticker{width:160px;height:160px;border-radius:8px;object-fit:contain;background:rgba(255,255,255,.02)}
|
|
192
|
+
.dht-poll{border:1px solid #2b2d31;background:#1e1f22;border-radius:8px;padding:12px 14px;margin-top:6px;max-width:480px}
|
|
193
|
+
.dht-poll-q{font-weight:700;margin-bottom:8px;font-size:15px}
|
|
194
|
+
.dht-poll-answers{display:flex;flex-direction:column;gap:6px}
|
|
195
|
+
.dht-poll-a{background:#2b2d31;border-radius:6px;padding:6px 10px;position:relative;overflow:hidden}
|
|
196
|
+
.dht-poll-bar{position:absolute;top:0;left:0;height:100%;background:rgba(88,101,242,.25);z-index:0}
|
|
197
|
+
.dht-poll-line{position:relative;z-index:1;display:flex;justify-content:space-between;gap:8px}
|
|
198
|
+
.dht-poll-pct{color:#b5bac1;font-size:12px;white-space:nowrap}
|
|
199
|
+
.dht-poll-foot{margin-top:8px;font-size:11px;color:#949ba4}
|
|
200
|
+
.dht-voice{display:inline-flex;align-items:center;gap:8px;background:#2b2d31;padding:6px 12px;border-radius:8px;color:#dbdee1;font-size:13px;margin:4px 0}
|
|
201
|
+
.dht-voice-wave svg{display:block}
|
|
202
|
+
.dht-voice-dur{color:#b5bac1;font-size:11px}
|
|
203
|
+
.dht-forwarded-wrap{margin:4px 0}
|
|
204
|
+
.dht-forwarded{border-left:3px solid #5865F2;background:rgba(88,101,242,.06);padding:6px 10px;border-radius:0 6px 6px 0;margin-bottom:4px}
|
|
205
|
+
.dht-forwarded-head{font-size:12px;color:#949ba4;margin-bottom:2px}
|
|
206
|
+
.dht-forwarded-icon{margin-right:4px}
|
|
207
|
+
.dht-forwarded-body{color:#dbdee1}
|
|
208
|
+
.dht-forwarded-attachments{margin-top:4px;font-size:11px;color:#949ba4}
|
|
209
|
+
.dht-edit-history{font-size:12px;color:#b5bac1;margin-top:4px}
|
|
210
|
+
.dht-edit-history summary{cursor:pointer;color:#949ba4}
|
|
211
|
+
.dht-edit-history li{margin-top:4px}
|
|
212
|
+
.dht-edit-history time{display:block;color:#6e727a;font-size:10px;margin-bottom:2px}
|
|
213
|
+
.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}
|
|
215
|
+
.dht-thread-badge--archived{background:#6c757d}
|
|
216
|
+
.dht-gifv{max-width:400px;max-height:300px;border-radius:6px}
|
|
217
|
+
.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}
|
|
218
|
+
.dht-mention--cmd{background:rgba(88,101,242,.22)!important}
|
|
219
|
+
.dht-embed-provider{color:#949ba4;font-size:11px;margin-bottom:2px}
|
|
220
|
+
.dht-embed-video{margin-top:6px;font-size:13px}
|
|
221
|
+
.dht-embed-video a{color:#8ab4ff;text-decoration:none}
|
|
222
|
+
.dht-embed-video a:hover{text-decoration:underline}
|
|
223
|
+
.dht-automod-rule{font-size:11px;color:#949ba4}
|
|
224
|
+
.dht-automod-content{margin:4px 0 0 12px;color:#dbdee1;border-left:3px solid #f23f42;padding-left:8px;font-size:13px}
|
|
225
|
+
.dht-reaction--burst{box-shadow:inset 0 0 0 1px gold}
|
|
226
|
+
`;
|
|
227
|
+
//# sourceMappingURL=index.js.map
|