discord-message-transcript 1.3.1-dev.3.35 → 1.3.2-dev.0.49

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.
Files changed (88) hide show
  1. package/dist/core/assetResolver/base64/imageToBase64.d.ts +2 -0
  2. package/dist/core/{imageToBase64.js → assetResolver/base64/imageToBase64.js} +18 -4
  3. package/dist/core/assetResolver/cdn/cdnCustomError.d.ts +16 -0
  4. package/dist/core/assetResolver/cdn/cdnCustomError.js +28 -0
  5. package/dist/core/assetResolver/cdn/cdnResolver.d.ts +3 -0
  6. package/dist/core/assetResolver/cdn/cdnResolver.js +90 -0
  7. package/dist/core/assetResolver/cdn/cloudinaryCdnResolver.d.ts +1 -0
  8. package/dist/core/assetResolver/cdn/cloudinaryCdnResolver.js +120 -0
  9. package/dist/core/assetResolver/cdn/sanitizeFileName.d.ts +1 -0
  10. package/dist/core/assetResolver/cdn/sanitizeFileName.js +17 -0
  11. package/dist/core/assetResolver/cdn/uploadCareCdnResolver.d.ts +1 -0
  12. package/dist/core/assetResolver/cdn/uploadCareCdnResolver.js +137 -0
  13. package/dist/core/assetResolver/cdn/validateCdnUrl.d.ts +1 -0
  14. package/dist/core/assetResolver/cdn/validateCdnUrl.js +8 -0
  15. package/dist/core/assetResolver/contants.d.ts +1 -0
  16. package/dist/core/assetResolver/contants.js +1 -0
  17. package/dist/core/assetResolver/index.d.ts +7 -0
  18. package/dist/core/assetResolver/index.js +22 -0
  19. package/dist/core/assetResolver/url/authorUrlResolver.d.ts +3 -0
  20. package/dist/core/assetResolver/url/authorUrlResolver.js +10 -0
  21. package/dist/core/assetResolver/url/imageUrlResolver.d.ts +4 -0
  22. package/dist/core/assetResolver/url/imageUrlResolver.js +20 -0
  23. package/dist/core/assetResolver/url/messageUrlResolver.d.ts +3 -0
  24. package/dist/core/{urlResolver.js → assetResolver/url/messageUrlResolver.js} +17 -43
  25. package/dist/core/assetResolver/url/urlResolver.d.ts +3 -0
  26. package/dist/core/assetResolver/url/urlResolver.js +24 -0
  27. package/dist/core/{componentToJson.d.ts → discordParser/componentToJson.d.ts} +1 -1
  28. package/dist/core/discordParser/componentToJson.js +159 -0
  29. package/dist/core/discordParser/fetchMessages.d.ts +7 -0
  30. package/dist/core/{fetchMessages.js → discordParser/fetchMessages.js} +1 -1
  31. package/dist/core/{getMentions.d.ts → discordParser/getMentions.d.ts} +1 -1
  32. package/dist/core/{getMentions.js → discordParser/getMentions.js} +1 -1
  33. package/dist/core/discordParser/index.d.ts +4 -0
  34. package/dist/core/discordParser/index.js +33 -0
  35. package/dist/core/mappers.d.ts +1 -1
  36. package/dist/core/mappers.js +1 -1
  37. package/dist/core/networkSecurity/constants.d.ts +3 -0
  38. package/dist/core/networkSecurity/constants.js +3 -0
  39. package/dist/core/networkSecurity/dns.d.ts +2 -0
  40. package/dist/core/networkSecurity/dns.js +29 -0
  41. package/dist/core/networkSecurity/index.d.ts +2 -0
  42. package/dist/core/networkSecurity/index.js +2 -0
  43. package/dist/core/networkSecurity/ip.d.ts +1 -0
  44. package/dist/core/networkSecurity/ip.js +110 -0
  45. package/dist/core/networkSecurity/lookup.d.ts +2 -0
  46. package/dist/core/networkSecurity/lookup.js +14 -0
  47. package/dist/core/networkSecurity/urlSafety.d.ts +3 -0
  48. package/dist/core/networkSecurity/urlSafety.js +71 -0
  49. package/dist/index.d.ts +3 -3
  50. package/dist/index.js +13 -56
  51. package/dist/renderers/json/json.d.ts +1 -1
  52. package/dist/renderers/json/json.js +3 -4
  53. package/dist/types/types.d.ts +36 -2
  54. package/dist/utils/sleep.d.ts +1 -0
  55. package/dist/utils/sleep.js +3 -0
  56. package/package.json +4 -7
  57. package/dist/core/cdnResolver.d.ts +0 -5
  58. package/dist/core/cdnResolver.js +0 -210
  59. package/dist/core/clientManager.d.ts +0 -3
  60. package/dist/core/clientManager.js +0 -9
  61. package/dist/core/componentHelpers.d.ts +0 -3
  62. package/dist/core/componentHelpers.js +0 -175
  63. package/dist/core/componentToJson.js +0 -145
  64. package/dist/core/error.d.ts +0 -3
  65. package/dist/core/error.js +0 -7
  66. package/dist/core/fetchMessages.d.ts +0 -19
  67. package/dist/core/imageToBase64.d.ts +0 -1
  68. package/dist/core/markdown.d.ts +0 -2
  69. package/dist/core/markdown.js +0 -175
  70. package/dist/core/urlResolver.d.ts +0 -5
  71. package/dist/renderers/html/clientRenderer.d.ts +0 -0
  72. package/dist/renderers/html/clientRenderer.js +0 -73
  73. package/dist/renderers/html/css.d.ts +0 -11
  74. package/dist/renderers/html/css.js +0 -663
  75. package/dist/renderers/html/html copy.d.ts +0 -19
  76. package/dist/renderers/html/html copy.js +0 -371
  77. package/dist/renderers/html/html-backup.d.ts +0 -19
  78. package/dist/renderers/html/html-backup.js +0 -371
  79. package/dist/renderers/html/html.d.ts +0 -19
  80. package/dist/renderers/html/html.js +0 -415
  81. package/dist/renderers/html/html2.d.ts +0 -8
  82. package/dist/renderers/html/html2.js +0 -233
  83. package/dist/renderers/html/js.d.ts +0 -4
  84. package/dist/renderers/html/js.js +0 -174
  85. package/dist/types/types copy.d.ts +0 -284
  86. package/dist/types/types copy.js +0 -35
  87. /package/dist/core/{limiter.d.ts → assetResolver/limiter.d.ts} +0 -0
  88. /package/dist/core/{limiter.js → assetResolver/limiter.js} +0 -0
@@ -1,48 +1,29 @@
1
1
  import { JsonComponentType } from "discord-message-transcript-base";
2
- import { cdnResolver } from "./cdnResolver.js";
3
- import { imageToBase64 } from "./imageToBase64.js";
4
- import { isJsonComponentInContainer } from "./componentToJson.js";
5
- import { FALLBACK_PIXEL, isSafeForHTML, resolveImageURL } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
6
- export async function urlResolver(url, options, cdnOptions, urlCache) {
7
- if (url == FALLBACK_PIXEL || url == "")
8
- return url;
9
- if (urlCache.has(url)) {
10
- const cache = urlCache.get(url);
11
- if (cache)
12
- return await cache;
13
- }
14
- let returnUrl;
15
- if (cdnOptions)
16
- returnUrl = cdnResolver(url, options, cdnOptions);
17
- else if (options.saveImages)
18
- returnUrl = imageToBase64(url, options.disableWarnings);
19
- if (returnUrl) {
20
- urlCache.set(url, returnUrl);
21
- return await returnUrl;
22
- }
23
- return url;
24
- }
2
+ import { imageUrlResolver } from "./imageUrlResolver.js";
3
+ import { isSafeForHTML } from "@/networkSecurity";
4
+ import { urlResolver } from "./urlResolver.js";
5
+ import { isJsonComponentInContainer } from "@/core/discordParser/componentToJson.js";
25
6
  export async function messagesUrlResolver(messages, options, cdnOptions, urlCache) {
26
7
  return await Promise.all(messages.map(async (message) => {
27
8
  // Needs to wait for resolve correct when used attachment://
28
9
  const attachments = await Promise.all(message.attachments.map(async (attachment) => {
29
- let url;
10
+ let safeUrlObject;
30
11
  if (attachment.contentType?.startsWith("image/")) {
31
- url = await resolveImageURL(attachment.url, options, false, message.attachments);
12
+ safeUrlObject = await imageUrlResolver(attachment.url, options, false, message.attachments);
32
13
  }
33
14
  else {
34
- url = await isSafeForHTML(attachment.url, options) ? attachment.url : "";
15
+ safeUrlObject = await isSafeForHTML(attachment.url, options);
35
16
  }
36
17
  return {
37
18
  ...attachment,
38
- url: await urlResolver(url, options, cdnOptions, urlCache)
19
+ url: await urlResolver(safeUrlObject, options, cdnOptions, urlCache)
39
20
  };
40
21
  }));
41
22
  const embedsPromise = Promise.all(message.embeds.map(async (embed) => {
42
- const authorIconUrl = embed.author?.iconURL ? await resolveImageURL(embed.author.iconURL, options, true, attachments) : null;
43
- const footerIconUrl = embed.footer?.iconURL ? await resolveImageURL(embed.footer.iconURL, options, true, attachments) : null;
44
- const imageUrl = embed.image?.url ? await resolveImageURL(embed.image.url, options, true, attachments) : null;
45
- const thumbnailUrl = embed.thumbnail?.url ? await resolveImageURL(embed.thumbnail.url, options, true, attachments) : null;
23
+ const authorIconUrl = embed.author?.iconURL ? await imageUrlResolver(embed.author.iconURL, options, true, attachments) : null;
24
+ const footerIconUrl = embed.footer?.iconURL ? await imageUrlResolver(embed.footer.iconURL, options, true, attachments) : null;
25
+ const imageUrl = embed.image?.url ? await imageUrlResolver(embed.image.url, options, true, attachments) : null;
26
+ const thumbnailUrl = embed.thumbnail?.url ? await imageUrlResolver(embed.thumbnail.url, options, true, attachments) : null;
46
27
  return {
47
28
  ...embed,
48
29
  author: embed.author ? { ...embed.author, iconURL: authorIconUrl ? await urlResolver(authorIconUrl, options, cdnOptions, urlCache) : null } : null,
@@ -53,14 +34,14 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
53
34
  }));
54
35
  async function componentsFunction(components) {
55
36
  return Promise.all(components.map(async (component) => {
56
- if (component.type == JsonComponentType.Section) {
37
+ if (component.type == JsonComponentType.Section && component.accessory) {
57
38
  if (component.accessory.type == JsonComponentType.Thumbnail) {
58
39
  return {
59
40
  ...component,
60
41
  accessory: {
61
42
  ...component.accessory,
62
43
  media: {
63
- url: await urlResolver((await resolveImageURL(component.accessory.media.url, options, false, attachments)), options, cdnOptions, urlCache),
44
+ url: await urlResolver((await imageUrlResolver(component.accessory.media.url, options, false, attachments)), options, cdnOptions, urlCache),
64
45
  }
65
46
  }
66
47
  };
@@ -72,15 +53,16 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
72
53
  items: await Promise.all(component.items.map(async (item) => {
73
54
  return {
74
55
  ...item,
75
- media: { url: await urlResolver((await resolveImageURL(item.media.url, options, false, attachments)), options, cdnOptions, urlCache) },
56
+ media: { url: await urlResolver((await imageUrlResolver(item.media.url, options, false, attachments)), options, cdnOptions, urlCache) },
76
57
  };
77
58
  }))
78
59
  };
79
60
  }
80
61
  if (component.type == JsonComponentType.File) {
62
+ const safeUrlObject = await isSafeForHTML(component.url, options);
81
63
  return {
82
64
  ...component,
83
- url: await urlResolver((await isSafeForHTML(component.url, options) ? component.url : ""), options, cdnOptions, urlCache),
65
+ url: await urlResolver(safeUrlObject, options, cdnOptions, urlCache),
84
66
  };
85
67
  }
86
68
  if (component.type == JsonComponentType.Container) {
@@ -105,11 +87,3 @@ export async function messagesUrlResolver(messages, options, cdnOptions, urlCach
105
87
  };
106
88
  }));
107
89
  }
108
- export async function authorUrlResolver(authors, options, cdnOptions, urlCache) {
109
- return await Promise.all(Array.from(authors.values()).map(async (author) => {
110
- return {
111
- ...author,
112
- avatarURL: await urlResolver((await resolveImageURL(author.avatarURL, options, false)), options, cdnOptions, urlCache),
113
- };
114
- }));
115
- }
@@ -0,0 +1,3 @@
1
+ import { CDNOptions, safeUrlReturn } from "@/types/types.js";
2
+ import { TranscriptOptionsBase } from "discord-message-transcript-base";
3
+ export declare function urlResolver(safeUrlObject: safeUrlReturn, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null, urlCache: Map<string, Promise<string>>): Promise<string>;
@@ -0,0 +1,24 @@
1
+ import { FALLBACK_PIXEL } from "discord-message-transcript-base";
2
+ import { cdnResolver } from "../cdn/cdnResolver.js";
3
+ import { imageToBase64 } from "../base64/imageToBase64.js";
4
+ export async function urlResolver(safeUrlObject, options, cdnOptions, urlCache) {
5
+ if (safeUrlObject.safe == false)
6
+ return "";
7
+ if (safeUrlObject.url == FALLBACK_PIXEL)
8
+ return safeUrlObject.url;
9
+ if (urlCache.has(safeUrlObject.url)) {
10
+ const cache = urlCache.get(safeUrlObject.url);
11
+ if (cache)
12
+ return await cache;
13
+ }
14
+ let returnUrl;
15
+ if (cdnOptions)
16
+ returnUrl = cdnResolver(safeUrlObject, options, cdnOptions);
17
+ else if (options.saveImages)
18
+ returnUrl = imageToBase64(safeUrlObject, options.disableWarnings);
19
+ if (returnUrl) {
20
+ urlCache.set(safeUrlObject.url, returnUrl);
21
+ return await returnUrl;
22
+ }
23
+ return safeUrlObject.url;
24
+ }
@@ -1,4 +1,4 @@
1
1
  import { TopLevelComponent } from "discord.js";
2
- import { JsonTopLevelComponent, JsonComponentInContainer, TranscriptOptionsBase } from "discord-message-transcript-base";
2
+ import { JsonTopLevelComponent, TranscriptOptionsBase, JsonComponentInContainer } from "discord-message-transcript-base";
3
3
  export declare function componentsToJson(components: TopLevelComponent[], options: TranscriptOptionsBase): Promise<JsonTopLevelComponent[]>;
4
4
  export declare function isJsonComponentInContainer(component: JsonTopLevelComponent): component is JsonComponentInContainer;
@@ -0,0 +1,159 @@
1
+ import { ComponentType } from "discord.js";
2
+ import { mapButtonStyle, mapSelectorType, mapSeparatorSpacing } from "../mappers.js";
3
+ import { JsonComponentType } from "discord-message-transcript-base";
4
+ import { isValidHexColor } from "discord-message-transcript-base";
5
+ export async function componentsToJson(components, options) {
6
+ const filtered = components.filter(c => options.includeV2Components || c.type === ComponentType.ActionRow);
7
+ const processed = await Promise.all(filtered.map(c => convertComponent(c, options)));
8
+ return processed.filter(c => c != null);
9
+ }
10
+ async function convertComponent(component, options) {
11
+ switch (component.type) {
12
+ case ComponentType.ActionRow:
13
+ return convertActionRow(component, options);
14
+ case ComponentType.Container:
15
+ return convertContainer(component, options);
16
+ case ComponentType.File:
17
+ return convertFile(component);
18
+ case ComponentType.MediaGallery:
19
+ return convertMediaGallery(component);
20
+ case ComponentType.Section:
21
+ return convertSection(component);
22
+ case ComponentType.Separator:
23
+ return convertSeparator(component);
24
+ case ComponentType.TextDisplay:
25
+ return convertTextDisplay(component);
26
+ default:
27
+ return null;
28
+ }
29
+ }
30
+ async function convertActionRow(component, options) {
31
+ const rowComponents = await Promise.all(component.components
32
+ .filter(c => (c.type === ComponentType.Button ? options.includeButtons : options.includeComponents))
33
+ .map(c => convertActionRowChild(c)));
34
+ if (rowComponents.length === 0)
35
+ return null;
36
+ return { type: JsonComponentType.ActionRow, components: rowComponents };
37
+ }
38
+ async function convertActionRowChild(component) {
39
+ switch (component.type) {
40
+ case ComponentType.Button: {
41
+ return {
42
+ type: JsonComponentType.Button,
43
+ style: mapButtonStyle(component.style),
44
+ label: component.label,
45
+ emoji: component.emoji?.name ?? null,
46
+ url: component.url,
47
+ disabled: component.disabled,
48
+ };
49
+ }
50
+ case ComponentType.StringSelect: {
51
+ return {
52
+ type: JsonComponentType.StringSelect,
53
+ placeholder: component.placeholder,
54
+ disabled: component.disabled,
55
+ options: component.options.map(option => ({
56
+ label: option.label,
57
+ description: option.description ?? null,
58
+ emoji: option.emoji ? { id: option.emoji.id ?? null, name: option.emoji.name ?? null, animated: option.emoji.animated ?? false } : null,
59
+ })),
60
+ };
61
+ }
62
+ default: {
63
+ return {
64
+ type: mapSelectorType(component.type),
65
+ placeholder: component.placeholder,
66
+ disabled: component.disabled,
67
+ };
68
+ }
69
+ }
70
+ }
71
+ async function convertContainer(component, options) {
72
+ const newOptions = { ...options, includeComponents: true, includeButtons: true };
73
+ const componentsJson = await componentsToJson(component.components, newOptions);
74
+ return {
75
+ type: JsonComponentType.Container,
76
+ components: componentsJson.filter(isJsonComponentInContainer), // Input components that are container-safe must always produce container-safe output.
77
+ hexAccentColor: isValidHexColor(component.hexAccentColor, false),
78
+ spoiler: component.spoiler,
79
+ };
80
+ }
81
+ function convertFile(component) {
82
+ return {
83
+ type: JsonComponentType.File,
84
+ fileName: component.data.name ?? null,
85
+ size: component.data.size ?? 0,
86
+ url: component.file.url,
87
+ spoiler: component.spoiler,
88
+ };
89
+ }
90
+ async function convertMediaGallery(component) {
91
+ const mediaItems = await Promise.all(component.items.map(item => {
92
+ return {
93
+ media: { url: item.media.url },
94
+ spoiler: item.spoiler,
95
+ };
96
+ }));
97
+ return {
98
+ type: JsonComponentType.MediaGallery,
99
+ items: mediaItems,
100
+ };
101
+ }
102
+ function convertSection(component) {
103
+ let accessoryJson = null;
104
+ switch (component.accessory.type) {
105
+ case ComponentType.Button: {
106
+ accessoryJson = {
107
+ type: JsonComponentType.Button,
108
+ style: mapButtonStyle(component.accessory.style),
109
+ label: component.accessory.label,
110
+ emoji: component.accessory.emoji?.name ? component.accessory.emoji.name : null,
111
+ url: component.accessory.url,
112
+ disabled: component.accessory.disabled,
113
+ };
114
+ break;
115
+ }
116
+ case ComponentType.Thumbnail: {
117
+ accessoryJson = {
118
+ type: JsonComponentType.Thumbnail,
119
+ media: {
120
+ url: component.accessory.media.url,
121
+ },
122
+ spoiler: component.accessory.spoiler,
123
+ };
124
+ break;
125
+ }
126
+ default:
127
+ break;
128
+ }
129
+ const sectionComponents = component.components.map(c => ({
130
+ type: JsonComponentType.TextDisplay,
131
+ content: c.content,
132
+ }));
133
+ return {
134
+ type: JsonComponentType.Section,
135
+ accessory: accessoryJson,
136
+ components: sectionComponents,
137
+ };
138
+ }
139
+ function convertSeparator(component) {
140
+ return {
141
+ type: JsonComponentType.Separator,
142
+ spacing: mapSeparatorSpacing(component.spacing),
143
+ divider: component.divider,
144
+ };
145
+ }
146
+ function convertTextDisplay(component) {
147
+ return {
148
+ type: JsonComponentType.TextDisplay,
149
+ content: component.content,
150
+ };
151
+ }
152
+ export function isJsonComponentInContainer(component) {
153
+ return (component.type == JsonComponentType.ActionRow ||
154
+ component.type == JsonComponentType.File ||
155
+ component.type == JsonComponentType.MediaGallery ||
156
+ component.type == JsonComponentType.Section ||
157
+ component.type == JsonComponentType.Separator ||
158
+ component.type == JsonComponentType.TextDisplay);
159
+ }
@@ -0,0 +1,7 @@
1
+ import { JsonMessage } from "discord-message-transcript-base";
2
+ import { FetchMessagesContext } from "@/types";
3
+ export declare function fetchMessages(ctx: FetchMessagesContext): Promise<{
4
+ messages: JsonMessage[];
5
+ end: boolean;
6
+ newLastMessageId: string | undefined;
7
+ }>;
@@ -1,7 +1,7 @@
1
1
  import { EmbedType } from "discord.js";
2
2
  import { componentsToJson } from "./componentToJson.js";
3
+ import { isValidHexColor, sanitize } from "discord-message-transcript-base";
3
4
  import { getMentions } from "./getMentions.js";
4
- import { isValidHexColor, sanitize } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
5
5
  export async function fetchMessages(ctx) {
6
6
  const { channel, options, transcriptState, lastMessageId } = ctx;
7
7
  const { authors, mentions } = transcriptState;
@@ -1,3 +1,3 @@
1
1
  import { Message } from "discord.js";
2
- import { MapMentions } from "../types/types.js";
2
+ import { MapMentions } from "@/types";
3
3
  export declare function getMentions(message: Message, mentions: MapMentions): Promise<void>;
@@ -1,5 +1,5 @@
1
1
  import { ChannelType } from "discord.js";
2
- import { isValidHexColor, sanitize } from "../../../discord-message-transcript-base/src/core/sanitizer.js";
2
+ import { isValidHexColor, sanitize } from "discord-message-transcript-base";
3
3
  export async function getMentions(message, mentions) {
4
4
  message.mentions.channels.forEach(channel => {
5
5
  if (!mentions.channels.has(channel.id)) {
@@ -0,0 +1,4 @@
1
+ import { CDNOptions, ReturnDiscordParser } from "@/types/types.js";
2
+ import { TranscriptOptionsBase } from "discord-message-transcript-base";
3
+ import { TextBasedChannel } from "discord.js";
4
+ export declare function discordParser(channel: TextBasedChannel, options: TranscriptOptionsBase, cdnOptions: CDNOptions | null): Promise<ReturnDiscordParser>;
@@ -0,0 +1,33 @@
1
+ import { fetchMessages } from "./fetchMessages.js";
2
+ import { Json } from "@/renderers/json/json.js";
3
+ export async function discordParser(channel, options, cdnOptions) {
4
+ const urlCache = new Map();
5
+ const authors = new Map();
6
+ const mentions = {
7
+ channels: new Map(),
8
+ roles: new Map(),
9
+ users: new Map(),
10
+ };
11
+ const fetchMessageParameter = {
12
+ channel: channel,
13
+ options: options,
14
+ transcriptState: {
15
+ authors: authors,
16
+ mentions: mentions,
17
+ },
18
+ lastMessageId: undefined
19
+ };
20
+ const jsonTranscript = channel.isDMBased() ? new Json(null, channel, options, cdnOptions, urlCache) : new Json(channel.guild, channel, options, cdnOptions, urlCache);
21
+ while (true) {
22
+ const { messages, end, newLastMessageId } = await fetchMessages(fetchMessageParameter);
23
+ jsonTranscript.addMessages(messages);
24
+ fetchMessageParameter.lastMessageId = newLastMessageId;
25
+ if (end || (jsonTranscript.getMessages().length >= options.quantity && options.quantity != 0)) {
26
+ break;
27
+ }
28
+ }
29
+ if (options.quantity > 0 && jsonTranscript.getMessages().length > options.quantity) {
30
+ jsonTranscript.sliceMessages(options.quantity);
31
+ }
32
+ return [jsonTranscript, { ...fetchMessageParameter.transcriptState, urlCache }];
33
+ }
@@ -1,6 +1,6 @@
1
1
  import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
2
2
  import { JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
3
- import { ReturnType } from "../types/types.js";
3
+ import { ReturnType } from "@/types";
4
4
  export declare function mapButtonStyle(style: ButtonStyle): JsonButtonStyle;
5
5
  export declare function mapSeparatorSpacing(spacing: SeparatorSpacingSize): JsonSeparatorSpacingSize;
6
6
  export declare function mapComponentType(componentType: ComponentType): JsonComponentType;
@@ -1,6 +1,6 @@
1
1
  import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
2
2
  import { CustomError, JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
3
- import { ReturnType } from "../types/types.js";
3
+ import { ReturnType } from "@/types";
4
4
  export function mapButtonStyle(style) {
5
5
  switch (style) {
6
6
  case ButtonStyle.Primary:
@@ -0,0 +1,3 @@
1
+ export declare const DNS_SERVERS: string[];
2
+ export declare const DNS_LOOKUP_TIMEOUT = 5000;
3
+ export declare const TRUSTED_DISCORD_HOSTS: string[];
@@ -0,0 +1,3 @@
1
+ export const DNS_SERVERS = ["1.1.1.1", "8.8.8.8"];
2
+ export const DNS_LOOKUP_TIMEOUT = 5000;
3
+ export const TRUSTED_DISCORD_HOSTS = ["discordapp.com", "discordapp.net"];
@@ -0,0 +1,2 @@
1
+ import { LookupResult } from "@/types";
2
+ export declare function resolveAllIps(host: string): Promise<LookupResult[]>;
@@ -0,0 +1,29 @@
1
+ import { Resolver } from "dns/promises";
2
+ import { DNS_LOOKUP_TIMEOUT, DNS_SERVERS } from "./constants.js";
3
+ export async function resolveAllIps(host) {
4
+ const resolver = new Resolver();
5
+ resolver.setServers(DNS_SERVERS);
6
+ const lookupPromise = (async () => {
7
+ const results = [];
8
+ const [v4, v6] = await Promise.allSettled([
9
+ resolver.resolve4(host),
10
+ resolver.resolve6(host)
11
+ ]);
12
+ if (v4.status === "fulfilled") {
13
+ for (const ip of v4.value) {
14
+ results.push({ address: ip, family: 4 });
15
+ }
16
+ }
17
+ if (v6.status === "fulfilled") {
18
+ for (const ip of v6.value) {
19
+ results.push({ address: ip, family: 6 });
20
+ }
21
+ }
22
+ if (results.length === 0) {
23
+ throw new Error(`No DNS records found for ${host}`);
24
+ }
25
+ return results;
26
+ })();
27
+ const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`DNS timeout for ${host}`)), DNS_LOOKUP_TIMEOUT));
28
+ return Promise.race([lookupPromise, timeoutPromise]);
29
+ }
@@ -0,0 +1,2 @@
1
+ export * from './urlSafety.js';
2
+ export * from './lookup.js';
@@ -0,0 +1,2 @@
1
+ export * from './urlSafety.js';
2
+ export * from './lookup.js';
@@ -0,0 +1 @@
1
+ export declare function isPrivateIp(ip: string): boolean;
@@ -0,0 +1,110 @@
1
+ import net from "node:net";
2
+ export function isPrivateIp(ip) {
3
+ const family = net.isIP(ip);
4
+ if (!family)
5
+ return true;
6
+ if (family === 4)
7
+ return isPrivateIPv4(ip);
8
+ return isPrivateIPv6(ip);
9
+ }
10
+ function isPrivateIPv4(ip) {
11
+ const parts = ip.split(".").map(Number);
12
+ if (parts.length !== 4 || parts.some(n => isNaN(n)))
13
+ return true;
14
+ const [a, b] = parts;
15
+ return (a === 0 ||
16
+ a === 10 ||
17
+ a === 127 ||
18
+ (a === 169 && b === 254) ||
19
+ (a === 172 && b >= 16 && b <= 31) ||
20
+ (a === 192 && b === 168) ||
21
+ (a === 100 && b >= 64 && b <= 127) ||
22
+ a >= 224);
23
+ }
24
+ function parseIPv6(ip) {
25
+ if (net.isIP(ip) !== 6)
26
+ return null;
27
+ // handle IPv4 at end
28
+ if (ip.includes(".")) {
29
+ const lastColon = ip.lastIndexOf(":");
30
+ const ipv4Part = ip.slice(lastColon + 1);
31
+ const nums = ipv4Part.split(".").map(Number);
32
+ if (nums.length === 4 && nums.every(n => !isNaN(n))) {
33
+ const hex = ((nums[0] << 8) | nums[1]).toString(16) +
34
+ ":" +
35
+ ((nums[2] << 8) | nums[3]).toString(16);
36
+ ip = ip.slice(0, lastColon) + ":" + hex;
37
+ }
38
+ }
39
+ const sections = ip.split("::");
40
+ let head = sections[0] ? sections[0].split(":") : [];
41
+ let tail = sections[1] ? sections[1].split(":") : [];
42
+ if (sections.length === 2) {
43
+ const missing = 8 - (head.length + tail.length);
44
+ head = [...head, ...Array(missing).fill("0"), ...tail];
45
+ }
46
+ if (head.length !== 8)
47
+ return null;
48
+ const bytes = [];
49
+ for (const part of head) {
50
+ const n = parseInt(part || "0", 16);
51
+ if (isNaN(n))
52
+ return null;
53
+ bytes.push((n >> 8) & 0xff);
54
+ bytes.push(n & 0xff);
55
+ }
56
+ return bytes;
57
+ }
58
+ function extractEmbeddedIPv4(bytes) {
59
+ const isMapped = bytes.slice(0, 10).every(b => b === 0) &&
60
+ bytes[10] === 0xff &&
61
+ bytes[11] === 0xff;
62
+ if (isMapped) {
63
+ return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
64
+ }
65
+ const isCompat = bytes.slice(0, 12).every(b => b === 0);
66
+ if (isCompat) {
67
+ return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
68
+ }
69
+ const isNat64 = bytes[0] === 0x00 &&
70
+ bytes[1] === 0x64 &&
71
+ bytes[2] === 0xff &&
72
+ bytes[3] === 0x9b &&
73
+ bytes.slice(4, 12).every(b => b === 0);
74
+ if (isNat64) {
75
+ return `${bytes[12]}.${bytes[13]}.${bytes[14]}.${bytes[15]}`;
76
+ }
77
+ return null;
78
+ }
79
+ function isPrivateIPv6(ip) {
80
+ const bytes = parseIPv6(ip);
81
+ if (!bytes)
82
+ return true;
83
+ const embedded = extractEmbeddedIPv4(bytes);
84
+ if (embedded)
85
+ return isPrivateIPv4(embedded);
86
+ // ::
87
+ if (bytes.every(b => b === 0))
88
+ return true;
89
+ // ::1
90
+ if (bytes.slice(0, 15).every(b => b === 0) && bytes[15] === 1)
91
+ return true;
92
+ const first = bytes[0];
93
+ const second = bytes[1];
94
+ // fc00::/7
95
+ if ((first & 0xfe) === 0xfc)
96
+ return true;
97
+ // fe80::/10
98
+ if (first === 0xfe && (second & 0xc0) === 0x80)
99
+ return true;
100
+ // multicast
101
+ if (first === 0xff)
102
+ return true;
103
+ // 2001:db8::/32
104
+ if (bytes[0] === 0x20 &&
105
+ bytes[1] === 0x01 &&
106
+ bytes[2] === 0x0d &&
107
+ bytes[3] === 0xb8)
108
+ return true;
109
+ return false;
110
+ }
@@ -0,0 +1,2 @@
1
+ export declare function urlToIpUrl(url: string, ip: string): string;
2
+ export declare function createLookup(safeIps: string[]): ((_hostname: string, _opts: any, cb: any) => void) | undefined;
@@ -0,0 +1,14 @@
1
+ import net from "node:net";
2
+ export function urlToIpUrl(url, ip) {
3
+ // If got here shouldn't throw a error
4
+ const u = new URL(url);
5
+ return `${u.protocol}//${ip}` + `${u.port ? ":" + u.port : ""}` + `${u.pathname}${u.search}`;
6
+ }
7
+ export function createLookup(safeIps) {
8
+ if (safeIps.length == 0)
9
+ return undefined;
10
+ return (_hostname, _opts, cb) => {
11
+ const ip = safeIps[Math.floor(Math.random() * safeIps.length)];
12
+ cb(null, ip, net.isIP(ip));
13
+ };
14
+ }
@@ -0,0 +1,3 @@
1
+ import { TranscriptOptionsBase } from "discord-message-transcript-base";
2
+ import { safeUrlReturn } from "@/types";
3
+ export declare function isSafeForHTML(url: string, options: TranscriptOptionsBase): Promise<safeUrlReturn>;
@@ -0,0 +1,71 @@
1
+ import { CustomWarn } from "discord-message-transcript-base";
2
+ import { TRUSTED_DISCORD_HOSTS } from "./constants.js";
3
+ import { isPrivateIp } from "./ip.js";
4
+ import { resolveAllIps } from "./dns.js";
5
+ export async function isSafeForHTML(url, options) {
6
+ const { safeMode, disableWarnings } = options;
7
+ if (!safeMode)
8
+ return { safe: true, safeIps: [], url: url };
9
+ let u;
10
+ try {
11
+ u = new URL(url);
12
+ }
13
+ catch {
14
+ CustomWarn(`Unsafe URL rejected: Invalid URL format\nURL: ${url}`, disableWarnings);
15
+ return { safe: false, safeIps: [], url: url };
16
+ }
17
+ const host = u.hostname.toLowerCase();
18
+ // If is from discord accept
19
+ if (isTrustedDiscordHost(host))
20
+ return { safe: true, safeIps: [], url: url };
21
+ // Don't accept if isn't https or http
22
+ if (!["http:", "https:"].includes(u.protocol)) {
23
+ CustomWarn(`Unsafe URL rejected: Invalid protocol "${u.protocol}"\nURL: ${url}`, disableWarnings);
24
+ return { safe: false, safeIps: [], url: url };
25
+ }
26
+ if (u.username || u.password) {
27
+ CustomWarn(`Unsafe URL rejected: Contains username or password\nURL: ${url}`, disableWarnings);
28
+ return { safe: false, safeIps: [], url: url };
29
+ }
30
+ if (u.port && !["80", "443", ""].includes(u.port)) {
31
+ CustomWarn(`Unsafe URL rejected: Invalid port "${u.port}"\nURL: ${url}`, disableWarnings);
32
+ return { safe: false, safeIps: [], url: url };
33
+ }
34
+ // Block localhost and loopback addresses (SSRF protection)
35
+ if (host === "localhost" ||
36
+ host === "127.0.0.1" ||
37
+ host.startsWith("0.")) {
38
+ CustomWarn(`Unsafe URL rejected: Blacklisted host "${host}"\nURL: ${url}`, disableWarnings);
39
+ return { safe: false, safeIps: [], url: url };
40
+ }
41
+ let ips;
42
+ try {
43
+ ips = await resolveAllIps(host);
44
+ }
45
+ catch (e) {
46
+ CustomWarn(`Unsafe URL rejected: DNS lookup failed or timed out for host "${host}". Error: ${e.message}\nURL: ${url}`, disableWarnings);
47
+ return { safe: false, safeIps: [], url: url };
48
+ }
49
+ const safeIps = [];
50
+ // Block private/internal network IPs (SSRF protection)
51
+ for (const ip of ips) {
52
+ if (isPrivateIp(ip.address)) {
53
+ CustomWarn(`Unsafe URL rejected: Private IP address "${ip.address}" resolved for host "${host}"\nURL: ${url}`, disableWarnings);
54
+ return { safe: false, safeIps: [], url: url };
55
+ }
56
+ safeIps.push(ip.address);
57
+ }
58
+ const path = u.pathname.toLowerCase();
59
+ // External SVGs can execute scripts → allow only from Discord CDN
60
+ if (path.endsWith(".svg")) {
61
+ CustomWarn(`Unsafe URL rejected: External SVG not from Discord CDN\nURL: ${url}`, disableWarnings);
62
+ return { safe: false, safeIps: [], url: url };
63
+ }
64
+ return { safe: true, safeIps: safeIps, url: url };
65
+ }
66
+ function isTrustedDiscordHost(host) {
67
+ host = host.toLowerCase();
68
+ return TRUSTED_DISCORD_HOSTS.some(trusted => {
69
+ return host === trusted || host.endsWith("." + trusted);
70
+ });
71
+ }