Add support for Reward campaigns

This commit is contained in:
2024-07-31 03:54:07 +02:00
parent 5433e1d9ce
commit 354d66f7bc
24 changed files with 765 additions and 576 deletions

View File

@ -7,6 +7,11 @@
<div class="col-lg-9">
{% include "partials/info_box.html" %}
{% include "partials/news.html" %}
<h2>Reward Campaigns</h2>
{% for campaign in reward_campaigns %}
{% include "partials/reward_campaign_card.html" %}
{% endfor %}
<h2>Organizations</h2>
{% for org in orgs %}
<h2 id="org-{{ org|slugify }}">
<a href="#org-{{ org|slugify }}">{{ org }}</a>

View File

@ -14,7 +14,7 @@
<a class="nav-link" href="https://github.com/sponsors/TheLovinator1">Donate</a>
</li>
<li>
<a class="nav-link" href='{% url "core:webhooks" %}'>Webhooks</a>
<a class="nav-link" href='{% url "core:reward_campaigns" %}'>Reward campaigns</a>
</li>
</ul>
</nav>

View File

@ -7,9 +7,6 @@
<p>
This site allows users to subscribe to Twitch drops notifications. You can choose to be alerted when new drops are found on Twitch or when the drops become available for farming.
</p>
<p>
You can add a Discord Webhook <a href="{% url 'core:webhooks' %}">here</a>.
</p>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
<div class="card mb-4 shadow-sm" id="campaign-{{ campaign.id }}">
<div class="row g-0">
<div class="col-md-2">
<img src="{{ campaign.image.image1_x_url }}"
alt="{{ campaign.name }}"
class="img-fluid rounded-start"
height="283"
width="212"
loading="lazy">
</div>
<div class="col-md-10">
<div class="card-body">
<h2 class="card-title h5">{{ campaign.name }}</h2>
<p class="card-text text-muted">{{ campaign.summary }}</p>
<p>
Starts at: <abbr title="{{ campaign.starts_at|date:'l d F H:i e' }}">{{ campaign.starts_at }}</abbr>
<br>
Ends at: <abbr title="{{ campaign.ends_at|date:'l d F H:i e' }}">{{ campaign.ends_at|timeuntil }}</abbr>
</p>
<a href="{{ campaign.external_url }}"
class="btn btn-primary"
target="_blank">Learn More</a>
{% if campaign.instructions %}
<div class="mt-3">
<h3 class="h6">Instructions</h3>
<p>{{ campaign.instructions }}</p>
</div>
{% endif %}
{% if campaign.rewards.exists %}
<div class="mt-3">
<h3 class="h6">Rewards</h3>
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
{% for reward in campaign.rewards.all %}
<div class="col d-flex align-items-center position-relative">
<img src="{{ reward.thumbnail_image.image1_x_url }}"
alt="{{ reward.name }} reward image"
class="img-fluid rounded me-3"
height="50"
width="50"
loading="lazy">
<div>
<strong>{{ reward.name }}</strong>
<br>
<a href="{{ reward.redemption_url }}"
class="btn btn-sm btn-link"
target="_blank">Redeem</a>
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,14 @@
{% extends "base.html" %}
{% load static %}
{% block content %}
<div class="container mt-4">
<div class="row">
<h2>Reward Campaigns</h2>
<div>
{% for campaign in reward_campaigns %}
{% include "partials/reward_campaign_card.html" %}
{% endfor %}
</div>
</div>
</div>
{% endblock content %}

View File

@ -24,35 +24,5 @@
</form>
</div>
<h2 class="mt-5">Webhooks</h2>
{% if webhooks %}
<ul class="list-group mt-3">
{% for webhook in webhooks %}
<li class="list-group-item d-flex justify-content-between align-items-center">
<div>
<img src="{{ webhook.avatar }}?size=32"
alt="{{ webhook.name }}"
class="rounded-circle"
height="32"
width="32">
<a href="{{ webhook.url }}" target="_blank">{{ webhook.name }}</a>
{% if webhook.status == 'Success' %}
<span class="badge bg-success">Working</span>
{% else %}
<span class="badge bg-danger">Failed</span>
{% endif %}
</div>
<form method="post" action="" class="mb-0">
{% csrf_token %}
<input type="hidden" name="webhook_id" value="{{ webhook.id }}">
<input type="hidden" name="webhook_name" value="{{ webhook.name }}">
<input type="hidden" name="webhook_url" value="{{ webhook.webhook_url }}">
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
</form>
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">No webhooks added yet.</p>
{% endif %}
</div>
{% endblock content %}

View File

@ -2,9 +2,7 @@ from __future__ import annotations
from django.urls import URLPattern, URLResolver, path
from .views.games import GameView
from .views.index import index
from .views.webhooks import WebhooksView
from core.views import GameView, RewardCampaignView, index
app_name: str = "core"
@ -15,5 +13,9 @@ urlpatterns: list[URLPattern | URLResolver] = [
view=GameView.as_view(),
name="games",
),
path("webhooks/", WebhooksView.as_view(), name="webhooks"),
path(
route="reward_campaigns/",
view=RewardCampaignView.as_view(),
name="reward_campaigns",
),
]

View File

