Add Steam embed thumbnail support
All checks were successful
Test and build Docker image / docker (push) Successful in 25s
All checks were successful
Test and build Docker image / docker (push) Successful in 25s
This commit is contained in:
parent
6acc27865a
commit
fc50aed740
9 changed files with 371 additions and 8 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue