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.
@@ -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,32 @@ 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
+ 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 && message.mentions.members.get
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
@@ -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:' || 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
+ '&': '&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.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",