From b025d5b1364209ba1e1aabddff00adcc163fd222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Fri, 10 Apr 2026 21:02:07 +0200 Subject: [PATCH] Add functionality to attach feeds to webhooks from the index page --- discord_rss_bot/main.py | 46 ++++++++++++++++++++++++ discord_rss_bot/templates/index.html | 45 ++++++++++++++++------- tests/test_main.py | 54 ++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 12 deletions(-) diff --git a/discord_rss_bot/main.py b/discord_rss_bot/main.py index 197d31a..f6c02db 100644 --- a/discord_rss_bot/main.py +++ b/discord_rss_bot/main.py @@ -308,6 +308,52 @@ async def post_create_feed( return RedirectResponse(url=f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) +@app.post("/attach_feed_webhook") +async def post_attach_feed_webhook( + feed_url: Annotated[str, Form()], + webhook_dropdown: Annotated[str, Form()], + reader: Annotated[Reader, Depends(get_reader_dependency)], + redirect_to: Annotated[str, Form()] = "", +) -> RedirectResponse: + """Attach an existing feed to one of the configured webhooks. + + Args: + feed_url: The feed URL to update. + webhook_dropdown: The webhook name selected from the dropdown. + reader: The Reader instance. + redirect_to: Optional redirect URL after update. + + Returns: + RedirectResponse: Redirect to index or feed page. + + Raises: + HTTPException: If feed or webhook cannot be found. + """ + clean_feed_url: str = urllib.parse.unquote(feed_url.strip()) + selected_webhook_name: str = webhook_dropdown.strip() + + try: + reader.get_feed(clean_feed_url) + except FeedNotFoundError as e: + raise HTTPException(status_code=404, detail="Feed not found") from e + + webhook_url: str = "" + hooks = cast("list[dict[str, str]]", list(reader.get_tag((), "webhooks", []))) + for hook in hooks: + if hook.get("name") == selected_webhook_name: + webhook_url = hook.get("url", "").strip() + break + + if not webhook_url: + raise HTTPException(status_code=404, detail="Webhook not found") + + reader.set_tag(clean_feed_url, "webhook", webhook_url) # pyright: ignore[reportArgumentType] + commit_state_change(reader, f"Attach feed {clean_feed_url} to webhook {selected_webhook_name}") + + redirect_url: str = redirect_to.strip() or f"/feed?feed_url={urllib.parse.quote(clean_feed_url)}" + return RedirectResponse(url=redirect_url, status_code=303) + + @app.post("/pause") async def post_pause_feed( feed_url: Annotated[str, Form()], diff --git a/discord_rss_bot/templates/index.html b/discord_rss_bot/templates/index.html index 341ec38..6c12656 100644 --- a/discord_rss_bot/templates/index.html +++ b/discord_rss_bot/templates/index.html @@ -133,20 +133,41 @@ diff --git a/tests/test_main.py b/tests/test_main.py index 6c88005..e40724d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -559,6 +559,60 @@ def test_delete_webhook() -> None: assert webhook_name not in response3.text, f"Webhook found in /webhooks: {response3.text}" +def test_attach_feed_webhook_from_index() -> None: + """Feeds without attached webhook should be attachable from the index page.""" + original_webhook_name = "original-webhook" + original_webhook_url = "https://discord.com/api/webhooks/111/original" + replacement_webhook_name = "replacement-webhook" + replacement_webhook_url = "https://discord.com/api/webhooks/222/replacement" + + # Start clean. + client.post(url="/remove", data={"feed_url": feed_url}) + client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url}) + client.post(url="/delete_webhook", data={"webhook_url": replacement_webhook_url}) + + # Add a webhook and a feed attached to it. + response = client.post( + url="/add_webhook", + data={"webhook_name": original_webhook_name, "webhook_url": original_webhook_url}, + ) + assert response.status_code == 200, f"Failed to add original webhook: {response.text}" + + response = client.post(url="/add", data={"feed_url": feed_url, "webhook_dropdown": original_webhook_name}) + assert response.status_code == 200, f"Failed to add feed: {response.text}" + + # Remove the original webhook so feed becomes "without attached webhook". + response = client.post(url="/delete_webhook", data={"webhook_url": original_webhook_url}) + assert response.status_code == 200, f"Failed to delete original webhook: {response.text}" + + # Add a replacement webhook we can attach to. + response = client.post( + url="/add_webhook", + data={"webhook_name": replacement_webhook_name, "webhook_url": replacement_webhook_url}, + ) + assert response.status_code == 200, f"Failed to add replacement webhook: {response.text}" + + # The feed should now be listed in "Feeds without attached webhook" section. + response = client.get(url="/") + assert response.status_code == 200, f"Failed to get /: {response.text}" + assert "Feeds without attached webhook:" in response.text + assert "/attach_feed_webhook" in response.text + + # Attach the feed to the new webhook. + response = client.post( + url="/attach_feed_webhook", + data={"feed_url": feed_url, "webhook_dropdown": replacement_webhook_name, "redirect_to": "/"}, + ) + assert response.status_code == 200, f"Failed to attach feed to webhook: {response.text}" + + reader = get_reader_dependency() + assert reader.get_tag(feed_url, "webhook", "") == replacement_webhook_url + + # Cleanup. + client.post(url="/remove", data={"feed_url": feed_url}) + client.post(url="/delete_webhook", data={"webhook_url": replacement_webhook_url}) + + def test_update_feed_not_found() -> None: """Test updating a non-existent feed.""" # Generate a feed URL that does not exist