transcriptify 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 (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/dist/src/generateHtml.d.ts +3 -0
  4. package/dist/src/generateHtml.js +303 -0
  5. package/dist/src/index.d.ts +5 -0
  6. package/dist/src/index.js +252 -0
  7. package/dist/src/transcript.d.ts +12 -0
  8. package/dist/src/transcript.js +179 -0
  9. package/dist/src/types/entities.d.ts +30 -0
  10. package/dist/src/types/entities.js +2 -0
  11. package/dist/src/utils/assetManager.d.ts +12 -0
  12. package/dist/src/utils/assetManager.js +295 -0
  13. package/dist/src/utils/authors.d.ts +4 -0
  14. package/dist/src/utils/authors.js +115 -0
  15. package/dist/src/utils/cache.d.ts +16 -0
  16. package/dist/src/utils/cache.js +29 -0
  17. package/dist/src/utils/extractors.d.ts +112 -0
  18. package/dist/src/utils/extractors.js +223 -0
  19. package/dist/src/utils/polls.d.ts +2 -0
  20. package/dist/src/utils/polls.js +91 -0
  21. package/dist/src/utils/transformer.d.ts +6 -0
  22. package/dist/src/utils/transformer.js +78 -0
  23. package/dist/src/utils/user.d.ts +4 -0
  24. package/dist/src/utils/user.js +56 -0
  25. package/dist/src/web/client.d.ts +20 -0
  26. package/dist/src/web/client.js +21 -0
  27. package/dist/src/web/discord-components/AudioPlayer.d.ts +5 -0
  28. package/dist/src/web/discord-components/AudioPlayer.js +231 -0
  29. package/dist/src/web/discord-components/Button.d.ts +3 -0
  30. package/dist/src/web/discord-components/Button.js +27 -0
  31. package/dist/src/web/discord-components/ChannelPinnedMessage.d.ts +7 -0
  32. package/dist/src/web/discord-components/ChannelPinnedMessage.js +11 -0
  33. package/dist/src/web/discord-components/DateSeperator.d.ts +3 -0
  34. package/dist/src/web/discord-components/DateSeperator.js +19 -0
  35. package/dist/src/web/discord-components/Embed.d.ts +2 -0
  36. package/dist/src/web/discord-components/Embed.js +78 -0
  37. package/dist/src/web/discord-components/ForwardedMessage.d.ts +2 -0
  38. package/dist/src/web/discord-components/ForwardedMessage.js +44 -0
  39. package/dist/src/web/discord-components/Message.d.ts +2 -0
  40. package/dist/src/web/discord-components/Message.js +543 -0
  41. package/dist/src/web/discord-components/PinnedMessagesModal.d.ts +6 -0
  42. package/dist/src/web/discord-components/PinnedMessagesModal.js +119 -0
  43. package/dist/src/web/discord-components/PinnedMessagesOverview.d.ts +5 -0
  44. package/dist/src/web/discord-components/PinnedMessagesOverview.js +22 -0
  45. package/dist/src/web/discord-components/Reply.d.ts +2 -0
  46. package/dist/src/web/discord-components/Reply.js +42 -0
  47. package/dist/src/web/discord-components/StickerPreview.d.ts +6 -0
  48. package/dist/src/web/discord-components/StickerPreview.js +40 -0
  49. package/dist/src/web/discord-components/ThemeSwitcher.d.ts +2 -0
  50. package/dist/src/web/discord-components/ThemeSwitcher.js +54 -0
  51. package/dist/src/web/discord-components/Transcript.d.ts +2 -0
  52. package/dist/src/web/discord-components/Transcript.js +174 -0
  53. package/dist/src/web/discord-components/UserJoinMessage.d.ts +3 -0
  54. package/dist/src/web/discord-components/UserJoinMessage.js +33 -0
  55. package/dist/src/web/discord-components/VideoPlayer.d.ts +6 -0
  56. package/dist/src/web/discord-components/VideoPlayer.js +222 -0
  57. package/dist/src/web/discord-components/icons/ChevronDownIcon.d.ts +1 -0
  58. package/dist/src/web/discord-components/icons/ChevronDownIcon.js +7 -0
  59. package/dist/src/web/discord-components/icons/CloseIcon.d.ts +1 -0
  60. package/dist/src/web/discord-components/icons/CloseIcon.js +7 -0
  61. package/dist/src/web/discord-components/icons/ExternalLinkIcon.d.ts +1 -0
  62. package/dist/src/web/discord-components/icons/ExternalLinkIcon.js +7 -0
  63. package/dist/src/web/discord-components/icons/FileAudioIcon.d.ts +1 -0
  64. package/dist/src/web/discord-components/icons/FileAudioIcon.js +7 -0
  65. package/dist/src/web/discord-components/icons/FileCodeIcon.d.ts +1 -0
  66. package/dist/src/web/discord-components/icons/FileCodeIcon.js +7 -0
  67. package/dist/src/web/discord-components/icons/FileDocumentIcon.d.ts +1 -0
  68. package/dist/src/web/discord-components/icons/FileDocumentIcon.js +7 -0
  69. package/dist/src/web/discord-components/icons/PinIcon.d.ts +1 -0
  70. package/dist/src/web/discord-components/icons/PinIcon.js +7 -0
  71. package/dist/src/web/discord-components/icons/VerifiedIcon.d.ts +1 -0
  72. package/dist/src/web/discord-components/icons/VerifiedIcon.js +7 -0
  73. package/dist/src/web/discord-components/index.d.ts +11 -0
  74. package/dist/src/web/discord-components/index.js +24 -0
  75. package/dist/src/web/discord-components/messageHelpers.d.ts +8 -0
  76. package/dist/src/web/discord-components/messageHelpers.js +72 -0
  77. package/dist/src/web/discord-components/themeColors.d.ts +9 -0
  78. package/dist/src/web/discord-components/themeColors.js +320 -0
  79. package/dist/src/web/discord-components/transcriptHelpers.d.ts +19 -0
  80. package/dist/src/web/discord-components/transcriptHelpers.js +120 -0
  81. package/dist/src/web/discord-components/types.d.ts +1 -0
  82. package/dist/src/web/discord-components/types.js +2 -0
  83. package/dist/src/web/discord-components/utils/date.d.ts +3 -0
  84. package/dist/src/web/discord-components/utils/date.js +50 -0
  85. package/dist/src/web/discord-components/utils/markdown.d.ts +11 -0
  86. package/dist/src/web/discord-components/utils/markdown.js +538 -0
  87. package/dist/src/web/discord-components/utils/markdownUtils.d.ts +12 -0
  88. package/dist/src/web/discord-components/utils/markdownUtils.js +140 -0
  89. package/dist/src/web/helpers/avatarHelpers.d.ts +2 -0
  90. package/dist/src/web/helpers/avatarHelpers.js +15 -0
  91. package/dist/src/web/helpers/cdnHelpers.d.ts +5 -0
  92. package/dist/src/web/helpers/cdnHelpers.js +48 -0
  93. package/dist/src/web/helpers/contentHelpers.d.ts +9 -0
  94. package/dist/src/web/helpers/contentHelpers.js +41 -0
  95. package/dist/src/web/helpers/renderContent.d.ts +2 -0
  96. package/dist/src/web/helpers/renderContent.js +15 -0
  97. package/dist/src/web/helpers/scrollHelpers.d.ts +2 -0
  98. package/dist/src/web/helpers/scrollHelpers.js +31 -0
  99. package/dist/src/web/helpers/timestampHelpers.d.ts +6 -0
  100. package/dist/src/web/helpers/timestampHelpers.js +66 -0
  101. package/dist/src/web/hooks/useMessageContent.d.ts +5 -0
  102. package/dist/src/web/hooks/useMessageContent.js +37 -0
  103. package/dist/src/web/index.d.ts +1 -0
  104. package/dist/src/web/index.js +17 -0
  105. package/dist/src/web/types/attachment.d.ts +6 -0
  106. package/dist/src/web/types/attachment.js +2 -0
  107. package/dist/src/web/types/author.d.ts +14 -0
  108. package/dist/src/web/types/author.js +2 -0
  109. package/dist/src/web/types/channel.d.ts +8 -0
  110. package/dist/src/web/types/channel.js +2 -0
  111. package/dist/src/web/types/embed.d.ts +52 -0
  112. package/dist/src/web/types/embed.js +2 -0
  113. package/dist/src/web/types/interaction.d.ts +8 -0
  114. package/dist/src/web/types/interaction.js +2 -0
  115. package/dist/src/web/types/markdown.d.ts +5 -0
  116. package/dist/src/web/types/markdown.js +2 -0
  117. package/dist/src/web/types/message.d.ts +73 -0
  118. package/dist/src/web/types/message.js +2 -0
  119. package/dist/src/web/types/poll.d.ts +11 -0
  120. package/dist/src/web/types/poll.js +2 -0
  121. package/dist/src/web/types/props.d.ts +155 -0
  122. package/dist/src/web/types/props.js +2 -0
  123. package/dist/src/web/types/reaction.d.ts +6 -0
  124. package/dist/src/web/types/reaction.js +2 -0
  125. package/dist/src/web/types/theme.d.ts +14 -0
  126. package/dist/src/web/types/theme.js +2 -0
  127. package/dist/src/web/types/ui.d.ts +10 -0
  128. package/dist/src/web/types/ui.js +2 -0
  129. package/dist/types/download.d.ts +12 -0
  130. package/dist/types/download.js +2 -0
  131. package/dist/types/exportableTranscript.d.ts +169 -0
  132. package/dist/types/exportableTranscript.js +2 -0
  133. package/dist/types/general.d.ts +90 -0
  134. package/dist/types/general.js +2 -0
  135. package/package.json +46 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # `Transcriptify`
2
+
3
+ [![npm](https://img.shields.io/npm/dw/Transcriptify)](http://npmjs.org/package/Transcriptify)
4
+ ![GitHub package.json version](https://img.shields.io/github/package-json/v/dev-hoehle/transcriptify)
5
+ ![GitHub Repo stars](https://img.shields.io/github/stars/dev-hoehle/transcriptify?style=social)
6
+
7
+
8
+ > Generate beautiful, secure HTML transcripts from Discord channels.
9
+
10
+ - **Parses Discord formatting**: preserves markdown (bold, italics, code, strikethrough), mentions, and embeds.
11
+ - **Attachment-friendly**: renders images, videos, audio and files with optional asset saving.
12
+ - **Safe by default**: built‑in XSS protection and sanitization.
13
+ - **Themeable output**: client-side themes and optional theme switching.
14
+ - **Simple API**: `import { createTranscript } from 'transcriptify'` — call `createTranscript(channel, options)`.
15
+ - **discord.js support**: works with v14 and v15.
16
+
17
+ ## Quickstart
18
+
19
+ Install (for local/dev usage):
20
+
21
+ ```bash
22
+ npm i transcriptify
23
+ ```
24
+
25
+ Create a `.env` with your bot token:
26
+
27
+ ```
28
+ TOKEN=your_bot_token_here
29
+ ```
30
+
31
+ Example ussage:
32
+ ```ts
33
+ import * as discord from "discord.js";
34
+ import { config } from "dotenv";
35
+ import { createTranscript } from "transcriptify";
36
+
37
+ config();
38
+
39
+ const { Guilds, GuildMessages, MessageContent } = discord.GatewayIntentBits;
40
+
41
+ const client = new discord.Client({ intents: [Guilds, GuildMessages, MessageContent] });
42
+
43
+ client.on("ready", async () => {
44
+ console.log("Logged in as", client.user?.tag);
45
+
46
+ const channel = await client.channels.fetch("1469427183960199470");
47
+ if (!channel || !channel.isTextBased()) {
48
+ console.error("Invalid channel provided.");
49
+ process.exit(1);
50
+ }
51
+
52
+ console.time("transcript");
53
+ const attachment = await createTranscript(channel, {
54
+ filename: "test-transcript.html",
55
+ });
56
+
57
+ console.timeEnd("transcript");
58
+
59
+ console.log("✓ Generated:", attachment?.name ?? attachment);
60
+ await client.destroy();
61
+ process.exit(0);
62
+ });
63
+
64
+ client.login(process.env.TOKEN as string);
65
+ ```
66
+
67
+ See the docs for a short usage guide and options: [docs/usage.md](https://github.com/dev-hoehle/transcriptify/docs/usage.md).
68
+
69
+ Preview:
70
+
71
+ ![preview](https://raw.githubusercontent.com/dev-hoehle/transcriptify/docs/assets/preview.gif)
72
+
73
+ ## Credits
74
+
75
+ - Inspired by: [ItzDerock/discord-html-transcripts](https://github.com/ItzDerock/discord-html-transcripts)
76
+ - Design / UI suggestions: GitHub Copilot
77
+
78
+ License: MIT — see [LICENSE](LICENSE)
@@ -0,0 +1,3 @@
1
+ import type { ExportableTranscript } from "../types/exportableTranscript";
2
+ import { TranscriptCreateOptions } from "../types/general";
3
+ export declare function generateHtml(transcript: ExportableTranscript, options?: TranscriptCreateOptions): Promise<string>;
@@ -0,0 +1,303 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.generateHtml = generateHtml;
7
+ const react_1 = __importDefault(require("react"));
8
+ const server_1 = __importDefault(require("react-dom/server"));
9
+ const promises_1 = __importDefault(require("fs/promises"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const Transcript_1 = __importDefault(require("./web/discord-components/Transcript"));
12
+ const assetManager_1 = require("./utils/assetManager");
13
+ const esbuild_1 = __importDefault(require("esbuild"));
14
+ function TranscriptSSRWrapper(props) {
15
+ return react_1.default.createElement(Transcript_1.default, {
16
+ channel: props.channel,
17
+ messages: props.messages,
18
+ theme: props.theme,
19
+ allowThemeSwitching: props.allowThemeSwitching,
20
+ allowThemeSwitchingPersist: props.allowThemeSwitchingPersist,
21
+ poweredBy: props.poweredBy,
22
+ className: props.className || "",
23
+ resolvedUsers: props.resolvedUsers,
24
+ resolvedRoles: props.resolvedRoles,
25
+ resolvedChannels: props.resolvedChannels,
26
+ exportedAt: props.exportedAt
27
+ });
28
+ }
29
+ function buildHtmlDocument(content, transcriptData, clientCode) {
30
+ return `<!DOCTYPE html>
31
+ <html lang="en">
32
+ <head>
33
+ <meta charset="UTF-8">
34
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
35
+ <title>Discord Transcript</title>
36
+ <script src="https://cdn.tailwindcss.com"></script>
37
+ <style id="app-css">[APP_CSS]</style>
38
+ <style id="highlight-css">[HIGHLIGHT_CSS]</style>
39
+ <script>window.__TRANSCRIPT_DATA__=${transcriptData};</script>
40
+ </head>
41
+ <body>
42
+ <div id="root">${content}</div>
43
+ <script type="module">${clientCode}</script>
44
+ </body>
45
+ </html>`;
46
+ }
47
+ async function generateHtml(transcript, options = {}) {
48
+ const { filename, poweredBy = true, allowThemeSwitching = true, theme = "dark", ignore = {} } = options;
49
+ const channelInfo = {
50
+ name: transcript.meta.channelName || "transcript",
51
+ topic: undefined,
52
+ id: transcript.meta.channelId,
53
+ guildId: transcript.meta.guildId ?? null
54
+ };
55
+ const messages = transcript.messages.filter((msg) => {
56
+ const authorId = typeof msg.author === "string" ? msg.author : null;
57
+ if (authorId && Array.isArray(ignore?.userIDs) && ignore.userIDs.includes(authorId))
58
+ return false;
59
+ if (ignore?.bots) {
60
+ const resolved = transcript.resolvedUsers || {};
61
+ const authorData = authorId ? resolved[authorId] : null;
62
+ if (authorData?.bot)
63
+ return false;
64
+ }
65
+ return true;
66
+ });
67
+ let processedMessages = messages;
68
+ let transcriptDataToProcess = null;
69
+ let assetManager = null;
70
+ const saveOpt = options.saveAssets ?? options.saveImages;
71
+ if (!saveOpt) {
72
+ try {
73
+ if (transcript.resolvedUsers && typeof transcript.resolvedUsers === "object") {
74
+ for (const [uid, u] of Object.entries(transcript.resolvedUsers)) {
75
+ try {
76
+ const user = u;
77
+ if (user && typeof user.avatar === "string" && !/^https?:\/\//i.test(user.avatar) && !/^assets\//.test(user.avatar)) {
78
+ const isAnimated = user.avatar.startsWith("a_");
79
+ const ext = isAnimated ? ".gif" : ".png";
80
+ user.avatar = `https://cdn.discordapp.com/avatars/${uid}/${user.avatar}${ext}?size=128`;
81
+ }
82
+ if (user &&
83
+ user.guildTag &&
84
+ user.guildTag.iconUrl &&
85
+ typeof user.guildTag.iconUrl === "string" &&
86
+ !/^https?:\/\//i.test(user.guildTag.iconUrl) &&
87
+ !/^assets\//.test(user.guildTag.iconUrl)) {
88
+ user.guildTag.iconUrl = `https://cdn.discordapp.com/guilds/${user.guildTag.id || "0"}/icons/${user.guildTag.iconUrl}.png`;
89
+ }
90
+ }
91
+ catch (e) { }
92
+ }
93
+ }
94
+ if (Array.isArray(messages)) {
95
+ for (const m of messages) {
96
+ if (m && Array.isArray(m.stickers)) {
97
+ for (const st0 of m.stickers) {
98
+ try {
99
+ const st = st0;
100
+ if (st && typeof st === "object") {
101
+ const id = st.id || st.sticker_id || st.assetId || null;
102
+ if (id && !st.url) {
103
+ st.url = `https://media.discordapp.net/stickers/${id}.webp?size=160&quality=lossless`;
104
+ }
105
+ }
106
+ }
107
+ catch (e) { }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+ catch (e) { }
114
+ }
115
+ if (saveOpt) {
116
+ const compression = typeof saveOpt === "object" && typeof saveOpt.compression === "number" ? saveOpt.compression : undefined;
117
+ const assetsDirOption = typeof saveOpt === "object" && typeof saveOpt.dir === "string" ? saveOpt.dir : "assets";
118
+ const assetsDir = path_1.default.isAbsolute(assetsDirOption) ? assetsDirOption : path_1.default.join(process.cwd(), assetsDirOption);
119
+ assetManager = new assetManager_1.AssetManager(assetsDir, { compression });
120
+ await assetManager.initialize();
121
+ transcriptDataToProcess = JSON.parse(JSON.stringify({
122
+ messages: processedMessages,
123
+ resolvedUsers: transcript.resolvedUsers,
124
+ resolvedRoles: transcript.resolvedRoles,
125
+ meta: transcript.meta
126
+ }));
127
+ if (transcriptDataToProcess.resolvedUsers && typeof transcriptDataToProcess.resolvedUsers === "object") {
128
+ for (const [uid, u] of Object.entries(transcriptDataToProcess.resolvedUsers)) {
129
+ try {
130
+ const user = u;
131
+ if (user && typeof user.avatar === "string" && !/^https?:\/\//i.test(user.avatar)) {
132
+ const isAnimated = user.avatar.startsWith("a_");
133
+ const ext = isAnimated ? ".gif" : ".png";
134
+ user.avatar = `https://cdn.discordapp.com/avatars/${uid}/${user.avatar}${ext}?size=128`;
135
+ }
136
+ if (user &&
137
+ user.guildTag &&
138
+ user.guildTag.iconUrl &&
139
+ typeof user.guildTag.iconUrl === "string" &&
140
+ !/^https?:\/\//i.test(user.guildTag.iconUrl)) {
141
+ user.guildTag.iconUrl = `https://cdn.discordapp.com/guilds/${user.guildTag.id || "0"}/icons/${user.guildTag.iconUrl}.png`;
142
+ }
143
+ }
144
+ catch (e) { }
145
+ }
146
+ }
147
+ if (Array.isArray(transcriptDataToProcess.messages)) {
148
+ for (const m of transcriptDataToProcess.messages) {
149
+ if (m && Array.isArray(m.stickers)) {
150
+ for (const st of m.stickers) {
151
+ try {
152
+ if (st && typeof st === "object") {
153
+ const id = st.id || st.sticker_id || st.assetId || null;
154
+ if (id && !st.url) {
155
+ st.url = `https://media.discordapp.net/stickers/${id}.webp?size=160&quality=lossless`;
156
+ }
157
+ }
158
+ }
159
+ catch (e) { }
160
+ }
161
+ }
162
+ }
163
+ }
164
+ await assetManager.downloadAssets(transcriptDataToProcess);
165
+ transcriptDataToProcess = assetManager.replaceUrls(transcriptDataToProcess);
166
+ processedMessages = transcriptDataToProcess.messages;
167
+ }
168
+ const resolvedTheme = (theme === "system" ? "dark" : theme);
169
+ const props = {
170
+ channel: channelInfo,
171
+ messages: processedMessages,
172
+ theme: resolvedTheme,
173
+ allowThemeSwitching,
174
+ allowThemeSwitchingPersist: true,
175
+ poweredBy: poweredBy,
176
+ className: "transcript-ssr"
177
+ };
178
+ let resolvedUsers = transcript.resolvedUsers || {};
179
+ let resolvedRoles = transcript.resolvedRoles || {};
180
+ let resolvedChannels = transcript.resolvedChannels || {};
181
+ if (transcriptDataToProcess) {
182
+ resolvedUsers = transcriptDataToProcess.resolvedUsers || resolvedUsers;
183
+ resolvedRoles = transcriptDataToProcess.resolvedRoles || resolvedRoles;
184
+ resolvedChannels = transcriptDataToProcess.resolvedChannels || resolvedChannels;
185
+ }
186
+ try {
187
+ const stripped = {};
188
+ for (const k of Object.keys(resolvedChannels || {})) {
189
+ const v = resolvedChannels[k];
190
+ if (v && typeof v === "object")
191
+ stripped[k] = { name: v.name ?? null };
192
+ else
193
+ stripped[k] = { name: v?.name ?? null };
194
+ }
195
+ resolvedChannels = stripped;
196
+ }
197
+ catch (e) { }
198
+ let effectiveResolvedUsers = resolvedUsers;
199
+ if (ignore?.guildBadges) {
200
+ effectiveResolvedUsers = {};
201
+ for (const k of Object.keys(resolvedUsers)) {
202
+ const v = resolvedUsers[k];
203
+ if (v && typeof v === "object") {
204
+ const { guildTag, ...rest } = v;
205
+ effectiveResolvedUsers[k] = rest;
206
+ }
207
+ else {
208
+ effectiveResolvedUsers[k] = v;
209
+ }
210
+ }
211
+ }
212
+ const ssrProps = Object.assign({}, props, {
213
+ resolvedUsers: effectiveResolvedUsers,
214
+ resolvedRoles,
215
+ resolvedChannels,
216
+ exportedAt: transcript.meta.generatedAt
217
+ });
218
+ const _origConsoleError = console.error;
219
+ console.error = (...args) => {
220
+ try {
221
+ const msg = String(args && args.length ? args[0] : "");
222
+ if (msg && msg.includes('Each child in a list should have a unique "key" prop')) {
223
+ const stack = new Error().stack || "";
224
+ const out = `=== React Key Warning ===\nMessage: ${msg}\nArgs:${JSON.stringify(args.slice(1), null, 2)}\nStack:${stack}\n\n`;
225
+ try {
226
+ promises_1.default.appendFile(path_1.default.join(process.cwd(), ".react-key-warning.log"), out, "utf-8");
227
+ }
228
+ catch (e) { }
229
+ }
230
+ }
231
+ catch (e) { }
232
+ return _origConsoleError.apply(console, args);
233
+ };
234
+ const content = server_1.default.renderToString(react_1.default.createElement(TranscriptSSRWrapper, ssrProps));
235
+ console.error = _origConsoleError;
236
+ const transcriptData = JSON.stringify({
237
+ channel: channelInfo,
238
+ messages: processedMessages,
239
+ theme: resolvedTheme,
240
+ allowThemeSwitching: allowThemeSwitching,
241
+ allowThemeSwitchingPersist: true,
242
+ resolvedUsers: effectiveResolvedUsers,
243
+ resolvedRoles,
244
+ resolvedChannels,
245
+ exportedAt: transcript.meta.generatedAt,
246
+ poweredBy: props.poweredBy,
247
+ cdnBase: saveOpt ? "" : undefined,
248
+ mediaBase: saveOpt ? "" : undefined
249
+ });
250
+ let clientCode = await bundleClientToString();
251
+ if (assetManager) {
252
+ try {
253
+ await assetManager.downloadAssets(clientCode);
254
+ clientCode = assetManager.replaceUrls(clientCode);
255
+ }
256
+ catch (e) { }
257
+ }
258
+ let html = buildHtmlDocument(content, transcriptData, clientCode);
259
+ try {
260
+ const appCssPath = path_1.default.join(process.cwd(), "src", "web", "index.css");
261
+ const highlightCssPath = path_1.default.join(process.cwd(), "src", "web", "highlight-theme.css");
262
+ let appCss = await promises_1.default.readFile(appCssPath, "utf-8").catch(() => "");
263
+ let highlightCss = await promises_1.default.readFile(highlightCssPath, "utf-8").catch(() => "");
264
+ const highlightThemePath = path_1.default.join(process.cwd(), "node_modules", "highlight.js", "styles", "atom-one-dark.css");
265
+ const highlightTheme = await promises_1.default.readFile(highlightThemePath, "utf-8").catch(() => "");
266
+ if (highlightTheme) {
267
+ highlightCss = highlightCss.replace(/@import\s+['"]highlight\.js\/styles\/atom-one-dark\.css['"];?/g, highlightTheme);
268
+ }
269
+ else {
270
+ highlightCss = highlightCss.replace(/@import\s+['"]highlight\.js\/styles\/atom-one-dark\.css['"];?/g, "");
271
+ }
272
+ appCss = appCss.replace(/^@tailwind.*$/gm, "");
273
+ html = html.replace("[APP_CSS]", appCss.replace(/<\/style>/g, ""));
274
+ html = html.replace("[HIGHLIGHT_CSS]", highlightCss.replace(/<\/style>/g, ""));
275
+ }
276
+ catch (e) { }
277
+ if (assetManager) {
278
+ try {
279
+ await assetManager.downloadAssets(html);
280
+ html = assetManager.replaceUrls(html);
281
+ }
282
+ catch (e) { }
283
+ }
284
+ if (typeof filename === "string" && filename.length > 0) {
285
+ const finalFilename = filename;
286
+ const outputPath = path_1.default.join(process.cwd(), finalFilename);
287
+ await promises_1.default.writeFile(outputPath, html, "utf-8");
288
+ return outputPath;
289
+ }
290
+ return html;
291
+ }
292
+ async function bundleClientToString() {
293
+ const result = await esbuild_1.default.build({
294
+ entryPoints: [path_1.default.join(process.cwd(), "src", "web", "client.tsx")],
295
+ bundle: true,
296
+ minify: true,
297
+ platform: "browser",
298
+ format: "esm",
299
+ write: false,
300
+ jsx: "automatic"
301
+ });
302
+ return result.outputFiles[0].text;
303
+ }
@@ -0,0 +1,5 @@
1
+ import { Collection, type Channel, type Message, type TextBasedChannel } from "discord.js";
2
+ import { TranscriptCreateOptions } from "../types/general";
3
+ export declare function createTranscript(channel: TextBasedChannel, options?: TranscriptCreateOptions): Promise<string>;
4
+ export declare function createTranscriptRaw(channel: TextBasedChannel, options?: TranscriptCreateOptions): Promise<Message[]>;
5
+ export declare function generateFromMessages(messages: Message[] | Collection<string, Message>, channel: Channel, options?: TranscriptCreateOptions): Promise<string>;
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createTranscript = createTranscript;
4
+ exports.createTranscriptRaw = createTranscriptRaw;
5
+ exports.generateFromMessages = generateFromMessages;
6
+ const discord_js_1 = require("discord.js");
7
+ const transformer_1 = require("./utils/transformer");
8
+ const polls_1 = require("./utils/polls");
9
+ const generateHtml_1 = require("./generateHtml");
10
+ const djsVersion = discord_js_1.version.split(".")[0];
11
+ if (djsVersion !== "14" && djsVersion !== "15") {
12
+ console.error(`Unsupported discord.js version: ${discord_js_1.version}. This library only supports v14 and v15 of discord.js.`);
13
+ process.exit(1);
14
+ }
15
+ async function createTranscript(channel, options = {}) {
16
+ if (!channel.isTextBased())
17
+ throw new TypeError("Provided channel must be text-based");
18
+ let channelMessages = [];
19
+ let lastMessageID;
20
+ const { limit } = options;
21
+ const resolveLimit = typeof limit === "undefined" ? Infinity : limit;
22
+ while (true) {
23
+ const fetchLimitOptions = { limit: 100, before: lastMessageID };
24
+ if (!lastMessageID)
25
+ delete fetchLimitOptions.before;
26
+ const messages = await channel.messages.fetch(fetchLimitOptions);
27
+ channelMessages.push(...messages.values());
28
+ lastMessageID = messages.lastKey();
29
+ if (messages.size < 100)
30
+ break;
31
+ if (channelMessages.length >= resolveLimit)
32
+ break;
33
+ }
34
+ if (resolveLimit < channelMessages.length)
35
+ channelMessages = channelMessages.slice(0, limit);
36
+ return generateFromMessages(channelMessages.reverse(), channel, options);
37
+ }
38
+ async function createTranscriptRaw(channel, options = {}) {
39
+ if (!channel.isTextBased())
40
+ throw new TypeError("Provided channel must be text-based");
41
+ let channelMessages = [];
42
+ let lastMessageID;
43
+ const { limit } = options;
44
+ const resolveLimit = typeof limit === "undefined" ? Infinity : limit;
45
+ while (true) {
46
+ const fetchLimitOptions = { limit: 100, before: lastMessageID };
47
+ if (!lastMessageID)
48
+ delete fetchLimitOptions.before;
49
+ const messages = await channel.messages.fetch(fetchLimitOptions);
50
+ channelMessages.push(...messages.values());
51
+ lastMessageID = messages.lastKey();
52
+ if (messages.size < 100)
53
+ break;
54
+ if (channelMessages.length >= resolveLimit)
55
+ break;
56
+ }
57
+ if (resolveLimit < channelMessages.length)
58
+ channelMessages = channelMessages.slice(0, limit);
59
+ return channelMessages.reverse();
60
+ }
61
+ async function generateFromMessages(messages, channel, options = {}) {
62
+ const messageArray = messages instanceof discord_js_1.Collection ? Array.from(messages.values()) : messages;
63
+ const batchSize = 25;
64
+ const messagesSerialized = [];
65
+ const resolvedAuthorsMap = new Map();
66
+ const resolvedRolesMap = new Map();
67
+ const resolvedChannelsMap = new Map();
68
+ const transcriptGuildId = channel.guild?.id ?? null;
69
+ const roleMentionRegex = /<@&(\d+)>/g;
70
+ for (let i = 0; i < messageArray.length; i += batchSize) {
71
+ const batch = messageArray.slice(i, i + batchSize);
72
+ const serialized = await Promise.all(batch.map((m) => (0, transformer_1.messageToSerializable)(m)));
73
+ for (let j = 0; j < serialized.length; j++) {
74
+ const result = serialized[j];
75
+ if (!result)
76
+ continue;
77
+ if (result.author) {
78
+ resolvedAuthorsMap.set(result.author.id, result.author);
79
+ }
80
+ if (result.message.poll) {
81
+ await (0, polls_1.enrichPollVoters)(batch[j], result.message.poll);
82
+ }
83
+ messagesSerialized.push(result.message);
84
+ }
85
+ for (const message of batch) {
86
+ let allContent = message.content ?? "";
87
+ if (message.embeds) {
88
+ const embedArray = Array.isArray(message.embeds)
89
+ ? message.embeds
90
+ : typeof message.embeds.values === "function"
91
+ ? Array.from(message.embeds.values())
92
+ : [];
93
+ for (const embed of embedArray) {
94
+ if (embed.title)
95
+ allContent += " " + embed.title;
96
+ if (embed.description)
97
+ allContent += " " + embed.description;
98
+ if (embed.fields) {
99
+ for (const field of embed.fields) {
100
+ if (field.name)
101
+ allContent += " " + field.name;
102
+ if (field.value)
103
+ allContent += " " + field.value;
104
+ }
105
+ }
106
+ if (embed.footer?.text)
107
+ allContent += " " + embed.footer.text;
108
+ }
109
+ }
110
+ let match;
111
+ roleMentionRegex.lastIndex = 0;
112
+ while ((match = roleMentionRegex.exec(allContent)) !== null) {
113
+ const roleId = match[1];
114
+ if (resolvedRolesMap.has(roleId))
115
+ continue;
116
+ try {
117
+ if (message.guild) {
118
+ const role = await message.guild.roles.fetch(roleId).catch(() => null);
119
+ if (role) {
120
+ resolvedRolesMap.set(roleId, {
121
+ name: role.name,
122
+ color: role.hexColor
123
+ });
124
+ }
125
+ }
126
+ }
127
+ catch { }
128
+ }
129
+ if (message.mentions?.roles) {
130
+ for (const role of message.mentions.roles.values?.() ?? []) {
131
+ if (!resolvedRolesMap.has(role.id)) {
132
+ resolvedRolesMap.set(role.id, {
133
+ name: role.name,
134
+ color: role.hexColor
135
+ });
136
+ }
137
+ }
138
+ }
139
+ if (message.mentions?.channels) {
140
+ for (const mentionedChannel of message.mentions.channels.values?.() ?? []) {
141
+ if (mentionedChannel && mentionedChannel.id && !resolvedChannelsMap.has(mentionedChannel.id)) {
142
+ const mentionGuildId = mentionedChannel.guildId ?? mentionedChannel.guild?.id ?? message.guild?.id ?? null;
143
+ if (transcriptGuildId && mentionGuildId && mentionGuildId !== transcriptGuildId) {
144
+ continue;
145
+ }
146
+ resolvedChannelsMap.set(mentionedChannel.id, {
147
+ name: mentionedChannel.name ?? null,
148
+ guildId: mentionGuildId
149
+ });
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+ const resolvedUsersObj = {};
156
+ for (const [userId, authorData] of resolvedAuthorsMap.entries()) {
157
+ const { id: _discard, ...authorWithoutId } = authorData ?? {};
158
+ resolvedUsersObj[userId] = authorWithoutId;
159
+ }
160
+ const resolvedRolesObj = {};
161
+ for (const [roleId, roleData] of resolvedRolesMap.entries()) {
162
+ resolvedRolesObj[roleId] = roleData;
163
+ }
164
+ const resolvedChannelsObj = {};
165
+ for (const [channelId, channelData] of resolvedChannelsMap.entries()) {
166
+ resolvedChannelsObj[channelId] = channelData;
167
+ }
168
+ if (options?.ignore?.attachments) {
169
+ const ignoreImgs = !!options.ignore.attachments.images;
170
+ const ignoreVids = !!options.ignore.attachments.videos;
171
+ const ignoreAudio = !!options.ignore.attachments.audio;
172
+ const ignoreFiles = !!options.ignore.attachments.files;
173
+ for (const m of messagesSerialized) {
174
+ if (!m.attachments || !Array.isArray(m.attachments))
175
+ continue;
176
+ const filtered = m.attachments.filter((a) => {
177
+ const url = typeof a === "string" ? a : (a && (a.url || "")) || "";
178
+ const isImage = /\.(png|jpe?g|gif|webp|svg|bmp|avif)(\?|$)/i.test(url);
179
+ const isVideo = /\.(mp4|mov|webm|avi|mkv)(\?|$)/i.test(url);
180
+ const isAudio = /\.(mp3|wav|ogg|m4a|flac|aac|weba)(\?|$)/i.test(url);
181
+ if (ignoreImgs && isImage)
182
+ return false;
183
+ if (ignoreVids && isVideo)
184
+ return false;
185
+ if (ignoreAudio && isAudio)
186
+ return false;
187
+ if (ignoreFiles && !isImage && !isVideo && !isAudio)
188
+ return false;
189
+ return true;
190
+ });
191
+ m.attachments = filtered;
192
+ }
193
+ }
194
+ function messageHasVisibleContent(m) {
195
+ if (m.content && String(m.content).trim().length > 0)
196
+ return true;
197
+ if (m.attachments && Array.isArray(m.attachments) && m.attachments.length > 0)
198
+ return true;
199
+ if (m.stickers && Array.isArray(m.stickers) && m.stickers.length > 0)
200
+ return true;
201
+ if (m.poll)
202
+ return true;
203
+ if (m.buttons && Array.isArray(m.buttons) && m.buttons.length > 0)
204
+ return true;
205
+ if (m.selects && Array.isArray(m.selects) && m.selects.length > 0)
206
+ return true;
207
+ if (m.forwardedMessage) {
208
+ if (m.forwardedMessage.content && String(m.forwardedMessage.content).trim().length > 0)
209
+ return true;
210
+ if (m.forwardedMessage.attachments && m.forwardedMessage.attachments.length > 0)
211
+ return true;
212
+ }
213
+ if (m.embeds && Array.isArray(m.embeds)) {
214
+ for (const e of m.embeds) {
215
+ if (!e)
216
+ continue;
217
+ if (e.title && String(e.title).trim())
218
+ return true;
219
+ if (e.description && String(e.description).trim())
220
+ return true;
221
+ if (e.footer && e.footer.text && String(e.footer.text).trim())
222
+ return true;
223
+ if (e.author && (e.author.name || e.author.url))
224
+ return true;
225
+ if (e.image && e.image.url)
226
+ return true;
227
+ if (e.thumbnail && e.thumbnail.url)
228
+ return true;
229
+ if (e.fields && Array.isArray(e.fields) && e.fields.some((f) => (f.name && String(f.name).trim()) || (f.value && String(f.value).trim())))
230
+ return true;
231
+ }
232
+ }
233
+ return false;
234
+ }
235
+ const filteredMessages = messagesSerialized.filter((m) => messageHasVisibleContent(m));
236
+ messagesSerialized.length = 0;
237
+ messagesSerialized.push(...filteredMessages);
238
+ const transcript = {
239
+ meta: {
240
+ channelId: channel.id ?? "unknown",
241
+ channelName: channel.name ?? null,
242
+ guildId: channel.guild?.id ?? null,
243
+ generatedAt: new Date().toISOString(),
244
+ messageCount: messagesSerialized.length
245
+ },
246
+ messages: messagesSerialized,
247
+ resolvedUsers: resolvedUsersObj,
248
+ resolvedRoles: resolvedRolesObj,
249
+ resolvedChannels: resolvedChannelsObj
250
+ };
251
+ return await (0, generateHtml_1.generateHtml)(transcript, options);
252
+ }
@@ -0,0 +1,12 @@
1
+ import { Collection, type Channel, type Message, type TextBasedChannel, type User } from "discord.js";
2
+ import { TranscriptCreateOptions } from "../types/general";
3
+ import type { SerializableMessage } from "../types/exportableTranscript";
4
+ type GuildTag = {
5
+ name?: string | null;
6
+ iconUrl?: string | null;
7
+ };
8
+ export declare function resolveGuildTagForUser(user: User): Promise<GuildTag>;
9
+ export declare function mapMessageToSerializable(m: Message): Promise<SerializableMessage>;
10
+ export declare function createTranscript(channel: TextBasedChannel, options?: TranscriptCreateOptions): Promise<string>;
11
+ export declare function generateFromMessages(messages: Message[] | Collection<string, Message>, channel: Channel, options?: TranscriptCreateOptions): Promise<string>;
12
+ export {};