discord-message-transcript 1.1.7 → 1.2.0-dev-next.0.16

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