discord-html-transcripts-fix 1.1.0 → 1.3.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 +71 -85
- package/dist/generator/index.js +231 -43
- package/dist/generator/renderers/attachment.js +27 -36
- package/dist/generator/renderers/components/Container.js +42 -14
- package/dist/generator/renderers/components/Media Gallery.js +24 -18
- 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/components.js +7 -2
- package/dist/generator/renderers/content.js +254 -76
- package/dist/generator/renderers/embed.js +79 -34
- package/dist/generator/renderers/message.js +358 -52
- package/dist/generator/renderers/reply.js +59 -40
- package/dist/generator/renderers/systemMessage.js +182 -68
- package/dist/generator/transcript.js +42 -56
- package/dist/index.js +81 -84
- package/dist/static/client.js +675 -202
- package/dist/types.js +1 -0
- package/dist/utils/buildProfiles.js +70 -60
- package/dist/utils/utils.js +73 -26
- package/package.json +1 -1
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
|
|
@@ -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,44 @@ 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
|
+
// Collect user flag names (Staff, Partner, HypeSquad…)
|
|
102
|
+
const flagNames = [];
|
|
103
|
+
if (author.flags && typeof author.flags.has === 'function' && discord_js_1.UserFlags) {
|
|
104
|
+
for (const flagName of Object.keys(discord_js_1.UserFlags)) {
|
|
105
|
+
// Filter out numeric reverse-lookups
|
|
106
|
+
if (/^\d+$/.test(flagName)) continue;
|
|
107
|
+
try {
|
|
108
|
+
if (author.flags.has(discord_js_1.UserFlags[flagName])) flagNames.push(flagName);
|
|
109
|
+
} catch (_e) { /* ignore unknown flag */ }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
users[id] = {
|
|
114
|
+
id,
|
|
115
115
|
username: author.username,
|
|
116
116
|
displayName: member?.nickname || author.displayName || author.username,
|
|
117
117
|
globalName: author.globalName || null,
|
|
@@ -119,6 +119,8 @@ async function buildExtendedContext(messages, channel) {
|
|
|
119
119
|
bannerColor: member?.displayHexColor && member.displayHexColor !== '#000000' ? member.displayHexColor : null,
|
|
120
120
|
bot: !!author.bot,
|
|
121
121
|
system: !!author.system,
|
|
122
|
+
verifiedBot: flagNames.includes('VerifiedBot'),
|
|
123
|
+
flags: flagNames,
|
|
122
124
|
joinedAt: member?.joinedAt ? member.joinedAt.toISOString() : null,
|
|
123
125
|
createdAt: author.createdAt ? author.createdAt.toISOString() : null,
|
|
124
126
|
roles: memberRoles.map((r) => ({
|
|
@@ -135,16 +137,18 @@ async function buildExtendedContext(messages, channel) {
|
|
|
135
137
|
|
|
136
138
|
for (const message of messages) {
|
|
137
139
|
if (message.author) includeUser(message.member, message.author);
|
|
138
|
-
if (message.interaction?.user)
|
|
140
|
+
if (message.interaction?.user) {
|
|
141
|
+
// FIX: try to find a member object for this user from later messages (interaction user may have authored their own messages elsewhere).
|
|
142
|
+
const memberMatch = message.interaction.member || (message.mentions?.members?.get?.(message.interaction.user.id)) || null;
|
|
143
|
+
includeUser(memberMatch, message.interaction.user);
|
|
144
|
+
}
|
|
139
145
|
if (message.thread?.lastMessage?.author) {
|
|
140
146
|
includeUser(message.thread.lastMessage.member, message.thread.lastMessage.author);
|
|
141
147
|
}
|
|
142
148
|
if (message.mentions) {
|
|
143
149
|
if (message.mentions.users) {
|
|
144
150
|
for (const [uid, u] of message.mentions.users) {
|
|
145
|
-
const member = message.mentions.members
|
|
146
|
-
? message.mentions.members.get(uid)
|
|
147
|
-
: null;
|
|
151
|
+
const member = message.mentions.members?.get?.(uid) || null;
|
|
148
152
|
includeUser(member, u);
|
|
149
153
|
}
|
|
150
154
|
}
|
|
@@ -155,8 +159,14 @@ async function buildExtendedContext(messages, channel) {
|
|
|
155
159
|
for (const [cid] of message.mentions.channels) includeChannelFromGuild(cid);
|
|
156
160
|
}
|
|
157
161
|
}
|
|
162
|
+
// also include authors of forwarded snapshots
|
|
163
|
+
if (Array.isArray(message.messageSnapshots)) {
|
|
164
|
+
for (const snap of message.messageSnapshots) {
|
|
165
|
+
if (snap?.author) includeUser(null, snap.author);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
158
168
|
}
|
|
159
169
|
|
|
160
|
-
return { users, roles, channels };
|
|
170
|
+
return { profiles, users, roles, channels };
|
|
161
171
|
}
|
|
162
172
|
//# 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:') 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.3.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",
|