discord-html-transcripts-fix 1.1.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 +86 -78
- package/dist/downloader/images.js +60 -85
- package/dist/generator/index.js +194 -43
- 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 +48 -48
- package/dist/generator/renderers/embed.js +55 -34
- package/dist/generator/renderers/message.js +159 -46
- package/dist/generator/renderers/reply.js +47 -39
- package/dist/generator/renderers/systemMessage.js +131 -70
- package/dist/generator/transcript.js +42 -56
- package/dist/index.js +59 -84
- package/dist/static/client.js +216 -208
- package/dist/types.js +1 -0
- package/dist/utils/buildProfiles.js +51 -59
- package/dist/utils/utils.js +73 -26
- package/package.json +1 -1
|
@@ -2,75 +2,47 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildProfiles = buildProfiles;
|
|
4
4
|
exports.buildExtendedContext = buildExtendedContext;
|
|
5
|
+
exports.buildAllContext = buildAllContext;
|
|
5
6
|
const discord_js_1 = require("discord.js");
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
|
-
*
|
|
9
|
-
* <discord-mention> web component lookups via window.$discordMessage.profiles.
|
|
9
|
+
* Backwards compatible: returns the profile dict used by <discord-mention> web components.
|
|
10
10
|
*/
|
|
11
11
|
async function buildProfiles(messages) {
|
|
12
|
-
const profiles =
|
|
13
|
-
for (const message of messages) {
|
|
14
|
-
const author = message.author;
|
|
15
|
-
if (author && !profiles[author.id]) {
|
|
16
|
-
profiles[author.id] = buildProfile(message.member, author);
|
|
17
|
-
}
|
|
18
|
-
if (message.interaction) {
|
|
19
|
-
const user = message.interaction.user;
|
|
20
|
-
if (user && !profiles[user.id]) {
|
|
21
|
-
profiles[user.id] = buildProfile(null, user);
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
if (message.thread && message.thread.lastMessage && message.thread.lastMessage.author) {
|
|
25
|
-
profiles[message.thread.lastMessage.author.id] = buildProfile(message.thread.lastMessage.member, message.thread.lastMessage.author);
|
|
26
|
-
}
|
|
27
|
-
// Also include users mentioned in messages — so popups work even if they never spoke
|
|
28
|
-
if (message.mentions && message.mentions.users) {
|
|
29
|
-
for (const [uid, u] of message.mentions.users) {
|
|
30
|
-
if (!profiles[uid]) {
|
|
31
|
-
const member = message.mentions.members && message.mentions.members.get
|
|
32
|
-
? message.mentions.members.get(uid)
|
|
33
|
-
: null;
|
|
34
|
-
profiles[uid] = buildProfile(member, u);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
12
|
+
const { profiles } = await buildAllContext(messages, null);
|
|
39
13
|
return profiles;
|
|
40
14
|
}
|
|
41
15
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
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,
|
|
49
|
-
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,
|
|
50
|
-
bot: author.bot,
|
|
51
|
-
verified: (_h = author.flags) === null || _h === void 0 ? void 0 : _h.has(discord_js_1.UserFlags.VerifiedBot),
|
|
52
|
-
};
|
|
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 };
|
|
53
22
|
}
|
|
54
23
|
|
|
55
24
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* users: full user/member details (avatar, banner color, all roles, joinedAt, createdAt, flags)
|
|
59
|
-
* roles: all roles referenced by any rendered user OR mentioned in messages
|
|
60
|
-
* channels: channel info (name, parent category, topic, type)
|
|
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).
|
|
61
27
|
*/
|
|
62
|
-
async function
|
|
28
|
+
async function buildAllContext(messages, channel) {
|
|
29
|
+
const profiles = {};
|
|
63
30
|
const users = {};
|
|
64
31
|
const roles = {};
|
|
65
32
|
const channels = {};
|
|
66
|
-
|
|
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;
|
|
67
39
|
|
|
68
40
|
const seenUserIds = new Set();
|
|
69
41
|
const seenRoleIds = new Set();
|
|
70
42
|
const seenChannelIds = new Set();
|
|
71
43
|
|
|
72
44
|
const includeRoleFromGuild = (roleId) => {
|
|
73
|
-
if (!guild || seenRoleIds.has(roleId)) return;
|
|
45
|
+
if (!guild || !roleId || seenRoleIds.has(roleId)) return;
|
|
74
46
|
const role = guild.roles.cache.get(roleId);
|
|
75
47
|
if (!role) return;
|
|
76
48
|
seenRoleIds.add(roleId);
|
|
@@ -87,7 +59,7 @@ async function buildExtendedContext(messages, channel) {
|
|
|
87
59
|
};
|
|
88
60
|
|
|
89
61
|
const includeChannelFromGuild = (channelId) => {
|
|
90
|
-
if (!guild || seenChannelIds.has(channelId)) return;
|
|
62
|
+
if (!guild || !channelId || seenChannelIds.has(channelId)) return;
|
|
91
63
|
const ch = guild.channels.cache.get(channelId);
|
|
92
64
|
if (!ch) return;
|
|
93
65
|
seenChannelIds.add(channelId);
|
|
@@ -102,16 +74,32 @@ async function buildExtendedContext(messages, channel) {
|
|
|
102
74
|
};
|
|
103
75
|
|
|
104
76
|
const includeUser = (member, author) => {
|
|
105
|
-
if (!author
|
|
106
|
-
|
|
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)
|
|
107
94
|
const memberRoles = member && member.roles && member.roles.cache
|
|
108
|
-
? Array.from(member.roles.cache.values())
|
|
95
|
+
? Array.from(member.roles.cache.values())
|
|
96
|
+
.filter((r) => r.id !== guild?.id)
|
|
109
97
|
.sort((a, b) => b.position - a.position)
|
|
110
98
|
: [];
|
|
111
|
-
// include role records as well
|
|
112
99
|
for (const r of memberRoles) includeRoleFromGuild(r.id);
|
|
113
|
-
|
|
114
|
-
|
|
100
|
+
|
|
101
|
+
users[id] = {
|
|
102
|
+
id,
|
|
115
103
|
username: author.username,
|
|
116
104
|
displayName: member?.nickname || author.displayName || author.username,
|
|
117
105
|
globalName: author.globalName || null,
|
|
@@ -142,9 +130,7 @@ async function buildExtendedContext(messages, channel) {
|
|
|
142
130
|
if (message.mentions) {
|
|
143
131
|
if (message.mentions.users) {
|
|
144
132
|
for (const [uid, u] of message.mentions.users) {
|
|
145
|
-
const member = message.mentions.members
|
|
146
|
-
? message.mentions.members.get(uid)
|
|
147
|
-
: null;
|
|
133
|
+
const member = message.mentions.members?.get?.(uid) || null;
|
|
148
134
|
includeUser(member, u);
|
|
149
135
|
}
|
|
150
136
|
}
|
|
@@ -155,8 +141,14 @@ async function buildExtendedContext(messages, channel) {
|
|
|
155
141
|
for (const [cid] of message.mentions.channels) includeChannelFromGuild(cid);
|
|
156
142
|
}
|
|
157
143
|
}
|
|
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
|
+
}
|
|
149
|
+
}
|
|
158
150
|
}
|
|
159
151
|
|
|
160
|
-
return { users, roles, channels };
|
|
152
|
+
return { profiles, users, roles, channels };
|
|
161
153
|
}
|
|
162
154
|
//# sourceMappingURL=buildProfiles.js.map
|
package/dist/utils/utils.js
CHANGED
|
@@ -7,46 +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) {
|
|
30
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 = '';
|
|
31
37
|
if (emoji.id) {
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
catch (_e) {
|
|
43
|
-
return '';
|
|
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
|
+
}
|
|
44
48
|
}
|
|
49
|
+
emojiCache.set(key, url);
|
|
50
|
+
return url;
|
|
45
51
|
}
|
|
46
|
-
|
|
47
|
-
* Converts a stream to a string
|
|
48
|
-
* @param stream - The stream to convert
|
|
49
|
-
*/
|
|
52
|
+
|
|
50
53
|
function streamToString(stream) {
|
|
51
54
|
const chunks = [];
|
|
52
55
|
return new Promise((resolve, reject) => {
|
|
@@ -55,4 +58,48 @@ function streamToString(stream) {
|
|
|
55
58
|
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
|
56
59
|
});
|
|
57
60
|
}
|
|
58
|
-
|
|
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",
|