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/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
- // loop through messages
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
- // add all users
10
- const author = message.author;
11
- if (!profiles[author.id]) {
12
- // add profile
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
- // add interaction users
16
- if (message.interaction) {
17
- const user = message.interaction.user;
18
- if (!profiles[user.id]) {
19
- profiles[user.id] = buildProfile(null, user);
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
- // threads
23
- if (message.thread && message.thread.lastMessage) {
24
- profiles[message.thread.lastMessage.author.id] = buildProfile(message.thread.lastMessage.member, message.thread.lastMessage.author);
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
- // return as a JSON
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
@@ -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 === 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) {
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
- return `https://cdn.discordapp.com/emojis/${emoji.id}.${emoji.animated ? 'gif' : 'png'}`;
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
- const codepoints = twemoji_1.default.convert
34
- .toCodePoint(emoji.name.indexOf(String.fromCharCode(0x200d)) < 0 ? emoji.name.replace(/\uFE0F/g, '') : emoji.name)
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
- //# 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.0.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",
@@ -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",