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/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
- * Original profile builder — kept for backwards compatibility with
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
- function buildProfile(member, author) {
43
- var _a, _b, _c, _d, _e, _f, _g, _h;
44
- return {
45
- 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,
46
- avatar: (_c = member === null || member === void 0 ? void 0 : member.displayAvatarURL({ size: 64 })) !== null && _c !== void 0 ? _c : author.displayAvatarURL({ size: 64 }),
47
- roleColor: member === null || member === void 0 ? void 0 : member.displayHexColor,
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
- * Extended dictionaries used by the mention-popup client script.
57
- * Contains:
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 buildExtendedContext(messages, channel) {
28
+ async function buildAllContext(messages, channel) {
29
+ const profiles = {};
63
30
  const users = {};
64
31
  const roles = {};
65
32
  const channels = {};
66
- const guild = channel && !channel.isDMBased && !channel.isDMBased() ? channel.guild : null;
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 || seenUserIds.has(author.id)) return;
106
- seenUserIds.add(author.id);
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()).filter((r) => r.id !== guild?.id)
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
- users[author.id] = {
114
- id: author.id,
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) includeUser(null, 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 && message.mentions.members.get
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
@@ -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 === 0)
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
- return `https://cdn.discordapp.com/emojis/${emoji.id}.${emoji.animated ? 'gif' : 'png'}`;
33
- }
34
- // Defensive: emoji.name may be null for deleted/unknown reactions
35
- if (typeof emoji.name !== 'string' || !emoji.name) return '';
36
- try {
37
- const codepoints = twemoji_1.default.convert
38
- .toCodePoint(emoji.name.indexOf(String.fromCharCode(0x200d)) < 0 ? emoji.name.replace(/\uFE0F/g, '') : emoji.name)
39
- .toLowerCase();
40
- return `https://cdnjs.cloudflare.com/ajax/libs/twemoji/14.0.2/svg/${codepoints}.svg`;
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
- //# sourceMappingURL=utils.js.map
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
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
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.1.0",
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",