├── .gitignore ├── .vscode └── settings.json ├── README.md ├── pom.xml └── src └── main ├── java └── me │ └── ryzeon │ └── transcripts │ ├── DiscordHtmlTranscripts.java │ ├── Formatter.java │ └── utils │ └── format │ ├── IFormatHelper.java │ └── impl │ ├── AudioFormat.java │ ├── ImageFormat.java │ └── VideoFormat.java └── resources └── template.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | 3 | target 4 | 5 | src/test/java/Example.java -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.configuration.updateBuildConfiguration": "automatic" 3 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord (JDA) HTML Transcripts 2 | [![](https://jitpack.io/v/Ryzeon/discord-html-transcripts.svg)](https://jitpack.io/#Ryzeon/discord-html-transcripts) 3 | 4 | Discord HTML Transcripts is a node.js module (recode on JDA) to generate nice looking HTML transcripts. Processes discord markdown like **bold**, *italics*, ~~strikethroughs~~, and more. Nicely formats attachments and embeds. Built in XSS protection, preventing users from inserting html tags. 5 | 6 | **This module is designed to work with [JDA](https://github.com/DV8FromTheWorld/JDA).** 7 | 8 | HTML Template stolen from [DiscordChatExporter](https://github.com/Tyrrrz/DiscordChatExporter). 9 | 10 | ## Installation 11 | 12 | ```xml 13 | 14 | 15 | jitpack.io 16 | https://jitpack.io 17 | 18 | 19 | ``` 20 | 21 | ```xml 22 | 23 | com.github.Ryzeon 24 | discord-html-transcripts 25 | Tag 26 | 27 | ``` 28 | 29 | ## Example Output 30 | ![output](https://img.derock.dev/5f5q0a.png) 31 | 32 | ## Usage 33 | ### Example usage using the built-in message fetcher. 34 | ```java 35 | DiscordHtmlTranscripts transcript = DiscordHtmlTranscripts.getInstance(); 36 | textChannel.sendFiles(transcript.createTranscript(textChannel)).queue() 37 | ``` 38 | 39 | ### Or if you prefer, you can pass in your own messages. 40 | ```java 41 | DiscordHtmlTranscripts transcript = DiscordHtmlTranscripts.getInstance(); 42 | transcript.generateFromMessages(messages); // return to InputStream 43 | ``` 44 | 45 | ### You can also put the transcript into a variable 46 | ```java 47 | DiscordHtmlTranscripts transcripts = new DiscordHtmlTranscripts(); 48 | try { 49 | testChannel.sendFiles(transcripts.createTranscript(testChannel, "test.html")).queue(); 50 | } catch (IOException e) { 51 | throw new RuntimeException(e); 52 | } 53 | ``` 54 | 55 | 56 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | me.ryzeon.html 8 | discord-html-transcripts 9 | 2.0-SNAPSHOT 10 | 11 | 12 | 13 | org.apache.maven.plugins 14 | maven-compiler-plugin 15 | 16 | 17 17 | 17 18 | 19 | 20 | 21 | maven-assembly-plugin 22 | 23 | 24 | 25 | me.ryzeon.html.DiscordHtmlTranscripts 26 | 27 | 28 | 29 | jar-with-dependencies 30 | 31 | 32 | 33 | 34 | make-assembly 35 | package 36 | 37 | single 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 17 47 | UTF-8 48 | 49 | 50 | 51 | 52 | 53 | org.projectlombok 54 | lombok 55 | 1.18.30 56 | provided 57 | 58 | 59 | 60 | net.dv8tion 61 | JDA 62 | 5.0.0 63 | 64 | 65 | 66 | 67 | org.jsoup 68 | jsoup 69 | 1.15.3 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/main/java/me/ryzeon/transcripts/DiscordHtmlTranscripts.java: -------------------------------------------------------------------------------- 1 | package me.ryzeon.transcripts; 2 | 3 | import kotlin.text.Charsets; 4 | import me.ryzeon.transcripts.utils.format.IFormatHelper; 5 | import me.ryzeon.transcripts.utils.format.impl.AudioFormat; 6 | import me.ryzeon.transcripts.utils.format.impl.ImageFormat; 7 | import me.ryzeon.transcripts.utils.format.impl.VideoFormat; 8 | import net.dv8tion.jda.api.entities.*; 9 | import net.dv8tion.jda.api.entities.channel.middleman.GuildChannel; 10 | import net.dv8tion.jda.api.entities.channel.middleman.GuildMessageChannel; 11 | import net.dv8tion.jda.api.utils.FileUpload; 12 | import org.jsoup.Jsoup; 13 | import org.jsoup.nodes.Document; 14 | import org.jsoup.nodes.Element; 15 | 16 | import java.io.ByteArrayInputStream; 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | import java.time.format.DateTimeFormatter; 20 | import java.util.*; 21 | import java.util.stream.Collectors; 22 | 23 | 24 | /** 25 | * Created by Ryzeon 26 | * Contributors: Zemux1613, Inkception, IncbomDev 27 | * Project: discord-html-transcripts 28 | * Date: 1/3/2023 @ 10:50 29 | */ 30 | public class DiscordHtmlTranscripts { 31 | 32 | private static DiscordHtmlTranscripts instance; 33 | private final IFormatHelper 34 | imageFormats = new ImageFormat(), 35 | videoFormats = new VideoFormat(), 36 | audioFormats = new AudioFormat(); 37 | 38 | /** 39 | * Get the instance of the DiscordHtmlTranscripts 40 | * 41 | * @return a singleton instance of the DiscordHtmlTranscripts 42 | */ 43 | public static DiscordHtmlTranscripts getInstance() { 44 | if (instance == null) { 45 | instance = new DiscordHtmlTranscripts(); 46 | } 47 | return instance; 48 | } 49 | 50 | private void handleFooter(Document document, MessageEmbed embed, Element embedContentContainer) { 51 | Element embedFooter = document.createElement("div"); 52 | embedFooter.addClass("chatlog__embed-footer"); 53 | 54 | 55 | if (Objects.requireNonNull(embed.getFooter()).getIconUrl() != null) { 56 | Element embedFooterIcon = document.createElement("img"); 57 | embedFooterIcon.addClass("chatlog__embed-footer-icon"); 58 | embedFooterIcon.attr("src", embed.getFooter().getIconUrl()); 59 | embedFooterIcon.attr("alt", "Footer icon"); 60 | embedFooterIcon.attr("loading", "lazy"); 61 | 62 | embedFooter.appendChild(embedFooterIcon); 63 | } 64 | 65 | Element embedFooterText = document.createElement("span"); 66 | embedFooterText.addClass("chatlog__embed-footer-text"); 67 | embedFooterText.text(embed.getTimestamp() != null 68 | ? embed.getFooter().getText() + " • " + embed.getTimestamp() 69 | .format(DateTimeFormatter.ofPattern("HH:mm:ss")) 70 | : embed.getFooter().getText()); 71 | 72 | embedFooter.appendChild(embedFooterText); 73 | 74 | embedContentContainer.appendChild(embedFooter); 75 | } 76 | 77 | private void handleEmbedImage(Document document, MessageEmbed embed, Element embedContentContainer) { 78 | Element embedImage = document.createElement("div"); 79 | embedImage.addClass("chatlog__embed-image-container"); 80 | 81 | Element embedImageLink = document.createElement("a"); 82 | embedImageLink.addClass("chatlog__embed-image-link"); 83 | embedImageLink.attr("href", embed.getImage().getUrl()); 84 | 85 | Element embedImageImage = document.createElement("img"); 86 | embedImageImage.addClass("chatlog__embed-image"); 87 | embedImageImage.attr("src", embed.getImage().getUrl()); 88 | embedImageImage.attr("alt", "Image"); 89 | embedImageImage.attr("loading", "lazy"); 90 | 91 | embedImageLink.appendChild(embedImageImage); 92 | embedImage.appendChild(embedImageLink); 93 | 94 | embedContentContainer.appendChild(embedImage); 95 | } 96 | 97 | private void handleEmbedThumbnail(Document document, MessageEmbed embed, Element embedContent) { 98 | Element embedThumbnail = document.createElement("div"); 99 | embedThumbnail.addClass("chatlog__embed-thumbnail-container"); 100 | 101 | Element embedThumbnailLink = document.createElement("a"); 102 | embedThumbnailLink.addClass("chatlog__embed-thumbnail-link"); 103 | embedThumbnailLink.attr("href", embed.getThumbnail().getUrl()); 104 | 105 | Element embedThumbnailImage = document.createElement("img"); 106 | embedThumbnailImage.addClass("chatlog__embed-thumbnail"); 107 | embedThumbnailImage.attr("src", embed.getThumbnail().getUrl()); 108 | embedThumbnailImage.attr("alt", "Thumbnail"); 109 | embedThumbnailImage.attr("loading", "lazy"); 110 | 111 | embedThumbnailLink.appendChild(embedThumbnailImage); 112 | embedThumbnail.appendChild(embedThumbnailLink); 113 | 114 | embedContent.appendChild(embedThumbnail); 115 | } 116 | 117 | private void handleEmbedFields(Document document, MessageEmbed embed, Element embedText) { 118 | Element embedFields = document.createElement("div"); 119 | embedFields.addClass("chatlog__embed-fields"); 120 | 121 | for (MessageEmbed.Field field : embed.getFields()) { 122 | Element embedField = document.createElement("div"); 123 | embedField.addClass("chatlog__embed-field"); 124 | 125 | // Field nmae 126 | Element embedFieldName = document.createElement("div"); 127 | embedFieldName.addClass("chatlog__embed-field-name"); 128 | 129 | Element embedFieldNameMarkdown = document.createElement("div"); 130 | embedFieldNameMarkdown.addClass("markdown preserve-whitespace"); 131 | embedFieldNameMarkdown.html(field.getName()); 132 | 133 | embedFieldName.appendChild(embedFieldNameMarkdown); 134 | embedField.appendChild(embedFieldName); 135 | 136 | 137 | // Field value 138 | Element embedFieldValue = document.createElement("div"); 139 | embedFieldValue.addClass("chatlog__embed-field-value"); 140 | 141 | Element embedFieldValueMarkdown = document.createElement("div"); 142 | embedFieldValueMarkdown.addClass("markdown preserve-whitespace"); 143 | embedFieldValueMarkdown 144 | .html(Formatter.format(field.getValue())); 145 | 146 | embedFieldValue.appendChild(embedFieldValueMarkdown); 147 | embedField.appendChild(embedFieldValue); 148 | 149 | embedFields.appendChild(embedField); 150 | } 151 | 152 | embedText.appendChild(embedFields); 153 | } 154 | 155 | private void handleEmbedDescription(Document document, MessageEmbed embed, Element embedText) { 156 | Element embedDescription = document.createElement("div"); 157 | embedDescription.addClass("chatlog__embed-description"); 158 | 159 | Element embedDescriptionMarkdown = document.createElement("div"); 160 | embedDescriptionMarkdown.addClass("markdown preserve-whitespace"); 161 | embedDescriptionMarkdown 162 | .html(Formatter.format(embed.getDescription())); 163 | 164 | embedDescription.appendChild(embedDescriptionMarkdown); 165 | embedText.appendChild(embedDescription); 166 | } 167 | 168 | private void handleEmbedTitle(Document document, MessageEmbed embed, Element embedText) { 169 | Element embedTitle = document.createElement("div"); 170 | embedTitle.addClass("chatlog__embed-title"); 171 | 172 | if (embed.getUrl() != null) { 173 | Element embedTitleLink = document.createElement("a"); 174 | embedTitleLink.addClass("chatlog__embed-title-link"); 175 | embedTitleLink.attr("href", embed.getUrl()); 176 | 177 | Element embedTitleMarkdown = document.createElement("div"); 178 | embedTitleMarkdown.addClass("markdown preserve-whitespace") 179 | .html(Formatter.format(embed.getTitle())); 180 | 181 | embedTitleLink.appendChild(embedTitleMarkdown); 182 | embedTitle.appendChild(embedTitleLink); 183 | } else { 184 | Element embedTitleMarkdown = document.createElement("div"); 185 | embedTitleMarkdown.addClass("markdown preserve-whitespace") 186 | .html(Formatter.format(embed.getTitle())); 187 | 188 | embedTitle.appendChild(embedTitleMarkdown); 189 | } 190 | embedText.appendChild(embedTitle); 191 | } 192 | 193 | private void handleEmbedAuthor(Document document, MessageEmbed embed, Element embedText) { 194 | Element embedAuthor = document.createElement("div"); 195 | embedAuthor.addClass("chatlog__embed-author"); 196 | 197 | if (embed.getAuthor().getIconUrl() != null) { 198 | Element embedAuthorIcon = document.createElement("img"); 199 | embedAuthorIcon.addClass("chatlog__embed-author-icon"); 200 | embedAuthorIcon.attr("src", embed.getAuthor().getIconUrl()); 201 | embedAuthorIcon.attr("alt", "Author icon"); 202 | embedAuthorIcon.attr("loading", "lazy"); 203 | 204 | embedAuthor.appendChild(embedAuthorIcon); 205 | } 206 | 207 | Element embedAuthorName = document.createElement("span"); 208 | embedAuthorName.addClass("chatlog__embed-author-name"); 209 | 210 | if (embed.getAuthor().getUrl() != null) { 211 | Element embedAuthorNameLink = document.createElement("a"); 212 | embedAuthorNameLink.addClass("chatlog__embed-author-name-link"); 213 | embedAuthorNameLink.attr("href", embed.getAuthor().getUrl()); 214 | embedAuthorNameLink.text(embed.getAuthor().getName()); 215 | 216 | embedAuthorName.appendChild(embedAuthorNameLink); 217 | } else { 218 | embedAuthorName.text(embed.getAuthor().getName()); 219 | } 220 | 221 | embedAuthor.appendChild(embedAuthorName); 222 | embedText.appendChild(embedAuthor); 223 | } 224 | 225 | private void handleUnknownAttachmentTypes(Document document, Message.Attachment attach, Element attachmentsDiv) { 226 | Element attachmentGeneric = document.createElement("div"); 227 | attachmentGeneric.addClass("chatlog__attachment-generic"); 228 | 229 | Element attachmentGenericIcon = document.createElement("svg"); 230 | attachmentGenericIcon.addClass("chatlog__attachment-generic-icon"); 231 | 232 | Element attachmentGenericIconUse = document.createElement("use"); 233 | attachmentGenericIconUse.attr("xlink:href", "#icon-attachment"); 234 | 235 | attachmentGenericIcon.appendChild(attachmentGenericIconUse); 236 | attachmentGeneric.appendChild(attachmentGenericIcon); 237 | 238 | Element attachmentGenericName = document.createElement("div"); 239 | attachmentGenericName.addClass("chatlog__attachment-generic-name"); 240 | 241 | Element attachmentGenericNameLink = document.createElement("a"); 242 | attachmentGenericNameLink.attr("href", attach.getUrl()); 243 | attachmentGenericNameLink.text(attach.getFileName()); 244 | 245 | attachmentGenericName.appendChild(attachmentGenericNameLink); 246 | attachmentGeneric.appendChild(attachmentGenericName); 247 | 248 | Element attachmentGenericSize = document.createElement("div"); 249 | attachmentGenericSize.addClass("chatlog__attachment-generic-size"); 250 | 251 | attachmentGenericSize.text(Formatter.formatBytes(attach.getSize())); 252 | attachmentGeneric.appendChild(attachmentGenericSize); 253 | 254 | attachmentsDiv.appendChild(attachmentGeneric); 255 | } 256 | 257 | private void handleAudios(Document document, Message.Attachment attach, Element attachmentsDiv) { 258 | Element attachmentAudio = document.createElement("audio"); 259 | attachmentAudio.addClass("chatlog__attachment-media"); 260 | attachmentAudio.attr("src", attach.getUrl()); 261 | attachmentAudio.attr("alt", "Audio attachment"); 262 | attachmentAudio.attr("controls", true); 263 | attachmentAudio.attr("title", 264 | "Audio: " + attach.getFileName() + Formatter.formatBytes(attach.getSize())); 265 | 266 | attachmentsDiv.appendChild(attachmentAudio); 267 | } 268 | 269 | private void handleVideos(Document document, Message.Attachment attach, Element attachmentsDiv) { 270 | Element attachmentVideo = document.createElement("video"); 271 | attachmentVideo.addClass("chatlog__attachment-media"); 272 | attachmentVideo.attr("src", attach.getUrl()); 273 | attachmentVideo.attr("alt", "Video attachment"); 274 | attachmentVideo.attr("controls", true); 275 | attachmentVideo.attr("title", 276 | "Video: " + attach.getFileName() + Formatter.formatBytes(attach.getSize())); 277 | 278 | attachmentsDiv.appendChild(attachmentVideo); 279 | } 280 | 281 | private void handleImages(Document document, Message.Attachment attach, Element attachmentsDiv) { 282 | Element attachmentLink = document.createElement("a"); 283 | 284 | Element attachmentImage = document.createElement("img"); 285 | attachmentImage.addClass("chatlog__attachment-media"); 286 | attachmentImage.attr("src", attach.getUrl()); 287 | attachmentImage.attr("alt", "Image attachment"); 288 | attachmentImage.attr("loading", "lazy"); 289 | attachmentImage.attr("title", 290 | "Image: " + attach.getFileName() + Formatter.formatBytes(attach.getSize())); 291 | 292 | attachmentLink.appendChild(attachmentImage); 293 | attachmentsDiv.appendChild(attachmentLink); 294 | } 295 | 296 | private void handleMessageReferences(GuildChannel channel, Document document, Message message, Element messageGroup) { 297 | // message.reference?.messageId 298 | // create symbol 299 | Element referenceSymbol = document.createElement("div"); 300 | referenceSymbol.addClass("chatlog__reference-symbol"); 301 | 302 | // create reference 303 | Element reference = document.createElement("div"); 304 | reference.addClass("chatlog__reference"); 305 | 306 | var referenceMessage = message.getReferencedMessage(); 307 | User author = referenceMessage.getAuthor(); 308 | Member member = channel.getGuild().getMember(author); 309 | var color = Formatter.toHex(Objects.requireNonNull(member.getColor())); 310 | 311 | // System.out.println("REFERENCE MSG " + referenceMessage.getContentDisplay()); 312 | reference.html("\"Avatar\"" + 314 | "" + author.getName() + "\"" + 316 | "
" + 317 | " " + 319 | "" + 320 | referenceMessage.getContentDisplay() != null 321 | ? referenceMessage.getContentDisplay().length() > 42 322 | ? referenceMessage.getContentDisplay().substring(0, 42) 323 | + "..." 324 | : referenceMessage.getContentDisplay() 325 | : "Click to see attachment" + 326 | "" + 327 | "" + 328 | "
"); 329 | 330 | messageGroup.appendChild(referenceSymbol); 331 | messageGroup.appendChild(reference); 332 | } 333 | 334 | public FileUpload createTranscript(GuildMessageChannel channel) throws IOException { 335 | return createTranscript(channel, null); 336 | } 337 | 338 | public FileUpload createTranscript(GuildMessageChannel channel, String fileName) throws IOException { 339 | return FileUpload.fromData(generateFromMessages(channel.getIterableHistory().stream().collect(Collectors.toList())), 340 | fileName != null ? fileName : "transcript.html"); 341 | } 342 | 343 | public InputStream generateFromMessages(Collection messages) throws IOException { 344 | InputStream htmlTemplate = findFile(); 345 | if (messages.isEmpty()) { 346 | throw new IllegalArgumentException("No messages to generate a transcript from"); 347 | } 348 | 349 | GuildChannel channel = messages.iterator().next().getChannel().asGuildMessageChannel(); 350 | 351 | Document document = Jsoup.parse(htmlTemplate, "UTF-8", "template.html"); 352 | document.outputSettings().indentAmount(0).prettyPrint(true); 353 | document.getElementsByClass("preamble__guild-icon").first().attr("src", channel.getGuild().getIconUrl()); // set guild icon 354 | 355 | document.getElementById("transcriptTitle").text("#" + channel.getName() + " | " + messages.size() + " messages"); // set title 356 | document.getElementById("guildname").text(channel.getGuild().getName()); // set guild name 357 | document.getElementById("ticketname").text("#" + channel.getName()); // set channel name 358 | 359 | Element chatLog = document.getElementById("chatlog"); // chat log 360 | for (Message message : messages.stream() 361 | .sorted(Comparator.comparing(ISnowflake::getTimeCreated)) 362 | .toList()) { 363 | 364 | if (message.getAuthor().isBot()) { 365 | continue; 366 | } 367 | // create message group 368 | Element messageGroup = document.createElement("div"); 369 | messageGroup.addClass("chatlog__message-group"); 370 | 371 | // message reference 372 | if (message.getReferencedMessage() != null) { // preguntar si es eso 373 | handleMessageReferences(channel, document, message, messageGroup); 374 | } 375 | 376 | var author = message.getAuthor(); 377 | 378 | Element authorElement = document.createElement("div"); 379 | authorElement.addClass("chatlog__author-avatar-container"); 380 | 381 | Element authorAvatar = document.createElement("img"); 382 | authorAvatar.addClass("chatlog__author-avatar"); 383 | authorAvatar.attr("alt", "Avatar"); 384 | authorAvatar.attr("loading", "lazy"); 385 | 386 | Element authorName = document.createElement("span"); 387 | authorName.addClass("chatlog__author-name"); 388 | 389 | if (author != null) { 390 | authorName.attr("title", Objects.requireNonNull(author.getGlobalName())); 391 | authorName.text(author.getName()); 392 | authorName.attr("data-user-id", author.getId()); 393 | authorAvatar.attr("src", Objects.requireNonNull(author.getEffectiveAvatarUrl())); 394 | } else { 395 | // Handle the case when author is null (e.g., when the message is from a bot) 396 | authorName.attr("title", "Bot"); 397 | authorName.text("Bot"); 398 | authorName.attr("data-user-id", "Bot"); 399 | authorAvatar.attr("src", "default_bot_avatar_url"); // replace with your default bot avatar URL 400 | } 401 | 402 | authorElement.appendChild(authorAvatar); 403 | messageGroup.appendChild(authorElement); 404 | 405 | // message content 406 | Element content = document.createElement("div"); 407 | content.addClass("chatlog__messages"); 408 | 409 | content.appendChild(authorName); 410 | 411 | if (author != null && author.isBot()) { 412 | Element botTag = document.createElement("span"); 413 | botTag.addClass("chatlog__bot-tag").text("BOT"); 414 | content.appendChild(botTag); 415 | } 416 | 417 | // timestamp 418 | Element timestamp = document.createElement("span"); 419 | timestamp.addClass("chatlog__timestamp"); 420 | timestamp 421 | .text(message.getTimeCreated().format(DateTimeFormatter.ofPattern("HH:mm:ss"))); 422 | 423 | content.appendChild(timestamp); 424 | 425 | Element messageContent = document.createElement("div"); 426 | messageContent.addClass("chatlog__message"); 427 | messageContent.attr("data-message-id", message.getId()); 428 | messageContent.attr("id", "message-" + message.getId()); 429 | messageContent.attr("title", "Message sent: " + message.getTimeCreated().format(DateTimeFormatter.ofPattern("HH:mm:ss"))); 430 | 431 | if (!message.getContentDisplay().isEmpty()) { 432 | Element messageContentContent = document.createElement("div"); 433 | messageContentContent.addClass("chatlog__content"); 434 | 435 | Element messageContentContentMarkdown = document.createElement("div"); 436 | messageContentContentMarkdown.addClass("markdown"); 437 | 438 | Element messageContentContentMarkdownSpan = document.createElement("span"); 439 | messageContentContentMarkdownSpan.addClass("preserve-whitespace"); 440 | messageContentContentMarkdownSpan.html(Formatter.format(message.getContentDisplay())); 441 | 442 | messageContentContentMarkdown.appendChild(messageContentContentMarkdownSpan); 443 | messageContentContent.appendChild(messageContentContentMarkdown); 444 | messageContent.appendChild(messageContentContent); 445 | } 446 | 447 | // messsage attachments 448 | if (!message.getAttachments().isEmpty()) { 449 | for (Message.Attachment attach : message.getAttachments()) { 450 | Element attachmentsDiv = document.createElement("div"); 451 | attachmentsDiv.addClass("chatlog__attachment"); 452 | 453 | var attachmentType = attach.getFileExtension(); 454 | if (imageFormats.isFormat(attachmentType)) { 455 | handleImages(document, attach, attachmentsDiv); 456 | } else if (videoFormats.isFormat(attachmentType)) { 457 | handleVideos(document, attach, attachmentsDiv); 458 | } else if (audioFormats.isFormat(attachmentType)) { 459 | handleAudios(document, attach, attachmentsDiv); 460 | } else { 461 | handleUnknownAttachmentTypes(document, attach, attachmentsDiv); 462 | } 463 | 464 | messageContent.appendChild(attachmentsDiv); 465 | } 466 | } 467 | 468 | content.appendChild(messageContent); 469 | 470 | if (!message.getEmbeds().isEmpty()) { 471 | for (MessageEmbed embed : message.getEmbeds()) { 472 | if (embed == null) { 473 | continue; 474 | } 475 | Element embedDiv = document.createElement("div"); 476 | embedDiv.addClass("chatlog__embed"); 477 | 478 | // embed color 479 | Element embedColorPill = document.createElement("div"); 480 | 481 | if (embed.getColor() == null) { 482 | embedColorPill.addClass("chatlog__embed-color-pill chatlog__embed-color-pill--default"); 483 | } else { 484 | embedColorPill.addClass("chatlog__embed-color-pill"); 485 | embedColorPill.attr("style", 486 | "background-color: #" + Formatter.toHex(embed.getColor())); 487 | } 488 | embedDiv.appendChild(embedColorPill); 489 | 490 | Element embedContentContainer = document.createElement("div"); 491 | embedContentContainer.addClass("chatlog__embed-content-container"); 492 | 493 | Element embedContent = document.createElement("div"); 494 | embedContent.addClass("chatlog__embed-content"); 495 | 496 | Element embedText = document.createElement("div"); 497 | embedText.addClass("chatlog__embed-text"); 498 | 499 | // embed author 500 | if (embed.getAuthor() != null && embed.getAuthor().getName() != null) { 501 | handleEmbedAuthor(document, embed, embedText); 502 | } 503 | 504 | // embed title 505 | if (embed.getTitle() != null) { 506 | handleEmbedTitle(document, embed, embedText); 507 | } 508 | 509 | // embed description 510 | if (embed.getDescription() != null) { 511 | handleEmbedDescription(document, embed, embedText); 512 | } 513 | 514 | // embed fields 515 | if (!embed.getFields().isEmpty()) { 516 | handleEmbedFields(document, embed, embedText); 517 | } 518 | 519 | embedContent.appendChild(embedText); 520 | 521 | // embed thumbnail 522 | if (embed.getThumbnail() != null) { 523 | handleEmbedThumbnail(document, embed, embedContent); 524 | } 525 | 526 | embedContentContainer.appendChild(embedContent); 527 | 528 | // embed image 529 | if (embed.getImage() != null) { 530 | handleEmbedImage(document, embed, embedContentContainer); 531 | } 532 | 533 | // embed footer 534 | if (embed.getFooter() != null) { 535 | handleFooter(document, embed, embedContentContainer); 536 | } 537 | 538 | embedDiv.appendChild(embedContentContainer); 539 | content.appendChild(embedDiv); 540 | } 541 | } 542 | 543 | messageGroup.appendChild(content); 544 | assert chatLog != null; 545 | chatLog.appendChild(messageGroup); 546 | } 547 | return new ByteArrayInputStream(document.outerHtml().getBytes(Charsets.UTF_8)); 548 | } 549 | 550 | private InputStream findFile() { 551 | InputStream inputStream = getClass().getClassLoader().getResourceAsStream("template.html"); 552 | if (inputStream == null) { 553 | throw new IllegalArgumentException("file is not found: " + "template.html"); 554 | } 555 | return inputStream; 556 | } 557 | } -------------------------------------------------------------------------------- /src/main/java/me/ryzeon/transcripts/Formatter.java: -------------------------------------------------------------------------------- 1 | package me.ryzeon.transcripts; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | import java.awt.*; 6 | import java.util.regex.Matcher; 7 | import java.util.regex.Pattern; 8 | 9 | /** 10 | * Created by Ryzeon 11 | * Project: discord-html-transcripts 12 | * Date: 2/12/21 @ 00:32 13 | * Twitter: @Ryzeon_ 😎 14 | * Github: github.ryzeon.me 15 | */ 16 | @UtilityClass 17 | public class Formatter { 18 | 19 | private final Pattern STRONG = Pattern.compile("\\*\\*(.+?)\\*\\*"); 20 | private final Pattern EM = Pattern.compile("\\*(.+?)\\*"); 21 | private final Pattern S = Pattern.compile("~~(.+?)~~"); 22 | private final Pattern U = Pattern.compile("__(.+?)__"); 23 | private final Pattern CODE = Pattern.compile("```(.+?)```"); 24 | private final Pattern CODE_1 = Pattern.compile("`(.+?)`"); 25 | private final Pattern QUOTE = Pattern.compile("^>{1,3} (.*)$"); 26 | private final Pattern LINK = Pattern.compile("\\[([^\\[]+)\\](\\((www|http:|https:)+[^\\s]+[\\w]\\))"); 27 | private final Pattern NEW_LINE = Pattern.compile("\\n"); 28 | 29 | public String formatBytes(long bytes) { 30 | int unit = 1024; 31 | if (bytes < unit) 32 | return bytes + " B"; 33 | int exp = (int) (Math.log(bytes) / Math.log(unit)); 34 | String pre = String.valueOf("KMGTPE".charAt(exp - 1)); 35 | return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre); 36 | } 37 | 38 | public String format(String originalText) { 39 | Matcher matcher = STRONG.matcher(originalText); 40 | String newText = originalText; 41 | while (matcher.find()) { 42 | String group = matcher.group(); 43 | newText = newText.replace(group, 44 | "" + group.replace("**", "") + ""); 45 | } 46 | matcher = EM.matcher(newText); 47 | while (matcher.find()) { 48 | String group = matcher.group(); 49 | newText = newText.replace(group, 50 | "" + group.replace("*", "") + ""); 51 | } 52 | matcher = S.matcher(newText); 53 | while (matcher.find()) { 54 | String group = matcher.group(); 55 | newText = newText.replace(group, 56 | "" + group.replace("~~", "") + ""); 57 | } 58 | matcher = U.matcher(newText); 59 | while (matcher.find()) { 60 | String group = matcher.group(); 61 | newText = newText.replace(group, 62 | "" + group.replace("__", "") + ""); 63 | } 64 | matcher = QUOTE.matcher(newText); 65 | while (matcher.find()) { 66 | String group = matcher.group(); 67 | newText = newText.replace(group, 68 | "" + group.replaceFirst(">>>", "").replaceFirst(">", "") + ""); 69 | } 70 | matcher = LINK.matcher(newText); 71 | while (matcher.find()) { 72 | String group = matcher.group(1); 73 | String link = matcher.group(2); 74 | String raw = "[" + group + "]" + link; 75 | 76 | newText = newText.replace(raw, "" + group + ""); 77 | } 78 | 79 | matcher = CODE.matcher(newText); 80 | boolean findCode = false; 81 | while (matcher.find()) { 82 | String group = matcher.group(); 83 | newText = newText.replace(group, 84 | "
" 85 | + group.replace("```", "").substring(3, -3) + "
"); 86 | findCode = true; 87 | } 88 | if (!findCode) { 89 | matcher = CODE_1.matcher(newText); 90 | while (matcher.find()) { 91 | String group = matcher.group(); 92 | newText = newText.replace(group, 93 | "" + group.replace("`", "") + ""); 94 | } 95 | } 96 | matcher = NEW_LINE.matcher(newText); 97 | while (matcher.find()) { 98 | newText = newText.replace(matcher.group(), "
"); 99 | } 100 | return newText; 101 | } 102 | 103 | public String toHex(Color color) { 104 | String hex = Integer.toHexString(color.getRGB() & 0xffffff); 105 | while (hex.length() < 6) { 106 | hex = "0" + hex; 107 | } 108 | return hex; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/main/java/me/ryzeon/transcripts/utils/format/IFormatHelper.java: -------------------------------------------------------------------------------- 1 | package me.ryzeon.transcripts.utils.format; 2 | 3 | import java.util.List; 4 | import java.util.function.Predicate; 5 | 6 | public interface IFormatHelper { 7 | 8 | List formats(); 9 | 10 | default boolean isFormat(String format) { 11 | return formats().stream().anyMatch(Predicate.isEqual(format)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/me/ryzeon/transcripts/utils/format/impl/AudioFormat.java: -------------------------------------------------------------------------------- 1 | package me.ryzeon.transcripts.utils.format.impl; 2 | 3 | import me.ryzeon.transcripts.utils.format.IFormatHelper; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class AudioFormat implements IFormatHelper { 9 | 10 | final List formats = Arrays.asList("mp3", "wav", "ogg", "flac"); 11 | 12 | @Override 13 | public List formats() { 14 | return formats; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/ryzeon/transcripts/utils/format/impl/ImageFormat.java: -------------------------------------------------------------------------------- 1 | package me.ryzeon.transcripts.utils.format.impl; 2 | 3 | import me.ryzeon.transcripts.utils.format.IFormatHelper; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.function.Predicate; 8 | 9 | public class ImageFormat implements IFormatHelper { 10 | 11 | final List formats = Arrays.asList("png", "jpg", "jpeg", "gif"); 12 | @Override 13 | public List formats() { 14 | return formats; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/me/ryzeon/transcripts/utils/format/impl/VideoFormat.java: -------------------------------------------------------------------------------- 1 | package me.ryzeon.transcripts.utils.format.impl; 2 | 3 | import me.ryzeon.transcripts.utils.format.IFormatHelper; 4 | 5 | import java.util.Arrays; 6 | import java.util.List; 7 | 8 | public class VideoFormat implements IFormatHelper { 9 | final List formats = Arrays.asList("mp4", "webm", "mkv", "avi", "mov", "flv", "wmv", "mpg", "mpeg"); 10 | @Override 11 | public List formats() { 12 | return formats; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Here title 6 | 7 | 8 | 9 | 618 | 619 | 621 | 622 | 623 | 628 | 629 | 630 | 631 | 636 | 637 | 668 | 669 | 670 | 671 | 673 | 675 | 677 | 679 | 680 | 681 | 682 | 683 | 684 | 685 |
686 |
687 | Guild icon 689 |
690 |
691 |
Guild Name Here
692 |
Channel Name Here
693 |
694 |
695 | 696 |
697 | 698 |
699 | 700 | 701 | 702 | --------------------------------------------------------------------------------