From 702576842abcb9100bf45238fe7842a3f5b1d823 Mon Sep 17 00:00:00 2001 From: Evanroby <107794516+Evanroby@users.noreply.github.com> Date: Wed, 6 May 2026 17:20:42 +0200 Subject: [PATCH] fix transcript rendering edge cases --- chat_exporter/construct/assets/component.py | 4 ++-- chat_exporter/construct/attachment_handler.py | 5 ++++- chat_exporter/construct/message.py | 9 ++++++--- chat_exporter/construct/transcript.py | 2 +- chat_exporter/ext/cache.py | 2 +- chat_exporter/parse/ast.py | 9 +++------ 6 files changed, 17 insertions(+), 14 deletions(-) diff --git a/chat_exporter/construct/assets/component.py b/chat_exporter/construct/assets/component.py index 7ae1388..34be2ea 100644 --- a/chat_exporter/construct/assets/component.py +++ b/chat_exporter/construct/assets/component.py @@ -246,7 +246,7 @@ async def build_menu_options(self, options): option_emoji = self._stringify_emoji(self._get_attr(option, "emoji", None)) is_default = bool(self._get_attr(option, "default", False)) default_class = "dropdownContentSelected" if is_default else "" - check_mark = "✓" if is_default else "" + check_mark = "✓" if is_default else "" label_escaped = html.escape(str(label)) if label else "" description_escaped = html.escape(str(description)) if description else "" @@ -551,7 +551,7 @@ def _build_media_properties(self, media, description, spoiler, css_class_prefix= if not spoiler and description: description_overlay = ( f'
' - f'{{html.escape(str(description))}}
' + f'{html.escape(str(description))}' ) return { diff --git a/chat_exporter/construct/attachment_handler.py b/chat_exporter/construct/attachment_handler.py index b86bbdd..108e0f2 100644 --- a/chat_exporter/construct/attachment_handler.py +++ b/chat_exporter/construct/attachment_handler.py @@ -38,7 +38,7 @@ async def process_asset(self, attachment: discord.Attachment) -> discord.Attachm :param attachment: discord.Attachment :return: str """ - file_name = urllib.parse.quote_plus(f"{datetime.datetime.utcnow().timestamp()}_{attachment.filename}") + file_name = urllib.parse.quote_plus(f"{datetime.datetime.now(datetime.timezone.utc).timestamp()}_{attachment.filename}") asset_path = self.base_path / file_name await attachment.save(asset_path) file_url = f"{self.url_base}/{file_name}" @@ -85,6 +85,7 @@ async def process_asset(self, attachment: discord.Attachment) -> discord.Attachm """Implement this to process the asset and return a url to the stored attachment. :param attachment: discord.Attachment :return: str""" + message = None try: if attachment.size > self.size_limit: file = discord.File(self.placeholder_path, filename="too_large.png") @@ -105,4 +106,6 @@ async def process_asset(self, attachment: discord.Attachment) -> discord.Attachm # discords http errors, including missing permissions raise e else: + if message is None: + raise aiohttp.ClientConnectionError("Webhook connection failed after 3 retries.") return message.attachments[0] diff --git a/chat_exporter/construct/message.py b/chat_exporter/construct/message.py index 3e37c4f..971d055 100644 --- a/chat_exporter/construct/message.py +++ b/chat_exporter/construct/message.py @@ -270,6 +270,8 @@ def get_interaction_status(interaction_message): message_edited_at = _set_edit_at(message_edited_at) avatar_url = message.author.display_avatar if message.author.display_avatar else DiscordUtils.default_avatar + reference_content = message.content.replace("\r\n", "\n").replace("\r", "\n") + reference_content = reference_content.replace("\n", " ").replace("
", " ") self.message.reference = await fill_out( self.guild, message_reference, @@ -279,7 +281,7 @@ def get_interaction_status(interaction_message): ("NAME_TAG", await discriminator(message.author.name, message.author.discriminator), PARSE_MODE_NONE), ("NAME", str(html.escape(message.author.display_name))), ("USER_COLOUR", user_colour, PARSE_MODE_NONE), - ("CONTENT", message.content.replace("\n", "").replace("
", ""), PARSE_MODE_REFERENCE), + ("CONTENT", reference_content, PARSE_MODE_REFERENCE), ("EDIT", message_edited_at, PARSE_MODE_NONE), ("ICON", icon, PARSE_MODE_NONE), ("USER_ID", str(message.author.id), PARSE_MODE_NONE), @@ -351,7 +353,7 @@ async def build_sticker(self): f"https://cdn.jsdelivr.net/gh/mahtoid/DiscordUtils@master/stickers/{sticker.pack_id}/{sticker.id}.gif" ) - self.rendered_content = await fill_out( + sticker_html = await fill_out( self.guild, img_attachment, [ @@ -359,6 +361,7 @@ async def build_sticker(self): ("ATTACH_URL_THUMB", str(sticker_image_url), PARSE_MODE_NONE), ], ) + self.rendered_content += sticker_html @staticmethod def calculate_grid_splits(n): @@ -687,7 +690,7 @@ def set_time(self, message: Optional[discord.Message] = None): return created_at_str, edited_at_str def to_local_time_str(self, time): - if not self.message.created_at.tzinfo: + if not time.tzinfo: time = timezone("UTC").localize(time) local_time = time.astimezone(timezone(self.pytz_timezone)) diff --git a/chat_exporter/construct/transcript.py b/chat_exporter/construct/transcript.py index 334c7c9..f8495bd 100644 --- a/chat_exporter/construct/transcript.py +++ b/chat_exporter/construct/transcript.py @@ -73,7 +73,7 @@ async def build_transcript(self): async def export_transcript(self, message_html: str, meta_data: str): guild_icon = ( self.channel.guild.icon - if (self.channel.guild.icon and len(self.channel.guild.icon) > 2) + if self.channel.guild.icon else DiscordUtils.default_avatar ) diff --git a/chat_exporter/ext/cache.py b/chat_exporter/ext/cache.py index e53ac19..5861ee1 100644 --- a/chat_exporter/ext/cache.py +++ b/chat_exporter/ext/cache.py @@ -54,7 +54,7 @@ def wrapper(*args, **kwargs): return _wrap_new_coroutine(value) wrapper.cache = _internal_cache - wrapper.clear_cache = _internal_cache.clear() + wrapper.clear_cache = _internal_cache.clear return wrapper return decorator diff --git a/chat_exporter/parse/ast.py b/chat_exporter/parse/ast.py index 3126490..a80fc36 100644 --- a/chat_exporter/parse/ast.py +++ b/chat_exporter/parse/ast.py @@ -4,6 +4,7 @@ from typing import List import pytz +import html class Node: @@ -184,11 +185,7 @@ def render(self, guild=None, bot=None): if member: member_name = member.display_name - escaped_name = ( - member_name.replace("<", self.ESCAPE_LT) - .replace(">", self.ESCAPE_GT) - .replace("&", self.ESCAPE_AMP) - ) + escaped_name = html.escape(member_name) return f'@{escaped_name}' else: return f'<@{self.user_id}>' @@ -236,7 +233,7 @@ def __init__(self, timestamp: int, format_str: str, original: str): self.original = original def render(self, guild=None, bot=None): - timestamp = self.timestamp - 1 + timestamp = self.timestamp try: time_stamp = time.gmtime(timestamp) datetime_stamp = datetime.datetime(2010, *time_stamp[1:6], tzinfo=pytz.utc)