discord-message-transcript 1.2.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/core/cdnResolver.d.ts +2 -2
- package/dist/core/cdnResolver.js +28 -15
- package/dist/core/componentToJson.js +2 -1
- package/dist/core/fetchMessages.js +4 -3
- package/dist/core/getMentions.js +13 -11
- package/dist/core/imageToBase64.d.ts +1 -1
- package/dist/core/imageToBase64.js +5 -5
- package/dist/core/urlResolver.js +27 -13
- package/dist/index.js +19 -19
- package/dist/renderers/json/json.js +4 -2
- package/dist/types/types.d.ts +14 -0
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CDNOptions } from "../types/types.js";
|
|
2
2
|
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
3
|
export declare function cdnResolver(url: string, options: TranscriptOptionsBase, cdnOptions: CDNOptions): Promise<string>;
|
|
4
|
-
export declare function uploadCareResolver(url: string, publicKey: string, cdnDomain: string): Promise<string>;
|
|
5
|
-
export declare function cloudinaryResolver(url: string, fileName: string, cloudName: string, apiKey: string, apiSecret: string): Promise<string>;
|
|
4
|
+
export declare function uploadCareResolver(url: string, publicKey: string, cdnDomain: string, disableWarnings: boolean): Promise<string>;
|
|
5
|
+
export declare function cloudinaryResolver(url: string, fileName: string, cloudName: string, apiKey: string, apiSecret: string, disableWarnings: boolean): Promise<string>;
|
package/dist/core/cdnResolver.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function cdnResolver(url, options, cdnOptions) {
|
|
|
15
15
|
if (response.statusCode !== 200) {
|
|
16
16
|
response.destroy();
|
|
17
17
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
18
|
-
Failed to fetch attachment with status code: ${response.statusCode} from ${url}
|
|
18
|
+
Failed to fetch attachment with status code: ${response.statusCode} from ${url}.`, options.disableWarnings);
|
|
19
19
|
return resolve(url);
|
|
20
20
|
}
|
|
21
21
|
const contentType = response.headers["content-type"];
|
|
@@ -23,7 +23,7 @@ Failed to fetch attachment with status code: ${response.statusCode} from ${url}.
|
|
|
23
23
|
if (!contentType || splitContentType.length != 2 || splitContentType[0].length == 0 || splitContentType[1].length == 0) {
|
|
24
24
|
response.destroy();
|
|
25
25
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
26
|
-
Failed to receive a valid content-type from ${url}
|
|
26
|
+
Failed to receive a valid content-type from ${url}.`, options.disableWarnings);
|
|
27
27
|
return resolve(url);
|
|
28
28
|
}
|
|
29
29
|
response.destroy();
|
|
@@ -40,13 +40,13 @@ Failed to receive a valid content-type from ${url}.`);
|
|
|
40
40
|
});
|
|
41
41
|
request.on('error', (err) => {
|
|
42
42
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
43
|
-
Error: ${err.message}
|
|
43
|
+
Error: ${err.message}`, options.disableWarnings);
|
|
44
44
|
return resolve(url);
|
|
45
45
|
});
|
|
46
46
|
request.setTimeout(15000, () => {
|
|
47
47
|
request.destroy();
|
|
48
48
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
49
|
-
Request timeout for ${url}
|
|
49
|
+
Request timeout for ${url}.`, options.disableWarnings);
|
|
50
50
|
resolve(url);
|
|
51
51
|
});
|
|
52
52
|
request.end();
|
|
@@ -54,32 +54,38 @@ Request timeout for ${url}.`);
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
async function cdnRedirectType(url, options, contentType, cdnOptions) {
|
|
57
|
+
let newUrl;
|
|
57
58
|
switch (cdnOptions.provider) {
|
|
58
59
|
case "CUSTOM": {
|
|
59
60
|
try {
|
|
60
|
-
|
|
61
|
+
newUrl = await cdnOptions.resolver(url, contentType, cdnOptions.customData);
|
|
62
|
+
break;
|
|
61
63
|
}
|
|
62
64
|
catch (error) {
|
|
63
65
|
CustomWarn(`Custom CDN resolver threw an error. Falling back to original URL.
|
|
64
66
|
This is most likely an issue in the custom CDN implementation provided by the user.
|
|
65
67
|
URL: ${url}
|
|
66
|
-
Error: ${error?.message ?? error}
|
|
68
|
+
Error: ${error?.message ?? error}`, options.disableWarnings);
|
|
67
69
|
return url;
|
|
68
70
|
}
|
|
69
71
|
}
|
|
70
72
|
case "CLOUDINARY": {
|
|
71
|
-
|
|
72
|
-
;
|
|
73
|
+
newUrl = await cloudinaryResolver(url, options.fileName, cdnOptions.cloudName, cdnOptions.apiKey, cdnOptions.apiSecret, options.disableWarnings);
|
|
74
|
+
break;
|
|
73
75
|
}
|
|
74
76
|
case "UPLOADCARE": {
|
|
75
|
-
|
|
77
|
+
newUrl = await uploadCareResolver(url, cdnOptions.publicKey, cdnOptions.cdnDomain, options.disableWarnings);
|
|
78
|
+
break;
|
|
76
79
|
}
|
|
77
80
|
}
|
|
81
|
+
if (validateCdnUrl(newUrl, options.disableWarnings))
|
|
82
|
+
return newUrl;
|
|
83
|
+
return url;
|
|
78
84
|
}
|
|
79
85
|
function sleep(ms) {
|
|
80
86
|
return new Promise(r => setTimeout(r, ms));
|
|
81
87
|
}
|
|
82
|
-
export async function uploadCareResolver(url, publicKey, cdnDomain) {
|
|
88
|
+
export async function uploadCareResolver(url, publicKey, cdnDomain, disableWarnings) {
|
|
83
89
|
try {
|
|
84
90
|
const form = new FormData();
|
|
85
91
|
form.append("pub_key", publicKey);
|
|
@@ -135,11 +141,11 @@ export async function uploadCareResolver(url, publicKey, cdnDomain) {
|
|
|
135
141
|
CustomWarn(`Uploadcare CDN upload failed. Using original URL as fallback.
|
|
136
142
|
Check Uploadcare public key, CDN domain, project settings, rate limits, and network access.
|
|
137
143
|
URL: ${url}
|
|
138
|
-
Error: ${error?.message ?? error}
|
|
144
|
+
Error: ${error?.message ?? error}`, disableWarnings);
|
|
139
145
|
return url;
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
|
-
export async function cloudinaryResolver(url, fileName, cloudName, apiKey, apiSecret) {
|
|
148
|
+
export async function cloudinaryResolver(url, fileName, cloudName, apiKey, apiSecret, disableWarnings) {
|
|
143
149
|
try {
|
|
144
150
|
const paramsToSign = {
|
|
145
151
|
folder: `discord-message-transcript/${fileName}`,
|
|
@@ -148,9 +154,9 @@ export async function cloudinaryResolver(url, fileName, cloudName, apiKey, apiSe
|
|
|
148
154
|
use_filename: "true",
|
|
149
155
|
};
|
|
150
156
|
const stringToSign = Object.keys(paramsToSign).sort().map(k => `${k}=${paramsToSign[k]}`).join("&");
|
|
151
|
-
// signature
|
|
157
|
+
// signature SHA256
|
|
152
158
|
const signature = crypto
|
|
153
|
-
.createHash("
|
|
159
|
+
.createHash("sha256")
|
|
154
160
|
.update(stringToSign + apiSecret)
|
|
155
161
|
.digest("hex");
|
|
156
162
|
const form = new FormData();
|
|
@@ -190,8 +196,15 @@ export async function cloudinaryResolver(url, fileName, cloudName, apiKey, apiSe
|
|
|
190
196
|
CustomWarn(`Failed to upload asset to Cloudinary CDN. Using original URL as fallback.
|
|
191
197
|
Check Cloudinary configuration (cloud name, API key, API secret) and network access.
|
|
192
198
|
URL: ${url}
|
|
193
|
-
Error: ${error?.message ?? error}
|
|
199
|
+
Error: ${error?.message ?? error}`, disableWarnings);
|
|
194
200
|
return url;
|
|
195
201
|
}
|
|
196
202
|
}
|
|
197
203
|
// Note: for debug use ${JSON.stringify(await res.json())} to understand the error
|
|
204
|
+
function validateCdnUrl(url, disableWarnings) {
|
|
205
|
+
if (url.includes('"') || url.includes('<') || url.includes('>')) {
|
|
206
|
+
CustomWarn(`Unsafe URL received from CDN, using fallback.\nURL: ${url}`, disableWarnings);
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ComponentType } from "discord.js";
|
|
2
2
|
import { mapButtonStyle, mapSelectorType, mapSeparatorSpacing } from "./mappers.js";
|
|
3
3
|
import { JsonComponentType } from "discord-message-transcript-base";
|
|
4
|
+
import { isValidHexColor } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
|
|
4
5
|
export async function componentsToJson(components, options) {
|
|
5
6
|
const processedComponents = await Promise.all(components.filter(component => !(!options.includeV2Components && component.type != ComponentType.ActionRow))
|
|
6
7
|
.map(async (component) => {
|
|
@@ -57,7 +58,7 @@ export async function componentsToJson(components, options) {
|
|
|
57
58
|
return {
|
|
58
59
|
type: JsonComponentType.Container,
|
|
59
60
|
components: componentsJson.filter(isJsonComponentInContainer), // Input components that are container-safe must always produce container-safe output.
|
|
60
|
-
hexAccentColor: component.hexAccentColor,
|
|
61
|
+
hexAccentColor: isValidHexColor(component.hexAccentColor, false),
|
|
61
62
|
spoiler: component.spoiler,
|
|
62
63
|
};
|
|
63
64
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EmbedType } from "discord.js";
|
|
2
2
|
import { componentsToJson } from "./componentToJson.js";
|
|
3
3
|
import { getMentions } from "./getMentions.js";
|
|
4
|
+
import { isValidHexColor, sanitize } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
|
|
4
5
|
export async function fetchMessages(ctx) {
|
|
5
6
|
const { channel, options, transcriptState, lastMessageId } = ctx;
|
|
6
7
|
const { authors, mentions } = transcriptState;
|
|
@@ -24,7 +25,7 @@ export async function fetchMessages(ctx) {
|
|
|
24
25
|
description: embed.description ?? null,
|
|
25
26
|
fields: embed.fields.map(field => ({ inline: field.inline ?? false, name: field.name, value: field.value })),
|
|
26
27
|
footer: embed.footer ? { iconURL: embed.footer?.iconURL ?? null, text: embed.footer.text } : null,
|
|
27
|
-
hexColor: embed.hexColor
|
|
28
|
+
hexColor: isValidHexColor(embed.hexColor, true),
|
|
28
29
|
image: embed.image?.url ? { url: embed.image.url } : null,
|
|
29
30
|
thumbnail: embed.thumbnail?.url ? { url: embed.thumbnail.url } : null,
|
|
30
31
|
timestamp: embed.timestamp,
|
|
@@ -37,11 +38,11 @@ export async function fetchMessages(ctx) {
|
|
|
37
38
|
authors.set(message.author.id, {
|
|
38
39
|
avatarURL: message.author.displayAvatarURL(),
|
|
39
40
|
bot: message.author.bot,
|
|
40
|
-
displayName: message.author.displayName,
|
|
41
|
+
displayName: sanitize(message.author.displayName),
|
|
41
42
|
guildTag: message.author.primaryGuild?.tag ?? null,
|
|
42
43
|
id: message.author.id,
|
|
43
44
|
member: message.member ? {
|
|
44
|
-
displayHexColor: message.member.displayHexColor,
|
|
45
|
+
displayHexColor: isValidHexColor(message.member.displayHexColor, false),
|
|
45
46
|
displayName: message.member.displayName,
|
|
46
47
|
} : null,
|
|
47
48
|
system: message.author.system,
|
package/dist/core/getMentions.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { ChannelType } from "discord.js";
|
|
2
|
+
import { isValidHexColor, sanitize } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
|
|
2
3
|
export async function getMentions(message, mentions) {
|
|
3
4
|
message.mentions.channels.forEach(channel => {
|
|
4
5
|
if (!mentions.channels.has(channel.id)) {
|
|
5
6
|
mentions.channels.set(channel.id, {
|
|
6
7
|
id: channel.id,
|
|
7
|
-
name: channel.type !== ChannelType.DM ? channel.name
|
|
8
|
+
name: channel.type !== ChannelType.DM ? channel.name ? sanitize(channel.name) : null
|
|
9
|
+
: channel.recipient?.displayName ? sanitize(channel.recipient?.displayName) : null
|
|
8
10
|
});
|
|
9
11
|
}
|
|
10
12
|
});
|
|
@@ -12,8 +14,8 @@ export async function getMentions(message, mentions) {
|
|
|
12
14
|
if (!mentions.roles.has(role.id)) {
|
|
13
15
|
mentions.roles.set(role.id, {
|
|
14
16
|
id: role.id,
|
|
15
|
-
color: role.hexColor,
|
|
16
|
-
name: role.name
|
|
17
|
+
color: isValidHexColor(role.hexColor, false),
|
|
18
|
+
name: sanitize(role.name)
|
|
17
19
|
});
|
|
18
20
|
}
|
|
19
21
|
});
|
|
@@ -22,8 +24,8 @@ export async function getMentions(message, mentions) {
|
|
|
22
24
|
if (!mentions.users.has(member.id)) {
|
|
23
25
|
mentions.users.set(member.id, {
|
|
24
26
|
id: member.id,
|
|
25
|
-
color: member.displayHexColor,
|
|
26
|
-
name: member.displayName
|
|
27
|
+
color: isValidHexColor(member.displayHexColor, true),
|
|
28
|
+
name: sanitize(member.displayName)
|
|
27
29
|
});
|
|
28
30
|
}
|
|
29
31
|
});
|
|
@@ -33,8 +35,8 @@ export async function getMentions(message, mentions) {
|
|
|
33
35
|
if (!mentions.users.has(user.id)) {
|
|
34
36
|
mentions.users.set(user.id, {
|
|
35
37
|
id: user.id,
|
|
36
|
-
color: user.hexAccentColor ?? null,
|
|
37
|
-
name: user.displayName
|
|
38
|
+
color: isValidHexColor(user.hexAccentColor ?? null, true),
|
|
39
|
+
name: sanitize(user.displayName)
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
});
|
|
@@ -55,7 +57,7 @@ async function fetchRoleMention(message, mentions) {
|
|
|
55
57
|
const role = await message.guild?.roles.fetch(id);
|
|
56
58
|
if (!role)
|
|
57
59
|
continue;
|
|
58
|
-
mentions.roles.set(role.id, { id: role.id, color: role.hexColor, name: role.name });
|
|
60
|
+
mentions.roles.set(role.id, { id: role.id, color: isValidHexColor(role.hexColor, false), name: sanitize(role.name) });
|
|
59
61
|
}
|
|
60
62
|
catch { } // Role may not exist
|
|
61
63
|
}
|
|
@@ -73,7 +75,7 @@ async function fetchUserMention(message, mentions) {
|
|
|
73
75
|
try {
|
|
74
76
|
const user = await message.guild.members.fetch(id);
|
|
75
77
|
if (user) {
|
|
76
|
-
mentions.users.set(user.id, { id: user.id, color: user.displayHexColor, name: user.displayName });
|
|
78
|
+
mentions.users.set(user.id, { id: user.id, color: isValidHexColor(user.displayHexColor, true), name: sanitize(user.displayName) });
|
|
77
79
|
continue; // Continue inside if to allow fallback to regular user fetch
|
|
78
80
|
}
|
|
79
81
|
}
|
|
@@ -83,7 +85,7 @@ async function fetchUserMention(message, mentions) {
|
|
|
83
85
|
const user = await message.client.users.fetch(id);
|
|
84
86
|
if (!user)
|
|
85
87
|
continue;
|
|
86
|
-
mentions.users.set(user.id, { id: user.id, color: message.guild ? null : user.hexAccentColor ?? null, name: user.displayName });
|
|
88
|
+
mentions.users.set(user.id, { id: user.id, color: message.guild ? null : isValidHexColor(user.hexAccentColor ?? null, true) ?? null, name: sanitize(user.displayName) });
|
|
87
89
|
}
|
|
88
90
|
catch { } // User may not exist
|
|
89
91
|
}
|
|
@@ -101,7 +103,7 @@ async function fetchChannelMention(message, mentions) {
|
|
|
101
103
|
const channel = await message.guild?.channels.fetch(id);
|
|
102
104
|
if (!channel)
|
|
103
105
|
continue;
|
|
104
|
-
mentions.channels.set(channel.id, { id: channel.id, name: channel.name });
|
|
106
|
+
mentions.channels.set(channel.id, { id: channel.id, name: sanitize(channel.name) });
|
|
105
107
|
}
|
|
106
108
|
catch { } // Channel may not exist
|
|
107
109
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare function imageToBase64(url: string): Promise<string>;
|
|
1
|
+
export declare function imageToBase64(url: string, disableWarnings: boolean): Promise<string>;
|
|
@@ -2,7 +2,7 @@ import https from 'https';
|
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import { CustomWarn } from 'discord-message-transcript-base';
|
|
4
4
|
import { getBase64Limiter } from './limiter.js';
|
|
5
|
-
export async function imageToBase64(url) {
|
|
5
|
+
export async function imageToBase64(url, disableWarnings) {
|
|
6
6
|
const limit = getBase64Limiter();
|
|
7
7
|
return limit(async () => {
|
|
8
8
|
return new Promise((resolve, reject) => {
|
|
@@ -11,7 +11,7 @@ export async function imageToBase64(url) {
|
|
|
11
11
|
if (response.statusCode !== 200) {
|
|
12
12
|
response.destroy();
|
|
13
13
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
14
|
-
Failed to fetch image with status code: ${response.statusCode} from ${url}
|
|
14
|
+
Failed to fetch image with status code: ${response.statusCode} from ${url}.`, disableWarnings);
|
|
15
15
|
return resolve(url);
|
|
16
16
|
}
|
|
17
17
|
const contentType = response.headers['content-type'];
|
|
@@ -31,20 +31,20 @@ Failed to fetch image with status code: ${response.statusCode} from ${url}.`);
|
|
|
31
31
|
response.on('error', (err) => {
|
|
32
32
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
33
33
|
Stream error while fetching from ${url}.
|
|
34
|
-
Error: ${err.message}
|
|
34
|
+
Error: ${err.message}`, disableWarnings);
|
|
35
35
|
resolve(url);
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
38
|
request.on('error', (err) => {
|
|
39
39
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
40
40
|
Error fetching image from ${url}
|
|
41
|
-
Error: ${err.message}
|
|
41
|
+
Error: ${err.message}`, disableWarnings);
|
|
42
42
|
return resolve(url);
|
|
43
43
|
});
|
|
44
44
|
request.setTimeout(15000, () => {
|
|
45
45
|
request.destroy();
|
|
46
46
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
47
|
-
Request timeout for ${url}
|
|
47
|
+
Request timeout for ${url}.`, disableWarnings);
|
|
48
48
|
resolve(url);
|
|
49
49
|
});
|
|
50
50
|
request.end();
|
package/dist/core/urlResolver.js
CHANGED
|
@@ -2,7 +2,10 @@ import { JsonComponentType } from "discord-message-transcript-base";
|
|
|
2
2
|
import { cdnResolver } from "./cdnResolver.js";
|
|
3
3
|
import { imageToBase64 } from "./imageToBase64.js";
|
|
4
4
|
import { isJsonComponentInContainer } from "./componentToJson.js";
|
|
5
|
+
import { FALLBACK_PIXEL, isSafeForHTML, resolveImageURL } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
|
|
5
6
|
export async function urlResolver(url, options, cdnOptions, urlCache) {
|
|
7
|
+
if (url == FALLBACK_PIXEL || url == "")
|
|
8
|
+
return url;
|
|
6
9
|
if (urlCache.has(url)) {
|
|
7
10
|
const cache = urlCache.get(url);
|
|
8
11
|
if (cache)
|
|
@@ -12,7 +15,7 @@ export async function urlResolver(url, options, cdnOptions, urlCache) {
|
|
|
12
15
|
if (cdnOptions)
|
|
13
16
|
returnUrl = cdnResolver(url, options, cdnOptions);
|
|
14
17
|
else if (options.saveImages)
|
|
15
|
-
returnUrl = imageToBase64(url);
|
|
18
|
+
returnUrl = imageToBase64(url, options.disableWarnings);
|
|
16
19
|
if (returnUrl) {
|
|
17
20
|
urlCache.set(url, returnUrl);
|
|
18
21
|
return await returnUrl;
|
|
@@ -21,19 +24,31 @@ export async function urlResolver(url, options, cdnOptions, urlCache) {
|
|
|
21
24
|
}
|
|
22
25
|
export async function messagesUrlResolver(messages, options, cdnOptions, urlCache) {
|
|
23
26
|
return await Promise.all(messages.map(async (message) => {
|
|
24
|
-
|
|
27
|
+
// Needs to wait for resolve correct when used attachment://
|
|
28
|
+
const attachments = await Promise.all(message.attachments.map(async (attachment) => {
|
|
29
|
+
let url;
|
|
30
|
+
if (attachment.contentType?.startsWith("image/")) {
|
|
31
|
+
url = await resolveImageURL(attachment.url, options, false, message.attachments);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
url = await isSafeForHTML(attachment.url, options) ? attachment.url : "";
|
|
35
|
+
}
|
|
25
36
|
return {
|
|
26
37
|
...attachment,
|
|
27
|
-
url: await urlResolver(
|
|
38
|
+
url: await urlResolver(url, options, cdnOptions, urlCache)
|
|
28
39
|
};
|
|
29
40
|
}));
|
|
30
41
|
const embedsPromise = Promise.all(message.embeds.map(async (embed) => {
|
|
42
|
+
const authorIconUrl = embed.author?.iconURL ? await resolveImageURL(embed.author.iconURL, options, true, attachments) : null;
|
|
43
|
+
const footerIconUrl = embed.footer?.iconURL ? await resolveImageURL(embed.footer.iconURL, options, true, attachments) : null;
|
|
44
|
+
const imageUrl = embed.image?.url ? await resolveImageURL(embed.image.url, options, true, attachments) : null;
|
|
45
|
+
const thumbnailUrl = embed.thumbnail?.url ? await resolveImageURL(embed.thumbnail.url, options, true, attachments) : null;
|
|
31
46
|
return {
|
|
32
47
|
...embed,
|
|
33
|
-
author: embed.author ? { ...embed.author, iconURL:
|
|
34
|
-
footer: embed.footer ? { ...embed.footer, iconURL:
|
|
35
|
-
image: embed.image?.url ? { url: await urlResolver(
|
|
36
|
-
thumbnail: embed.thumbnail?.url ? { url: await urlResolver(
|
|
48
|
+
author: embed.author ? { ...embed.author, iconURL: authorIconUrl ? await urlResolver(authorIconUrl, options, cdnOptions, urlCache) : null } : null,
|
|
49
|
+
footer: embed.footer ? { ...embed.footer, iconURL: footerIconUrl ? await urlResolver(footerIconUrl, options, cdnOptions, urlCache) : null } : null,
|
|
50
|
+
image: embed.image?.url && imageUrl ? { url: await urlResolver(imageUrl, options, cdnOptions, urlCache) } : null,
|
|
51
|
+
thumbnail: embed.thumbnail?.url && thumbnailUrl ? { url: await urlResolver(thumbnailUrl, options, cdnOptions, urlCache) } : null,
|
|
37
52
|
};
|
|
38
53
|
}));
|
|
39
54
|
async function componentsFunction(components) {
|
|
@@ -45,7 +60,7 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
45
60
|
accessory: {
|
|
46
61
|
...component.accessory,
|
|
47
62
|
media: {
|
|
48
|
-
url: await urlResolver(component.accessory.media.url, options, cdnOptions, urlCache),
|
|
63
|
+
url: await urlResolver((await resolveImageURL(component.accessory.media.url, options, false, attachments)), options, cdnOptions, urlCache),
|
|
49
64
|
}
|
|
50
65
|
}
|
|
51
66
|
};
|
|
@@ -57,7 +72,7 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
57
72
|
items: await Promise.all(component.items.map(async (item) => {
|
|
58
73
|
return {
|
|
59
74
|
...item,
|
|
60
|
-
media: { url: await urlResolver(item.media.url, options, cdnOptions, urlCache) },
|
|
75
|
+
media: { url: await urlResolver((await resolveImageURL(item.media.url, options, false, attachments)), options, cdnOptions, urlCache) },
|
|
61
76
|
};
|
|
62
77
|
}))
|
|
63
78
|
};
|
|
@@ -65,7 +80,7 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
65
80
|
if (component.type == JsonComponentType.File) {
|
|
66
81
|
return {
|
|
67
82
|
...component,
|
|
68
|
-
url: await urlResolver(component.url, options, cdnOptions, urlCache),
|
|
83
|
+
url: await urlResolver((await isSafeForHTML(component.url, options) ? component.url : ""), options, cdnOptions, urlCache),
|
|
69
84
|
};
|
|
70
85
|
}
|
|
71
86
|
if (component.type == JsonComponentType.Container) {
|
|
@@ -78,8 +93,7 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
78
93
|
}));
|
|
79
94
|
}
|
|
80
95
|
const componentsPromise = componentsFunction(message.components);
|
|
81
|
-
const [
|
|
82
|
-
attachmentsPromise,
|
|
96
|
+
const [embeds, components] = await Promise.all([
|
|
83
97
|
embedsPromise,
|
|
84
98
|
componentsPromise
|
|
85
99
|
]);
|
|
@@ -95,7 +109,7 @@ export async function authorUrlResolver(authors, options, cdnOptions, urlCache)
|
|
|
95
109
|
return await Promise.all(Array.from(authors.values()).map(async (author) => {
|
|
96
110
|
return {
|
|
97
111
|
...author,
|
|
98
|
-
avatarURL: await urlResolver(author.avatarURL, options, cdnOptions, urlCache),
|
|
112
|
+
avatarURL: await urlResolver((await resolveImageURL(author.avatarURL, options, false)), options, cdnOptions, urlCache),
|
|
99
113
|
};
|
|
100
114
|
}));
|
|
101
115
|
}
|
package/dist/index.js
CHANGED
|
@@ -27,15 +27,16 @@ export async function createTranscript(channel, options = {}) {
|
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
const artificialReturnType = options.returnType == ReturnType.Attachment ? ReturnTypeBase.Buffer : options.returnType ? returnTypeMapper(options.returnType) : ReturnTypeBase.Buffer;
|
|
30
|
-
const { fileName = null, includeAttachments = true, includeButtons = true, includeComponents = true, includeEmpty = false, includeEmbeds = true, includePolls = true, includeReactions = true, includeV2Components = true, localDate = 'en-GB', quantity = 0, returnFormat = ReturnFormat.HTML, saveImages = false, selfContained = false, timeZone = 'UTC', watermark = true, } = options;
|
|
30
|
+
const { fileName = null, disableWarnings = false, includeAttachments = true, includeButtons = true, includeComponents = true, includeEmpty = false, includeEmbeds = true, includePolls = true, includeReactions = true, includeV2Components = true, localDate = 'en-GB', quantity = 0, returnFormat = ReturnFormat.HTML, safeMode = true, saveImages = false, selfContained = false, timeZone = 'UTC', watermark = true, } = options;
|
|
31
31
|
const checkedFileName = (fileName ?? `Transcript-${channel.isDMBased() ? "DirectMessage" : channel.name}-${channel.id}`);
|
|
32
32
|
let validQuantity = true;
|
|
33
33
|
if (quantity < 0) {
|
|
34
|
-
CustomWarn("Quantity can't be a negative number, please use 0 for unlimited messages.\nUsing 0 as fallback!");
|
|
34
|
+
CustomWarn("Quantity can't be a negative number, please use 0 for unlimited messages.\nUsing 0 as fallback!", options.disableWarnings ?? false);
|
|
35
35
|
validQuantity = false;
|
|
36
36
|
}
|
|
37
37
|
const internalOptions = {
|
|
38
38
|
fileName: checkedFileName,
|
|
39
|
+
disableWarnings,
|
|
39
40
|
includeAttachments,
|
|
40
41
|
includeButtons,
|
|
41
42
|
includeComponents,
|
|
@@ -48,6 +49,7 @@ export async function createTranscript(channel, options = {}) {
|
|
|
48
49
|
quantity: validQuantity ? quantity : 0,
|
|
49
50
|
returnFormat,
|
|
50
51
|
returnType: artificialReturnType,
|
|
52
|
+
safeMode,
|
|
51
53
|
saveImages,
|
|
52
54
|
selfContained,
|
|
53
55
|
timeZone,
|
|
@@ -90,23 +92,21 @@ export async function createTranscript(channel, options = {}) {
|
|
|
90
92
|
...options.cdnOptions
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
(()
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
]);
|
|
109
|
-
}
|
|
95
|
+
await Promise.all([
|
|
96
|
+
(async () => {
|
|
97
|
+
jsonTranscript.setAuthors(await authorUrlResolver(authors, internalOptions, options.cdnOptions ?? null, urlCache));
|
|
98
|
+
authors.clear();
|
|
99
|
+
})(),
|
|
100
|
+
(() => {
|
|
101
|
+
jsonTranscript.setMentions({ channels: Array.from(mentions.channels.values()), roles: Array.from(mentions.roles.values()), users: Array.from(mentions.users.values()) });
|
|
102
|
+
mentions.channels.clear();
|
|
103
|
+
mentions.roles.clear();
|
|
104
|
+
mentions.users.clear();
|
|
105
|
+
})(),
|
|
106
|
+
(async () => {
|
|
107
|
+
jsonTranscript.setMessages(await messagesUrlResolver(jsonTranscript.getMessages(), internalOptions, options.cdnOptions ?? null, urlCache));
|
|
108
|
+
})()
|
|
109
|
+
]);
|
|
110
110
|
const outputJson = await jsonTranscript.toJson();
|
|
111
111
|
urlCache.clear();
|
|
112
112
|
const result = await output(outputJson);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseGuildTextChannel, DMChannel } from "discord.js";
|
|
2
2
|
import { urlResolver } from "../../core/urlResolver.js";
|
|
3
|
+
import { resolveImageURL } from "../../../../discord-message-transcript-base/src/core/sanitizer.js";
|
|
3
4
|
export class Json {
|
|
4
5
|
guild;
|
|
5
6
|
channel;
|
|
@@ -43,8 +44,9 @@ export class Json {
|
|
|
43
44
|
async toJson() {
|
|
44
45
|
const channel = await this.channel.fetch();
|
|
45
46
|
const channelImg = channel instanceof DMChannel ? channel.recipient?.displayAvatarURL() ?? "cdn.discordapp.com/embed/avatars/4.png" : channel.isDMBased() ? channel.iconURL() ?? (await channel.fetchOwner()).displayAvatarURL() : null;
|
|
47
|
+
const safeChannelImg = await resolveImageURL(channelImg, this.options, true);
|
|
46
48
|
const guild = !channel.isDMBased() ? this.guild : null;
|
|
47
|
-
const guildIcon = guild
|
|
49
|
+
const guildIcon = guild ? await resolveImageURL(guild.iconURL(), this.options, true) : null;
|
|
48
50
|
const guildJson = !guild ? null : {
|
|
49
51
|
name: guild.name,
|
|
50
52
|
id: guild.id,
|
|
@@ -58,7 +60,7 @@ export class Json {
|
|
|
58
60
|
parent: channel.isDMBased() ? null : (channel.parent ? { name: channel.parent.name, id: channel.parent.id } : null),
|
|
59
61
|
topic: (channel instanceof BaseGuildTextChannel) ? channel.topic : null,
|
|
60
62
|
id: channel.id,
|
|
61
|
-
img:
|
|
63
|
+
img: safeChannelImg ? await urlResolver(safeChannelImg, this.options, this.cdnOptions, this.urlCache) : null,
|
|
62
64
|
},
|
|
63
65
|
authors: this.authors,
|
|
64
66
|
messages: this.messages.reverse(),
|
package/dist/types/types.d.ts
CHANGED
|
@@ -73,6 +73,12 @@ export interface TranscriptOptions<T extends ReturnType> {
|
|
|
73
73
|
* Configuration for uploading attachments and other assets to a CDN.
|
|
74
74
|
*/
|
|
75
75
|
cdnOptions: CDNOptions;
|
|
76
|
+
/**
|
|
77
|
+
* Disable all warnings to keep console output clean.
|
|
78
|
+
* ⚠️ Can hide issues like unsafe URLs or fallback usage.
|
|
79
|
+
* @default false
|
|
80
|
+
*/
|
|
81
|
+
disableWarnings: boolean;
|
|
76
82
|
/**
|
|
77
83
|
* The name of the generated file (without extension).
|
|
78
84
|
* @default `Transcript-channel-name-channel-id`
|
|
@@ -146,6 +152,14 @@ export interface TranscriptOptions<T extends ReturnType> {
|
|
|
146
152
|
* @default ReturnType.Attachment
|
|
147
153
|
*/
|
|
148
154
|
returnType: T;
|
|
155
|
+
/**
|
|
156
|
+
* Enables safe mode, blocking potentially unsafe URLs and content.
|
|
157
|
+
* Prevents suspicious links, images, or HTML from being included in the final transcript.
|
|
158
|
+
*
|
|
159
|
+
* ⚠️ Disabling may allow unsafe content to appear in the transcript.
|
|
160
|
+
* @default true
|
|
161
|
+
*/
|
|
162
|
+
safeMode: boolean;
|
|
149
163
|
/**
|
|
150
164
|
* Whether to save images as base64 data directly in the transcript.
|
|
151
165
|
* This is an alternative to using a CDN and results in larger file sizes.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discord-message-transcript",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"typescript": "^5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"discord-message-transcript-base": "1.
|
|
51
|
+
"discord-message-transcript-base": "1.3.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"discord.js": ">=14.19.0 <15"
|