Add endpoint to see all the entries for a webhook
This commit is contained in:
parent
f9882c7e43
commit
5215b80643
5 changed files with 555 additions and 193 deletions
|
|
@ -948,7 +948,7 @@ async def get_feed(feed_url: str, request: Request, starting_after: str = ""):
|
||||||
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
return templates.TemplateResponse(request=request, name="feed.html", context=context)
|
||||||
|
|
||||||
|
|
||||||
def create_html_for_feed(entries: Iterable[Entry], current_feed_url: str = "") -> str: # noqa: PLR0914
|
def create_html_for_feed(entries: Iterable[Entry], current_feed_url: str = "") -> str: # noqa: C901, PLR0914
|
||||||
"""Create HTML for the search results.
|
"""Create HTML for the search results.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
|
|
@ -988,6 +988,16 @@ def create_html_for_feed(entries: Iterable[Entry], current_feed_url: str = "") -
|
||||||
if current_feed_url and source_feed_url != current_feed_url:
|
if current_feed_url and source_feed_url != current_feed_url:
|
||||||
from_another_feed = f"<span class='badge bg-warning text-dark'>From another feed: {source_feed_url}</span>"
|
from_another_feed = f"<span class='badge bg-warning text-dark'>From another feed: {source_feed_url}</span>"
|
||||||
|
|
||||||
|
# Add feed link when viewing from webhook_entries or aggregated views
|
||||||
|
feed_link: str = ""
|
||||||
|
if not current_feed_url or source_feed_url != current_feed_url:
|
||||||
|
encoded_feed_url: str = urllib.parse.quote(source_feed_url)
|
||||||
|
feed_title: str = entry.feed.title if hasattr(entry.feed, "title") and entry.feed.title else source_feed_url
|
||||||
|
feed_link = (
|
||||||
|
f"<a class='text-muted' style='font-size: 0.85em;' "
|
||||||
|
f"href='/feed?feed_url={encoded_feed_url}'>{feed_title}</a><br>"
|
||||||
|
)
|
||||||
|
|
||||||
entry_id: str = urllib.parse.quote(entry.id)
|
entry_id: str = urllib.parse.quote(entry.id)
|
||||||
to_discord_html: str = f"<a class='text-muted' href='/post_entry?entry_id={entry_id}'>Send to Discord</a>"
|
to_discord_html: str = f"<a class='text-muted' href='/post_entry?entry_id={entry_id}'>Send to Discord</a>"
|
||||||
|
|
||||||
|
|
@ -1015,7 +1025,7 @@ def create_html_for_feed(entries: Iterable[Entry], current_feed_url: str = "") -
|
||||||
|
|
||||||
html += f"""<div class="p-2 mb-2 border border-dark">
|
html += f"""<div class="p-2 mb-2 border border-dark">
|
||||||
{blacklisted}{whitelisted}{from_another_feed}<a class="text-muted text-decoration-none" href="{entry.link}"><h2>{entry.title}</h2></a>
|
{blacklisted}{whitelisted}{from_another_feed}<a class="text-muted text-decoration-none" href="{entry.link}"><h2>{entry.title}</h2></a>
|
||||||
{f"By {entry.author} @" if entry.author else ""}{published} - {to_discord_html}
|
{feed_link}{f"By {entry.author} @" if entry.author else ""}{published} - {to_discord_html}
|
||||||
|
|
||||||
{text}
|
{text}
|
||||||
{video_embed_html}
|
{video_embed_html}
|
||||||
|
|
@ -1397,6 +1407,109 @@ def extract_youtube_video_id(url: str) -> str | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/webhook_entries", response_class=HTMLResponse)
|
||||||
|
async def get_webhook_entries( # noqa: C901, PLR0912, PLR0914
|
||||||
|
webhook_url: str,
|
||||||
|
request: Request,
|
||||||
|
starting_after: str = "",
|
||||||
|
) -> HTMLResponse:
|
||||||
|
"""Get all latest entries from all feeds for a specific webhook.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
webhook_url: The webhook URL to get entries for.
|
||||||
|
request: The request object.
|
||||||
|
starting_after: The entry to start after. Used for pagination.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTMLResponse: The webhook entries page.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
HTTPException: If no feeds are found for this webhook or webhook doesn't exist.
|
||||||
|
"""
|
||||||
|
entries_per_page: int = 20
|
||||||
|
clean_webhook_url: str = urllib.parse.unquote(webhook_url.strip())
|
||||||
|
|
||||||
|
# Get the webhook name from the webhooks list
|
||||||
|
webhooks: list[dict[str, str]] = cast("list[dict[str, str]]", list(reader.get_tag((), "webhooks", [])))
|
||||||
|
webhook_name: str = ""
|
||||||
|
for hook in webhooks:
|
||||||
|
if hook["url"] == clean_webhook_url:
|
||||||
|
webhook_name = hook["name"]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not webhook_name:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Webhook not found: {clean_webhook_url}")
|
||||||
|
|
||||||
|
# Get all feeds associated with this webhook
|
||||||
|
all_feeds: list[Feed] = list(reader.get_feeds())
|
||||||
|
webhook_feeds: list[Feed] = []
|
||||||
|
|
||||||
|
for feed in all_feeds:
|
||||||
|
try:
|
||||||
|
feed_webhook: str = str(reader.get_tag(feed.url, "webhook", ""))
|
||||||
|
if feed_webhook == clean_webhook_url:
|
||||||
|
webhook_feeds.append(feed)
|
||||||
|
except TagNotFoundError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Get all entries from all feeds for this webhook, sorted by published date
|
||||||
|
all_entries: list[Entry] = [entry for feed in webhook_feeds for entry in reader.get_entries(feed=feed)]
|
||||||
|
|
||||||
|
# Sort entries by published date (newest first)
|
||||||
|
all_entries.sort(
|
||||||
|
key=lambda e: e.published or datetime.now(tz=UTC),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Handle pagination
|
||||||
|
if starting_after:
|
||||||
|
try:
|
||||||
|
start_after_entry: Entry | None = reader.get_entry((
|
||||||
|
starting_after.split("|", maxsplit=1)[0],
|
||||||
|
starting_after.split("|")[1],
|
||||||
|
))
|
||||||
|
except (FeedNotFoundError, EntryNotFoundError):
|
||||||
|
start_after_entry = None
|
||||||
|
else:
|
||||||
|
start_after_entry = None
|
||||||
|
|
||||||
|
# Find the index of the starting entry
|
||||||
|
start_index: int = 0
|
||||||
|
if start_after_entry:
|
||||||
|
for idx, entry in enumerate(all_entries):
|
||||||
|
if entry.id == start_after_entry.id and entry.feed.url == start_after_entry.feed.url:
|
||||||
|
start_index = idx + 1
|
||||||
|
break
|
||||||
|
|
||||||
|
# Get the page of entries
|
||||||
|
paginated_entries: list[Entry] = all_entries[start_index : start_index + entries_per_page]
|
||||||
|
|
||||||
|
# Get the last entry for pagination
|
||||||
|
last_entry: Entry | None = None
|
||||||
|
if paginated_entries:
|
||||||
|
last_entry = paginated_entries[-1]
|
||||||
|
|
||||||
|
# Create the html for the entries
|
||||||
|
html: str = create_html_for_feed(paginated_entries)
|
||||||
|
|
||||||
|
# Check if there are more entries available
|
||||||
|
total_entries: int = len(all_entries)
|
||||||
|
is_show_more_entries_button_visible: bool = (start_index + entries_per_page) < total_entries
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"request": request,
|
||||||
|
"webhook_name": webhook_name,
|
||||||
|
"webhook_url": clean_webhook_url,
|
||||||
|
"entries": paginated_entries,
|
||||||
|
"html": html,
|
||||||
|
"last_entry": last_entry,
|
||||||
|
"is_show_more_entries_button_visible": is_show_more_entries_button_visible,
|
||||||
|
"total_entries": total_entries,
|
||||||
|
"feeds_count": len(webhook_feeds),
|
||||||
|
}
|
||||||
|
return templates.TemplateResponse(request=request, name="webhook_entries.html", context=context)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
sentry_sdk.init(
|
sentry_sdk.init(
|
||||||
dsn="https://6e77a0d7acb9c7ea22e85a375e0ff1f4@o4505228040339456.ingest.us.sentry.io/4508792887967744",
|
dsn="https://6e77a0d7acb9c7ea22e85a375e0ff1f4@o4505228040339456.ingest.us.sentry.io/4508792887967744",
|
||||||
|
|
|
||||||
|
|
@ -1,154 +1,155 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- List all feeds -->
|
<!-- List all feeds -->
|
||||||
<ul>
|
<ul>
|
||||||
<!-- Check if any feeds -->
|
<!-- Check if any feeds -->
|
||||||
{% if feeds %}
|
{% if feeds %}
|
||||||
<p>
|
<p>
|
||||||
{{ feed_count.total }} feed{{'s' if feed_count.total > 1 else "" }}
|
{{ feed_count.total }} feed{{'s' if feed_count.total > 1 else "" }}
|
||||||
<!-- How many broken feeds -->
|
<!-- How many broken feeds -->
|
||||||
<!-- Make broken feed text red if true. -->
|
<!-- Make broken feed text red if true. -->
|
||||||
{% if feed_count.broken %}
|
{% if feed_count.broken %}
|
||||||
- <span class="text-danger">{{ feed_count.broken }} broken</span>
|
- <span class="text-danger">{{ feed_count.broken }} broken</span>
|
||||||
|
{% else %}
|
||||||
|
- {{ feed_count.broken }} broken
|
||||||
|
{% endif %}
|
||||||
|
<!-- How many enabled feeds -->
|
||||||
|
<!-- Make amount of enabled feeds yellow if some are disabled. -->
|
||||||
|
{% if feed_count.total != feed_count.updates_enabled %}
|
||||||
|
- <span class="text-warning">{{ feed_count.updates_enabled }} enabled</span>
|
||||||
|
{% else %}
|
||||||
|
- {{ feed_count.updates_enabled }} enabled
|
||||||
|
{% endif %}
|
||||||
|
<!-- How many entries -->
|
||||||
|
- {{ entry_count.total }} entries
|
||||||
|
<abbr title="Average entries per day for the past 1, 3 and 12 months">
|
||||||
|
({{ entry_count.averages[0]|round(1) }},
|
||||||
|
{{ entry_count.averages[1]|round(1) }},
|
||||||
|
{{ entry_count.averages[2]|round(1) }})
|
||||||
|
</abbr>
|
||||||
|
</p>
|
||||||
|
<!-- Loop through the webhooks and add the feeds grouped by domain -->
|
||||||
|
{% for hook_from_context in webhooks %}
|
||||||
|
<div class="p-2 mb-3 border border-dark">
|
||||||
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h2 class="h5 mb-0">
|
||||||
|
<a class="text-muted" href="/webhooks">{{ hook_from_context.name }}</a>
|
||||||
|
</h2>
|
||||||
|
<a class="text-muted"
|
||||||
|
href="/webhook_entries?webhook_url={{ hook_from_context.url|encode_url }}">View Latest Entries</a>
|
||||||
|
</div>
|
||||||
|
<!-- Group feeds by domain within each webhook -->
|
||||||
|
{% set feeds_for_hook = [] %}
|
||||||
|
{% for feed_webhook in feeds %}
|
||||||
|
{% if hook_from_context.url == feed_webhook.webhook %}
|
||||||
|
{% set _ = feeds_for_hook.append(feed_webhook) %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if feeds_for_hook %}
|
||||||
|
<!-- Create a dictionary to hold feeds grouped by domain -->
|
||||||
|
{% set domains = {} %}
|
||||||
|
{% for feed_item in feeds_for_hook %}
|
||||||
|
{% set feed = feed_item.feed %}
|
||||||
|
{% set domain = feed_item.domain %}
|
||||||
|
{% if domain not in domains %}
|
||||||
|
{% set _ = domains.update({domain: []}) %}
|
||||||
|
{% endif %}
|
||||||
|
{% set _ = domains[domain].append(feed) %}
|
||||||
|
{% endfor %}
|
||||||
|
<!-- Display domains and their feeds -->
|
||||||
|
{% for domain, domain_feeds in domains.items() %}
|
||||||
|
<div class="card bg-dark border border-dark mb-2">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="h6 mb-0 text-white-50">{{ domain }} ({{ domain_feeds|length }})</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-2">
|
||||||
|
<ul class="list-group list-unstyled mb-0">
|
||||||
|
{% for feed in domain_feeds %}
|
||||||
|
<li>
|
||||||
|
<a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}">
|
||||||
|
{% if feed.title %}
|
||||||
|
{{ feed.title }}
|
||||||
|
{% else %}
|
||||||
|
{{ feed.url }}
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% if not feed.updates_enabled %}<span class="text-warning">Disabled</span>{% endif %}
|
||||||
|
{% if feed.last_exception %}<span class="text-danger">({{ feed.last_exception.value_str }})</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<p class="text-muted">No feeds associated with this webhook.</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
- {{ feed_count.broken }} broken
|
<p>
|
||||||
|
Hello there!
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
You need to add a webhook <a class="text-muted" href="/add_webhook">here</a> to get started. After that, you can
|
||||||
|
add feeds <a class="text-muted" href="/add">here</a>. You can find both of these links in the navigation bar
|
||||||
|
above.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
If you have any questions or suggestions, feel free to contact me on <a class="text-muted" href="mailto:tlovinator@gmail.com">tlovinator@gmail.com</a> or TheLovinator#9276 on Discord.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Thanks!
|
||||||
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- How many enabled feeds -->
|
<!-- Show feeds without webhooks -->
|
||||||
<!-- Make amount of enabled feeds yellow if some are disabled. -->
|
{% if broken_feeds %}
|
||||||
{% if feed_count.total != feed_count.updates_enabled %}
|
<div class="p-2 mb-2 border border-dark">
|
||||||
- <span class="text-warning">{{ feed_count.updates_enabled }} enabled</span>
|
<ul class="list-group text-danger">
|
||||||
{% else %}
|
Feeds without webhook:
|
||||||
- {{ feed_count.updates_enabled }} enabled
|
{% for broken_feed in broken_feeds %}
|
||||||
{% endif %}
|
<a class="text-muted"
|
||||||
<!-- How many entries -->
|
href="/feed?feed_url={{ broken_feed.url|encode_url }}">
|
||||||
- {{ entry_count.total }} entries
|
{# Display username@youtube for YouTube feeds #}
|
||||||
<abbr title="Average entries per day for the past 1, 3 and 12 months">
|
{% if "youtube.com/feeds/videos.xml" in broken_feed.url %}
|
||||||
({{ entry_count.averages[0]|round(1) }},
|
{% if "user=" in broken_feed.url %}
|
||||||
{{ entry_count.averages[1]|round(1) }},
|
{{ broken_feed.url.split("user=")[1] }}@youtube
|
||||||
{{ entry_count.averages[2]|round(1) }})
|
{% elif "channel_id=" in broken_feed.url %}
|
||||||
</abbr>
|
{{ broken_feed.title if broken_feed.title else broken_feed.url.split("channel_id=")[1] }}@youtube
|
||||||
</p>
|
{% else %}
|
||||||
|
{{ broken_feed.url }}
|
||||||
<!-- Loop through the webhooks and add the feeds grouped by domain -->
|
{% endif %}
|
||||||
{% for hook_from_context in webhooks %}
|
{% else %}
|
||||||
<div class="p-2 mb-3 border border-dark">
|
{{ broken_feed.url }}
|
||||||
<h2 class="h5 mb-3">
|
{% endif %}
|
||||||
<a class="text-muted" href="/webhooks">{{ hook_from_context.name }}</a>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<!-- Group feeds by domain within each webhook -->
|
|
||||||
{% set feeds_for_hook = [] %}
|
|
||||||
{% for feed_webhook in feeds %}
|
|
||||||
{% if hook_from_context.url == feed_webhook.webhook %}
|
|
||||||
{% set _ = feeds_for_hook.append(feed_webhook) %}
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if feeds_for_hook %}
|
|
||||||
<!-- Create a dictionary to hold feeds grouped by domain -->
|
|
||||||
{% set domains = {} %}
|
|
||||||
{% for feed_item in feeds_for_hook %}
|
|
||||||
{% set feed = feed_item.feed %}
|
|
||||||
{% set domain = feed_item.domain %}
|
|
||||||
{% if domain not in domains %}
|
|
||||||
{% set _ = domains.update({domain: []}) %}
|
|
||||||
{% endif %}
|
|
||||||
{% set _ = domains[domain].append(feed) %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Display domains and their feeds -->
|
|
||||||
{% for domain, domain_feeds in domains.items() %}
|
|
||||||
<div class="card bg-dark border border-dark mb-2">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="h6 mb-0 text-white-50">{{ domain }} ({{ domain_feeds|length }})</h3>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-2">
|
|
||||||
<ul class="list-group list-unstyled mb-0">
|
|
||||||
{% for feed in domain_feeds %}
|
|
||||||
<li>
|
|
||||||
<a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}">
|
|
||||||
{% if feed.title %}{{ feed.title }}{% else %}{{ feed.url }}{% endif %}
|
|
||||||
</a>
|
</a>
|
||||||
{% if not feed.updates_enabled %}<span class="text-warning">Disabled</span>{% endif %}
|
|
||||||
{% if feed.last_exception %}<span
|
|
||||||
class="text-danger">({{ feed.last_exception.value_str }})</span>{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<p class="text-muted">No feeds associated with this webhook.</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
<!-- Show feeds that has no attached webhook -->
|
||||||
{% endfor %}
|
{% if feeds_without_attached_webhook %}
|
||||||
{% else %}
|
<div class="p-2 mb-2 border border-dark">
|
||||||
<p>
|
<ul class="list-group text-danger">
|
||||||
Hello there!
|
Feeds without attached webhook:
|
||||||
<br>
|
{% for feed in feeds_without_attached_webhook %}
|
||||||
<br>
|
<a class="text-muted" href="/feed?feed_url={{ feed.url|encode_url }}">
|
||||||
You need to add a webhook <a class="text-muted" href="/add_webhook">here</a> to get started. After that, you can
|
{# Display username@youtube for YouTube feeds #}
|
||||||
add feeds <a class="text-muted" href="/add">here</a>. You can find both of these links in the navigation bar
|
{% if "youtube.com/feeds/videos.xml" in feed.url %}
|
||||||
above.
|
{% if "user=" in feed.url %}
|
||||||
<br>
|
{{ feed.url.split("user=")[1] }}@youtube
|
||||||
<br>
|
{% elif "channel_id=" in feed.url %}
|
||||||
If you have any questions or suggestions, feel free to contact me on <a class="text-muted"
|
{{ feed.title if feed.title else feed.url.split("channel_id=")[1] }}@youtube
|
||||||
href="mailto:tlovinator@gmail.com">tlovinator@gmail.com</a> or TheLovinator#9276 on Discord.
|
{% else %}
|
||||||
<br>
|
{{ feed.url }}
|
||||||
<br>
|
{% endif %}
|
||||||
Thanks!
|
{% else %}
|
||||||
</p>
|
{{ feed.url }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</a>
|
||||||
<!-- Show feeds without webhooks -->
|
{% endfor %}
|
||||||
{% if broken_feeds %}
|
</ul>
|
||||||
<div class="p-2 mb-2 border border-dark">
|
</div>
|
||||||
<ul class="list-group text-danger">
|
{% endif %}
|
||||||
Feeds without webhook:
|
</ul>
|
||||||
{% for broken_feed in broken_feeds %}
|
|
||||||
<a class="text-muted" href="/feed?feed_url={{ broken_feed.url|encode_url }}">
|
|
||||||
{# Display username@youtube for YouTube feeds #}
|
|
||||||
{% if "youtube.com/feeds/videos.xml" in broken_feed.url %}
|
|
||||||
{% if "user=" in broken_feed.url %}
|
|
||||||
{{ broken_feed.url.split("user=")[1] }}@youtube
|
|
||||||
{% elif "channel_id=" in broken_feed.url %}
|
|
||||||
{{ broken_feed.title if broken_feed.title else broken_feed.url.split("channel_id=")[1] }}@youtube
|
|
||||||
{% else %}
|
|
||||||
{{ broken_feed.url }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{{ broken_feed.url }}
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Show feeds that has no attached webhook -->
|
|
||||||
{% if feeds_without_attached_webhook %}
|
|
||||||
<div class="p-2 mb-2 border border-dark">
|
|
||||||
<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
|
|
||||||
{% else %}
|
|
||||||
{{ feed.url }}
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
{{ feed.url }}
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
||||||
38
discord_rss_bot/templates/webhook_entries.html
Normal file
38
discord_rss_bot/templates/webhook_entries.html
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}
|
||||||
|
| {{ webhook_name }} - Latest Entries
|
||||||
|
{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="card mb-3 border border-dark p-3 text-light">
|
||||||
|
<!-- Webhook Title -->
|
||||||
|
<h2>{{ webhook_name }} - Latest Entries ({{ total_entries }} total from {{ feeds_count }} feeds)</h2>
|
||||||
|
<!-- Webhook Info -->
|
||||||
|
<div class="mt-3">
|
||||||
|
<p class="text-muted">
|
||||||
|
<code>{{ webhook_url }}</code>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{# Rendered HTML content #}
|
||||||
|
{% if entries %}
|
||||||
|
<pre>{{ html|safe }}</pre>
|
||||||
|
{% if is_show_more_entries_button_visible and last_entry %}
|
||||||
|
<a class="btn btn-dark mt-3"
|
||||||
|
href="/webhook_entries?webhook_url={{ webhook_url|encode_url }}&starting_after={{ last_entry.feed.url|encode_url }}|{{ last_entry.id|encode_url }}">
|
||||||
|
Show more entries
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
{% elif feeds_count == 0 %}
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
No feeds found for {{ webhook_name }}. <a href="/add" class="alert-link">Add feeds</a> to this webhook to see entries here.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
No entries found for {{ webhook_name }}. <a href="/settings" class="alert-link">Update feeds</a> to fetch new entries.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock content %}
|
||||||
|
|
@ -1,55 +1,63 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}
|
{% block title %}
|
||||||
| Webhooks
|
| Webhooks
|
||||||
{% endblock title %}
|
{% endblock title %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container my-4 text-light">
|
<div class="container my-4 text-light">
|
||||||
{% for hook in hooks_with_data %}
|
{% for hook in hooks_with_data %}
|
||||||
<div class="border border-dark mb-4 shadow-sm p-3">
|
<div class="border border-dark mb-4 shadow-sm p-3">
|
||||||
<div class="text-muted">
|
<div class="text-muted">
|
||||||
<h4>{{ hook.custom_name }}</h4>
|
<h4>{{ hook.custom_name }}</h4>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li>
|
<li>
|
||||||
<strong>
|
<strong>
|
||||||
<abbr title="Name configured in Discord">
|
<abbr title="Name configured in Discord">
|
||||||
Discord name:</strong> {{ hook.name }}
|
Discord name:</strong> {{ hook.name }}
|
||||||
</abbr>
|
</abbr>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<strong>Webhook:</strong>
|
<strong>Webhook:</strong>
|
||||||
<a class="text-muted"
|
<a class="text-muted" href="{{ hook.url }}">{{ hook.url | replace('https://discord.com/api/webhooks', '') }}</a>
|
||||||
href="{{ hook.url }}">{{ hook.url | replace("https://discord.com/api/webhooks", "") }}</a>
|
</li>
|
||||||
</li>
|
</ul>
|
||||||
</ul>
|
<hr />
|
||||||
<hr>
|
<form action="/modify_webhook" method="post" class="row g-3">
|
||||||
<form action="/modify_webhook" method="post" class="row g-3">
|
<input type="hidden" name="old_hook" value="{{ hook.url }}" />
|
||||||
<input type="hidden" name="old_hook" value="{{ hook.url }}" />
|
<div class="col-md-8">
|
||||||
<div class="col-md-8">
|
<label for="new_hook" class="form-label">Modify Webhook</label>
|
||||||
<label for="new_hook" class="form-label">Modify Webhook</label>
|
<input type="text"
|
||||||
<input type="text" name="new_hook" id="new_hook" class="form-control border text-muted bg-dark"
|
name="new_hook"
|
||||||
placeholder="Enter new webhook URL" />
|
id="new_hook"
|
||||||
|
class="form-control border text-muted bg-dark"
|
||||||
|
placeholder="Enter new webhook URL" />
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<button type="submit" class="btn btn-primary w-100">Modify</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex justify-content-between mt-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<a href="/webhook_entries?webhook_url={{ hook.url|encode_url }}"
|
||||||
|
class="btn btn-info btn-sm">View Latest Entries</a>
|
||||||
|
</div>
|
||||||
|
<form action="/delete_webhook" method="post">
|
||||||
|
<input type="hidden" name="webhook_url" value="{{ hook.url }}" />
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this webhook?');">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 d-flex align-items-end">
|
{% endfor %}
|
||||||
<button type="submit" class="btn btn-primary w-100">Modify</button>
|
<div class="border border-dark p-3">
|
||||||
</div>
|
You can append <code>?thread_id=THREAD_ID</code> to the URL to send messages to a thread.
|
||||||
</form>
|
</div>
|
||||||
|
<br />
|
||||||
|
<div class="text-end">
|
||||||
|
<a class="btn btn-primary mb-3" href="/add_webhook">Add New Webhook</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex justify-content-between mt-2">
|
{% endblock content %}
|
||||||
<form action="/delete_webhook" method="post">
|
|
||||||
<input type="hidden" name="webhook_url" value="{{ hook.url }}" />
|
|
||||||
<button type="submit" class="btn btn-danger"
|
|
||||||
onclick="return confirm('Are you sure you want to delete this webhook?');">Delete</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
<div class="border border-dark p-3">
|
|
||||||
You can append <code>?thread_id=THREAD_ID</code> to the URL to send messages to a thread.
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<div class="text-end">
|
|
||||||
<a class="btn btn-primary mb-3" href="/add_webhook">Add New Webhook</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
|
|
|
||||||
|
|
@ -581,3 +581,205 @@ def test_create_html_marks_entries_from_another_feed(monkeypatch: pytest.MonkeyP
|
||||||
|
|
||||||
assert "From another feed: https://example.com/feed-b.xml" in html
|
assert "From another feed: https://example.com/feed-b.xml" in html
|
||||||
assert "From another feed: https://example.com/feed-a.xml" not in html
|
assert "From another feed: https://example.com/feed-a.xml" not in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_webhook_not_found() -> None:
|
||||||
|
"""Test webhook_entries endpoint returns 404 when webhook doesn't exist."""
|
||||||
|
nonexistent_webhook_url = "https://discord.com/api/webhooks/999999/nonexistent"
|
||||||
|
|
||||||
|
response: Response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": nonexistent_webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 404, f"Expected 404 for non-existent webhook, got: {response.status_code}"
|
||||||
|
assert "Webhook not found" in response.text
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_no_feeds() -> None:
|
||||||
|
"""Test webhook_entries endpoint displays message when webhook has no feeds."""
|
||||||
|
# Clean up any existing feeds first
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
||||||
|
# Clean up and create a webhook
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
# Get webhook_entries without adding any feeds
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}"
|
||||||
|
assert webhook_name in response.text, "Webhook name not found in response"
|
||||||
|
assert "No feeds found" in response.text or "Add feeds" in response.text, "Expected message about no feeds"
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_with_feeds_no_entries() -> None:
|
||||||
|
"""Test webhook_entries endpoint when webhook has feeds but no entries yet."""
|
||||||
|
# Clean up and create fresh webhook
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
# Use a feed URL that exists but has no entries (or clean feed)
|
||||||
|
empty_feed_url = "https://lovinator.space/empty_feed.xml"
|
||||||
|
client.post(url="/remove", data={"feed_url": empty_feed_url})
|
||||||
|
|
||||||
|
# Add the feed
|
||||||
|
response = client.post(
|
||||||
|
url="/add",
|
||||||
|
data={"feed_url": empty_feed_url, "webhook_dropdown": webhook_name},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get webhook_entries
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}"
|
||||||
|
assert webhook_name in response.text, "Webhook name not found in response"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
client.post(url="/remove", data={"feed_url": empty_feed_url})
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_with_entries() -> None:
|
||||||
|
"""Test webhook_entries endpoint displays entries correctly."""
|
||||||
|
# Clean up and create webhook
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
# Remove and add the feed
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
response = client.post(
|
||||||
|
url="/add",
|
||||||
|
data={"feed_url": feed_url, "webhook_dropdown": webhook_name},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add feed: {response.text}"
|
||||||
|
|
||||||
|
# Get webhook_entries
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}"
|
||||||
|
assert webhook_name in response.text, "Webhook name not found in response"
|
||||||
|
# Should show entries (the feed has entries)
|
||||||
|
assert "total from" in response.text, "Expected to see entry count"
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_multiple_feeds() -> None:
|
||||||
|
"""Test webhook_entries endpoint shows feed count correctly."""
|
||||||
|
# Clean up and create webhook
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
# Remove and add feed
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
response = client.post(
|
||||||
|
url="/add",
|
||||||
|
data={"feed_url": feed_url, "webhook_dropdown": webhook_name},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add feed: {response.text}"
|
||||||
|
|
||||||
|
# Get webhook_entries
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}"
|
||||||
|
assert webhook_name in response.text, "Webhook name not found in response"
|
||||||
|
# Should show entries and feed count
|
||||||
|
assert "feed" in response.text.lower(), "Expected to see feed information"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_pagination() -> None:
|
||||||
|
"""Test webhook_entries endpoint pagination functionality."""
|
||||||
|
# Clean up and create webhook
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
# Remove and add the feed
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
response = client.post(
|
||||||
|
url="/add",
|
||||||
|
data={"feed_url": feed_url, "webhook_dropdown": webhook_name},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add feed: {response.text}"
|
||||||
|
|
||||||
|
# Get first page of webhook_entries
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries: {response.text}"
|
||||||
|
|
||||||
|
# Check if pagination button is shown when there are many entries
|
||||||
|
# The button should be visible if total_entries > 20 (entries_per_page)
|
||||||
|
if "Load More Entries" in response.text:
|
||||||
|
# Extract the starting_after parameter from the pagination form
|
||||||
|
# This is a simple check that pagination elements exist
|
||||||
|
assert 'name="starting_after"' in response.text, "Expected pagination form with starting_after parameter"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
||||||
|
|
||||||
|
def test_webhook_entries_url_encoding() -> None:
|
||||||
|
"""Test webhook_entries endpoint handles URL encoding correctly."""
|
||||||
|
# Clean up and create webhook
|
||||||
|
client.post(url="/delete_webhook", data={"webhook_url": webhook_url})
|
||||||
|
response: Response = client.post(
|
||||||
|
url="/add_webhook",
|
||||||
|
data={"webhook_name": webhook_name, "webhook_url": webhook_url},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add webhook: {response.text}"
|
||||||
|
|
||||||
|
# Remove and add the feed
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
response = client.post(
|
||||||
|
url="/add",
|
||||||
|
data={"feed_url": feed_url, "webhook_dropdown": webhook_name},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200, f"Failed to add feed: {response.text}"
|
||||||
|
|
||||||
|
# Get webhook_entries with URL-encoded webhook URL
|
||||||
|
encoded_webhook_url = urllib.parse.quote(webhook_url)
|
||||||
|
response = client.get(
|
||||||
|
url="/webhook_entries",
|
||||||
|
params={"webhook_url": encoded_webhook_url},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200, f"Failed to get /webhook_entries with encoded URL: {response.text}"
|
||||||
|
assert webhook_name in response.text, "Webhook name not found in response"
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
client.post(url="/remove", data={"feed_url": feed_url})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue