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)