@@ -20,6 +24,8 @@
[rss]
[atom]
+ [discord]
[csv]
[rss]
[atom]
+ [discord]
[csv]
+
{% endfor %}
{% endif %}
{% endblock extra_head %}
diff --git a/templates/twitch/reward_campaign_detail.html b/templates/twitch/reward_campaign_detail.html
index c3bfe3c..ffd9388 100644
--- a/templates/twitch/reward_campaign_detail.html
+++ b/templates/twitch/reward_campaign_detail.html
@@ -13,6 +13,10 @@
type="application/atom+xml"
title="Reward campaigns (Atom)"
href="{% url 'twitch:reward_campaign_feed_atom' %}" />
+
{% endblock extra_head %}
{% block content %}
@@ -36,6 +40,8 @@
title="RSS feed for all reward campaigns">RSS feed for all reward campaigns
[atom]
+ [discord]
{% if reward_campaign.summary %}{{ reward_campaign.summary|linebreaksbr }}
{% endif %}
diff --git a/templates/twitch/reward_campaign_list.html b/templates/twitch/reward_campaign_list.html
index 6ef71e6..99b8e7f 100644
--- a/templates/twitch/reward_campaign_list.html
+++ b/templates/twitch/reward_campaign_list.html
@@ -12,6 +12,10 @@
type="application/atom+xml"
title="Reward campaigns (Atom)"
href="{% url 'twitch:reward_campaign_feed_atom' %}" />
+
{% endblock extra_head %}
{% block content %}
Reward Campaigns
@@ -21,6 +25,8 @@
title="RSS feed for all reward campaigns">[rss]
[atom]
+ [discord]
This is an archive of old Twitch reward campaigns because we do not monitor them.
diff --git a/twitch/feeds.py b/twitch/feeds.py
index 43ab43d..69626ca 100644
--- a/twitch/feeds.py
+++ b/twitch/feeds.py
@@ -39,6 +39,26 @@ logger: logging.Logger = logging.getLogger("ttvdrops")
RSS_STYLESHEETS: list[str] = [static("rss_styles.xslt")]
+def discord_timestamp(dt: datetime.datetime | None) -> SafeText:
+ """Convert a datetime to a Discord relative timestamp format.
+
+ Discord timestamps use the format where R means relative time.
+ Example: displays as "2 hours ago" in Discord.
+
+ Args:
+ dt: The datetime to convert. If None, returns an empty string.
+
+ Returns:
+ SafeText: Escaped Discord timestamp token (e.g. <t:1773450272:R>) marked
+ safe for HTML insertion, or empty string if dt is None.
+ """
+ if dt is None:
+ return SafeText("")
+ unix_timestamp: int = int(dt.timestamp())
+ # Keep this escaped so Atom/RSS HTML renderers don't treat as an HTML tag.
+ return SafeText(f"<t:{unix_timestamp}:R>")
+
+
class BrowserFriendlyRss201rev2Feed(feedgenerator.Rss201rev2Feed):
"""RSS 2.0 feed generator with a browser-renderable XML content type."""
@@ -340,6 +360,49 @@ def generate_date_html(item: Model) -> list[SafeText]:
return parts
+def generate_discord_date_html(item: Model) -> list[SafeText]:
+ """Generate HTML snippets for dates using Discord relative timestamp format.
+
+ Args:
+ item (Model): The campaign item containing start_at and end_at.
+
+ Returns:
+ list[SafeText]: A list of SafeText elements with Discord timestamp formatted dates.
+ """
+ parts: list[SafeText] = []
+ end_at: datetime.datetime | None = getattr(item, "end_at", None)
+ start_at: datetime.datetime | None = getattr(item, "start_at", None)
+
+ if start_at or end_at:
+ start_part: SafeString = (
+ format_html(
+ "Starts: {} ({})",
+ start_at.strftime("%Y-%m-%d %H:%M %Z"),
+ discord_timestamp(start_at),
+ )
+ if start_at
+ else SafeText("")
+ )
+ end_part: SafeString = (
+ format_html(
+ "Ends: {} ({})",
+ end_at.strftime("%Y-%m-%d %H:%M %Z"),
+ discord_timestamp(end_at),
+ )
+ if end_at
+ else SafeText("")
+ )
+ # Start date and end date separated by a line break if both present
+ if start_part and end_part:
+ parts.append(format_html("{} {}
", start_part, end_part))
+ elif start_part:
+ parts.append(format_html("{}
", start_part))
+ elif end_part:
+ parts.append(format_html("{}
", end_part))
+
+ return parts
+
+
def generate_drops_summary_html(item: DropCampaign) -> list[SafeString]:
"""Generate HTML summary for drops and append to parts list.
@@ -1409,3 +1472,134 @@ class RewardCampaignAtomFeed(TTVDropsAtomBaseFeed, RewardCampaignFeed):
def feed_url(self) -> str:
"""Return the URL to the Atom feed itself."""
return reverse("twitch:reward_campaign_feed_atom")
+
+
+# Discord feed variants: Atom feeds with Discord relative timestamps
+class OrganizationDiscordFeed(TTVDropsAtomBaseFeed, OrganizationRSSFeed):
+ """Discord feed for latest organizations with Discord relative timestamps."""
+
+ subtitle: str = OrganizationRSSFeed.description
+
+ def feed_url(self) -> str:
+ """Return the URL to the Discord feed itself."""
+ return reverse("twitch:organization_feed_discord")
+
+
+class GameDiscordFeed(TTVDropsAtomBaseFeed, GameFeed):
+ """Discord feed for newly added games with Discord relative timestamps."""
+
+ subtitle: str = GameFeed.description
+
+ def feed_url(self) -> str:
+ """Return the URL to the Discord feed itself."""
+ return reverse("twitch:game_feed_discord")
+
+
+class DropCampaignDiscordFeed(TTVDropsAtomBaseFeed, DropCampaignFeed):
+ """Discord feed for latest drop campaigns with Discord relative timestamps."""
+
+ subtitle: str = DropCampaignFeed.description
+
+ def item_description(self, item: DropCampaign) -> SafeText:
+ """Return a description of the campaign with Discord timestamps."""
+ parts: list[SafeText] = []
+
+ parts.extend(generate_item_image(item))
+ parts.extend(generate_description_html(item=item))
+ parts.extend(generate_discord_date_html(item=item))
+ parts.extend(generate_drops_summary_html(item=item))
+ parts.extend(generate_channels_html(item))
+ parts.extend(genereate_details_link_html(item))
+
+ return SafeText("".join(str(p) for p in parts))
+
+ def feed_url(self) -> str:
+ """Return the URL to the Discord feed itself."""
+ return reverse("twitch:campaign_feed_discord")
+
+
+class GameCampaignDiscordFeed(TTVDropsAtomBaseFeed, GameCampaignFeed):
+ """Discord feed for latest drop campaigns for a specific game with Discord relative timestamps."""
+
+ def item_description(self, item: DropCampaign) -> SafeText:
+ """Return a description of the campaign with Discord timestamps."""
+ parts: list[SafeText] = []
+
+ parts.extend(generate_item_image_tag(item))
+ parts.extend(generate_details_link(item))
+ parts.extend(generate_discord_date_html(item))
+ parts.extend(generate_drops_summary_html(item))
+ parts.extend(generate_channels_html(item))
+
+ return SafeText("".join(str(p) for p in parts))
+
+ def feed_url(self, obj: Game) -> str:
+ """Return the URL to the Discord feed itself."""
+ return reverse("twitch:game_campaign_feed_discord", args=[obj.twitch_id])
+
+
+class RewardCampaignDiscordFeed(TTVDropsAtomBaseFeed, RewardCampaignFeed):
+ """Discord feed for latest reward campaigns with Discord relative timestamps."""
+
+ subtitle: str = RewardCampaignFeed.description
+
+ def item_description(self, item: RewardCampaign) -> SafeText:
+ """Return a description of the reward campaign with Discord timestamps."""
+ parts: list = []
+
+ if item.summary:
+ parts.append(format_html("{}
", item.summary))
+
+ if item.starts_at or item.ends_at:
+ start_part = (
+ format_html(
+ "Starts: {} ({})",
+ item.starts_at.strftime("%Y-%m-%d %H:%M %Z"),
+ discord_timestamp(item.starts_at),
+ )
+ if item.starts_at
+ else ""
+ )
+ end_part = (
+ format_html(
+ "Ends: {} ({})",
+ item.ends_at.strftime("%Y-%m-%d %H:%M %Z"),
+ discord_timestamp(item.ends_at),
+ )
+ if item.ends_at
+ else ""
+ )
+ if start_part and end_part:
+ parts.append(format_html("{} {}
", start_part, end_part))
+ elif start_part:
+ parts.append(format_html("{}
", start_part))
+ elif end_part:
+ parts.append(format_html("{}
", end_part))
+
+ if item.is_sitewide:
+ parts.append(
+ SafeText("This is a sitewide reward campaign
"),
+ )
+ elif item.game:
+ parts.append(
+ format_html(
+ "Game: {}
",
+ item.game.display_name or item.game.name,
+ ),
+ )
+
+ if item.about_url:
+ parts.append(
+ format_html('Learn more
', item.about_url),
+ )
+
+ if item.external_url:
+ parts.append(
+ format_html('Redeem reward
', item.external_url),
+ )
+
+ return SafeText("".join(str(p) for p in parts))
+
+ def feed_url(self) -> str:
+ """Return the URL to the Discord feed itself."""
+ return reverse("twitch:reward_campaign_feed_discord")
diff --git a/twitch/tests/test_feeds.py b/twitch/tests/test_feeds.py
index 30c1fca..384327d 100644
--- a/twitch/tests/test_feeds.py
+++ b/twitch/tests/test_feeds.py
@@ -1,8 +1,11 @@
"""Test RSS feeds."""
+import datetime
import logging
+import re
from collections.abc import Callable
from contextlib import AbstractContextManager
+from datetime import UTC
from datetime import timedelta
from pathlib import Path
from typing import TYPE_CHECKING
@@ -21,6 +24,7 @@ from twitch.feeds import GameFeed
from twitch.feeds import OrganizationRSSFeed
from twitch.feeds import RewardCampaignFeed
from twitch.feeds import TTVDropsBaseFeed
+from twitch.feeds import discord_timestamp
from twitch.models import Channel
from twitch.models import ChatBadge
from twitch.models import ChatBadgeSet
@@ -37,8 +41,6 @@ STYLESHEET_PATH: Path = (
)
if TYPE_CHECKING:
- import datetime
-
from django.test.client import _MonkeyPatchedWSGIResponse
from django.utils.feedgenerator import Enclosure
@@ -1183,3 +1185,221 @@ def test_rss_feeds_return_200(
url: str = reverse(viewname=url_name, kwargs=kwargs)
response: _MonkeyPatchedWSGIResponse = client.get(url)
assert response.status_code == 200
+
+
+class DiscordFeedTestCase(TestCase):
+ """Test Discord feeds with relative timestamps."""
+
+ def setUp(self) -> None:
+ """Set up test fixtures."""
+ self.org: Organization = Organization.objects.create(
+ twitch_id="test-org-discord",
+ name="Test Organization Discord",
+ )
+ self.org.save()
+
+ self.game: Game = Game.objects.create(
+ twitch_id="test-game-discord",
+ slug="test-game-discord",
+ name="Test Game Discord",
+ display_name="Test Game Discord",
+ )
+ self.game.owners.add(self.org)
+ self.campaign: DropCampaign = DropCampaign.objects.create(
+ twitch_id="test-campaign-discord",
+ name="Test Campaign Discord",
+ game=self.game,
+ start_at=timezone.now(),
+ end_at=timezone.now() + timedelta(days=7),
+ operation_names=["DropCampaignDetails"],
+ )
+
+ self.game.box_art_size_bytes = 42
+ self.game.box_art_mime_type = "image/png"
+ self.game.box_art = "https://example.com/box.png"
+ self.game.save()
+
+ self.campaign.image_size_bytes = 314
+ self.campaign.image_mime_type = "image/gif"
+ self.campaign.image_url = "https://example.com/campaign.png"
+ self.campaign.save()
+
+ self.reward_campaign: RewardCampaign = RewardCampaign.objects.create(
+ twitch_id="test-reward-discord",
+ name="Test Reward Campaign Discord",
+ brand="Test Brand",
+ starts_at=timezone.now() - timedelta(days=1),
+ ends_at=timezone.now() + timedelta(days=7),
+ status="ACTIVE",
+ summary="Test reward summary",
+ instructions="Watch and complete objectives",
+ external_url="https://example.com/reward",
+ about_url="https://example.com/about",
+ is_sitewide=False,
+ game=self.game,
+ )
+
+ def test_discord_timestamp_helper(self) -> None:
+ """Test discord_timestamp helper function."""
+ dt: datetime.datetime = datetime.datetime(2026, 3, 14, 12, 0, 0, tzinfo=UTC)
+ result: str = str(discord_timestamp(dt))
+ assert result.startswith("<t:")
+ assert result.endswith(":R>")
+
+ # Test None input
+ assert not str(discord_timestamp(None))
+
+ def test_organization_discord_feed(self) -> None:
+ """Test organization Discord feed returns 200."""
+ url: str = reverse("twitch:organization_feed_discord")
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+ assert response["Content-Type"] == "application/xml; charset=utf-8"
+ assert response["Content-Disposition"] == "inline"
+ content: str = response.content.decode("utf-8")
+ assert " None:
+ """Test game Discord feed returns 200."""
+ url: str = reverse("twitch:game_feed_discord")
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+ assert response["Content-Type"] == "application/xml; charset=utf-8"
+ content: str = response.content.decode("utf-8")
+ assert " None:
+ """Test campaign Discord feed returns 200 with Discord timestamps."""
+ url: str = reverse("twitch:campaign_feed_discord")
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+ assert response["Content-Type"] == "application/xml; charset=utf-8"
+ content: str = response.content.decode("utf-8")
+ assert " None:
+ """Test game-specific campaign Discord feed returns 200."""
+ url: str = reverse(
+ "twitch:game_campaign_feed_discord",
+ args=[self.game.twitch_id],
+ )
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+ assert response["Content-Type"] == "application/xml; charset=utf-8"
+ content: str = response.content.decode("utf-8")
+ assert " None:
+ """Test reward campaign Discord feed returns 200."""
+ url: str = reverse("twitch:reward_campaign_feed_discord")
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+ assert response["Content-Type"] == "application/xml; charset=utf-8"
+ content: str = response.content.decode("utf-8")
+ assert " None:
+ """All Discord feeds should use absolute URL entry IDs and matching self links."""
+ discord_feed_cases: list[tuple[str, dict[str, str], str]] = [
+ (
+ "twitch:campaign_feed_discord",
+ {},
+ f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
+ ),
+ (
+ "twitch:game_feed_discord",
+ {},
+ f"http://testserver{reverse('twitch:game_detail', args=[self.game.twitch_id])}",
+ ),
+ (
+ "twitch:game_campaign_feed_discord",
+ {"twitch_id": self.game.twitch_id},
+ f"http://testserver{reverse('twitch:campaign_detail', args=[self.campaign.twitch_id])}",
+ ),
+ (
+ "twitch:organization_feed_discord",
+ {},
+ f"http://testserver{reverse('twitch:organization_detail', args=[self.org.twitch_id])}",
+ ),
+ (
+ "twitch:reward_campaign_feed_discord",
+ {},
+ f"http://testserver{reverse('twitch:reward_campaign_detail', args=[self.reward_campaign.twitch_id])}",
+ ),
+ ]
+
+ for url_name, kwargs, expected_entry_id in discord_feed_cases:
+ url: str = reverse(url_name, kwargs=kwargs)
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+
+ assert response.status_code == 200
+ content: str = response.content.decode("utf-8")
+
+ expected_self_link: str = f'href="http://testserver{url}"'
+ msg: str = f"Expected self link in Discord feed {url_name}, got: {content}"
+ assert 'rel="self"' in content, msg
+
+ msg = f"Expected self link to match feed URL for {url_name}, got: {content}"
+ assert expected_self_link in content, msg
+
+ msg = f"Expected entry ID to be absolute URL for {url_name}, got: {content}"
+ assert f"{expected_entry_id} " in content, msg
+
+ def test_discord_feeds_include_stylesheet_processing_instruction(self) -> None:
+ """Discord feeds should include an xml-stylesheet processing instruction."""
+ feed_urls: list[str] = [
+ reverse("twitch:campaign_feed_discord"),
+ reverse("twitch:game_feed_discord"),
+ reverse("twitch:game_campaign_feed_discord", args=[self.game.twitch_id]),
+ reverse("twitch:organization_feed_discord"),
+ reverse("twitch:reward_campaign_feed_discord"),
+ ]
+
+ for url in feed_urls:
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+
+ content: str = response.content.decode("utf-8")
+ assert " None:
+ """Discord campaign feed should contain Discord relative timestamps."""
+ url: str = reverse("twitch:campaign_feed_discord")
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+ content: str = response.content.decode("utf-8")
+
+ # Should contain Discord timestamp format (double-escaped in XML payload)
+ discord_pattern: re.Pattern[str] = re.compile(r"<t:\d+:R>")
+ assert discord_pattern.search(content), (
+ f"Expected Discord timestamp format <t:UNIX_TIMESTAMP:R> in content, got: {content}"
+ )
+ assert "()" not in content
+
+ def test_discord_reward_campaign_feed_contains_discord_timestamps(self) -> None:
+ """Discord reward campaign feed should contain Discord relative timestamps."""
+ url: str = reverse("twitch:reward_campaign_feed_discord")
+ response: _MonkeyPatchedWSGIResponse = self.client.get(url)
+ assert response.status_code == 200
+ content: str = response.content.decode("utf-8")
+
+ # Should contain Discord timestamp format (double-escaped in XML payload)
+ discord_pattern: re.Pattern[str] = re.compile(r"<t:\d+:R>")
+ assert discord_pattern.search(content), (
+ f"Expected Discord timestamp format <t:UNIX_TIMESTAMP:R> in content, got: {content}"
+ )
+ assert "()" not in content
diff --git a/twitch/urls.py b/twitch/urls.py
index 1485220..2947512 100644
--- a/twitch/urls.py
+++ b/twitch/urls.py
@@ -4,23 +4,29 @@ from django.urls import path
from twitch import views
from twitch.feeds import DropCampaignAtomFeed
+from twitch.feeds import DropCampaignDiscordFeed
from twitch.feeds import DropCampaignFeed
from twitch.feeds import GameAtomFeed
from twitch.feeds import GameCampaignAtomFeed
+from twitch.feeds import GameCampaignDiscordFeed
from twitch.feeds import GameCampaignFeed
+from twitch.feeds import GameDiscordFeed
from twitch.feeds import GameFeed
from twitch.feeds import OrganizationAtomFeed
+from twitch.feeds import OrganizationDiscordFeed
from twitch.feeds import OrganizationRSSFeed
from twitch.feeds import RewardCampaignAtomFeed
+from twitch.feeds import RewardCampaignDiscordFeed
from twitch.feeds import RewardCampaignFeed
if TYPE_CHECKING:
from django.urls.resolvers import URLPattern
+ from django.urls.resolvers import URLResolver
app_name = "twitch"
-urlpatterns: list[URLPattern] = [
+urlpatterns: list[URLPattern | URLResolver] = [
path("", views.dashboard, name="dashboard"),
path("badges/", views.badge_list_view, name="badge_list"),
path("badges//", views.badge_set_detail_view, name="badge_set_detail"),
@@ -128,4 +134,22 @@ urlpatterns: list[URLPattern] = [
RewardCampaignAtomFeed(),
name="reward_campaign_feed_atom",
),
+ # Discord feeds (Atom feeds with Discord relative timestamps)
+ path("discord/campaigns/", DropCampaignDiscordFeed(), name="campaign_feed_discord"),
+ path("discord/games/", GameDiscordFeed(), name="game_feed_discord"),
+ path(
+ "discord/games//campaigns/",
+ GameCampaignDiscordFeed(),
+ name="game_campaign_feed_discord",
+ ),
+ path(
+ "discord/organizations/",
+ OrganizationDiscordFeed(),
+ name="organization_feed_discord",
+ ),
+ path(
+ "discord/reward-campaigns/",
+ RewardCampaignDiscordFeed(),
+ name="reward_campaign_feed_discord",
+ ),
]
diff --git a/twitch/views.py b/twitch/views.py
index 2e6d8e7..c4ef275 100644
--- a/twitch/views.py
+++ b/twitch/views.py
@@ -41,14 +41,19 @@ from pygments.formatters import HtmlFormatter
from pygments.lexers.data import JsonLexer
from twitch.feeds import DropCampaignAtomFeed
+from twitch.feeds import DropCampaignDiscordFeed
from twitch.feeds import DropCampaignFeed
from twitch.feeds import GameAtomFeed
from twitch.feeds import GameCampaignAtomFeed
+from twitch.feeds import GameCampaignDiscordFeed
from twitch.feeds import GameCampaignFeed
+from twitch.feeds import GameDiscordFeed
from twitch.feeds import GameFeed
from twitch.feeds import OrganizationAtomFeed
+from twitch.feeds import OrganizationDiscordFeed
from twitch.feeds import OrganizationRSSFeed
from twitch.feeds import RewardCampaignAtomFeed
+from twitch.feeds import RewardCampaignDiscordFeed
from twitch.feeds import RewardCampaignFeed
from twitch.models import Channel
from twitch.models import ChatBadge
@@ -1829,38 +1834,52 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
"description": "Latest organizations added to TTVDrops",
"url": absolute(reverse("twitch:organization_feed")),
"atom_url": absolute(reverse("twitch:organization_feed_atom")),
+ "discord_url": absolute(reverse("twitch:organization_feed_discord")),
"example_xml": render_feed(OrganizationRSSFeed()),
"example_xml_atom": render_feed(OrganizationAtomFeed())
if show_atom
else "",
+ "example_xml_discord": render_feed(OrganizationDiscordFeed())
+ if show_atom
+ else "",
},
{
"title": "All Games",
"description": "Latest games added to TTVDrops",
"url": absolute(reverse("twitch:game_feed")),
"atom_url": absolute(reverse("twitch:game_feed_atom")),
+ "discord_url": absolute(reverse("twitch:game_feed_discord")),
"example_xml": render_feed(GameFeed()),
"example_xml_atom": render_feed(GameAtomFeed()) if show_atom else "",
+ "example_xml_discord": render_feed(GameDiscordFeed()) if show_atom else "",
},
{
"title": "All Drop Campaigns",
"description": "Latest drop campaigns across all games",
"url": absolute(reverse("twitch:campaign_feed")),
"atom_url": absolute(reverse("twitch:campaign_feed_atom")),
+ "discord_url": absolute(reverse("twitch:campaign_feed_discord")),
"example_xml": render_feed(DropCampaignFeed()),
"example_xml_atom": render_feed(DropCampaignAtomFeed())
if show_atom
else "",
+ "example_xml_discord": render_feed(DropCampaignDiscordFeed())
+ if show_atom
+ else "",
},
{
"title": "All Reward Campaigns",
"description": "Latest reward campaigns (Quest rewards) on Twitch",
"url": absolute(reverse("twitch:reward_campaign_feed")),
"atom_url": absolute(reverse("twitch:reward_campaign_feed_atom")),
+ "discord_url": absolute(reverse("twitch:reward_campaign_feed_discord")),
"example_xml": render_feed(RewardCampaignFeed()),
"example_xml_atom": render_feed(RewardCampaignAtomFeed())
if show_atom
else "",
+ "example_xml_discord": render_feed(RewardCampaignDiscordFeed())
+ if show_atom
+ else "",
},
]
@@ -1890,6 +1909,16 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
if sample_game
else absolute("/atom/games//campaigns/")
),
+ "discord_url": (
+ absolute(
+ reverse(
+ "twitch:game_campaign_feed_discord",
+ args=[sample_game.twitch_id],
+ ),
+ )
+ if sample_game
+ else absolute("/discord/games//campaigns/")
+ ),
"has_sample": bool(sample_game),
"example_xml": render_feed(GameCampaignFeed(), sample_game.twitch_id)
if sample_game
@@ -1899,6 +1928,11 @@ def docs_rss_view(request: HttpRequest) -> HttpResponse:
if sample_game and show_atom
else ""
),
+ "example_xml_discord": (
+ render_feed(GameCampaignDiscordFeed(), sample_game.twitch_id)
+ if sample_game and show_atom
+ else ""
+ ),
},
]