@ -5,15 +5,16 @@ from typing import TYPE_CHECKING
import hishel
from django.conf import settings
from django.db.models.manager import BaseManager
from django.template.response import TemplateResponse
from django.views.generic import ListView
from core.data import WebhookData
from twitch_app.models import Organization
from twitch_app.models import Game, RewardCampaign
if TYPE_CHECKING:
from pathlib import Path
from django.db.models.manager import BaseManager
from django.http import HttpRequest, HttpResponse
from httpx import Response
@ -59,11 +60,24 @@ def get_webhook_data(webhook: str) -> WebhookData:
def index(request: HttpRequest) -> HttpResponse:
"""Render the index page."""
orgs: BaseManager[Organization] = Organization.objects.all()
webhooks: list[WebhookData] = [get_webhook_data(webhook) for webhook in get_webhooks(request)]
reward_campaigns: BaseManager[RewardCampaign] = RewardCampaign.objects.all()
return TemplateResponse(
request=request,
template="index.html",
context={"orgs": orgs, "webhooks": webhooks},
context={"reward_campaigns": reward_campaigns},
)
class GameView(ListView):
model = Game
template_name: str = "games.html"
context_object_name: str = "games"
paginate_by = 100
class RewardCampaignView(ListView):
model = RewardCampaign
template_name: str = "reward_campaigns.html"
context_object_name: str = "reward_campaigns"
paginate_by = 100

View File

View File

@ -1,16 +0,0 @@
from __future__ import annotations
import logging
from django.views.generic import ListView
from twitch_app.models import Game
logger: logging.Logger = logging.getLogger(__name__)
class GameView(ListView):
model = Game
template_name: str = "games.html"
context_object_name: str = "games"
paginate_by = 100

View File

@ -1,118 +0,0 @@
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
import hishel
from django.conf import settings
from django.contrib import messages
from django.http.response import HttpResponse
from django.views.generic import FormView
from core.data import WebhookData
from core.forms import DiscordSettingForm
from twitch_app.models import Game
if TYPE_CHECKING:
from pathlib import Path
from django.http import HttpRequest
cache_dir: Path = settings.DATA_DIR / "cache"
cache_dir.mkdir(exist_ok=True, parents=True)
storage = hishel.FileStorage(base_path=cache_dir)
controller = hishel.Controller(
cacheable_status_codes=[200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414, 501],
allow_stale=True,
always_revalidate=True,
)
if TYPE_CHECKING:
from django.http import (
HttpResponse,
)
from httpx import Response
logger: logging.Logger = logging.getLogger(__name__)
def get_webhooks(request: HttpRequest) -> list[str]:
"""Get the webhooks from the cookie."""
cookie: str = request.COOKIES.get("webhooks", "")
return list(filter(None, cookie.split(",")))
def get_avatar(webhook_response: Response) -> str:
"""Get the avatar URL from the webhook response."""
avatar: str = "https://cdn.discordapp.com/embed/avatars/0.png"
if webhook_response.is_success and webhook_response.json().get("id") and webhook_response.json().get("avatar"):
avatar = f'https://cdn.discordapp.com/avatars/{webhook_response.json().get("id")}/{webhook_response.json().get("avatar")}.png'
return avatar
def get_webhook_data(webhook: str) -> WebhookData:
"""Get the webhook data."""
with hishel.CacheClient(storage=storage, controller=controller) as client:
webhook_response: Response = client.get(url=webhook, extensions={"cache_metadata": True})
return WebhookData(
name=webhook_response.json().get("name") if webhook_response.is_success else "Unknown",
url=webhook,
avatar=get_avatar(webhook_response),
status="Success" if webhook_response.is_success else "Failed",
response=webhook_response.text,
)
class WebhooksView(FormView):
model = Game
template_name = "webhooks.html"
form_class = DiscordSettingForm
context_object_name: str = "webhooks"
paginate_by = 100
def get_context_data(self: WebhooksView, **kwargs: dict[str, WebhooksView] | DiscordSettingForm) -> dict[str, Any]:
"""Get the context data for the view."""
context: dict[str, DiscordSettingForm | list[WebhookData]] = super().get_context_data(**kwargs)
webhooks: list[str] = get_webhooks(self.request)
context.update({
"webhooks": [get_webhook_data(webhook) for webhook in webhooks],
"form": DiscordSettingForm(),
})
return context
def form_valid(self: WebhooksView, form: DiscordSettingForm) -> HttpResponse:
"""Handle valid form submission."""
webhook = str(form.cleaned_data["webhook_url"])
with hishel.CacheClient(storage=storage, controller=controller) as client:
webhook_response: Response = client.get(url=webhook, extensions={"cache_metadata": True})
if not webhook_response.is_success:
messages.error(self.request, "Failed to get webhook information. Is the URL correct?")
return self.render_to_response(self.get_context_data(form=form))
webhook_name: str | None = str(webhook_response.json().get("name")) if webhook_response.is_success else None
cookie: str = self.request.COOKIES.get("webhooks", "")
webhooks: list[str] = cookie.split(",")
webhooks = list(filter(None, webhooks))
if webhook in webhooks:
if webhook_name:
messages.error(self.request, f"Webhook {webhook_name} already exists.")
else:
messages.error(self.request, "Webhook already exists.")
return self.render_to_response(self.get_context_data(form=form))
webhooks.append(webhook)
response: HttpResponse = self.render_to_response(self.get_context_data(form=form))
response.set_cookie(key="webhooks", value=",".join(webhooks), max_age=315360000) # 10 years
messages.success(self.request, "Webhook successfully added.")
return response
def form_invalid(self: WebhooksView, form: DiscordSettingForm) -> HttpResponse:
messages.error(self.request, "Failed to add webhook.")
return self.render_to_response(self.get_context_data(form=form))