discord-message-transcript 1.3.1-dev.3.35 → 1.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.
- package/dist/core/cdnResolver.d.ts +2 -2
- package/dist/core/cdnResolver.js +12 -9
- package/dist/core/componentToJson.js +1 -1
- package/dist/core/fetchMessages.d.ts +1 -1
- package/dist/core/fetchMessages.js +1 -1
- package/dist/core/getMentions.d.ts +1 -1
- package/dist/core/getMentions.js +1 -1
- package/dist/core/imageToBase64.d.ts +2 -1
- package/dist/core/imageToBase64.js +17 -4
- package/dist/core/mappers.d.ts +1 -1
- package/dist/core/mappers.js +1 -1
- package/dist/core/networkSecurity/constants.d.ts +3 -0
- package/dist/core/networkSecurity/constants.js +3 -0
- package/dist/core/networkSecurity/dns.d.ts +2 -0
- package/dist/core/networkSecurity/dns.js +29 -0
- package/dist/core/networkSecurity/index.d.ts +2 -0
- package/dist/core/networkSecurity/index.js +2 -0
- package/dist/core/networkSecurity/ip.d.ts +1 -0
- package/dist/core/networkSecurity/ip.js +110 -0
- package/dist/core/networkSecurity/lookup.d.ts +2 -0
- package/dist/core/networkSecurity/lookup.js +14 -0
- package/dist/core/networkSecurity/urlSafety.d.ts +3 -0
- package/dist/core/networkSecurity/urlSafety.js +71 -0
- package/dist/core/resolveImageUrl.d.ts +4 -0
- package/dist/core/resolveImageUrl.js +20 -0
- package/dist/core/urlResolver.d.ts +2 -2
- package/dist/core/urlResolver.js +20 -15
- package/dist/index.d.ts +3 -3
- package/dist/index.js +8 -8
- package/dist/renderers/json/json.d.ts +1 -1
- package/dist/renderers/json/json.js +2 -2
- package/dist/types/types.d.ts +12 -0
- package/package.json +4 -7
- package/dist/core/clientManager.d.ts +0 -3
- package/dist/core/clientManager.js +0 -9
- package/dist/core/componentHelpers.d.ts +0 -3
- package/dist/core/componentHelpers.js +0 -175
- package/dist/core/error.d.ts +0 -3
- package/dist/core/error.js +0 -7
- package/dist/core/markdown.d.ts +0 -2
- package/dist/core/markdown.js +0 -175
- package/dist/renderers/html/clientRenderer.d.ts +0 -0
- package/dist/renderers/html/clientRenderer.js +0 -73
- package/dist/renderers/html/css.d.ts +0 -11
- package/dist/renderers/html/css.js +0 -663
- package/dist/renderers/html/html copy.d.ts +0 -19
- package/dist/renderers/html/html copy.js +0 -371
- package/dist/renderers/html/html-backup.d.ts +0 -19
- package/dist/renderers/html/html-backup.js +0 -371
- package/dist/renderers/html/html.d.ts +0 -19
- package/dist/renderers/html/html.js +0 -415
- package/dist/renderers/html/html2.d.ts +0 -8
- package/dist/renderers/html/html2.js +0 -233
- package/dist/renderers/html/js.d.ts +0 -4
- package/dist/renderers/html/js.js +0 -174
- package/dist/types/types copy.d.ts +0 -284
- package/dist/types/types copy.js +0 -35
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { CDNOptions } from "
|
|
1
|
+
import { CDNOptions, safeUrlReturn } from "@/types";
|
|
2
2
|
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
-
export declare function cdnResolver(
|
|
3
|
+
export declare function cdnResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions): Promise<string>;
|
|
4
4
|
export declare function uploadCareResolver(url: string, publicKey: string, cdnDomain: string, disableWarnings: boolean): Promise<string>;
|
|
5
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
|
@@ -1,21 +1,24 @@
|
|
|
1
|
-
import https from 'https';
|
|
2
|
-
import http from 'http';
|
|
3
1
|
import { CustomWarn } from "discord-message-transcript-base";
|
|
4
2
|
import crypto from 'crypto';
|
|
5
3
|
import { getCDNLimiter } from "./limiter.js";
|
|
6
|
-
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import http from 'http';
|
|
6
|
+
import { createLookup } from "@/networkSecurity";
|
|
7
|
+
export async function cdnResolver(safeUrlObject, options, cdnOptions) {
|
|
8
|
+
const url = safeUrlObject.url;
|
|
7
9
|
const limit = getCDNLimiter();
|
|
8
10
|
return limit(async () => {
|
|
9
11
|
return new Promise((resolve, reject) => {
|
|
10
|
-
const client = url.startsWith('https') ? https : http;
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
headers: { "User-Agent": "discord-message-transcript" }
|
|
12
|
+
const client = safeUrlObject.url.startsWith('https') ? https : http;
|
|
13
|
+
const lookup = createLookup(safeUrlObject.safeIps);
|
|
14
|
+
const request = client.get(url, {
|
|
15
|
+
headers: { "User-Agent": "discord-message-transcript" },
|
|
16
|
+
lookup: lookup
|
|
14
17
|
}, async (response) => {
|
|
15
18
|
if (response.statusCode !== 200) {
|
|
16
19
|
response.destroy();
|
|
17
20
|
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}.`, options.disableWarnings);
|
|
21
|
+
Failed to fetch attachment with status code: ${response.statusCode} from ${safeUrlObject.url}.`, options.disableWarnings);
|
|
19
22
|
return resolve(url);
|
|
20
23
|
}
|
|
21
24
|
const contentType = response.headers["content-type"];
|
|
@@ -47,7 +50,7 @@ Error: ${err.message}`, options.disableWarnings);
|
|
|
47
50
|
request.destroy();
|
|
48
51
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
49
52
|
Request timeout for ${url}.`, options.disableWarnings);
|
|
50
|
-
resolve(url);
|
|
53
|
+
return resolve(url);
|
|
51
54
|
});
|
|
52
55
|
request.end();
|
|
53
56
|
});
|
|
@@ -1,7 +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 "
|
|
4
|
+
import { isValidHexColor } from "discord-message-transcript-base";
|
|
5
5
|
export async function componentsToJson(components, options) {
|
|
6
6
|
const processedComponents = await Promise.all(components.filter(component => !(!options.includeV2Components && component.type != ComponentType.ActionRow))
|
|
7
7
|
.map(async (component) => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TextBasedChannel } from "discord.js";
|
|
2
2
|
import { JsonAuthor, JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
-
import { MapMentions } from "
|
|
3
|
+
import { MapMentions } from "@/types";
|
|
4
4
|
export declare function fetchMessages(ctx: FetchMessagesContext): Promise<{
|
|
5
5
|
messages: JsonMessage[];
|
|
6
6
|
end: boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EmbedType } from "discord.js";
|
|
2
2
|
import { componentsToJson } from "./componentToJson.js";
|
|
3
|
+
import { isValidHexColor, sanitize } from "discord-message-transcript-base";
|
|
3
4
|
import { getMentions } from "./getMentions.js";
|
|
4
|
-
import { isValidHexColor, sanitize } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
|
|
5
5
|
export async function fetchMessages(ctx) {
|
|
6
6
|
const { channel, options, transcriptState, lastMessageId } = ctx;
|
|
7
7
|
const { authors, mentions } = transcriptState;
|
package/dist/core/getMentions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ChannelType } from "discord.js";
|
|
2
|
-
import { isValidHexColor, sanitize } from "
|
|
2
|
+
import { isValidHexColor, sanitize } from "discord-message-transcript-base";
|
|
3
3
|
export async function getMentions(message, mentions) {
|
|
4
4
|
message.mentions.channels.forEach(channel => {
|
|
5
5
|
if (!mentions.channels.has(channel.id)) {
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import { safeUrlReturn } from '@/types';
|
|
2
|
+
export declare function imageToBase64(safeUrlObject: safeUrlReturn, disableWarnings: boolean): Promise<string>;
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import https from 'https';
|
|
2
|
-
import http from 'http';
|
|
3
1
|
import { CustomWarn } from 'discord-message-transcript-base';
|
|
4
2
|
import { getBase64Limiter } from './limiter.js';
|
|
5
|
-
|
|
3
|
+
import https from 'https';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import { createLookup } from '@/networkSecurity';
|
|
6
|
+
const MAX_BYTES = 25 * 1024 * 1024; // 25MB
|
|
7
|
+
export async function imageToBase64(safeUrlObject, disableWarnings) {
|
|
8
|
+
const url = safeUrlObject.url;
|
|
6
9
|
const limit = getBase64Limiter();
|
|
7
10
|
return limit(async () => {
|
|
8
11
|
return new Promise((resolve, reject) => {
|
|
9
12
|
const client = url.startsWith('https') ? https : http;
|
|
10
|
-
const
|
|
13
|
+
const lookup = createLookup(safeUrlObject.safeIps);
|
|
14
|
+
const request = client.get(url, {
|
|
15
|
+
headers: { "User-Agent": "discord-message-transcript" },
|
|
16
|
+
lookup: lookup
|
|
17
|
+
}, (response) => {
|
|
11
18
|
if (response.statusCode !== 200) {
|
|
12
19
|
response.destroy();
|
|
13
20
|
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
@@ -19,8 +26,14 @@ Failed to fetch image with status code: ${response.statusCode} from ${url}.`, di
|
|
|
19
26
|
response.destroy();
|
|
20
27
|
return resolve(url);
|
|
21
28
|
}
|
|
29
|
+
let total = 0;
|
|
22
30
|
const chunks = [];
|
|
23
31
|
response.on('data', (chunk) => {
|
|
32
|
+
total += chunk.length;
|
|
33
|
+
if (total > MAX_BYTES) {
|
|
34
|
+
response.destroy();
|
|
35
|
+
return resolve(url);
|
|
36
|
+
}
|
|
24
37
|
chunks.push(chunk);
|
|
25
38
|
});
|
|
26
39
|
response.on('end', () => {
|
package/dist/core/mappers.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
|
|
2
2
|
import { JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
|
|
3
|
-
import { ReturnType } from "
|
|
3
|
+
import { ReturnType } from "@/types";
|
|
4
4
|
export declare function mapButtonStyle(style: ButtonStyle): JsonButtonStyle;
|
|
5
5
|
export declare function mapSeparatorSpacing(spacing: SeparatorSpacingSize): JsonSeparatorSpacingSize;
|
|
6
6
|
export declare function mapComponentType(componentType: ComponentType): JsonComponentType;
|
package/dist/core/mappers.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
|
|
2
2
|
import { CustomError, JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
|
|
3
|
-
import { ReturnType } from "
|
|
3
|
+
import { ReturnType } from "@/types";
|
|
4
4
|
export function mapButtonStyle(style) {
|
|
5
5
|
switch (style) {
|
|
6
6
|
case ButtonStyle.Primary:
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Resolver } from "dns/promises";
|
|
2
|
+
import { DNS_LOOKUP_TIMEOUT, DNS_SERVERS } from "./constants.js";
|
|
3
|
+
export async function resolveAllIps(host) {
|
|
4
|
+
const resolver = new Resolver();
|
|
5
|
+
resolver.setServers(DNS_SERVERS);
|
|
6
|
+
const lookupPromise = (async () => {
|
|
7
|
+
const results = [];
|
|
8
|
+
const [v4, v6] = await Promise.allSettled([
|
|
9
|
+
resolver.resolve4(host),
|
|
10
|
+
resolver.resolve6(host)
|
|
11
|
+
]);
|
|
12
|
+
if (v4.status === "fulfilled") {
|
|
13
|
+
for (const ip of v4.value) {
|
|
14
|
+
results.push({ address: ip, family: 4 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (v6.status === "fulfilled") {
|
|
18
|
+
for (const ip of v6.value) {
|
|
19
|
+
results.push({ address: ip, family: 6 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (results.length === 0) {
|
|
23
|
+
throw new Error(`No DNS records found for ${host}`);
|
|
24
|
+
}
|
|
25
|
+
return results;
|
|
26
|
+
})();
|
|
27
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`DNS timeout for ${host}`)), DNS_LOOKUP_TIMEOUT));
|
|
28
|
+
return Promise.race([lookupPromise, timeoutPromise]);
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function isPrivateIp(ip: string): boolean;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
export function isPrivateIp(ip) {
|
|
3
|
+
const family = net.isIP(ip);
|
|
4
|
+
if (!family)
|
|
5
|
+
return true;
|
|
6
|
+
if (family === 4)
|
|
7
|
+
return isPrivateIPv4(ip);
|
|
8
|
+
return isPrivateIPv6(ip);
|
|
9
|
+
}
|
|
10
|
+
function isPrivateIPv4(ip) {
|
|
11
|
+
const parts = ip.split(".").map(Number);
|
|
12
|
+
if (parts.length !== 4 || parts.some(n => isNaN(n)))
|
|
13
|
+
return true;
|
|
14
|
+
const [a, b] = parts;
|
|
15
|
+
return (a === 0 ||
|
|
16
|
+
a === 10 ||
|
|
17
|
+
a === 127 ||
|
|
18
|
+
(a === 169 && b === 254) ||
|
|
19
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
20
|
+
(a === 192 && b === 168) ||
|
|
21
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
22
|
+
a >= 224);
|
|
23
|
+
}
|
|
24
|
+
function parseIPv6(ip) {
|
|
25
|
+
if (net.isIP(ip) !== 6)
|
|
26
|
+
return null;
|
|
27
|
+
// handle IPv4 at end
|
|
28
|
+
if (ip.includes(".")) {
|
|
29
|
+
const lastColon = ip.lastIndexOf(":");
|
|
30
|
+
const ipv4Part = ip.slice(lastColon + 1);
|
|
31
|
+
const nums = ipv4Part.split(".").map(Number);
|
|
32
|
+
if (nums.length === 4 && nums.every(n => !isNaN(n))) {
|
|
33
|
+
const hex = ((nums[0] << 8) | nums[1]).toString(16) +
|
|
34
|
+
":" +
|
|
35
|
+
((nums[2] << 8) | nums[3]).toString(16);
|
|
36
|
+
ip = ip.slice(0, lastColon) + ":" + hex;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const sections = ip.split("::");
|
|
40
|
+
let head = sections[0] ? sections[0].split(":") : [];
|
|
41
|
+
let tail = sections[1] ? sections[1].split(":") : [];
|
|
42
|
+
if (sections.length === 2) {
|
|
43
|
+
const missing = 8 - (head.length + tail.length);
|
|
44
|
+
head = [...head, ...Array(missing).fill("0"), ...tail];
|
|
45
|
+
}
|
|
46
|
+
if (head.length !== 8)
|
|
47
|
+
return null;
|
|
48
|
+
const bytes = [];
|
|
49
|
+
for (const part of head) {
|
|
50
|
+
const n = parseInt(part || "0", 16);
|
|
51
|
+
if (isNaN(n))
|
|
52
|
+
return null;
|
|
53
|
+
bytes.push((n >> 8) & 0xff);
|
|
54
|
+
bytes.push(n & 0xff);
|
|
55
|
+
}
|
|
56
|
+
return bytes;
|
|
57
|
+
}
|
|
58
|
+
function extractEmbeddedIPv4(bytes) {
|
|
59
|
+
const isMapped = bytes.slice(0, 10).every(b => b === 0) &&
|
|
60
|
+
bytes[10] === 0xff &&
|
|
61
|
+
bytes[11] === 0xff;
|
|
62
|
+
if (isMapped) {
|
|
63
|
+
return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
|
|
64
|
+
}
|
|
65
|
+
const isCompat = bytes.slice(0, 12).every(b => b === 0);
|
|
66
|
+
if (isCompat) {
|
|
67
|
+
return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
|
|
68
|
+
}
|
|
69
|
+
const isNat64 = bytes[0] === 0x00 &&
|
|
70
|
+
bytes[1] === 0x64 &&
|
|
71
|
+
bytes[2] === 0xff &&
|
|
72
|
+
bytes[3] === 0x9b &&
|
|
73
|
+
bytes.slice(4, 12).every(b => b === 0);
|
|
74
|
+
if (isNat64) {
|
|
75
|
+
return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function isPrivateIPv6(ip) {
|
|
80
|
+
const bytes = parseIPv6(ip);
|
|
81
|
+
if (!bytes)
|
|
82
|
+
return true;
|
|
83
|
+
const embedded = extractEmbeddedIPv4(bytes);
|
|
84
|
+
if (embedded)
|
|
85
|
+
return isPrivateIPv4(embedded);
|
|
86
|
+
// ::
|
|
87
|
+
if (bytes.every(b => b === 0))
|
|
88
|
+
return true;
|
|
89
|
+
// ::1
|
|
90
|
+
if (bytes.slice(0, 15).every(b => b === 0) && bytes[15] === 1)
|
|
91
|
+
return true;
|
|
92
|
+
const first = bytes[0];
|
|
93
|
+
const second = bytes[1];
|
|
94
|
+
// fc00::/7
|
|
95
|
+
if ((first & 0xfe) === 0xfc)
|
|
96
|
+
return true;
|
|
97
|
+
// fe80::/10
|
|
98
|
+
if (first === 0xfe && (second & 0xc0) === 0x80)
|
|
99
|
+
return true;
|
|
100
|
+
// multicast
|
|
101
|
+
if (first === 0xff)
|
|
102
|
+
return true;
|
|
103
|
+
// 2001:db8::/32
|
|
104
|
+
if (bytes[0] === 0x20 &&
|
|
105
|
+
bytes[1] === 0x01 &&
|
|
106
|
+
bytes[2] === 0x0d &&
|
|
107
|
+
bytes[3] === 0xb8)
|
|
108
|
+
return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import net from "node:net";
|
|
2
|
+
export function urlToIpUrl(url, ip) {
|
|
3
|
+
// If got here shouldn't throw a error
|
|
4
|
+
const u = new URL(url);
|
|
5
|
+
return `${u.protocol}//${ip}` + `${u.port ? ":" + u.port : ""}` + `${u.pathname}${u.search}`;
|
|
6
|
+
}
|
|
7
|
+
export function createLookup(safeIps) {
|
|
8
|
+
if (safeIps.length == 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
return (_hostname, _opts, cb) => {
|
|
11
|
+
const ip = safeIps[Math.floor(Math.random() * safeIps.length)];
|
|
12
|
+
cb(null, ip, net.isIP(ip));
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { CustomWarn } from "discord-message-transcript-base";
|
|
2
|
+
import { TRUSTED_DISCORD_HOSTS } from "./constants.js";
|
|
3
|
+
import { isPrivateIp } from "./ip.js";
|
|
4
|
+
import { resolveAllIps } from "./dns.js";
|
|
5
|
+
export async function isSafeForHTML(url, options) {
|
|
6
|
+
const { safeMode, disableWarnings } = options;
|
|
7
|
+
if (!safeMode)
|
|
8
|
+
return { safe: true, safeIps: [], url: url };
|
|
9
|
+
let u;
|
|
10
|
+
try {
|
|
11
|
+
u = new URL(url);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
CustomWarn(`Unsafe URL rejected: Invalid URL format\nURL: ${url}`, disableWarnings);
|
|
15
|
+
return { safe: false, safeIps: [], url: url };
|
|
16
|
+
}
|
|
17
|
+
const host = u.hostname.toLowerCase();
|
|
18
|
+
// If is from discord accept
|
|
19
|
+
if (isTrustedDiscordHost(host))
|
|
20
|
+
return { safe: true, safeIps: [], url: url };
|
|
21
|
+
// Don't accept if isn't https or http
|
|
22
|
+
if (!["http:", "https:"].includes(u.protocol)) {
|
|
23
|
+
CustomWarn(`Unsafe URL rejected: Invalid protocol "${u.protocol}"\nURL: ${url}`, disableWarnings);
|
|
24
|
+
return { safe: false, safeIps: [], url: url };
|
|
25
|
+
}
|
|
26
|
+
if (u.username || u.password) {
|
|
27
|
+
CustomWarn(`Unsafe URL rejected: Contains username or password\nURL: ${url}`, disableWarnings);
|
|
28
|
+
return { safe: false, safeIps: [], url: url };
|
|
29
|
+
}
|
|
30
|
+
if (u.port && !["80", "443", ""].includes(u.port)) {
|
|
31
|
+
CustomWarn(`Unsafe URL rejected: Invalid port "${u.port}"\nURL: ${url}`, disableWarnings);
|
|
32
|
+
return { safe: false, safeIps: [], url: url };
|
|
33
|
+
}
|
|
34
|
+
// Block localhost and loopback addresses (SSRF protection)
|
|
35
|
+
if (host === "localhost" ||
|
|
36
|
+
host === "127.0.0.1" ||
|
|
37
|
+
host.startsWith("0.")) {
|
|
38
|
+
CustomWarn(`Unsafe URL rejected: Blacklisted host "${host}"\nURL: ${url}`, disableWarnings);
|
|
39
|
+
return { safe: false, safeIps: [], url: url };
|
|
40
|
+
}
|
|
41
|
+
let ips;
|
|
42
|
+
try {
|
|
43
|
+
ips = await resolveAllIps(host);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
CustomWarn(`Unsafe URL rejected: DNS lookup failed or timed out for host "${host}". Error: ${e.message}\nURL: ${url}`, disableWarnings);
|
|
47
|
+
return { safe: false, safeIps: [], url: url };
|
|
48
|
+
}
|
|
49
|
+
const safeIps = [];
|
|
50
|
+
// Block private/internal network IPs (SSRF protection)
|
|
51
|
+
for (const ip of ips) {
|
|
52
|
+
if (isPrivateIp(ip.address)) {
|
|
53
|
+
CustomWarn(`Unsafe URL rejected: Private IP address "${ip.address}" resolved for host "${host}"\nURL: ${url}`, disableWarnings);
|
|
54
|
+
return { safe: false, safeIps: [], url: url };
|
|
55
|
+
}
|
|
56
|
+
safeIps.push(ip.address);
|
|
57
|
+
}
|
|
58
|
+
const path = u.pathname.toLowerCase();
|
|
59
|
+
// External SVGs can execute scripts → allow only from Discord CDN
|
|
60
|
+
if (path.endsWith(".svg")) {
|
|
61
|
+
CustomWarn(`Unsafe URL rejected: External SVG not from Discord CDN\nURL: ${url}`, disableWarnings);
|
|
62
|
+
return { safe: false, safeIps: [], url: url };
|
|
63
|
+
}
|
|
64
|
+
return { safe: true, safeIps: safeIps, url: url };
|
|
65
|
+
}
|
|
66
|
+
function isTrustedDiscordHost(host) {
|
|
67
|
+
host = host.toLowerCase();
|
|
68
|
+
return TRUSTED_DISCORD_HOSTS.some(trusted => {
|
|
69
|
+
return host === trusted || host.endsWith("." + trusted);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { JsonAttachment, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
2
|
+
import { safeUrlReturn } from "@/types";
|
|
3
|
+
export declare function resolveImageURL(url: string, options: TranscriptOptionsBase, canReturnNull: false, attachments?: JsonAttachment[]): Promise<safeUrlReturn>;
|
|
4
|
+
export declare function resolveImageURL(url: string | null, options: TranscriptOptionsBase, canReturnNull: true, attachments?: JsonAttachment[]): Promise<safeUrlReturn | null>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FALLBACK_PIXEL } from "discord-message-transcript-base";
|
|
2
|
+
import { isSafeForHTML } from "@/networkSecurity";
|
|
3
|
+
export async function resolveImageURL(url, options, canReturnNull, attachments) {
|
|
4
|
+
if (!url)
|
|
5
|
+
return null;
|
|
6
|
+
// Resolve attachment:// references to actual attachment URL
|
|
7
|
+
if (url.startsWith("attachment://")) {
|
|
8
|
+
const name = url.slice("attachment://".length).trim();
|
|
9
|
+
const found = attachments?.find(a => a.name === name);
|
|
10
|
+
if (!found)
|
|
11
|
+
return { safe: true, safeIps: [], url: FALLBACK_PIXEL };
|
|
12
|
+
url = found.url;
|
|
13
|
+
}
|
|
14
|
+
const safeUrlReturn = await isSafeForHTML(url, options);
|
|
15
|
+
if (safeUrlReturn.safe)
|
|
16
|
+
return safeUrlReturn;
|
|
17
|
+
if (canReturnNull)
|
|
18
|
+
return null;
|
|
19
|
+
return { safe: true, safeIps: [], url: FALLBACK_PIXEL };
|
|
20
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { JsonAuthor, JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
2
|
-
import { CDNOptions } from "
|
|
3
|
-
export declare function urlResolver(
|
|
2
|
+
import { CDNOptions, safeUrlReturn } from "@/types";
|
|
3
|
+
export declare function urlResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<string>;
|
|
4
4
|
export declare function messagesUrlResolver(messages: JsonMessage[], options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonMessage[]>;
|
|
5
5
|
export declare function authorUrlResolver(authors: Map<string, JsonAuthor>, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonAuthor[]>;
|
package/dist/core/urlResolver.js
CHANGED
|
@@ -2,40 +2,44 @@ 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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (
|
|
10
|
-
|
|
5
|
+
import { FALLBACK_PIXEL } from "discord-message-transcript-base";
|
|
6
|
+
import { resolveImageURL } from "./resolveImageUrl.js";
|
|
7
|
+
import { isSafeForHTML } from "@/networkSecurity";
|
|
8
|
+
export async function urlResolver(safeUrlObject, options, cdnOptions, urlCache) {
|
|
9
|
+
if (safeUrlObject.safe == false)
|
|
10
|
+
return "";
|
|
11
|
+
if (safeUrlObject.url == FALLBACK_PIXEL)
|
|
12
|
+
return safeUrlObject.url;
|
|
13
|
+
if (urlCache.has(safeUrlObject.url)) {
|
|
14
|
+
const cache = urlCache.get(safeUrlObject.url);
|
|
11
15
|
if (cache)
|
|
12
16
|
return await cache;
|
|
13
17
|
}
|
|
14
18
|
let returnUrl;
|
|
15
19
|
if (cdnOptions)
|
|
16
|
-
returnUrl = cdnResolver(
|
|
20
|
+
returnUrl = cdnResolver(safeUrlObject, options, cdnOptions);
|
|
17
21
|
else if (options.saveImages)
|
|
18
|
-
returnUrl = imageToBase64(
|
|
22
|
+
returnUrl = imageToBase64(safeUrlObject, options.disableWarnings);
|
|
19
23
|
if (returnUrl) {
|
|
20
|
-
urlCache.set(url, returnUrl);
|
|
24
|
+
urlCache.set(safeUrlObject.url, returnUrl);
|
|
21
25
|
return await returnUrl;
|
|
22
26
|
}
|
|
23
|
-
return url;
|
|
27
|
+
return safeUrlObject.url;
|
|
24
28
|
}
|
|
25
29
|
export async function messagesUrlResolver(messages, options, cdnOptions, urlCache) {
|
|
26
30
|
return await Promise.all(messages.map(async (message) => {
|
|
27
31
|
// Needs to wait for resolve correct when used attachment://
|
|
28
32
|
const attachments = await Promise.all(message.attachments.map(async (attachment) => {
|
|
29
|
-
let
|
|
33
|
+
let safeUrlObject;
|
|
30
34
|
if (attachment.contentType?.startsWith("image/")) {
|
|
31
|
-
|
|
35
|
+
safeUrlObject = await resolveImageURL(attachment.url, options, false, message.attachments);
|
|
32
36
|
}
|
|
33
37
|
else {
|
|
34
|
-
|
|
38
|
+
safeUrlObject = await isSafeForHTML(attachment.url, options);
|
|
35
39
|
}
|
|
36
40
|
return {
|
|
37
41
|
...attachment,
|
|
38
|
-
url: await urlResolver(
|
|
42
|
+
url: await urlResolver(safeUrlObject, options, cdnOptions, urlCache)
|
|
39
43
|
};
|
|
40
44
|
}));
|
|
41
45
|
const embedsPromise = Promise.all(message.embeds.map(async (embed) => {
|
|
@@ -78,9 +82,10 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
78
82
|
};
|
|
79
83
|
}
|
|
80
84
|
if (component.type == JsonComponentType.File) {
|
|
85
|
+
const safeUrlObject = await isSafeForHTML(component.url, options);
|
|
81
86
|
return {
|
|
82
87
|
...component,
|
|
83
|
-
url: await urlResolver(
|
|
88
|
+
url: await urlResolver(safeUrlObject, options, cdnOptions, urlCache),
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
if (component.type == JsonComponentType.Container) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType, CDNOptions, MimeType } from "
|
|
1
|
+
export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType, CDNOptions, MimeType } from "@/types";
|
|
2
2
|
export { ReturnFormat, LocalDate, TimeZone } from "discord-message-transcript-base";
|
|
3
|
-
export { setBase64Concurrency, setCDNConcurrency } from '
|
|
3
|
+
export { setBase64Concurrency, setCDNConcurrency } from '@/core/limiter.js';
|
|
4
4
|
import { TextBasedChannel } from "discord.js";
|
|
5
|
-
import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "
|
|
5
|
+
import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "@/types";
|
|
6
6
|
/**
|
|
7
7
|
* Creates a transcript of a Discord channel's messages.
|
|
8
8
|
* Depending on the `returnType` option, this function can return an `AttachmentBuilder`,
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
export { ReturnType } from "
|
|
1
|
+
export { ReturnType } from "@/types";
|
|
2
2
|
export { ReturnFormat } from "discord-message-transcript-base";
|
|
3
|
-
export { setBase64Concurrency, setCDNConcurrency } from '
|
|
3
|
+
export { setBase64Concurrency, setCDNConcurrency } from '@/core/limiter.js';
|
|
4
4
|
import { AttachmentBuilder } from "discord.js";
|
|
5
|
-
import { Json } from "
|
|
6
|
-
import { fetchMessages } from "
|
|
7
|
-
import { ReturnType } from "
|
|
8
|
-
import { output } from "
|
|
5
|
+
import { Json } from "@/renderers/json/json.js";
|
|
6
|
+
import { fetchMessages } from "@/core/fetchMessages.js";
|
|
7
|
+
import { ReturnType } from "@/types";
|
|
8
|
+
import { output } from "@/core/output.js";
|
|
9
9
|
import { ReturnTypeBase, ReturnFormat, outputBase, CustomError, CustomWarn } from "discord-message-transcript-base";
|
|
10
|
-
import { returnTypeMapper } from "
|
|
11
|
-
import { authorUrlResolver, messagesUrlResolver } from "
|
|
10
|
+
import { returnTypeMapper } from "@/core/mappers.js";
|
|
11
|
+
import { authorUrlResolver, messagesUrlResolver } from "@/core/urlResolver.js";
|
|
12
12
|
/**
|
|
13
13
|
* Creates a transcript of a Discord channel's messages.
|
|
14
14
|
* Depending on the `returnType` option, this function can return an `AttachmentBuilder`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Guild, TextBasedChannel } from "discord.js";
|
|
2
2
|
import { ArrayMentions, JsonAuthor, JsonMessage, TranscriptOptionsBase, JsonData } from "discord-message-transcript-base";
|
|
3
|
-
import { CDNOptions } from "
|
|
3
|
+
import { CDNOptions } from "@/types";
|
|
4
4
|
export declare class Json {
|
|
5
5
|
private guild;
|
|
6
6
|
private channel;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseGuildTextChannel, DMChannel } from "discord.js";
|
|
2
|
-
import { urlResolver } from "
|
|
3
|
-
import { resolveImageURL } from "
|
|
2
|
+
import { urlResolver } from "@/core/urlResolver.js";
|
|
3
|
+
import { resolveImageURL } from "@/core/resolveImageUrl.js";
|
|
4
4
|
export class Json {
|
|
5
5
|
guild;
|
|
6
6
|
channel;
|
package/dist/types/types.d.ts
CHANGED
|
@@ -286,3 +286,15 @@ export type CDNOptionsUploadcare = {
|
|
|
286
286
|
*/
|
|
287
287
|
cdnDomain: string;
|
|
288
288
|
};
|
|
289
|
+
/**
|
|
290
|
+
* Result from dns.lookup
|
|
291
|
+
*/
|
|
292
|
+
export type LookupResult = {
|
|
293
|
+
address: string;
|
|
294
|
+
family: 4 | 6;
|
|
295
|
+
};
|
|
296
|
+
export interface safeUrlReturn {
|
|
297
|
+
safe: boolean;
|
|
298
|
+
safeIps: string[];
|
|
299
|
+
url: string;
|
|
300
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discord-message-transcript",
|
|
3
|
-
"version": "1.3.1
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -44,11 +44,8 @@
|
|
|
44
44
|
"url": "https://github.com/HenriqueMairesse/discord-message-transcript/issues"
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://github.com/HenriqueMairesse/discord-message-transcript#readme",
|
|
47
|
-
"devDependencies": {
|
|
48
|
-
"typescript": "^5.9.3"
|
|
49
|
-
},
|
|
50
47
|
"dependencies": {
|
|
51
|
-
"discord-message-transcript-base": "1.3.1
|
|
48
|
+
"discord-message-transcript-base": "1.3.1"
|
|
52
49
|
},
|
|
53
50
|
"peerDependencies": {
|
|
54
51
|
"discord.js": ">=14.19.0 <15"
|
|
@@ -57,7 +54,7 @@
|
|
|
57
54
|
"access": "public"
|
|
58
55
|
},
|
|
59
56
|
"scripts": {
|
|
60
|
-
"
|
|
61
|
-
"build": "tsc"
|
|
57
|
+
"clean": "pnpm exec rimraf dist",
|
|
58
|
+
"build": "pnpm run clean && tsc"
|
|
62
59
|
}
|
|
63
60
|
}
|