Allow images to be between 0..10
All checks were successful
Test and build Docker image / docker (push) Successful in 1m54s
All checks were successful
Test and build Docker image / docker (push) Successful in 1m54s
This commit is contained in:
parent
a0c186559f
commit
3346994763
8 changed files with 496 additions and 74 deletions
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
|
@ -3,25 +3,29 @@
|
|||
"argnames",
|
||||
"argvalues",
|
||||
"autoexport",
|
||||
"autoplay",
|
||||
"botuser",
|
||||
"DISCORDTIMESTAMPPLACEHOLDER",
|
||||
"domcontentloaded",
|
||||
"Genshins",
|
||||
"healthcheck",
|
||||
"Hoyolab",
|
||||
"HTMX",
|
||||
"KHTML",
|
||||
"levelname",
|
||||
"Lovinator",
|
||||
"markdownified",
|
||||
"markdownify",
|
||||
"networkidle",
|
||||
"overwritable",
|
||||
"pipx",
|
||||
"pyproject",
|
||||
"Skulbladi",
|
||||
"thead",
|
||||
"thelovinator",
|
||||
"ttvdrops",
|
||||
"uvicorn"
|
||||
"uvicorn",
|
||||
"youtu"
|
||||
],
|
||||
"python.analysis.typeCheckingMode": "basic"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ if TYPE_CHECKING:
|
|||
from collections.abc import Iterable
|
||||
|
||||
from reader._types import EntryData
|
||||
from reader.types import JSONType
|
||||
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -103,13 +104,6 @@ class JsonResponseLike(Protocol):
|
|||
...
|
||||
|
||||
|
||||
MAX_DISCORD_UPLOAD_BYTES: int = 8 * 1024 * 1024
|
||||
MAX_MEDIA_GALLERY_ITEMS: int = 10
|
||||
MESSAGE_FLAG_IS_COMPONENTS_V2: int = 1 << 15
|
||||
TTVDROPS_HOST: str = "ttvdrops.lovinator.space"
|
||||
TTVDROPS_BASE_URL: str = f"https://{TTVDROPS_HOST}"
|
||||
SENT_WEBHOOKS_TAG: str = "sent_webhooks"
|
||||
SAVE_SENT_WEBHOOKS_TAG: str = "save_sent_webhooks"
|
||||
MESSAGE_PAYLOAD_KEYS: tuple[str, ...] = (
|
||||
"allowed_mentions",
|
||||
"applied_tags",
|
||||
|
|
@ -290,6 +284,41 @@ def get_screenshot_layout(reader: Reader, feed: Feed) -> ScreenshotLayout:
|
|||
return "desktop"
|
||||
|
||||
|
||||
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):
|
||||
return 1
|
||||
if isinstance(value, int):
|
||||
return min(max(value, 0), 10)
|
||||
if isinstance(value, str) and value.strip().lower() in {"1", "first", "first_image", "first-only"}:
|
||||
return 1
|
||||
if isinstance(value, str) and value.strip().lower() in {"0", "none", "no_images", "off", "disabled"}:
|
||||
return 0
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
parsed_value: int = int(value.strip())
|
||||
except ValueError:
|
||||
return 1
|
||||
return min(max(parsed_value, 0), 10)
|
||||
return 1
|
||||
|
||||
|
||||
def get_feed_media_gallery_image_limit(reader: Reader, feed: Feed | str) -> int:
|
||||
"""Resolve how many feed images should be sent in Discord media galleries.
|
||||
|
||||
Returns:
|
||||
The configured image limit, normalized to a supported Discord gallery size.
|
||||
"""
|
||||
feed_url: str = str(getattr(feed, "url", feed))
|
||||
try:
|
||||
value = cast("JsonValue", reader.get_tag(feed, "media_gallery_image_limit", 1))
|
||||
except ReaderError:
|
||||
logger.exception("Error getting %s tag for feed: %s", "media_gallery_image_limit", feed_url)
|
||||
return 1
|
||||
|
||||
return coerce_media_gallery_image_limit(value)
|
||||
|
||||
|
||||
def feed_saves_sent_webhooks(reader: Reader, feed: Feed | str) -> bool:
|
||||
"""Return whether sent Discord webhook messages should be stored for a feed.
|
||||
|
||||
|
|
@ -297,9 +326,9 @@ def feed_saves_sent_webhooks(reader: Reader, feed: Feed | str) -> bool:
|
|||
"""
|
||||
feed_url: str = feed.url if isinstance(feed, Feed) else str(feed)
|
||||
try:
|
||||
value = cast("JsonValue", reader.get_tag(feed, SAVE_SENT_WEBHOOKS_TAG, True))
|
||||
value = cast("JsonValue", reader.get_tag(feed, "save_sent_webhooks", True))
|
||||
except ReaderError:
|
||||
logger.exception("Error getting %s tag for feed: %s", SAVE_SENT_WEBHOOKS_TAG, feed_url)
|
||||
logger.exception("Error getting %s tag for feed: %s", "save_sent_webhooks", feed_url)
|
||||
return True
|
||||
|
||||
if isinstance(value, bool):
|
||||
|
|
@ -317,7 +346,7 @@ def get_sent_webhook_records(reader: Reader) -> list[SentWebhookRecord]:
|
|||
Returns:
|
||||
list[SentWebhookRecord]: Saved sent webhook records.
|
||||
"""
|
||||
raw_records = cast("JsonValue", reader.get_tag((), SENT_WEBHOOKS_TAG, []))
|
||||
raw_records = cast("JsonValue", reader.get_tag((), "sent_webhooks", []))
|
||||
if not isinstance(raw_records, list):
|
||||
return []
|
||||
|
||||
|
|
@ -329,7 +358,7 @@ def get_sent_webhook_records(reader: Reader) -> list[SentWebhookRecord]:
|
|||
|
||||
def save_sent_webhook_records(reader: Reader, records: list[SentWebhookRecord]) -> None:
|
||||
"""Save sent webhook records to the global reader tag."""
|
||||
reader.set_tag((), SENT_WEBHOOKS_TAG, records) # pyright: ignore[reportArgumentType]
|
||||
reader.set_tag((), "sent_webhooks", records) # pyright: ignore[reportArgumentType]
|
||||
|
||||
|
||||
def get_webhook_request_payload(webhook: DiscordWebhook) -> JsonObject:
|
||||
|
|
@ -441,7 +470,7 @@ def get_webhook_message_edit_payload(payload: JsonObject, record: SentWebhookRec
|
|||
if edit_payload.get("attachments") == [] and not previous_attachments:
|
||||
edit_payload.pop("attachments", None)
|
||||
|
||||
if json_value_to_int(edit_payload.get("flags")) & MESSAGE_FLAG_IS_COMPONENTS_V2:
|
||||
if json_value_to_int(edit_payload.get("flags")) & 1 << 15:
|
||||
edit_payload.pop("content", None)
|
||||
edit_payload.pop("embeds", None)
|
||||
edit_payload.pop("poll", None)
|
||||
|
|
@ -1071,7 +1100,7 @@ def create_screenshot_webhook(webhook_url: str, entry: Entry, reader: Reader) ->
|
|||
)
|
||||
screenshot_extension: str = "png"
|
||||
|
||||
if screenshot_bytes and len(screenshot_bytes) > MAX_DISCORD_UPLOAD_BYTES:
|
||||
if screenshot_bytes and len(screenshot_bytes) > 8 * 1024 * 1024:
|
||||
logger.info(
|
||||
"Screenshot for entry %s is too large as PNG (%d bytes). Trying JPEG compression.",
|
||||
entry.id,
|
||||
|
|
@ -1097,7 +1126,7 @@ def create_screenshot_webhook(webhook_url: str, entry: Entry, reader: Reader) ->
|
|||
screenshot_bytes = jpeg_bytes
|
||||
screenshot_extension = "jpg"
|
||||
|
||||
if len(screenshot_bytes) <= MAX_DISCORD_UPLOAD_BYTES:
|
||||
if len(screenshot_bytes) <= 8 * 1024 * 1024:
|
||||
break
|
||||
|
||||
if screenshot_bytes is None:
|
||||
|
|
@ -1108,7 +1137,7 @@ def create_screenshot_webhook(webhook_url: str, entry: Entry, reader: Reader) ->
|
|||
)
|
||||
return create_text_webhook(webhook_url, entry, reader=reader, use_default_message_on_empty=True)
|
||||
|
||||
if len(screenshot_bytes) > MAX_DISCORD_UPLOAD_BYTES:
|
||||
if len(screenshot_bytes) > 8 * 1024 * 1024:
|
||||
logger.warning(
|
||||
"Screenshot for entry %s is still too large after compression (%d bytes). Falling back to text message.",
|
||||
entry.id,
|
||||
|
|
@ -1331,7 +1360,7 @@ def add_unique_media_gallery_item(
|
|||
image_url: str,
|
||||
*,
|
||||
description: str,
|
||||
limit: int = MAX_MEDIA_GALLERY_ITEMS,
|
||||
limit: int = 10,
|
||||
) -> None:
|
||||
"""Append a valid media gallery item while preserving order and uniqueness."""
|
||||
clean_image_url: str = image_url.strip()
|
||||
|
|
@ -1352,7 +1381,7 @@ def normalize_ttvdrops_media_url(image_url: str) -> str:
|
|||
clean_image_url: str = image_url.strip()
|
||||
if not clean_image_url:
|
||||
return ""
|
||||
return urljoin(TTVDROPS_BASE_URL, clean_image_url)
|
||||
return urljoin("https://ttvdrops.lovinator.space/", clean_image_url)
|
||||
|
||||
|
||||
def get_ttvdrops_campaign_api_url(entry: Entry) -> str:
|
||||
|
|
@ -1368,7 +1397,7 @@ def get_ttvdrops_campaign_api_url(entry: Entry) -> str:
|
|||
continue
|
||||
|
||||
parsed_url = urlparse(str(candidate_url))
|
||||
if parsed_url.netloc.lower() != TTVDROPS_HOST:
|
||||
if parsed_url.netloc.lower() != "ttvdrops.lovinator.space":
|
||||
continue
|
||||
|
||||
if re.fullmatch(r"/twitch/api/v1/campaigns/[^/]+/?", parsed_url.path):
|
||||
|
|
@ -1465,25 +1494,34 @@ def fetch_ttvdrops_campaign_media_items(entry: Entry) -> list[JsonObject]:
|
|||
return extract_ttvdrops_media_gallery_items(response_json)
|
||||
|
||||
|
||||
def get_entry_media_gallery_items(entry: Entry, custom_embed: CustomEmbed) -> list[JsonObject]:
|
||||
def get_entry_media_gallery_items(
|
||||
entry: Entry,
|
||||
custom_embed: CustomEmbed,
|
||||
*,
|
||||
image_limit: int = 10,
|
||||
) -> list[JsonObject]:
|
||||
"""Return items for a Discord Media Gallery component.
|
||||
|
||||
Returns:
|
||||
Media Gallery items capped to Discord's item limit.
|
||||
"""
|
||||
image_limit = coerce_media_gallery_image_limit(image_limit)
|
||||
if image_limit == 0:
|
||||
return []
|
||||
|
||||
media_items: list[JsonObject] = []
|
||||
ttvdrops_media_items: list[JsonObject] = fetch_ttvdrops_campaign_media_items(entry)
|
||||
if ttvdrops_media_items:
|
||||
return ttvdrops_media_items[:MAX_MEDIA_GALLERY_ITEMS]
|
||||
return ttvdrops_media_items[:image_limit]
|
||||
|
||||
description: str = entry.title or entry.id
|
||||
for image_url in get_image_urls(entry.summary, entry.content, limit=MAX_MEDIA_GALLERY_ITEMS):
|
||||
for image_url in get_image_urls(entry.summary, entry.content, limit=image_limit):
|
||||
add_unique_media_gallery_item(media_items, image_url, description=description)
|
||||
|
||||
add_unique_media_gallery_item(media_items, custom_embed.image_url, description=description)
|
||||
add_unique_media_gallery_item(media_items, custom_embed.thumbnail_url, description=description)
|
||||
|
||||
return media_items[:MAX_MEDIA_GALLERY_ITEMS]
|
||||
return media_items[:image_limit]
|
||||
|
||||
|
||||
def truncate_component_text(content: str) -> str:
|
||||
|
|
@ -1544,7 +1582,7 @@ def create_media_gallery_component(media_items: list[JsonObject]) -> JsonObject:
|
|||
"media": {"url": media_item["url"]},
|
||||
"description": media_item["description"],
|
||||
}
|
||||
for media_item in media_items[:MAX_MEDIA_GALLERY_ITEMS]
|
||||
for media_item in media_items[:10]
|
||||
if isinstance(media_item.get("url"), str) and isinstance(media_item.get("description"), str)
|
||||
],
|
||||
}
|
||||
|
|
@ -1570,13 +1608,13 @@ def create_components_v2_webhook(
|
|||
]
|
||||
return DiscordWebhook(
|
||||
url=webhook_url,
|
||||
flags=MESSAGE_FLAG_IS_COMPONENTS_V2,
|
||||
flags=1 << 15,
|
||||
components=components,
|
||||
rate_limit_retry=True,
|
||||
)
|
||||
|
||||
|
||||
def create_embed_webhook( # noqa: C901
|
||||
def create_embed_webhook( # noqa: C901, PLR0912
|
||||
webhook_url: str,
|
||||
entry: Entry,
|
||||
reader: Reader,
|
||||
|
|
@ -1596,7 +1634,16 @@ def create_embed_webhook( # noqa: C901
|
|||
|
||||
# Get the embed data from the database.
|
||||
custom_embed: CustomEmbed = replace_tags_in_embed(feed=feed, entry=entry, reader=reader)
|
||||
media_gallery_items: list[JsonObject] = get_entry_media_gallery_items(entry, custom_embed)
|
||||
media_gallery_image_limit: int = get_feed_media_gallery_image_limit(reader, feed)
|
||||
if media_gallery_image_limit == 0:
|
||||
custom_embed.image_url = ""
|
||||
custom_embed.thumbnail_url = ""
|
||||
|
||||
media_gallery_items: list[JsonObject] = get_entry_media_gallery_items(
|
||||
entry,
|
||||
custom_embed,
|
||||
image_limit=media_gallery_image_limit,
|
||||
)
|
||||
if media_gallery_items:
|
||||
return create_components_v2_webhook(webhook_url, entry, custom_embed, media_gallery_items)
|
||||
|
||||
|
|
@ -1886,7 +1933,14 @@ def create_feed(reader: Reader, feed_url: str, webhook_dropdown: str) -> None:
|
|||
reader.set_tag(clean_feed_url, "webhook", webhook_url) # pyright: ignore[reportArgumentType]
|
||||
|
||||
# Store sent Discord message ids by default so modified feed entries can edit the original webhook message.
|
||||
reader.set_tag(clean_feed_url, SAVE_SENT_WEBHOOKS_TAG, True) # pyright: ignore[reportArgumentType]
|
||||
reader.set_tag(clean_feed_url, "save_sent_webhooks", True) # pyright: ignore[reportArgumentType]
|
||||
|
||||
# Keep the existing delivery behavior for new feeds unless changed from the feed page.
|
||||
reader.set_tag(
|
||||
clean_feed_url,
|
||||
"media_gallery_image_limit",
|
||||
cast("JSONType", 1),
|
||||
)
|
||||
|
||||
# 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]
|
||||
|
|
|
|||
|
|
@ -51,12 +51,13 @@ from discord_rss_bot.custom_message import get_embed
|
|||
from discord_rss_bot.custom_message import get_first_image
|
||||
from discord_rss_bot.custom_message import replace_tags_in_text_message
|
||||
from discord_rss_bot.custom_message import save_embed
|
||||
from discord_rss_bot.feeds import SAVE_SENT_WEBHOOKS_TAG
|
||||
from discord_rss_bot.feeds import SentWebhookRecord
|
||||
from discord_rss_bot.feeds import coerce_media_gallery_image_limit
|
||||
from discord_rss_bot.feeds import create_feed
|
||||
from discord_rss_bot.feeds import extract_domain
|
||||
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_media_gallery_image_limit
|
||||
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 send_entry_to_discord
|
||||
|
|
@ -71,6 +72,7 @@ from discord_rss_bot.filter.evaluator import evaluate_entry_filters
|
|||
from discord_rss_bot.filter.evaluator import get_entry_decision_key
|
||||
from discord_rss_bot.filter.evaluator import get_entry_fields
|
||||
from discord_rss_bot.filter.evaluator import get_filter_values_from_reader
|
||||
from discord_rss_bot.filter.evaluator import has_filter_values
|
||||
from discord_rss_bot.git_backup import commit_state_change
|
||||
from discord_rss_bot.git_backup import get_backup_path
|
||||
from discord_rss_bot.is_url_valid import is_url_valid
|
||||
|
|
@ -1346,12 +1348,44 @@ async def post_set_feed_save_sent_webhooks(
|
|||
except FeedNotFoundError as e:
|
||||
raise HTTPException(status_code=404, detail="Feed not found") from e
|
||||
|
||||
reader.set_tag(clean_feed_url, SAVE_SENT_WEBHOOKS_TAG, should_save) # pyright: ignore[reportArgumentType]
|
||||
reader.set_tag(clean_feed_url, "save_sent_webhooks", should_save) # pyright: ignore[reportArgumentType]
|
||||
action: str = "Enable" if should_save else "Disable"
|
||||
commit_state_change(reader, f"{action} sent webhook storage for {clean_feed_url}")
|
||||
return RedirectResponse(url=f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303)
|
||||
|
||||
|
||||
@app.post("/set_feed_media_gallery_image_limit")
|
||||
async def post_set_feed_media_gallery_image_limit(
|
||||
feed_url: Annotated[str, Form()],
|
||||
image_limit: Annotated[int, Form()],
|
||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||
) -> RedirectResponse:
|
||||
"""Set whether a feed sends one image or a full media gallery.
|
||||
|
||||
Returns:
|
||||
RedirectResponse: Redirect to the feed page.
|
||||
|
||||
Raises:
|
||||
HTTPException: If the feed does not exist.
|
||||
"""
|
||||
clean_feed_url: str = feed_url.strip()
|
||||
clean_image_limit: int = coerce_media_gallery_image_limit(image_limit)
|
||||
clean_image_limit_json: JSONType = cast("JSONType", clean_image_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,
|
||||
"media_gallery_image_limit",
|
||||
clean_image_limit_json,
|
||||
)
|
||||
commit_state_change(reader, f"Set media gallery image limit to {clean_image_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")
|
||||
async def post_set_update_interval(
|
||||
feed_url: Annotated[str, Form()],
|
||||
|
|
@ -1620,6 +1654,9 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
|||
current_webhook_name = hook.get("name", "").strip()
|
||||
break
|
||||
|
||||
has_blacklist_filters: bool = has_filter_values(get_filter_values_from_reader(reader, feed, "blacklist"))
|
||||
has_whitelist_filters: bool = has_filter_values(get_filter_values_from_reader(reader, feed, "whitelist"))
|
||||
|
||||
# Only show button if more than 10 entries.
|
||||
total_entries: int = reader.get_entry_counts(feed=feed).total or 0
|
||||
is_show_more_entries_button_visible: bool = total_entries > entries_per_page
|
||||
|
|
@ -1668,6 +1705,10 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
|||
"webhooks": webhooks,
|
||||
"current_webhook_url": current_webhook_url,
|
||||
"current_webhook_name": current_webhook_name,
|
||||
"has_blacklist_filters": has_blacklist_filters,
|
||||
"has_whitelist_filters": has_whitelist_filters,
|
||||
"media_gallery_image_limit": get_feed_media_gallery_image_limit(reader, feed),
|
||||
"max_media_gallery_items": 10,
|
||||
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
||||
}
|
||||
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
||||
|
|
@ -1728,6 +1769,10 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
|
|||
"webhooks": webhooks,
|
||||
"current_webhook_url": current_webhook_url,
|
||||
"current_webhook_name": current_webhook_name,
|
||||
"has_blacklist_filters": has_blacklist_filters,
|
||||
"has_whitelist_filters": has_whitelist_filters,
|
||||
"media_gallery_image_limit": get_feed_media_gallery_image_limit(reader, feed),
|
||||
"max_media_gallery_items": 10,
|
||||
"save_sent_webhooks": feed_saves_sent_webhooks(reader, feed),
|
||||
}
|
||||
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,11 @@ body {
|
|||
max-width: 120px;
|
||||
}
|
||||
|
||||
.image-limit-control {
|
||||
min-width: 12rem;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.screenshot-requirement {
|
||||
color: #9c9c9c;
|
||||
font-size: 0.9rem;
|
||||
|
|
@ -65,6 +70,10 @@ body {
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.feed-summary-list {
|
||||
padding-left: 1.15rem;
|
||||
}
|
||||
|
||||
.sent-webhooks__entry,
|
||||
.sent-webhooks__preview {
|
||||
min-width: 14rem;
|
||||
|
|
|
|||
|
|
@ -236,10 +236,12 @@
|
|||
<label for="author_icon_url" class="col-sm-6 col-form-label">Author icon URL</label>
|
||||
<input name="author_icon_url" type="text" class="form-control bg-dark border-dark text-muted"
|
||||
id="author_icon_url" {% if author_icon_url %} value="{{- author_icon_url -}}" {% endif %} />
|
||||
<label for="image_url" class="col-sm-6 col-form-label">Image URL - Add {% raw %}{{image_1}}{% endraw %}
|
||||
for first image</label>
|
||||
<label for="image_url" class="col-sm-6 col-form-label">Image URL</label>
|
||||
<input name="image_url" type="text" class="form-control bg-dark border-dark text-muted" id="image_url"
|
||||
{% if image_url %} value="{{- image_url -}}" {% endif %} />
|
||||
<div class="form-text">
|
||||
Use {% raw %}{{image_1}}{% endraw %} to use the first image URL found in the entry. You can also configure how many images gets sent via the feed page.
|
||||
</div>
|
||||
<label for="thumbnail_url" class="col-sm-6 col-form-label">Thumbnail</label>
|
||||
<input name="thumbnail_url" type="text" class="form-control bg-dark border-dark text-muted"
|
||||
id="thumbnail_url" {% if thumbnail_url %} value="{{- thumbnail_url -}}" {% endif %} />
|
||||
|
|
|
|||
|
|
@ -30,6 +30,18 @@
|
|||
Text
|
||||
{% endif %}
|
||||
</span>
|
||||
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
<span class="badge status-chip bg-secondary">
|
||||
Images:
|
||||
{% if media_gallery_image_limit == 0 %}
|
||||
No images
|
||||
{% elif media_gallery_image_limit == 1 %}
|
||||
First image only
|
||||
{% else %}
|
||||
Up to {{ media_gallery_image_limit }} images
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if delivery_mode == "screenshot" %}
|
||||
<span class="badge status-chip bg-secondary">
|
||||
Screenshot layout:
|
||||
|
|
@ -42,6 +54,72 @@
|
|||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<section class="mt-4 pt-3 border-top border-secondary-subtle">
|
||||
<h3 class="h6 text-uppercase text-muted mb-2">Feed Summary</h3>
|
||||
<p class="text-muted mb-2">
|
||||
This feed
|
||||
{% if feed.updates_enabled %}
|
||||
will send new entries
|
||||
{% else %}
|
||||
is paused and will not send new entries
|
||||
{% endif %}
|
||||
{% if current_webhook_name %}
|
||||
to <strong>{{ current_webhook_name }}</strong>
|
||||
{% elif current_webhook_url %}
|
||||
to a webhook that is no longer saved
|
||||
{% else %}
|
||||
after a webhook is attached
|
||||
{% endif %}
|
||||
as
|
||||
{% if delivery_mode == "embed" %}
|
||||
an embed.
|
||||
{% elif delivery_mode == "screenshot" %}
|
||||
a screenshot of the entry link page in {{ screenshot_layout }} mode.
|
||||
{% else %}
|
||||
a text message.
|
||||
{% endif %}
|
||||
</p>
|
||||
<ul class="text-muted small mb-0 feed-summary-list">
|
||||
<li>
|
||||
Update interval:
|
||||
{% if feed_interval %}
|
||||
{{ feed_interval }} minutes.
|
||||
{% else %}
|
||||
{{ global_interval }} minutes because of the global default.
|
||||
{% endif %}
|
||||
</li>
|
||||
{% if delivery_mode == "embed" %}
|
||||
<li>
|
||||
Embed images:
|
||||
{% if media_gallery_image_limit == 0 %}
|
||||
none.
|
||||
{% elif media_gallery_image_limit == 1 %}
|
||||
first image only.
|
||||
{% else %}
|
||||
up to {{ media_gallery_image_limit }} images.
|
||||
{% endif %}
|
||||
</li>
|
||||
{% elif delivery_mode == "screenshot" %}
|
||||
<li>Screenshot layout: {{ screenshot_layout }}.</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
Filters:
|
||||
{% if has_blacklist_filters and has_whitelist_filters %}
|
||||
whitelist and blacklist are active; blacklist wins when both match.
|
||||
{% elif has_blacklist_filters %}
|
||||
blacklist is active.
|
||||
{% elif has_whitelist_filters %}
|
||||
whitelist is active.
|
||||
{% else %}
|
||||
no whitelist or blacklist filters are configured.
|
||||
{% endif %}
|
||||
</li>
|
||||
<li>
|
||||
Updating the Discord webhook when the feed entry changes is
|
||||
{{ 'enabled.' if save_sent_webhooks else 'disabled.' }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
{% if feed.last_exception %}
|
||||
<div class="alert alert-danger mt-4 mb-0" role="alert">
|
||||
<h5 class="alert-heading mb-2">{{ feed.last_exception.type_name }}</h5>
|
||||
|
|
@ -88,24 +166,56 @@
|
|||
name="feed_url"
|
||||
value="{{ feed.url }}">Send text message instead of embed</button>
|
||||
</form>
|
||||
<form action="/use_screenshot" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">
|
||||
Send full-page screenshot instead of embed
|
||||
</button>
|
||||
</form>
|
||||
{% elif delivery_mode == "screenshot" %}
|
||||
<form action="/use_embed" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Send embed instead of screenshot</button>
|
||||
</form>
|
||||
<form action="/use_text" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Send text message instead of screenshot</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/use_embed" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Send embed instead of text message</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
|
||||
<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">Screenshot Delivery</h3>
|
||||
<span class="badge {{ 'bg-info' if delivery_mode == 'screenshot' else 'bg-secondary' }}">
|
||||
{% if delivery_mode == "screenshot" %}
|
||||
Active:
|
||||
{% if screenshot_layout == "mobile" %}
|
||||
Mobile
|
||||
{% else %}
|
||||
Desktop
|
||||
{% endif %}
|
||||
{% else %}
|
||||
Inactive
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-muted mb-3">
|
||||
Screenshot delivery sends a full-page screenshot of the entry link instead of the normal
|
||||
embed or text message.
|
||||
</p>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% if delivery_mode != "screenshot" %}
|
||||
<form action="/use_screenshot" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Use screenshot delivery</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/use_embed" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Disable screenshot delivery</button>
|
||||
</form>
|
||||
{% if screenshot_layout == "mobile" %}
|
||||
<form action="/use_screenshot_desktop" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
|
|
@ -119,26 +229,61 @@
|
|||
value="{{ feed.url }}">Use mobile screenshot layout</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<form action="/use_embed" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">Send embed instead of text message</button>
|
||||
</form>
|
||||
<form action="/use_screenshot" method="post" class="d-inline">
|
||||
<button class="btn btn-outline-light btn-sm"
|
||||
name="feed_url"
|
||||
value="{{ feed.url }}">
|
||||
Send full-page screenshot instead of text message
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<div class="w-100 mt-1 screenshot-requirement">
|
||||
Screenshot mode requires Chromium to be installed for Playwright. Run <code>uv run playwright install chromium</code> once on this machine.
|
||||
</div>
|
||||
<div class="mt-2 screenshot-requirement">
|
||||
Screenshot mode requires Chromium to be installed for Playwright. Run
|
||||
<code>uv run playwright install chromium</code> once on this machine.
|
||||
</div>
|
||||
</section>
|
||||
<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">Image Delivery</h3>
|
||||
<span class="badge {{ 'bg-info' if media_gallery_image_limit < max_media_gallery_items else 'bg-secondary' }}">
|
||||
{% if media_gallery_image_limit == 0 %}
|
||||
No images
|
||||
{% elif media_gallery_image_limit == 1 %}
|
||||
First image only
|
||||
{% else %}
|
||||
Up to {{ media_gallery_image_limit }} images
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<p id="imageDeliveryHelp" class="text-muted mb-3">
|
||||
Choose 0 to send no entry images, 1 for the first image only,
|
||||
or up to {{ max_media_gallery_items }} for a Discord media gallery.
|
||||
This only affects embed delivery.
|
||||
</p>
|
||||
<form action="/set_feed_media_gallery_image_limit"
|
||||
method="post"
|
||||
class="mb-0">
|
||||
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
|
||||
<label class="form-label small text-muted mb-2" for="image_limit">
|
||||
Images per entry:
|
||||
<output name="image_limit_value" for="image_limit">{{ media_gallery_image_limit }}</output>
|
||||
</label>
|
||||
<div class="d-flex flex-wrap align-items-center gap-3">
|
||||
<div class="flex-grow-1 image-limit-control">
|
||||
<input id="image_limit"
|
||||
class="form-range"
|
||||
type="range"
|
||||
name="image_limit"
|
||||
min="0"
|
||||
max="{{ max_media_gallery_items }}"
|
||||
step="1"
|
||||
value="{{ media_gallery_image_limit }}"
|
||||
aria-describedby="imageDeliveryHelp"
|
||||
oninput="this.form.elements.image_limit_value.value = this.value" />
|
||||
<div class="d-flex justify-content-between text-muted small">
|
||||
<span>0</span>
|
||||
<span>{{ max_media_gallery_items }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-outline-light btn-sm" type="submit">Save image limit</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
</section>
|
||||
{% endif %}
|
||||
<section class="mt-4 pt-3 border-top border-secondary-subtle">
|
||||
<h3 class="h6 text-uppercase text-muted mb-3">Customization</h3>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
|
|
|
|||
|
|
@ -338,6 +338,45 @@ def test_get_screenshot_layout_defaults_to_desktop() -> None:
|
|||
assert result == "desktop"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tag_value", "expected_limit"),
|
||||
[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(7, 7),
|
||||
(-1, 0),
|
||||
(99, 10),
|
||||
("first", 1),
|
||||
("off", 0),
|
||||
("8", 8),
|
||||
("unknown", 1),
|
||||
],
|
||||
)
|
||||
def test_get_feed_media_gallery_image_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_media_gallery_image_limit(reader, feed)
|
||||
|
||||
assert result == expected_limit
|
||||
|
||||
|
||||
def test_get_feed_media_gallery_image_limit_defaults_to_first_image() -> 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_media_gallery_image_limit(reader, feed)
|
||||
|
||||
assert result == 1
|
||||
|
||||
|
||||
def test_create_feed_inherits_global_screenshot_layout() -> None:
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda resource, key, default=None: { # noqa: ARG005
|
||||
|
|
@ -374,7 +413,24 @@ def test_create_feed_enables_sent_webhook_tracking_by_default() -> None:
|
|||
|
||||
create_feed(reader, "https://example.com/feed.xml", "Main")
|
||||
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", feeds.SAVE_SENT_WEBHOOKS_TAG, True)
|
||||
reader.set_tag.assert_any_call("https://example.com/feed.xml", "save_sent_webhooks", True)
|
||||
|
||||
|
||||
def test_create_feed_sets_default_media_gallery_image_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",
|
||||
"media_gallery_image_limit",
|
||||
1,
|
||||
)
|
||||
|
||||
|
||||
def test_create_feed_falls_back_to_embed_when_global_delivery_mode_is_invalid() -> None:
|
||||
|
|
@ -581,6 +637,7 @@ def test_create_embed_webhook_uses_media_gallery_for_entry_images(
|
|||
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||
) -> None:
|
||||
reader = MagicMock()
|
||||
reader.get_tag.return_value = 10
|
||||
entry = MagicMock()
|
||||
entry.id = "entry-1"
|
||||
entry.title = "Entry title"
|
||||
|
|
@ -599,7 +656,7 @@ def test_create_embed_webhook_uses_media_gallery_for_entry_images(
|
|||
|
||||
webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
assert webhook.flags == feeds.MESSAGE_FLAG_IS_COMPONENTS_V2
|
||||
assert webhook.flags == 1 << 15
|
||||
components = get_test_webhook_components(webhook)
|
||||
assert components[0] == {
|
||||
"type": 10,
|
||||
|
|
@ -616,6 +673,69 @@ 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_can_limit_media_gallery_to_first_image(
|
||||
mock_replace_tags_in_embed: MagicMock,
|
||||
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||
) -> None:
|
||||
reader = MagicMock()
|
||||
reader.get_tag.return_value = 1
|
||||
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" />'),
|
||||
MagicMock(value='<img src="https://example.com/content-2.jpg" />'),
|
||||
]
|
||||
entry.feed.url = "https://example.com/feed.xml"
|
||||
mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(description="Entry body")
|
||||
|
||||
webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
gallery = get_test_webhook_components(webhook)[1]
|
||||
assert isinstance(gallery, dict)
|
||||
mock_fetch_ttvdrops_campaign_media_items.assert_called_once_with(entry)
|
||||
assert gallery["items"] == [
|
||||
{"media": {"url": "https://example.com/content-1.jpg"}, "description": "Entry title"},
|
||||
]
|
||||
|
||||
|
||||
@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_can_disable_media_images(
|
||||
mock_replace_tags_in_embed: MagicMock,
|
||||
mock_fetch_ttvdrops_campaign_media_items: MagicMock,
|
||||
) -> None:
|
||||
reader = MagicMock()
|
||||
reader.get_tag.return_value = 0
|
||||
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="Entry body",
|
||||
image_url="https://example.com/custom-image.jpg",
|
||||
thumbnail_url="https://example.com/custom-thumbnail.jpg",
|
||||
)
|
||||
|
||||
webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
|
||||
|
||||
assert "components" not in webhook.json
|
||||
assert "embeds" in webhook.json
|
||||
embeds = webhook.json["embeds"]
|
||||
assert isinstance(embeds, list)
|
||||
assert isinstance(embeds[0], dict)
|
||||
assert "image" not in embeds[0]
|
||||
assert "thumbnail" not in embeds[0]
|
||||
mock_fetch_ttvdrops_campaign_media_items.assert_not_called()
|
||||
|
||||
|
||||
@patch("discord_rss_bot.feeds.fetch_ttvdrops_campaign_media_items")
|
||||
@patch("discord_rss_bot.feeds.replace_tags_in_embed")
|
||||
def test_create_embed_webhook_prefers_ttvdrops_reward_images_and_alt_text(
|
||||
|
|
@ -1046,9 +1166,9 @@ def test_execute_webhook_records_sent_webhook_message(mock_send_webhook_message:
|
|||
state: dict[str, feeds.JsonValue] = {}
|
||||
|
||||
def get_tag(_resource: str | tuple[()], key: str, default: feeds.JsonValue = None) -> feeds.JsonValue:
|
||||
if key == feeds.SENT_WEBHOOKS_TAG:
|
||||
return state.get(feeds.SENT_WEBHOOKS_TAG, default)
|
||||
if key == feeds.SAVE_SENT_WEBHOOKS_TAG:
|
||||
if key == "sent_webhooks":
|
||||
return state.get("sent_webhooks", default)
|
||||
if key == "save_sent_webhooks":
|
||||
return True
|
||||
if key == "webhook":
|
||||
return webhook_url
|
||||
|
|
@ -1083,7 +1203,7 @@ def test_execute_webhook_records_sent_webhook_message(mock_send_webhook_message:
|
|||
|
||||
execute_webhook(webhook, entry, reader)
|
||||
|
||||
records = state[feeds.SENT_WEBHOOKS_TAG]
|
||||
records = state["sent_webhooks"]
|
||||
assert isinstance(records, list)
|
||||
assert len(records) == 1
|
||||
assert isinstance(records[0], dict)
|
||||
|
|
@ -1104,7 +1224,7 @@ def test_execute_webhook_does_not_record_when_feed_tracking_disabled(mock_send_w
|
|||
webhook_url = "https://discord.com/api/webhooks/123/abc"
|
||||
reader = MagicMock()
|
||||
reader.get_tag.side_effect = lambda _resource, key, default=None: {
|
||||
feeds.SAVE_SENT_WEBHOOKS_TAG: False,
|
||||
"save_sent_webhooks": False,
|
||||
"webhook": webhook_url,
|
||||
}.get(key, default)
|
||||
|
||||
|
|
@ -1139,7 +1259,7 @@ def test_send_webhook_message_posts_components_with_httpx(mock_request: MagicMoc
|
|||
]
|
||||
webhook = feeds.DiscordWebhook(
|
||||
url="https://discord.com/api/webhooks/123/abc?thread_id=456",
|
||||
flags=feeds.MESSAGE_FLAG_IS_COMPONENTS_V2,
|
||||
flags=1 << 15,
|
||||
components=components,
|
||||
)
|
||||
|
||||
|
|
@ -1150,7 +1270,7 @@ def test_send_webhook_message_posts_components_with_httpx(mock_request: MagicMoc
|
|||
assert mock_request.call_args.args == ("POST", "https://discord.com/api/webhooks/123/abc")
|
||||
assert mock_request.call_args.kwargs["json"] == {
|
||||
"components": components,
|
||||
"flags": feeds.MESSAGE_FLAG_IS_COMPONENTS_V2,
|
||||
"flags": 1 << 15,
|
||||
}
|
||||
assert mock_request.call_args.kwargs["params"] == {
|
||||
"thread_id": "456",
|
||||
|
|
@ -1185,7 +1305,7 @@ def test_update_sent_webhooks_for_modified_entries_edits_changed_payload(
|
|||
webhook_url = "https://discord.com/api/webhooks/123/abc"
|
||||
old_payload: JsonObject = {"content": "Old title", "embeds": [], "attachments": []}
|
||||
state: dict[str, feeds.JsonValue] = {
|
||||
feeds.SENT_WEBHOOKS_TAG: [
|
||||
"sent_webhooks": [
|
||||
{
|
||||
"feed_url": "https://example.com/feed.xml",
|
||||
"entry_id": "entry-3",
|
||||
|
|
@ -1199,9 +1319,9 @@ def test_update_sent_webhooks_for_modified_entries_edits_changed_payload(
|
|||
}
|
||||
|
||||
def get_tag(_resource: str | tuple[()], key: str, default: feeds.JsonValue = None) -> feeds.JsonValue:
|
||||
if key == feeds.SENT_WEBHOOKS_TAG:
|
||||
return state[feeds.SENT_WEBHOOKS_TAG]
|
||||
if key == feeds.SAVE_SENT_WEBHOOKS_TAG:
|
||||
if key == "sent_webhooks":
|
||||
return state["sent_webhooks"]
|
||||
if key == "save_sent_webhooks":
|
||||
return True
|
||||
return default
|
||||
|
||||
|
|
@ -1240,7 +1360,7 @@ def test_update_sent_webhooks_for_modified_entries_edits_changed_payload(
|
|||
mock_edit_sent_webhook_message.assert_called_once()
|
||||
edit_payload = mock_edit_sent_webhook_message.call_args.args[3]
|
||||
assert edit_payload == {"content": "New title"}
|
||||
records = state[feeds.SENT_WEBHOOKS_TAG]
|
||||
records = state["sent_webhooks"]
|
||||
assert isinstance(records, list)
|
||||
assert isinstance(records[0], dict)
|
||||
assert isinstance(records[0]["payload"], dict)
|
||||
|
|
|
|||
|
|
@ -229,6 +229,12 @@ def test_get() -> None:
|
|||
|
||||
response: Response = client.get(url="/feed", params={"feed_url": encoded_feed_url(feed_url)})
|
||||
assert response.status_code == 200, f"/feed failed: {response.text}"
|
||||
assert "Feed Summary" in response.text
|
||||
assert "This feed" in response.text
|
||||
assert "Screenshot Delivery" in response.text
|
||||
assert "Image Delivery" in response.text
|
||||
assert 'type="range"' in response.text
|
||||
assert 'max="10"' in response.text
|
||||
|
||||
response: Response = client.get(url="/")
|
||||
assert response.status_code == 200, f"/ failed: {response.text}"
|
||||
|
|
@ -524,6 +530,8 @@ def test_c3kay_feed_delivery_mode_toggle_routes_update_stored_tags() -> None:
|
|||
assert reader.get_tag(c3kay_feed_url, "delivery_mode") == "screenshot"
|
||||
assert reader.get_tag(c3kay_feed_url, "screenshot_layout") == "mobile"
|
||||
assert reader.get_tag(c3kay_feed_url, "should_send_embed") is False
|
||||
assert "Disable screenshot delivery" in response.text
|
||||
assert "Send embed instead of screenshot" not in response.text
|
||||
|
||||
response = client.post(url="/use_embed", data={"feed_url": c3kay_feed_url})
|
||||
assert response.status_code == 200, f"Failed to set embed mode: {response.text}"
|
||||
|
|
@ -561,7 +569,42 @@ def test_set_feed_save_sent_webhooks_route_updates_stored_tag() -> None:
|
|||
)
|
||||
|
||||
assert response.status_code == 303, f"/set_feed_save_sent_webhooks failed: {response.text}"
|
||||
assert stub_reader.tags[stub_reader.feed.url, feeds.SAVE_SENT_WEBHOOKS_TAG] is False
|
||||
assert stub_reader.tags[stub_reader.feed.url, "save_sent_webhooks"] is False
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
||||
def test_set_feed_media_gallery_image_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_media_gallery_image_limit",
|
||||
data={"feed_url": stub_reader.feed.url, "image_limit": "7"},
|
||||
follow_redirects=False,
|
||||
)
|
||||
|
||||
assert response.status_code == 303, f"/set_feed_media_gallery_image_limit failed: {response.text}"
|
||||
assert stub_reader.tags[stub_reader.feed.url, "media_gallery_image_limit"] == 7
|
||||
finally:
|
||||
app.dependency_overrides = {}
|
||||
|
||||
|
|
@ -585,7 +628,7 @@ def test_sent_webhooks_view_shows_saved_records() -> None:
|
|||
key: str,
|
||||
default: feeds.JsonValue = None,
|
||||
) -> feeds.JsonValue:
|
||||
if resource == () and key == feeds.SENT_WEBHOOKS_TAG:
|
||||
if resource == () and key == "sent_webhooks":
|
||||
return [
|
||||
{
|
||||
"feed_url": sent_feed_url,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue