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.
Files changed (44) hide show
  1. package/LICENSE +201 -0
  2. package/dist/core/clientManager.d.ts +3 -0
  3. package/dist/core/clientManager.js +9 -0
  4. package/dist/core/componentHelpers.d.ts +3 -0
  5. package/dist/core/componentHelpers.js +175 -0
  6. package/dist/core/componentToJson.d.ts +3 -0
  7. package/dist/core/componentToJson.js +174 -0
  8. package/dist/core/error.d.ts +3 -0
  9. package/dist/core/error.js +7 -0
  10. package/dist/core/fetchMessages.d.ts +7 -0
  11. package/dist/core/fetchMessages.js +169 -0
  12. package/dist/core/getMentions.d.ts +3 -0
  13. package/dist/core/getMentions.js +99 -0
  14. package/dist/core/imageToBase64.d.ts +1 -0
  15. package/dist/core/imageToBase64.js +30 -0
  16. package/dist/core/mappers.d.ts +8 -0
  17. package/dist/core/mappers.js +101 -0
  18. package/dist/core/markdown.d.ts +2 -0
  19. package/dist/core/markdown.js +175 -0
  20. package/dist/core/output.d.ts +4 -0
  21. package/dist/core/output.js +28 -0
  22. package/dist/index.d.ts +24 -0
  23. package/dist/index.js +120 -0
  24. package/dist/renderers/html/clientRenderer.d.ts +0 -0
  25. package/dist/renderers/html/clientRenderer.js +73 -0
  26. package/dist/renderers/html/css.d.ts +11 -0
  27. package/dist/renderers/html/css.js +663 -0
  28. package/dist/renderers/html/html copy.d.ts +19 -0
  29. package/dist/renderers/html/html copy.js +371 -0
  30. package/dist/renderers/html/html-backup.d.ts +19 -0
  31. package/dist/renderers/html/html-backup.js +371 -0
  32. package/dist/renderers/html/html.d.ts +19 -0
  33. package/dist/renderers/html/html.js +415 -0
  34. package/dist/renderers/html/html2.d.ts +8 -0
  35. package/dist/renderers/html/html2.js +233 -0
  36. package/dist/renderers/html/js.d.ts +4 -0
  37. package/dist/renderers/html/js.js +174 -0
  38. package/dist/renderers/json/json.d.ts +16 -0
  39. package/dist/renderers/json/json.js +55 -0
  40. package/dist/types/types copy.d.ts +284 -0
  41. package/dist/types/types copy.js +35 -0
  42. package/dist/types/types.d.ts +137 -0
  43. package/dist/types/types.js +7 -0
  44. 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,3 @@
1
+ import { Message } from "discord.js";
2
+ import { MapMentions } from "../types/types.js";
3
+ export declare function getMentions(message: Message, mentions: MapMentions): void;
@@ -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,2 @@
1
+ import { JsonMessageMentions } from "../types/types";
2
+ export declare function markdownToHTML(text: string, mentions: JsonMessageMentions, dateFormat: Intl.DateTimeFormat): string;
@@ -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,4 @@
1
+ import { AttachmentBuilder } from "discord.js";
2
+ import Stream from 'stream';
3
+ import { JsonData, Uploadable } from "discord-message-transcript-base";
4
+ export declare function output(json: JsonData): Promise<string | Stream | AttachmentBuilder | Buffer | Uploadable>;
@@ -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
+ }
@@ -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>>;