Add webhook avatar and username to embed settings
All checks were successful
Test and build Docker image / docker (push) Successful in 1m40s

Allow configuring the webhook identity (name and avatar) directly
from the embed settings page. The embed-level values are applied
before per-feed tag overrides and extension overrides, so they
act as a baseline that can still be overridden by the custom
message page or extensions.
This commit is contained in:
Joakim Hellsén 2026-07-21 06:20:19 +02:00
commit e2c120d99f
Signed by: Joakim Hellsén
SSH key fingerprint: SHA256:/9h/CsExpFp+PRhsfA0xznFx2CGfTT5R/kpuFfUgEQk
5 changed files with 96 additions and 21 deletions

View file

@ -41,6 +41,8 @@ class CustomEmbed:
footer_text: str = ""
footer_icon_url: str = ""
show_steam_game_icon_in_thumbnail: bool = True
avatar_url: str = ""
username: str = ""
def try_to_replace(custom_message: str, template: str, replace_with: str) -> str:
@ -313,7 +315,6 @@ def replace_tags_in_embed(feed: Feed, entry: Entry, reader: Reader) -> CustomEmb
embed.thumbnail_url = embed.thumbnail_url.replace("\\n", "\n")
embed.footer_text = embed.footer_text.replace("\\n", "\n")
embed.footer_icon_url = embed.footer_icon_url.replace("\\n", "\n")
return embed
@ -334,6 +335,8 @@ def _replace_embed_tags(embed: CustomEmbed, template: str, replace_with: str) ->
embed.thumbnail_url = try_to_replace(embed.thumbnail_url, template, replace_with)
embed.footer_text = try_to_replace(embed.footer_text, template, replace_with)
embed.footer_icon_url = try_to_replace(embed.footer_icon_url, template, replace_with)
embed.avatar_url = try_to_replace(embed.avatar_url, template, replace_with)
embed.username = try_to_replace(embed.username, template, replace_with)
def get_custom_message(reader: Reader, feed: Feed) -> str:
@ -461,6 +464,8 @@ def save_embed(reader: Reader, feed: Feed, embed: CustomEmbed) -> None:
"footer_text": embed.footer_text,
"footer_icon_url": embed.footer_icon_url,
"show_steam_game_icon_in_thumbnail": embed.show_steam_game_icon_in_thumbnail,
"avatar_url": embed.avatar_url,
"username": embed.username,
}
reader.set_tag(feed, "embed", json.dumps(embed_dict)) # pyright: ignore[reportArgumentType]
@ -495,6 +500,8 @@ def get_embed(reader: Reader, feed: Feed) -> CustomEmbed:
footer_text="",
footer_icon_url="",
show_steam_game_icon_in_thumbnail=True,
avatar_url="",
username="",
)
@ -521,6 +528,8 @@ def get_embed_data(embed_data: dict[str, str | int | bool]) -> CustomEmbed:
"show_steam_game_icon_in_thumbnail",
True,
)
avatar_url: str = str(embed_data.get("avatar_url", ""))
username: str = str(embed_data.get("username", ""))
return CustomEmbed(
title=title,
@ -534,4 +543,6 @@ def get_embed_data(embed_data: dict[str, str | int | bool]) -> CustomEmbed:
footer_text=footer_text,
footer_icon_url=footer_icon_url,
show_steam_game_icon_in_thumbnail=show_steam_game_icon_in_thumbnail,
avatar_url=avatar_url,
username=username,
)

View file

@ -454,13 +454,19 @@ class HoyolabExtension(FeedExtension):
webhook.add_file(file=video_response.content, filename=f"{entry.id}.mp4")
def _apply_author_from_post(self, webhook: DiscordWebhook, post_data: JsonObject) -> None:
"""Set webhook author (username and avatar) from post user data."""
"""Set webhook author (username and avatar) from post user data.
Only sets values that haven't been explicitly configured via
the feed's embed or message template settings.
"""
user: JsonObject = _as_json_object(post_data.get("user"))
author_name: str = str(user.get("nickname", ""))
avatar_url: str = str(user.get("avatar_url", ""))
if author_name:
webhook.avatar_url = avatar_url
webhook.username = author_name
if not webhook.username:
webhook.username = author_name
if not webhook.avatar_url:
webhook.avatar_url = avatar_url
def _apply_structured_content(self, webhook: DiscordWebhook, post_data: JsonObject) -> None:
"""Parse structured content for YouTube embeds and add them to the webhook."""

View file

@ -52,6 +52,7 @@ from discord_rss_bot.custom_message import get_custom_message
from discord_rss_bot.custom_message import get_image_urls
from discord_rss_bot.custom_message import get_validated_message_avatar_url
from discord_rss_bot.custom_message import get_validated_message_username
from discord_rss_bot.custom_message import normalize_message_username
from discord_rss_bot.custom_message import replace_tags_in_embed
from discord_rss_bot.custom_message import replace_tags_in_text_message
from discord_rss_bot.extensions import auto_enable_extensions_for_feed
@ -1754,12 +1755,14 @@ def create_components_v2_webhook(
},
create_media_gallery_component(media_items),
]
return DiscordWebhook(
webhook: DiscordWebhook = DiscordWebhook(
url=webhook_url,
flags=1 << 15,
components=components,
rate_limit_retry=True,
)
_apply_embed_identity(webhook, custom_embed)
return webhook
def create_embed_webhook( # ruff:ignore[complex-structure, too-many-branches]
@ -1851,10 +1854,25 @@ def create_embed_webhook( # ruff:ignore[complex-structure, too-many-branches]
if custom_embed.footer_icon_url and not custom_embed.footer_text:
discord_embed.set_footer(text="-", icon_url=custom_embed.footer_icon_url)
_apply_embed_identity(webhook, custom_embed)
webhook.add_embed(discord_embed)
return webhook
def _apply_embed_identity(webhook: DiscordWebhook, custom_embed: CustomEmbed) -> None:
"""Apply the embed's avatar_url and username to the webhook.
Sets the webhook-level avatar and display name from the custom embed
configuration, if valid. These can be overridden later by per-feed
``message_avatar_url`` / ``message_username`` tags.
"""
if custom_embed.avatar_url and is_url_valid(custom_embed.avatar_url):
webhook.avatar_url = custom_embed.avatar_url
embed_username: str = normalize_message_username(custom_embed.username)
if embed_username:
webhook.username = embed_username
def get_webhook_url(reader: Reader, entry: Entry) -> str:
"""Get the webhook URL for the entry.

View file

@ -1506,6 +1506,8 @@ async def get_embed_page(
"thumbnail_url": embed.thumbnail_url,
"footer_text": embed.footer_text,
"footer_icon_url": embed.footer_icon_url,
"avatar_url": embed.avatar_url,
"username": embed.username,
"show_steam_game_icon_in_thumbnail": embed.show_steam_game_icon_in_thumbnail,
"is_steam_feed": is_steam_feed_url(feed.url or ""),
"extension_variables": extension_variables,
@ -1520,7 +1522,7 @@ async def get_embed_page(
@app.post("/embed", response_class=HTMLResponse)
async def post_embed( # ruff:ignore[complex-structure]
async def post_embed( # ruff:ignore[complex-structure, too-many-branches]
feed_url: Annotated[str, Form()],
reader: Annotated[Reader, Depends(get_reader_dependency)],
title: Annotated[str, Form()] = "",
@ -1534,6 +1536,8 @@ async def post_embed( # ruff:ignore[complex-structure]
footer_text: Annotated[str, Form()] = "",
footer_icon_url: Annotated[str, Form()] = "",
show_steam_game_icon_in_thumbnail: Annotated[str, Form()] = "",
avatar_url: Annotated[str, Form()] = "",
username: Annotated[str, Form()] = "",
) -> RedirectResponse:
"""Set the embed settings.
@ -1550,6 +1554,8 @@ async def post_embed( # ruff:ignore[complex-structure]
footer_text: The footer text of the embed.
footer_icon_url: The footer icon url of the embed.
show_steam_game_icon_in_thumbnail: Whether to use Steam game icon as thumbnail.
avatar_url: Optional webhook avatar URL override for embed messages.
username: Optional webhook display name override for embed messages.
reader: The Reader instance.
Returns:
@ -1580,6 +1586,10 @@ async def post_embed( # ruff:ignore[complex-structure]
custom_embed.footer_text = footer_text
if footer_icon_url != custom_embed.footer_icon_url:
custom_embed.footer_icon_url = footer_icon_url
if avatar_url != custom_embed.avatar_url:
custom_embed.avatar_url = avatar_url
if username != custom_embed.username:
custom_embed.username = username
# Handle the steam thumbnail toggle (checkbox: "true" when checked, "" when unchecked).
new_steam_icon_toggle: bool = show_steam_game_icon_in_thumbnail.strip().lower() in {"true", "on", "1"}

View file

@ -238,6 +238,8 @@
<li>You can remove the embed from links by adding &lt; and &gt; around the link. (For example &lt;{% raw %}{{entry_link}}{% endraw %}&gt;)</li>
</ul>
</div>
<hr class="border-secondary my-3" />
<h5>Embed content</h5>
<label for="title" class="col-sm-6 col-form-label">Title</label>
<input name="title" type="text" class="form-control bg-dark border-dark text-muted" id="title"
{% if title %} value="{{- title -}}" {% endif %} />
@ -247,15 +249,6 @@
<label for="color" class="col-sm-6 col-form-label">Embed color</label>
<input name="color" type="color" class="form-control form-control-color bg-dark border-dark text-muted"
id="color" {% if color %} value="{{- color -}}" {% endif %} />
<label for="author_name" class="col-sm-6 col-form-label">Author name</label>
<input name="author_name" type="text" class="form-control bg-dark border-dark text-muted"
id="author_name" {% if author_name %} value="{{- author_name -}}" {% endif %} />
<label for="author_url" class="col-sm-6 col-form-label">Author URL</label>
<input name="author_url" type="text" class="form-control bg-dark border-dark text-muted" id="author_url"
{% if author_url %} value="{{- author_url -}}" {% endif %} />
<label for="author_icon_url" class="col-sm-6 col-form-label">Author icon URL</label>
<input name="author_icon_url" type="text" class="form-control bg-dark border-dark text-muted"
id="author_icon_url" {% if author_icon_url %} value="{{- author_icon_url -}}" {% endif %} />
<label for="image_url" class="col-sm-6 col-form-label">Image URL</label>
<input name="image_url" type="text" class="form-control bg-dark border-dark text-muted" id="image_url"
{% if image_url %} value="{{- image_url -}}" {% endif %} />
@ -265,7 +258,31 @@
<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 %} />
<hr class="border-secondary my-3" />
<h5>Author</h5>
<label for="author_name" class="col-sm-6 col-form-label">Author name</label>
<input name="author_name" type="text" class="form-control bg-dark border-dark text-muted"
id="author_name" {% if author_name %} value="{{- author_name -}}" {% endif %} />
<label for="author_url" class="col-sm-6 col-form-label">Author URL</label>
<input name="author_url" type="text" class="form-control bg-dark border-dark text-muted" id="author_url"
{% if author_url %} value="{{- author_url -}}" {% endif %} />
<label for="author_icon_url" class="col-sm-6 col-form-label">Author icon URL</label>
<input name="author_icon_url" type="text" class="form-control bg-dark border-dark text-muted"
id="author_icon_url" {% if author_icon_url %} value="{{- author_icon_url -}}" {% endif %} />
<hr class="border-secondary my-3" />
<h5>Footer</h5>
<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 %} />
<label for="footer_icon_url" class="col-sm-6 col-form-label">Footer icon</label>
<input name="footer_icon_url" type="text" class="form-control bg-dark border-dark text-muted"
id="footer_icon_url" {% if footer_icon_url %} value="{{- footer_icon_url -}}" {% endif %} />
{% if is_steam_feed %}
<hr class="border-secondary my-3" />
<h5>Options</h5>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" name="show_steam_game_icon_in_thumbnail"
id="show_steam_game_icon_in_thumbnail" value="true"
@ -275,12 +292,25 @@
</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 %} />
<label for="footer_icon_url" class="col-sm-6 col-form-label">Footer icon</label>
<input name="footer_icon_url" type="text" class="form-control bg-dark border-dark text-muted"
id="footer_icon_url" {% if footer_icon_url %} value="{{- footer_icon_url -}}" {% endif %} />
<hr class="border-secondary my-3" />
<h5>Webhook identity</h5>
<div class="form-text">
<ul class="list-inline">
<li>These override the webhook's display name and avatar (profile picture) for messages from this feed.</li>
<li>Leave blank to use the webhook's default name and avatar.</li>
<li>Username rules: 180 characters; cannot contain <code>@</code>, <code>#</code>, <code>:</code>, or <code>`</code>; cannot contain <code>clyde</code> or <code>discord</code>.</li>
<li>Avatar must be a full <code>http://</code> or <code>https://</code> image URL.</li>
</ul>
</div>
<label for="username" class="col-sm-6 col-form-label">Webhook name</label>
<input name="username" type="text" class="form-control bg-dark border-dark text-muted"
id="username" maxlength="80" placeholder="e.g. My Feed Bot"
{% if username %} value="{{- username -}}" {% endif %} />
<label for="avatar_url" class="col-sm-6 col-form-label">Webhook avatar</label>
<input name="avatar_url" type="text" class="form-control bg-dark border-dark text-muted"
id="avatar_url" placeholder="e.g. https://example.com/avatar.png or {% raw %}{{hoyolab_author_avatar_url}}{% endraw %}"
{% if avatar_url %} value="{{- avatar_url -}}" {% endif %} />
</div>
</div>
<!-- Add a hidden feed_url field to the form -->