Add webhook text length stuff
Some checks failed
Test and build Docker image / docker (push) Failing after 15s
Some checks failed
Test and build Docker image / docker (push) Failing after 15s
This commit is contained in:
parent
d16fa92e4d
commit
85f25263f3
9 changed files with 525 additions and 26 deletions
|
|
@ -301,6 +301,38 @@ def get_screenshot_layout(reader: Reader, feed: Feed) -> ScreenshotLayout:
|
||||||
return "desktop"
|
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
|
def coerce_media_gallery_image_limit(value: JsonValue) -> int: # noqa: PLR0911
|
||||||
"""Return the supported media gallery image limit for a stored tag value."""
|
"""Return the supported media gallery image limit for a stored tag value."""
|
||||||
if isinstance(value, bool):
|
if isinstance(value, bool):
|
||||||
|
|
@ -1090,7 +1122,11 @@ def create_text_webhook(
|
||||||
if not webhook_message:
|
if not webhook_message:
|
||||||
webhook_message = "No message found."
|
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)
|
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)
|
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.
|
"""Set the description of the embed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
custom_embed (custom_message.CustomEmbed): The custom embed to get the description from.
|
custom_embed (custom_message.CustomEmbed): The custom embed to get the description from.
|
||||||
discord_embed (DiscordEmbed): The Discord embed to set the description on.
|
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.
|
# Discord allows 2048, but we keep a small safety margin by default.
|
||||||
max_description_length: int = 2000
|
|
||||||
embed_description: str = custom_embed.description
|
embed_description: str = custom_embed.description
|
||||||
embed_description = (
|
if len(embed_description) > max_description_length:
|
||||||
f"{embed_description[:max_description_length]}..."
|
if max_description_length <= 3: # noqa: PLR2004
|
||||||
if len(embed_description) > max_description_length
|
embed_description = embed_description[:max_description_length]
|
||||||
else embed_description
|
else:
|
||||||
)
|
embed_description = f"{embed_description[: max_description_length - 3]}..."
|
||||||
discord_embed.set_description(embed_description) if embed_description else None
|
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]
|
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.
|
"""Trim a Text Display component to a conservative Discord-safe length.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Original or truncated component text.
|
Original or truncated component text.
|
||||||
"""
|
"""
|
||||||
max_text_display_length: int = 4000
|
|
||||||
if len(content) <= max_text_display_length:
|
if len(content) <= max_text_display_length:
|
||||||
return content
|
return content
|
||||||
|
if max_text_display_length <= 3: # noqa: PLR2004
|
||||||
|
return content[:max_text_display_length]
|
||||||
return f"{content[: max_text_display_length - 3]}..."
|
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.
|
"""Build markdown text for a Components V2 Text Display.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
@ -1592,7 +1643,7 @@ def get_component_text_display_content(custom_embed: CustomEmbed, entry: Entry)
|
||||||
fallback_text = f"[{fallback_text}]({entry.link})"
|
fallback_text = f"[{fallback_text}]({entry.link})"
|
||||||
parts.append(fallback_text)
|
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:
|
def create_media_gallery_component(media_items: list[JsonObject]) -> JsonObject:
|
||||||
|
|
@ -1619,6 +1670,8 @@ def create_components_v2_webhook(
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
custom_embed: CustomEmbed,
|
custom_embed: CustomEmbed,
|
||||||
media_items: list[JsonObject],
|
media_items: list[JsonObject],
|
||||||
|
*,
|
||||||
|
max_text_length: int,
|
||||||
) -> DiscordWebhook:
|
) -> DiscordWebhook:
|
||||||
"""Create a Components V2 webhook with text and a media gallery.
|
"""Create a Components V2 webhook with text and a media gallery.
|
||||||
|
|
||||||
|
|
@ -1628,7 +1681,11 @@ def create_components_v2_webhook(
|
||||||
components: list[JsonValue] = [
|
components: list[JsonValue] = [
|
||||||
{
|
{
|
||||||
"type": 10,
|
"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),
|
create_media_gallery_component(media_items),
|
||||||
]
|
]
|
||||||
|
|
@ -1661,6 +1718,7 @@ def create_embed_webhook( # noqa: C901, PLR0912
|
||||||
# Get the embed data from the database.
|
# Get the embed data from the database.
|
||||||
custom_embed: CustomEmbed = replace_tags_in_embed(feed=feed, entry=entry, reader=reader)
|
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)
|
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:
|
if media_gallery_image_limit == 0:
|
||||||
custom_embed.image_url = ""
|
custom_embed.image_url = ""
|
||||||
custom_embed.thumbnail_url = ""
|
custom_embed.thumbnail_url = ""
|
||||||
|
|
@ -1671,11 +1729,21 @@ def create_embed_webhook( # noqa: C901, PLR0912
|
||||||
image_limit=media_gallery_image_limit,
|
image_limit=media_gallery_image_limit,
|
||||||
)
|
)
|
||||||
if media_gallery_items:
|
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()
|
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)
|
set_title(custom_embed=custom_embed, discord_embed=discord_embed)
|
||||||
|
|
||||||
custom_embed_author_url: str | None = custom_embed.author_url
|
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"
|
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.
|
"""Truncate the webhook message if it is too long.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
webhook_message (str): The webhook message to truncate.
|
webhook_message (str): The webhook message to truncate.
|
||||||
|
max_content_length: The maximum number of characters Discord should receive.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
str: The truncated webhook message.
|
str: The truncated webhook message.
|
||||||
"""
|
"""
|
||||||
max_content_length: int = 4000
|
if len(webhook_message) <= max_content_length:
|
||||||
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
|
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:
|
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",
|
"media_gallery_image_limit",
|
||||||
cast("JSONType", 1),
|
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.
|
# 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]
|
reader.set_tag(clean_feed_url, "custom_message", default_custom_message) # pyright: ignore[reportArgumentType]
|
||||||
|
|
|
||||||
|
|
@ -54,11 +54,13 @@ from discord_rss_bot.custom_message import save_embed
|
||||||
from discord_rss_bot.feeds import FeedUpdateError
|
from discord_rss_bot.feeds import FeedUpdateError
|
||||||
from discord_rss_bot.feeds import SentWebhookRecord
|
from discord_rss_bot.feeds import SentWebhookRecord
|
||||||
from discord_rss_bot.feeds import coerce_media_gallery_image_limit
|
from discord_rss_bot.feeds import coerce_media_gallery_image_limit
|
||||||
|
from discord_rss_bot.feeds import coerce_webhook_text_length_limit
|
||||||
from discord_rss_bot.feeds import create_feed
|
from discord_rss_bot.feeds import create_feed
|
||||||
from discord_rss_bot.feeds import extract_domain
|
from discord_rss_bot.feeds import extract_domain
|
||||||
from discord_rss_bot.feeds import feed_saves_sent_webhooks
|
from discord_rss_bot.feeds import feed_saves_sent_webhooks
|
||||||
from discord_rss_bot.feeds import get_feed_delivery_mode
|
from discord_rss_bot.feeds import get_feed_delivery_mode
|
||||||
from discord_rss_bot.feeds import get_feed_media_gallery_image_limit
|
from discord_rss_bot.feeds import get_feed_media_gallery_image_limit
|
||||||
|
from discord_rss_bot.feeds import get_feed_webhook_text_length_limit
|
||||||
from discord_rss_bot.feeds import get_screenshot_layout
|
from discord_rss_bot.feeds import get_screenshot_layout
|
||||||
from discord_rss_bot.feeds import get_sent_webhook_records
|
from discord_rss_bot.feeds import get_sent_webhook_records
|
||||||
from discord_rss_bot.feeds import send_entry_to_discord
|
from discord_rss_bot.feeds import send_entry_to_discord
|
||||||
|
|
@ -1477,6 +1479,38 @@ async def post_set_feed_media_gallery_image_limit(
|
||||||
return RedirectResponse(url=f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303)
|
return RedirectResponse(url=f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/set_feed_webhook_text_length_limit")
|
||||||
|
async def post_set_feed_webhook_text_length_limit(
|
||||||
|
feed_url: Annotated[str, Form()],
|
||||||
|
text_length_limit: Annotated[int, Form()],
|
||||||
|
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||||
|
) -> RedirectResponse:
|
||||||
|
"""Set the maximum webhook text length for a feed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RedirectResponse: Redirect to the feed page.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If the feed does not exist.
|
||||||
|
"""
|
||||||
|
clean_feed_url: str = feed_url.strip()
|
||||||
|
clean_text_length_limit: int = coerce_webhook_text_length_limit(text_length_limit)
|
||||||
|
clean_text_length_limit_json: JSONType = cast("JSONType", clean_text_length_limit)
|
||||||
|
|
||||||
|
try:
|
||||||
|
reader.get_feed(clean_feed_url)
|
||||||
|
except FeedNotFoundError as e:
|
||||||
|
raise HTTPException(status_code=404, detail="Feed not found") from e
|
||||||
|
|
||||||
|
reader.set_tag(
|
||||||
|
clean_feed_url,
|
||||||
|
"webhook_text_length_limit",
|
||||||
|
clean_text_length_limit_json,
|
||||||
|
)
|
||||||
|
commit_state_change(reader, f"Set webhook text length limit to {clean_text_length_limit} for {clean_feed_url}")
|
||||||
|
return RedirectResponse(url=f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/set_update_interval")
|
@app.post("/set_update_interval")
|
||||||
async def post_set_update_interval(
|
async def post_set_update_interval(
|
||||||
feed_url: Annotated[str, Form()],
|
feed_url: Annotated[str, Form()],
|
||||||
|
|
@ -1681,6 +1715,28 @@ async def post_set_global_delivery_mode(
|
||||||
return RedirectResponse(url="/settings", status_code=303)
|
return RedirectResponse(url="/settings", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/set_global_webhook_text_length_limit")
|
||||||
|
async def post_set_global_webhook_text_length_limit(
|
||||||
|
text_length_limit: Annotated[int, Form()],
|
||||||
|
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||||
|
) -> RedirectResponse:
|
||||||
|
"""Set the global default webhook text length limit for newly added feeds.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text_length_limit: The max webhook text length.
|
||||||
|
reader: The Reader instance.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
RedirectResponse: Redirect to the settings page.
|
||||||
|
"""
|
||||||
|
clean_text_length_limit: int = coerce_webhook_text_length_limit(text_length_limit)
|
||||||
|
clean_text_length_limit_json: JSONType = cast("JSONType", clean_text_length_limit)
|
||||||
|
|
||||||
|
reader.set_tag((), "webhook_text_length_limit", clean_text_length_limit_json) # pyright: ignore[reportArgumentType]
|
||||||
|
commit_state_change(reader, f"Set global webhook text length limit to {clean_text_length_limit}")
|
||||||
|
return RedirectResponse(url="/settings", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@app.get("/add", response_class=HTMLResponse)
|
@app.get("/add", response_class=HTMLResponse)
|
||||||
def get_add(
|
def get_add(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|
@ -1796,6 +1852,8 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
||||||
"has_whitelist_filters": has_whitelist_filters,
|
"has_whitelist_filters": has_whitelist_filters,
|
||||||
"media_gallery_image_limit": get_feed_media_gallery_image_limit(reader, feed),
|
"media_gallery_image_limit": get_feed_media_gallery_image_limit(reader, feed),
|
||||||
"max_media_gallery_items": 10,
|
"max_media_gallery_items": 10,
|
||||||
|
"webhook_text_length_limit": get_feed_webhook_text_length_limit(reader, feed),
|
||||||
|
"max_webhook_text_length_limit": 4000,
|
||||||
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
||||||
|
|
@ -1860,6 +1918,8 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
||||||
"has_whitelist_filters": has_whitelist_filters,
|
"has_whitelist_filters": has_whitelist_filters,
|
||||||
"media_gallery_image_limit": get_feed_media_gallery_image_limit(reader, feed),
|
"media_gallery_image_limit": get_feed_media_gallery_image_limit(reader, feed),
|
||||||
"max_media_gallery_items": 10,
|
"max_media_gallery_items": 10,
|
||||||
|
"webhook_text_length_limit": get_feed_webhook_text_length_limit(reader, feed),
|
||||||
|
"max_webhook_text_length_limit": 4000,
|
||||||
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
||||||
|
|
@ -2071,6 +2131,10 @@ async def get_settings(
|
||||||
if global_delivery_mode not in {"embed", "text"}:
|
if global_delivery_mode not in {"embed", "text"}:
|
||||||
global_delivery_mode = "embed"
|
global_delivery_mode = "embed"
|
||||||
|
|
||||||
|
global_webhook_text_length_limit: int = coerce_webhook_text_length_limit(
|
||||||
|
reader.get_tag((), "webhook_text_length_limit", 4000)
|
||||||
|
)
|
||||||
|
|
||||||
# Get all feeds with their intervals
|
# Get all feeds with their intervals
|
||||||
feeds: Iterable[Feed] = reader.get_feeds()
|
feeds: Iterable[Feed] = reader.get_feeds()
|
||||||
feed_intervals = []
|
feed_intervals = []
|
||||||
|
|
@ -2094,6 +2158,8 @@ async def get_settings(
|
||||||
"global_interval": global_interval,
|
"global_interval": global_interval,
|
||||||
"global_delivery_mode": global_delivery_mode,
|
"global_delivery_mode": global_delivery_mode,
|
||||||
"global_screenshot_layout": global_screenshot_layout,
|
"global_screenshot_layout": global_screenshot_layout,
|
||||||
|
"global_webhook_text_length_limit": global_webhook_text_length_limit,
|
||||||
|
"max_webhook_text_length_limit": 4000,
|
||||||
"feed_intervals": feed_intervals,
|
"feed_intervals": feed_intervals,
|
||||||
}
|
}
|
||||||
return templates.TemplateResponse(request=request, name="settings.html", context=context)
|
return templates.TemplateResponse(request=request, name="settings.html", context=context)
|
||||||
|
|
|
||||||
|
|
@ -85,4 +85,8 @@ def get_reader(custom_location: Path | None = None) -> Reader:
|
||||||
if reader.get_tag((), "delivery_mode", None) is None:
|
if reader.get_tag((), "delivery_mode", None) is None:
|
||||||
reader.set_tag((), "delivery_mode", "embed") # pyright: ignore[reportArgumentType]
|
reader.set_tag((), "delivery_mode", "embed") # pyright: ignore[reportArgumentType]
|
||||||
|
|
||||||
|
# Set the default webhook text length limit for new feeds if not already configured.
|
||||||
|
if reader.get_tag((), "webhook_text_length_limit", None) is None:
|
||||||
|
reader.set_tag((), "webhook_text_length_limit", 4000) # pyright: ignore[reportArgumentType]
|
||||||
|
|
||||||
return reader
|
return reader
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<span class="badge status-chip bg-secondary">Text limit: {{ webhook_text_length_limit }} chars</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<section class="mt-4 pt-3 border-top border-secondary-subtle">
|
<section class="mt-4 pt-3 border-top border-secondary-subtle">
|
||||||
|
|
@ -102,6 +103,7 @@
|
||||||
{% elif delivery_mode == "screenshot" %}
|
{% elif delivery_mode == "screenshot" %}
|
||||||
<li>Screenshot layout: {{ screenshot_layout }}.</li>
|
<li>Screenshot layout: {{ screenshot_layout }}.</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<li>Webhook text is truncated to {{ webhook_text_length_limit }} characters.</li>
|
||||||
<li>
|
<li>
|
||||||
Filters:
|
Filters:
|
||||||
{% if has_blacklist_filters and has_whitelist_filters %}
|
{% if has_blacklist_filters and has_whitelist_filters %}
|
||||||
|
|
@ -284,6 +286,38 @@
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<section class="mt-4 pt-3 border-top border-secondary-subtle">
|
||||||
|
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
|
||||||
|
<h3 class="h6 text-uppercase text-muted mb-0">Text Delivery</h3>
|
||||||
|
<span class="badge {{ 'bg-info' if webhook_text_length_limit < max_webhook_text_length_limit else 'bg-secondary' }}">
|
||||||
|
Max {{ webhook_text_length_limit }} characters
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p id="textDeliveryHelp" class="text-muted mb-3">
|
||||||
|
Limit message length. Text mode allows values up to 4000 characters. Embeds are capped at 2000 characters.
|
||||||
|
</p>
|
||||||
|
<form action="/set_feed_webhook_text_length_limit"
|
||||||
|
method="post"
|
||||||
|
class="mb-0">
|
||||||
|
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
|
||||||
|
<div class="d-flex flex-wrap align-items-end gap-3">
|
||||||
|
<div>
|
||||||
|
<label class="form-label small text-muted mb-2" for="text_length_limit">Max characters</label>
|
||||||
|
<input id="text_length_limit"
|
||||||
|
class="form-control form-control-sm bg-dark border-dark text-light"
|
||||||
|
type="number"
|
||||||
|
name="text_length_limit"
|
||||||
|
min="1"
|
||||||
|
max="{{ max_webhook_text_length_limit }}"
|
||||||
|
step="1"
|
||||||
|
value="{{ webhook_text_length_limit }}"
|
||||||
|
aria-describedby="textDeliveryHelp"
|
||||||
|
required />
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-outline-light btn-sm" type="submit">Save text limit</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
<section class="mt-4 pt-3 border-top border-secondary-subtle">
|
<section class="mt-4 pt-3 border-top border-secondary-subtle">
|
||||||
<h3 class="h6 text-uppercase text-muted mb-3">Customization</h3>
|
<h3 class="h6 text-uppercase text-muted mb-3">Customization</h3>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
Settings | discord-rss-bot
|
Settings | discord-rss-bot
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% block description %}
|
{% block description %}
|
||||||
Adjust default update intervals, delivery modes, and screenshot layout for feeds managed by your bot.
|
Adjust default update intervals, delivery modes, screenshot layout, and webhook text limits for feeds managed by your bot.
|
||||||
{% endblock description %}
|
{% endblock description %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section>
|
<section>
|
||||||
|
|
@ -78,6 +78,28 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<form action="/set_global_webhook_text_length_limit"
|
||||||
|
method="post"
|
||||||
|
class="mt-4">
|
||||||
|
<div class="settings-form-row mb-2">
|
||||||
|
<label for="global_text_length_limit" class="form-label mb-1">Default text limit for new feeds</label>
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<input id="global_text_length_limit"
|
||||||
|
type="number"
|
||||||
|
class="form-control settings-input"
|
||||||
|
name="text_length_limit"
|
||||||
|
min="1"
|
||||||
|
max="{{ max_webhook_text_length_limit }}"
|
||||||
|
value="{{ global_webhook_text_length_limit }}"
|
||||||
|
required />
|
||||||
|
<button class="btn btn-primary px-4" type="submit">Save</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-text text-muted mt-2">
|
||||||
|
New feeds inherit this value. Existing feeds keep their current
|
||||||
|
per-feed text limit. Text mode allows values up to 4000 characters. Embeds are capped at 2000 characters.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</section>
|
</section>
|
||||||
<section class="mt-5">
|
<section class="mt-5">
|
||||||
<div class="text-light">
|
<div class="text-light">
|
||||||
|
|
|
||||||
|
|
@ -379,6 +379,43 @@ def test_get_feed_media_gallery_image_limit_defaults_to_first_image() -> None:
|
||||||
assert result == 1
|
assert result == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("tag_value", "expected_limit"),
|
||||||
|
[
|
||||||
|
(1, 1),
|
||||||
|
(25, 25),
|
||||||
|
(-1, 1),
|
||||||
|
(99_999, 4000),
|
||||||
|
("200", 200),
|
||||||
|
("0", 1),
|
||||||
|
("unknown", 4000),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_feed_webhook_text_length_limit_normalizes_stored_tag(
|
||||||
|
tag_value: feeds.JsonValue,
|
||||||
|
expected_limit: int,
|
||||||
|
) -> None:
|
||||||
|
reader = MagicMock()
|
||||||
|
feed = MagicMock()
|
||||||
|
feed.url = "https://example.com/feed.xml"
|
||||||
|
reader.get_tag.return_value = tag_value
|
||||||
|
|
||||||
|
result = feeds.get_feed_webhook_text_length_limit(reader, feed)
|
||||||
|
|
||||||
|
assert result == expected_limit
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_feed_webhook_text_length_limit_defaults_to_discord_limit() -> None:
|
||||||
|
reader = MagicMock()
|
||||||
|
feed = MagicMock()
|
||||||
|
feed.url = "https://example.com/feed.xml"
|
||||||
|
reader.get_tag.side_effect = lambda resource, key, default=None: default # noqa: ARG005
|
||||||
|
|
||||||
|
result = feeds.get_feed_webhook_text_length_limit(reader, feed)
|
||||||
|
|
||||||
|
assert result == 4000
|
||||||
|
|
||||||
|
|
||||||
def test_create_feed_inherits_global_screenshot_layout() -> None:
|
def test_create_feed_inherits_global_screenshot_layout() -> None:
|
||||||
reader = MagicMock()
|
reader = MagicMock()
|
||||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
|
@ -435,6 +472,41 @@ def test_create_feed_sets_default_media_gallery_image_limit() -> None:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_feed_sets_default_webhook_text_length_limit() -> None:
|
||||||
|
reader = MagicMock()
|
||||||
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"webhooks": [{"name": "Main", "url": "https://discord.com/api/webhooks/123/abc"}],
|
||||||
|
"screenshot_layout": "desktop",
|
||||||
|
"delivery_mode": "embed",
|
||||||
|
}.get(key, default)
|
||||||
|
|
||||||
|
create_feed(reader, "https://example.com/feed.xml", "Main")
|
||||||
|
|
||||||
|
reader.set_tag.assert_any_call(
|
||||||
|
"https://example.com/feed.xml",
|
||||||
|
"webhook_text_length_limit",
|
||||||
|
4000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_feed_inherits_global_webhook_text_length_limit() -> None:
|
||||||
|
reader = MagicMock()
|
||||||
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"webhooks": [{"name": "Main", "url": "https://discord.com/api/webhooks/123/abc"}],
|
||||||
|
"screenshot_layout": "desktop",
|
||||||
|
"delivery_mode": "embed",
|
||||||
|
"webhook_text_length_limit": 2500,
|
||||||
|
}.get(key, default)
|
||||||
|
|
||||||
|
create_feed(reader, "https://example.com/feed.xml", "Main")
|
||||||
|
|
||||||
|
reader.set_tag.assert_any_call(
|
||||||
|
"https://example.com/feed.xml",
|
||||||
|
"webhook_text_length_limit",
|
||||||
|
2500,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_create_feed_falls_back_to_embed_when_global_delivery_mode_is_invalid() -> None:
|
def test_create_feed_falls_back_to_embed_when_global_delivery_mode_is_invalid() -> None:
|
||||||
reader = MagicMock()
|
reader = MagicMock()
|
||||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
|
@ -664,6 +736,32 @@ def test_create_screenshot_webhook_falls_back_to_text_on_failure(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@patch("discord_rss_bot.feeds.replace_tags_in_text_message")
|
||||||
|
def test_create_text_webhook_uses_feed_text_length_limit(mock_replace_tags_in_text_message: MagicMock) -> None:
|
||||||
|
mock_replace_tags_in_text_message.return_value = "start-" + ("x" * 40) + "-end"
|
||||||
|
|
||||||
|
entry = MagicMock()
|
||||||
|
entry.feed.url = "https://example.com/feed.xml"
|
||||||
|
reader = MagicMock()
|
||||||
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"custom_message": "{{entry_title}}",
|
||||||
|
"webhook_text_length_limit": 20,
|
||||||
|
}.get(key, default)
|
||||||
|
|
||||||
|
webhook = feeds.create_text_webhook(
|
||||||
|
"https://discord.com/api/webhooks/123/abc",
|
||||||
|
entry,
|
||||||
|
reader,
|
||||||
|
use_default_message_on_empty=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert webhook.content is not None
|
||||||
|
assert len(webhook.content) == 20
|
||||||
|
assert webhook.content.startswith("start-")
|
||||||
|
assert webhook.content.endswith("-end")
|
||||||
|
assert "..." in webhook.content
|
||||||
|
|
||||||
|
|
||||||
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
|
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
|
||||||
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
||||||
def test_create_embed_webhook_uses_media_gallery_for_entry_images(
|
def test_create_embed_webhook_uses_media_gallery_for_entry_images(
|
||||||
|
|
@ -671,7 +769,10 @@ def test_create_embed_webhook_uses_media_gallery_for_entry_images(
|
||||||
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
reader = MagicMock()
|
reader = MagicMock()
|
||||||
reader.get_tag.return_value = 10
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"media_gallery_image_limit": 10,
|
||||||
|
"webhook_text_length_limit": 4000,
|
||||||
|
}.get(key, default)
|
||||||
entry = MagicMock()
|
entry = MagicMock()
|
||||||
entry.id = "entry-1"
|
entry.id = "entry-1"
|
||||||
entry.title = "Entry title"
|
entry.title = "Entry title"
|
||||||
|
|
@ -707,6 +808,38 @@ def test_create_embed_webhook_uses_media_gallery_for_entry_images(
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
|
||||||
|
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
||||||
|
def test_create_embed_webhook_uses_feed_text_length_limit_for_media_gallery(
|
||||||
|
mock_replace_tags_in_embed: MagicMock,
|
||||||
|
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
reader = MagicMock()
|
||||||
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"media_gallery_image_limit": 10,
|
||||||
|
"webhook_text_length_limit": 20,
|
||||||
|
}.get(key, default)
|
||||||
|
entry = MagicMock()
|
||||||
|
entry.id = "entry-1"
|
||||||
|
entry.title = "Entry title"
|
||||||
|
entry.link = "https://example.com/entry"
|
||||||
|
entry.summary = '<img src="https://example.com/summary.jpg" />'
|
||||||
|
entry.content = [MagicMock(value='<img src="https://example.com/content-1.jpg" />')]
|
||||||
|
entry.feed.url = "https://example.com/feed.xml"
|
||||||
|
mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(description="x" * 100)
|
||||||
|
|
||||||
|
webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||||
|
|
||||||
|
assert webhook.flags == 1 << 15
|
||||||
|
components = get_test_webhook_components(webhook)
|
||||||
|
text_component = components[0]
|
||||||
|
assert isinstance(text_component, dict)
|
||||||
|
assert isinstance(text_component["content"], str)
|
||||||
|
assert len(text_component["content"]) == 20
|
||||||
|
assert text_component["content"].endswith("...")
|
||||||
|
mock_fetch_ttvdrops_campaign_media_items.assert_called_once_with(entry)
|
||||||
|
|
||||||
|
|
||||||
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
|
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
|
||||||
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
||||||
def test_create_embed_webhook_can_limit_media_gallery_to_first_image(
|
def test_create_embed_webhook_can_limit_media_gallery_to_first_image(
|
||||||
|
|
@ -714,7 +847,10 @@ def test_create_embed_webhook_can_limit_media_gallery_to_first_image(
|
||||||
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
reader = MagicMock()
|
reader = MagicMock()
|
||||||
reader.get_tag.return_value = 1
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"media_gallery_image_limit": 1,
|
||||||
|
"webhook_text_length_limit": 4000,
|
||||||
|
}.get(key, default)
|
||||||
entry = MagicMock()
|
entry = MagicMock()
|
||||||
entry.id = "entry-1"
|
entry.id = "entry-1"
|
||||||
entry.title = "Entry title"
|
entry.title = "Entry title"
|
||||||
|
|
@ -744,7 +880,10 @@ def test_create_embed_webhook_can_disable_media_images(
|
||||||
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||||
) -> None:
|
) -> None:
|
||||||
reader = MagicMock()
|
reader = MagicMock()
|
||||||
reader.get_tag.return_value = 0
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"media_gallery_image_limit": 0,
|
||||||
|
"webhook_text_length_limit": 4000,
|
||||||
|
}.get(key, default)
|
||||||
entry = MagicMock()
|
entry = MagicMock()
|
||||||
entry.id = "entry-1"
|
entry.id = "entry-1"
|
||||||
entry.title = "Entry title"
|
entry.title = "Entry title"
|
||||||
|
|
@ -770,6 +909,38 @@ def test_create_embed_webhook_can_disable_media_images(
|
||||||
mock_fetch_ttvdrops_campaign_media_items.assert_not_called()
|
mock_fetch_ttvdrops_campaign_media_items.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items", return_value=[])
|
||||||
|
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
||||||
|
def test_create_embed_webhook_uses_feed_text_length_limit_for_regular_embed_description(
|
||||||
|
mock_replace_tags_in_embed: MagicMock,
|
||||||
|
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
reader = MagicMock()
|
||||||
|
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||||
|
"media_gallery_image_limit": 0,
|
||||||
|
"webhook_text_length_limit": 20,
|
||||||
|
}.get(key, default)
|
||||||
|
entry = MagicMock()
|
||||||
|
entry.id = "entry-1"
|
||||||
|
entry.title = "Entry title"
|
||||||
|
entry.link = "https://example.com/entry"
|
||||||
|
entry.summary = ""
|
||||||
|
entry.content = []
|
||||||
|
entry.feed.url = "https://example.com/feed.xml"
|
||||||
|
mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(description="x" * 100)
|
||||||
|
|
||||||
|
webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||||
|
|
||||||
|
assert "components" not in webhook.json
|
||||||
|
embeds = webhook.json.get("embeds")
|
||||||
|
assert isinstance(embeds, list)
|
||||||
|
assert isinstance(embeds[0], dict)
|
||||||
|
assert isinstance(embeds[0].get("description"), str)
|
||||||
|
assert len(embeds[0]["description"]) == 20
|
||||||
|
assert embeds[0]["description"].endswith("...")
|
||||||
|
mock_fetch_ttvdrops_campaign_media_items.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items")
|
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items")
|
||||||
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
||||||
def test_create_embed_webhook_prefers_ttvdrops_reward_images_and_alt_text(
|
def test_create_embed_webhook_prefers_ttvdrops_reward_images_and_alt_text(
|
||||||
|
|
|
||||||
|
|
@ -508,6 +508,26 @@ def test_post_set_global_screenshot_layout_triggers_backup(monkeypatch: pytest.M
|
||||||
assert "Set global screenshot layout to mobile" in commit_message
|
assert "Set global screenshot layout to mobile" in commit_message
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_set_global_webhook_text_length_limit_triggers_backup(
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
tmp_path: Path,
|
||||||
|
) -> None:
|
||||||
|
"""Posting to /set_global_webhook_text_length_limit should trigger a git backup."""
|
||||||
|
backup_path: Path = tmp_path / "backup"
|
||||||
|
monkeypatch.setenv("GIT_BACKUP_PATH", str(backup_path))
|
||||||
|
monkeypatch.delenv("GIT_BACKUP_REMOTE", raising=False)
|
||||||
|
|
||||||
|
with patch("discord_rss_bot.main.commit_state_change") as mock_commit:
|
||||||
|
response = client.post(url="/set_global_webhook_text_length_limit", data={"text_length_limit": "2500"})
|
||||||
|
assert response.status_code == 200, f"Failed to set global webhook text length limit: {response.text}"
|
||||||
|
mock_commit.assert_called_once()
|
||||||
|
|
||||||
|
call_args = mock_commit.call_args
|
||||||
|
assert call_args is not None
|
||||||
|
commit_message: str = call_args[0][1]
|
||||||
|
assert "Set global webhook text length limit to 2500" in commit_message
|
||||||
|
|
||||||
|
|
||||||
def test_post_custom_message_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
def test_post_custom_message_triggers_backup(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
||||||
"""Posting to /custom should trigger a git backup."""
|
"""Posting to /custom should trigger a git backup."""
|
||||||
backup_path: Path = tmp_path / "backup"
|
backup_path: Path = tmp_path / "backup"
|
||||||
|
|
|
||||||
|
|
@ -283,8 +283,13 @@ def test_get() -> None:
|
||||||
assert "This feed" in response.text
|
assert "This feed" in response.text
|
||||||
assert "Screenshot Delivery" in response.text
|
assert "Screenshot Delivery" in response.text
|
||||||
assert "Image Delivery" in response.text
|
assert "Image Delivery" in response.text
|
||||||
|
assert "Text Delivery" in response.text
|
||||||
|
assert "main embed" in response.text
|
||||||
|
assert "2000 characters" in response.text
|
||||||
assert 'type="range"' in response.text
|
assert 'type="range"' in response.text
|
||||||
assert 'max="10"' in response.text
|
assert 'max="10"' in response.text
|
||||||
|
assert 'id="text_length_limit"' in response.text
|
||||||
|
assert 'max="4000"' in response.text
|
||||||
|
|
||||||
response: Response = client.get(url="/")
|
response: Response = client.get(url="/")
|
||||||
assert response.status_code == 200, f"/ failed: {response.text}"
|
assert response.status_code == 200, f"/ failed: {response.text}"
|
||||||
|
|
@ -603,6 +608,10 @@ def test_settings_page_shows_screenshot_layout_setting() -> None:
|
||||||
assert response.status_code == 200, f"/settings failed: {response.text}"
|
assert response.status_code == 200, f"/settings failed: {response.text}"
|
||||||
assert "Default delivery mode for new feeds" in response.text
|
assert "Default delivery mode for new feeds" in response.text
|
||||||
assert "Default screenshot layout for new feeds" in response.text
|
assert "Default screenshot layout for new feeds" in response.text
|
||||||
|
assert "Default webhook text limit for new feeds" in response.text
|
||||||
|
assert "regular embed mode" in response.text
|
||||||
|
assert "2000 characters" in response.text
|
||||||
|
assert 'id="global_text_length_limit"' in response.text
|
||||||
assert "uv run playwright install chromium" in response.text
|
assert "uv run playwright install chromium" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -615,6 +624,18 @@ def test_set_global_delivery_mode() -> None:
|
||||||
assert re.search(r"<option\s+value=\"text\"[^>]*\bselected\b", response.text)
|
assert re.search(r"<option\s+value=\"text\"[^>]*\bselected\b", response.text)
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_global_webhook_text_length_limit() -> None:
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/set_global_webhook_text_length_limit",
|
||||||
|
data={"text_length_limit": "2500"},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to set global webhook text length limit: {response.text}"
|
||||||
|
|
||||||
|
response = client.get(url="/settings")
|
||||||
|
assert response.status_code == 200, f"/settings failed after setting webhook text length limit: {response.text}"
|
||||||
|
assert 'value="2500"' in response.text
|
||||||
|
|
||||||
|
|
||||||
def test_add_page_shows_global_default_delivery_mode_hint() -> None:
|
def test_add_page_shows_global_default_delivery_mode_hint() -> None:
|
||||||
response: Response = client.post(url="/set_global_delivery_mode", data={"delivery_mode": "text"})
|
response: Response = client.post(url="/set_global_delivery_mode", data={"delivery_mode": "text"})
|
||||||
assert response.status_code == 200, f"Failed to set global delivery mode: {response.text}"
|
assert response.status_code == 200, f"Failed to set global delivery mode: {response.text}"
|
||||||
|
|
@ -742,6 +763,41 @@ def test_set_feed_media_gallery_image_limit_route_updates_stored_tag() -> None:
|
||||||
app.dependency_overrides = {}
|
app.dependency_overrides = {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_feed_webhook_text_length_limit_route_updates_stored_tag() -> None:
|
||||||
|
@dataclass(slots=True)
|
||||||
|
class DummyFeed:
|
||||||
|
url: str
|
||||||
|
title: str
|
||||||
|
|
||||||
|
class StubReader:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.feed = DummyFeed(url="https://example.com/feed.xml", title="Example")
|
||||||
|
self.tags: dict[tuple[str, str], int] = {}
|
||||||
|
|
||||||
|
def get_feed(self, feed_url: str) -> DummyFeed:
|
||||||
|
assert feed_url == self.feed.url
|
||||||
|
return self.feed
|
||||||
|
|
||||||
|
def set_tag(self, resource: str, key: str, value: int) -> None:
|
||||||
|
self.tags[resource, key] = value
|
||||||
|
|
||||||
|
stub_reader = StubReader()
|
||||||
|
app.dependency_overrides[get_reader_dependency] = lambda: stub_reader
|
||||||
|
|
||||||
|
try:
|
||||||
|
with patch("discord_rss_bot.main.commit_state_change"):
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/set_feed_webhook_text_length_limit",
|
||||||
|
data={"feed_url": stub_reader.feed.url, "text_length_limit": "2500"},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 303, f"/set_feed_webhook_text_length_limit failed: {response.text}"
|
||||||
|
assert stub_reader.tags[stub_reader.feed.url, "webhook_text_length_limit"] == 2500
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides = {}
|
||||||
|
|
||||||
|
|
||||||
def test_sent_webhooks_view_shows_saved_records() -> None:
|
def test_sent_webhooks_view_shows_saved_records() -> None:
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class DummyFeed:
|
class DummyFeed:
|
||||||
|
|
|
||||||
|
|
@ -192,6 +192,48 @@ def test_get_reader_preserves_existing_global_delivery_mode() -> None:
|
||||||
get_reader.cache_clear()
|
get_reader.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_reader_sets_default_global_webhook_text_length_limit() -> None:
|
||||||
|
"""get_reader should initialize global webhook text length limit to 4000 when missing."""
|
||||||
|
get_reader.cache_clear()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
Path.mkdir(Path(temp_dir), exist_ok=True)
|
||||||
|
|
||||||
|
custom_loc: pathlib.Path = pathlib.Path(temp_dir, "webhook_text_length_limit_default_db.sqlite")
|
||||||
|
reader: Reader = get_reader(custom_location=custom_loc)
|
||||||
|
|
||||||
|
webhook_text_length_limit = reader.get_tag((), "webhook_text_length_limit", None)
|
||||||
|
assert webhook_text_length_limit == 4000, (
|
||||||
|
f"Expected default global webhook text length limit to be 4000, got: {webhook_text_length_limit}"
|
||||||
|
)
|
||||||
|
|
||||||
|
reader.close()
|
||||||
|
get_reader.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_reader_preserves_existing_global_webhook_text_length_limit() -> None:
|
||||||
|
"""get_reader should not overwrite an existing global webhook text length limit value."""
|
||||||
|
get_reader.cache_clear()
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as temp_dir:
|
||||||
|
Path.mkdir(Path(temp_dir), exist_ok=True)
|
||||||
|
|
||||||
|
custom_loc: pathlib.Path = pathlib.Path(temp_dir, "webhook_text_length_limit_existing_db.sqlite")
|
||||||
|
first_reader: Reader = get_reader(custom_location=custom_loc)
|
||||||
|
first_reader.set_tag((), "webhook_text_length_limit", 2500) # pyright: ignore[reportArgumentType]
|
||||||
|
first_reader.close()
|
||||||
|
get_reader.cache_clear()
|
||||||
|
|
||||||
|
second_reader: Reader = get_reader(custom_location=custom_loc)
|
||||||
|
webhook_text_length_limit = second_reader.get_tag((), "webhook_text_length_limit", None)
|
||||||
|
assert webhook_text_length_limit == 2500, (
|
||||||
|
f"Expected existing global webhook text length limit to stay 2500, got: {webhook_text_length_limit}"
|
||||||
|
)
|
||||||
|
|
||||||
|
second_reader.close()
|
||||||
|
get_reader.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
def test_get_reader_enables_autodiscover_plugin() -> None:
|
def test_get_reader_enables_autodiscover_plugin() -> None:
|
||||||
"""get_reader should store advertised feed links when HTML parsing fails."""
|
"""get_reader should store advertised feed links when HTML parsing fails."""
|
||||||
get_reader.cache_clear()
|
get_reader.cache_clear()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue