Add options for custom Username & Avatar Image
This commit is contained in:
parent
fc50aed740
commit
f3989eefa9
5 changed files with 161 additions and 11 deletions
|
|
@ -25,6 +25,11 @@ logger: logging.Logger = logging.getLogger(__name__)
|
|||
|
||||
DISCORD_TIMESTAMP_TAG_RE: re.Pattern[str] = re.compile(r"<t:\d+(?::[tTdDfFrRsS])?>")
|
||||
|
||||
# Discord webhook username: nickname rules, max 80 chars; no "clyde"/"discord" substrings.
|
||||
DISCORD_WEBHOOK_USERNAME_MAX_LENGTH: int = 80
|
||||
DISCORD_WEBHOOK_USERNAME_FORBIDDEN_CHARS: frozenset[str] = frozenset("@#:`")
|
||||
DISCORD_WEBHOOK_USERNAME_FORBIDDEN_SUBSTRINGS: tuple[str, ...] = ("clyde", "discord")
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class CustomEmbed:
|
||||
|
|
@ -391,6 +396,93 @@ def get_custom_message(reader: Reader, feed: Feed) -> str:
|
|||
return custom_message
|
||||
|
||||
|
||||
def get_message_username(reader: Reader, feed: Feed) -> str:
|
||||
"""Get the stored custom webhook username for a feed.
|
||||
|
||||
Returns:
|
||||
Stored username (may be empty or invalid for Discord).
|
||||
"""
|
||||
try:
|
||||
return str(reader.get_tag(feed, "message_username", ""))
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
def get_message_avatar_url(reader: Reader, feed: Feed) -> str:
|
||||
"""Get the stored custom webhook avatar URL for a feed.
|
||||
|
||||
Returns:
|
||||
Stored avatar URL (may be empty or invalid for Discord).
|
||||
"""
|
||||
try:
|
||||
return str(reader.get_tag(feed, "message_avatar_url", ""))
|
||||
except ValueError:
|
||||
return ""
|
||||
|
||||
|
||||
def normalize_message_username(username: str | None) -> str:
|
||||
"""Return a Discord-safe webhook username, or empty string if unusable.
|
||||
|
||||
Blank or invalid values are rejected so Discord uses the webhook default.
|
||||
|
||||
Returns:
|
||||
Valid username, or empty string when the override should not be sent.
|
||||
"""
|
||||
if not username:
|
||||
return ""
|
||||
|
||||
cleaned: str = username.strip()
|
||||
if not cleaned:
|
||||
return ""
|
||||
if len(cleaned) > DISCORD_WEBHOOK_USERNAME_MAX_LENGTH:
|
||||
return ""
|
||||
if any(character in cleaned for character in DISCORD_WEBHOOK_USERNAME_FORBIDDEN_CHARS):
|
||||
return ""
|
||||
lowered: str = cleaned.lower()
|
||||
if any(forbidden in lowered for forbidden in DISCORD_WEBHOOK_USERNAME_FORBIDDEN_SUBSTRINGS):
|
||||
return ""
|
||||
return cleaned
|
||||
|
||||
|
||||
def normalize_message_avatar_url(avatar_url: str | None) -> str:
|
||||
"""Return a usable webhook avatar URL, or empty string if unusable.
|
||||
|
||||
Blank or invalid values are rejected so Discord uses the webhook default.
|
||||
|
||||
Returns:
|
||||
Valid http(s) URL, or empty string when the override should not be sent.
|
||||
"""
|
||||
if not avatar_url:
|
||||
return ""
|
||||
|
||||
cleaned: str = avatar_url.strip()
|
||||
if not cleaned:
|
||||
return ""
|
||||
if not cleaned.lower().startswith(("http://", "https://")):
|
||||
return ""
|
||||
if not is_url_valid(cleaned):
|
||||
return ""
|
||||
return cleaned
|
||||
|
||||
|
||||
def get_validated_message_username(reader: Reader, feed: Feed) -> str:
|
||||
"""Get a Discord-safe custom webhook username for a feed, if configured.
|
||||
|
||||
Returns:
|
||||
Valid username to send, or empty string to use the webhook default.
|
||||
"""
|
||||
return normalize_message_username(get_message_username(reader, feed))
|
||||
|
||||
|
||||
def get_validated_message_avatar_url(reader: Reader, feed: Feed) -> str:
|
||||
"""Get a usable custom webhook avatar URL for a feed, if configured.
|
||||
|
||||
Returns:
|
||||
Valid avatar URL to send, or empty string to use the webhook default.
|
||||
"""
|
||||
return normalize_message_avatar_url(get_message_avatar_url(reader, feed))
|
||||
|
||||
|
||||
def save_embed(reader: Reader, feed: Feed, embed: CustomEmbed) -> None:
|
||||
"""Set embed tag in feed.
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ from requests import RequestException
|
|||
from discord_rss_bot.custom_message import CustomEmbed
|
||||
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 replace_tags_in_embed
|
||||
from discord_rss_bot.custom_message import replace_tags_in_text_message
|
||||
from discord_rss_bot.filter.evaluator import get_entry_filter_decision_from_reader
|
||||
|
|
@ -644,6 +646,10 @@ def get_webhook_message_edit_payload(payload: JsonObject, record: SentWebhookRec
|
|||
if edit_payload.get("attachments") == []:
|
||||
edit_payload.pop("attachments", None)
|
||||
|
||||
# Username/avatar can only be set when creating a message, not when editing.
|
||||
edit_payload.pop("username", None)
|
||||
edit_payload.pop("avatar_url", None)
|
||||
|
||||
return edit_payload
|
||||
|
||||
|
||||
|
|
@ -962,6 +968,21 @@ def edit_sent_webhook_message(
|
|||
)
|
||||
|
||||
|
||||
def apply_feed_webhook_identity(webhook: DiscordWebhook, entry: Entry, reader: Reader) -> DiscordWebhook:
|
||||
"""Apply per-feed custom username and avatar when valid; ignore blank/invalid values.
|
||||
|
||||
Returns:
|
||||
The same webhook instance with optional identity overrides.
|
||||
"""
|
||||
username: str = get_validated_message_username(reader, entry.feed)
|
||||
avatar_url: str = get_validated_message_avatar_url(reader, entry.feed)
|
||||
if username:
|
||||
webhook.username = username
|
||||
if avatar_url:
|
||||
webhook.avatar_url = avatar_url
|
||||
return webhook
|
||||
|
||||
|
||||
def create_webhook_for_entry(
|
||||
webhook_url: str,
|
||||
entry: Entry,
|
||||
|
|
@ -983,7 +1004,8 @@ def create_webhook_for_entry(
|
|||
if post_id:
|
||||
post_data = fetch_hoyolab_post(post_id)
|
||||
if post_data:
|
||||
return create_hoyolab_webhook(webhook_url, entry, post_data), delivery_mode
|
||||
webhook = create_hoyolab_webhook(webhook_url, entry, post_data)
|
||||
return apply_feed_webhook_identity(webhook, entry, reader), delivery_mode
|
||||
logger.warning(
|
||||
"Failed to create Hoyolab webhook for feed %s, falling back to regular processing",
|
||||
entry.feed.url,
|
||||
|
|
@ -992,18 +1014,18 @@ def create_webhook_for_entry(
|
|||
logger.warning("No entry link found for feed %s, falling back to regular processing", entry.feed.url)
|
||||
|
||||
if delivery_mode == "embed":
|
||||
return create_embed_webhook(webhook_url, entry, reader=reader), delivery_mode
|
||||
webhook = create_embed_webhook(webhook_url, entry, reader=reader)
|
||||
return apply_feed_webhook_identity(webhook, entry, reader), delivery_mode
|
||||
if delivery_mode == "screenshot":
|
||||
return create_screenshot_webhook(webhook_url, entry, reader=reader), delivery_mode
|
||||
return (
|
||||
create_text_webhook(
|
||||
webhook_url,
|
||||
entry,
|
||||
reader=reader,
|
||||
use_default_message_on_empty=use_default_message_on_empty,
|
||||
),
|
||||
delivery_mode,
|
||||
webhook = create_screenshot_webhook(webhook_url, entry, reader=reader)
|
||||
return apply_feed_webhook_identity(webhook, entry, reader), delivery_mode
|
||||
webhook = create_text_webhook(
|
||||
webhook_url,
|
||||
entry,
|
||||
reader=reader,
|
||||
use_default_message_on_empty=use_default_message_on_empty,
|
||||
)
|
||||
return apply_feed_webhook_identity(webhook, entry, reader), delivery_mode
|
||||
|
||||
|
||||
def collect_modified_entries_during_update(reader: Reader, update_callback: UpdateCallback) -> list[tuple[str, str]]:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ type TagValue = JsonValue
|
|||
_FEED_TAGS: tuple[str, ...] = (
|
||||
"webhook",
|
||||
"custom_message",
|
||||
"message_username",
|
||||
"message_avatar_url",
|
||||
"delivery_mode",
|
||||
"screenshot_layout",
|
||||
"should_send_embed",
|
||||
|
|
|
|||
|
|
@ -49,6 +49,8 @@ from discord_rss_bot.custom_message import CustomEmbed
|
|||
from discord_rss_bot.custom_message import get_custom_message
|
||||
from discord_rss_bot.custom_message import get_embed
|
||||
from discord_rss_bot.custom_message import get_first_image
|
||||
from discord_rss_bot.custom_message import get_message_avatar_url
|
||||
from discord_rss_bot.custom_message import get_message_username
|
||||
from discord_rss_bot.custom_message import replace_tags_in_text_message
|
||||
from discord_rss_bot.custom_message import save_embed
|
||||
from discord_rss_bot.feeds import FeedUpdateError
|
||||
|
|
@ -1145,11 +1147,15 @@ async def post_set_custom(
|
|||
feed_url: Annotated[str, Form()],
|
||||
reader: Annotated[Reader, Depends(get_reader_dependency)],
|
||||
custom_message: Annotated[str, Form()] = "",
|
||||
message_username: Annotated[str, Form()] = "",
|
||||
message_avatar_url: Annotated[str, Form()] = "",
|
||||
) -> RedirectResponse:
|
||||
"""Set the custom message, this is used when sending the message.
|
||||
|
||||
Args:
|
||||
custom_message: The custom message.
|
||||
message_username: Optional Discord webhook username override for this feed.
|
||||
message_avatar_url: Optional Discord webhook avatar URL override for this feed.
|
||||
feed_url: The feed we should set the custom message for.
|
||||
reader: The Reader instance.
|
||||
|
||||
|
|
@ -1159,6 +1165,10 @@ async def post_set_custom(
|
|||
our_custom_message: JSONType | str = custom_message.strip()
|
||||
our_custom_message = typing.cast("JSONType", our_custom_message)
|
||||
|
||||
# Store raw values; blank/invalid values are ignored when building the Discord payload.
|
||||
reader.set_tag(feed_url, "message_username", typing.cast("JSONType", message_username.strip()))
|
||||
reader.set_tag(feed_url, "message_avatar_url", typing.cast("JSONType", message_avatar_url.strip()))
|
||||
|
||||
clean_feed_url: str = feed_url.strip()
|
||||
feed: Feed = reader.get_feed(urllib.parse.unquote(clean_feed_url))
|
||||
|
||||
|
|
@ -1192,6 +1202,8 @@ async def get_custom(
|
|||
"request": request,
|
||||
"feed": feed,
|
||||
"custom_message": get_custom_message(reader, feed),
|
||||
"message_username": get_message_username(reader, feed),
|
||||
"message_avatar_url": get_message_avatar_url(reader, feed),
|
||||
}
|
||||
|
||||
# Get the first entry, this is used to show the user what the custom message will look like.
|
||||
|
|
|
|||
|
|
@ -240,6 +240,28 @@
|
|||
id="custom_message" {% if custom_message %} value="{{- custom_message -}}" {% endif %} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Optional Discord webhook username & avatar for this feed -->
|
||||
<div class="row pb-2">
|
||||
<div class="col-sm-12">
|
||||
<div class="form-text">
|
||||
<ul class="list-inline">
|
||||
<li>Optional: override the Discord webhook username and avatar for messages from this feed.</li>
|
||||
<li>Leave blank to use the webhook's default name and avatar.</li>
|
||||
<li>Invalid values (empty, disallowed characters, or bad avatar URLs) are ignored when sending.</li>
|
||||
<li>Username rules: 1–80 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="message_username" class="col-sm-6 col-form-label">Message Username</label>
|
||||
<input name="message_username" type="text" class="form-control bg-dark border-dark text-muted"
|
||||
id="message_username" maxlength="80" placeholder="e.g. Power Of The Shell"
|
||||
{% if message_username %} value="{{- message_username -}}" {% endif %} />
|
||||
<label for="message_avatar_url" class="col-sm-6 col-form-label">Message Avatar Image</label>
|
||||
<input name="message_avatar_url" type="url" class="form-control bg-dark border-dark text-muted"
|
||||
id="message_avatar_url" placeholder="e.g. https://example.com/icon.png"
|
||||
{% if message_avatar_url %} value="{{- message_avatar_url -}}" {% endif %} />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Add a hidden feed_url field to the form -->
|
||||
<input type="hidden" name="feed_url" value="{{ feed.url }}" />
|
||||
<!-- Submit button -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue