Add functionality to attach feeds to webhooks from the index page

This commit is contained in:
Joakim Hellsén 2026-04-10 21:02:07 +02:00
commit b025d5b136
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
3 changed files with 133 additions and 12 deletions

View file

@ -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()],

View file

@ -133,20 +133,41 @@
<ul class="list-group text-danger">
Feeds without attached webhook:
{% for feed in feeds_without_attached_webhook %}
<a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}">
{# Display username@youtube for YouTube feeds #}
{% if "youtube.com/feeds/videos.xml" in feed.url %}
{% if "user=" in feed.url %}
{{ feed.url.split("user=")[1] }}@youtube
{% elif "channel_id=" in feed.url %}
{{ feed.title if feed.title else feed.url.split("channel_id=")[1] }}@youtube
<li class="list-group-item bg-dark border-dark text-danger">
<div class="d-flex flex-wrap align-items-center gap-2">
<a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}">
{# Display username@youtube for YouTube feeds #}
{% if "youtube.com/feeds/videos.xml" in feed.url %}
{% if "user=" in feed.url %}
{{ feed.url.split("user=")[1] }}@youtube
{% elif "channel_id=" in feed.url %}
{{ feed.title if feed.title else feed.url.split("channel_id=")[1] }}@youtube
{% else %}
{{ feed.url }}
{% endif %}
{% else %}
{{ feed.url }}
{% endif %}
</a>
{% if webhooks %}
<form action="/attach_feed_webhook"
method="post"
class="d-flex flex-wrap align-items-center gap-2 ms-md-auto">
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<input type="hidden" name="redirect_to" value="/" />
<select name="webhook_dropdown"
class="form-select form-select-sm bg-dark border-dark text-muted"
required>
<option value="" selected disabled>Select webhook...</option>
{% for hook in webhooks %}<option value="{{ hook.name }}">{{ hook.name }}</option>{% endfor %}
</select>
<button class="btn btn-outline-light btn-sm" type="submit">Attach</button>
</form>
{% else %}
{{ feed.url }}
<span class="text-muted small">Add a webhook first to attach this feed.</span>
{% endif %}
{% else %}
{{ feed.url }}
{% endif %}
</a>
</div>
</li>
{% endfor %}
</ul>
</div>

View file

@ -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