diff --git a/discord_rss_bot/custom_message.py b/discord_rss_bot/custom_message.py
index e5f1b14..4a5c819 100644
--- a/discord_rss_bot/custom_message.py
+++ b/discord_rss_bot/custom_message.py
@@ -38,6 +38,7 @@ class CustomEmbed:
thumbnail_url: str = ""
footer_text: str = ""
footer_icon_url: str = ""
+ show_steam_game_icon_in_thumbnail: bool = False
def try_to_replace(custom_message: str, template: str, replace_with: str) -> str:
@@ -398,7 +399,7 @@ def save_embed(reader: Reader, feed: Feed, embed: CustomEmbed) -> None:
feed: The feed to set the tag in.
embed: The embed to set.
"""
- embed_dict: dict[str, str | int] = {
+ embed_dict: dict[str, str | int | bool] = {
"title": embed.title,
"description": embed.description,
"color": embed.color,
@@ -409,6 +410,7 @@ def save_embed(reader: Reader, feed: Feed, embed: CustomEmbed) -> None:
"thumbnail_url": embed.thumbnail_url,
"footer_text": embed.footer_text,
"footer_icon_url": embed.footer_icon_url,
+ "show_steam_game_icon_in_thumbnail": embed.show_steam_game_icon_in_thumbnail,
}
reader.set_tag(feed, "embed", json.dumps(embed_dict)) # pyright: ignore[reportArgumentType]
@@ -428,7 +430,7 @@ def get_embed(reader: Reader, feed: Feed) -> CustomEmbed:
if embed:
if not isinstance(embed, str):
return get_embed_data(embed) # pyright: ignore[reportArgumentType]
- embed_data: dict[str, str | int] = json.loads(embed)
+ embed_data: dict[str, str | int | bool] = json.loads(embed)
return get_embed_data(embed_data)
return CustomEmbed(
@@ -442,10 +444,22 @@ def get_embed(reader: Reader, feed: Feed) -> CustomEmbed:
thumbnail_url="",
footer_text="",
footer_icon_url="",
+ show_steam_game_icon_in_thumbnail=False,
)
-def get_embed_data(embed_data: dict[str, str | int]) -> CustomEmbed:
+def coerce_embed_bool(value: object) -> bool:
+ """Normalize stored embed booleans from JSON or form-like values."""
+ if isinstance(value, bool):
+ return value
+ if isinstance(value, int):
+ return value != 0
+ if isinstance(value, str):
+ return value.strip().lower() in {"1", "true", "on", "yes"}
+ return False
+
+
+def get_embed_data(embed_data: dict[str, str | int | bool]) -> CustomEmbed:
"""Get embed data from embed_data.
Args:
@@ -464,6 +478,9 @@ def get_embed_data(embed_data: dict[str, str | int]) -> CustomEmbed:
thumbnail_url: str = str(embed_data.get("thumbnail_url", ""))
footer_text: str = str(embed_data.get("footer_text", ""))
footer_icon_url: str = str(embed_data.get("footer_icon_url", ""))
+ show_steam_game_icon_in_thumbnail: bool = coerce_embed_bool(
+ embed_data.get("show_steam_game_icon_in_thumbnail", False),
+ )
return CustomEmbed(
title=title,
@@ -476,4 +493,5 @@ def get_embed_data(embed_data: dict[str, str | int]) -> CustomEmbed:
thumbnail_url=thumbnail_url,
footer_text=footer_text,
footer_icon_url=footer_icon_url,
+ show_steam_game_icon_in_thumbnail=show_steam_game_icon_in_thumbnail,
)
diff --git a/discord_rss_bot/feeds.py b/discord_rss_bot/feeds.py
index ffdbfb3..a75629e 100644
--- a/discord_rss_bot/feeds.py
+++ b/discord_rss_bot/feeds.py
@@ -12,6 +12,7 @@ import re
import time
from collections.abc import Callable
from contextlib import suppress
+from pathlib import Path
from typing import TYPE_CHECKING
from typing import Any
from typing import Literal
@@ -186,6 +187,123 @@ def extract_domain(url: str) -> str: # noqa: PLR0911
return "Other"
+STEAM_STORE_APP_ID_PATH_PREFIXES: tuple[tuple[str, ...], ...] = (
+ ("feeds", "news", "app"),
+ ("news", "app"),
+ ("app",),
+)
+
+
+def is_steam_feed_url(url: str) -> bool:
+ """Return whether a feed URL belongs to Steam."""
+ if not url:
+ return False
+
+ try:
+ parsed_url: ParseResult = urlparse(url)
+ except ValueError:
+ return False
+
+ normalized_netloc: str = parsed_url.netloc.lower().removeprefix("www.")
+ return normalized_netloc in frozenset({"store.steampowered.com", "steamcommunity.com"})
+
+
+def _extract_steam_app_id_from_path(normalized_netloc: str, path_segments: list[str]) -> str | None:
+ if normalized_netloc == "store.steampowered.com":
+ for prefix in STEAM_STORE_APP_ID_PATH_PREFIXES:
+ prefix_length: int = len(prefix)
+ if len(path_segments) > prefix_length and tuple(path_segments[:prefix_length]) == prefix:
+ app_id: str = path_segments[prefix_length]
+ if app_id.isdigit():
+ return app_id
+
+ if normalized_netloc == "steamcommunity.com" and len(path_segments) > 1:
+ app_type: str = path_segments[0]
+ app_id = path_segments[1]
+ if app_type in frozenset({"games", "app"}) and app_id.isdigit():
+ return app_id
+
+ return None
+
+
+def _extract_steam_app_id_from_query(parsed_url: ParseResult) -> str | None:
+ query_params: dict[str, list[str]] = parse_qs(parsed_url.query)
+ for key in ("appid", "app_id", "appids"):
+ for value in query_params.get(key, []):
+ cleaned_value: str = value.strip()
+ if cleaned_value.isdigit():
+ return cleaned_value
+ return None
+
+
+def extract_steam_app_id_from_url(url: str) -> str | None:
+ """Extract a Steam application ID from a Steam URL when present.
+
+ Args:
+ url: The Steam URL to inspect.
+
+ Returns:
+ str | None: The application ID when present in the path or query string, otherwise None.
+ """
+ if not url:
+ return None
+
+ try:
+ parsed_url: ParseResult = urlparse(url)
+ except ValueError:
+ return None
+
+ normalized_netloc: str = parsed_url.netloc.lower().removeprefix("www.")
+ path_segments: list[str] = [segment for segment in parsed_url.path.split("/") if segment]
+ return _extract_steam_app_id_from_path(normalized_netloc, path_segments) or _extract_steam_app_id_from_query(
+ parsed_url
+ )
+
+
+def get_local_steam_game_icon_file(app_id: str) -> WebhookFile | None:
+ """Return a local Steam game icon file for Discord upload when available."""
+ icon_path: Path = Path(__file__).resolve().parent.parent / "icons" / f"{app_id}.png"
+ if not icon_path.is_file():
+ return None
+
+ try:
+ icon_bytes: bytes = icon_path.read_bytes()
+ except OSError:
+ logger.exception("Failed to read local Steam icon for app %s from %s", app_id, icon_path)
+ return None
+
+ if not icon_bytes:
+ logger.warning("Local Steam icon file is empty for app %s: %s", app_id, icon_path)
+ return None
+
+ content_hash: str = hashlib.sha256(icon_bytes).hexdigest()[:12]
+ return WebhookFile(
+ filename=f"steam-app-{app_id}-{content_hash}.png",
+ content=icon_bytes,
+ )
+
+
+def get_steam_game_thumbnail(entry: Entry) -> tuple[str | None, WebhookFile | None]:
+ """Return the preferred Steam thumbnail source for an entry.
+
+ Returns:
+ tuple[str | None, WebhookFile | None]: Thumbnail URL and optional uploaded file.
+ """
+ feed_url: str = str(getattr(entry.feed, "url", "") or "")
+ if not is_steam_feed_url(feed_url):
+ return None, None
+
+ app_id: str | None = extract_steam_app_id_from_url(feed_url) or extract_steam_app_id_from_url(str(entry.link or ""))
+ if not app_id:
+ return None, None
+
+ local_icon_file: WebhookFile | None = get_local_steam_game_icon_file(app_id)
+ if local_icon_file:
+ return f"attachment://{local_icon_file.filename}", local_icon_file
+
+ return f"https://cdn.cloudflare.steamstatic.com/steam/apps/{app_id}/capsule_sm_120.jpg", None
+
+
def send_entry_to_discord(entry: Entry, reader: Reader) -> str | None:
"""Send a single entry to Discord.
@@ -1719,6 +1837,10 @@ def create_embed_webhook( # noqa: C901, PLR0912
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)
+ steam_game_thumbnail_url: str | None = None
+ steam_game_thumbnail_file: WebhookFile | None = None
+ if custom_embed.show_steam_game_icon_in_thumbnail:
+ steam_game_thumbnail_url, steam_game_thumbnail_file = get_steam_game_thumbnail(entry)
if media_gallery_image_limit == 0:
custom_embed.image_url = ""
custom_embed.thumbnail_url = ""
@@ -1771,8 +1893,9 @@ def create_embed_webhook( # noqa: C901, PLR0912
icon_url=custom_embed.author_icon_url,
)
- if custom_embed.thumbnail_url:
- discord_embed.set_thumbnail(url=custom_embed.thumbnail_url)
+ embed_thumbnail_url: str = steam_game_thumbnail_url or custom_embed.thumbnail_url
+ if embed_thumbnail_url:
+ discord_embed.set_thumbnail(url=embed_thumbnail_url)
if custom_embed.image_url:
discord_embed.set_image(url=custom_embed.image_url)
@@ -1786,6 +1909,9 @@ def create_embed_webhook( # noqa: C901, PLR0912
if custom_embed.footer_icon_url and not custom_embed.footer_text:
discord_embed.set_footer(text="-", icon_url=custom_embed.footer_icon_url)
+ if steam_game_thumbnail_file:
+ webhook.add_file(file=steam_game_thumbnail_file.content, filename=steam_game_thumbnail_file.filename)
+
webhook.add_embed(discord_embed)
return webhook
diff --git a/discord_rss_bot/main.py b/discord_rss_bot/main.py
index 746cd3e..acc07d9 100644
--- a/discord_rss_bot/main.py
+++ b/discord_rss_bot/main.py
@@ -63,6 +63,7 @@ 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_sent_webhook_records
+from discord_rss_bot.feeds import is_steam_feed_url
from discord_rss_bot.feeds import send_entry_to_discord
from discord_rss_bot.feeds import send_to_discord
from discord_rss_bot.feeds import update_feed_and_collect_modified_entries
@@ -1221,7 +1222,7 @@ async def get_embed_page(
# Get previous data, this is used when creating the form.
embed: CustomEmbed = get_embed(reader, feed)
- context: dict[str, Request | Feed | str | Entry | CustomEmbed] = {
+ context: dict[str, Request | Feed | str | Entry | CustomEmbed | bool] = {
"request": request,
"feed": feed,
"title": embed.title,
@@ -1234,6 +1235,7 @@ async def get_embed_page(
"author_icon_url": embed.author_icon_url,
"footer_text": embed.footer_text,
"footer_icon_url": embed.footer_icon_url,
+ "show_steam_game_icon_in_thumbnail": embed.show_steam_game_icon_in_thumbnail,
}
if custom_embed := get_embed(reader, feed):
context["custom_embed"] = custom_embed
@@ -1258,6 +1260,7 @@ async def post_embed( # noqa: C901
author_icon_url: Annotated[str, Form()] = "",
footer_text: Annotated[str, Form()] = "",
footer_icon_url: Annotated[str, Form()] = "",
+ show_steam_game_icon_in_thumbnail: Annotated[bool, Form()] = False,
) -> RedirectResponse:
"""Set the embed settings.
@@ -1273,6 +1276,7 @@ async def post_embed( # noqa: C901
author_icon_url: The author icon url of the embed.
footer_text: The footer text of the embed.
footer_icon_url: The footer icon url of the embed.
+ show_steam_game_icon_in_thumbnail: Whether to use the Steam game icon as the embed thumbnail.
reader: The Reader instance.
Returns:
@@ -1303,6 +1307,8 @@ async def post_embed( # noqa: C901
custom_embed.footer_text = footer_text
if footer_icon_url != custom_embed.footer_icon_url:
custom_embed.footer_icon_url = footer_icon_url
+ if show_steam_game_icon_in_thumbnail != custom_embed.show_steam_game_icon_in_thumbnail:
+ custom_embed.show_steam_game_icon_in_thumbnail = show_steam_game_icon_in_thumbnail
# Save the data.
save_embed(reader, feed, custom_embed)
@@ -1855,6 +1861,7 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
"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),
+ "is_steam_feed": is_steam_feed_url(feed.url),
}
return templates.TemplateResponse(request=request, name="feed.html", context=context)
@@ -1921,6 +1928,7 @@ async def get_feed( # noqa: C901, PLR0912, PLR0914, PLR0915
"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),
+ "is_steam_feed": is_steam_feed_url(feed.url),
}
return templates.TemplateResponse(request=request, name="feed.html", context=context)
diff --git a/discord_rss_bot/templates/embed.html b/discord_rss_bot/templates/embed.html
index 4bcc3a7..c2f4595 100644
--- a/discord_rss_bot/templates/embed.html
+++ b/discord_rss_bot/templates/embed.html
@@ -245,6 +245,17 @@
+
+
+
+
diff --git a/discord_rss_bot/templates/feed.html b/discord_rss_bot/templates/feed.html
index 76c8434..dcefd1a 100644
--- a/discord_rss_bot/templates/feed.html
+++ b/discord_rss_bot/templates/feed.html
@@ -338,6 +338,9 @@
{% endif %}
+ {% if is_steam_feed %}
+ Steam feeds can enable thumbnails in embed settings.
+ {% endif %}
Feed URL
diff --git a/icons/570.png b/icons/570.png
new file mode 100644
index 0000000..e97fc97
Binary files /dev/null and b/icons/570.png differ
diff --git a/tests/test_custom_message.py b/tests/test_custom_message.py
index 38cf649..ecd744a 100644
--- a/tests/test_custom_message.py
+++ b/tests/test_custom_message.py
@@ -355,6 +355,7 @@ def test_save_embed_serializes_embed_and_writes_feed_tag() -> None:
thumbnail_url="https://example.com/thumb.png",
footer_text="Footer",
footer_icon_url="https://example.com/footer.png",
+ show_steam_game_icon_in_thumbnail=True,
)
save_embed(reader=reader, feed=feed, embed=embed)
@@ -365,6 +366,7 @@ def test_save_embed_serializes_embed_and_writes_feed_tag() -> None:
parsed = typing.cast("dict[str, str]", __import__("json").loads(call_args[2]))
assert parsed["title"] == "Title"
assert parsed["footer_icon_url"] == "https://example.com/footer.png"
+ assert parsed["show_steam_game_icon_in_thumbnail"] is True
def test_get_embed_returns_default_embed_when_tag_is_empty() -> None:
@@ -420,8 +422,10 @@ def test_get_embed_data_coerces_values_to_strings() -> None:
"thumbnail_url": 8,
"footer_text": 9,
"footer_icon_url": 10,
+ "show_steam_game_icon_in_thumbnail": "true",
},
)
assert embed.title == "1"
assert embed.footer_icon_url == "10"
+ assert embed.show_steam_game_icon_in_thumbnail is True
diff --git a/tests/test_feeds.py b/tests/test_feeds.py
index 793fcbf..f5e87b9 100644
--- a/tests/test_feeds.py
+++ b/tests/test_feeds.py
@@ -379,6 +379,21 @@ def test_get_feed_media_gallery_image_limit_defaults_to_first_image() -> None:
assert result == 1
+@pytest.mark.parametrize(
+ ("url", "expected_app_id"),
+ [
+ ("https://store.steampowered.com/feeds/news/app/570/?cc=us&l=english", "570"),
+ ("https://store.steampowered.com/news/app/440/view/1234567890", "440"),
+ ("https://store.steampowered.com/app/730/CounterStrike_2/", "730"),
+ ("https://steamcommunity.com/games/570/rss/", "570"),
+ ("https://steamcommunity.com/app/730/announcements/detail/1234567890", "730"),
+ ("https://example.com/feed.xml", None),
+ ],
+)
+def test_extract_steam_app_id_from_url(url: str, expected_app_id: str | None) -> None:
+ assert feeds.extract_steam_app_id_from_url(url) == expected_app_id
+
+
@pytest.mark.parametrize(
("tag_value", "expected_limit"),
[
@@ -909,6 +924,122 @@ def test_create_embed_webhook_can_disable_media_images(
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_can_use_steam_game_icon_thumbnail(
+ 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": 4000,
+ }.get(key, default)
+ entry = MagicMock()
+ entry.id = "entry-steam-1"
+ entry.title = "Dota 2 patch notes"
+ entry.link = "https://steamcommunity.com/games/570/announcements/detail/1234567890"
+ entry.summary = ""
+ entry.content = []
+ entry.feed.url = "https://store.steampowered.com/feeds/news/app/570/?cc=us&l=english"
+ mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(
+ description="Steam news",
+ thumbnail_url="https://example.com/custom-thumb.jpg",
+ show_steam_game_icon_in_thumbnail=True,
+ )
+
+ with patch("discord_rss_bot.feeds.Path.is_file", return_value=False):
+ 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 embeds[0]["thumbnail"] == {
+ "url": "https://cdn.cloudflare.steamstatic.com/steam/apps/570/capsule_sm_120.jpg",
+ }
+ assert webhook.files == []
+ 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_prefers_local_steam_game_icon_thumbnail(
+ mock_replace_tags_in_embed: MagicMock,
+ mock_fetch_ttvdrops_campaign_media_items: MagicMock,
+) -> None:
+ local_icon_bytes = b"local-steam-icon"
+
+ reader = MagicMock()
+ 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.id = "entry-steam-local-1"
+ entry.title = "Dota 2 patch notes"
+ entry.link = "https://steamcommunity.com/games/570/announcements/detail/1234567890"
+ entry.summary = ""
+ entry.content = []
+ entry.feed.url = "https://store.steampowered.com/feeds/news/app/570/?cc=us&l=english"
+ mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(
+ description="Steam news",
+ thumbnail_url="https://example.com/custom-thumb.jpg",
+ show_steam_game_icon_in_thumbnail=True,
+ )
+
+ with (
+ patch("discord_rss_bot.feeds.Path.is_file", return_value=True),
+ patch("discord_rss_bot.feeds.Path.read_bytes", return_value=local_icon_bytes),
+ ):
+ webhook = feeds.create_embed_webhook("https://discord.com/api/webhooks/123/abc", entry, reader)
+
+ embeds = webhook.json.get("embeds")
+ assert isinstance(embeds, list)
+ assert isinstance(embeds[0], dict)
+ assert len(webhook.files) == 1
+ uploaded_icon = webhook.files[0]
+ assert uploaded_icon.content == local_icon_bytes
+ assert uploaded_icon.filename.startswith("steam-app-570-")
+ assert uploaded_icon.filename.endswith(".png")
+ assert embeds[0]["thumbnail"] == {"url": f"attachment://{uploaded_icon.filename}"}
+ 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_does_not_inject_steam_thumbnail_when_app_id_is_missing(
+ 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": 4000,
+ }.get(key, default)
+ entry = MagicMock()
+ entry.id = "entry-steam-2"
+ entry.title = "Steam group post"
+ entry.link = "https://steamcommunity.com/groups/example/announcements/detail/1234567890"
+ entry.summary = ""
+ entry.content = []
+ entry.feed.url = "https://steamcommunity.com/groups/example/rss/"
+ mock_replace_tags_in_embed.return_value = feeds.CustomEmbed(
+ description="Steam group news",
+ thumbnail_url="https://example.com/custom-thumb.jpg",
+ show_steam_game_icon_in_thumbnail=True,
+ )
+
+ 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 "thumbnail" not in embeds[0]
+ 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(
@@ -936,8 +1067,8 @@ def test_create_embed_webhook_uses_feed_text_length_limit_for_regular_embed_desc
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("...")
+ assert len(embeds[0]["description"]) == 20 # pyright: ignore[reportArgumentType]
+ assert embeds[0]["description"].endswith("...") # pyright: ignore[reportOptionalMemberAccess, reportAttributeAccessIssue]
mock_fetch_ttvdrops_campaign_media_items.assert_not_called()
diff --git a/tests/test_main.py b/tests/test_main.py
index b378a29..710499d 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -303,6 +303,60 @@ def test_get() -> None:
assert response.status_code == 200, f"/whitelist failed: {response.text}"
+def test_feed_page_shows_steam_thumbnail_hint_for_steam_feeds() -> None:
+ @dataclass(slots=True)
+ class DummyFeed:
+ url: str
+ title: str
+ updates_enabled: bool = True
+ last_exception: None = None
+ added: None = None
+ last_updated: None = None
+ last_retrieved: None = None
+ update_after: None = None
+
+ class StubReader:
+ def __init__(self) -> None:
+ self.feed = DummyFeed(
+ url="https://store.steampowered.com/feeds/news/app/570/?cc=US&l=english",
+ title="Dota 2",
+ )
+
+ def get_feed(self, feed_url: str) -> DummyFeed:
+ assert feed_url == self.feed.url
+ return self.feed
+
+ def get_tag(self, _resource: object, key: str, default: TestTagValue = None) -> TestTagValue:
+ return {
+ "webhooks": [],
+ "delivery_mode": "embed",
+ "screenshot_layout": "desktop",
+ "media_gallery_image_limit": 0,
+ "webhook_text_length_limit": 4000,
+ "save_sent_webhooks": True,
+ }.get(key, default)
+
+ def get_entry_counts(self, **_kwargs: TestKwargValue) -> SimpleNamespace:
+ return SimpleNamespace(total=0)
+
+ def get_entries(self, **_kwargs: TestKwargValue) -> list[Entry]:
+ return []
+
+ def get_feed_counts(self, **_kwargs: TestKwargValue) -> SimpleNamespace:
+ return SimpleNamespace()
+
+ stub = StubReader()
+ app.dependency_overrides[get_reader_dependency] = lambda: stub
+
+ try:
+ with patch("discord_rss_bot.main.create_html_for_feed", return_value=""):
+ response: Response = client.get(url="/feed", params={"feed_url": stub.feed.url})
+
+ assert response.status_code == 200, f"/feed failed: {response.text}"
+ finally:
+ app.dependency_overrides = {}
+
+
def test_blacklist_page_uses_live_preview_layout() -> None:
ensure_preview_feed_exists()
@@ -2562,6 +2616,7 @@ def test_post_embed_saves_all_fields() -> None:
"thumbnail_url": "https://example.com/thumb.png",
"footer_text": "Footer Text",
"footer_icon_url": "https://example.com/footer.png",
+ "show_steam_game_icon_in_thumbnail": "true",
},
follow_redirects=False,
)
@@ -2584,6 +2639,7 @@ def test_post_embed_saves_all_fields() -> None:
assert saved["thumbnail_url"] == "https://example.com/thumb.png"
assert saved["footer_text"] == "Footer Text"
assert saved["footer_icon_url"] == "https://example.com/footer.png"
+ assert saved["show_steam_game_icon_in_thumbnail"] is True
finally:
app.dependency_overrides = {}
@@ -2603,6 +2659,7 @@ def test_post_embed_allows_clearing_description() -> None:
"thumbnail_url": "",
"footer_text": "",
"footer_icon_url": "",
+ "show_steam_game_icon_in_thumbnail": False,
})
stub = _make_stub_reader_for_embed(stored_embed=existing)
app.dependency_overrides[get_reader_dependency] = lambda: stub
@@ -2660,6 +2717,7 @@ def test_post_embed_allows_clearing_all_fields() -> None:
"thumbnail_url": "https://old.example.com/thumb.png",
"footer_text": "Old Footer",
"footer_icon_url": "https://old.example.com/footer.png",
+ "show_steam_game_icon_in_thumbnail": True,
})
stub = _make_stub_reader_for_embed(stored_embed=existing)
app.dependency_overrides[get_reader_dependency] = lambda: stub
@@ -2699,6 +2757,7 @@ def test_post_embed_allows_clearing_all_fields() -> None:
assert not saved["thumbnail_url"]
assert not saved["footer_text"]
assert not saved["footer_icon_url"]
+ assert saved["show_steam_game_icon_in_thumbnail"] is False
finally:
app.dependency_overrides = {}
@@ -2716,6 +2775,7 @@ def test_post_embed_untouched_fields_retain_values() -> None:
"thumbnail_url": "",
"footer_text": "Old Footer",
"footer_icon_url": "",
+ "show_steam_game_icon_in_thumbnail": False,
})
stub = _make_stub_reader_for_embed(stored_embed=existing)
app.dependency_overrides[get_reader_dependency] = lambda: stub
@@ -2755,6 +2815,7 @@ def test_post_embed_untouched_fields_retain_values() -> None:
assert saved["author_name"] == "Author"
assert saved["author_url"] == "https://a.example.com"
assert saved["footer_text"] == "Old Footer"
+ assert saved["show_steam_game_icon_in_thumbnail"] is False
finally:
app.dependency_overrides = {}
@@ -2792,6 +2853,7 @@ def test_post_embed_saves_empty_description_when_no_prior_embed_exists() -> None
saved: dict[str, str] = json.loads(json_arg)
assert saved["title"] == "Just a Title"
assert not saved["description"], f"Expected empty description, got {saved['description']!r}"
+ assert saved["show_steam_game_icon_in_thumbnail"] is False
finally:
app.dependency_overrides = {}