discord-message-transcript 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/dist/core/clientManager.d.ts +3 -0
- package/dist/core/clientManager.js +9 -0
- package/dist/core/componentHelpers.d.ts +3 -0
- package/dist/core/componentHelpers.js +175 -0
- package/dist/core/componentToJson.d.ts +3 -0
- package/dist/core/componentToJson.js +174 -0
- package/dist/core/error.d.ts +3 -0
- package/dist/core/error.js +7 -0
- package/dist/core/fetchMessages.d.ts +7 -0
- package/dist/core/fetchMessages.js +169 -0
- package/dist/core/getMentions.d.ts +3 -0
- package/dist/core/getMentions.js +99 -0
- package/dist/core/imageToBase64.d.ts +1 -0
- package/dist/core/imageToBase64.js +30 -0
- package/dist/core/mappers.d.ts +8 -0
- package/dist/core/mappers.js +101 -0
- package/dist/core/markdown.d.ts +2 -0
- package/dist/core/markdown.js +175 -0
- package/dist/core/output.d.ts +4 -0
- package/dist/core/output.js +28 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +120 -0
- package/dist/renderers/html/clientRenderer.d.ts +0 -0
- package/dist/renderers/html/clientRenderer.js +73 -0
- package/dist/renderers/html/css.d.ts +11 -0
- package/dist/renderers/html/css.js +663 -0
- package/dist/renderers/html/html copy.d.ts +19 -0
- package/dist/renderers/html/html copy.js +371 -0
- package/dist/renderers/html/html-backup.d.ts +19 -0
- package/dist/renderers/html/html-backup.js +371 -0
- package/dist/renderers/html/html.d.ts +19 -0
- package/dist/renderers/html/html.js +415 -0
- package/dist/renderers/html/html2.d.ts +8 -0
- package/dist/renderers/html/html2.js +233 -0
- package/dist/renderers/html/js.d.ts +4 -0
- package/dist/renderers/html/js.js +174 -0
- package/dist/renderers/json/json.d.ts +16 -0
- package/dist/renderers/json/json.js +55 -0
- package/dist/types/types copy.d.ts +284 -0
- package/dist/types/types copy.js +35 -0
- package/dist/types/types.d.ts +137 -0
- package/dist/types/types.js +7 -0
- package/package.json +45 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { EmbedType } from "discord.js";
|
|
2
|
+
import { componentsToJson } from "./componentToJson.js";
|
|
3
|
+
import { urlToBase64 } from "./imageToBase64.js";
|
|
4
|
+
import { CustomError } from "discord-message-transcript-base";
|
|
5
|
+
import { getMentions } from "./getMentions.js";
|
|
6
|
+
export async function fetchMessages(channel, options, authors, mentions, after) {
|
|
7
|
+
const originalMessages = await channel.messages.fetch({ limit: 100, cache: false, after: after });
|
|
8
|
+
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
|
+
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
|
+
return {
|
|
31
|
+
contentType: attachment.contentType,
|
|
32
|
+
name: attachment.name,
|
|
33
|
+
size: attachment.size,
|
|
34
|
+
spoiler: attachment.spoiler,
|
|
35
|
+
url: attachmentUrl,
|
|
36
|
+
};
|
|
37
|
+
}));
|
|
38
|
+
// 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
|
+
const embeds = message.system && message.embeds.length == 1 && message.embeds[0].data.type == EmbedType.PollResult && !options.includePolls ? []
|
|
40
|
+
: 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
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
author: embed.author ? { name: embed.author.name, url: embed.author.url ?? null, iconURL: authorIcon } : null,
|
|
85
|
+
description: embed.description ?? null,
|
|
86
|
+
fields: embed.fields.map(field => ({ inline: field.inline ?? false, name: field.name, value: field.value })),
|
|
87
|
+
footer: embed.footer ? { iconURL: footerIcon, text: embed.footer.text } : null,
|
|
88
|
+
hexColor: embed.hexColor ?? null,
|
|
89
|
+
image: imageUrl ? { url: imageUrl } : null,
|
|
90
|
+
thumbnail: thumbnailUrl ? { url: thumbnailUrl } : null,
|
|
91
|
+
timestamp: embed.timestamp,
|
|
92
|
+
title: embed.title,
|
|
93
|
+
type: embed.data.type ?? "rich",
|
|
94
|
+
url: embed.url,
|
|
95
|
+
};
|
|
96
|
+
}));
|
|
97
|
+
if (!authors.has(message.author.id)) {
|
|
98
|
+
authors.set(message.author.id, {
|
|
99
|
+
avatarURL: authorAvatar,
|
|
100
|
+
bot: message.author.bot,
|
|
101
|
+
displayName: message.author.displayName,
|
|
102
|
+
guildTag: message.author.primaryGuild?.tag ?? null,
|
|
103
|
+
id: message.author.id,
|
|
104
|
+
member: message.member ? {
|
|
105
|
+
displayHexColor: message.member.displayHexColor,
|
|
106
|
+
displayName: message.member.displayName,
|
|
107
|
+
} : null,
|
|
108
|
+
system: message.author.system,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const components = await componentsToJson(message.components, options);
|
|
112
|
+
getMentions(message, mentions);
|
|
113
|
+
return {
|
|
114
|
+
attachments: options.includeAttachments ? attachments : [],
|
|
115
|
+
authorId: message.author.id,
|
|
116
|
+
components: components,
|
|
117
|
+
content: message.content,
|
|
118
|
+
createdTimestamp: message.createdTimestamp,
|
|
119
|
+
embeds: options.includeEmbeds || options.includePolls ? embeds : [],
|
|
120
|
+
id: message.id,
|
|
121
|
+
mentions: message.mentions.everyone,
|
|
122
|
+
poll: options.includePolls && message.poll ? {
|
|
123
|
+
answers: Array.from(message.poll.answers.values()).map(answer => ({
|
|
124
|
+
count: answer.voteCount,
|
|
125
|
+
emoji: answer.emoji ? {
|
|
126
|
+
animated: answer.emoji.animated ?? false,
|
|
127
|
+
id: answer.emoji.id,
|
|
128
|
+
name: answer.emoji.name,
|
|
129
|
+
} : null,
|
|
130
|
+
id: answer.id,
|
|
131
|
+
text: answer.text ?? "",
|
|
132
|
+
})),
|
|
133
|
+
expiry: message.poll.expiresTimestamp ? formatTimeLeftPoll(message.poll.expiresTimestamp) : null,
|
|
134
|
+
isFinalized: message.poll.resultsFinalized,
|
|
135
|
+
question: message.poll.question.text ?? "",
|
|
136
|
+
} : null,
|
|
137
|
+
reactions: options.includeReactions ? message.reactions.cache.map(reaction => {
|
|
138
|
+
if (reaction.emoji.name == null)
|
|
139
|
+
return null;
|
|
140
|
+
return {
|
|
141
|
+
count: reaction.count,
|
|
142
|
+
emoji: reaction.emoji.name,
|
|
143
|
+
};
|
|
144
|
+
}).filter(r => r != null) : [],
|
|
145
|
+
references: message.reference ? { messageId: message.reference.messageId ?? null } : null,
|
|
146
|
+
system: message.system,
|
|
147
|
+
};
|
|
148
|
+
}));
|
|
149
|
+
const messages = rawMessages.filter(m => !(!options.includeEmpty && m.attachments.length == 0 && m.components.length == 0 && m.content == "" && m.embeds.length == 0 && !m.poll));
|
|
150
|
+
const end = originalMessages.size !== 100;
|
|
151
|
+
return { messages, end };
|
|
152
|
+
}
|
|
153
|
+
function formatTimeLeftPoll(timestamp) {
|
|
154
|
+
const now = new Date();
|
|
155
|
+
const leftDate = new Date(timestamp);
|
|
156
|
+
const diffSeconds = Math.floor((leftDate.getTime() - now.getTime()) / 1000);
|
|
157
|
+
const day = Math.floor(diffSeconds / 86400);
|
|
158
|
+
if (day > 0)
|
|
159
|
+
return `${day}d left`;
|
|
160
|
+
const hour = Math.floor(diffSeconds / 3600);
|
|
161
|
+
if (hour > 0)
|
|
162
|
+
return `${hour}h left`;
|
|
163
|
+
const min = Math.floor(diffSeconds / 60);
|
|
164
|
+
if (min > 0)
|
|
165
|
+
return `${min}m left`;
|
|
166
|
+
if (diffSeconds > 0)
|
|
167
|
+
return `${diffSeconds}s left`;
|
|
168
|
+
return "now"; // fallback
|
|
169
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { ChannelType } from "discord.js";
|
|
2
|
+
export function getMentions(message, mentions) {
|
|
3
|
+
message.mentions.channels.forEach(channel => {
|
|
4
|
+
if (!mentions.channels.has(channel.id)) {
|
|
5
|
+
mentions.channels.set(channel.id, {
|
|
6
|
+
id: channel.id,
|
|
7
|
+
name: channel.type !== ChannelType.DM ? channel.name : channel.recipient?.displayName ?? null
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
message.mentions.roles.forEach(role => {
|
|
12
|
+
if (!mentions.roles.has(role.id)) {
|
|
13
|
+
mentions.roles.set(role.id, {
|
|
14
|
+
id: role.id,
|
|
15
|
+
color: role.hexColor,
|
|
16
|
+
name: role.name
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
if (message.guild) {
|
|
21
|
+
}
|
|
22
|
+
if (message.mentions.members) {
|
|
23
|
+
message.mentions.members.forEach(member => {
|
|
24
|
+
if (!mentions.users.has(member.id)) {
|
|
25
|
+
mentions.users.set(member.id, {
|
|
26
|
+
id: member.id,
|
|
27
|
+
color: member.displayHexColor,
|
|
28
|
+
name: member.displayName
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
message.mentions.users.forEach(user => {
|
|
35
|
+
if (!mentions.users.has(user.id)) {
|
|
36
|
+
mentions.users.set(user.id, {
|
|
37
|
+
id: user.id,
|
|
38
|
+
color: user.hexAccentColor ?? null,
|
|
39
|
+
name: user.displayName
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
fetchRoleMention(message, mentions);
|
|
45
|
+
fetchChannelMention(message, mentions);
|
|
46
|
+
fetchUserMention(message, mentions);
|
|
47
|
+
}
|
|
48
|
+
// Needs to fix that sometimes discord laks to provide all roles mentions in a message
|
|
49
|
+
function fetchRoleMention(message, mentions) {
|
|
50
|
+
const roleIds = [];
|
|
51
|
+
for (const match of message.content.matchAll(/<@&(\d+)>/g)) {
|
|
52
|
+
const roleId = match[1];
|
|
53
|
+
if (roleId && !roleIds.includes(roleId) && !mentions.roles.has(roleId)) {
|
|
54
|
+
roleIds.push(roleId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
roleIds.forEach(async (id) => {
|
|
58
|
+
const role = await message.guild?.roles.fetch(id);
|
|
59
|
+
if (!role)
|
|
60
|
+
return;
|
|
61
|
+
mentions.roles.set(role.id, { id: role.id, color: role.hexColor, name: role.name });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function fetchUserMention(message, mentions) {
|
|
65
|
+
const usersId = [];
|
|
66
|
+
for (const match of message.content.matchAll(/<@(\d+)>/g)) {
|
|
67
|
+
const userId = match[1];
|
|
68
|
+
if (userId && !usersId.includes(userId) && !mentions.roles.has(userId)) {
|
|
69
|
+
usersId.push(userId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
usersId.forEach(async (id) => {
|
|
73
|
+
if (message.guild) {
|
|
74
|
+
const user = await message.guild.members.fetch(id);
|
|
75
|
+
if (!user)
|
|
76
|
+
return;
|
|
77
|
+
mentions.users.set(user.id, { id: user.id, color: user.displayHexColor, name: user.displayName });
|
|
78
|
+
}
|
|
79
|
+
const user = await message.client.users.fetch(id);
|
|
80
|
+
if (!user)
|
|
81
|
+
return;
|
|
82
|
+
mentions.users.set(user.id, { id: user.id, color: user.hexAccentColor ?? null, name: user.displayName });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function fetchChannelMention(message, mentions) {
|
|
86
|
+
const channelIds = [];
|
|
87
|
+
for (const match of message.content.matchAll(/<#(\d+)>/g)) {
|
|
88
|
+
const channelId = match[1];
|
|
89
|
+
if (channelId && !channelIds.includes(channelId) && !mentions.channels.has(channelId)) {
|
|
90
|
+
channelIds.push(channelId);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
channelIds.forEach(async (id) => {
|
|
94
|
+
const channel = await message.guild?.channels.fetch(id);
|
|
95
|
+
if (!channel)
|
|
96
|
+
return;
|
|
97
|
+
mentions.channels.set(channel.id, { id: channel.id, name: channel.name });
|
|
98
|
+
});
|
|
99
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function urlToBase64(url: string): Promise<string>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
import { CustomError } from 'discord-message-transcript-base';
|
|
3
|
+
export async function urlToBase64(url) {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
const request = https.get(url, (response) => {
|
|
6
|
+
if (response.statusCode !== 200) {
|
|
7
|
+
reject(new CustomError(`Failed to fetch image with status code: ${response.statusCode} from ${url}`));
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
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;
|
|
14
|
+
}
|
|
15
|
+
const chunks = [];
|
|
16
|
+
response.on('data', (chunk) => {
|
|
17
|
+
chunks.push(chunk);
|
|
18
|
+
});
|
|
19
|
+
response.on('end', () => {
|
|
20
|
+
const buffer = Buffer.concat(chunks);
|
|
21
|
+
const base64 = buffer.toString('base64');
|
|
22
|
+
resolve(`data:${contentType};base64,${base64}`);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
request.on('error', (err) => {
|
|
26
|
+
reject(new CustomError(`Error fetching image from ${url}: ${err.message}`));
|
|
27
|
+
});
|
|
28
|
+
request.end();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
|
|
2
|
+
import { JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
|
|
3
|
+
import { ReturnType } from "../types/types.js";
|
|
4
|
+
export declare function mapButtonStyle(style: ButtonStyle): JsonButtonStyle;
|
|
5
|
+
export declare function mapSeparatorSpacing(spacing: SeparatorSpacingSize): JsonSeparatorSpacingSize;
|
|
6
|
+
export declare function mapComponentType(componentType: ComponentType): JsonComponentType;
|
|
7
|
+
export declare function mapSelectorType(selectorType: ComponentType.UserSelect | ComponentType.RoleSelect | ComponentType.MentionableSelect | ComponentType.ChannelSelect): JsonComponentType.UserSelect | JsonComponentType.RoleSelect | JsonComponentType.MentionableSelect | JsonComponentType.ChannelSelect;
|
|
8
|
+
export declare function returnTypeMapper(type: ReturnType): ReturnTypeBase;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { ButtonStyle, ComponentType, SeparatorSpacingSize } from "discord.js";
|
|
2
|
+
import { CustomError, JsonButtonStyle, JsonComponentType, JsonSeparatorSpacingSize, ReturnTypeBase } from "discord-message-transcript-base";
|
|
3
|
+
import { ReturnType } from "../types/types.js";
|
|
4
|
+
export function mapButtonStyle(style) {
|
|
5
|
+
switch (style) {
|
|
6
|
+
case ButtonStyle.Primary:
|
|
7
|
+
return JsonButtonStyle.Primary;
|
|
8
|
+
case ButtonStyle.Secondary:
|
|
9
|
+
return JsonButtonStyle.Secondary;
|
|
10
|
+
case ButtonStyle.Success:
|
|
11
|
+
return JsonButtonStyle.Success;
|
|
12
|
+
case ButtonStyle.Danger:
|
|
13
|
+
return JsonButtonStyle.Danger;
|
|
14
|
+
case ButtonStyle.Link:
|
|
15
|
+
return JsonButtonStyle.Link;
|
|
16
|
+
case ButtonStyle.Premium:
|
|
17
|
+
return JsonButtonStyle.Premium;
|
|
18
|
+
default:
|
|
19
|
+
throw new CustomError(`Unknow ButtonStyle: ${style}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function mapSeparatorSpacing(spacing) {
|
|
23
|
+
switch (spacing) {
|
|
24
|
+
case SeparatorSpacingSize.Small:
|
|
25
|
+
return JsonSeparatorSpacingSize.Small;
|
|
26
|
+
case SeparatorSpacingSize.Large:
|
|
27
|
+
return JsonSeparatorSpacingSize.Large;
|
|
28
|
+
default:
|
|
29
|
+
throw new CustomError(`Unknow SeparatorSpacingSize: ${spacing}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function mapComponentType(componentType) {
|
|
33
|
+
switch (componentType) {
|
|
34
|
+
case ComponentType.ActionRow:
|
|
35
|
+
return JsonComponentType.ActionRow;
|
|
36
|
+
case ComponentType.Button:
|
|
37
|
+
return JsonComponentType.Button;
|
|
38
|
+
case ComponentType.StringSelect:
|
|
39
|
+
return JsonComponentType.StringSelect;
|
|
40
|
+
case ComponentType.TextInput:
|
|
41
|
+
return JsonComponentType.TextInput;
|
|
42
|
+
case ComponentType.UserSelect:
|
|
43
|
+
return JsonComponentType.UserSelect;
|
|
44
|
+
case ComponentType.RoleSelect:
|
|
45
|
+
return JsonComponentType.RoleSelect;
|
|
46
|
+
case ComponentType.MentionableSelect:
|
|
47
|
+
return JsonComponentType.MentionableSelect;
|
|
48
|
+
case ComponentType.ChannelSelect:
|
|
49
|
+
return JsonComponentType.ChannelSelect;
|
|
50
|
+
case ComponentType.Section:
|
|
51
|
+
return JsonComponentType.Section;
|
|
52
|
+
case ComponentType.TextDisplay:
|
|
53
|
+
return JsonComponentType.TextDisplay;
|
|
54
|
+
case ComponentType.Thumbnail:
|
|
55
|
+
return JsonComponentType.Thumbnail;
|
|
56
|
+
case ComponentType.MediaGallery:
|
|
57
|
+
return JsonComponentType.MediaGallery;
|
|
58
|
+
case ComponentType.File:
|
|
59
|
+
return JsonComponentType.File;
|
|
60
|
+
case ComponentType.Separator:
|
|
61
|
+
return JsonComponentType.Separator;
|
|
62
|
+
case ComponentType.ContentInventoryEntry:
|
|
63
|
+
return JsonComponentType.ContentInventoryEntry;
|
|
64
|
+
case ComponentType.Container:
|
|
65
|
+
return JsonComponentType.Container;
|
|
66
|
+
case ComponentType.Label:
|
|
67
|
+
return JsonComponentType.Label;
|
|
68
|
+
case ComponentType.FileUpload:
|
|
69
|
+
return JsonComponentType.FileUpload;
|
|
70
|
+
default:
|
|
71
|
+
throw new CustomError(`Unknow ComponentType: ${componentType}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
export function mapSelectorType(selectorType) {
|
|
75
|
+
switch (selectorType) {
|
|
76
|
+
case ComponentType.UserSelect:
|
|
77
|
+
return JsonComponentType.UserSelect;
|
|
78
|
+
case ComponentType.RoleSelect:
|
|
79
|
+
return JsonComponentType.RoleSelect;
|
|
80
|
+
case ComponentType.MentionableSelect:
|
|
81
|
+
return JsonComponentType.MentionableSelect;
|
|
82
|
+
case ComponentType.ChannelSelect:
|
|
83
|
+
return JsonComponentType.ChannelSelect;
|
|
84
|
+
default:
|
|
85
|
+
throw new CustomError(`Unknow SelectorComponentType: ${selectorType}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function returnTypeMapper(type) {
|
|
89
|
+
switch (type) {
|
|
90
|
+
case ReturnType.Buffer:
|
|
91
|
+
return ReturnTypeBase.Buffer;
|
|
92
|
+
case ReturnType.Stream:
|
|
93
|
+
return ReturnTypeBase.Stream;
|
|
94
|
+
case ReturnType.String:
|
|
95
|
+
return ReturnTypeBase.String;
|
|
96
|
+
case ReturnType.Uploadable:
|
|
97
|
+
return ReturnTypeBase.Uploadable;
|
|
98
|
+
default:
|
|
99
|
+
throw new CustomError(`Can't convert ReturnType.Attachment to ReturnTypeBase!`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
export function markdownToHTML(text, mentions, dateFormat) {
|
|
2
|
+
const codeBlock = [];
|
|
3
|
+
const codeLine = [];
|
|
4
|
+
// Code Block (```)
|
|
5
|
+
text = text.replace(/```(?:(\S+)\n)?([\s\S]+?)```/g, (_m, lang, code) => {
|
|
6
|
+
const rawLang = lang?.toLowerCase();
|
|
7
|
+
const normalizedLang = rawLang ? (LANGUAGE_ALIAS[rawLang] ?? rawLang) : null;
|
|
8
|
+
const language = normalizedLang && SUPPORTED_LANGUAGES.has(rawLang) ? normalizedLang : 'plaintext';
|
|
9
|
+
codeBlock.push(`<pre><code class="language-${language}">${code.trimEnd()}</code></pre>`);
|
|
10
|
+
return `%$%CODE!BLOCK!${codeBlock.length - 1}%$%`;
|
|
11
|
+
});
|
|
12
|
+
// Code line (`)
|
|
13
|
+
text = text.replace(/`([^`]+)`/g, (_m, code) => {
|
|
14
|
+
codeLine.push(`<code>${code}</code>`);
|
|
15
|
+
return `%$%CODE!LINE!${codeLine.length - 1}%$%`;
|
|
16
|
+
});
|
|
17
|
+
// Citation (> | >>>)
|
|
18
|
+
text = text.replace(/(^> ?.*(?:(?:\n^> ?.*)+)?)/gm, (match) => {
|
|
19
|
+
const cleanContent = match.split('\n').map(line => {
|
|
20
|
+
return line.replace(/^>+ ?/, '');
|
|
21
|
+
}).join('\n');
|
|
22
|
+
return `<blockquote class="quote-multi">${cleanContent}</blockquote>`;
|
|
23
|
+
});
|
|
24
|
+
// Headers (#)
|
|
25
|
+
text = text.replace(/^### (.*)(?=\n|$)/gm, `<h3>$1</h3>`);
|
|
26
|
+
text = text.replace(/^## (.*)(?=\n|$)/gm, `<h2>$1</h2>`);
|
|
27
|
+
text = text.replace(/^# (.*)(?=\n|$)/gm, `<h1>$1</h1>`);
|
|
28
|
+
// Subtext(-#)
|
|
29
|
+
text = text.replace(/^-# (.*)(?=\n|$)/gm, `<p class="subtext">$1</p>`);
|
|
30
|
+
// List (- | *)
|
|
31
|
+
text = text.replace(/^(\s*)[-*] (.*)(?=\n|$)/gm, (_m, indentation, text) => {
|
|
32
|
+
const isSubItem = indentation.length > 0;
|
|
33
|
+
const bullet = isSubItem ? '◦' : '•';
|
|
34
|
+
return `<p class="pList">${indentation}${bullet} ${text}</p>`;
|
|
35
|
+
});
|
|
36
|
+
// Spoiler (||)
|
|
37
|
+
text = text.replace(/\|\|(.*?)\|\|/gs, `<span class="spoilerMsg">$1</span>`);
|
|
38
|
+
// Bold & Italic (***)
|
|
39
|
+
text = text.replace(/\*\*\*(.*?)\*\*\*/gs, `<strong><em>$1</em></strong>`);
|
|
40
|
+
// Bold (**)
|
|
41
|
+
text = text.replace(/\*\*(.*?)\*\*/gs, `<strong>$1</strong>`);
|
|
42
|
+
// Underline(__)
|
|
43
|
+
text = text.replace(/__(.*?)__/gs, `<u>$1</u>`);
|
|
44
|
+
// Italic (*)
|
|
45
|
+
text = text.replace(/\*(.*?)\*/gs, `<em>$1</em>`);
|
|
46
|
+
text = text.replace(/\_(.*?)\_/gs, `<em>$1</em>`);
|
|
47
|
+
// Strikethrough (~~)
|
|
48
|
+
text = text.replace(/~~(.*?)~~/gs, `<s>$1</s>`);
|
|
49
|
+
// Links ([]() && https)
|
|
50
|
+
text = text.replace(/\[([^\]]+)\]\((https?:\/\/[^\s]+)\)/g, (_m, text, link) => `<a href="${link}" target="_blank">${text}</a>`);
|
|
51
|
+
text = text.replace(/(?<!href=")(https?:\/\/[^\s]+)/g, (_m, link) => `<a href="${link}" target="_blank">${link}</a>`);
|
|
52
|
+
// Mentions (@)
|
|
53
|
+
if (mentions.users.length != 0) {
|
|
54
|
+
const users = new Map(mentions.users.map(user => [user.id, user]));
|
|
55
|
+
text = text.replace(/<@!?(\d+)>/g, (_m, id) => {
|
|
56
|
+
let user = users.get(id);
|
|
57
|
+
return user ? `<span class="mention" style="color: ${user.color ?? "#dbdee1"}">@${user.name}</span> ` : `<span class="mention"><@${id}></span> `;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (mentions.roles.length != 0) {
|
|
61
|
+
const roles = new Map(mentions.roles.map(role => [role.id, role]));
|
|
62
|
+
text = text.replace(/<@&(\d+)>/g, (_m, id) => {
|
|
63
|
+
const role = roles.get(id);
|
|
64
|
+
return role ? `<span class="mention" style="color: ${role.color}">@${role.name}</span> ` : `<span class="mention"><@&${id}></span> `;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (mentions.channels.length != 0) {
|
|
68
|
+
const channels = new Map(mentions.channels.map(channel => [channel.id, channel]));
|
|
69
|
+
text = text.replace(/<#(\d+)>/g, (_m, id) => {
|
|
70
|
+
const channel = channels.get(id);
|
|
71
|
+
return channel && channel.name ? `<span class="mention">#${channel.name}</span> ` : `<span class="mention"><#${id}></span> `;
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (mentions.everyone) {
|
|
75
|
+
text = text.replace("@everyone", `<span class="mention">@everyone</span> `);
|
|
76
|
+
text = text.replace("@here", `<span class="mention">@here</span> `);
|
|
77
|
+
}
|
|
78
|
+
// Timestamp
|
|
79
|
+
const { locale, timeZone } = dateFormat.resolvedOptions();
|
|
80
|
+
text = text.replace(/<t:(\d+)(?::([tTdDfFR]))?>/g, (_m, timestamp, format) => {
|
|
81
|
+
const date = new Date(parseInt(timestamp, 10) * 1000);
|
|
82
|
+
const style = format || 'f';
|
|
83
|
+
const isoString = date.toISOString();
|
|
84
|
+
const titleFormatter = new Intl.DateTimeFormat(locale, {
|
|
85
|
+
dateStyle: 'full',
|
|
86
|
+
timeStyle: 'full',
|
|
87
|
+
timeZone: timeZone,
|
|
88
|
+
});
|
|
89
|
+
const fullDateForTitle = titleFormatter.format(date);
|
|
90
|
+
if (style === 'R') {
|
|
91
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' });
|
|
92
|
+
const seconds = Math.floor((date.getTime() - Date.now()) / 1000);
|
|
93
|
+
const minutes = Math.floor(seconds / 60);
|
|
94
|
+
const hours = Math.floor(minutes / 60);
|
|
95
|
+
const days = Math.floor(hours / 24);
|
|
96
|
+
let relativeString;
|
|
97
|
+
if (Math.abs(days) > 30)
|
|
98
|
+
relativeString = rtf.format(Math.floor(days / 30.44), 'month');
|
|
99
|
+
else if (Math.abs(days) > 0)
|
|
100
|
+
relativeString = rtf.format(days, 'day');
|
|
101
|
+
else if (Math.abs(hours) > 0)
|
|
102
|
+
relativeString = rtf.format(hours, 'hour');
|
|
103
|
+
else if (Math.abs(minutes) > 0)
|
|
104
|
+
relativeString = rtf.format(minutes, 'minute');
|
|
105
|
+
else
|
|
106
|
+
relativeString = rtf.format(seconds, 'second');
|
|
107
|
+
return `<time datetime="${isoString}" title="${fullDateForTitle}">${relativeString}</time>`;
|
|
108
|
+
}
|
|
109
|
+
else if (isStyleKey(style)) {
|
|
110
|
+
const formatter = new Intl.DateTimeFormat(locale, {
|
|
111
|
+
...styleOptions[style],
|
|
112
|
+
timeZone: timeZone,
|
|
113
|
+
});
|
|
114
|
+
const formattedDate = formatter.format(date);
|
|
115
|
+
return `<time datetime="${isoString}" title="${fullDateForTitle}">${formattedDate}</time>`;
|
|
116
|
+
}
|
|
117
|
+
return _m;
|
|
118
|
+
});
|
|
119
|
+
// Break Line
|
|
120
|
+
text = text.replace(/\n/g, '<br>');
|
|
121
|
+
// Clear Unecessary Break Line
|
|
122
|
+
text = text.replace(/(<\/(?:p|h[1-3]|blockquote)>)\s*<br>/g, '$1');
|
|
123
|
+
// Remove Placeholders
|
|
124
|
+
text = text.replace(/%\$%CODE!BLOCK!(\d+)%\$%/g, (_m, number) => {
|
|
125
|
+
return codeBlock[number];
|
|
126
|
+
});
|
|
127
|
+
text = text.replace(/%\$%CODE!LINE!(\d+)%\$%/g, (_m, number) => {
|
|
128
|
+
return codeLine[number];
|
|
129
|
+
});
|
|
130
|
+
return text;
|
|
131
|
+
}
|
|
132
|
+
// Check if styleKey is valid
|
|
133
|
+
const styleOptions = {
|
|
134
|
+
't': { timeStyle: 'short' },
|
|
135
|
+
'T': { timeStyle: 'medium' },
|
|
136
|
+
'd': { dateStyle: 'short' },
|
|
137
|
+
'D': { dateStyle: 'long' },
|
|
138
|
+
'f': { dateStyle: 'long', timeStyle: 'short' },
|
|
139
|
+
'F': { dateStyle: 'full', timeStyle: 'short' },
|
|
140
|
+
};
|
|
141
|
+
function isStyleKey(key) {
|
|
142
|
+
return key in styleOptions;
|
|
143
|
+
}
|
|
144
|
+
// At least I hope
|
|
145
|
+
const SUPPORTED_LANGUAGES = new Set([
|
|
146
|
+
'bash', 'sh', 'shell',
|
|
147
|
+
'c',
|
|
148
|
+
'cpp',
|
|
149
|
+
'css',
|
|
150
|
+
'javascript', 'js',
|
|
151
|
+
'typescript', 'ts',
|
|
152
|
+
'json',
|
|
153
|
+
'xml',
|
|
154
|
+
'yaml', 'yml',
|
|
155
|
+
'java',
|
|
156
|
+
'kotlin',
|
|
157
|
+
'php',
|
|
158
|
+
'python', 'py',
|
|
159
|
+
'ruby', 'rb',
|
|
160
|
+
'sql',
|
|
161
|
+
'lua',
|
|
162
|
+
'markdown', 'md',
|
|
163
|
+
'plaintext', 'txt'
|
|
164
|
+
]);
|
|
165
|
+
const LANGUAGE_ALIAS = {
|
|
166
|
+
sh: 'bash',
|
|
167
|
+
shell: 'bash',
|
|
168
|
+
js: 'javascript',
|
|
169
|
+
ts: 'typescript',
|
|
170
|
+
py: 'python',
|
|
171
|
+
rb: 'ruby',
|
|
172
|
+
md: 'markdown',
|
|
173
|
+
yml: 'yaml',
|
|
174
|
+
txt: 'plaintext'
|
|
175
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
import { outputBase } from "discord-message-transcript-base";
|
|
3
|
+
export async function output(json) {
|
|
4
|
+
const stringJSON = JSON.stringify(json);
|
|
5
|
+
if (json.options.returnFormat == "JSON") {
|
|
6
|
+
if (json.options.returnType == "string") {
|
|
7
|
+
return stringJSON;
|
|
8
|
+
}
|
|
9
|
+
const buffer = Buffer.from(stringJSON, 'utf-8');
|
|
10
|
+
if (json.options.returnType == "buffer") {
|
|
11
|
+
return buffer;
|
|
12
|
+
}
|
|
13
|
+
if (json.options.returnType == "stream") {
|
|
14
|
+
return Readable.from([stringJSON]);
|
|
15
|
+
}
|
|
16
|
+
if (json.options.returnType == "uploadable") {
|
|
17
|
+
return {
|
|
18
|
+
content: stringJSON,
|
|
19
|
+
contentType: 'application/json',
|
|
20
|
+
fileName: json.options.fileName
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (json.options.returnFormat == "HTML") {
|
|
25
|
+
return await outputBase(json);
|
|
26
|
+
}
|
|
27
|
+
throw new Error("Return format or return type invalid!");
|
|
28
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export { CreateTranscriptOptions, ConvertTranscriptOptions, TranscriptOptions, ReturnType } from "./types/types.js";
|
|
2
|
+
export { ReturnFormat, LocalDate, TimeZone } from "discord-message-transcript-base";
|
|
3
|
+
import { TextBasedChannel } from "discord.js";
|
|
4
|
+
import { ConvertTranscriptOptions, CreateTranscriptOptions, OutputType, ReturnType } from "./types/types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a transcript of a Discord channel's messages.
|
|
7
|
+
* Depending on the `returnType` option, this function can return an `AttachmentBuilder`,
|
|
8
|
+
* a `string` (for HTML or JSON), a `Buffer`, a `Stream`, or an `Uploadable` object.
|
|
9
|
+
*
|
|
10
|
+
* @param channel The Discord text-based channel (e.g., `TextChannel`, `DMChannel`) to create a transcript from.
|
|
11
|
+
* @param options Configuration options for creating the transcript. See {@link CreateTranscriptOptions} for details.
|
|
12
|
+
* @returns A promise that resolves to the transcript in the specified format.
|
|
13
|
+
*/
|
|
14
|
+
export declare function createTranscript<T extends ReturnType = typeof ReturnType.Attachment>(channel: TextBasedChannel, options?: CreateTranscriptOptions<T>): Promise<OutputType<T>>;
|
|
15
|
+
/**
|
|
16
|
+
* Converts a JSON transcript string into an HTML transcript.
|
|
17
|
+
* Depending on the `returnType` option, this function can return an `AttachmentBuilder`,
|
|
18
|
+
* a `string`, a `Buffer`, a `Stream`, or an `Uploadable` object.
|
|
19
|
+
*
|
|
20
|
+
* @param jsonString The JSON string representing the transcript data.
|
|
21
|
+
* @param options Configuration options for converting the transcript. See {@link ConvertTranscriptOptions} for details.
|
|
22
|
+
* @returns A promise that resolves to the HTML transcript in the specified format.
|
|
23
|
+
*/
|
|
24
|
+
export declare function renderHTMLFromJSON<T extends ReturnType = typeof ReturnType.Attachment>(jsonString: string, options?: ConvertTranscriptOptions<T>): Promise<OutputType<T>>;
|