discord-html-transcripts-fix 2.2.0 → 2.3.1

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.
@@ -4,6 +4,7 @@ exports.buildProfiles = buildProfiles;
4
4
  exports.buildExtendedContext = buildExtendedContext;
5
5
  exports.buildAllContext = buildAllContext;
6
6
  const discord_js_1 = require("discord.js");
7
+ const utils_1 = require("./utils");
7
8
 
8
9
  /**
9
10
  * Backwards compatible: returns the profile dict used by <discord-mention> web components.
@@ -37,7 +38,8 @@ async function buildAllContext(messages, channel) {
37
38
  ? channel.guild
38
39
  : null;
39
40
 
40
- const seenUserIds = new Set();
41
+ // id -> 'member' | 'bare': whether the entry was built from the guild member.
42
+ const userState = new Map();
41
43
  const seenRoleIds = new Set();
42
44
  const seenChannelIds = new Set();
43
45
 
@@ -73,12 +75,48 @@ async function buildAllContext(messages, channel) {
73
75
  };
74
76
  };
75
77
 
78
+ // Plain-object input may carry dates as strings; discord.js gives Dates.
79
+ const isoDate = (value) => {
80
+ const date = value instanceof Date ? value
81
+ : (typeof value === 'string' || typeof value === 'number') ? new Date(value) : null;
82
+ return date && !Number.isNaN(date.getTime()) ? date.toISOString() : null;
83
+ };
84
+
76
85
  const includeUser = (member, author) => {
77
- if (!author) return;
78
- const id = author.id;
79
- if (seenUserIds.has(id)) return;
80
- seenUserIds.add(id);
86
+ const id = author?.id;
87
+ if (!id) return;
88
+ // The first sighting must not decide: a user seen first without their
89
+ // member (a mention, a command, a forward) would otherwise keep a profile
90
+ // without nickname, roles and color for the whole transcript.
91
+ const state = userState.get(id);
92
+ if (state === 'member' || (state === 'bare' && !member)) return;
93
+ userState.set(id, member ? 'member' : 'bare');
94
+ try {
95
+ buildUser(id, member, author);
96
+ return;
97
+ }
98
+ catch (err) {
99
+ // One malformed user must not cost every other profile in the transcript.
100
+ console.warn(`[discord-html-transcripts-fix] could not build the profile of user ${id}${member ? ' from their guild member' : ''}:`, err);
101
+ }
102
+ // A broken member — discord.js' role getters throw when the @everyone role
103
+ // is not cached, for one — still leaves the user's own name and avatar.
104
+ if (member && !profiles[id]) {
105
+ try {
106
+ buildUser(id, null, author);
107
+ }
108
+ catch (err) {
109
+ console.warn(`[discord-html-transcripts-fix] could not build the profile of user ${id}:`, err);
110
+ }
111
+ }
112
+ };
81
113
 
114
+ // discord.js gives a method; a user serialised with toJSON() carries the URL itself.
115
+ const avatarOf = (subject, size) => typeof subject?.displayAvatarURL === 'function'
116
+ ? subject.displayAvatarURL({ size, forceStatic: true })
117
+ : (typeof subject?.displayAvatarURL === 'string' ? subject.displayAvatarURL : undefined);
118
+
119
+ const buildUser = (id, member, author) => {
82
120
  // Compute member roles first so we can derive the name color from the
83
121
  // highest *listed* role (top of the role list), matching how Discord
84
122
  // colors usernames. Falls back to displayHexColor (highest *colored*
@@ -103,9 +141,9 @@ async function buildAllContext(messages, channel) {
103
141
  // whose hash still carries the `a_` prefix). The static variant (.webp
104
142
  // or .png) always works, so render avatars as static in transcripts.
105
143
  // Avatars in a historical archive don't gain much from being animated.
106
- profiles[id] = {
144
+ const profile = {
107
145
  author: member?.nickname ?? author.displayName ?? author.username,
108
- avatar: member?.displayAvatarURL?.({ size: 64, forceStatic: true }) ?? author.displayAvatarURL?.({ size: 64, forceStatic: true }),
146
+ avatar: avatarOf(member, 64) ?? avatarOf(author, 64),
109
147
  roleColor: effectiveColor,
110
148
  roleIcon: member?.roles?.icon?.iconURL?.() ?? undefined,
111
149
  roleName: member?.roles?.hoist?.name ?? undefined,
@@ -125,19 +163,19 @@ async function buildAllContext(messages, channel) {
125
163
  }
126
164
  }
127
165
 
128
- users[id] = {
166
+ const user = {
129
167
  id,
130
168
  username: author.username,
131
169
  displayName: member?.nickname || author.displayName || author.username,
132
170
  globalName: author.globalName || null,
133
- avatar: member?.displayAvatarURL?.({ size: 128, forceStatic: true }) || author.displayAvatarURL?.({ size: 128, forceStatic: true }) || null,
171
+ avatar: avatarOf(member, 128) || avatarOf(author, 128) || null,
134
172
  bannerColor: effectiveColor,
135
173
  bot: !!author.bot,
136
174
  system: !!author.system,
137
175
  verifiedBot: flagNames.includes('VerifiedBot'),
138
176
  flags: flagNames,
139
- joinedAt: member?.joinedAt ? member.joinedAt.toISOString() : null,
140
- createdAt: author.createdAt ? author.createdAt.toISOString() : null,
177
+ joinedAt: isoDate(member?.joinedAt),
178
+ createdAt: isoDate(author.createdAt),
141
179
  roles: memberRoles.map((r) => ({
142
180
  id: r.id,
143
181
  name: r.name,
@@ -148,9 +186,12 @@ async function buildAllContext(messages, channel) {
148
186
  ? { name: memberRoles[0].name, color: memberRoles[0].hexColor }
149
187
  : null,
150
188
  };
189
+ // Both at once, so a failure above never leaves half an entry behind.
190
+ profiles[id] = profile;
191
+ users[id] = user;
151
192
  };
152
193
 
153
- for (const message of messages) {
194
+ const collect = (message) => {
154
195
  if (message.author) includeUser(message.member, message.author);
155
196
  if (message.interaction?.user) {
156
197
  // FIX: try to find a member object for this user from later messages (interaction user may have authored their own messages elsewhere).
@@ -174,11 +215,28 @@ async function buildAllContext(messages, channel) {
174
215
  for (const [cid] of message.mentions.channels) includeChannelFromGuild(cid);
175
216
  }
176
217
  }
177
- // also include authors of forwarded snapshots
178
- if (Array.isArray(message.messageSnapshots)) {
179
- for (const snap of message.messageSnapshots) {
180
- if (snap?.author) includeUser(null, snap.author);
181
- }
218
+ // Authors of forwarded snapshots — from plain-object input only, exactly
219
+ // as the renderer names them. Discord hides who wrote a forward, and a
220
+ // discord.js snapshot of a message the bot has cached is that cached
221
+ // original, author included (see renderSnapshots).
222
+ // messageSnapshots is a discord.js Collection, not an array.
223
+ const snaps = message.messageSnapshots;
224
+ const snapList = !snaps ? [] : Array.isArray(snaps) ? snaps : typeof snaps.values === 'function' ? Array.from(snaps.values()) : [];
225
+ for (const snap of snapList) {
226
+ // Raw payloads nest the snapshot under `message`, like the renderer reads it.
227
+ const snapMessage = snap?.message || snap;
228
+ if (!snapMessage || (0, utils_1.isLibraryStructure)(snapMessage)) continue;
229
+ includeUser(null, snapMessage.author);
230
+ }
231
+ };
232
+
233
+ for (const message of messages) {
234
+ try {
235
+ collect(message);
236
+ }
237
+ catch (err) {
238
+ // A malformed message costs its own entries, not every profile.
239
+ console.warn('[discord-html-transcripts-fix] could not collect the profiles of message', message?.id, err);
182
240
  }
183
241
  }
184
242