Add support for extensions
Some checks failed
Test and build Docker image / docker (push) Failing after 3s

This commit is contained in:
Joakim Hellsén 2026-07-20 06:02:25 +02:00
commit 793f67db42
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
39 changed files with 3670 additions and 1309 deletions

View file

@ -202,7 +202,7 @@
{% raw %}
{{entry_text}}
{% endraw %}
</code> Same as entry_content if it exists, otherwise entry_summary
</code>{{ entry.content[0].value|discord_markdown if entry and entry.content else (entry.summary|discord_markdown if entry and entry.summary else '') }}
</li>
<li>
<code>
@ -217,18 +217,22 @@
{% raw %}
{{image_1}}
{% endraw %}
</code>First image in the entry if it exists
</li>
</ul>
<ul class="list-inline">
<li>Examples:</li>
<li>
<code>
{% raw %}
{{feed_title}}\n{{entry_content}}
{% endraw %}
</code>
</code>{{ first_image }}
</li>
{% if extension_variables %}
<br />
<li>Extension variables (from enabled extensions):</li>
{% for var_name in extension_variables %}
<li>
<code>{% raw %}{{{% endraw %}{{ var_name }}{% raw %}}}{% endraw %}</code>
{% if var_name in extension_values and extension_values[var_name] %}
{{ extension_values[var_name] }}
{% else %}
(empty)
{% endif %}
</li>
{% endfor %}
{% endif %}
</ul>
{% else %}
Something went wrong, there was no entry found. If this feed has entries and you still see this

View file

@ -195,7 +195,7 @@
{% raw %}
{{entry_text}}
{% endraw %}
</code> Same as entry_content if it exists, otherwise entry_summary
</code>{{ entry.content[0].value|discord_markdown if entry and entry.content else (entry.summary|discord_markdown if entry and entry.summary else '') }}
</li>
<li>
<code>
@ -210,13 +210,27 @@
{% raw %}
{{image_1}}
{% endraw %}
</code>First image in the entry if it exists
</li>
</ul>
{% else %}
Something went wrong, there was no entry found. If this feed has entries and you still see this
message, please contact the developer.
{% endif %}
</code>{{ first_image }}
</li>
{% if extension_variables %}
<br />
<li>Extension variables (from enabled extensions):</li>
{% for var_name in extension_variables %}
<li>
<code>{% raw %}{{{% endraw %}{{ var_name }}{% raw %}}}{% endraw %}</code>
{% if var_name in extension_values and extension_values[var_name] %}
{{ extension_values[var_name] }}
{% else %}
(empty)
{% endif %}
</li>
{% endfor %}
{% endif %}
</ul>
{% else %}
Something went wrong, there was no entry found. If this feed has entries and you still see this
message, please contact the developer.
{% endif %}
</div>
<div class="form-text">
<ul class="list-inline">
@ -251,17 +265,16 @@
<label for="thumbnail_url" class="col-sm-6 col-form-label">Thumbnail</label>
<input name="thumbnail_url" type="text" class="form-control bg-dark border-dark text-muted"
id="thumbnail_url" {% if thumbnail_url %} value="{{- thumbnail_url -}}" {% endif %} />
{% if is_steam_feed %}
<div class="form-check mt-2">
<input
class="form-check-input"
type="checkbox"
id="show_steam_game_icon_in_thumbnail"
name="show_steam_game_icon_in_thumbnail"
value="true"
{% if show_steam_game_icon_in_thumbnail %}checked{% endif %}
/>
<label class="form-check-label" for="show_steam_game_icon_in_thumbnail"> Use Steam game icon as thumbnail when available </label>
<input class="form-check-input" type="checkbox" name="show_steam_game_icon_in_thumbnail"
id="show_steam_game_icon_in_thumbnail" value="true"
{% if show_steam_game_icon_in_thumbnail %}checked{% endif %}>
<label class="form-check-label" for="show_steam_game_icon_in_thumbnail">
Use Steam game icon as thumbnail override
</label>
</div>
{% endif %}
<label for="footer_text" class="col-sm-6 col-form-label">Footer text</label>
<input name="footer_text" type="text" class="form-control bg-dark border-dark text-muted"
id="footer_text" {% if footer_text %} value="{{- footer_text -}}" {% endif %} />

View file

@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}
Extensions: {{ feed.title if feed.title else feed.url }} | discord-rss-bot
{% endblock title %}
{% block description %}
Enable or disable content extension plugins for {{ feed.title if feed.title else feed.url }}.
{% endblock description %}
{% block content %}
<div class="p-2 border border-dark">
<form action="/extensions" method="post">
<div class="row pb-2">
<div class="col-sm-12">
<div class="form-text">
<ul class="list-inline">
<li>Extensions extract additional variables from feed entries that you can use in your message template or embed.</li>
<li>Each extension creates a template variable like <code>{% raw %}{{ extension_variable_name }}{% endraw %}</code>.</li>
<li>Enable the ones you need and use their variables in the <a href="/custom?feed_url={{ feed.url|encode_url }}">message template</a> or <a href="/embed?feed_url={{ feed.url|encode_url }}">embed settings</a>.</li>
</ul>
</div>
{% if not discovered_extensions %}
<div class="alert alert-info mt-3" role="alert">
<p class="mb-0">
No extension plugins were found.
Place <code>.py</code> files containing <code>FeedExtension</code> subclasses
in the extensions directory
(<code>{{ extensions_dir }}</code>).
</p>
</div>
{% else %}
<div class="mt-3">
<p class="text-muted small mb-2">
Discovered extensions in <code>{{ extensions_dir }}</code>:
</p>
<div class="list-group list-group-flush">
{% for name, cls in discovered_extensions|dictsort %}
<div class="list-group-item bg-transparent border-secondary text-light px-0">
<div class="d-flex align-items-center gap-2">
<input class="form-check-input"
type="checkbox"
id="ext-{{ name }}"
name="enabled_extensions"
value="{{ name }}"
{% if name in enabled_extensions %}checked{% endif %} />
<label class="form-check-label flex-grow-1" for="ext-{{ name }}">
<code>{{ name }}</code>
{% if cls.description %}
<br />
<span class="text-muted small">{{ cls.description }}</span>
{% endif %}
</label>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
<!-- Add a hidden feed_url field to the form -->
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<!-- Submit button -->
<div class="d-md-flex mt-3">
<button class="btn btn-dark btn-sm"
{% if not discovered_extensions %}disabled{% endif %}>Save extensions</button>
</div>
</form>
</div>
{% endblock content %}

View file

@ -14,7 +14,7 @@
<div class="feed-page__content">
<h2 class="h3 mb-1">
<a class="text-muted text-decoration-none feed-page__wrap"
href="{{ feed.url }}">{{ feed.title }}</a>
href="{{ feed.url }}">{{ feed | feed_display_name }}</a>
</h2>
<p class="text-muted mb-0">{{ total_entries }} entries</p>
</div>
@ -30,18 +30,16 @@
Text
{% endif %}
</span>
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
<span class="badge status-chip bg-secondary">
Images:
{% if media_gallery_image_limit == 0 %}
No images
{% elif media_gallery_image_limit == 1 %}
First image only
{% else %}
Up to {{ media_gallery_image_limit }} images
{% endif %}
</span>
{% endif %}
<span class="badge status-chip bg-secondary">
Images:
{% if media_gallery_image_limit == 0 %}
No images
{% elif media_gallery_image_limit == 1 %}
First image only
{% else %}
Up to {{ media_gallery_image_limit }} images
{% endif %}
</span>
{% if delivery_mode == "screenshot" %}
<span class="badge status-chip bg-secondary">
Screenshot layout:
@ -177,10 +175,8 @@
<br>
Embed: a Discord embed with title, description, and images.
<br>
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
Screenshot: full-page screenshot of the entry link.
<br>
{% endif %}
Screenshot: full-page screenshot of the entry link.
<br>
Text: plain message.
</p>
<div class="d-flex" role="group" aria-label="Delivery mode">
@ -197,23 +193,21 @@
style="border-top-right-radius: 0;
border-bottom-right-radius: 0">Embed</span>
{% endif %}
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
{% if chromium_installed %}
{% if delivery_mode != "screenshot" %}
<form action="/use_screenshot" method="post" class="d-inline">
<button class="btn btn-outline-light btn-sm rounded-0"
name="feed_url"
value="{{ feed.url }}">Screenshot</button>
</form>
{% else %}
<span class="btn btn-primary btn-sm disabled rounded-0">Screenshot</span>
{% endif %}
{% if chromium_installed %}
{% if delivery_mode != "screenshot" %}
<form action="/use_screenshot" method="post" class="d-inline">
<button class="btn btn-outline-light btn-sm rounded-0"
name="feed_url"
value="{{ feed.url }}">Screenshot</button>
</form>
{% else %}
<span class="btn btn-outline-light btn-sm rounded-0"
title="Playwright Chromium is not installed. Run 'uv run playwright install chromium' to enable."
style="opacity: 0.5;
cursor: not-allowed">Screenshot</span>
<span class="btn btn-primary btn-sm disabled rounded-0">Screenshot</span>
{% endif %}
{% else %}
<span class="btn btn-outline-light btn-sm rounded-0"
title="Playwright Chromium is not installed. Run 'uv run playwright install chromium' to enable."
style="opacity: 0.5;
cursor: not-allowed">Screenshot</span>
{% endif %}
{% if delivery_mode != "text" %}
<form action="/use_text" method="post" class="d-inline">
@ -229,14 +223,14 @@
border-bottom-left-radius: 0">Text</span>
{% endif %}
</div>
{% if not chromium_installed and not "youtube.com/feeds/videos.xml" in feed.url %}
{% if not chromium_installed %}
<div class="mt-2 small text-muted">
Screenshot mode requires Chromium for Playwright.
Run <code class="text-light">uv run playwright install chromium</code> to enable it.
</div>
{% endif %}
</section>
{% if delivery_mode == "screenshot" and chromium_installed and not "youtube.com/feeds/videos.xml" in feed.url %}
{% if delivery_mode == "screenshot" and chromium_installed %}
<hr class="border-secondary" />
<section class="mb-3">
<h4 class="h6 text-muted mb-2">Screenshot Layout</h4>
@ -259,54 +253,52 @@
</div>
</section>
{% endif %}
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
<hr class="border-secondary" />
<section class="mb-3">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
<h4 class="h6 text-muted mb-0">Image Delivery</h4>
<span class="badge {{ 'bg-info' if media_gallery_image_limit < max_media_gallery_items else 'bg-secondary' }}">
{% if media_gallery_image_limit == 0 %}
No images
{% elif media_gallery_image_limit == 1 %}
First image only
{% else %}
Up to {{ media_gallery_image_limit }} images
{% endif %}
</span>
</div>
<p id="imageDeliveryHelp" class="text-muted small mb-2">
How many images to include per entry. Only applies to embed mode.
</p>
<form action="/set_feed_media_gallery_image_limit"
method="post"
class="mb-0">
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<label class="form-label small text-muted mb-2" for="image_limit">
Images per entry:
<output name="image_limit_value" for="image_limit">{{ media_gallery_image_limit }}</output>
</label>
<div class="d-flex flex-wrap align-items-center gap-3">
<div class="flex-grow-1 image-limit-control">
<input id="image_limit"
class="form-range"
type="range"
name="image_limit"
min="0"
max="{{ max_media_gallery_items }}"
step="1"
value="{{ media_gallery_image_limit }}"
aria-describedby="imageDeliveryHelp"
oninput="this.form.elements.image_limit_value.value = this.value" />
<div class="d-flex justify-content-between text-muted small">
<span>0</span>
<span>{{ max_media_gallery_items }}</span>
</div>
<hr class="border-secondary" />
<section class="mb-3">
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
<h4 class="h6 text-muted mb-0">Image Delivery</h4>
<span class="badge {{ 'bg-info' if media_gallery_image_limit < max_media_gallery_items else 'bg-secondary' }}">
{% if media_gallery_image_limit == 0 %}
No images
{% elif media_gallery_image_limit == 1 %}
First image only
{% else %}
Up to {{ media_gallery_image_limit }} images
{% endif %}
</span>
</div>
<p id="imageDeliveryHelp" class="text-muted small mb-2">
How many images to include per entry. Only applies to embed mode.
</p>
<form action="/set_feed_media_gallery_image_limit"
method="post"
class="mb-0">
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
<label class="form-label small text-muted mb-2" for="image_limit">
Images per entry:
<output name="image_limit_value" for="image_limit">{{ media_gallery_image_limit }}</output>
</label>
<div class="d-flex flex-wrap align-items-center gap-3">
<div class="flex-grow-1 image-limit-control">
<input id="image_limit"
class="form-range"
type="range"
name="image_limit"
min="0"
max="{{ max_media_gallery_items }}"
step="1"
value="{{ media_gallery_image_limit }}"
aria-describedby="imageDeliveryHelp"
oninput="this.form.elements.image_limit_value.value = this.value" />
<div class="d-flex justify-content-between text-muted small">
<span>0</span>
<span>{{ max_media_gallery_items }}</span>
</div>
<button class="btn btn-outline-light btn-sm" type="submit">Save image limit</button>
</div>
</form>
</section>
{% endif %}
<button class="btn btn-outline-light btn-sm" type="submit">Save image limit</button>
</div>
</form>
</section>
<hr class="border-secondary" />
<section>
<div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-2">
@ -489,15 +481,12 @@
<div class="btn-group btn-group-sm" role="group">
<a class="btn {{ 'btn-primary' if delivery_mode == 'text' else 'btn-outline-light' }}"
href="/custom?feed_url={{ feed.url|encode_url }}">Customize message</a>
{% if not "youtube.com/feeds/videos.xml" in feed.url %}
<a class="btn {{ 'btn-primary' if delivery_mode == 'embed' else 'btn-outline-light' }}"
href="/embed?feed_url={{ feed.url|encode_url }}">Customize embed</a>
{% endif %}
<a class="btn {{ 'btn-primary' if delivery_mode == 'embed' else 'btn-outline-light' }}"
href="/embed?feed_url={{ feed.url|encode_url }}">Customize embed</a>
</div>
<a class="btn btn-sm btn-outline-light"
href="/extensions?feed_url={{ feed.url|encode_url }}">Extensions</a>
</div>
{% if is_steam_feed %}
<div class="form-text small mt-2">Steam feeds can enable thumbnails in embed settings.</div>
{% endif %}
</div>
</div>
</div>

View file

@ -73,11 +73,7 @@
{% 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 %}
{{ feed | feed_display_name }}
</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 %}
@ -116,18 +112,7 @@
{% 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 %}
{{ broken_feed | feed_display_name }}
</a>
{% endfor %}
</ul>
@ -142,18 +127,7 @@
<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 %}
{{ feed | feed_display_name }}
</a>
{% if webhooks %}
<form action="/attach_feed_webhook"