Add webhook text length stuff
Some checks failed
Test and build Docker image / docker (push) Failing after 15s

This commit is contained in:
Joakim Hellsén 2026-07-02 04:40:38 +02:00
commit 85f25263f3
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
9 changed files with 525 additions and 26 deletions

View file

@ -301,6 +301,38 @@ def get_screenshot_layout(reader: Reader, feed: Feed) -> ScreenshotLayout:
return "desktop"
def coerce_webhook_text_length_limit(value: JsonValue) -> int:
"""Return the supported webhook text length limit for a stored tag value."""
if isinstance(value, bool):
return 4000
if isinstance(value, int):
return min(max(value, 1), 4000)
if isinstance(value, str):
try:
parsed_value: int = int(value.strip())
except ValueError:
return 4000
return min(max(parsed_value, 1), 4000)
return 4000
def get_feed_webhook_text_length_limit(reader: Reader, feed: Feed | str) -> int:
"""Resolve how much webhook text a feed may send to Discord.
Returns:
The configured webhook text length limit, normalized to a supported Discord length.
"""
feed_url: str = str(getattr(feed, "url", feed))
try:
value = reader.get_tag(feed, "webhook_text_length_limit", 4000)
except ReaderError:
logger.exception("Error getting %s tag for feed: %s", "webhook_text_length_limit", feed_url)
return 4000
return coerce_webhook_text_length_limit(value)
def coerce_media_gallery_image_limit(value: JsonValue) -> int: # noqa: PLR0911
"""Return the supported media gallery image limit for a stored tag value."""
if isinstance(value, bool):
@ -1090,7 +1122,11 @@ def create_text_webhook(
if not webhook_message:
webhook_message = "No message found."
webhook_message = truncate_webhook_message(webhook_message)
webhook_text_length_limit: int = get_feed_webhook_text_length_limit(reader, entry.feed)
webhook_message = truncate_webhook_message(
webhook_message,
max_content_length=webhook_text_length_limit,
)
return DiscordWebhook(url=webhook_url, content=webhook_message, rate_limit_retry=True)
@ -1345,21 +1381,26 @@ def send_discord_quest_notification(entry: Entry, webhook_url: str, reader: Read
logger.info("No quest notification found in entry: %s", entry.id)
def set_description(custom_embed: CustomEmbed, discord_embed: DiscordEmbed) -> None:
def set_description(
custom_embed: CustomEmbed,
discord_embed: DiscordEmbed,
*,
max_description_length: int = 2000,
) -> None:
"""Set the description of the embed.
Args:
custom_embed (custom_message.CustomEmbed): The custom embed to get the description from.
discord_embed (DiscordEmbed): The Discord embed to set the description on.
max_description_length: The maximum embed description length to send.
"""
# Its actually 2048, but we will use 2000 to be safe.
max_description_length: int = 2000
# Discord allows 2048, but we keep a small safety margin by default.
embed_description: str = custom_embed.description
embed_description = (
f"{embed_description[:max_description_length]}..."
if len(embed_description) > max_description_length
else embed_description
)
if len(embed_description) > max_description_length:
if max_description_length <= 3: # noqa: PLR2004
embed_description = embed_description[:max_description_length]
else:
embed_description = f"{embed_description[: max_description_length - 3]}..."
discord_embed.set_description(embed_description) if embed_description else None
@ -1550,19 +1591,29 @@ def get_entry_media_gallery_items(
return media_items[:image_limit]
def truncate_component_text(content: str) -> str:
def truncate_component_text(
content: str,
*,
max_text_display_length: int = 4000,
) -> str:
"""Trim a Text Display component to a conservative Discord-safe length.
Returns:
Original or truncated component text.
"""
max_text_display_length: int = 4000
if len(content) <= max_text_display_length:
return content
if max_text_display_length <= 3: # noqa: PLR2004
return content[:max_text_display_length]
return f"{content[: max_text_display_length - 3]}..."
def get_component_text_display_content(custom_embed: CustomEmbed, entry: Entry) -> str:
def get_component_text_display_content(
custom_embed: CustomEmbed,
entry: Entry,
*,
max_text_length: int = 4000,
) -> str:
"""Build markdown text for a Components V2 Text Display.
Returns:
@ -1592,7 +1643,7 @@ def get_component_text_display_content(custom_embed: CustomEmbed, entry: Entry)
fallback_text = f"[{fallback_text}]({entry.link})"
parts.append(fallback_text)
return truncate_component_text("\n\n".join(parts))
return truncate_component_text("\n\n".join(parts), max_text_display_length=max_text_length)
def create_media_gallery_component(media_items: list[JsonObject]) -> JsonObject:
@ -1619,6 +1670,8 @@ def create_components_v2_webhook(
entry: Entry,
custom_embed: CustomEmbed,
media_items: list[JsonObject],
*,
max_text_length: int,
) -> DiscordWebhook:
"""Create a Components V2 webhook with text and a media gallery.
@ -1628,7 +1681,11 @@ def create_components_v2_webhook(
components: list[JsonValue] = [
{
"type": 10,
"content": get_component_text_display_content(custom_embed, entry),
"content": get_component_text_display_content(
custom_embed,
entry,
max_text_length=max_text_length,
),
},
create_media_gallery_component(media_items),
]
@ -1661,6 +1718,7 @@ def create_embed_webhook( # noqa: C901, PLR0912
# Get the embed data from the database.
custom_embed: CustomEmbed = replace_tags_in_embed(feed=feed, entry=entry, reader=reader)
media_gallery_image_limit: int = get_feed_media_gallery_image_limit(reader, feed)
webhook_text_length_limit: int = get_feed_webhook_text_length_limit(reader, feed)
if media_gallery_image_limit == 0:
custom_embed.image_url = ""
custom_embed.thumbnail_url = ""
@ -1671,11 +1729,21 @@ def create_embed_webhook( # noqa: C901, PLR0912
image_limit=media_gallery_image_limit,
)
if media_gallery_items:
return create_components_v2_webhook(webhook_url, entry, custom_embed, media_gallery_items)
return create_components_v2_webhook(
webhook_url,
entry,
custom_embed,
media_gallery_items,
max_text_length=webhook_text_length_limit,
)
discord_embed: DiscordEmbed = DiscordEmbed()
set_description(custom_embed=custom_embed, discord_embed=discord_embed)
set_description(
custom_embed=custom_embed,
discord_embed=discord_embed,
max_description_length=min(webhook_text_length_limit, 2000),
)
set_title(custom_embed=custom_embed, discord_embed=discord_embed)
custom_embed_author_url: str | None = custom_embed.author_url
@ -1889,20 +1957,28 @@ def should_send_embed_check(reader: Reader, entry: Entry) -> bool:
return get_entry_delivery_mode(reader, entry) == "embed"
def truncate_webhook_message(webhook_message: str) -> str:
def truncate_webhook_message(
webhook_message: str,
*,
max_content_length: int = 4000,
) -> str:
"""Truncate the webhook message if it is too long.
Args:
webhook_message (str): The webhook message to truncate.
max_content_length: The maximum number of characters Discord should receive.
Returns:
str: The truncated webhook message.
"""
max_content_length: int = 4000
if len(webhook_message) > max_content_length:
half_length = (max_content_length - 3) // 2 # Subtracting 3 for the "..." in the middle
webhook_message = f"{webhook_message[:half_length]}...{webhook_message[-half_length:]}"
return webhook_message
if len(webhook_message) <= max_content_length:
return webhook_message
if max_content_length <= 3: # noqa: PLR2004
return webhook_message[:max_content_length]
head_length = (max_content_length - 3) // 2
tail_length = max_content_length - 3 - head_length
return f"{webhook_message[:head_length]}...{webhook_message[-tail_length:]}"
def get_raw_autodiscover_links(reader: Reader, feed_url: str) -> object | None:
@ -1995,6 +2071,14 @@ def create_feed(reader: Reader, feed_url: str, webhook_dropdown: str) -> None:
"media_gallery_image_limit",
cast("JSONType", 1),
)
global_webhook_text_length_limit: int = coerce_webhook_text_length_limit(
cast("JsonValue", reader.get_tag((), "webhook_text_length_limit", 4000)),
)
reader.set_tag(
clean_feed_url,
"webhook_text_length_limit",
cast("JSONType", global_webhook_text_length_limit),
)
# This is the default message that will be sent to Discord.
reader.set_tag(clean_feed_url, "custom_message", default_custom_message) # pyright: ignore[reportArgumentType]