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.
- package/dist/core/assetResolver/base64/imageToBase64.d.ts +2 -0
- package/dist/core/{imageToBase64.js → assetResolver/base64/imageToBase64.js} +18 -4
- package/dist/core/assetResolver/cdn/cdnCustomError.d.ts +16 -0
- package/dist/core/assetResolver/cdn/cdnCustomError.js +28 -0
- package/dist/core/assetResolver/cdn/cdnResolver.d.ts +3 -0
- package/dist/core/assetResolver/cdn/cdnResolver.js +90 -0
- package/dist/core/assetResolver/cdn/cloudinaryCdnResolver.d.ts +1 -0
- package/dist/core/assetResolver/cdn/cloudinaryCdnResolver.js +120 -0
- package/dist/core/assetResolver/cdn/sanitizeFileName.d.ts +1 -0
- package/dist/core/assetResolver/cdn/sanitizeFileName.js +17 -0
- package/dist/core/assetResolver/cdn/uploadCareCdnResolver.d.ts +1 -0
- package/dist/core/assetResolver/cdn/uploadCareCdnResolver.js +137 -0
- package/dist/core/assetResolver/cdn/validateCdnUrl.d.ts +1 -0
- package/dist/core/assetResolver/cdn/validateCdnUrl.js +8 -0
- package/dist/core/assetResolver/contants.d.ts +1 -0
- package/dist/core/assetResolver/contants.js +1 -0
- package/dist/core/assetResolver/index.d.ts +7 -0
- package/dist/core/assetResolver/index.js +22 -0
- package/dist/core/assetResolver/url/authorUrlResolver.d.ts +3 -0
- package/dist/core/assetResolver/url/authorUrlResolver.js +10 -0
- package/dist/core/assetResolver/url/imageUrlResolver.d.ts +4 -0
- package/dist/core/assetResolver/url/imageUrlResolver.js +20 -0
- package/dist/core/assetResolver/url/messageUrlResolver.d.ts +3 -0
- package/dist/core/{urlResolver.js → assetResolver/url/messageUrlResolver.js} +17 -43
- package/dist/core/assetResolver/url/urlResolver.d.ts +3 -0
- package/dist/core/assetResolver/url/urlResolver.js +24 -0
- package/dist/core/{componentToJson.d.ts → discordParser/componentToJson.d.ts} +1 -1
- package/dist/core/discordParser/componentToJson.js +159 -0
- package/dist/core/discordParser/fetchMessages.d.ts +7 -0
- package/dist/core/{fetchMessages.js → discordParser/fetchMessages.js} +1 -1
- package/dist/core/{getMentions.d.ts → discordParser/getMentions.d.ts} +1 -1
- package/dist/core/{getMentions.js → discordParser/getMentions.js} +1 -1
- package/dist/core/discordParser/index.d.ts +4 -0
- package/dist/core/discordParser/index.js +33 -0
- package/dist/core/mappers.d.ts +1 -1
- package/dist/core/mappers.js +1 -1
- package/dist/core/networkSecurity/constants.d.ts +3 -0
- package/dist/core/networkSecurity/constants.js +3 -0
- package/dist/core/networkSecurity/dns.d.ts +2 -0
- package/dist/core/networkSecurity/dns.js +29 -0
- package/dist/core/networkSecurity/index.d.ts +2 -0
- package/dist/core/networkSecurity/index.js +2 -0
- package/dist/core/networkSecurity/ip.d.ts +1 -0
- package/dist/core/networkSecurity/ip.js +110 -0
- package/dist/core/networkSecurity/lookup.d.ts +2 -0
- package/dist/core/networkSecurity/lookup.js +14 -0
- package/dist/core/networkSecurity/urlSafety.d.ts +3 -0
- package/dist/core/networkSecurity/urlSafety.js +71 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +13 -56
- package/dist/renderers/json/json.d.ts +1 -1
- package/dist/renderers/json/json.js +3 -4
- package/dist/types/types.d.ts +36 -2
- package/dist/utils/sleep.d.ts +1 -0
- package/dist/utils/sleep.js +3 -0
- package/package.json +4 -7
- package/dist/core/cdnResolver.d.ts +0 -5
- package/dist/core/cdnResolver.js +0 -210
- package/dist/core/clientManager.d.ts +0 -3
- package/dist/core/clientManager.js +0 -9
- package/dist/core/componentHelpers.d.ts +0 -3
- package/dist/core/componentHelpers.js +0 -175
- package/dist/core/componentToJson.js +0 -145
- package/dist/core/error.d.ts +0 -3
- package/dist/core/error.js +0 -7
- package/dist/core/fetchMessages.d.ts +0 -19
- package/dist/core/imageToBase64.d.ts +0 -1
- package/dist/core/markdown.d.ts +0 -2
- package/dist/core/markdown.js +0 -175
- package/dist/core/urlResolver.d.ts +0 -5
- package/dist/renderers/html/clientRenderer.d.ts +0 -0
- package/dist/renderers/html/clientRenderer.js +0 -73
- package/dist/renderers/html/css.d.ts +0 -11
- package/dist/renderers/html/css.js +0 -663
- package/dist/renderers/html/html copy.d.ts +0 -19
- package/dist/renderers/html/html copy.js +0 -371
- package/dist/renderers/html/html-backup.d.ts +0 -19
- package/dist/renderers/html/html-backup.js +0 -371
- package/dist/renderers/html/html.d.ts +0 -19
- package/dist/renderers/html/html.js +0 -415
- package/dist/renderers/html/html2.d.ts +0 -8
- package/dist/renderers/html/html2.js +0 -233
- package/dist/renderers/html/js.d.ts +0 -4
- package/dist/renderers/html/js.js +0 -174
- package/dist/types/types copy.d.ts +0 -284
- package/dist/types/types copy.js +0 -35
- /package/dist/core/{limiter.d.ts → assetResolver/limiter.d.ts} +0 -0
- /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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
|
10
|
+
let safeUrlObject;
|
|
30
11
|
if (attachment.contentType?.startsWith("image/")) {
|
|
31
|
-
|
|
12
|
+
safeUrlObject = await imageUrlResolver(attachment.url, options, false, message.attachments);
|
|
32
13
|
}
|
|
33
14
|
else {
|
|
34
|
-
|
|
15
|
+
safeUrlObject = await isSafeForHTML(attachment.url, options);
|
|
35
16
|
}
|
|
36
17
|
return {
|
|
37
18
|
...attachment,
|
|
38
|
-
url: await urlResolver(
|
|
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
|
|
43
|
-
const footerIconUrl = embed.footer?.iconURL ? await
|
|
44
|
-
const imageUrl = embed.image?.url ? await
|
|
45
|
-
const thumbnailUrl = embed.thumbnail?.url ? await
|
|
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
|
|
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
|
|
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(
|
|
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,
|
|
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,5 +1,5 @@
|
|
|
1
1
|
import { ChannelType } from "discord.js";
|
|
2
|
-
import { isValidHexColor, sanitize } from "
|
|
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
|
+
}
|
package/dist/core/mappers.d.ts
CHANGED
|
@@ -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 "
|
|
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;
|
package/dist/core/mappers.js
CHANGED
|
@@ -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 "
|
|
3
|
+
import { ReturnType } from "@/types";
|
|
4
4
|
export function mapButtonStyle(style) {
|
|
5
5
|
switch (style) {
|
|
6
6
|
case ButtonStyle.Primary:
|
|
@@ -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 @@
|
|
|
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,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,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
|
+
}
|