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.
@@ -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>;
@@ -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
- 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
- }
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
- 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, 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
- 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}`);
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
- 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();
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.provider) {
45
+ switch (cdnOptions.type) {
58
46
  case "CUSTOM": {
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, cdnOptions.cloudName, cdnOptions.apiKey, cdnOptions.apiSecret);
72
- ;
47
+ return await cdnOptions.customCdnResolver(url, contentType, cdnOptions.other);
73
48
  }
74
- case "UPLOADCARE": {
75
- return await uploadCareResolver(url, cdnOptions.publicKey);
49
+ case "CLOUDFLARE": {
50
+ return await cdnCloudflare();
76
51
  }
77
52
  }
78
53
  }
79
- function sleep(ms) {
80
- return new Promise(r => setTimeout(r, ms));
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, urlCache: Map<string, Promise<string>>): Promise<JsonTopLevelComponent[]>;
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, urlCache) {
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, urlCache);
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, urlCache),
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, urlCache) },
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, urlCache),
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(ctx: FetchMessagesContext): Promise<{
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(ctx) {
6
- const { channel, options, cdnOptions, transcriptState, lastMessageId } = ctx;
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, urlCache),
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, urlCache) : Promise.resolve(null),
24
- embed.thumbnail?.url ? urlResolver(embed.thumbnail.url, options, cdnOptions, urlCache) : Promise.resolve(null),
25
- embed.image?.url ? urlResolver(embed.image.url, options, cdnOptions, urlCache) : Promise.resolve(null),
26
- embed.footer?.iconURL ? urlResolver(embed.footer.iconURL, options, cdnOptions, urlCache) : Promise.resolve(null),
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, urlCache),
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, urlCache);
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, newLastMessageId };
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
- 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
- });
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
- 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}.`);
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, urlCache: Map<string, Promise<string>>): Promise<string>;
3
+ export declare function urlResolver(url: string, options: TranscriptOptionsBase, cdnOptions: CDNOptions<unknown> | null): Promise<string>;
@@ -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, urlCache) {
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
- returnUrl = cdnResolver(url, cdnOptions);
12
- else if (options.saveImages)
13
- returnUrl = imageToBase64(url);
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, newLastMessageId } = await fetchMessages(fetchMessageParameter);
58
+ const { messages, end } = await fetchMessages(channel, internalOptions, options.cdnOptions ?? null, authors, mentions, lastMessageID);
71
59
  jsonTranscript.addMessages(messages);
72
- fetchMessageParameter.lastMessageId = newLastMessageId;
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
- urlCache.clear(); // Clean cache
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)) {
@@ -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 CDNBase = Partial<{
148
+ export type CDNOptions<Other> = (Partial<{
144
149
  includeAudio: boolean;
145
150
  includeImage: boolean;
146
151
  includeVideo: boolean;
147
152
  includeOthers: boolean;
148
- }>;
149
- export type CDNOptions = (CDNBase & CDNOptionsCustom<any>) | (CDNBase & CDNOptionsCloudinary) | (CDNBase & CDNOptionsUploadcare);
150
- export type CDNOptionsCustom<T = unknown> = {
151
- provider: "CUSTOM";
152
- resolver: (url: string, contentType: MimeType | null, customData: T) => Promise<string> | string;
153
- customData: T;
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
+ });
@@ -5,3 +5,7 @@ export const ReturnType = {
5
5
  String: "string",
6
6
  Uploadable: "uploadable"
7
7
  };
8
+ export const CDNType = {
9
+ CUSTOM: "CUSTOM",
10
+ CLOUDFLARE: "CLOUDFLARE",
11
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discord-message-transcript",
3
- "version": "1.2.0-dev-next.0.20",
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-next.0.20"
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"
@@ -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>;
@@ -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
- }