This commit is contained in:
parent
11244c669f
commit
4627d1cea0
16 changed files with 569 additions and 6 deletions
194
twitch/feeds.py
194
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 <t:UNIX_TIMESTAMP:R> where R means relative time.
|
||||
Example: <t:1773450272:R> 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 <t:...:R> 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("<p>{}<br />{}</p>", start_part, end_part))
|
||||
elif start_part:
|
||||
parts.append(format_html("<p>{}</p>", start_part))
|
||||
elif end_part:
|
||||
parts.append(format_html("<p>{}</p>", 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("<p>{}</p>", 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("<p>{}<br />{}</p>", start_part, end_part))
|
||||
elif start_part:
|
||||
parts.append(format_html("<p>{}</p>", start_part))
|
||||
elif end_part:
|
||||
parts.append(format_html("<p>{}</p>", end_part))
|
||||
|
||||
if item.is_sitewide:
|
||||
parts.append(
|
||||
SafeText("<p><strong>This is a sitewide reward campaign</strong></p>"),
|
||||
)
|
||||
elif item.game:
|
||||
parts.append(
|
||||
format_html(
|
||||
"<p>Game: {}</p>",
|
||||
item.game.display_name or item.game.name,
|
||||
),
|
||||
)
|
||||
|
||||
if item.about_url:
|
||||
parts.append(
|
||||
format_html('<p><a href="{}">Learn more</a></p>', item.about_url),
|
||||
)
|
||||
|
||||
if item.external_url:
|
||||
parts.append(
|
||||
format_html('<p><a href="{}">Redeem reward</a></p>', 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")
|
||||
|
|
|
|||
|
|
@ -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 "<feed" in content
|
||||
assert "http://www.w3.org/2005/Atom" in content
|
||||
|
||||
def test_game_discord_feed(self) -> 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 "<feed" in content
|
||||
assert "Owned by Test Organization Discord." in content
|
||||
|
||||
def test_campaign_discord_feed(self) -> 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 "<feed" in content
|
||||
# Should contain Discord timestamp format (double-escaped in XML payload)
|
||||
assert "&lt;t:" in content
|
||||
assert ":R&gt;" in content
|
||||
assert "()" not in content
|
||||
|
||||
def test_game_campaign_discord_feed(self) -> 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 "<feed" in content
|
||||
assert "Test Game Discord" in content
|
||||
|
||||
def test_reward_campaign_discord_feed(self) -> 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 "<feed" in content
|
||||
# Should contain Discord timestamp format (double-escaped in XML payload)
|
||||
assert "&lt;t:" in content
|
||||
assert ":R&gt;" in content
|
||||
assert "()" not in content
|
||||
|
||||
def test_discord_feeds_use_url_ids_and_correct_self_links(self) -> 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"<id>{expected_entry_id}</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 "<?xml-stylesheet" in content
|
||||
assert "rss_styles.xslt" in content
|
||||
assert 'type="text/xsl"' in content
|
||||
assert 'media="screen"' in content
|
||||
|
||||
def test_discord_campaign_feed_contains_discord_timestamps(self) -> 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"&lt;t:\d+:R&gt;")
|
||||
assert discord_pattern.search(content), (
|
||||
f"Expected Discord timestamp format &lt;t:UNIX_TIMESTAMP:R&gt; 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"&lt;t:\d+:R&gt;")
|
||||
assert discord_pattern.search(content), (
|
||||
f"Expected Discord timestamp format &lt;t:UNIX_TIMESTAMP:R&gt; in content, got: {content}"
|
||||
)
|
||||
assert "()" not in content
|
||||
|
|
|
|||
|
|
@ -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/<str:set_id>/", 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/<str:twitch_id>/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",
|
||||
),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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/<game_id>/campaigns/")
|
||||
),
|
||||
"discord_url": (
|
||||
absolute(
|
||||
reverse(
|
||||
"twitch:game_campaign_feed_discord",
|
||||
args=[sample_game.twitch_id],
|
||||
),
|
||||
)
|
||||
if sample_game
|
||||
else absolute("/discord/games/<game_id>/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 ""
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue