diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ed381e6..d0f44c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Test code on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] workflow_dispatch: jobs: @@ -16,7 +16,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: "3.11" - cache: 'poetry' + cache: "poetry" - run: poetry install - run: poetry run pytest env: @@ -41,4 +41,4 @@ jobs: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} diff --git a/discord_rss_bot/custom_filters.py b/discord_rss_bot/custom_filters.py index 38dc078..7b1f19c 100644 --- a/discord_rss_bot/custom_filters.py +++ b/discord_rss_bot/custom_filters.py @@ -11,7 +11,7 @@ from discord_rss_bot.settings import get_reader reader: Reader = get_reader() -@lru_cache() +@lru_cache def encode_url(url_to_quote: str) -> str: """%-escape the URL so it can be used in a URL. @@ -28,8 +28,7 @@ def encode_url(url_to_quote: str) -> str: def entry_is_whitelisted(entry_to_check: Entry) -> bool: - """ - Check if the entry is whitelisted. + """Check if the entry is whitelisted. Args: entry_to_check: The feed to check. @@ -42,8 +41,7 @@ def entry_is_whitelisted(entry_to_check: Entry) -> bool: def entry_is_blacklisted(entry_to_check: Entry) -> bool: - """ - Check if the entry is blacklisted. + """Check if the entry is blacklisted. Args: entry_to_check: The feed to check. diff --git a/discord_rss_bot/custom_message.py b/discord_rss_bot/custom_message.py index da80949..010425d 100644 --- a/discord_rss_bot/custom_message.py +++ b/discord_rss_bot/custom_message.py @@ -22,31 +22,6 @@ class CustomEmbed: footer_icon_url: str -def return_image(found_images) -> list[tuple[str, str]] | None: - soup: BeautifulSoup = BeautifulSoup(found_images, features="lxml") - images = soup.find_all("img") - for image in images: - image_src: str = str(image["src"]) or "" - image_alt: str = "Link to image" - if image.get("alt"): - image_alt = image.get("alt") - return [(image_src, image_alt)] - - -def get_first_image_html(html: str): - """Get images from a entry. - - Args: - html: The HTML to get the images from. - - Returns: - Returns a list of images. - """ - if images := BeautifulSoup(html, features="lxml").find_all("img"): - return images[0].attrs["src"] - return None - - def try_to_replace(custom_message: str, template: str, replace_with: str) -> str: """Try to replace a tag in custom_message. @@ -84,7 +59,7 @@ def replace_tags_in_text_message(entry: Entry) -> str: summary: str = entry.summary or "" - first_image = get_image(summary, content) + first_image = get_first_image(summary, content) summary = convert_html_to_md(summary) content = convert_html_to_md(content) @@ -127,8 +102,8 @@ def replace_tags_in_text_message(entry: Entry) -> str: return custom_message.replace("\\n", "\n") -def get_image(summary, content): - """Get image from summary or content +def get_first_image(summary, content): + """Get image from summary or content. Args: summary: The summary from the entry @@ -137,12 +112,10 @@ def get_image(summary, content): Returns: The first image """ - if content: - if images := BeautifulSoup(content, features="lxml").find_all("img"): - return images[0].attrs["src"] - if summary: - if images := BeautifulSoup(summary, features="lxml").find_all("img"): - return images[0].attrs["src"] + if content and (images := BeautifulSoup(content, features="lxml").find_all("img")): + return images[0].attrs["src"] + if summary and (images := BeautifulSoup(summary, features="lxml").find_all("img")): + return images[0].attrs["src"] return "" @@ -156,7 +129,6 @@ def replace_tags_in_embed(feed: Feed, entry: Entry) -> CustomEmbed: Returns: Returns the embed with the tags replaced. """ - custom_reader: Reader = get_reader() embed: CustomEmbed = get_embed(feed=feed, custom_reader=custom_reader) @@ -167,7 +139,7 @@ def replace_tags_in_embed(feed: Feed, entry: Entry) -> CustomEmbed: summary: str = entry.summary or "" - first_image = get_image(summary, content) + first_image = get_first_image(summary, content) summary = convert_html_to_md(summary) content = convert_html_to_md(content) @@ -274,7 +246,6 @@ def get_embed(custom_reader: Reader, feed: Feed) -> CustomEmbed: Returns: Returns the contents from the embed tag. """ - if embed := custom_reader.get_tag(feed, "embed", ""): if type(embed) != str: return get_embed_data(embed) diff --git a/discord_rss_bot/feeds.py b/discord_rss_bot/feeds.py index 75406f5..bae5edc 100644 --- a/discord_rss_bot/feeds.py +++ b/discord_rss_bot/feeds.py @@ -1,22 +1,29 @@ -from typing import Iterable +from typing import TYPE_CHECKING from discord_webhook import DiscordEmbed, DiscordWebhook from fastapi import HTTPException from reader import Entry, Feed, FeedExistsError, Reader, TagNotFoundError -from requests import Response from discord_rss_bot import custom_message from discord_rss_bot.filter.blacklist import should_be_skipped from discord_rss_bot.filter.whitelist import has_white_tags, should_be_sent from discord_rss_bot.settings import default_custom_message, get_reader +if TYPE_CHECKING: + from collections.abc import Iterable -def send_entry_to_discord(entry: Entry, custom_reader: Reader | None = None): - """ - Send a single entry to Discord. + from requests import Response + + +def send_entry_to_discord(entry: Entry, custom_reader: Reader | None = None) -> str | None: + """Send a single entry to Discord. Args: entry: The entry to send to Discord. + custom_reader: The reader to use. If None, the default reader will be used. + + Returns: + str | None: The error message if there was an error, otherwise None. """ # Get the default reader if we didn't get a custom one. reader: Reader = get_reader() if custom_reader is None else custom_reader @@ -27,7 +34,7 @@ def send_entry_to_discord(entry: Entry, custom_reader: Reader | None = None): return "No webhook URL found." # Try to get the custom message for the feed. If the user has none, we will use the default message. - if custom_message.get_custom_message(reader, entry.feed) != "": + if not custom_message.get_custom_message(reader, entry.feed): webhook_message = custom_message.replace_tags_in_text_message(entry=entry) else: webhook_message: str = default_custom_message @@ -39,11 +46,19 @@ def send_entry_to_discord(entry: Entry, custom_reader: Reader | None = None): webhook: DiscordWebhook = DiscordWebhook(url=webhook_url, content=webhook_message, rate_limit_retry=True) response: Response = webhook.execute() - if not response.ok: - return f"Error sending entry to Discord: {response.text}" + return None if response.ok else f"Error sending entry to Discord: {response.text}" def create_embed_webhook(webhook_url: str, entry: Entry) -> DiscordWebhook: + """Create a webhook with an embed. + + Args: + webhook_url (str): The webhook URL. + entry (Entry): The entry to send to Discord. + + Returns: + DiscordWebhook: The webhook with the embed. + """ webhook: DiscordWebhook = DiscordWebhook(url=webhook_url, rate_limit_retry=True) feed: Feed = entry.feed @@ -66,7 +81,11 @@ def create_embed_webhook(webhook_url: str, entry: Entry) -> DiscordWebhook: if custom_embed.author_name and not custom_embed.author_url and custom_embed.author_icon_url: discord_embed.set_author(name=custom_embed.author_name, icon_url=custom_embed.author_icon_url) if custom_embed.author_name and custom_embed.author_url and custom_embed.author_icon_url: - discord_embed.set_author(name=custom_embed.author_name, url=custom_embed.author_url, icon_url=custom_embed.author_icon_url) # noqa: E501 + discord_embed.set_author( + name=custom_embed.author_name, + url=custom_embed.author_url, + icon_url=custom_embed.author_icon_url, + ) if custom_embed.thumbnail_url: discord_embed.set_thumbnail(url=custom_embed.thumbnail_url) if custom_embed.image_url: @@ -85,8 +104,7 @@ def create_embed_webhook(webhook_url: str, entry: Entry) -> DiscordWebhook: def send_to_discord(custom_reader: Reader | None = None, feed: Feed | None = None, do_once: bool = False) -> None: - """ - Send entries to Discord. + """Send entries to Discord. If response was not ok, we will log the error and mark the entry as unread, so it will be sent again next time. @@ -120,7 +138,7 @@ def send_to_discord(custom_reader: Reader | None = None, feed: Feed | None = Non else: # If the user has set the custom message to an empty string, we will use the default message, otherwise we # will use the custom message. - if custom_message.get_custom_message(reader, entry.feed) != "": + if not custom_message.get_custom_message(reader, entry.feed): webhook_message = custom_message.replace_tags_in_text_message(entry) else: webhook_message: str = default_custom_message diff --git a/discord_rss_bot/filter/__init__.py b/discord_rss_bot/filter/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/discord_rss_bot/filter/blacklist.py b/discord_rss_bot/filter/blacklist.py index 45092ba..f6ef586 100644 --- a/discord_rss_bot/filter/blacklist.py +++ b/discord_rss_bot/filter/blacklist.py @@ -4,11 +4,12 @@ from discord_rss_bot.filter.utils import is_word_in_text def has_black_tags(custom_reader: Reader, feed: Feed) -> bool: - """ - Return True if the feed has any of the following tags: + """Return True if the feed has blacklist tags. + + The following tags are checked: - blacklist_title - blacklist_summary - - blacklist_content + - blacklist_content. Args: custom_reader: The reader. @@ -25,8 +26,7 @@ def has_black_tags(custom_reader: Reader, feed: Feed) -> bool: def should_be_skipped(custom_reader: Reader, entry: Entry) -> bool: - """ - Return True if the entry is in the blacklist. + """Return True if the entry is in the blacklist. Args: custom_reader: The reader. @@ -40,15 +40,17 @@ def should_be_skipped(custom_reader: Reader, entry: Entry) -> bool: blacklist_summary: str = str(custom_reader.get_tag(feed, "blacklist_summary", "")) blacklist_content: str = str(custom_reader.get_tag(feed, "blacklist_content", "")) blacklist_author: str = str(custom_reader.get_tag(feed, "blacklist_author", "")) - # TODO: Also add support for entry_text + # TODO: Also add support for entry_text and more. if entry.title and blacklist_title and is_word_in_text(blacklist_title, entry.title): return True - elif entry.summary and blacklist_summary and is_word_in_text(blacklist_summary, entry.summary): + if entry.summary and blacklist_summary and is_word_in_text(blacklist_summary, entry.summary): return True - elif entry.author and blacklist_author and is_word_in_text(blacklist_author, entry.author): + if entry.author and blacklist_author and is_word_in_text(blacklist_author, entry.author): return True - elif entry.content: - if entry.content[0].value and blacklist_content and is_word_in_text(blacklist_content, entry.content[0].value): - return True - return False + return bool( + entry.content + and entry.content[0].value + and blacklist_content + and is_word_in_text(blacklist_content, entry.content[0].value), + ) diff --git a/discord_rss_bot/filter/utils.py b/discord_rss_bot/filter/utils.py index 8fa621b..cc0d550 100644 --- a/discord_rss_bot/filter/utils.py +++ b/discord_rss_bot/filter/utils.py @@ -2,7 +2,8 @@ import re def is_word_in_text(words: str, text: str) -> bool: - """ + """Check if the word is in the text. + Args: words: The words to search for. text: The text to search in. diff --git a/discord_rss_bot/filter/whitelist.py b/discord_rss_bot/filter/whitelist.py index 03c8f45..eb884f6 100644 --- a/discord_rss_bot/filter/whitelist.py +++ b/discord_rss_bot/filter/whitelist.py @@ -4,11 +4,12 @@ from discord_rss_bot.filter.utils import is_word_in_text def has_white_tags(custom_reader: Reader, feed: Feed) -> bool: - """ - Return True if the feed has any of the following tags: + """Return True if the feed has whitelist tags. + + The following tags are checked: - whitelist_title - whitelist_summary - - whitelist_content + - whitelist_content. Args: custom_reader: The reader. @@ -25,8 +26,7 @@ def has_white_tags(custom_reader: Reader, feed: Feed) -> bool: def should_be_sent(custom_reader: Reader, entry: Entry) -> bool: - """ - Return True if the entry is in the whitelist. + """Return True if the entry is in the whitelist. Args: custom_reader: The reader. @@ -43,11 +43,13 @@ def should_be_sent(custom_reader: Reader, entry: Entry) -> bool: if entry.title and whitelist_title and is_word_in_text(whitelist_title, entry.title): return True - elif entry.summary and whitelist_summary and is_word_in_text(whitelist_summary, entry.summary): + if entry.summary and whitelist_summary and is_word_in_text(whitelist_summary, entry.summary): return True - elif entry.author and whitelist_author and is_word_in_text(whitelist_author, entry.author): + if entry.author and whitelist_author and is_word_in_text(whitelist_author, entry.author): return True - elif entry.content: - if entry.content[0].value and whitelist_content and is_word_in_text(whitelist_content, entry.content[0].value): - return True - return False + return bool( + entry.content + and entry.content[0].value + and whitelist_content + and is_word_in_text(whitelist_content, entry.content[0].value), + ) diff --git a/discord_rss_bot/healthcheck.py b/discord_rss_bot/healthcheck.py index 1ede4d1..b14a2a7 100644 --- a/discord_rss_bot/healthcheck.py +++ b/discord_rss_bot/healthcheck.py @@ -7,14 +7,15 @@ def healthcheck() -> None: """Check if the website is up. sys.exit(0): success - the container is healthy and ready for use. - sys.exit(1): unhealthy - the container is not working correctly.""" + sys.exit(1): unhealthy - the container is not working correctly. + """ # TODO: We should check more than just that the website is up. try: r: requests.Response = requests.get(url="http://localhost:5000", timeout=5) if r.ok: sys.exit(0) except requests.exceptions.RequestException as e: - print(f"Healthcheck failed: {e}", file=sys.stderr) + print(f"Healthcheck failed: {e}", file=sys.stderr) # noqa: T201 sys.exit(1) diff --git a/discord_rss_bot/main.py b/discord_rss_bot/main.py index dcfc6cb..aac510e 100644 --- a/discord_rss_bot/main.py +++ b/discord_rss_bot/main.py @@ -1,9 +1,9 @@ import json import urllib.parse +from collections.abc import Iterable from dataclasses import dataclass -from datetime import datetime +from datetime import datetime, timezone from functools import lru_cache -from typing import Iterable import httpx import uvicorn @@ -22,7 +22,7 @@ from discord_rss_bot.custom_message import ( CustomEmbed, get_custom_message, get_embed, - get_image, + get_first_image, replace_tags_in_text_message, save_embed, ) @@ -47,45 +47,44 @@ templates.env.filters["discord_markdown"] = convert_html_to_md @app.post("/add_webhook") -async def post_add_webhook(webhook_name: str = Form(), webhook_url: str = Form()): - """ - Add a feed to the database. +async def post_add_webhook(webhook_name: str = Form(), webhook_url: str = Form()) -> RedirectResponse: + """Add a feed to the database. Args: webhook_name: The name of the webhook. webhook_url: The url of the webhook. """ - if add_webhook(reader, webhook_name, webhook_url): - return RedirectResponse(url="/", status_code=303) + add_webhook(reader, webhook_name, webhook_url) + return RedirectResponse(url="/", status_code=303) @app.post("/delete_webhook") -async def post_delete_webhook(webhook_url: str = Form()): - """ - Delete a webhook from the database. +async def post_delete_webhook(webhook_url: str = Form()) -> RedirectResponse: + """Delete a webhook from the database. Args: webhook_url: The url of the webhook. """ - if remove_webhook(reader, webhook_url): - return RedirectResponse(url="/", status_code=303) + # TODO: Check if the webhook is in use by any feeds before deleting it. + remove_webhook(reader, webhook_url) + return RedirectResponse(url="/", status_code=303) @app.post("/add") -async def post_create_feed(feed_url: str = Form(), webhook_dropdown: str = Form()): - """ - Add a feed to the database. +async def post_create_feed(feed_url: str = Form(), webhook_dropdown: str = Form()) -> RedirectResponse: + """Add a feed to the database. Args: feed_url: The feed to add. webhook_dropdown: The webhook to use. """ + clean_feed_url: str = feed_url.strip() create_feed(reader, feed_url, webhook_dropdown) - return RedirectResponse(url=f"/feed/?feed_url={feed_url}", status_code=303) + return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) @app.post("/pause") -async def post_pause_feed(feed_url: str = Form()): +async def post_pause_feed(feed_url: str = Form()) -> RedirectResponse: """Pause a feed. Args: @@ -97,7 +96,7 @@ async def post_pause_feed(feed_url: str = Form()): @app.post("/unpause") -async def post_unpause_feed(feed_url: str = Form()): +async def post_unpause_feed(feed_url: str = Form()) -> RedirectResponse: """Unpause a feed. Args: @@ -115,7 +114,7 @@ async def post_set_whitelist( whitelist_content: str = Form(None), whitelist_author: str = Form(None), feed_url: str = Form(), -): +) -> RedirectResponse: """Set what the whitelist should be sent, if you have this set only words in the whitelist will be sent. Args: @@ -127,7 +126,7 @@ async def post_set_whitelist( """ clean_feed_url: str = feed_url.strip() if whitelist_title: - reader.set_tag(clean_feed_url, "whitelist_title", whitelist_title) # type: ignore + reader.set_tag(clean_feed_url, "whitelist_title", [whitelist_title]) if whitelist_summary: reader.set_tag(clean_feed_url, "whitelist_summary", whitelist_summary) # type: ignore if whitelist_content: @@ -172,8 +171,10 @@ async def post_set_blacklist( blacklist_content: str = Form(None), blacklist_author: str = Form(None), feed_url: str = Form(), -): - """Set the blacklist, if this is set we will check if words are in the title, summary or content +) -> RedirectResponse: + """Set the blacklist. + + If this is set we will check if words are in the title, summary or content and then don't send that entry. Args: @@ -193,7 +194,7 @@ async def post_set_blacklist( if blacklist_author: reader.set_tag(clean_feed_url, "blacklist_author", blacklist_author) # type: ignore - return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(feed_url)}", status_code=303) + return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) @app.get("/blacklist", response_class=HTMLResponse) @@ -218,9 +219,8 @@ async def get_blacklist(feed_url: str, request: Request): @app.post("/custom") -async def post_set_custom(custom_message: str = Form(""), feed_url: str = Form()): - """ - Set the custom message, this is used when sending the message. +async def post_set_custom(custom_message: str = Form(""), feed_url: str = Form()) -> RedirectResponse: + """Set the custom message, this is used when sending the message. Args: custom_message: The custom message. @@ -231,7 +231,8 @@ async def post_set_custom(custom_message: str = Form(""), feed_url: str = Form() else: reader.set_tag(feed_url, "custom_message", settings.default_custom_message) # type: ignore - return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(feed_url)}", status_code=303) + clean_feed_url: str = feed_url.strip() + return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) @app.get("/custom", response_class=HTMLResponse) @@ -304,11 +305,25 @@ async def post_embed( author_icon_url: str = Form(""), footer_text: str = Form(""), footer_icon_url: str = Form(""), -): +) -> RedirectResponse: """Set the embed settings. Args: feed_url: What feed we should get the custom message for. + title: The title of the embed. + description: The description of the embed. + color: The color of the embed. + image_url: The image url of the embed. + thumbnail_url: The thumbnail url of the embed. + author_name: The author name of the embed. + author_url: The author url of the embed. + author_icon_url: The author icon url of the embed. + footer_text: The footer text of the embed. + footer_icon_url: The footer icon url of the embed. + + + Returns: + RedirectResponse: Redirect to the embed page. """ clean_feed_url: str = feed_url.strip() feed: Feed = reader.get_feed(urllib.parse.unquote(clean_feed_url)) @@ -328,31 +343,37 @@ async def post_embed( # Save the data. save_embed(reader, feed, custom_embed) - return RedirectResponse(url=f"/feed/?feed_url={clean_feed_url}", status_code=303) + return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) @app.post("/use_embed") -async def post_use_embed(feed_url: str = Form()): +async def post_use_embed(feed_url: str = Form()) -> RedirectResponse: """Use embed instead of text. Args: feed_url: The feed to change. + + Returns: + RedirectResponse: Redirect to the feed page. """ clean_feed_url: str = feed_url.strip() reader.set_tag(clean_feed_url, "should_send_embed", True) # type: ignore - return RedirectResponse(url=f"/feed/?feed_url={clean_feed_url}", status_code=303) + return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) @app.post("/use_text") -async def post_use_text(feed_url: str = Form()): +async def post_use_text(feed_url: str = Form()) -> RedirectResponse: """Use text instead of embed. Args: feed_url: The feed to change. + + Returns: + RedirectResponse: Redirect to the feed page. """ clean_feed_url: str = feed_url.strip() reader.set_tag(clean_feed_url, "should_send_embed", False) # type: ignore - return RedirectResponse(url=f"/feed/?feed_url={clean_feed_url}", status_code=303) + return RedirectResponse(url=f"/feed/?feed_url={urllib.parse.quote(clean_feed_url)}", status_code=303) @app.get("/add", response_class=HTMLResponse) @@ -367,11 +388,14 @@ def get_add(request: Request): @app.get("/feed", response_class=HTMLResponse) async def get_feed(feed_url: str, request: Request): - """ - Get a feed by URL. + """Get a feed by URL. Args: feed_url: The feed to add. + request: The request object. + + Returns: + HTMLResponse: The feed page. """ clean_feed_url: str = urllib.parse.unquote(feed_url.strip()) @@ -404,19 +428,18 @@ def create_html_for_feed(entries: Iterable[Entry]) -> str: """Create HTML for the search results. Args: - search_results: The search results. - custom_reader: The reader. If None, we will get the reader from the settings. + entries: The entries to create HTML for. """ html: str = "" for entry in entries: - first_image = "" + first_image: str = "" summary: str | None = entry.summary content = "" if entry.content: for content_item in entry.content: content: str = content_item.value - first_image = get_image(summary, content) + first_image = get_first_image(summary, content) text: str = replace_tags_in_text_message(entry) or "
{% raw %}
- {{feed_author}}
+ {{ feed_author }}
{% endraw %}
-
{{feed.author}}
+ {{ feed.author }}
{% raw %}
- {{feed_added}}
+ {{ feed_added }}
{% endraw %}
-
{{feed.added}}
+ {{ feed.added }}
{% raw %}
- {{feed_last_exception}}
+ {{ feed_last_exception }}
{% endraw %}
-
{{feed.last_exception}}
+ {{ feed.last_exception }}
{% raw %}
- {{feed_last_updated}}
+ {{ feed_last_updated }}
{% endraw %}
-
{{feed.last_updated}}
+ {{ feed.last_updated }}
{% raw %}
- {{feed_link}}
+ {{ feed_link }}
{% endraw %}
-
{{feed.link}}
+ {{ feed.link }}
{% raw %}
- {{feed_subtitle}}
+ {{ feed_subtitle }}
{% endraw %}
-
{{feed.subtitle}}
+ {{ feed.subtitle }}
{% raw %}
- {{feed_title}}
+ {{ feed_title }}
{% endraw %}
-
{{feed.title}}
+ {{ feed.title }}
{% raw %}
- {{feed_updated}}
+ {{ feed_updated }}
{% endraw %}
-
{{feed.updated}}
+ {{ feed.updated }}
{% raw %}
- {{feed_updates_enabled}}
+ {{ feed_updates_enabled }}
{% endraw %}
-
{{feed.updates_enabled}}
+ {{ feed.updates_enabled }}
{% raw %}
- {{feed_url}}
+ {{ feed_url }}
{% endraw %}
-
{{feed.url}}
+ {{ feed.url }}
{% raw %}
- {{feed_user_title}}
+ {{ feed_user_title }}
{% endraw %}
-
{{feed.user_title}}
+ {{ feed.user_title }}
{% raw %}
- {{feed_version}}
+ {{ feed_version }}
{% endraw %}
-
{{feed.version}}
+ {{ feed.version }}
{% raw %}
- {{entry_added}}
+ {{ entry_added }}
{% endraw %}
-
{{entry.added}}
+ {{ entry.added }}
{% raw %}
- {{entry_author}}
+ {{ entry_author }}
{% endraw %}
-
{{entry.author}}
+ {{ entry.author }}
- {% raw %}
- {{entry_content}}
- {% endraw %}
-
{{entry.content[0].value|discord_markdown}}
-
- {% raw %}
- {{entry_content_raw}}
- {% endraw %}
-
{{entry.content[0].value}}
-
+ {% raw %}
+ {{ entry_content }}
+ {% endraw %}
+
{{ entry.content[0].value|discord_markdown }}
+
+ {% raw %}
+ {{ entry_content_raw }}
+ {% endraw %}
+
{{ entry.content[0].value }}
+
{% raw %}
- {{entry_id}}
+ {{ entry_id }}
{% endraw %}
-
{{entry.id}}
+ {{ entry.id }}
{% raw %}
- {{entry_important}}
+ {{ entry_important }}
{% endraw %}
-
{{entry.important}}
+ {{ entry.important }}
{% raw %}
- {{entry_link}}
+ {{ entry_link }}
{% endraw %}
-
{{entry.link}}
+ {{ entry.link }}
{% raw %}
- {{entry_published}}
+ {{ entry_published }}
{% endraw %}
-
{{entry.published}}
+ {{ entry.published }}
{% raw %}
- {{entry_read}}
+ {{ entry_read }}
{% endraw %}
-
{{entry.read}}
+ {{ entry.read }}
{% raw %}
- {{entry_read_modified}}
+ {{ entry_read_modified }}
{% endraw %}
-
{{entry.read_modified}}
+ {{ entry.read_modified }}
- {% raw %}
- {{entry_summary}}
- {% endraw %}
-
{{entry.summary|discord_markdown}}
-
- {% raw %}
- {{entry_summary_raw}}
- {% endraw %}
-
{{entry.summary}}
-
+ {% raw %}
+ {{ entry_summary }}
+ {% endraw %}
+
{{ entry.summary|discord_markdown }}
+
+ {% raw %}
+ {{ entry_summary_raw }}
+ {% endraw %}
+
{{ entry.summary }}
+
{% raw %}
- {{entry_title}}
+ {{ entry_title }}
{% endraw %}
-
{{entry.title}}
+ {{ entry.title }}
{% raw %}
- {{entry_text}}
+ {{ entry_text }}
{% endraw %}
Same as entry_content if it exists, otherwise entry_summary
{% raw %}
- {{entry_updated}}
+ {{ entry_updated }}
{% endraw %}
-
{{entry.updated}}
+ {{ entry.updated }}
{% raw %}
- {{image_1}}
+ {{ image_1 }}
{% endraw %}
First image in the entry if it exists
{% raw %}
- {{feed_title}}\n{{entry_content}}
+ {{ feed_title }}\n{{ entry_content }}
{% endraw %}
code
code