discord-message-transcript 1.3.1-dev.1.47 → 1.3.1-dev.1.48
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/{imageToBase64.js → assetResolver/base64/imageToBase64.js} +3 -2
- package/dist/core/assetResolver/cdn/cdnCustomError.d.ts +16 -0
- package/dist/core/assetResolver/cdn/cdnCustomError.js +28 -0
- package/dist/core/assetResolver/cdn/cdnResolver.d.ts +3 -0
- package/dist/core/assetResolver/cdn/cdnResolver.js +90 -0
- package/dist/core/assetResolver/cdn/cloudinaryCdnResolver.d.ts +1 -0
- package/dist/core/assetResolver/cdn/cloudinaryCdnResolver.js +120 -0
- package/dist/core/assetResolver/cdn/sanitizeFileName.d.ts +1 -0
- package/dist/core/assetResolver/cdn/sanitizeFileName.js +17 -0
- package/dist/core/assetResolver/cdn/uploadCareCdnResolver.d.ts +1 -0
- package/dist/core/assetResolver/cdn/uploadCareCdnResolver.js +137 -0
- package/dist/core/assetResolver/cdn/validateCdnUrl.d.ts +1 -0
- package/dist/core/assetResolver/cdn/validateCdnUrl.js +8 -0
- package/dist/core/assetResolver/contants.d.ts +1 -0
- package/dist/core/assetResolver/contants.js +1 -0
- package/dist/core/assetResolver/index.d.ts +5 -0
- package/dist/core/assetResolver/index.js +5 -0
- package/dist/core/assetResolver/url/authorUrlResolver.d.ts +3 -0
- package/dist/core/assetResolver/url/authorUrlResolver.js +10 -0
- package/dist/core/assetResolver/url/imageUrlResolver.d.ts +4 -0
- package/dist/core/{resolveImageUrl.js → assetResolver/url/imageUrlResolver.js} +1 -1
- package/dist/core/assetResolver/url/messageUrlResolver.d.ts +3 -0
- package/dist/core/{urlResolver.js → assetResolver/url/messageUrlResolver.js} +10 -41
- package/dist/core/assetResolver/url/urlResolver.d.ts +3 -0
- package/dist/core/assetResolver/url/urlResolver.js +24 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/renderers/json/json.js +3 -4
- package/dist/utils/sleep.d.ts +1 -0
- package/dist/utils/sleep.js +3 -0
- package/package.json +2 -2
- package/dist/core/cdnResolver.d.ts +0 -5
- package/dist/core/cdnResolver.js +0 -213
- package/dist/core/resolveImageUrl.d.ts +0 -4
- package/dist/core/urlResolver.d.ts +0 -5
- /package/dist/core/{imageToBase64.d.ts → assetResolver/base64/imageToBase64.d.ts} +0 -0
- /package/dist/core/{limiter.d.ts → assetResolver/limiter.d.ts} +0 -0
- /package/dist/core/{limiter.js → assetResolver/limiter.js} +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { CustomWarn } from 'discord-message-transcript-base';
|
|
2
|
-
import { getBase64Limiter } from '
|
|
2
|
+
import { getBase64Limiter } from '../limiter.js';
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import { createLookup } from '@/networkSecurity';
|
|
6
|
+
import { USER_AGENT } from '../contants.js';
|
|
6
7
|
const MAX_BYTES = 25 * 1024 * 1024; // 25MB
|
|
7
8
|
export async function imageToBase64(safeUrlObject, disableWarnings) {
|
|
8
9
|
const url = safeUrlObject.url;
|
|
@@ -12,7 +13,7 @@ export async function imageToBase64(safeUrlObject, disableWarnings) {
|
|
|
12
13
|
const client = url.startsWith('https') ? https : http;
|
|
13
14
|
const lookup = createLookup(safeUrlObject.safeIps);
|
|
14
15
|
const request = client.get(url, {
|
|
15
|
-
headers: { "User-Agent":
|
|
16
|
+
headers: { "User-Agent": USER_AGENT },
|
|
16
17
|
lookup: lookup
|
|
17
18
|
}, (response) => {
|
|
18
19
|
if (response.statusCode !== 200) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class CDNProviderError extends Error {
|
|
2
|
+
provider: "CLOUDINARY" | "UPLOADCARE" | "CUSTOM";
|
|
3
|
+
code: string;
|
|
4
|
+
status?: number;
|
|
5
|
+
hint?: string;
|
|
6
|
+
errorMessage?: string;
|
|
7
|
+
constructor(opts: {
|
|
8
|
+
provider: "CLOUDINARY" | "UPLOADCARE" | "CUSTOM";
|
|
9
|
+
message: string;
|
|
10
|
+
code: string;
|
|
11
|
+
status?: number;
|
|
12
|
+
hint?: string;
|
|
13
|
+
errorMessage?: string;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
export declare function warnCdnError(provider: string, url: string, err: any, disableWarnings: boolean): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CustomWarn } from "discord-message-transcript-base";
|
|
2
|
+
export class CDNProviderError extends Error {
|
|
3
|
+
provider;
|
|
4
|
+
code;
|
|
5
|
+
status;
|
|
6
|
+
hint;
|
|
7
|
+
errorMessage;
|
|
8
|
+
constructor(opts) {
|
|
9
|
+
super(opts.message);
|
|
10
|
+
this.provider = opts.provider;
|
|
11
|
+
this.code = opts.code;
|
|
12
|
+
this.status = opts.status;
|
|
13
|
+
this.hint = opts.hint;
|
|
14
|
+
this.errorMessage = opts.errorMessage;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function warnCdnError(provider, url, err, disableWarnings) {
|
|
18
|
+
if (err instanceof CDNProviderError) {
|
|
19
|
+
CustomWarn(`[CDN:${err.provider}] Upload failed → fallback to original URL
|
|
20
|
+
URL: ${url}
|
|
21
|
+
Reason: ${err.message}
|
|
22
|
+
Code: ${err.code}${err.status ? ` (HTTP ${err.status})` : ""}${err.hint ? `\nHint: ${err.hint}` : ""} ${err.errorMessage ? `\nError Message: ${err.errorMessage}` : ""}`, disableWarnings);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
CustomWarn(`[CDN:${provider}] Unknown error → fallback to original URL
|
|
26
|
+
URL: ${url}
|
|
27
|
+
Error: ${err?.message ?? err}`, disableWarnings);
|
|
28
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { CDNOptions, safeUrlReturn } from "@/types/types.js";
|
|
2
|
+
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
+
export declare function cdnResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions): Promise<string>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { CustomWarn } from "discord-message-transcript-base";
|
|
2
|
+
import { getCDNLimiter } from "../limiter.js";
|
|
3
|
+
import { createLookup } from "@/networkSecurity";
|
|
4
|
+
import https from 'https';
|
|
5
|
+
import http from 'http';
|
|
6
|
+
import { uploadCareCdnResolver } from "./uploadCareCdnResolver.js";
|
|
7
|
+
import { cloudinaryCdnResolver } from "./cloudinaryCdnResolver.js";
|
|
8
|
+
import { validateCdnUrl } from "./validateCdnUrl.js";
|
|
9
|
+
import { USER_AGENT } from "../contants.js";
|
|
10
|
+
export async function cdnResolver(safeUrlObject, options, cdnOptions) {
|
|
11
|
+
const url = safeUrlObject.url;
|
|
12
|
+
const limit = getCDNLimiter();
|
|
13
|
+
return limit(async () => {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const client = safeUrlObject.url.startsWith('https') ? https : http;
|
|
16
|
+
const lookup = createLookup(safeUrlObject.safeIps);
|
|
17
|
+
const request = client.get(url, {
|
|
18
|
+
headers: { "User-Agent": USER_AGENT },
|
|
19
|
+
lookup: lookup
|
|
20
|
+
}, async (response) => {
|
|
21
|
+
if (response.statusCode !== 200) {
|
|
22
|
+
response.destroy();
|
|
23
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
24
|
+
Failed to fetch attachment with status code: ${response.statusCode} from ${safeUrlObject.url}.`, options.disableWarnings);
|
|
25
|
+
return resolve(url);
|
|
26
|
+
}
|
|
27
|
+
const contentType = response.headers["content-type"];
|
|
28
|
+
const splitContentType = contentType ? contentType?.split('/') : [];
|
|
29
|
+
if (!contentType || splitContentType.length != 2 || splitContentType[0].length == 0 || splitContentType[1].length == 0) {
|
|
30
|
+
response.destroy();
|
|
31
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
32
|
+
Failed to receive a valid content-type from ${url}.`, options.disableWarnings);
|
|
33
|
+
return resolve(url);
|
|
34
|
+
}
|
|
35
|
+
response.destroy();
|
|
36
|
+
const isImage = contentType.startsWith('image/') && contentType !== 'image/gif';
|
|
37
|
+
const isAudio = contentType.startsWith('audio/');
|
|
38
|
+
const isVideo = contentType.startsWith('video/') || contentType === 'image/gif';
|
|
39
|
+
if ((cdnOptions.includeImage && isImage) ||
|
|
40
|
+
(cdnOptions.includeAudio && isAudio) ||
|
|
41
|
+
(cdnOptions.includeVideo && isVideo) ||
|
|
42
|
+
(cdnOptions.includeOthers && !isAudio && !isImage && !isVideo)) {
|
|
43
|
+
return resolve(await cdnRedirectType(url, options, contentType, cdnOptions));
|
|
44
|
+
}
|
|
45
|
+
return resolve(url);
|
|
46
|
+
});
|
|
47
|
+
request.on('error', (err) => {
|
|
48
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
49
|
+
Error: ${err.message}`, options.disableWarnings);
|
|
50
|
+
return resolve(url);
|
|
51
|
+
});
|
|
52
|
+
request.setTimeout(15000, () => {
|
|
53
|
+
request.destroy();
|
|
54
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
55
|
+
Request timeout for ${url}.`, options.disableWarnings);
|
|
56
|
+
return resolve(url);
|
|
57
|
+
});
|
|
58
|
+
request.end();
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
async function cdnRedirectType(url, options, contentType, cdnOptions) {
|
|
63
|
+
let newUrl;
|
|
64
|
+
switch (cdnOptions.provider) {
|
|
65
|
+
case "CUSTOM": {
|
|
66
|
+
try {
|
|
67
|
+
newUrl = await cdnOptions.resolver(url, contentType, cdnOptions.customData);
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
CustomWarn(`Custom CDN resolver threw an error. Falling back to original URL.
|
|
72
|
+
This is most likely an issue in the custom CDN implementation provided by the user.
|
|
73
|
+
URL: ${url}
|
|
74
|
+
Error: ${error?.message ?? error}`, options.disableWarnings);
|
|
75
|
+
return url;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
case "CLOUDINARY": {
|
|
79
|
+
newUrl = await cloudinaryCdnResolver(url, options.fileName, cdnOptions.cloudName, cdnOptions.apiKey, cdnOptions.apiSecret, options.disableWarnings);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
case "UPLOADCARE": {
|
|
83
|
+
newUrl = await uploadCareCdnResolver(url, cdnOptions.publicKey, cdnOptions.cdnDomain, options.disableWarnings);
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (validateCdnUrl(newUrl, options.disableWarnings))
|
|
88
|
+
return newUrl;
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function cloudinaryCdnResolver(url: string, fileName: string, cloudName: string, apiKey: string, apiSecret: string, disableWarnings: boolean): Promise<string>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { CDNProviderError, warnCdnError } from './cdnCustomError.js';
|
|
3
|
+
import { sanitizeFileName } from './sanitizeFileName.js';
|
|
4
|
+
import { USER_AGENT } from '../contants.js';
|
|
5
|
+
// https://cloudinary.com/documentation/upload_images
|
|
6
|
+
export async function cloudinaryCdnResolver(url, fileName, cloudName, apiKey, apiSecret, disableWarnings) {
|
|
7
|
+
try {
|
|
8
|
+
if (!cloudName || !apiKey || !apiSecret) {
|
|
9
|
+
throw new CDNProviderError({
|
|
10
|
+
provider: "CLOUDINARY",
|
|
11
|
+
code: "CONFIG_MISSING",
|
|
12
|
+
message: "Cloudinary configuration is missing required fields.",
|
|
13
|
+
hint: "Verify cloudName, apiKey and apiSecret."
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
const paramsToSign = {
|
|
17
|
+
folder: `discord-message-transcript/${sanitizeFileName(fileName)}`,
|
|
18
|
+
timestamp: Math.floor(Date.now() / 1000).toString(),
|
|
19
|
+
unique_filename: "true",
|
|
20
|
+
use_filename: "true",
|
|
21
|
+
};
|
|
22
|
+
const stringToSign = Object.keys(paramsToSign).sort().map(k => `${k}=${paramsToSign[k]}`).join("&");
|
|
23
|
+
// signature SHA256
|
|
24
|
+
const signature = crypto
|
|
25
|
+
.createHash("sha256")
|
|
26
|
+
.update(stringToSign + apiSecret)
|
|
27
|
+
.digest("hex");
|
|
28
|
+
const form = new FormData();
|
|
29
|
+
form.append("folder", paramsToSign.folder);
|
|
30
|
+
form.append("file", url);
|
|
31
|
+
form.append("api_key", apiKey);
|
|
32
|
+
form.append("timestamp", paramsToSign.timestamp);
|
|
33
|
+
form.append("signature", signature);
|
|
34
|
+
form.append("use_filename", paramsToSign.use_filename);
|
|
35
|
+
form.append("unique_filename", paramsToSign.unique_filename);
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await fetch(`https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
body: form,
|
|
41
|
+
headers: { "User-Agent": USER_AGENT }
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
catch (networkErr) {
|
|
45
|
+
throw new CDNProviderError({
|
|
46
|
+
provider: "CLOUDINARY",
|
|
47
|
+
code: "NETWORK_ERROR",
|
|
48
|
+
message: "Network error while contacting Cloudinary.",
|
|
49
|
+
hint: "Check internet connection, DNS, firewall or proxy.",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
let body = {};
|
|
54
|
+
try {
|
|
55
|
+
body = await res.json();
|
|
56
|
+
}
|
|
57
|
+
catch { } // It isn't a problem body be empty
|
|
58
|
+
switch (res.status) {
|
|
59
|
+
case 400:
|
|
60
|
+
throw new CDNProviderError({
|
|
61
|
+
provider: "CLOUDINARY",
|
|
62
|
+
code: "BAD_REQUEST",
|
|
63
|
+
status: res.status,
|
|
64
|
+
message: body?.error?.message ?? "Invalid upload parameters.",
|
|
65
|
+
hint: "Check folder name, file URL accessibility, and signature.",
|
|
66
|
+
errorMessage: body.error.message ? body.error.message : undefined
|
|
67
|
+
});
|
|
68
|
+
case 401:
|
|
69
|
+
case 403:
|
|
70
|
+
throw new CDNProviderError({
|
|
71
|
+
provider: "CLOUDINARY",
|
|
72
|
+
code: "INVALID_CREDENTIALS",
|
|
73
|
+
status: res.status,
|
|
74
|
+
message: "Cloudinary rejected credentials.",
|
|
75
|
+
hint: "Check apiKey/apiSecret and cloudName.",
|
|
76
|
+
errorMessage: body.error.message ? body.error.message : undefined
|
|
77
|
+
});
|
|
78
|
+
case 420:
|
|
79
|
+
throw new CDNProviderError({
|
|
80
|
+
provider: "CLOUDINARY",
|
|
81
|
+
code: "RATE_LIMIT",
|
|
82
|
+
status: res.status,
|
|
83
|
+
message: "Cloudinary rate limit exceeded.",
|
|
84
|
+
hint: "Reduce concurrency.",
|
|
85
|
+
errorMessage: body.error.message ? body.error.message : undefined
|
|
86
|
+
});
|
|
87
|
+
case 500:
|
|
88
|
+
throw new CDNProviderError({
|
|
89
|
+
provider: "CLOUDINARY",
|
|
90
|
+
code: "CLOUDINARY_INTERNAL_ERROR",
|
|
91
|
+
status: res.status,
|
|
92
|
+
message: "Cloudinary has a internal error.",
|
|
93
|
+
hint: "Contact support or check https://status.cloudinary.com.",
|
|
94
|
+
errorMessage: body.error.message ? body.error.message : undefined
|
|
95
|
+
});
|
|
96
|
+
default:
|
|
97
|
+
throw new CDNProviderError({
|
|
98
|
+
provider: "CLOUDINARY",
|
|
99
|
+
code: "HTTP_ERROR",
|
|
100
|
+
status: res.status,
|
|
101
|
+
message: `Unexpected Cloudinary response.`,
|
|
102
|
+
errorMessage: body.error.message ? body.error.message : undefined
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const json = await res.json();
|
|
107
|
+
if (!json.secure_url) {
|
|
108
|
+
throw new CDNProviderError({
|
|
109
|
+
provider: "CLOUDINARY",
|
|
110
|
+
code: "INVALID_RESPONSE",
|
|
111
|
+
message: "Cloudinary response missing secure_url."
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return json.secure_url;
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
warnCdnError("CLOUDINARY", url, error, disableWarnings);
|
|
118
|
+
return url;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sanitizeFileName(fileName: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
const INVALID_REGEX = /[<>:"/\\|?*\x00-\x1F]|[^\p{L}\p{N}._ -]/gu;
|
|
3
|
+
const MAX_LENGTH = 100;
|
|
4
|
+
export function sanitizeFileName(fileName) {
|
|
5
|
+
fileName = fileName
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(INVALID_REGEX, "")
|
|
8
|
+
.replace(/\s+/g, " ");
|
|
9
|
+
fileName = fileName.replace(/[. ]+$/, "");
|
|
10
|
+
if (fileName.length > MAX_LENGTH) {
|
|
11
|
+
fileName = fileName.slice(0, MAX_LENGTH);
|
|
12
|
+
}
|
|
13
|
+
if (!fileName.length) {
|
|
14
|
+
fileName = `fallbackFile-${randomUUID()}`;
|
|
15
|
+
}
|
|
16
|
+
return fileName;
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function uploadCareCdnResolver(url: string, publicKey: string, cdnDomain: string, disableWarnings: boolean): Promise<string>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { sleep } from "@/utils/sleep.js";
|
|
2
|
+
import { CDNProviderError, warnCdnError } from "./cdnCustomError.js";
|
|
3
|
+
import { USER_AGENT } from "../contants.js";
|
|
4
|
+
// https://uploadcare.com/api-refs/upload-api/#tag/Upload/operation/fromURLUpload
|
|
5
|
+
export async function uploadCareCdnResolver(url, publicKey, cdnDomain, disableWarnings) {
|
|
6
|
+
try {
|
|
7
|
+
if (!publicKey || !cdnDomain) {
|
|
8
|
+
throw new CDNProviderError({
|
|
9
|
+
provider: "UPLOADCARE",
|
|
10
|
+
code: "CONFIG_MISSING",
|
|
11
|
+
message: "Uploadcare configuration is missing required fields.",
|
|
12
|
+
hint: "Verify cdnDomain and publickey."
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
const form = new FormData();
|
|
16
|
+
form.append("pub_key", publicKey);
|
|
17
|
+
form.append("source_url", url);
|
|
18
|
+
form.append("store", "1");
|
|
19
|
+
form.append("check_URL_duplicates", "1");
|
|
20
|
+
form.append("save_URL_duplicates", "1");
|
|
21
|
+
let res;
|
|
22
|
+
try {
|
|
23
|
+
res = await fetch("https://upload.uploadcare.com/from_url/", {
|
|
24
|
+
method: "POST",
|
|
25
|
+
body: form,
|
|
26
|
+
headers: { "User-Agent": USER_AGENT }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
throw new CDNProviderError({
|
|
31
|
+
provider: "UPLOADCARE",
|
|
32
|
+
code: "NETWORK_ERROR",
|
|
33
|
+
message: "Network error while contacting Uploadcare.",
|
|
34
|
+
hint: "Check DNS, firewall or internet connection."
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
if (!res.ok) {
|
|
38
|
+
let body = {};
|
|
39
|
+
try {
|
|
40
|
+
body = await res.text(); // Uploadcare use text/plain for error messages
|
|
41
|
+
}
|
|
42
|
+
catch { } // It isn't a problem body be empty
|
|
43
|
+
switch (res.status) {
|
|
44
|
+
case 400:
|
|
45
|
+
throw new CDNProviderError({
|
|
46
|
+
provider: "UPLOADCARE",
|
|
47
|
+
code: "BAD_REQUEST",
|
|
48
|
+
status: 400,
|
|
49
|
+
message: "Uploadcare rejected parameters.",
|
|
50
|
+
hint: "Check publickKey, file URL accessibility, and signature.",
|
|
51
|
+
errorMessage: body ?? undefined
|
|
52
|
+
});
|
|
53
|
+
case 403:
|
|
54
|
+
throw new CDNProviderError({
|
|
55
|
+
provider: "UPLOADCARE",
|
|
56
|
+
code: "INVALID_KEY",
|
|
57
|
+
status: 403,
|
|
58
|
+
message: "Uploadcare rejected public key.",
|
|
59
|
+
hint: "Verify public key and project settings.",
|
|
60
|
+
errorMessage: body ?? undefined
|
|
61
|
+
});
|
|
62
|
+
case 429:
|
|
63
|
+
throw new CDNProviderError({
|
|
64
|
+
provider: "UPLOADCARE",
|
|
65
|
+
code: "RATE_LIMIT",
|
|
66
|
+
status: 429,
|
|
67
|
+
message: "Uploadcare rate limit exceeded.",
|
|
68
|
+
hint: "Reduce concurrency.",
|
|
69
|
+
errorMessage: body ?? undefined
|
|
70
|
+
});
|
|
71
|
+
case 500:
|
|
72
|
+
throw new CDNProviderError({
|
|
73
|
+
provider: "UPLOADCARE",
|
|
74
|
+
code: "UPLOADCARE_INTERNAL",
|
|
75
|
+
status: res.status,
|
|
76
|
+
message: "Uploadcare internal error.",
|
|
77
|
+
hint: "Check https://status.uploadcare.com",
|
|
78
|
+
errorMessage: body ?? undefined
|
|
79
|
+
});
|
|
80
|
+
default:
|
|
81
|
+
throw new CDNProviderError({
|
|
82
|
+
provider: "UPLOADCARE",
|
|
83
|
+
code: "HTTP_ERROR",
|
|
84
|
+
status: res.status,
|
|
85
|
+
message: "Unexpected Uploadcare response.",
|
|
86
|
+
errorMessage: body ?? undefined
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const json = await res.json();
|
|
91
|
+
if (json.uuid) {
|
|
92
|
+
return `https://${cdnDomain}/${json.uuid}/`;
|
|
93
|
+
}
|
|
94
|
+
if (!json.token) {
|
|
95
|
+
throw new CDNProviderError({
|
|
96
|
+
provider: "UPLOADCARE",
|
|
97
|
+
code: "INVALID_RESPONSE",
|
|
98
|
+
message: "Uploadcare response missing uuid/token."
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
let delay = 200;
|
|
102
|
+
let maxDelay = 2000;
|
|
103
|
+
for (let i = 0; i < 10; i++) {
|
|
104
|
+
await sleep(delay);
|
|
105
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
106
|
+
const resToken = await fetch(`https://upload.uploadcare.com/from_url/status/?token=${json.token}&pub_key=${publicKey}`, { headers: { "User-Agent": USER_AGENT } });
|
|
107
|
+
if (!resToken.ok) {
|
|
108
|
+
throw new CDNProviderError({
|
|
109
|
+
provider: "UPLOADCARE",
|
|
110
|
+
code: "STATUS_ERROR",
|
|
111
|
+
status: resToken.status,
|
|
112
|
+
message: "Uploadcare status endpoint failed."
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const jsonToken = await resToken.json();
|
|
116
|
+
if (jsonToken.status === "success" && jsonToken.file_id) {
|
|
117
|
+
return `https://${cdnDomain}/${jsonToken.file_id}/`;
|
|
118
|
+
}
|
|
119
|
+
if (jsonToken.status === "error") {
|
|
120
|
+
throw new CDNProviderError({
|
|
121
|
+
provider: "UPLOADCARE",
|
|
122
|
+
code: "UPLOAD_FAILED",
|
|
123
|
+
message: jsonToken.error || "Uploadcare processing failed."
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw new CDNProviderError({
|
|
128
|
+
provider: "UPLOADCARE",
|
|
129
|
+
code: "TIMEOUT",
|
|
130
|
+
message: "Uploadcare polling timeout."
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
warnCdnError("UPLOADCARE", url, error, disableWarnings);
|
|
135
|
+
return url;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function validateCdnUrl(url: string, disableWarnings: boolean): boolean;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { CustomWarn } from "discord-message-transcript-base";
|
|
2
|
+
export function validateCdnUrl(url, disableWarnings) {
|
|
3
|
+
if (url.includes('"') || url.includes('<') || url.includes('>')) {
|
|
4
|
+
CustomWarn(`Unsafe URL received from CDN, using fallback.\nURL: ${url}`, disableWarnings);
|
|
5
|
+
return false;
|
|
6
|
+
}
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const USER_AGENT = "discord-message-transcript";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const USER_AGENT = "discord-message-transcript";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { JsonAuthor, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
2
|
+
import { CDNOptions } from "@/types/types.js";
|
|
3
|
+
export declare function authorUrlResolver(authors: Map<string, JsonAuthor>, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonAuthor[]>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { imageUrlResolver } from "./imageUrlResolver.js";
|
|
2
|
+
import { urlResolver } from "./urlResolver.js";
|
|
3
|
+
export async function authorUrlResolver(authors, options, cdnOptions, urlCache) {
|
|
4
|
+
return await Promise.all(Array.from(authors.values()).map(async (author) => {
|
|
5
|
+
return {
|
|
6
|
+
...author,
|
|
7
|
+
avatarURL: await urlResolver((await imageUrlResolver(author.avatarURL, options, false)), options, cdnOptions, urlCache),
|
|
8
|
+
};
|
|
9
|
+
}));
|
|
10
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { JsonAttachment, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
2
|
+
import { safeUrlReturn } from "@/types";
|
|
3
|
+
export declare function imageUrlResolver(url: string, options: TranscriptOptionsBase, canReturnNull: false, attachments?: JsonAttachment[]): Promise<safeUrlReturn>;
|
|
4
|
+
export declare function imageUrlResolver(url: string | null, options: TranscriptOptionsBase, canReturnNull: true, attachments?: JsonAttachment[]): Promise<safeUrlReturn | null>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FALLBACK_PIXEL } from "discord-message-transcript-base";
|
|
2
2
|
import { isSafeForHTML } from "@/networkSecurity";
|
|
3
|
-
export async function
|
|
3
|
+
export async function imageUrlResolver(url, options, canReturnNull, attachments) {
|
|
4
4
|
if (!url)
|
|
5
5
|
return null;
|
|
6
6
|
// Resolve attachment:// references to actual attachment URL
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { CDNOptions } from "@/types/types.js";
|
|
2
|
+
import { JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
+
export declare function messagesUrlResolver(messages: JsonMessage[], options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonMessage[]>;
|
|
@@ -1,38 +1,15 @@
|
|
|
1
1
|
import { JsonComponentType } from "discord-message-transcript-base";
|
|
2
|
-
import {
|
|
3
|
-
import { imageToBase64 } from "./imageToBase64.js";
|
|
4
|
-
import { isJsonComponentInContainer } from "./componentToJson.js";
|
|
5
|
-
import { FALLBACK_PIXEL } from "discord-message-transcript-base";
|
|
6
|
-
import { resolveImageURL } from "./resolveImageUrl.js";
|
|
2
|
+
import { imageUrlResolver } from "./imageUrlResolver.js";
|
|
7
3
|
import { isSafeForHTML } from "@/networkSecurity";
|
|
8
|
-
|
|
9
|
-
|
|
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);
|
|
15
|
-
if (cache)
|
|
16
|
-
return await cache;
|
|
17
|
-
}
|
|
18
|
-
let returnUrl;
|
|
19
|
-
if (cdnOptions)
|
|
20
|
-
returnUrl = cdnResolver(safeUrlObject, options, cdnOptions);
|
|
21
|
-
else if (options.saveImages)
|
|
22
|
-
returnUrl = imageToBase64(safeUrlObject, options.disableWarnings);
|
|
23
|
-
if (returnUrl) {
|
|
24
|
-
urlCache.set(safeUrlObject.url, returnUrl);
|
|
25
|
-
return await returnUrl;
|
|
26
|
-
}
|
|
27
|
-
return safeUrlObject.url;
|
|
28
|
-
}
|
|
4
|
+
import { urlResolver } from "./urlResolver.js";
|
|
5
|
+
import { isJsonComponentInContainer } from "@/core/componentToJson.js";
|
|
29
6
|
export async function messagesUrlResolver(messages, options, cdnOptions, urlCache) {
|
|
30
7
|
return await Promise.all(messages.map(async (message) => {
|
|
31
8
|
// Needs to wait for resolve correct when used attachment://
|
|
32
9
|
const attachments = await Promise.all(message.attachments.map(async (attachment) => {
|
|
33
10
|
let safeUrlObject;
|
|
34
11
|
if (attachment.contentType?.startsWith("image/")) {
|
|
35
|
-
safeUrlObject = await
|
|
12
|
+
safeUrlObject = await imageUrlResolver(attachment.url, options, false, message.attachments);
|
|
36
13
|
}
|
|
37
14
|
else {
|
|
38
15
|
safeUrlObject = await isSafeForHTML(attachment.url, options);
|
|
@@ -43,10 +20,10 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
43
20
|
};
|
|
44
21
|
}));
|
|
45
22
|
const embedsPromise = Promise.all(message.embeds.map(async (embed) => {
|
|
46
|
-
const authorIconUrl = embed.author?.iconURL ? await
|
|
47
|
-
const footerIconUrl = embed.footer?.iconURL ? await
|
|
48
|
-
const imageUrl = embed.image?.url ? await
|
|
49
|
-
const thumbnailUrl = embed.thumbnail?.url ? await
|
|
23
|
+
const authorIconUrl = embed.author?.iconURL ? await imageUrlResolver(embed.author.iconURL, options, true, attachments) : null;
|
|
24
|
+
const footerIconUrl = embed.footer?.iconURL ? await imageUrlResolver(embed.footer.iconURL, options, true, attachments) : null;
|
|
25
|
+
const imageUrl = embed.image?.url ? await imageUrlResolver(embed.image.url, options, true, attachments) : null;
|
|
26
|
+
const thumbnailUrl = embed.thumbnail?.url ? await imageUrlResolver(embed.thumbnail.url, options, true, attachments) : null;
|
|
50
27
|
return {
|
|
51
28
|
...embed,
|
|
52
29
|
author: embed.author ? { ...embed.author, iconURL: authorIconUrl ? await urlResolver(authorIconUrl, options, cdnOptions, urlCache) : null } : null,
|
|
@@ -64,7 +41,7 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
64
41
|
accessory: {
|
|
65
42
|
...component.accessory,
|
|
66
43
|
media: {
|
|
67
|
-
url: await urlResolver((await
|
|
44
|
+
url: await urlResolver((await imageUrlResolver(component.accessory.media.url, options, false, attachments)), options, cdnOptions, urlCache),
|
|
68
45
|
}
|
|
69
46
|
}
|
|
70
47
|
};
|
|
@@ -76,7 +53,7 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
76
53
|
items: await Promise.all(component.items.map(async (item) => {
|
|
77
54
|
return {
|
|
78
55
|
...item,
|
|
79
|
-
media: { url: await urlResolver((await
|
|
56
|
+
media: { url: await urlResolver((await imageUrlResolver(item.media.url, options, false, attachments)), options, cdnOptions, urlCache) },
|
|
80
57
|
};
|
|
81
58
|
}))
|
|
82
59
|
};
|
|
@@ -110,11 +87,3 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
|
|
|
110
87
|
};
|
|
111
88
|
}));
|
|
112
89
|
}
|
|
113
|
-
export async function authorUrlResolver(authors, options, cdnOptions, urlCache) {
|
|
114
|
-
return await Promise.all(Array.from(authors.values()).map(async (author) => {
|
|
115
|
-
return {
|
|
116
|
-
...author,
|
|
117
|
-
avatarURL: await urlResolver((await resolveImageURL(author.avatarURL, options, false)), options, cdnOptions, urlCache),
|
|
118
|
-
};
|
|
119
|
-
}));
|
|
120
|
-
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { CDNOptions, safeUrlReturn } from "@/types/types.js";
|
|
2
|
+
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
+
export declare function urlResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<string>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FALLBACK_PIXEL } from "discord-message-transcript-base";
|
|
2
|
+
import { cdnResolver } from "../cdn/cdnResolver.js";
|
|
3
|
+
import { imageToBase64 } from "../base64/imageToBase64.js";
|
|
4
|
+
export async function urlResolver(safeUrlObject, options, cdnOptions, urlCache) {
|
|
5
|
+
if (safeUrlObject.safe == false)
|
|
6
|
+
return "";
|
|
7
|
+
if (safeUrlObject.url == FALLBACK_PIXEL)
|
|
8
|
+
return safeUrlObject.url;
|
|
9
|
+
if (urlCache.has(safeUrlObject.url)) {
|
|
10
|
+
const cache = urlCache.get(safeUrlObject.url);
|
|
11
|
+
if (cache)
|
|
12
|
+
return await cache;
|
|
13
|
+
}
|
|
14
|
+
let returnUrl;
|
|
15
|
+
if (cdnOptions)
|
|
16
|
+
returnUrl = cdnResolver(safeUrlObject, options, cdnOptions);
|
|
17
|
+
else if (options.saveImages)
|
|
18
|
+
returnUrl = imageToBase64(safeUrlObject, options.disableWarnings);
|
|
19
|
+
if (returnUrl) {
|
|
20
|
+
urlCache.set(safeUrlObject.url, returnUrl);
|
|
21
|
+
return await returnUrl;
|
|
22
|
+
}
|
|
23
|
+
return safeUrlObject.url;
|
|
24
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
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 '@/assetResolver';
|
|
4
4
|
import { TextBasedChannel } from "discord.js";
|
|
5
5
|
import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "@/types";
|
|
6
6
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { ReturnType } from "@/types";
|
|
2
2
|
export { ReturnFormat } from "discord-message-transcript-base";
|
|
3
|
-
export { setBase64Concurrency, setCDNConcurrency } from '@/
|
|
3
|
+
export { setBase64Concurrency, setCDNConcurrency } from '@/assetResolver';
|
|
4
4
|
import { AttachmentBuilder } from "discord.js";
|
|
5
5
|
import { Json } from "@/renderers/json/json.js";
|
|
6
6
|
import { fetchMessages } from "@/core/fetchMessages.js";
|
|
@@ -8,7 +8,7 @@ import { ReturnType } from "@/types";
|
|
|
8
8
|
import { output } from "@/core/output.js";
|
|
9
9
|
import { ReturnTypeBase, ReturnFormat, outputBase, CustomError, CustomWarn } from "discord-message-transcript-base";
|
|
10
10
|
import { returnTypeMapper } from "@/core/mappers.js";
|
|
11
|
-
import { authorUrlResolver, messagesUrlResolver } from "@/
|
|
11
|
+
import { authorUrlResolver, messagesUrlResolver } from "@/assetResolver";
|
|
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,5 @@
|
|
|
1
1
|
import { BaseGuildTextChannel, DMChannel } from "discord.js";
|
|
2
|
-
import { urlResolver } from "@/
|
|
3
|
-
import { resolveImageURL } from "@/core/resolveImageUrl.js";
|
|
2
|
+
import { imageUrlResolver, urlResolver } from "@/assetResolver";
|
|
4
3
|
export class Json {
|
|
5
4
|
guild;
|
|
6
5
|
channel;
|
|
@@ -44,9 +43,9 @@ export class Json {
|
|
|
44
43
|
async toJson() {
|
|
45
44
|
const channel = await this.channel.fetch();
|
|
46
45
|
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
|
|
46
|
+
const safeChannelImg = await imageUrlResolver(channelImg, this.options, true);
|
|
48
47
|
const guild = !channel.isDMBased() ? this.guild : null;
|
|
49
|
-
const guildIcon = guild ? await
|
|
48
|
+
const guildIcon = guild ? await imageUrlResolver(guild.iconURL(), this.options, true) : null;
|
|
50
49
|
const guildJson = !guild ? null : {
|
|
51
50
|
name: guild.name,
|
|
52
51
|
id: guild.id,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function sleep(ms: number): Promise<unknown>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discord-message-transcript",
|
|
3
|
-
"version": "1.3.1-dev.1.
|
|
3
|
+
"version": "1.3.1-dev.1.48",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://github.com/HenriqueMairesse/discord-message-transcript#readme",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"discord-message-transcript-base": "1.3.1-dev.1.
|
|
48
|
+
"discord-message-transcript-base": "1.3.1-dev.1.48"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"discord.js": ">=14.19.0 <15"
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { CDNOptions, safeUrlReturn } from "@/types";
|
|
2
|
-
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
-
export declare function cdnResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions): 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
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import { CustomWarn } from "discord-message-transcript-base";
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
import { getCDNLimiter } from "./limiter.js";
|
|
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;
|
|
9
|
-
const limit = getCDNLimiter();
|
|
10
|
-
return limit(async () => {
|
|
11
|
-
return new Promise((resolve, reject) => {
|
|
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
|
|
17
|
-
}, async (response) => {
|
|
18
|
-
if (response.statusCode !== 200) {
|
|
19
|
-
response.destroy();
|
|
20
|
-
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
21
|
-
Failed to fetch attachment with status code: ${response.statusCode} from ${safeUrlObject.url}.`, options.disableWarnings);
|
|
22
|
-
return resolve(url);
|
|
23
|
-
}
|
|
24
|
-
const contentType = response.headers["content-type"];
|
|
25
|
-
const splitContentType = contentType ? contentType?.split('/') : [];
|
|
26
|
-
if (!contentType || splitContentType.length != 2 || splitContentType[0].length == 0 || splitContentType[1].length == 0) {
|
|
27
|
-
response.destroy();
|
|
28
|
-
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
29
|
-
Failed to receive a valid content-type from ${url}.`, options.disableWarnings);
|
|
30
|
-
return resolve(url);
|
|
31
|
-
}
|
|
32
|
-
response.destroy();
|
|
33
|
-
const isImage = contentType.startsWith('image/') && contentType !== 'image/gif';
|
|
34
|
-
const isAudio = contentType.startsWith('audio/');
|
|
35
|
-
const isVideo = contentType.startsWith('video/') || contentType === 'image/gif';
|
|
36
|
-
if ((cdnOptions.includeImage && isImage) ||
|
|
37
|
-
(cdnOptions.includeAudio && isAudio) ||
|
|
38
|
-
(cdnOptions.includeVideo && isVideo) ||
|
|
39
|
-
(cdnOptions.includeOthers && !isAudio && !isImage && !isVideo)) {
|
|
40
|
-
return resolve(await cdnRedirectType(url, options, contentType, cdnOptions));
|
|
41
|
-
}
|
|
42
|
-
return resolve(url);
|
|
43
|
-
});
|
|
44
|
-
request.on('error', (err) => {
|
|
45
|
-
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
46
|
-
Error: ${err.message}`, options.disableWarnings);
|
|
47
|
-
return resolve(url);
|
|
48
|
-
});
|
|
49
|
-
request.setTimeout(15000, () => {
|
|
50
|
-
request.destroy();
|
|
51
|
-
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
52
|
-
Request timeout for ${url}.`, options.disableWarnings);
|
|
53
|
-
return resolve(url);
|
|
54
|
-
});
|
|
55
|
-
request.end();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
async function cdnRedirectType(url, options, contentType, cdnOptions) {
|
|
60
|
-
let newUrl;
|
|
61
|
-
switch (cdnOptions.provider) {
|
|
62
|
-
case "CUSTOM": {
|
|
63
|
-
try {
|
|
64
|
-
newUrl = await cdnOptions.resolver(url, contentType, cdnOptions.customData);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
CustomWarn(`Custom CDN resolver threw an error. Falling back to original URL.
|
|
69
|
-
This is most likely an issue in the custom CDN implementation provided by the user.
|
|
70
|
-
URL: ${url}
|
|
71
|
-
Error: ${error?.message ?? error}`, options.disableWarnings);
|
|
72
|
-
return url;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
case "CLOUDINARY": {
|
|
76
|
-
newUrl = await cloudinaryResolver(url, options.fileName, cdnOptions.cloudName, cdnOptions.apiKey, cdnOptions.apiSecret, options.disableWarnings);
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
case "UPLOADCARE": {
|
|
80
|
-
newUrl = await uploadCareResolver(url, cdnOptions.publicKey, cdnOptions.cdnDomain, options.disableWarnings);
|
|
81
|
-
break;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (validateCdnUrl(newUrl, options.disableWarnings))
|
|
85
|
-
return newUrl;
|
|
86
|
-
return url;
|
|
87
|
-
}
|
|
88
|
-
function sleep(ms) {
|
|
89
|
-
return new Promise(r => setTimeout(r, ms));
|
|
90
|
-
}
|
|
91
|
-
export async function uploadCareResolver(url, publicKey, cdnDomain, disableWarnings) {
|
|
92
|
-
try {
|
|
93
|
-
const form = new FormData();
|
|
94
|
-
form.append("pub_key", publicKey);
|
|
95
|
-
form.append("source_url", url);
|
|
96
|
-
form.append("store", "1");
|
|
97
|
-
form.append("check_URL_duplicates", "1");
|
|
98
|
-
form.append("save_URL_duplicates", "1");
|
|
99
|
-
const res = await fetch("https://upload.uploadcare.com/from_url/", {
|
|
100
|
-
method: "POST",
|
|
101
|
-
body: form,
|
|
102
|
-
headers: {
|
|
103
|
-
"User-Agent": "discord-message-transcript"
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
if (!res.ok) {
|
|
107
|
-
switch (res.status) {
|
|
108
|
-
case 400:
|
|
109
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status} - Request failed input parameters validation.`);
|
|
110
|
-
case 403:
|
|
111
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status} - Request was not allowed.`);
|
|
112
|
-
case 429:
|
|
113
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status} - Request was throttled.`);
|
|
114
|
-
default:
|
|
115
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const json = await res.json();
|
|
119
|
-
if (json.uuid) {
|
|
120
|
-
return `https://${cdnDomain}/${json.uuid}/`;
|
|
121
|
-
}
|
|
122
|
-
let delay = 200;
|
|
123
|
-
let maxDelay = 2000;
|
|
124
|
-
if (json.token) {
|
|
125
|
-
for (let i = 0; i < 10; i++) {
|
|
126
|
-
await sleep(delay);
|
|
127
|
-
delay = Math.min(delay * 2, maxDelay);
|
|
128
|
-
const resToken = await fetch(`https://upload.uploadcare.com/from_url/status/?token=${json.token}&pub_key=${publicKey}`, { headers: { "User-Agent": "discord-message-transcript" } });
|
|
129
|
-
if (!resToken.ok)
|
|
130
|
-
throw new Error(`Uploadcare status failed with status code ${resToken.status}`);
|
|
131
|
-
const jsonToken = await resToken.json();
|
|
132
|
-
if (jsonToken.status === "success" && jsonToken.file_id) {
|
|
133
|
-
return `https://${cdnDomain}/${jsonToken.file_id}/`;
|
|
134
|
-
}
|
|
135
|
-
if (jsonToken.status === "error") {
|
|
136
|
-
throw new Error(jsonToken.error || "Uploadcare failed");
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
throw new Error("Uploadcare polling timeout");
|
|
140
|
-
}
|
|
141
|
-
return url;
|
|
142
|
-
}
|
|
143
|
-
catch (error) {
|
|
144
|
-
CustomWarn(`Uploadcare CDN upload failed. Using original URL as fallback.
|
|
145
|
-
Check Uploadcare public key, CDN domain, project settings, rate limits, and network access.
|
|
146
|
-
URL: ${url}
|
|
147
|
-
Error: ${error?.message ?? error}`, disableWarnings);
|
|
148
|
-
return url;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
export async function cloudinaryResolver(url, fileName, cloudName, apiKey, apiSecret, disableWarnings) {
|
|
152
|
-
try {
|
|
153
|
-
const paramsToSign = {
|
|
154
|
-
folder: `discord-message-transcript/${fileName}`,
|
|
155
|
-
timestamp: Math.floor(Date.now() / 1000).toString(),
|
|
156
|
-
unique_filename: "true",
|
|
157
|
-
use_filename: "true",
|
|
158
|
-
};
|
|
159
|
-
const stringToSign = Object.keys(paramsToSign).sort().map(k => `${k}=${paramsToSign[k]}`).join("&");
|
|
160
|
-
// signature SHA256
|
|
161
|
-
const signature = crypto
|
|
162
|
-
.createHash("sha256")
|
|
163
|
-
.update(stringToSign + apiSecret)
|
|
164
|
-
.digest("hex");
|
|
165
|
-
const form = new FormData();
|
|
166
|
-
form.append("folder", paramsToSign.folder);
|
|
167
|
-
form.append("file", url);
|
|
168
|
-
form.append("api_key", apiKey);
|
|
169
|
-
form.append("timestamp", paramsToSign.timestamp);
|
|
170
|
-
form.append("signature", signature);
|
|
171
|
-
form.append("use_filename", paramsToSign.use_filename);
|
|
172
|
-
form.append("unique_filename", paramsToSign.unique_filename);
|
|
173
|
-
const res = await fetch(`https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, {
|
|
174
|
-
method: "POST",
|
|
175
|
-
body: form,
|
|
176
|
-
headers: {
|
|
177
|
-
"User-Agent": "discord-message-transcript"
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
if (!res.ok) {
|
|
181
|
-
switch (res.status) {
|
|
182
|
-
case 400:
|
|
183
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status} - Bad request / invalid params.`);
|
|
184
|
-
case 403:
|
|
185
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status} - Invalid credentials or unauthorized.`);
|
|
186
|
-
case 429:
|
|
187
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status} - Rate limited.`);
|
|
188
|
-
default:
|
|
189
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status}.`);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
const json = await res.json();
|
|
193
|
-
if (!json.secure_url) {
|
|
194
|
-
throw new Error("Cloudinary response missing secure_url");
|
|
195
|
-
}
|
|
196
|
-
return json.secure_url;
|
|
197
|
-
}
|
|
198
|
-
catch (error) {
|
|
199
|
-
CustomWarn(`Failed to upload asset to Cloudinary CDN. Using original URL as fallback.
|
|
200
|
-
Check Cloudinary configuration (cloud name, API key, API secret) and network access.
|
|
201
|
-
URL: ${url}
|
|
202
|
-
Error: ${error?.message ?? error}`, disableWarnings);
|
|
203
|
-
return url;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// Note: for debug use ${JSON.stringify(await res.json())} to understand the error
|
|
207
|
-
function validateCdnUrl(url, disableWarnings) {
|
|
208
|
-
if (url.includes('"') || url.includes('<') || url.includes('>')) {
|
|
209
|
-
CustomWarn(`Unsafe URL received from CDN, using fallback.\nURL: ${url}`, disableWarnings);
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
return true;
|
|
213
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
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>;
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
import { JsonAuthor, JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
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
|
-
export declare function messagesUrlResolver(messages: JsonMessage[], options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonMessage[]>;
|
|
5
|
-
export declare function authorUrlResolver(authors: Map<string, JsonAuthor>, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<JsonAuthor[]>;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|