Improve feed explenation; add link to templates
This commit is contained in:
parent
92ca0404a6
commit
768e6f2111
19 changed files with 200 additions and 279 deletions
58
core/urls.py
58
core/urls.py
|
|
@ -2,9 +2,27 @@ from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from core import views
|
from core.views import dashboard
|
||||||
|
from core.views import dataset_backup_download_view
|
||||||
|
from core.views import dataset_backups_view
|
||||||
|
from core.views import debug_view
|
||||||
|
from core.views import docs_rss_view
|
||||||
|
from core.views import search_view
|
||||||
|
from twitch.feeds import DropCampaignAtomFeed
|
||||||
|
from twitch.feeds import DropCampaignDiscordFeed
|
||||||
from twitch.feeds import DropCampaignFeed
|
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 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:
|
if TYPE_CHECKING:
|
||||||
from django.urls.resolvers import URLPattern
|
from django.urls.resolvers import URLPattern
|
||||||
|
|
@ -15,21 +33,21 @@ app_name = "core"
|
||||||
|
|
||||||
urlpatterns: list[URLPattern | URLResolver] = [
|
urlpatterns: list[URLPattern | URLResolver] = [
|
||||||
# /
|
# /
|
||||||
path("", views.dashboard, name="dashboard"),
|
path("", dashboard, name="dashboard"),
|
||||||
# /search/
|
# /search/
|
||||||
path("search/", views.search_view, name="search"),
|
path("search/", search_view, name="search"),
|
||||||
# /debug/
|
# /debug/
|
||||||
path("debug/", views.debug_view, name="debug"),
|
path("debug/", debug_view, name="debug"),
|
||||||
# /datasets/
|
# /datasets/
|
||||||
path("datasets/", views.dataset_backups_view, name="dataset_backups"),
|
path("datasets/", dataset_backups_view, name="dataset_backups"),
|
||||||
# /datasets/download/<relative_path>/
|
# /datasets/download/<relative_path>/
|
||||||
path(
|
path(
|
||||||
"datasets/download/<path:relative_path>/",
|
"datasets/download/<path:relative_path>/",
|
||||||
views.dataset_backup_download_view,
|
dataset_backup_download_view,
|
||||||
name="dataset_backup_download",
|
name="dataset_backup_download",
|
||||||
),
|
),
|
||||||
# /docs/rss/
|
# /docs/rss/
|
||||||
path("docs/rss/", views.docs_rss_view, name="docs_rss"),
|
path("docs/rss/", docs_rss_view, name="docs_rss"),
|
||||||
# RSS feeds
|
# RSS feeds
|
||||||
# /rss/campaigns/ - all active campaigns
|
# /rss/campaigns/ - all active campaigns
|
||||||
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"),
|
path("rss/campaigns/", DropCampaignFeed(), name="campaign_feed"),
|
||||||
|
|
@ -38,59 +56,59 @@ urlpatterns: list[URLPattern | URLResolver] = [
|
||||||
# /rss/games/<twitch_id>/campaigns/ - active campaigns for a specific game
|
# /rss/games/<twitch_id>/campaigns/ - active campaigns for a specific game
|
||||||
path(
|
path(
|
||||||
"rss/games/<str:twitch_id>/campaigns/",
|
"rss/games/<str:twitch_id>/campaigns/",
|
||||||
views.GameCampaignFeed(),
|
GameCampaignFeed(),
|
||||||
name="game_campaign_feed",
|
name="game_campaign_feed",
|
||||||
),
|
),
|
||||||
# /rss/organizations/ - newly added organizations
|
# /rss/organizations/ - newly added organizations
|
||||||
path(
|
path(
|
||||||
"rss/organizations/",
|
"rss/organizations/",
|
||||||
views.OrganizationRSSFeed(),
|
OrganizationRSSFeed(),
|
||||||
name="organization_feed",
|
name="organization_feed",
|
||||||
),
|
),
|
||||||
# /rss/reward-campaigns/ - all active reward campaigns
|
# /rss/reward-campaigns/ - all active reward campaigns
|
||||||
path(
|
path(
|
||||||
"rss/reward-campaigns/",
|
"rss/reward-campaigns/",
|
||||||
views.RewardCampaignFeed(),
|
RewardCampaignFeed(),
|
||||||
name="reward_campaign_feed",
|
name="reward_campaign_feed",
|
||||||
),
|
),
|
||||||
# Atom feeds (added alongside RSS to preserve backward compatibility)
|
# Atom feeds (added alongside RSS to preserve backward compatibility)
|
||||||
path("atom/campaigns/", views.DropCampaignAtomFeed(), name="campaign_feed_atom"),
|
path("atom/campaigns/", DropCampaignAtomFeed(), name="campaign_feed_atom"),
|
||||||
path("atom/games/", views.GameAtomFeed(), name="game_feed_atom"),
|
path("atom/games/", GameAtomFeed(), name="game_feed_atom"),
|
||||||
path(
|
path(
|
||||||
"atom/games/<str:twitch_id>/campaigns/",
|
"atom/games/<str:twitch_id>/campaigns/",
|
||||||
views.GameCampaignAtomFeed(),
|
view=GameCampaignAtomFeed(),
|
||||||
name="game_campaign_feed_atom",
|
name="game_campaign_feed_atom",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"atom/organizations/",
|
"atom/organizations/",
|
||||||
views.OrganizationAtomFeed(),
|
OrganizationAtomFeed(),
|
||||||
name="organization_feed_atom",
|
name="organization_feed_atom",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"atom/reward-campaigns/",
|
"atom/reward-campaigns/",
|
||||||
views.RewardCampaignAtomFeed(),
|
RewardCampaignAtomFeed(),
|
||||||
name="reward_campaign_feed_atom",
|
name="reward_campaign_feed_atom",
|
||||||
),
|
),
|
||||||
# Discord feeds (Atom feeds with Discord relative timestamps)
|
# Discord feeds (Atom feeds with Discord relative timestamps)
|
||||||
path(
|
path(
|
||||||
"discord/campaigns/",
|
"discord/campaigns/",
|
||||||
views.DropCampaignDiscordFeed(),
|
DropCampaignDiscordFeed(),
|
||||||
name="campaign_feed_discord",
|
name="campaign_feed_discord",
|
||||||
),
|
),
|
||||||
path("discord/games/", views.GameDiscordFeed(), name="game_feed_discord"),
|
path("discord/games/", GameDiscordFeed(), name="game_feed_discord"),
|
||||||
path(
|
path(
|
||||||
"discord/games/<str:twitch_id>/campaigns/",
|
"discord/games/<str:twitch_id>/campaigns/",
|
||||||
views.GameCampaignDiscordFeed(),
|
GameCampaignDiscordFeed(),
|
||||||
name="game_campaign_feed_discord",
|
name="game_campaign_feed_discord",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"discord/organizations/",
|
"discord/organizations/",
|
||||||
views.OrganizationDiscordFeed(),
|
OrganizationDiscordFeed(),
|
||||||
name="organization_feed_discord",
|
name="organization_feed_discord",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"discord/reward-campaigns/",
|
"discord/reward-campaigns/",
|
||||||
views.RewardCampaignDiscordFeed(),
|
RewardCampaignDiscordFeed(),
|
||||||
name="reward_campaign_feed_discord",
|
name="reward_campaign_feed_discord",
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
||||||
189
core/views.py
189
core/views.py
|
|
@ -3,7 +3,6 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from copy import copy
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -27,21 +26,6 @@ from django.utils import timezone
|
||||||
|
|
||||||
from kick.models import KickChannel
|
from kick.models import KickChannel
|
||||||
from kick.models import KickDropCampaign
|
from kick.models import KickDropCampaign
|
||||||
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 Channel
|
||||||
from twitch.models import ChatBadge
|
from twitch.models import ChatBadge
|
||||||
from twitch.models import ChatBadgeSet
|
from twitch.models import ChatBadgeSet
|
||||||
|
|
@ -53,13 +37,11 @@ from twitch.models import RewardCampaign
|
||||||
from twitch.models import TimeBasedDrop
|
from twitch.models import TimeBasedDrop
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from collections.abc import Callable
|
|
||||||
from os import stat_result
|
from os import stat_result
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.http.request import QueryDict
|
|
||||||
|
|
||||||
|
|
||||||
logger: logging.Logger = logging.getLogger("ttvdrops.views")
|
logger: logging.Logger = logging.getLogger("ttvdrops.views")
|
||||||
|
|
@ -279,178 +261,33 @@ def sitemap_view(request: HttpRequest) -> HttpResponse: # noqa: PLR0915
|
||||||
|
|
||||||
# MARK: /docs/rss/
|
# MARK: /docs/rss/
|
||||||
def docs_rss_view(request: HttpRequest) -> HttpResponse:
|
def docs_rss_view(request: HttpRequest) -> HttpResponse:
|
||||||
"""View for /docs/rss that lists all available RSS feeds.
|
"""View for /docs/rss that lists all available feeds and explains how to use them.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request: The HTTP request object.
|
request: The HTTP request object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Rendered HTML response with list of RSS feeds.
|
HttpResponse: The rendered documentation page.
|
||||||
"""
|
"""
|
||||||
|
now: datetime.datetime = timezone.now()
|
||||||
def absolute(path: str) -> str:
|
sample_game: Game | None = (
|
||||||
try:
|
Game.objects
|
||||||
return request.build_absolute_uri(path)
|
.filter(drop_campaigns__start_at__lte=now, drop_campaigns__end_at__gte=now)
|
||||||
except Exception:
|
.distinct()
|
||||||
logger.exception("Failed to build absolute URL for %s", path)
|
.first()
|
||||||
return path
|
)
|
||||||
|
|
||||||
def _pretty_example(xml_str: str, max_items: int = 1) -> str:
|
|
||||||
try:
|
|
||||||
trimmed: str = xml_str.strip()
|
|
||||||
first_item: int = trimmed.find("<item")
|
|
||||||
if first_item != -1 and max_items == 1:
|
|
||||||
second_item: int = trimmed.find("<item", first_item + 5)
|
|
||||||
if second_item != -1:
|
|
||||||
end_channel: int = trimmed.find("</channel>", second_item)
|
|
||||||
if end_channel != -1:
|
|
||||||
trimmed = trimmed[:second_item] + trimmed[end_channel:]
|
|
||||||
formatted: str = trimmed.replace("><", ">\n<")
|
|
||||||
return "\n".join(line for line in formatted.splitlines() if line.strip())
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Failed to pretty-print RSS example")
|
|
||||||
return xml_str
|
|
||||||
|
|
||||||
def render_feed(feed_view: Callable[..., HttpResponse], *args: object) -> str:
|
|
||||||
try:
|
|
||||||
limited_request: HttpRequest = copy(request)
|
|
||||||
# Add limit=1 to GET parameters
|
|
||||||
get_data: QueryDict = request.GET.copy()
|
|
||||||
get_data["limit"] = "1"
|
|
||||||
limited_request.GET = get_data
|
|
||||||
|
|
||||||
response: HttpResponse = feed_view(limited_request, *args)
|
|
||||||
return _pretty_example(response.content.decode("utf-8"))
|
|
||||||
except Exception:
|
|
||||||
logger.exception(
|
|
||||||
"Failed to render %s for RSS docs",
|
|
||||||
feed_view.__class__.__name__,
|
|
||||||
)
|
|
||||||
return ""
|
|
||||||
|
|
||||||
show_atom: bool = bool(request.GET.get("show_atom"))
|
|
||||||
|
|
||||||
feeds: list[dict[str, str]] = [
|
|
||||||
{
|
|
||||||
"title": "All Organizations",
|
|
||||||
"description": "Latest organizations added to TTVDrops",
|
|
||||||
"url": absolute(reverse("core:organization_feed")),
|
|
||||||
"atom_url": absolute(reverse("core:organization_feed_atom")),
|
|
||||||
"discord_url": absolute(reverse("core: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("core:game_feed")),
|
|
||||||
"atom_url": absolute(reverse("core:game_feed_atom")),
|
|
||||||
"discord_url": absolute(reverse("core: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("core:campaign_feed")),
|
|
||||||
"atom_url": absolute(reverse("core:campaign_feed_atom")),
|
|
||||||
"discord_url": absolute(reverse("core: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("core:reward_campaign_feed")),
|
|
||||||
"atom_url": absolute(reverse("core:reward_campaign_feed_atom")),
|
|
||||||
"discord_url": absolute(reverse("core: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 "",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
sample_game: Game | None = Game.objects.order_by("-added_at").first()
|
|
||||||
sample_org: Organization | None = Organization.objects.order_by("-added_at").first()
|
|
||||||
if sample_org is None and sample_game is not None:
|
|
||||||
sample_org = sample_game.owners.order_by("-pk").first()
|
|
||||||
|
|
||||||
filtered_feeds: list[dict[str, str | bool]] = [
|
|
||||||
{
|
|
||||||
"title": "Campaigns for a Single Game",
|
|
||||||
"description": "Latest drop campaigns for one game.",
|
|
||||||
"url": (
|
|
||||||
absolute(
|
|
||||||
reverse("core:game_campaign_feed", args=[sample_game.twitch_id]),
|
|
||||||
)
|
|
||||||
if sample_game
|
|
||||||
else absolute("/rss/games/<game_id>/campaigns/")
|
|
||||||
),
|
|
||||||
"atom_url": (
|
|
||||||
absolute(
|
|
||||||
reverse(
|
|
||||||
"core:game_campaign_feed_atom",
|
|
||||||
args=[sample_game.twitch_id],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if sample_game
|
|
||||||
else absolute("/atom/games/<game_id>/campaigns/")
|
|
||||||
),
|
|
||||||
"discord_url": (
|
|
||||||
absolute(
|
|
||||||
reverse(
|
|
||||||
"core: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
|
|
||||||
else "",
|
|
||||||
"example_xml_atom": (
|
|
||||||
render_feed(GameCampaignAtomFeed(), sample_game.twitch_id)
|
|
||||||
if sample_game and show_atom
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
"example_xml_discord": (
|
|
||||||
render_feed(GameCampaignDiscordFeed(), sample_game.twitch_id)
|
|
||||||
if sample_game and show_atom
|
|
||||||
else ""
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
seo_context: dict[str, Any] = _build_seo_context(
|
seo_context: dict[str, Any] = _build_seo_context(
|
||||||
page_title="Twitch RSS Feeds",
|
page_title="Feed Documentation",
|
||||||
page_description="RSS feeds for Twitch drops.",
|
page_description="Documentation for the RSS feeds available on ttvdrops.lovinator.space, including how to use them and what data they contain.",
|
||||||
page_url=request.build_absolute_uri(reverse("core:docs_rss")),
|
page_url=request.build_absolute_uri(reverse("core:docs_rss")),
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(
|
return render(
|
||||||
request,
|
request,
|
||||||
"twitch/docs_rss.html",
|
"twitch/docs_rss.html",
|
||||||
{
|
{
|
||||||
"feeds": feeds,
|
"game": sample_game,
|
||||||
"filtered_feeds": filtered_feeds,
|
|
||||||
"sample_game": sample_game,
|
|
||||||
"sample_org": sample_org,
|
|
||||||
**seo_context,
|
**seo_context,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@
|
||||||
title="Atom feed for Twitch campaigns">[atom]</a>
|
title="Atom feed for Twitch campaigns">[atom]</a>
|
||||||
<a href="{% url 'core:campaign_feed_discord' %}"
|
<a href="{% url 'core:campaign_feed_discord' %}"
|
||||||
title="Discord feed for Twitch campaigns">[discord]</a>
|
title="Discord feed for Twitch campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% if campaigns_by_game %}
|
{% if campaigns_by_game %}
|
||||||
|
|
@ -223,6 +224,7 @@
|
||||||
title="Atom feed for all Kick campaigns">[atom]</a>
|
title="Atom feed for all Kick campaigns">[atom]</a>
|
||||||
<a href="{% url 'kick:campaign_feed_discord' %}"
|
<a href="{% url 'kick:campaign_feed_discord' %}"
|
||||||
title="Discord feed for all Kick campaigns">[discord]</a>
|
title="Discord feed for all Kick campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% if kick_campaigns_by_game %}
|
{% if kick_campaigns_by_game %}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@
|
||||||
title="Atom feed for {{ campaign.category.name }} campaigns">[atom]</a>
|
title="Atom feed for {{ campaign.category.name }} campaigns">[atom]</a>
|
||||||
<a href="{% url 'kick:game_campaign_feed_discord' campaign.category.kick_id %}"
|
<a href="{% url 'kick:game_campaign_feed_discord' campaign.category.kick_id %}"
|
||||||
title="Discord feed for {{ campaign.category.name }} campaigns">[discord]</a>
|
title="Discord feed for {{ campaign.category.name }} campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p style="margin: 0.25rem 0; color: #666;">
|
<p style="margin: 0.25rem 0; color: #666;">
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
title="Atom feed for all campaigns">[atom]</a>
|
title="Atom feed for all campaigns">[atom]</a>
|
||||||
<a href="{% url 'kick:campaign_feed_discord' %}"
|
<a href="{% url 'kick:campaign_feed_discord' %}"
|
||||||
title="Discord feed for all campaigns">[discord]</a>
|
title="Discord feed for all campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
<form method="get" action="{% url 'kick:campaign_list' %}">
|
<form method="get" action="{% url 'kick:campaign_list' %}">
|
||||||
<div style="display: flex;
|
<div style="display: flex;
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
title="Atom feed for {{ category.name }} campaigns">[atom]</a>
|
title="Atom feed for {{ category.name }} campaigns">[atom]</a>
|
||||||
<a href="{% url 'kick:game_campaign_feed_discord' category.kick_id %}"
|
<a href="{% url 'kick:game_campaign_feed_discord' category.kick_id %}"
|
||||||
title="Discord feed for {{ category.name }} campaigns">[discord]</a>
|
title="Discord feed for {{ category.name }} campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
{% if category.kick_url %}
|
{% if category.kick_url %}
|
||||||
<p style="margin: 0.25rem 0;">
|
<p style="margin: 0.25rem 0;">
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@
|
||||||
title="Atom feed for all games">[atom]</a>
|
title="Atom feed for all games">[atom]</a>
|
||||||
<a href="{% url 'kick:game_feed_discord' %}"
|
<a href="{% url 'kick:game_feed_discord' %}"
|
||||||
title="Discord feed for all games">[discord]</a>
|
title="Discord feed for all games">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
{% if categories %}
|
{% if categories %}
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
title="Atom feed for all campaigns">[atom]</a>
|
title="Atom feed for all campaigns">[atom]</a>
|
||||||
<a href="{% url 'kick:campaign_feed_discord' %}"
|
<a href="{% url 'kick:campaign_feed_discord' %}"
|
||||||
title="Discord feed for all campaigns">[discord]</a>
|
title="Discord feed for all campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
{% if active_campaigns %}
|
{% if active_campaigns %}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
title="Atom feed for all organizations">[atom]</a>
|
title="Atom feed for all organizations">[atom]</a>
|
||||||
<a href="{% url 'kick:organization_feed_discord' %}"
|
<a href="{% url 'kick:organization_feed_discord' %}"
|
||||||
title="Discord feed for all organizations">[discord]</a>
|
title="Discord feed for all organizations">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
{% if orgs %}
|
{% if orgs %}
|
||||||
<ul>
|
<ul>
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,7 @@
|
||||||
title="Atom feed for {{ campaign.game.display_name }} campaigns">[atom]</a>
|
title="Atom feed for {{ campaign.game.display_name }} campaigns">[atom]</a>
|
||||||
<a href="{% url 'core:game_campaign_feed_discord' campaign.game.twitch_id %}"
|
<a href="{% url 'core:game_campaign_feed_discord' campaign.game.twitch_id %}"
|
||||||
title="Discord feed for {{ campaign.game.display_name }} campaigns">[discord]</a>
|
title="Discord feed for {{ campaign.game.display_name }} campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
title="Atom feed for all campaigns">[atom]</a>
|
title="Atom feed for all campaigns">[atom]</a>
|
||||||
<a href="{% url 'core:campaign_feed_discord' %}"
|
<a href="{% url 'core:campaign_feed_discord' %}"
|
||||||
title="Discord feed for all campaigns">[discord]</a>
|
title="Discord feed for all campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
<a href="{% url 'twitch:export_campaigns_csv' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}"
|
<a href="{% url 'twitch:export_campaigns_csv' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
title="Export campaigns as CSV">[csv]</a>
|
title="Export campaigns as CSV">[csv]</a>
|
||||||
<a href="{% url 'twitch:export_campaigns_json' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}"
|
<a href="{% url 'twitch:export_campaigns_json' %}{% if request.GET %}?{{ request.GET.urlencode }}{% endif %}"
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@
|
||||||
title="Atom feed for campaigns">[atom]</a>
|
title="Atom feed for campaigns">[atom]</a>
|
||||||
<a href="{% url 'core:campaign_feed_discord' %}"
|
<a href="{% url 'core:campaign_feed_discord' %}"
|
||||||
title="Discord feed for campaigns">[discord]</a>
|
title="Discord feed for campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
{% if campaigns_by_game %}
|
{% if campaigns_by_game %}
|
||||||
|
|
|
||||||
|
|
@ -6,99 +6,148 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<main>
|
<main>
|
||||||
<h1>RSS Feeds Documentation</h1>
|
<h1>RSS Feeds Documentation</h1>
|
||||||
<p>This page lists all available RSS feeds for TTVDrops.</p>
|
|
||||||
<p>
|
<p>
|
||||||
Atom feeds are also available for the same resources under the
|
You have three types of feeds available for Twitch drops data: RSS, Atom, and Discord.
|
||||||
<code>/atom/</code> endpoints.
|
RSS and Atom feeds are similar and can be used in any RSS reader application.
|
||||||
|
The main difference is that Atom feeds include additional metadata and support for more complex content, while RSS feeds are more widely supported by older applications.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
Discord feeds are available under the <code>/discord/</code> endpoints. These are Atom feeds
|
Discord feeds are available under the <code>/discord/</code> endpoints. These are Atom feeds
|
||||||
that include Discord relative timestamps (e.g., <code><t:1773450272:R></code>) for dates,
|
that include Discord relative timestamps (e.g., <code><t:1773450272:R></code>) for dates,
|
||||||
making them ideal for Discord bots and integrations.
|
making them ideal for Discord bots and integrations. Future enhancements may include Discord-specific formatting or content.
|
||||||
</p>
|
</p>
|
||||||
<section>
|
<section>
|
||||||
<h2>Global RSS Feeds</h2>
|
<h2>Global RSS Feeds</h2>
|
||||||
<p>These feeds contain all items across the entire site:</p>
|
<table>
|
||||||
<ul>
|
<thead>
|
||||||
{% for feed in feeds %}
|
<tr>
|
||||||
<li>
|
<th>Description</th>
|
||||||
<h3>{{ feed.title }}</h3>
|
<th>RSS</th>
|
||||||
<p>{{ feed.description }}</p>
|
<th>Atom</th>
|
||||||
<p>
|
<th>Discord</th>
|
||||||
<a href="{{ feed.url }}">Subscribe to {{ feed.title }} RSS Feed</a>
|
</tr>
|
||||||
{% if feed.atom_url %}
|
</thead>
|
||||||
|
|
<tbody>
|
||||||
<a href="{{ feed.atom_url }}">Subscribe to {{ feed.title }} Atom Feed</a>
|
<tr>
|
||||||
{% endif %}
|
<td>New Twitch games</td>
|
||||||
{% if feed.discord_url %}
|
<td>
|
||||||
|
|
<a href="https://ttvdrops.lovinator.space/rss/games/">https://ttvdrops.lovinator.space/rss/games/</a>
|
||||||
<a href="{{ feed.discord_url }}">Subscribe to {{ feed.title }} Discord Feed</a>
|
</td>
|
||||||
{% endif %}
|
<td>
|
||||||
</p>
|
<a href="https://ttvdrops.lovinator.space/atom/games/">https://ttvdrops.lovinator.space/atom/games/</a>
|
||||||
<pre><code class="language-xml">{% if feed.example_xml %}{{ feed.example_xml|escape }}{% else %}No example XML available yet.{% endif %}</code></pre>
|
</td>
|
||||||
{% if feed.example_xml_atom %}
|
<td>
|
||||||
<h4>Atom example</h4>
|
<a href="https://ttvdrops.lovinator.space/discord/games/">https://ttvdrops.lovinator.space/discord/games/</a>
|
||||||
<pre><code class="language-xml">{{ feed.example_xml_atom|escape }}</code></pre>
|
</td>
|
||||||
{% endif %}
|
</tr>
|
||||||
{% if feed.example_xml_discord %}
|
<tr>
|
||||||
<h4>Discord example</h4>
|
<td>Latest Twitch drop campaigns</td>
|
||||||
<pre><code class="language-xml">{{ feed.example_xml_discord|escape }}</code></pre>
|
<td>
|
||||||
{% endif %}
|
<a href="https://ttvdrops.lovinator.space/rss/campaigns/">https://ttvdrops.lovinator.space/rss/campaigns/</a>
|
||||||
</li>
|
</td>
|
||||||
{% endfor %}
|
<td>
|
||||||
</ul>
|
<a href="https://ttvdrops.lovinator.space/atom/campaigns/">https://ttvdrops.lovinator.space/atom/campaigns/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/discord/campaigns/">https://ttvdrops.lovinator.space/discord/campaigns/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Latest Twitch organizations</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/rss/organizations/">https://ttvdrops.lovinator.space/rss/organizations/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/atom/organizations/">https://ttvdrops.lovinator.space/atom/organizations/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/discord/organizations/">https://ttvdrops.lovinator.space/discord/organizations/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Latest Twitch reward campaigns</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/rss/reward-campaigns/">https://ttvdrops.lovinator.space/rss/reward-campaigns/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/atom/reward-campaigns/">https://ttvdrops.lovinator.space/atom/reward-campaigns/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/discord/reward-campaigns/">https://ttvdrops.lovinator.space/discord/reward-campaigns/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Latest Kick campaigns</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/kick/rss/campaigns/">https://ttvdrops.lovinator.space/kick/rss/campaigns/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/kick/atom/campaigns/">https://ttvdrops.lovinator.space/kick/atom/campaigns/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/discord/campaigns/">https://ttvdrops.lovinator.space/discord/campaigns/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Latest Kick games</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/kick/rss/games/">https://ttvdrops.lovinator.space/kick/rss/games/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/kick/atom/games/">https://ttvdrops.lovinator.space/kick/atom/games/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/discord/games/">https://ttvdrops.lovinator.space/discord/games/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Latest Kick organizations</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/kick/rss/organizations/">https://ttvdrops.lovinator.space/kick/rss/organizations/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/kick/atom/organizations/">https://ttvdrops.lovinator.space/kick/atom/organizations/</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="https://ttvdrops.lovinator.space/discord/organizations/">https://ttvdrops.lovinator.space/discord/organizations/</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2>Filtered RSS Feeds</h2>
|
<h2>Filtered RSS Feeds</h2>
|
||||||
<p>
|
<p>You can subscribe to RSS feeds scoped to a specific game.</p>
|
||||||
You can subscribe to RSS feeds scoped to a specific game or organization. When available, links below point to live examples; otherwise use the endpoint template.
|
<table>
|
||||||
</p>
|
<thead>
|
||||||
<ul>
|
<tr>
|
||||||
{% for feed in filtered_feeds %}
|
<th>Game</th>
|
||||||
<li>
|
<th>RSS</th>
|
||||||
<h3>{{ feed.title }}</h3>
|
<th>Atom</th>
|
||||||
<p>{{ feed.description }}</p>
|
<th>Discord</th>
|
||||||
<p>
|
</tr>
|
||||||
Endpoint: <code>{{ feed.url }}</code>
|
</thead>
|
||||||
{% if feed.atom_url %} | Atom: <code>{{ feed.atom_url }}</code>{% endif %}
|
<tbody>
|
||||||
{% if feed.discord_url %} | Discord: <code>{{ feed.discord_url }}</code>{% endif %}
|
<tr>
|
||||||
</p>
|
<td>{{ game.display_name }}</td>
|
||||||
{% if feed.has_sample %}
|
<td>
|
||||||
<p>
|
<a href="{% url 'core:game_campaign_feed' game.twitch_id %}">
|
||||||
<a href="{{ feed.url }}">View a live example</a>
|
https://ttvdrops.lovinator.space/rss/games/{{ game.twitch_id }}/
|
||||||
{% if feed.atom_url %}
|
</a>
|
||||||
|
|
</td>
|
||||||
<a href="{{ feed.atom_url }}">View Atom example</a>
|
<td>
|
||||||
{% endif %}
|
<a href="{% url 'core:game_campaign_feed_atom' game.twitch_id %}">
|
||||||
{% if feed.discord_url %}
|
https://ttvdrops.lovinator.space/atom/games/{{ game.twitch_id }}/
|
||||||
|
|
</a>
|
||||||
<a href="{{ feed.discord_url }}">View Discord example</a>
|
</td>
|
||||||
{% endif %}
|
<td>
|
||||||
</p>
|
<a href="{% url 'core:game_campaign_feed_discord' game.twitch_id %}">
|
||||||
{% endif %}
|
https://ttvdrops.lovinator.space/discord/games/{{ game.twitch_id }}/
|
||||||
<pre><code class="language-xml">{% if feed.example_xml %}{{ feed.example_xml|escape }}{% else %}No example XML available yet.{% endif %}</code></pre>
|
</a>
|
||||||
{% if feed.example_xml_atom %}
|
</td>
|
||||||
<h4>Atom example</h4>
|
</tr>
|
||||||
<pre><code class="language-xml">{{ feed.example_xml_atom|escape }}</code></pre>
|
</tbody>
|
||||||
{% endif %}
|
</table>
|
||||||
{% if feed.example_xml_discord %}
|
|
||||||
<h4>Discord example</h4>
|
|
||||||
<pre><code class="language-xml">{{ feed.example_xml_discord|escape }}</code></pre>
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
<section>
|
|
||||||
<h2>How to Use RSS Feeds</h2>
|
|
||||||
<p>
|
|
||||||
RSS feeds allow you to stay updated with new content. You can use any RSS reader application to subscribe to these feeds.
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li>Copy the feed URL</li>
|
|
||||||
<li>Paste it into your favorite RSS reader (Feedly, Inoreader, NetNewsWire, etc.)</li>
|
|
||||||
<li>Get automatic updates when new content is added</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@
|
||||||
title="Atom feed for {{ game.display_name }} campaigns">[atom]</a>
|
title="Atom feed for {{ game.display_name }} campaigns">[atom]</a>
|
||||||
<a href="{% url 'core:game_campaign_feed_discord' game.twitch_id %}"
|
<a href="{% url 'core:game_campaign_feed_discord' game.twitch_id %}"
|
||||||
title="Discord feed for {{ game.display_name }} campaigns">[discord]</a>
|
title="Discord feed for {{ game.display_name }} campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
title="Atom feed for all games">[atom]</a>
|
title="Atom feed for all games">[atom]</a>
|
||||||
<a href="{% url 'core:game_feed_discord' %}"
|
<a href="{% url 'core:game_feed_discord' %}"
|
||||||
title="Discord feed for all games">[discord]</a>
|
title="Discord feed for all games">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
<a href="{% url 'twitch:export_games_csv' %}"
|
<a href="{% url 'twitch:export_games_csv' %}"
|
||||||
title="Export all games as CSV">[csv]</a>
|
title="Export all games as CSV">[csv]</a>
|
||||||
<a href="{% url 'twitch:export_games_json' %}"
|
<a href="{% url 'twitch:export_games_json' %}"
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
title="Atom feed for all games">[atom]</a>
|
title="Atom feed for all games">[atom]</a>
|
||||||
<a href="{% url 'core:game_feed_discord' %}"
|
<a href="{% url 'core:game_feed_discord' %}"
|
||||||
title="Discord feed for all games">[discord]</a>
|
title="Discord feed for all games">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
<a href="{% url 'twitch:export_games_csv' %}"
|
<a href="{% url 'twitch:export_games_csv' %}"
|
||||||
title="Export all games as CSV">[csv]</a>
|
title="Export all games as CSV">[csv]</a>
|
||||||
<a href="{% url 'twitch:export_games_json' %}"
|
<a href="{% url 'twitch:export_games_json' %}"
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
title="Atom feed for all organizations">[atom]</a>
|
title="Atom feed for all organizations">[atom]</a>
|
||||||
<a href="{% url 'core:organization_feed_discord' %}"
|
<a href="{% url 'core:organization_feed_discord' %}"
|
||||||
title="Discord feed for all organizations">[discord]</a>
|
title="Discord feed for all organizations">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
<a href="{% url 'twitch:export_organizations_csv' %}"
|
<a href="{% url 'twitch:export_organizations_csv' %}"
|
||||||
title="Export all organizations as CSV">[csv]</a>
|
title="Export all organizations as CSV">[csv]</a>
|
||||||
<a href="{% url 'twitch:export_organizations_json' %}"
|
<a href="{% url 'twitch:export_organizations_json' %}"
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,12 @@
|
||||||
<div style="margin-bottom: 1rem;">
|
<div style="margin-bottom: 1rem;">
|
||||||
<a href="{% url 'core:reward_campaign_feed' %}"
|
<a href="{% url 'core:reward_campaign_feed' %}"
|
||||||
style="margin-right: 1rem"
|
style="margin-right: 1rem"
|
||||||
title="RSS feed for all reward campaigns">RSS feed for all reward campaigns</a>
|
title="RSS feed for all reward campaigns">[rss]</a>
|
||||||
<a href="{% url 'core:reward_campaign_feed_atom' %}"
|
<a href="{% url 'core:reward_campaign_feed_atom' %}"
|
||||||
title="Atom feed for all reward campaigns">[atom]</a>
|
title="Atom feed for all reward campaigns">[atom]</a>
|
||||||
<a href="{% url 'core:reward_campaign_feed_discord' %}"
|
<a href="{% url 'core:reward_campaign_feed_discord' %}"
|
||||||
title="Discord feed for all reward campaigns">[discord]</a>
|
title="Discord feed for all reward campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
<!-- Campaign Summary -->
|
<!-- Campaign Summary -->
|
||||||
{% if reward_campaign.summary %}<p id="campaign-summary">{{ reward_campaign.summary|linebreaksbr }}</p>{% endif %}
|
{% if reward_campaign.summary %}<p id="campaign-summary">{{ reward_campaign.summary|linebreaksbr }}</p>{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
title="Atom feed for all reward campaigns">[atom]</a>
|
title="Atom feed for all reward campaigns">[atom]</a>
|
||||||
<a href="{% url 'core:reward_campaign_feed_discord' %}"
|
<a href="{% url 'core:reward_campaign_feed_discord' %}"
|
||||||
title="Discord feed for all reward campaigns">[discord]</a>
|
title="Discord feed for all reward campaigns">[discord]</a>
|
||||||
|
<a href="{% url 'core:docs_rss' %}" title="RSS feed documentation">[explain]</a>
|
||||||
</div>
|
</div>
|
||||||
<p>This is an archive of old Twitch reward campaigns because we do not monitor them.</p>
|
<p>This is an archive of old Twitch reward campaigns because we do not monitor them.</p>
|
||||||
<p>
|
<p>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue