discord-message-transcript 1.2.0-dev.1.2.0.12.0 → 1.2.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/README.md +1 -0
- package/dist/core/cdnResolver.d.ts +4 -1
- package/dist/core/cdnResolver.js +182 -41
- package/dist/core/componentToJson.d.ts +3 -3
- package/dist/core/componentToJson.js +8 -9
- package/dist/core/fetchMessages.d.ts +14 -2
- package/dist/core/fetchMessages.js +19 -22
- package/dist/core/imageToBase64.js +42 -32
- package/dist/core/limiter.d.ts +5 -0
- package/dist/core/limiter.js +47 -0
- package/dist/core/urlResolver.d.ts +4 -2
- package/dist/core/urlResolver.js +96 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +55 -11
- package/dist/renderers/json/json.d.ts +12 -7
- package/dist/renderers/json/json.js +17 -4
- package/dist/types/types.d.ts +151 -36
- package/dist/types/types.js +18 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
import { CDNOptions } from "../types/types.js";
|
|
2
|
-
|
|
2
|
+
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
+
export declare function cdnResolver(url: string, options: TranscriptOptionsBase, cdnOptions: CDNOptions): Promise<string>;
|
|
4
|
+
export declare function uploadCareResolver(url: string, publicKey: string, cdnDomain: string): Promise<string>;
|
|
5
|
+
export declare function cloudinaryResolver(url: string, fileName: string, cloudName: string, apiKey: string, apiSecret: string): Promise<string>;
|
package/dist/core/cdnResolver.js
CHANGED
|
@@ -1,56 +1,197 @@
|
|
|
1
1
|
import https from 'https';
|
|
2
2
|
import http from 'http';
|
|
3
3
|
import { CustomWarn } from "discord-message-transcript-base";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
import crypto from 'crypto';
|
|
5
|
+
import { getCDNLimiter } from "./limiter.js";
|
|
6
|
+
export async function cdnResolver(url, options, cdnOptions) {
|
|
7
|
+
const limit = getCDNLimiter();
|
|
8
|
+
return limit(async () => {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
const client = url.startsWith('https') ? https : http;
|
|
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
|
+
}
|
|
9
29
|
response.destroy();
|
|
10
|
-
|
|
30
|
+
const isImage = contentType.startsWith('image/') && contentType !== 'image/gif';
|
|
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, options, contentType, cdnOptions));
|
|
38
|
+
}
|
|
11
39
|
return resolve(url);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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}.`);
|
|
40
|
+
});
|
|
41
|
+
request.on('error', (err) => {
|
|
42
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
43
|
+
Error: ${err.message}`);
|
|
18
44
|
return resolve(url);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
(cdnOptions.includeOthers && !isAudio && !isImage && !isVideo)) {
|
|
28
|
-
return resolve(await cdnRedirectType(url, contentType, cdnOptions));
|
|
29
|
-
}
|
|
30
|
-
return resolve(url);
|
|
45
|
+
});
|
|
46
|
+
request.setTimeout(15000, () => {
|
|
47
|
+
request.destroy();
|
|
48
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of uploading to CDN.
|
|
49
|
+
Request timeout for ${url}.`);
|
|
50
|
+
resolve(url);
|
|
51
|
+
});
|
|
52
|
+
request.end();
|
|
31
53
|
});
|
|
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();
|
|
42
54
|
});
|
|
43
55
|
}
|
|
44
|
-
async function cdnRedirectType(url, contentType, cdnOptions) {
|
|
45
|
-
switch (cdnOptions.
|
|
56
|
+
async function cdnRedirectType(url, options, contentType, cdnOptions) {
|
|
57
|
+
switch (cdnOptions.provider) {
|
|
46
58
|
case "CUSTOM": {
|
|
47
|
-
|
|
59
|
+
try {
|
|
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, options.fileName, cdnOptions.cloudName, cdnOptions.apiKey, cdnOptions.apiSecret);
|
|
72
|
+
;
|
|
48
73
|
}
|
|
49
|
-
case "
|
|
50
|
-
return await
|
|
74
|
+
case "UPLOADCARE": {
|
|
75
|
+
return await uploadCareResolver(url, cdnOptions.publicKey, cdnOptions.cdnDomain);
|
|
51
76
|
}
|
|
52
77
|
}
|
|
53
78
|
}
|
|
54
|
-
|
|
55
|
-
return
|
|
79
|
+
function sleep(ms) {
|
|
80
|
+
return new Promise(r => setTimeout(r, ms));
|
|
81
|
+
}
|
|
82
|
+
export async function uploadCareResolver(url, publicKey, cdnDomain) {
|
|
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://${cdnDomain}/${json.uuid}/`;
|
|
112
|
+
}
|
|
113
|
+
let delay = 200;
|
|
114
|
+
let maxDelay = 2000;
|
|
115
|
+
if (json.token) {
|
|
116
|
+
for (let i = 0; i < 10; i++) {
|
|
117
|
+
await sleep(delay);
|
|
118
|
+
delay = Math.min(delay * 2, maxDelay);
|
|
119
|
+
const resToken = await fetch(`https://upload.uploadcare.com/from_url/status/?token=${json.token}&pub_key=${publicKey}`, { headers: { "User-Agent": "discord-message-transcript" } });
|
|
120
|
+
if (!resToken.ok)
|
|
121
|
+
throw new Error(`Uploadcare status failed with status code ${resToken.status}`);
|
|
122
|
+
const jsonToken = await resToken.json();
|
|
123
|
+
if (jsonToken.status === "success" && jsonToken.file_id) {
|
|
124
|
+
return `https://${cdnDomain}/${jsonToken.file_id}/`;
|
|
125
|
+
}
|
|
126
|
+
if (jsonToken.status === "error") {
|
|
127
|
+
throw new Error(jsonToken.error || "Uploadcare failed");
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
throw new Error("Uploadcare polling timeout");
|
|
131
|
+
}
|
|
132
|
+
return url;
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
CustomWarn(`Uploadcare CDN upload failed. Using original URL as fallback.
|
|
136
|
+
Check Uploadcare public key, CDN domain, project settings, rate limits, and network access.
|
|
137
|
+
URL: ${url}
|
|
138
|
+
Error: ${error?.message ?? error}`);
|
|
139
|
+
return url;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
export async function cloudinaryResolver(url, fileName, cloudName, apiKey, apiSecret) {
|
|
143
|
+
try {
|
|
144
|
+
const paramsToSign = {
|
|
145
|
+
folder: `discord-message-transcript/${fileName}`,
|
|
146
|
+
timestamp: Math.floor(Date.now() / 1000).toString(),
|
|
147
|
+
unique_filename: "true",
|
|
148
|
+
use_filename: "true",
|
|
149
|
+
};
|
|
150
|
+
const stringToSign = Object.keys(paramsToSign).sort().map(k => `${k}=${paramsToSign[k]}`).join("&");
|
|
151
|
+
// signature SHA1
|
|
152
|
+
const signature = crypto
|
|
153
|
+
.createHash("sha1")
|
|
154
|
+
.update(stringToSign + apiSecret)
|
|
155
|
+
.digest("hex");
|
|
156
|
+
const form = new FormData();
|
|
157
|
+
form.append("folder", paramsToSign.folder);
|
|
158
|
+
form.append("file", url);
|
|
159
|
+
form.append("api_key", apiKey);
|
|
160
|
+
form.append("timestamp", paramsToSign.timestamp);
|
|
161
|
+
form.append("signature", signature);
|
|
162
|
+
form.append("use_filename", paramsToSign.use_filename);
|
|
163
|
+
form.append("unique_filename", paramsToSign.unique_filename);
|
|
164
|
+
const res = await fetch(`https://api.cloudinary.com/v1_1/${cloudName}/auto/upload`, {
|
|
165
|
+
method: "POST",
|
|
166
|
+
body: form,
|
|
167
|
+
headers: {
|
|
168
|
+
"User-Agent": "discord-message-transcript"
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) {
|
|
172
|
+
switch (res.status) {
|
|
173
|
+
case 400:
|
|
174
|
+
throw new Error(`Cloudinary upload failed with status code ${res.status} - Bad request / invalid params.`);
|
|
175
|
+
case 403:
|
|
176
|
+
throw new Error(`Cloudinary upload failed with status code ${res.status} - Invalid credentials or unauthorized.`);
|
|
177
|
+
case 429:
|
|
178
|
+
throw new Error(`Cloudinary upload failed with status code ${res.status} - Rate limited.`);
|
|
179
|
+
default:
|
|
180
|
+
throw new Error(`Cloudinary upload failed with status code ${res.status}.`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const json = await res.json();
|
|
184
|
+
if (!json.secure_url) {
|
|
185
|
+
throw new Error("Cloudinary response missing secure_url");
|
|
186
|
+
}
|
|
187
|
+
return json.secure_url;
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
CustomWarn(`Failed to upload asset to Cloudinary CDN. Using original URL as fallback.
|
|
191
|
+
Check Cloudinary configuration (cloud name, API key, API secret) and network access.
|
|
192
|
+
URL: ${url}
|
|
193
|
+
Error: ${error?.message ?? error}`);
|
|
194
|
+
return url;
|
|
195
|
+
}
|
|
56
196
|
}
|
|
197
|
+
// Note: for debug use ${JSON.stringify(await res.json())} to understand the error
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { TopLevelComponent } from "discord.js";
|
|
2
|
-
import { JsonTopLevelComponent, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
-
|
|
4
|
-
export declare function
|
|
2
|
+
import { JsonTopLevelComponent, JsonComponentInContainer, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
+
export declare function componentsToJson(components: TopLevelComponent[], options: TranscriptOptionsBase): Promise<JsonTopLevelComponent[]>;
|
|
4
|
+
export declare function isJsonComponentInContainer(component: JsonTopLevelComponent): component is JsonComponentInContainer;
|
|
@@ -1,8 +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
|
-
|
|
5
|
-
export async function componentsToJson(components, options, cdnOptions) {
|
|
4
|
+
export async function componentsToJson(components, options) {
|
|
6
5
|
const processedComponents = await Promise.all(components.filter(component => !(!options.includeV2Components && component.type != ComponentType.ActionRow))
|
|
7
6
|
.map(async (component) => {
|
|
8
7
|
switch (component.type) {
|
|
@@ -54,7 +53,7 @@ export async function componentsToJson(components, options, cdnOptions) {
|
|
|
54
53
|
}
|
|
55
54
|
case ComponentType.Container: {
|
|
56
55
|
const newOptions = { ...options, includeComponents: true, includeButtons: true };
|
|
57
|
-
const componentsJson = await componentsToJson(component.components, newOptions
|
|
56
|
+
const componentsJson = await componentsToJson(component.components, newOptions);
|
|
58
57
|
return {
|
|
59
58
|
type: JsonComponentType.Container,
|
|
60
59
|
components: componentsJson.filter(isJsonComponentInContainer), // Input components that are container-safe must always produce container-safe output.
|
|
@@ -67,17 +66,17 @@ export async function componentsToJson(components, options, cdnOptions) {
|
|
|
67
66
|
type: JsonComponentType.File,
|
|
68
67
|
fileName: component.data.name ?? null,
|
|
69
68
|
size: component.data.size ?? 0,
|
|
70
|
-
url:
|
|
69
|
+
url: component.file.url,
|
|
71
70
|
spoiler: component.spoiler,
|
|
72
71
|
};
|
|
73
72
|
}
|
|
74
73
|
case ComponentType.MediaGallery: {
|
|
75
|
-
const mediaItems =
|
|
74
|
+
const mediaItems = component.items.map(item => {
|
|
76
75
|
return {
|
|
77
|
-
media: { url:
|
|
76
|
+
media: { url: item.media.url },
|
|
78
77
|
spoiler: item.spoiler,
|
|
79
78
|
};
|
|
80
|
-
})
|
|
79
|
+
});
|
|
81
80
|
return {
|
|
82
81
|
type: JsonComponentType.MediaGallery,
|
|
83
82
|
items: mediaItems,
|
|
@@ -99,7 +98,7 @@ export async function componentsToJson(components, options, cdnOptions) {
|
|
|
99
98
|
accessoryJson = {
|
|
100
99
|
type: JsonComponentType.Thumbnail,
|
|
101
100
|
media: {
|
|
102
|
-
url:
|
|
101
|
+
url: component.accessory.media.url,
|
|
103
102
|
},
|
|
104
103
|
spoiler: component.accessory.spoiler,
|
|
105
104
|
};
|
|
@@ -135,7 +134,7 @@ export async function componentsToJson(components, options, cdnOptions) {
|
|
|
135
134
|
}));
|
|
136
135
|
return processedComponents.filter(c => c != null);
|
|
137
136
|
}
|
|
138
|
-
function isJsonComponentInContainer(component) {
|
|
137
|
+
export function isJsonComponentInContainer(component) {
|
|
139
138
|
return (component.type == JsonComponentType.ActionRow ||
|
|
140
139
|
component.type == JsonComponentType.File ||
|
|
141
140
|
component.type == JsonComponentType.MediaGallery ||
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { TextBasedChannel } from "discord.js";
|
|
2
2
|
import { JsonAuthor, JsonMessage, TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
3
|
-
import {
|
|
4
|
-
export declare function fetchMessages(
|
|
3
|
+
import { MapMentions } from "../types/types.js";
|
|
4
|
+
export declare function fetchMessages(ctx: FetchMessagesContext): Promise<{
|
|
5
5
|
messages: JsonMessage[];
|
|
6
6
|
end: boolean;
|
|
7
|
+
newLastMessageId: string | undefined;
|
|
7
8
|
}>;
|
|
9
|
+
export type FetchMessagesContext = {
|
|
10
|
+
channel: TextBasedChannel;
|
|
11
|
+
options: TranscriptOptionsBase;
|
|
12
|
+
transcriptState: TranscriptState;
|
|
13
|
+
lastMessageId: string | undefined;
|
|
14
|
+
};
|
|
15
|
+
type TranscriptState = {
|
|
16
|
+
authors: Map<string, JsonAuthor>;
|
|
17
|
+
mentions: MapMentions;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -1,45 +1,41 @@
|
|
|
1
1
|
import { EmbedType } from "discord.js";
|
|
2
2
|
import { componentsToJson } from "./componentToJson.js";
|
|
3
|
-
import { urlResolver } from "./urlResolver.js";
|
|
4
3
|
import { getMentions } from "./getMentions.js";
|
|
5
|
-
export async function fetchMessages(
|
|
6
|
-
const
|
|
4
|
+
export async function fetchMessages(ctx) {
|
|
5
|
+
const { channel, options, transcriptState, lastMessageId } = ctx;
|
|
6
|
+
const { authors, mentions } = transcriptState;
|
|
7
|
+
const originalMessages = await channel.messages.fetch({ limit: 100, cache: false, before: lastMessageId });
|
|
7
8
|
const rawMessages = await Promise.all(originalMessages.map(async (message) => {
|
|
8
|
-
const attachments =
|
|
9
|
+
const attachments = message.attachments.map(attachment => {
|
|
9
10
|
return {
|
|
10
11
|
contentType: attachment.contentType,
|
|
11
12
|
name: attachment.name,
|
|
12
13
|
size: attachment.size,
|
|
13
14
|
spoiler: attachment.spoiler,
|
|
14
|
-
url:
|
|
15
|
+
url: attachment.url,
|
|
15
16
|
};
|
|
16
|
-
})
|
|
17
|
+
});
|
|
17
18
|
// 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
|
|
18
|
-
const embeds = message.system && message.embeds.length == 1 && message.embeds[0].data.type == EmbedType.PollResult && !options.includePolls
|
|
19
|
-
|
|
20
|
-
|
|
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),
|
|
25
|
-
]);
|
|
19
|
+
const embeds = message.system && message.embeds.length == 1 && message.embeds[0].data.type == EmbedType.PollResult && !options.includePolls
|
|
20
|
+
? []
|
|
21
|
+
: message.embeds.map(embed => {
|
|
26
22
|
return {
|
|
27
|
-
author: embed.author ? { name: embed.author.name, url: embed.author.url ?? null, iconURL:
|
|
23
|
+
author: embed.author ? { name: embed.author.name, url: embed.author.url ?? null, iconURL: embed.author?.iconURL ?? null } : null,
|
|
28
24
|
description: embed.description ?? null,
|
|
29
25
|
fields: embed.fields.map(field => ({ inline: field.inline ?? false, name: field.name, value: field.value })),
|
|
30
|
-
footer: embed.footer ? { iconURL:
|
|
26
|
+
footer: embed.footer ? { iconURL: embed.footer?.iconURL ?? null, text: embed.footer.text } : null,
|
|
31
27
|
hexColor: embed.hexColor ?? null,
|
|
32
|
-
image:
|
|
33
|
-
thumbnail:
|
|
28
|
+
image: embed.image?.url ? { url: embed.image.url } : null,
|
|
29
|
+
thumbnail: embed.thumbnail?.url ? { url: embed.thumbnail.url } : null,
|
|
34
30
|
timestamp: embed.timestamp,
|
|
35
31
|
title: embed.title,
|
|
36
32
|
type: embed.data.type ?? "rich",
|
|
37
33
|
url: embed.url,
|
|
38
34
|
};
|
|
39
|
-
})
|
|
35
|
+
});
|
|
40
36
|
if (!authors.has(message.author.id)) {
|
|
41
37
|
authors.set(message.author.id, {
|
|
42
|
-
avatarURL:
|
|
38
|
+
avatarURL: message.author.displayAvatarURL(),
|
|
43
39
|
bot: message.author.bot,
|
|
44
40
|
displayName: message.author.displayName,
|
|
45
41
|
guildTag: message.author.primaryGuild?.tag ?? null,
|
|
@@ -51,7 +47,7 @@ export async function fetchMessages(channel, options, cdnOptions, authors, menti
|
|
|
51
47
|
system: message.author.system,
|
|
52
48
|
});
|
|
53
49
|
}
|
|
54
|
-
const components = await componentsToJson(message.components, options
|
|
50
|
+
const components = await componentsToJson(message.components, options);
|
|
55
51
|
await getMentions(message, mentions);
|
|
56
52
|
return {
|
|
57
53
|
attachments: options.includeAttachments ? attachments : [],
|
|
@@ -89,9 +85,10 @@ export async function fetchMessages(channel, options, cdnOptions, authors, menti
|
|
|
89
85
|
system: message.system,
|
|
90
86
|
};
|
|
91
87
|
}));
|
|
88
|
+
const newLastMessageId = originalMessages.last()?.id;
|
|
92
89
|
const messages = rawMessages.filter(m => !(!options.includeEmpty && m.attachments.length == 0 && m.components.length == 0 && m.content == "" && m.embeds.length == 0 && !m.poll));
|
|
93
90
|
const end = originalMessages.size !== 100;
|
|
94
|
-
return { messages, end };
|
|
91
|
+
return { messages, end, newLastMessageId };
|
|
95
92
|
}
|
|
96
93
|
function formatTimeLeftPoll(timestamp) {
|
|
97
94
|
const now = new Date();
|
|
@@ -1,43 +1,53 @@
|
|
|
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';
|
|
4
5
|
export async function imageToBase64(url) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
6
|
+
const limit = getBase64Limiter();
|
|
7
|
+
return limit(async () => {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
const client = url.startsWith('https') ? https : http;
|
|
10
|
+
const request = client.get(url, { headers: { "User-Agent": "discord-message-transcript" } }, (response) => {
|
|
11
|
+
if (response.statusCode !== 200) {
|
|
12
|
+
response.destroy();
|
|
13
|
+
CustomWarn(`This is not an issue with the package. Using the original URL as fallback instead of converting to base64.
|
|
14
|
+
Failed to fetch image with status code: ${response.statusCode} from ${url}.`);
|
|
15
|
+
return resolve(url);
|
|
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
|
+
});
|
|
21
37
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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}`);
|
|
42
|
+
return resolve(url);
|
|
26
43
|
});
|
|
27
|
-
|
|
28
|
-
|
|
44
|
+
request.setTimeout(15000, () => {
|
|
45
|
+
request.destroy();
|
|
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}.`);
|
|
29
48
|
resolve(url);
|
|
30
49
|
});
|
|
50
|
+
request.end();
|
|
31
51
|
});
|
|
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();
|
|
42
52
|
});
|
|
43
53
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
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>;
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
import { TranscriptOptionsBase } from "discord-message-transcript-base";
|
|
1
|
+
import { JsonAuthor, JsonMessage, 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
|
|
3
|
+
export declare function urlResolver(url: string, 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[]>;
|
package/dist/core/urlResolver.js
CHANGED
|
@@ -1,9 +1,101 @@
|
|
|
1
|
+
import { JsonComponentType } from "discord-message-transcript-base";
|
|
1
2
|
import { cdnResolver } from "./cdnResolver.js";
|
|
2
3
|
import { imageToBase64 } from "./imageToBase64.js";
|
|
3
|
-
|
|
4
|
+
import { isJsonComponentInContainer } from "./componentToJson.js";
|
|
5
|
+
export async function urlResolver(url, options, cdnOptions, urlCache) {
|
|
6
|
+
if (urlCache.has(url)) {
|
|
7
|
+
const cache = urlCache.get(url);
|
|
8
|
+
if (cache)
|
|
9
|
+
return await cache;
|
|
10
|
+
}
|
|
11
|
+
let returnUrl;
|
|
4
12
|
if (cdnOptions)
|
|
5
|
-
|
|
6
|
-
if (options.saveImages)
|
|
7
|
-
|
|
13
|
+
returnUrl = cdnResolver(url, options, cdnOptions);
|
|
14
|
+
else if (options.saveImages)
|
|
15
|
+
returnUrl = imageToBase64(url);
|
|
16
|
+
if (returnUrl) {
|
|
17
|
+
urlCache.set(url, returnUrl);
|
|
18
|
+
return await returnUrl;
|
|
19
|
+
}
|
|
8
20
|
return url;
|
|
9
21
|
}
|
|
22
|
+
export async function messagesUrlResolver(messages, options, cdnOptions, urlCache) {
|
|
23
|
+
return await Promise.all(messages.map(async (message) => {
|
|
24
|
+
const attachmentsPromise = Promise.all(message.attachments.map(async (attachment) => {
|
|
25
|
+
return {
|
|
26
|
+
...attachment,
|
|
27
|
+
url: await urlResolver(attachment.url, options, cdnOptions, urlCache)
|
|
28
|
+
};
|
|
29
|
+
}));
|
|
30
|
+
const embedsPromise = Promise.all(message.embeds.map(async (embed) => {
|
|
31
|
+
return {
|
|
32
|
+
...embed,
|
|
33
|
+
author: embed.author ? { ...embed.author, iconURL: embed.author.iconURL ? await urlResolver(embed.author.iconURL, options, cdnOptions, urlCache) : null } : null,
|
|
34
|
+
footer: embed.footer ? { ...embed.footer, iconURL: embed.footer.iconURL ? await urlResolver(embed.footer.iconURL, options, cdnOptions, urlCache) : null } : null,
|
|
35
|
+
image: embed.image?.url ? { url: await urlResolver(embed.image.url, options, cdnOptions, urlCache) } : null,
|
|
36
|
+
thumbnail: embed.thumbnail?.url ? { url: await urlResolver(embed.thumbnail.url, options, cdnOptions, urlCache) } : null,
|
|
37
|
+
};
|
|
38
|
+
}));
|
|
39
|
+
async function componentsFunction(components) {
|
|
40
|
+
return Promise.all(components.map(async (component) => {
|
|
41
|
+
if (component.type == JsonComponentType.Section) {
|
|
42
|
+
if (component.accessory.type == JsonComponentType.Thumbnail) {
|
|
43
|
+
return {
|
|
44
|
+
...component,
|
|
45
|
+
accessory: {
|
|
46
|
+
...component.accessory,
|
|
47
|
+
media: {
|
|
48
|
+
url: await urlResolver(component.accessory.media.url, options, cdnOptions, urlCache),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (component.type == JsonComponentType.MediaGallery) {
|
|
55
|
+
return {
|
|
56
|
+
...component,
|
|
57
|
+
items: await Promise.all(component.items.map(async (item) => {
|
|
58
|
+
return {
|
|
59
|
+
...item,
|
|
60
|
+
media: { url: await urlResolver(item.media.url, options, cdnOptions, urlCache) },
|
|
61
|
+
};
|
|
62
|
+
}))
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (component.type == JsonComponentType.File) {
|
|
66
|
+
return {
|
|
67
|
+
...component,
|
|
68
|
+
url: await urlResolver(component.url, options, cdnOptions, urlCache),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (component.type == JsonComponentType.Container) {
|
|
72
|
+
return {
|
|
73
|
+
...component,
|
|
74
|
+
components: (await componentsFunction(component.components)).filter(isJsonComponentInContainer), // Input components that are container-safe must always produce container-safe output.
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return component;
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
const componentsPromise = componentsFunction(message.components);
|
|
81
|
+
const [attachments, embeds, components] = await Promise.all([
|
|
82
|
+
attachmentsPromise,
|
|
83
|
+
embedsPromise,
|
|
84
|
+
componentsPromise
|
|
85
|
+
]);
|
|
86
|
+
return {
|
|
87
|
+
...message,
|
|
88
|
+
attachments: attachments,
|
|
89
|
+
embeds: embeds,
|
|
90
|
+
components: components,
|
|
91
|
+
};
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
export async function authorUrlResolver(authors, options, cdnOptions, urlCache) {
|
|
95
|
+
return await Promise.all(Array.from(authors.values()).map(async (author) => {
|
|
96
|
+
return {
|
|
97
|
+
...author,
|
|
98
|
+
avatarURL: await urlResolver(author.avatarURL, options, cdnOptions, urlCache),
|
|
99
|
+
};
|
|
100
|
+
}));
|
|
101
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType, CDNOptions, MimeType } from "./types/types.js";
|
|
2
2
|
export { ReturnFormat, LocalDate, TimeZone } from "discord-message-transcript-base";
|
|
3
|
+
export { setBase64Concurrency, setCDNConcurrency } from './core/limiter.js';
|
|
3
4
|
import { TextBasedChannel } from "discord.js";
|
|
4
5
|
import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "./types/types.js";
|
|
5
6
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { ReturnType } from "./types/types.js";
|
|
2
2
|
export { ReturnFormat } from "discord-message-transcript-base";
|
|
3
|
+
export { setBase64Concurrency, setCDNConcurrency } from './core/limiter.js';
|
|
3
4
|
import { AttachmentBuilder } from "discord.js";
|
|
4
5
|
import { Json } from "./renderers/json/json.js";
|
|
5
6
|
import { fetchMessages } from "./core/fetchMessages.js";
|
|
6
7
|
import { ReturnType } from "./types/types.js";
|
|
7
8
|
import { output } from "./core/output.js";
|
|
8
|
-
import { ReturnTypeBase, ReturnFormat, outputBase, CustomError } from "discord-message-transcript-base";
|
|
9
|
+
import { ReturnTypeBase, ReturnFormat, outputBase, CustomError, CustomWarn } from "discord-message-transcript-base";
|
|
9
10
|
import { returnTypeMapper } from "./core/mappers.js";
|
|
11
|
+
import { authorUrlResolver, messagesUrlResolver } from "./core/urlResolver.js";
|
|
10
12
|
/**
|
|
11
13
|
* Creates a transcript of a Discord channel's messages.
|
|
12
14
|
* Depending on the `returnType` option, this function can return an `AttachmentBuilder`,
|
|
@@ -27,6 +29,11 @@ export async function createTranscript(channel, options = {}) {
|
|
|
27
29
|
const artificialReturnType = options.returnType == ReturnType.Attachment ? ReturnTypeBase.Buffer : options.returnType ? returnTypeMapper(options.returnType) : ReturnTypeBase.Buffer;
|
|
28
30
|
const { fileName = null, includeAttachments = true, includeButtons = true, includeComponents = true, includeEmpty = false, includeEmbeds = true, includePolls = true, includeReactions = true, includeV2Components = true, localDate = 'en-GB', quantity = 0, returnFormat = ReturnFormat.HTML, saveImages = false, selfContained = false, timeZone = 'UTC', watermark = true, } = options;
|
|
29
31
|
const checkedFileName = (fileName ?? `Transcript-${channel.isDMBased() ? "DirectMessage" : channel.name}-${channel.id}`);
|
|
32
|
+
let validQuantity = true;
|
|
33
|
+
if (quantity < 0) {
|
|
34
|
+
CustomWarn("Quantity can't be a negative number, please use 0 for unlimited messages.\nUsing 0 as fallback!");
|
|
35
|
+
validQuantity = false;
|
|
36
|
+
}
|
|
30
37
|
const internalOptions = {
|
|
31
38
|
fileName: checkedFileName,
|
|
32
39
|
includeAttachments,
|
|
@@ -38,7 +45,7 @@ export async function createTranscript(channel, options = {}) {
|
|
|
38
45
|
includeReactions,
|
|
39
46
|
includeV2Components,
|
|
40
47
|
localDate,
|
|
41
|
-
quantity,
|
|
48
|
+
quantity: validQuantity ? quantity : 0,
|
|
42
49
|
returnFormat,
|
|
43
50
|
returnType: artificialReturnType,
|
|
44
51
|
saveImages,
|
|
@@ -46,26 +53,63 @@ export async function createTranscript(channel, options = {}) {
|
|
|
46
53
|
timeZone,
|
|
47
54
|
watermark
|
|
48
55
|
};
|
|
49
|
-
const
|
|
50
|
-
let lastMessageID;
|
|
56
|
+
const urlCache = new Map();
|
|
51
57
|
const authors = new Map();
|
|
52
58
|
const mentions = {
|
|
53
59
|
channels: new Map(),
|
|
54
60
|
roles: new Map(),
|
|
55
61
|
users: new Map(),
|
|
56
62
|
};
|
|
63
|
+
const fetchMessageParameter = {
|
|
64
|
+
channel: channel,
|
|
65
|
+
options: internalOptions,
|
|
66
|
+
transcriptState: {
|
|
67
|
+
authors: authors,
|
|
68
|
+
mentions: mentions,
|
|
69
|
+
},
|
|
70
|
+
lastMessageId: undefined
|
|
71
|
+
};
|
|
72
|
+
const jsonTranscript = channel.isDMBased() ? new Json(null, channel, internalOptions, options.cdnOptions ?? null, urlCache) : new Json(channel.guild, channel, internalOptions, options.cdnOptions ?? null, urlCache);
|
|
57
73
|
while (true) {
|
|
58
|
-
const { messages, end } = await fetchMessages(
|
|
74
|
+
const { messages, end, newLastMessageId } = await fetchMessages(fetchMessageParameter);
|
|
59
75
|
jsonTranscript.addMessages(messages);
|
|
60
|
-
|
|
61
|
-
if (end || (jsonTranscript.
|
|
76
|
+
fetchMessageParameter.lastMessageId = newLastMessageId;
|
|
77
|
+
if (end || (jsonTranscript.getMessages().length >= quantity && quantity != 0)) {
|
|
62
78
|
break;
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
|
-
jsonTranscript.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
if (quantity > 0 && jsonTranscript.getMessages().length > quantity) {
|
|
82
|
+
jsonTranscript.sliceMessages(quantity);
|
|
83
|
+
}
|
|
84
|
+
if (options.cdnOptions) {
|
|
85
|
+
options.cdnOptions = {
|
|
86
|
+
includeAudio: true,
|
|
87
|
+
includeImage: true,
|
|
88
|
+
includeVideo: true,
|
|
89
|
+
includeOthers: true,
|
|
90
|
+
...options.cdnOptions
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (options.cdnOptions || options.saveImages) {
|
|
94
|
+
await Promise.all([
|
|
95
|
+
(async () => {
|
|
96
|
+
jsonTranscript.setAuthors(await authorUrlResolver(authors, internalOptions, options.cdnOptions ?? null, urlCache));
|
|
97
|
+
authors.clear();
|
|
98
|
+
})(),
|
|
99
|
+
(() => {
|
|
100
|
+
jsonTranscript.setMentions({ channels: Array.from(mentions.channels.values()), roles: Array.from(mentions.roles.values()), users: Array.from(mentions.users.values()) });
|
|
101
|
+
mentions.channels.clear();
|
|
102
|
+
mentions.roles.clear();
|
|
103
|
+
mentions.users.clear();
|
|
104
|
+
})(),
|
|
105
|
+
(async () => {
|
|
106
|
+
jsonTranscript.setMessages(await messagesUrlResolver(jsonTranscript.getMessages(), internalOptions, options.cdnOptions ?? null, urlCache));
|
|
107
|
+
})()
|
|
108
|
+
]);
|
|
109
|
+
}
|
|
110
|
+
const outputJson = await jsonTranscript.toJson();
|
|
111
|
+
urlCache.clear();
|
|
112
|
+
const result = await output(outputJson);
|
|
69
113
|
if (!options.returnType || options.returnType == "attachment") {
|
|
70
114
|
if (!(result instanceof Buffer)) {
|
|
71
115
|
throw new CustomError("Expected buffer from output when *attachment* returnType is used.");
|
|
@@ -1,15 +1,20 @@
|
|
|
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 "../../types/types.js";
|
|
3
4
|
export declare class Json {
|
|
4
|
-
guild
|
|
5
|
-
channel
|
|
6
|
-
authors
|
|
7
|
-
messages
|
|
8
|
-
options
|
|
9
|
-
mentions
|
|
10
|
-
|
|
5
|
+
private guild;
|
|
6
|
+
private channel;
|
|
7
|
+
private authors;
|
|
8
|
+
private messages;
|
|
9
|
+
private options;
|
|
10
|
+
private mentions;
|
|
11
|
+
private cdnOptions;
|
|
12
|
+
private urlCache;
|
|
13
|
+
constructor(guild: Guild | null, channel: TextBasedChannel, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>);
|
|
11
14
|
addMessages(messages: JsonMessage[]): void;
|
|
12
15
|
sliceMessages(size: number): void;
|
|
16
|
+
setMessages(messages: JsonMessage[]): void;
|
|
17
|
+
getMessages(): JsonMessage[];
|
|
13
18
|
setAuthors(authors: JsonAuthor[]): void;
|
|
14
19
|
setMentions(mentions: ArrayMentions): void;
|
|
15
20
|
toJson(): Promise<JsonData>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BaseGuildTextChannel, DMChannel } from "discord.js";
|
|
2
|
+
import { urlResolver } from "../../core/urlResolver.js";
|
|
2
3
|
export class Json {
|
|
3
4
|
guild;
|
|
4
5
|
channel;
|
|
@@ -6,13 +7,17 @@ export class Json {
|
|
|
6
7
|
messages;
|
|
7
8
|
options;
|
|
8
9
|
mentions;
|
|
9
|
-
|
|
10
|
+
cdnOptions;
|
|
11
|
+
urlCache;
|
|
12
|
+
constructor(guild, channel, options, cdnOptions, urlCache) {
|
|
10
13
|
this.guild = guild;
|
|
11
14
|
this.channel = channel;
|
|
12
15
|
this.messages = [];
|
|
13
16
|
this.options = options;
|
|
14
17
|
this.authors = [];
|
|
15
18
|
this.mentions = { channels: [], roles: [], users: [] };
|
|
19
|
+
this.cdnOptions = cdnOptions;
|
|
20
|
+
this.urlCache = urlCache;
|
|
16
21
|
}
|
|
17
22
|
addMessages(messages) {
|
|
18
23
|
this.messages.push(...messages);
|
|
@@ -21,7 +26,13 @@ export class Json {
|
|
|
21
26
|
if (size > this.messages.length || size == 0) {
|
|
22
27
|
return;
|
|
23
28
|
}
|
|
24
|
-
this.messages = this.messages.slice(0, size
|
|
29
|
+
this.messages = this.messages.slice(0, size);
|
|
30
|
+
}
|
|
31
|
+
setMessages(messages) {
|
|
32
|
+
this.messages = messages;
|
|
33
|
+
}
|
|
34
|
+
getMessages() {
|
|
35
|
+
return this.messages;
|
|
25
36
|
}
|
|
26
37
|
setAuthors(authors) {
|
|
27
38
|
this.authors = authors;
|
|
@@ -31,11 +42,13 @@ export class Json {
|
|
|
31
42
|
}
|
|
32
43
|
async toJson() {
|
|
33
44
|
const channel = await this.channel.fetch();
|
|
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;
|
|
34
46
|
const guild = !channel.isDMBased() ? this.guild : null;
|
|
47
|
+
const guildIcon = guild?.iconURL();
|
|
35
48
|
const guildJson = !guild ? null : {
|
|
36
49
|
name: guild.name,
|
|
37
50
|
id: guild.id,
|
|
38
|
-
icon:
|
|
51
|
+
icon: guildIcon ? await urlResolver(guildIcon, this.options, this.cdnOptions, this.urlCache) : null,
|
|
39
52
|
};
|
|
40
53
|
return {
|
|
41
54
|
options: this.options,
|
|
@@ -45,7 +58,7 @@ export class Json {
|
|
|
45
58
|
parent: channel.isDMBased() ? null : (channel.parent ? { name: channel.parent.name, id: channel.parent.id } : null),
|
|
46
59
|
topic: (channel instanceof BaseGuildTextChannel) ? channel.topic : null,
|
|
47
60
|
id: channel.id,
|
|
48
|
-
img:
|
|
61
|
+
img: channelImg ? await urlResolver(channelImg, this.options, this.cdnOptions, this.urlCache) : null,
|
|
49
62
|
},
|
|
50
63
|
authors: this.authors,
|
|
51
64
|
messages: this.messages.reverse(),
|
package/dist/types/types.d.ts
CHANGED
|
@@ -1,19 +1,51 @@
|
|
|
1
1
|
import { JsonMessageMentionsChannels, JsonMessageMentionsRoles, JsonMessageMentionsUsers, LocalDate, TimeZone, Uploadable, ReturnFormat } from "discord-message-transcript-base";
|
|
2
2
|
import { AttachmentBuilder } from "discord.js";
|
|
3
3
|
import Stream from 'stream';
|
|
4
|
+
/**
|
|
5
|
+
* An enum-like object providing the possible return types for the transcript functions.
|
|
6
|
+
*/
|
|
4
7
|
export declare const ReturnType: {
|
|
8
|
+
/**
|
|
9
|
+
* Returns a `discord.js` AttachmentBuilder.
|
|
10
|
+
*/
|
|
5
11
|
readonly Attachment: "attachment";
|
|
12
|
+
/**
|
|
13
|
+
* Returns a `Buffer`.
|
|
14
|
+
*/
|
|
6
15
|
readonly Buffer: "buffer";
|
|
16
|
+
/**
|
|
17
|
+
* Returns a `Stream.Readable`.
|
|
18
|
+
* */
|
|
7
19
|
readonly Stream: "stream";
|
|
20
|
+
/**
|
|
21
|
+
* Returns a `string`.
|
|
22
|
+
* */
|
|
8
23
|
readonly String: "string";
|
|
24
|
+
/**
|
|
25
|
+
* Returns an `Uploadable` object with content, contentType, and fileName.
|
|
26
|
+
*/
|
|
9
27
|
readonly Uploadable: "uploadable";
|
|
10
28
|
};
|
|
29
|
+
/**
|
|
30
|
+
* The type representing the possible values of the `ReturnType` enum.
|
|
31
|
+
*/
|
|
11
32
|
export type ReturnType = typeof ReturnType[keyof typeof ReturnType];
|
|
33
|
+
/**
|
|
34
|
+
* A conditional type that maps the `ReturnType` string literal to the actual TypeScript type returned by the function.
|
|
35
|
+
* @template T The `ReturnType` literal.
|
|
36
|
+
*/
|
|
12
37
|
export type OutputType<T extends ReturnType> = T extends typeof ReturnType.Buffer ? Buffer : T extends typeof ReturnType.Stream ? Stream : T extends typeof ReturnType.String ? string : T extends typeof ReturnType.Uploadable ? Uploadable : AttachmentBuilder;
|
|
38
|
+
/**
|
|
39
|
+
* Options for creating a transcript, with all properties being optional.
|
|
40
|
+
* @see TranscriptOptions
|
|
41
|
+
*/
|
|
13
42
|
export type CreateTranscriptOptions<T extends ReturnType> = Partial<TranscriptOptions<T>>;
|
|
43
|
+
/**
|
|
44
|
+
* Options for converting a JSON transcript to an HTML transcript.
|
|
45
|
+
*/
|
|
14
46
|
export type ConvertTranscriptOptions<T extends ReturnType> = Partial<{
|
|
15
47
|
/**
|
|
16
|
-
* The type
|
|
48
|
+
* The desired output type for the transcript.
|
|
17
49
|
* - ReturnType.Attachment - The transcript content as a `Attachment`
|
|
18
50
|
* - ReturnType.String - The transcript content as a string.
|
|
19
51
|
* - ReturnType.Buffer - The transcript content as a `Buffer`.
|
|
@@ -23,24 +55,27 @@ export type ConvertTranscriptOptions<T extends ReturnType> = Partial<{
|
|
|
23
55
|
*/
|
|
24
56
|
returnType: T;
|
|
25
57
|
/**
|
|
26
|
-
* Whether the generated HTML should
|
|
58
|
+
* Whether the generated HTML should have its CSS and JS embedded directly in the file.
|
|
27
59
|
* @default false
|
|
28
60
|
*/
|
|
29
61
|
selfContained: boolean;
|
|
30
62
|
/**
|
|
31
|
-
*
|
|
63
|
+
* Whether to include the 'Generated with discord-message-transcript' watermark in the footer.
|
|
32
64
|
* @default true
|
|
33
65
|
*/
|
|
34
66
|
watermark: boolean;
|
|
35
67
|
}>;
|
|
36
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Defines the complete set of options for creating a transcript.
|
|
70
|
+
*/
|
|
71
|
+
export interface TranscriptOptions<T extends ReturnType> {
|
|
37
72
|
/**
|
|
38
|
-
* CDN
|
|
73
|
+
* Configuration for uploading attachments and other assets to a CDN.
|
|
39
74
|
*/
|
|
40
|
-
cdnOptions: CDNOptions
|
|
75
|
+
cdnOptions: CDNOptions;
|
|
41
76
|
/**
|
|
42
|
-
* The name of the file
|
|
43
|
-
*
|
|
77
|
+
* The name of the generated file (without extension).
|
|
78
|
+
* @default `Transcript-channel-name-channel-id`
|
|
44
79
|
*/
|
|
45
80
|
fileName: string;
|
|
46
81
|
/**
|
|
@@ -49,17 +84,17 @@ export interface TranscriptOptions<T extends ReturnType, Other = unknown> {
|
|
|
49
84
|
*/
|
|
50
85
|
includeAttachments: boolean;
|
|
51
86
|
/**
|
|
52
|
-
* Whether to include buttons in the transcript.
|
|
87
|
+
* Whether to include message component buttons in the transcript.
|
|
53
88
|
* @default true
|
|
54
89
|
*/
|
|
55
90
|
includeButtons: boolean;
|
|
56
91
|
/**
|
|
57
|
-
* Whether to include components in the transcript.
|
|
92
|
+
* Whether to include non-button message components (like select menus) in the transcript.
|
|
58
93
|
* @default true
|
|
59
94
|
*/
|
|
60
95
|
includeComponents: boolean;
|
|
61
96
|
/**
|
|
62
|
-
* Whether to include
|
|
97
|
+
* Whether to include messages that have no content.
|
|
63
98
|
* @default false
|
|
64
99
|
*/
|
|
65
100
|
includeEmpty: boolean;
|
|
@@ -74,23 +109,23 @@ export interface TranscriptOptions<T extends ReturnType, Other = unknown> {
|
|
|
74
109
|
*/
|
|
75
110
|
includePolls: boolean;
|
|
76
111
|
/**
|
|
77
|
-
* Whether to include reactions in the transcript.
|
|
112
|
+
* Whether to include message reactions in the transcript.
|
|
78
113
|
* @default true
|
|
79
114
|
*/
|
|
80
115
|
includeReactions: boolean;
|
|
81
116
|
/**
|
|
82
|
-
* Whether to include V2 components
|
|
117
|
+
* Whether to include newer (V2) components like `Container`, `MediaGallery`, etc.
|
|
83
118
|
* @default true
|
|
84
119
|
*/
|
|
85
120
|
includeV2Components: boolean;
|
|
86
121
|
/**
|
|
87
|
-
* The locale to use for formatting dates.
|
|
88
|
-
*
|
|
122
|
+
* The locale to use for formatting dates (e.g., 'en-US', 'pt-BR').
|
|
123
|
+
* Must be a valid BCP 47 language tag.
|
|
89
124
|
* @default 'en-GB'
|
|
90
125
|
*/
|
|
91
126
|
localDate: LocalDate;
|
|
92
127
|
/**
|
|
93
|
-
* The maximum number of messages to fetch. Set to 0 to fetch all messages.
|
|
128
|
+
* The maximum number of messages to fetch. Set to `0` to fetch all messages in the channel.
|
|
94
129
|
* @default 0
|
|
95
130
|
*/
|
|
96
131
|
quantity: number;
|
|
@@ -102,7 +137,7 @@ export interface TranscriptOptions<T extends ReturnType, Other = unknown> {
|
|
|
102
137
|
*/
|
|
103
138
|
returnFormat: ReturnFormat;
|
|
104
139
|
/**
|
|
105
|
-
* The type
|
|
140
|
+
* The desired output type for the transcript.
|
|
106
141
|
* - ReturnType.Attachment - The transcript content as a `Attachment`
|
|
107
142
|
* - ReturnType.String - The transcript content as a string.
|
|
108
143
|
* - ReturnType.Buffer - The transcript content as a `Buffer`.
|
|
@@ -112,48 +147,128 @@ export interface TranscriptOptions<T extends ReturnType, Other = unknown> {
|
|
|
112
147
|
*/
|
|
113
148
|
returnType: T;
|
|
114
149
|
/**
|
|
115
|
-
* Whether to save images
|
|
150
|
+
* Whether to save images as base64 data directly in the transcript.
|
|
151
|
+
* This is an alternative to using a CDN and results in larger file sizes.
|
|
152
|
+
* Will not work if using CDN.
|
|
116
153
|
* @default false
|
|
117
154
|
*/
|
|
118
155
|
saveImages: boolean;
|
|
119
156
|
/**
|
|
120
|
-
* Whether the generated HTML should
|
|
121
|
-
* Only
|
|
157
|
+
* Whether the generated HTML should have its CSS and JS embedded directly in the file.
|
|
158
|
+
* Only applicable if `returnFormat` is `HTML`.
|
|
122
159
|
* @default false
|
|
123
160
|
*/
|
|
124
161
|
selfContained: boolean;
|
|
125
162
|
/**
|
|
126
|
-
* The timezone to use for formatting dates.
|
|
127
|
-
*
|
|
163
|
+
* The timezone to use for formatting dates (e.g., 'UTC', 'America/New_York').
|
|
164
|
+
* Must be a valid IANA time zone name.
|
|
128
165
|
* @default 'UTC'
|
|
129
166
|
*/
|
|
130
167
|
timeZone: TimeZone;
|
|
131
168
|
/**
|
|
132
|
-
*
|
|
169
|
+
* Whether to include the 'Generated with discord-message-transcript' watermark in the footer.
|
|
133
170
|
* @default true
|
|
134
171
|
*/
|
|
135
172
|
watermark: boolean;
|
|
136
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Defines the structure for storing discovered mentions (users, roles, channels) during transcript creation.
|
|
176
|
+
* Uses Maps for efficient lookups.
|
|
177
|
+
*/
|
|
137
178
|
export interface MapMentions {
|
|
138
179
|
channels: Map<string, JsonMessageMentionsChannels>;
|
|
139
180
|
roles: Map<string, JsonMessageMentionsRoles>;
|
|
140
181
|
users: Map<string, JsonMessageMentionsUsers>;
|
|
141
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* A string template type for representing a MIME type (e.g., 'image/png', 'application/json').
|
|
185
|
+
*/
|
|
142
186
|
export type MimeType = `${string}/${string}`;
|
|
143
|
-
|
|
187
|
+
/**
|
|
188
|
+
* Base options applicable to all CDN providers.
|
|
189
|
+
*/
|
|
190
|
+
export type CDNBase = Partial<{
|
|
191
|
+
/**
|
|
192
|
+
* Whether to upload audio files to the CDN.
|
|
193
|
+
* @default true
|
|
194
|
+
*/
|
|
144
195
|
includeAudio: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Whether to upload image files (excluding GIFs) to the CDN.
|
|
198
|
+
* @default true
|
|
199
|
+
*/
|
|
145
200
|
includeImage: boolean;
|
|
201
|
+
/**
|
|
202
|
+
* Whether to upload video files (and GIFs) to the CDN.
|
|
203
|
+
* @default true
|
|
204
|
+
*/
|
|
146
205
|
includeVideo: boolean;
|
|
206
|
+
/**
|
|
207
|
+
* Whether to upload any other file types to the CDN.
|
|
208
|
+
* @default true
|
|
209
|
+
*/
|
|
147
210
|
includeOthers: boolean;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
211
|
+
}>;
|
|
212
|
+
/**
|
|
213
|
+
* A discriminated union of all possible CDN configurations.
|
|
214
|
+
*/
|
|
215
|
+
export type CDNOptions = (CDNBase & CDNOptionsCustom<any>) | (CDNBase & CDNOptionsCloudinary) | (CDNBase & CDNOptionsUploadcare);
|
|
216
|
+
/**
|
|
217
|
+
* Configuration for using a custom, user-provided CDN resolver function.
|
|
218
|
+
*/
|
|
219
|
+
export type CDNOptionsCustom<T = unknown> = {
|
|
220
|
+
/** Specifies the use of a custom CDN resolver. */
|
|
221
|
+
provider: "CUSTOM";
|
|
222
|
+
/**
|
|
223
|
+
* An async function that takes a URL and returns a new URL.
|
|
224
|
+
* @param url The original Discord asset URL.
|
|
225
|
+
* @param contentType The MIME type of the asset.
|
|
226
|
+
* @param customData Any additional data you want to pass to your resolver.
|
|
227
|
+
* @returns The new URL of the asset on your CDN.
|
|
228
|
+
*/
|
|
229
|
+
resolver: (url: string, contentType: MimeType | null, customData: T) => Promise<string> | string;
|
|
230
|
+
/**
|
|
231
|
+
* Any custom data you wish to make available within your resolver function.
|
|
232
|
+
*/
|
|
233
|
+
customData: T;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Configuration for using Cloudinary as the CDN.
|
|
237
|
+
*/
|
|
238
|
+
export type CDNOptionsCloudinary = {
|
|
239
|
+
/**
|
|
240
|
+
* Specifies the use of the built-in Cloudinary provider.
|
|
241
|
+
*/
|
|
242
|
+
provider: "CLOUDINARY";
|
|
243
|
+
/**
|
|
244
|
+
* Your Cloudinary cloud name.
|
|
245
|
+
*/
|
|
246
|
+
cloudName: string;
|
|
247
|
+
/**
|
|
248
|
+
* Your Cloudinary API key.
|
|
249
|
+
* */
|
|
250
|
+
apiKey: string;
|
|
251
|
+
/**
|
|
252
|
+
* Your Cloudinary API secret.
|
|
253
|
+
*/
|
|
254
|
+
apiSecret: string;
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Configuration for using Uploadcare as the CDN.
|
|
258
|
+
*/
|
|
259
|
+
export type CDNOptionsUploadcare = {
|
|
260
|
+
/**
|
|
261
|
+
* Specifies the use of the built-in Uploadcare provider.
|
|
262
|
+
*/
|
|
263
|
+
provider: "UPLOADCARE";
|
|
264
|
+
/**
|
|
265
|
+
* Your Uploadcare public key.
|
|
266
|
+
*/
|
|
267
|
+
publicKey: string;
|
|
268
|
+
/**
|
|
269
|
+
* Your Uploadcare CDN domain.
|
|
270
|
+
* Example: "aaa111aaa1.ucarecd.net".
|
|
271
|
+
* DO NOT INCLUDE https://
|
|
272
|
+
*/
|
|
273
|
+
cdnDomain: string;
|
|
274
|
+
};
|
package/dist/types/types.js
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An enum-like object providing the possible return types for the transcript functions.
|
|
3
|
+
*/
|
|
1
4
|
export const ReturnType = {
|
|
5
|
+
/**
|
|
6
|
+
* Returns a `discord.js` AttachmentBuilder.
|
|
7
|
+
*/
|
|
2
8
|
Attachment: "attachment",
|
|
9
|
+
/**
|
|
10
|
+
* Returns a `Buffer`.
|
|
11
|
+
*/
|
|
3
12
|
Buffer: "buffer",
|
|
13
|
+
/**
|
|
14
|
+
* Returns a `Stream.Readable`.
|
|
15
|
+
* */
|
|
4
16
|
Stream: "stream",
|
|
17
|
+
/**
|
|
18
|
+
* Returns a `string`.
|
|
19
|
+
* */
|
|
5
20
|
String: "string",
|
|
21
|
+
/**
|
|
22
|
+
* Returns an `Uploadable` object with content, contentType, and fileName.
|
|
23
|
+
*/
|
|
6
24
|
Uploadable: "uploadable"
|
|
7
25
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "discord-message-transcript",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -48,11 +48,14 @@
|
|
|
48
48
|
"typescript": "^5.9.3"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"discord-message-transcript-base": "1.2.0
|
|
51
|
+
"discord-message-transcript-base": "1.2.0"
|
|
52
52
|
},
|
|
53
53
|
"peerDependencies": {
|
|
54
54
|
"discord.js": ">=14.19.0 <15"
|
|
55
55
|
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
56
59
|
"scripts": {
|
|
57
60
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
58
61
|
"build": "tsc"
|