discord-message-transcript 1.2.0-dev-next.0.20 → 1.2.0-dev.1.2.0.11.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 +1 -3
- package/dist/core/cdnResolver.js +39 -169
- package/dist/core/componentToJson.d.ts +1 -1
- package/dist/core/componentToJson.js +5 -5
- package/dist/core/fetchMessages.d.ts +1 -15
- package/dist/core/fetchMessages.js +10 -13
- package/dist/core/imageToBase64.js +32 -42
- package/dist/core/urlResolver.d.ts +1 -1
- package/dist/core/urlResolver.js +4 -14
- package/dist/index.d.ts +1 -2
- package/dist/index.js +5 -24
- package/dist/types/types.d.ts +14 -19
- package/dist/types/types.js +4 -0
- package/package.json +2 -5
- package/dist/core/limiter.d.ts +0 -5
- package/dist/core/limiter.js +0 -47
|
@@ -1,4 +1,2 @@
|
|
|
1
1
|
import { CDNOptions } from "../types/types.js";
|
|
2
|
-
export declare function cdnResolver(url: string, cdnOptions: CDNOptions): Promise<string>;
|
|
3
|
-
export declare function uploadCareResolver(url: string, publicKey: string): Promise<string>;
|
|
4
|
-
export declare function cloudinaryResolver(url: string, cloudName: string, apiKey: string, apiSecret: string): Promise<string>;
|
|
2
|
+
export declare function cdnResolver(url: string, cdnOptions: CDNOptions<unknown>): Promise<string>;
|
package/dist/core/cdnResolver.js
CHANGED
|
@@ -1,186 +1,56 @@
|
|
|
1
1
|
import https from 'https';
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import { CustomWarn } from "discord-message-transcript-base";
|
|
4
|
-
import crypto from 'crypto';
|
|
5
|
-
import { getCDNLimiter } from "./limiter.js";
|
|
6
4
|
export async function cdnResolver(url, cdnOptions) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const request = client.request(url, {
|
|
12
|
-
method: 'HEAD',
|
|
13
|
-
headers: { "User-Agent": "discord-message-transcript" }
|
|
14
|
-
}, async (response) => {
|
|
15
|
-
if (response.statusCode !== 200) {
|
|
16
|
-
response.destroy();
|
|
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}.`);
|
|
19
|
-
return resolve(url);
|
|
20
|
-
}
|
|
21
|
-
const contentType = response.headers["content-type"];
|
|
22
|
-
const splitContentType = contentType ? contentType?.split('/') : [];
|
|
23
|
-
if (!contentType || splitContentType.length != 2 || splitContentType[0].length == 0 || splitContentType[1].length == 0) {
|
|
24
|
-
response.destroy();
|
|
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}.`);
|
|
27
|
-
return resolve(url);
|
|
28
|
-
}
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const client = url.startsWith('https') ? https : http;
|
|
7
|
+
const request = client.get(url, { headers: { "User-Agent": "discord-message-transcript" } }, async (response) => {
|
|
8
|
+
if (response.statusCode !== 200) {
|
|
29
9
|
response.destroy();
|
|
30
|
-
|
|
31
|
-
const isAudio = contentType.startsWith('audio/');
|
|
32
|
-
const isVideo = contentType.startsWith('video/') || contentType === 'image/gif';
|
|
33
|
-
if ((cdnOptions.includeImage && isImage) ||
|
|
34
|
-
(cdnOptions.includeAudio && isAudio) ||
|
|
35
|
-
(cdnOptions.includeVideo && isVideo) ||
|
|
36
|
-
(cdnOptions.includeOthers && !isAudio && !isImage && !isVideo)) {
|
|
37
|
-
return resolve(await cdnRedirectType(url, contentType, cdnOptions));
|
|
38
|
-
}
|
|
10
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.\nFailed to fetch attachment with status code: ${response.statusCode} from ${url}.`);
|
|
39
11
|
return resolve(url);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
12
|
+
}
|
|
13
|
+
const contentType = response.headers["content-type"];
|
|
14
|
+
const splitContentType = contentType ? contentType?.split('/') : [];
|
|
15
|
+
if (!contentType || splitContentType.length != 2 || splitContentType[0].length == 0 || splitContentType[1].length == 0) {
|
|
16
|
+
response.destroy();
|
|
17
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.\nFailed to receive a valid content-type from ${url}.`);
|
|
44
18
|
return resolve(url);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
19
|
+
}
|
|
20
|
+
response.destroy();
|
|
21
|
+
const isImage = contentType.startsWith('image/') && contentType !== 'image/gif';
|
|
22
|
+
const isAudio = contentType.startsWith('audio/');
|
|
23
|
+
const isVideo = contentType.startsWith('video/') || contentType === 'image/gif';
|
|
24
|
+
if ((cdnOptions.includeImage && isImage) ||
|
|
25
|
+
(cdnOptions.includeAudio && isAudio) ||
|
|
26
|
+
(cdnOptions.includeVideo && isVideo) ||
|
|
27
|
+
(cdnOptions.includeOthers && !isAudio && !isImage && !isVideo)) {
|
|
28
|
+
return resolve(await cdnRedirectType(url, contentType, cdnOptions));
|
|
29
|
+
}
|
|
30
|
+
return resolve(url);
|
|
53
31
|
});
|
|
32
|
+
request.on('error', (err) => {
|
|
33
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.\nError message: ${err.message}`);
|
|
34
|
+
return resolve(url);
|
|
35
|
+
});
|
|
36
|
+
request.setTimeout(15000, () => {
|
|
37
|
+
request.destroy();
|
|
38
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.\nRequest timeout for ${url}. Using original URL.`);
|
|
39
|
+
resolve(url);
|
|
40
|
+
});
|
|
41
|
+
request.end();
|
|
54
42
|
});
|
|
55
43
|
}
|
|
56
44
|
async function cdnRedirectType(url, contentType, cdnOptions) {
|
|
57
|
-
switch (cdnOptions.
|
|
45
|
+
switch (cdnOptions.type) {
|
|
58
46
|
case "CUSTOM": {
|
|
59
|
-
|
|
60
|
-
return await cdnOptions.resolver(url, contentType, cdnOptions.customData);
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
CustomWarn(`Custom CDN resolver threw an error. Falling back to original URL.
|
|
64
|
-
This is most likely an issue in the custom CDN implementation provided by the user.
|
|
65
|
-
URL: ${url}
|
|
66
|
-
Error: ${error?.message ?? error}`);
|
|
67
|
-
return url;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
case "CLOUDINARY": {
|
|
71
|
-
return await cloudinaryResolver(url, cdnOptions.cloudName, cdnOptions.apiKey, cdnOptions.apiSecret);
|
|
72
|
-
;
|
|
47
|
+
return await cdnOptions.customCdnResolver(url, contentType, cdnOptions.other);
|
|
73
48
|
}
|
|
74
|
-
case "
|
|
75
|
-
return await
|
|
49
|
+
case "CLOUDFLARE": {
|
|
50
|
+
return await cdnCloudflare();
|
|
76
51
|
}
|
|
77
52
|
}
|
|
78
53
|
}
|
|
79
|
-
function
|
|
80
|
-
return
|
|
81
|
-
}
|
|
82
|
-
export async function uploadCareResolver(url, publicKey) {
|
|
83
|
-
try {
|
|
84
|
-
const form = new FormData();
|
|
85
|
-
form.append("pub_key", publicKey);
|
|
86
|
-
form.append("source_url", url);
|
|
87
|
-
form.append("store", "1");
|
|
88
|
-
form.append("check_URL_duplicates", "1");
|
|
89
|
-
form.append("save_URL_duplicates", "1");
|
|
90
|
-
const res = await fetch("https://upload.uploadcare.com/from_url/", {
|
|
91
|
-
method: "POST",
|
|
92
|
-
body: form,
|
|
93
|
-
headers: {
|
|
94
|
-
"User-Agent": "discord-message-transcript"
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
if (!res.ok) {
|
|
98
|
-
switch (res.status) {
|
|
99
|
-
case 400:
|
|
100
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status} - Request failed input parameters validation.`);
|
|
101
|
-
case 403:
|
|
102
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status} - Request was not allowed.`);
|
|
103
|
-
case 429:
|
|
104
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status} - Request was throttled.`);
|
|
105
|
-
default:
|
|
106
|
-
throw new Error(`Uploadcare initial request failed with status code ${res.status}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const json = await res.json();
|
|
110
|
-
if (json.uuid) {
|
|
111
|
-
return `https://ucarecdn.com/${json.uuid}/`;
|
|
112
|
-
}
|
|
113
|
-
if (json.token) {
|
|
114
|
-
for (let i = 0; i < 10; i++) {
|
|
115
|
-
await sleep(500);
|
|
116
|
-
const resToken = await fetch(`https://upload.uploadcare.com/from_url/status/?token=${json.token}&pub_key=${publicKey}`, { headers: { "User-Agent": "discord-message-transcript" } });
|
|
117
|
-
if (!resToken.ok)
|
|
118
|
-
throw new Error(`Uploadcare status failed with status code ${resToken.status}`);
|
|
119
|
-
const jsonToken = await resToken.json();
|
|
120
|
-
if (jsonToken.status === "success" && jsonToken.file_id) {
|
|
121
|
-
return `https://ucarecdn.com/${jsonToken.file_id}/`;
|
|
122
|
-
}
|
|
123
|
-
if (jsonToken.status === "error") {
|
|
124
|
-
throw new Error(jsonToken.error || "Uploadcare failed");
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
throw new Error("Uploadcare polling timeout");
|
|
128
|
-
}
|
|
129
|
-
return url;
|
|
130
|
-
}
|
|
131
|
-
catch (error) {
|
|
132
|
-
CustomWarn(`Uploadcare CDN upload failed. Using original URL as fallback.
|
|
133
|
-
Check Uploadcare public key, project settings, rate limits, and network access.
|
|
134
|
-
URL: ${url}
|
|
135
|
-
Error: ${error?.message ?? error}`);
|
|
136
|
-
return url;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
export async function cloudinaryResolver(url, cloudName, apiKey, apiSecret) {
|
|
140
|
-
try {
|
|
141
|
-
const timestamp = Math.floor(Date.now() / 1000);
|
|
142
|
-
// signature SHA1
|
|
143
|
-
const signature = crypto
|
|
144
|
-
.createHash("sha1")
|
|
145
|
-
.update(`timestamp=${timestamp}${apiSecret}`)
|
|
146
|
-
.digest("hex");
|
|
147
|
-
const form = new FormData();
|
|
148
|
-
form.append("file", url);
|
|
149
|
-
form.append("api_key", apiKey);
|
|
150
|
-
form.append("timestamp", timestamp.toString());
|
|
151
|
-
form.append("signature", signature);
|
|
152
|
-
form.append("use_filename", "true");
|
|
153
|
-
form.append("unique_filename", "false");
|
|
154
|
-
const res = await fetch(`https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, {
|
|
155
|
-
method: "POST",
|
|
156
|
-
body: form,
|
|
157
|
-
headers: {
|
|
158
|
-
"User-Agent": "discord-message-transcript"
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
if (!res.ok) {
|
|
162
|
-
switch (res.status) {
|
|
163
|
-
case 400:
|
|
164
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status} - Bad request / invalid params.`);
|
|
165
|
-
case 403:
|
|
166
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status} - Invalid credentials or unauthorized.`);
|
|
167
|
-
case 429:
|
|
168
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status} - Rate limited.`);
|
|
169
|
-
default:
|
|
170
|
-
throw new Error(`Cloudinary upload failed with status code ${res.status}`);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
const json = await res.json();
|
|
174
|
-
if (!json.secure_url) {
|
|
175
|
-
throw new Error("Cloudinary response missing secure_url");
|
|
176
|
-
}
|
|
177
|
-
return json.secure_url;
|
|
178
|
-
}
|
|
179
|
-
catch (error) {
|
|
180
|
-
CustomWarn(`Failed to upload asset to Cloudinary CDN. Using original URL as fallback.
|
|
181
|
-
Check Cloudinary configuration (cloud name, API key, API secret) and network access.
|
|
182
|
-
URL: ${url}
|
|
183
|
-
Error: ${error?.message ?? error}`);
|
|
184
|
-
return url;
|
|
185
|
-
}
|
|
54
|
+
async function cdnCloudflare() {
|
|
55
|
+
return ''; // TODO: Implement cloudflare cdn
|
|
186
56
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { TopLevelComponent } from "discord.js";
|
|
2
2
|
import { JsonTopLevelComponent, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
3
|
import { CDNOptions } from "../types/types.js";
|
|
4
|
-
export declare function componentsToJson(components: TopLevelComponent[], options: TranscriptOptionsBase, cdnOptions: CDNOptions | null
|
|
4
|
+
export declare function componentsToJson(components: TopLevelComponent[], options: TranscriptOptionsBase, cdnOptions: CDNOptions<unknown> | null): Promise<JsonTopLevelComponent[]>;
|
|
@@ -2,7 +2,7 @@ import { ComponentType } from "discord.js";
|
|
|
2
2
|
import { mapButtonStyle, mapSelectorType, mapSeparatorSpacing } from "./mappers.js";
|
|
3
3
|
import { JsonComponentType } from "discord-message-transcript-base";
|
|
4
4
|
import { urlResolver } from "./urlResolver.js";
|
|
5
|
-
export async function componentsToJson(components, options, cdnOptions
|
|
5
|
+
export async function componentsToJson(components, options, cdnOptions) {
|
|
6
6
|
const processedComponents = await Promise.all(components.filter(component => !(!options.includeV2Components && component.type != ComponentType.ActionRow))
|
|
7
7
|
.map(async (component) => {
|
|
8
8
|
switch (component.type) {
|
|
@@ -54,7 +54,7 @@ export async function componentsToJson(components, options, cdnOptions, urlCache
|
|
|
54
54
|
}
|
|
55
55
|
case ComponentType.Container: {
|
|
56
56
|
const newOptions = { ...options, includeComponents: true, includeButtons: true };
|
|
57
|
-
const componentsJson = await componentsToJson(component.components, newOptions, cdnOptions
|
|
57
|
+
const componentsJson = await componentsToJson(component.components, newOptions, cdnOptions);
|
|
58
58
|
return {
|
|
59
59
|
type: JsonComponentType.Container,
|
|
60
60
|
components: componentsJson.filter(isJsonComponentInContainer), // Input components that are container-safe must always produce container-safe output.
|
|
@@ -67,14 +67,14 @@ export async function componentsToJson(components, options, cdnOptions, urlCache
|
|
|
67
67
|
type: JsonComponentType.File,
|
|
68
68
|
fileName: component.data.name ?? null,
|
|
69
69
|
size: component.data.size ?? 0,
|
|
70
|
-
url: await urlResolver(component.file.url, options, cdnOptions
|
|
70
|
+
url: await urlResolver(component.file.url, options, cdnOptions),
|
|
71
71
|
spoiler: component.spoiler,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
case ComponentType.MediaGallery: {
|
|
75
75
|
const mediaItems = await Promise.all(component.items.map(async (item) => {
|
|
76
76
|
return {
|
|
77
|
-
media: { url: await urlResolver(item.media.url, options, cdnOptions
|
|
77
|
+
media: { url: await urlResolver(item.media.url, options, cdnOptions) },
|
|
78
78
|
spoiler: item.spoiler,
|
|
79
79
|
};
|
|
80
80
|
}));
|
|
@@ -99,7 +99,7 @@ export async function componentsToJson(components, options, cdnOptions, urlCache
|
|
|
99
99
|
accessoryJson = {
|
|
100
100
|
type: JsonComponentType.Thumbnail,
|
|
101
101
|
media: {
|
|
102
|
-
url: await urlResolver(component.accessory.media.url, options, cdnOptions
|
|
102
|
+
url: await urlResolver(component.accessory.media.url, options, cdnOptions),
|
|
103
103
|
},
|
|
104
104
|
spoiler: component.accessory.spoiler,
|
|
105
105
|
};
|
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
import { TextBasedChannel } from "discord.js";
|
|
2
2
|
import { JsonAuthor, JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
3
|
import { CDNOptions, MapMentions } from "../types/types.js";
|
|
4
|
-
export declare function fetchMessages(
|
|
4
|
+
export declare function fetchMessages(channel: TextBasedChannel, options: TranscriptOptionsBase, cdnOptions: CDNOptions<unknown> | null, authors: Map<string, JsonAuthor>, mentions: MapMentions, after?: string): Promise<{
|
|
5
5
|
messages: JsonMessage[];
|
|
6
6
|
end: boolean;
|
|
7
|
-
newLastMessageId: string | undefined;
|
|
8
7
|
}>;
|
|
9
|
-
export type FetchMessagesContext = {
|
|
10
|
-
channel: TextBasedChannel;
|
|
11
|
-
options: TranscriptOptionsBase;
|
|
12
|
-
cdnOptions: CDNOptions | null;
|
|
13
|
-
transcriptState: TranscriptState;
|
|
14
|
-
lastMessageId: string | undefined;
|
|
15
|
-
};
|
|
16
|
-
type TranscriptState = {
|
|
17
|
-
authors: Map<string, JsonAuthor>;
|
|
18
|
-
mentions: MapMentions;
|
|
19
|
-
urlCache: Map<string, Promise<string>>;
|
|
20
|
-
};
|
|
21
|
-
export {};
|
|
@@ -2,10 +2,8 @@ import { EmbedType } from "discord.js";
|
|
|
2
2
|
import { componentsToJson } from "./componentToJson.js";
|
|
3
3
|
import { urlResolver } from "./urlResolver.js";
|
|
4
4
|
import { getMentions } from "./getMentions.js";
|
|
5
|
-
export async function fetchMessages(
|
|
6
|
-
const
|
|
7
|
-
const { authors, mentions, urlCache } = transcriptState;
|
|
8
|
-
const originalMessages = await channel.messages.fetch({ limit: 100, cache: false, before: lastMessageId });
|
|
5
|
+
export async function fetchMessages(channel, options, cdnOptions, authors, mentions, after) {
|
|
6
|
+
const originalMessages = await channel.messages.fetch({ limit: 100, cache: false, after: after });
|
|
9
7
|
const rawMessages = await Promise.all(originalMessages.map(async (message) => {
|
|
10
8
|
const attachments = await Promise.all(message.attachments.map(async (attachment) => {
|
|
11
9
|
return {
|
|
@@ -13,17 +11,17 @@ export async function fetchMessages(ctx) {
|
|
|
13
11
|
name: attachment.name,
|
|
14
12
|
size: attachment.size,
|
|
15
13
|
spoiler: attachment.spoiler,
|
|
16
|
-
url: await urlResolver(attachment.url, options, cdnOptions
|
|
14
|
+
url: await urlResolver(attachment.url, options, cdnOptions),
|
|
17
15
|
};
|
|
18
16
|
}));
|
|
19
17
|
// This only works because embeds with the type poll_result that are send when a poll end are marked as a message send by the system
|
|
20
18
|
const embeds = message.system && message.embeds.length == 1 && message.embeds[0].data.type == EmbedType.PollResult && !options.includePolls ? []
|
|
21
19
|
: await Promise.all(message.embeds.map(async (embed) => {
|
|
22
20
|
const [authorIcon, thumbnailUrl, imageUrl, footerIcon] = await Promise.all([
|
|
23
|
-
embed.author?.iconURL ? urlResolver(embed.author.iconURL, options, cdnOptions
|
|
24
|
-
embed.thumbnail?.url ? urlResolver(embed.thumbnail.url, options, cdnOptions
|
|
25
|
-
embed.image?.url ? urlResolver(embed.image.url, options, cdnOptions
|
|
26
|
-
embed.footer?.iconURL ? urlResolver(embed.footer.iconURL, options, cdnOptions
|
|
21
|
+
embed.author?.iconURL ? urlResolver(embed.author.iconURL, options, cdnOptions) : Promise.resolve(null),
|
|
22
|
+
embed.thumbnail?.url ? urlResolver(embed.thumbnail.url, options, cdnOptions) : Promise.resolve(null),
|
|
23
|
+
embed.image?.url ? urlResolver(embed.image.url, options, cdnOptions) : Promise.resolve(null),
|
|
24
|
+
embed.footer?.iconURL ? urlResolver(embed.footer.iconURL, options, cdnOptions) : Promise.resolve(null),
|
|
27
25
|
]);
|
|
28
26
|
return {
|
|
29
27
|
author: embed.author ? { name: embed.author.name, url: embed.author.url ?? null, iconURL: authorIcon } : null,
|
|
@@ -41,7 +39,7 @@ export async function fetchMessages(ctx) {
|
|
|
41
39
|
}));
|
|
42
40
|
if (!authors.has(message.author.id)) {
|
|
43
41
|
authors.set(message.author.id, {
|
|
44
|
-
avatarURL: await urlResolver(message.author.displayAvatarURL(), options, cdnOptions
|
|
42
|
+
avatarURL: await urlResolver(message.author.displayAvatarURL(), options, cdnOptions),
|
|
45
43
|
bot: message.author.bot,
|
|
46
44
|
displayName: message.author.displayName,
|
|
47
45
|
guildTag: message.author.primaryGuild?.tag ?? null,
|
|
@@ -53,7 +51,7 @@ export async function fetchMessages(ctx) {
|
|
|
53
51
|
system: message.author.system,
|
|
54
52
|
});
|
|
55
53
|
}
|
|
56
|
-
const components = await componentsToJson(message.components, options, cdnOptions
|
|
54
|
+
const components = await componentsToJson(message.components, options, cdnOptions);
|
|
57
55
|
await getMentions(message, mentions);
|
|
58
56
|
return {
|
|
59
57
|
attachments: options.includeAttachments ? attachments : [],
|
|
@@ -91,10 +89,9 @@ export async function fetchMessages(ctx) {
|
|
|
91
89
|
system: message.system,
|
|
92
90
|
};
|
|
93
91
|
}));
|
|
94
|
-
const newLastMessageId = originalMessages.last()?.id;
|
|
95
92
|
const messages = rawMessages.filter(m => !(!options.includeEmpty && m.attachments.length == 0 && m.components.length == 0 && m.content == "" && m.embeds.length == 0 && !m.poll));
|
|
96
93
|
const end = originalMessages.size !== 100;
|
|
97
|
-
return { messages, end
|
|
94
|
+
return { messages, end };
|
|
98
95
|
}
|
|
99
96
|
function formatTimeLeftPoll(timestamp) {
|
|
100
97
|
const now = new Date();
|
|
@@ -1,53 +1,43 @@
|
|
|
1
1
|
import https from 'https';
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import { CustomWarn } from 'discord-message-transcript-base';
|
|
4
|
-
import { getBase64Limiter } from './limiter.js';
|
|
5
4
|
export async function imageToBase64(url) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const contentType = response.headers['content-type'];
|
|
18
|
-
if (!contentType || !contentType.startsWith('image/') || contentType === 'image/gif') {
|
|
19
|
-
response.destroy();
|
|
20
|
-
return resolve(url);
|
|
21
|
-
}
|
|
22
|
-
const chunks = [];
|
|
23
|
-
response.on('data', (chunk) => {
|
|
24
|
-
chunks.push(chunk);
|
|
25
|
-
});
|
|
26
|
-
response.on('end', () => {
|
|
27
|
-
const buffer = Buffer.concat(chunks);
|
|
28
|
-
const base64 = buffer.toString('base64');
|
|
29
|
-
resolve(`data:${contentType};base64,${base64}`);
|
|
30
|
-
});
|
|
31
|
-
response.on('error', (err) => {
|
|
32
|
-
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
33
|
-
Stream error while fetching from ${url}.
|
|
34
|
-
Error: ${err.message}`);
|
|
35
|
-
resolve(url);
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
request.on('error', (err) => {
|
|
39
|
-
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
40
|
-
Error fetching image from ${url}
|
|
41
|
-
Error: ${err.message}`);
|
|
5
|
+
return new Promise((resolve, reject) => {
|
|
6
|
+
const client = url.startsWith('https') ? https : http;
|
|
7
|
+
const request = client.get(url, { headers: { "User-Agent": "discord-message-transcript" } }, (response) => {
|
|
8
|
+
if (response.statusCode !== 200) {
|
|
9
|
+
response.destroy();
|
|
10
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.\nFailed to fetch image with status code: ${response.statusCode} from ${url}.`);
|
|
11
|
+
return resolve(url);
|
|
12
|
+
}
|
|
13
|
+
const contentType = response.headers['content-type'];
|
|
14
|
+
if (!contentType || !contentType.startsWith('image/') || contentType === 'image/gif') {
|
|
15
|
+
response.destroy();
|
|
42
16
|
return resolve(url);
|
|
17
|
+
}
|
|
18
|
+
const chunks = [];
|
|
19
|
+
response.on('data', (chunk) => {
|
|
20
|
+
chunks.push(chunk);
|
|
21
|
+
});
|
|
22
|
+
response.on('end', () => {
|
|
23
|
+
const buffer = Buffer.concat(chunks);
|
|
24
|
+
const base64 = buffer.toString('base64');
|
|
25
|
+
resolve(`data:${contentType};base64,${base64}`);
|
|
43
26
|
});
|
|
44
|
-
|
|
45
|
-
|
|
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}.`);
|
|
27
|
+
response.on('error', (err) => {
|
|
28
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.\nStream error while fetching from ${url}.\nError message: ${err.message}`);
|
|
48
29
|
resolve(url);
|
|
49
30
|
});
|
|
50
|
-
request.end();
|
|
51
31
|
});
|
|
32
|
+
request.on('error', (err) => {
|
|
33
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.\nError fetching image from ${url}\nError message: ${err.message}`);
|
|
34
|
+
return resolve(url);
|
|
35
|
+
});
|
|
36
|
+
request.setTimeout(15000, () => {
|
|
37
|
+
request.destroy();
|
|
38
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.\nRequest timeout for ${url}. Using original URL.`);
|
|
39
|
+
resolve(url);
|
|
40
|
+
});
|
|
41
|
+
request.end();
|
|
52
42
|
});
|
|
53
43
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
2
2
|
import { CDNOptions } from "../types/types.js";
|
|
3
|
-
export declare function urlResolver(url: string, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null
|
|
3
|
+
export declare function urlResolver(url: string, options: TranscriptOptionsBase, cdnOptions: CDNOptions<unknown> | null): Promise<string>;
|
package/dist/core/urlResolver.js
CHANGED
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
import { cdnResolver } from "./cdnResolver.js";
|
|
2
2
|
import { imageToBase64 } from "./imageToBase64.js";
|
|
3
|
-
export async function urlResolver(url, options, cdnOptions
|
|
4
|
-
if (urlCache.has(url)) {
|
|
5
|
-
const cache = urlCache.get(url);
|
|
6
|
-
if (cache)
|
|
7
|
-
return await cache;
|
|
8
|
-
}
|
|
9
|
-
let returnUrl;
|
|
3
|
+
export async function urlResolver(url, options, cdnOptions) {
|
|
10
4
|
if (cdnOptions)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (returnUrl) {
|
|
15
|
-
urlCache.set(url, returnUrl);
|
|
16
|
-
return await returnUrl;
|
|
17
|
-
}
|
|
5
|
+
return await cdnResolver(url, cdnOptions);
|
|
6
|
+
if (options.saveImages)
|
|
7
|
+
return await imageToBase64(url);
|
|
18
8
|
return url;
|
|
19
9
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType, CDNOptions, MimeType } from "./types/types.js";
|
|
1
|
+
export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType, CDNOptions, CDNType, MimeType } from "./types/types.js";
|
|
2
2
|
export { ReturnFormat, LocalDate, TimeZone } from "discord-message-transcript-base";
|
|
3
|
-
export { setBase64Concurrency, setCDNConcurrency } from './core/limiter.js';
|
|
4
3
|
import { TextBasedChannel } from "discord.js";
|
|
5
4
|
import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "./types/types.js";
|
|
6
5
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export { ReturnType } from "./types/types.js";
|
|
1
|
+
export { ReturnType, CDNType } from "./types/types.js";
|
|
2
2
|
export { ReturnFormat } from "discord-message-transcript-base";
|
|
3
|
-
export { setBase64Concurrency, setCDNConcurrency } from './core/limiter.js';
|
|
4
3
|
import { AttachmentBuilder } from "discord.js";
|
|
5
4
|
import { Json } from "./renderers/json/json.js";
|
|
6
5
|
import { fetchMessages } from "./core/fetchMessages.js";
|
|
@@ -48,42 +47,24 @@ export async function createTranscript(channel, options = {}) {
|
|
|
48
47
|
watermark
|
|
49
48
|
};
|
|
50
49
|
const jsonTranscript = channel.isDMBased() ? new Json(null, channel, internalOptions) : new Json(channel.guild, channel, internalOptions);
|
|
50
|
+
let lastMessageID;
|
|
51
51
|
const authors = new Map();
|
|
52
52
|
const mentions = {
|
|
53
53
|
channels: new Map(),
|
|
54
54
|
roles: new Map(),
|
|
55
55
|
users: new Map(),
|
|
56
56
|
};
|
|
57
|
-
const urlCache = new Map();
|
|
58
|
-
const fetchMessageParameter = {
|
|
59
|
-
channel: channel,
|
|
60
|
-
options: internalOptions,
|
|
61
|
-
cdnOptions: options.cdnOptions ?? null,
|
|
62
|
-
transcriptState: {
|
|
63
|
-
authors: authors,
|
|
64
|
-
mentions: mentions,
|
|
65
|
-
urlCache: urlCache
|
|
66
|
-
},
|
|
67
|
-
lastMessageId: undefined
|
|
68
|
-
};
|
|
69
57
|
while (true) {
|
|
70
|
-
const { messages, end
|
|
58
|
+
const { messages, end } = await fetchMessages(channel, internalOptions, options.cdnOptions ?? null, authors, mentions, lastMessageID);
|
|
71
59
|
jsonTranscript.addMessages(messages);
|
|
72
|
-
|
|
60
|
+
lastMessageID = messages[messages.length - 1]?.id;
|
|
73
61
|
if (end || (jsonTranscript.messages.length >= quantity && quantity != 0)) {
|
|
74
62
|
break;
|
|
75
63
|
}
|
|
76
64
|
}
|
|
77
|
-
|
|
78
|
-
if (quantity > 0 && jsonTranscript.messages.length > quantity) {
|
|
79
|
-
jsonTranscript.sliceMessages(quantity);
|
|
80
|
-
}
|
|
65
|
+
jsonTranscript.sliceMessages(quantity);
|
|
81
66
|
jsonTranscript.setAuthors(Array.from(authors.values()));
|
|
82
|
-
authors.clear(); // Clean cache
|
|
83
67
|
jsonTranscript.setMentions({ channels: Array.from(mentions.channels.values()), roles: Array.from(mentions.roles.values()), users: Array.from(mentions.users.values()) });
|
|
84
|
-
mentions.channels.clear(); // Clean cache
|
|
85
|
-
mentions.roles.clear(); // Clean cache
|
|
86
|
-
mentions.users.clear(); // Clean cache
|
|
87
68
|
const result = await output(await jsonTranscript.toJson());
|
|
88
69
|
if (!options.returnType || options.returnType == "attachment") {
|
|
89
70
|
if (!(result instanceof Buffer)) {
|
package/dist/types/types.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface TranscriptOptions<T extends ReturnType, Other = unknown> {
|
|
|
37
37
|
/**
|
|
38
38
|
* CDN Options
|
|
39
39
|
*/
|
|
40
|
-
cdnOptions: CDNOptions
|
|
40
|
+
cdnOptions: CDNOptions<Other>;
|
|
41
41
|
/**
|
|
42
42
|
* The name of the file to be created.
|
|
43
43
|
* Default depends if is DM or Guild
|
|
@@ -139,26 +139,21 @@ export interface MapMentions {
|
|
|
139
139
|
roles: Map<string, JsonMessageMentionsRoles>;
|
|
140
140
|
users: Map<string, JsonMessageMentionsUsers>;
|
|
141
141
|
}
|
|
142
|
+
export declare const CDNType: {
|
|
143
|
+
readonly CUSTOM: "CUSTOM";
|
|
144
|
+
readonly CLOUDFLARE: "CLOUDFLARE";
|
|
145
|
+
};
|
|
146
|
+
export type CDNType = typeof CDNType[keyof typeof CDNType];
|
|
142
147
|
export type MimeType = `${string}/${string}`;
|
|
143
|
-
export type
|
|
148
|
+
export type CDNOptions<Other> = (Partial<{
|
|
144
149
|
includeAudio: boolean;
|
|
145
150
|
includeImage: boolean;
|
|
146
151
|
includeVideo: boolean;
|
|
147
152
|
includeOthers: boolean;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
};
|
|
155
|
-
export type CDNOptionsCloudinary = {
|
|
156
|
-
provider: "CLOUDINARY";
|
|
157
|
-
cloudName: string;
|
|
158
|
-
apiKey: string;
|
|
159
|
-
apiSecret: string;
|
|
160
|
-
};
|
|
161
|
-
export type CDNOptionsUploadcare = {
|
|
162
|
-
provider: "UPLOADCARE";
|
|
163
|
-
publicKey: string;
|
|
164
|
-
};
|
|
153
|
+
}>) & ({
|
|
154
|
+
type: Exclude<CDNType, "CUSTOM">;
|
|
155
|
+
} | {
|
|
156
|
+
type: "CUSTOM";
|
|
157
|
+
customCdnResolver: (url: string, contentType: MimeType | null, other: Other) => Promise<string> | string;
|
|
158
|
+
other: Other;
|
|
159
|
+
});
|
package/dist/types/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discord-message-transcript",
|
|
3
|
-
"version": "1.2.0-dev
|
|
3
|
+
"version": "1.2.0-dev.1.2.0.11.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,14 +48,11 @@
|
|
|
48
48
|
"typescript": "^5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"discord-message-transcript-base": "1.2.0-dev
|
|
51
|
+
"discord-message-transcript-base": "1.2.0-dev.1.2.0.11.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"discord.js": ">=14.19.0 <15"
|
|
55
55
|
},
|
|
56
|
-
"publishConfig": {
|
|
57
|
-
"access": "public"
|
|
58
|
-
},
|
|
59
56
|
"scripts": {
|
|
60
57
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
61
58
|
"build": "tsc"
|
package/dist/core/limiter.d.ts
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
export declare function getCDNLimiter(): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
2
|
-
export declare function getBase64Limiter(): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
3
|
-
export declare function setCDNConcurrency(n: number): void;
|
|
4
|
-
export declare function setBase64Concurrency(n: number): void;
|
|
5
|
-
export declare function createLimiter(concurrency: number): <T>(fn: () => Promise<T>) => Promise<T>;
|
package/dist/core/limiter.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
const globalLimiters = {
|
|
2
|
-
cdn: createLimiter(12),
|
|
3
|
-
base64: createLimiter(6)
|
|
4
|
-
};
|
|
5
|
-
export function getCDNLimiter() {
|
|
6
|
-
return globalLimiters.cdn;
|
|
7
|
-
}
|
|
8
|
-
export function getBase64Limiter() {
|
|
9
|
-
return globalLimiters.base64;
|
|
10
|
-
}
|
|
11
|
-
export function setCDNConcurrency(n) {
|
|
12
|
-
globalLimiters.cdn = createLimiter(n);
|
|
13
|
-
}
|
|
14
|
-
export function setBase64Concurrency(n) {
|
|
15
|
-
globalLimiters.base64 = createLimiter(n);
|
|
16
|
-
}
|
|
17
|
-
export function createLimiter(concurrency) {
|
|
18
|
-
if (concurrency <= 0) {
|
|
19
|
-
throw new Error("Limiter must be greater than 0");
|
|
20
|
-
}
|
|
21
|
-
let active = 0;
|
|
22
|
-
const queue = [];
|
|
23
|
-
const next = () => {
|
|
24
|
-
active = Math.max(0, active - 1);
|
|
25
|
-
if (queue.length > 0 && active < concurrency) {
|
|
26
|
-
const run = queue.shift();
|
|
27
|
-
run?.();
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
return function limit(fn) {
|
|
31
|
-
return new Promise((resolve, reject) => {
|
|
32
|
-
const run = () => {
|
|
33
|
-
active++;
|
|
34
|
-
Promise.resolve()
|
|
35
|
-
.then(fn)
|
|
36
|
-
.then(resolve, reject)
|
|
37
|
-
.finally(next);
|
|
38
|
-
};
|
|
39
|
-
if (active < concurrency) {
|
|
40
|
-
run();
|
|
41
|
-
}
|
|
42
|
-
else {
|
|
43
|
-
queue.push(run);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
};
|
|
47
|
-
}
|