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/dist/types.js
CHANGED
|
@@ -13,5 +13,6 @@ var ExportReturnType;
|
|
|
13
13
|
ExportReturnType["Buffer"] = "buffer";
|
|
14
14
|
ExportReturnType["String"] = "string";
|
|
15
15
|
ExportReturnType["Attachment"] = "attachment";
|
|
16
|
+
ExportReturnType["Stream"] = "stream";
|
|
16
17
|
})(ExportReturnType || (exports.ExportReturnType = ExportReturnType = {}));
|
|
17
18
|
//# sourceMappingURL=types.js.map
|
|
@@ -1,42 +1,154 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildProfiles = buildProfiles;
|
|
4
|
+
exports.buildExtendedContext = buildExtendedContext;
|
|
5
|
+
exports.buildAllContext = buildAllContext;
|
|
4
6
|
const discord_js_1 = require("discord.js");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Backwards compatible: returns the profile dict used by <discord-mention> web components.
|
|
10
|
+
*/
|
|
5
11
|
async function buildProfiles(messages) {
|
|
12
|
+
const { profiles } = await buildAllContext(messages, null);
|
|
13
|
+
return profiles;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Backwards compatible: returns extended users/roles/channels.
|
|
18
|
+
*/
|
|
19
|
+
async function buildExtendedContext(messages, channel) {
|
|
20
|
+
const { users, roles, channels } = await buildAllContext(messages, channel);
|
|
21
|
+
return { users, roles, channels };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Single-pass collector — builds the legacy `profiles` dict AND the extended
|
|
26
|
+
* users/roles/channels dicts in one go (saves an O(n) pass for big tickets).
|
|
27
|
+
*/
|
|
28
|
+
async function buildAllContext(messages, channel) {
|
|
6
29
|
const profiles = {};
|
|
7
|
-
|
|
30
|
+
const users = {};
|
|
31
|
+
const roles = {};
|
|
32
|
+
const channels = {};
|
|
33
|
+
|
|
34
|
+
// FIX: previous version was `!channel.isDMBased && !channel.isDMBased()`
|
|
35
|
+
// which is always false (isDMBased is a truthy function reference).
|
|
36
|
+
const guild = channel && typeof channel.isDMBased === 'function' && !channel.isDMBased()
|
|
37
|
+
? channel.guild
|
|
38
|
+
: null;
|
|
39
|
+
|
|
40
|
+
const seenUserIds = new Set();
|
|
41
|
+
const seenRoleIds = new Set();
|
|
42
|
+
const seenChannelIds = new Set();
|
|
43
|
+
|
|
44
|
+
const includeRoleFromGuild = (roleId) => {
|
|
45
|
+
if (!guild || !roleId || seenRoleIds.has(roleId)) return;
|
|
46
|
+
const role = guild.roles.cache.get(roleId);
|
|
47
|
+
if (!role) return;
|
|
48
|
+
seenRoleIds.add(roleId);
|
|
49
|
+
roles[roleId] = {
|
|
50
|
+
id: role.id,
|
|
51
|
+
name: role.name,
|
|
52
|
+
color: role.hexColor && role.hexColor !== '#000000' ? role.hexColor : null,
|
|
53
|
+
position: role.position,
|
|
54
|
+
hoist: role.hoist,
|
|
55
|
+
mentionable: role.mentionable,
|
|
56
|
+
memberCount: typeof role.members?.size === 'number' ? role.members.size : null,
|
|
57
|
+
managed: role.managed,
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const includeChannelFromGuild = (channelId) => {
|
|
62
|
+
if (!guild || !channelId || seenChannelIds.has(channelId)) return;
|
|
63
|
+
const ch = guild.channels.cache.get(channelId);
|
|
64
|
+
if (!ch) return;
|
|
65
|
+
seenChannelIds.add(channelId);
|
|
66
|
+
channels[channelId] = {
|
|
67
|
+
id: ch.id,
|
|
68
|
+
name: ch.name,
|
|
69
|
+
type: ch.type,
|
|
70
|
+
parent: ch.parent ? ch.parent.name : null,
|
|
71
|
+
topic: 'topic' in ch && typeof ch.topic === 'string' ? ch.topic : null,
|
|
72
|
+
nsfw: 'nsfw' in ch ? !!ch.nsfw : false,
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const includeUser = (member, author) => {
|
|
77
|
+
if (!author) return;
|
|
78
|
+
const id = author.id;
|
|
79
|
+
if (seenUserIds.has(id)) return;
|
|
80
|
+
seenUserIds.add(id);
|
|
81
|
+
|
|
82
|
+
// legacy profile (used by <discord-mention>)
|
|
83
|
+
profiles[id] = {
|
|
84
|
+
author: member?.nickname ?? author.displayName ?? author.username,
|
|
85
|
+
avatar: member?.displayAvatarURL?.({ size: 64 }) ?? author.displayAvatarURL?.({ size: 64 }),
|
|
86
|
+
roleColor: member?.displayHexColor,
|
|
87
|
+
roleIcon: member?.roles?.icon?.iconURL?.() ?? undefined,
|
|
88
|
+
roleName: member?.roles?.hoist?.name ?? undefined,
|
|
89
|
+
bot: !!author.bot,
|
|
90
|
+
verified: author.flags?.has?.(discord_js_1.UserFlags.VerifiedBot) ?? false,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// extended record (used by popup)
|
|
94
|
+
const memberRoles = member && member.roles && member.roles.cache
|
|
95
|
+
? Array.from(member.roles.cache.values())
|
|
96
|
+
.filter((r) => r.id !== guild?.id)
|
|
97
|
+
.sort((a, b) => b.position - a.position)
|
|
98
|
+
: [];
|
|
99
|
+
for (const r of memberRoles) includeRoleFromGuild(r.id);
|
|
100
|
+
|
|
101
|
+
users[id] = {
|
|
102
|
+
id,
|
|
103
|
+
username: author.username,
|
|
104
|
+
displayName: member?.nickname || author.displayName || author.username,
|
|
105
|
+
globalName: author.globalName || null,
|
|
106
|
+
avatar: member?.displayAvatarURL?.({ size: 128 }) || author.displayAvatarURL?.({ size: 128 }) || null,
|
|
107
|
+
bannerColor: member?.displayHexColor && member.displayHexColor !== '#000000' ? member.displayHexColor : null,
|
|
108
|
+
bot: !!author.bot,
|
|
109
|
+
system: !!author.system,
|
|
110
|
+
joinedAt: member?.joinedAt ? member.joinedAt.toISOString() : null,
|
|
111
|
+
createdAt: author.createdAt ? author.createdAt.toISOString() : null,
|
|
112
|
+
roles: memberRoles.map((r) => ({
|
|
113
|
+
id: r.id,
|
|
114
|
+
name: r.name,
|
|
115
|
+
color: r.hexColor && r.hexColor !== '#000000' ? r.hexColor : null,
|
|
116
|
+
position: r.position,
|
|
117
|
+
})),
|
|
118
|
+
highestRole: memberRoles[0]
|
|
119
|
+
? { name: memberRoles[0].name, color: memberRoles[0].hexColor }
|
|
120
|
+
: null,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
|
|
8
124
|
for (const message of messages) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
profiles[author.id] = buildProfile(message.member, author);
|
|
125
|
+
if (message.author) includeUser(message.member, message.author);
|
|
126
|
+
if (message.interaction?.user) includeUser(null, message.interaction.user);
|
|
127
|
+
if (message.thread?.lastMessage?.author) {
|
|
128
|
+
includeUser(message.thread.lastMessage.member, message.thread.lastMessage.author);
|
|
14
129
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
130
|
+
if (message.mentions) {
|
|
131
|
+
if (message.mentions.users) {
|
|
132
|
+
for (const [uid, u] of message.mentions.users) {
|
|
133
|
+
const member = message.mentions.members?.get?.(uid) || null;
|
|
134
|
+
includeUser(member, u);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (message.mentions.roles) {
|
|
138
|
+
for (const [rid] of message.mentions.roles) includeRoleFromGuild(rid);
|
|
139
|
+
}
|
|
140
|
+
if (message.mentions.channels) {
|
|
141
|
+
for (const [cid] of message.mentions.channels) includeChannelFromGuild(cid);
|
|
20
142
|
}
|
|
21
143
|
}
|
|
22
|
-
//
|
|
23
|
-
if (
|
|
24
|
-
|
|
144
|
+
// also include authors of forwarded snapshots
|
|
145
|
+
if (Array.isArray(message.messageSnapshots)) {
|
|
146
|
+
for (const snap of message.messageSnapshots) {
|
|
147
|
+
if (snap?.author) includeUser(null, snap.author);
|
|
148
|
+
}
|
|
25
149
|
}
|
|
26
150
|
}
|
|
27
|
-
|
|
28
|
-
return profiles;
|
|
29
|
-
}
|
|
30
|
-
function buildProfile(member, author) {
|
|
31
|
-
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
32
|
-
return {
|
|
33
|
-
author: (_b = (_a = member === null || member === void 0 ? void 0 : member.nickname) !== null && _a !== void 0 ? _a : author.displayName) !== null && _b !== void 0 ? _b : author.username,
|
|
34
|
-
avatar: (_c = member === null || member === void 0 ? void 0 : member.displayAvatarURL({ size: 64 })) !== null && _c !== void 0 ? _c : author.displayAvatarURL({ size: 64 }),
|
|
35
|
-
roleColor: member === null || member === void 0 ? void 0 : member.displayHexColor,
|
|
36
|
-
roleIcon: (_e = (_d = member === null || member === void 0 ? void 0 : member.roles.icon) === null || _d === void 0 ? void 0 : _d.iconURL()) !== null && _e !== void 0 ? _e : undefined,
|
|
37
|
-
roleName: (_g = (_f = member === null || member === void 0 ? void 0 : member.roles.hoist) === null || _f === void 0 ? void 0 : _f.name) !== null && _g !== void 0 ? _g : undefined,
|
|
38
|
-
bot: author.bot,
|
|
39
|
-
verified: (_h = author.flags) === null || _h === void 0 ? void 0 : _h.has(discord_js_1.UserFlags.VerifiedBot),
|
|
40
|
-
};
|
|
151
|
+
|
|
152
|
+
return { profiles, users, roles, channels };
|
|
41
153
|
}
|
|
42
|
-
//# sourceMappingURL=buildProfiles.js.map
|
|
154
|
+
//# sourceMappingURL=buildProfiles.js.map
|
package/dist/utils/utils.js
CHANGED
|
@@ -7,38 +7,49 @@ exports.isDefined = isDefined;
|
|
|
7
7
|
exports.formatBytes = formatBytes;
|
|
8
8
|
exports.parseDiscordEmoji = parseDiscordEmoji;
|
|
9
9
|
exports.streamToString = streamToString;
|
|
10
|
+
exports.safeJsonForScript = safeJsonForScript;
|
|
11
|
+
exports.safeHref = safeHref;
|
|
12
|
+
exports.safeColor = safeColor;
|
|
13
|
+
exports.safeImageMime = safeImageMime;
|
|
14
|
+
exports.escapeHtml = escapeHtml;
|
|
10
15
|
const twemoji_1 = __importDefault(require("twemoji"));
|
|
16
|
+
|
|
11
17
|
function isDefined(value) {
|
|
12
18
|
return value !== undefined && value !== null;
|
|
13
19
|
}
|
|
14
|
-
|
|
15
|
-
* Returns a tuple of [bytes (i.e. 1024), bytesUnit (i.e. KB)]
|
|
16
|
-
* @param bytes
|
|
17
|
-
* @param decimals
|
|
18
|
-
* @returns
|
|
19
|
-
*/
|
|
20
|
+
|
|
20
21
|
function formatBytes(bytes, decimals = 2) {
|
|
21
|
-
if (bytes
|
|
22
|
-
return [0, 'Bytes'];
|
|
22
|
+
if (!bytes || !Number.isFinite(bytes)) return [0, 'Bytes'];
|
|
23
23
|
const k = 1024;
|
|
24
24
|
const dm = decimals < 0 ? 0 : decimals;
|
|
25
25
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
26
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
26
|
+
const i = Math.min(sizes.length - 1, Math.max(0, Math.floor(Math.log(bytes) / Math.log(k))));
|
|
27
27
|
return [parseFloat((bytes / Math.pow(k, i)).toFixed(dm)), sizes[i]];
|
|
28
28
|
}
|
|
29
|
+
|
|
30
|
+
const emojiCache = new Map();
|
|
29
31
|
function parseDiscordEmoji(emoji) {
|
|
32
|
+
if (!emoji) return '';
|
|
33
|
+
const key = emoji.id || emoji.name;
|
|
34
|
+
if (!key) return '';
|
|
35
|
+
if (emojiCache.has(key)) return emojiCache.get(key);
|
|
36
|
+
let url = '';
|
|
30
37
|
if (emoji.id) {
|
|
31
|
-
|
|
38
|
+
url = `https://cdn.discordapp.com/emojis/${emoji.id}.${emoji.animated ? 'gif' : 'png'}`;
|
|
39
|
+
} else if (typeof emoji.name === 'string' && emoji.name) {
|
|
40
|
+
try {
|
|
41
|
+
const codepoints = twemoji_1.default.convert
|
|
42
|
+
.toCodePoint(emoji.name.indexOf(String.fromCharCode(0x200d)) < 0 ? emoji.name.replace(/️/g, '') : emoji.name)
|
|
43
|
+
.toLowerCase();
|
|
44
|
+
url = `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${codepoints}.svg`;
|
|
45
|
+
} catch (_e) {
|
|
46
|
+
url = '';
|
|
47
|
+
}
|
|
32
48
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.toLowerCase();
|
|
36
|
-
return `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${codepoints}.svg`;
|
|
49
|
+
emojiCache.set(key, url);
|
|
50
|
+
return url;
|
|
37
51
|
}
|
|
38
|
-
|
|
39
|
-
* Converts a stream to a string
|
|
40
|
-
* @param stream - The stream to convert
|
|
41
|
-
*/
|
|
52
|
+
|
|
42
53
|
function streamToString(stream) {
|
|
43
54
|
const chunks = [];
|
|
44
55
|
return new Promise((resolve, reject) => {
|
|
@@ -47,4 +58,48 @@ function streamToString(stream) {
|
|
|
47
58
|
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
48
59
|
});
|
|
49
60
|
}
|
|
50
|
-
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* JSON.stringify safe to embed in <script>. Escapes <, >, &, U+2028, U+2029.
|
|
64
|
+
*/
|
|
65
|
+
function safeJsonForScript(obj) {
|
|
66
|
+
return JSON.stringify(obj)
|
|
67
|
+
.replace(/</g, '\\u003c')
|
|
68
|
+
.replace(/>/g, '\\u003e')
|
|
69
|
+
.replace(/&/g, '\\u0026')
|
|
70
|
+
.replace(/\u2028/g, '\\u2028')
|
|
71
|
+
.replace(/\u2029/g, '\\u2029');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function safeHref(url) {
|
|
75
|
+
if (typeof url !== 'string' || !url) return '#';
|
|
76
|
+
try {
|
|
77
|
+
const u = new URL(url, 'https://example.invalid');
|
|
78
|
+
const proto = u.protocol.toLowerCase();
|
|
79
|
+
if (proto === 'http:' || proto === 'https:' || proto === 'mailto:' || proto === 'discord:') return url;
|
|
80
|
+
return '#';
|
|
81
|
+
} catch (_e) {
|
|
82
|
+
return '#';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeColor(c, fallback = '#5865F2') {
|
|
87
|
+
if (typeof c !== 'string') return fallback;
|
|
88
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(c) ? c : fallback;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const ALLOWED_IMAGE_MIME = new Set([
|
|
92
|
+
'image/png', 'image/jpeg', 'image/jpg', 'image/gif', 'image/webp', 'image/avif', 'image/svg+xml',
|
|
93
|
+
]);
|
|
94
|
+
function safeImageMime(mime, fallback = 'image/png') {
|
|
95
|
+
if (typeof mime !== 'string') return fallback;
|
|
96
|
+
const m = mime.split(';')[0].trim().toLowerCase();
|
|
97
|
+
return ALLOWED_IMAGE_MIME.has(m) ? m : fallback;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function escapeHtml(str) {
|
|
101
|
+
return String(str == null ? '' : str).replace(/[&<>"']/g, (c) => ({
|
|
102
|
+
'&': '&', '<': '<', '>': '>', '"': '"', "'": '''
|
|
103
|
+
}[c]));
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=utils.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discord-html-transcripts-fix",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"@types/react-dom": "^19.2.3",
|
|
20
20
|
"@typescript-eslint/eslint-plugin": "^8.50.0",
|
|
21
21
|
"@typescript-eslint/parser": "^8.50.0",
|
|
22
|
-
"debug": "^4.4.3",
|
|
23
22
|
"discord.js": "^14.25.1",
|
|
24
23
|
"dotenv": "^17.2.3",
|
|
25
24
|
"eslint": "^9.39.2",
|
|
@@ -34,16 +33,17 @@
|
|
|
34
33
|
"dependencies": {
|
|
35
34
|
"@lit-labs/ssr": "^3.3.1",
|
|
36
35
|
"@skyra/discord-components-core": "^4.0.2",
|
|
36
|
+
"debug": "^4.4.3",
|
|
37
37
|
"discord-markdown-parser": "~1.3.0",
|
|
38
38
|
"highlight.js": "^11.11.1",
|
|
39
|
+
"react": "^19.2.3",
|
|
40
|
+
"react-dom": "^19.2.3",
|
|
39
41
|
"simple-markdown": "^0.7.3",
|
|
40
42
|
"twemoji": "^14.0.2",
|
|
41
43
|
"undici": "^7.16.0"
|
|
42
44
|
},
|
|
43
45
|
"peerDependencies": {
|
|
44
|
-
"discord.js": "^14.0.0 || ^15.0.0"
|
|
45
|
-
"react": "^19.2.3",
|
|
46
|
-
"react-dom": "^19.2.3"
|
|
46
|
+
"discord.js": "^14.0.0 || ^15.0.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"test": "echo \"Error: no test specified\" && exit 1",
|