213 lines
6.5 KiB
Python
213 lines
6.5 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC
|
|
from typing import TYPE_CHECKING
|
|
from typing import Any
|
|
from typing import Literal
|
|
|
|
from django.http import JsonResponse
|
|
from django.utils import timezone
|
|
from django.views.decorators.http import require_GET
|
|
|
|
from kick.models import KickDropCampaign
|
|
|
|
if TYPE_CHECKING:
|
|
from datetime import datetime
|
|
|
|
from django.db.models import QuerySet
|
|
from django.http import HttpRequest
|
|
|
|
from kick.models import KickCategory
|
|
from kick.models import KickChannel
|
|
from kick.models import KickOrganization
|
|
from kick.models import KickReward
|
|
from kick.models import KickUser
|
|
|
|
CampaignStatus = Literal["active", "upcoming", "expired", "unknown"]
|
|
VALID_STATUS_FILTERS: frozenset[str] = frozenset({"active", "upcoming", "expired"})
|
|
DEFAULT_PAGE_SIZE = 100
|
|
MAX_PAGE_SIZE = 500
|
|
|
|
|
|
def _datetime_to_json(value: datetime | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return value.astimezone(UTC).isoformat().replace("+00:00", "Z")
|
|
|
|
|
|
def _campaign_status(campaign: KickDropCampaign, now: datetime) -> CampaignStatus:
|
|
if campaign.starts_at and campaign.ends_at:
|
|
if campaign.starts_at <= now <= campaign.ends_at:
|
|
return "active"
|
|
if campaign.starts_at > now:
|
|
return "upcoming"
|
|
return "expired"
|
|
return "unknown"
|
|
|
|
|
|
def _serialize_category(category: KickCategory | None) -> dict[str, Any] | None:
|
|
if category is None:
|
|
return None
|
|
return {
|
|
"kick_id": category.kick_id,
|
|
"name": category.name,
|
|
"slug": category.slug,
|
|
"image_url": category.image_url,
|
|
}
|
|
|
|
|
|
def _serialize_organization(
|
|
organization: KickOrganization | None,
|
|
) -> dict[str, Any] | None:
|
|
if organization is None:
|
|
return None
|
|
return {
|
|
"kick_id": organization.kick_id,
|
|
"name": organization.name,
|
|
"logo_url": organization.logo_url,
|
|
"url": organization.url,
|
|
}
|
|
|
|
|
|
def _serialize_user(user: KickUser | None) -> dict[str, Any] | None:
|
|
if user is None:
|
|
return None
|
|
return {
|
|
"kick_id": user.kick_id,
|
|
"username": user.username,
|
|
"profile_picture": user.profile_picture,
|
|
}
|
|
|
|
|
|
def _serialize_channel(channel: KickChannel) -> dict[str, Any]:
|
|
return {
|
|
"kick_id": channel.kick_id,
|
|
"slug": channel.slug,
|
|
"url": channel.channel_url,
|
|
"user": _serialize_user(channel.user),
|
|
}
|
|
|
|
|
|
def _serialize_reward(reward: KickReward) -> dict[str, Any]:
|
|
return {
|
|
"kick_id": reward.kick_id,
|
|
"name": reward.name,
|
|
"image_url": reward.full_image_url,
|
|
"required_minutes_watched": reward.required_units,
|
|
}
|
|
|
|
|
|
def _serialize_campaign(
|
|
campaign: KickDropCampaign,
|
|
now: datetime,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"kick_id": campaign.kick_id,
|
|
"name": campaign.name,
|
|
"status": _campaign_status(campaign, now),
|
|
"image_url": campaign.image_url,
|
|
"connect_url": campaign.connect_url,
|
|
"url": campaign.url,
|
|
"starts_at": _datetime_to_json(campaign.starts_at),
|
|
"ends_at": _datetime_to_json(campaign.ends_at),
|
|
"category": _serialize_category(campaign.category),
|
|
"organization": _serialize_organization(campaign.organization),
|
|
"channels": [
|
|
_serialize_channel(channel) for channel in campaign.channels.all()
|
|
],
|
|
"rewards": [_serialize_reward(reward) for reward in campaign.rewards.all()],
|
|
"is_fully_imported": campaign.is_fully_imported,
|
|
"added_at": _datetime_to_json(campaign.added_at),
|
|
"updated_at": _datetime_to_json(campaign.updated_at),
|
|
}
|
|
|
|
|
|
def _parse_integer_query(
|
|
request: HttpRequest,
|
|
name: str,
|
|
default: int,
|
|
) -> int | JsonResponse:
|
|
raw_value: str | None = request.GET.get(name)
|
|
if raw_value is None:
|
|
return default
|
|
try:
|
|
return int(raw_value)
|
|
except ValueError:
|
|
return JsonResponse(
|
|
{
|
|
"detail": [
|
|
{
|
|
"type": "int_parsing",
|
|
"loc": ["query", name],
|
|
"msg": "Input should be a valid integer",
|
|
},
|
|
],
|
|
},
|
|
status=422,
|
|
)
|
|
|
|
|
|
@require_GET
|
|
def campaign_list_api(request: HttpRequest) -> JsonResponse:
|
|
"""Return a paginated JSON representation of fully imported Kick campaigns."""
|
|
page = _parse_integer_query(request, "page", 1)
|
|
if isinstance(page, JsonResponse):
|
|
return page
|
|
page = max(page, 1)
|
|
|
|
page_size = _parse_integer_query(request, "page_size", DEFAULT_PAGE_SIZE)
|
|
if isinstance(page_size, JsonResponse):
|
|
return page_size
|
|
page_size = min(max(page_size, 1), MAX_PAGE_SIZE)
|
|
|
|
status_filter: str | None = request.GET.get("status")
|
|
if status_filter is not None and status_filter not in VALID_STATUS_FILTERS:
|
|
return JsonResponse(
|
|
{
|
|
"detail": [
|
|
{
|
|
"type": "literal_error",
|
|
"loc": ["query", "status"],
|
|
"msg": "Input should be 'active', 'upcoming' or 'expired'",
|
|
},
|
|
],
|
|
},
|
|
status=422,
|
|
)
|
|
|
|
now: datetime = timezone.now()
|
|
queryset: QuerySet[KickDropCampaign] = (
|
|
KickDropCampaign.objects
|
|
.filter(is_fully_imported=True)
|
|
.select_related("category", "organization")
|
|
.prefetch_related("channels__user", "rewards")
|
|
.order_by("-starts_at", "kick_id")
|
|
)
|
|
|
|
game_filter: int | JsonResponse | None = None
|
|
if request.GET.get("game") is not None:
|
|
game_filter = _parse_integer_query(request, "game", 0)
|
|
if isinstance(game_filter, JsonResponse):
|
|
return game_filter
|
|
if game_filter is not None:
|
|
queryset = queryset.filter(category__kick_id=game_filter)
|
|
|
|
if status_filter == "active":
|
|
queryset = queryset.filter(starts_at__lte=now, ends_at__gte=now)
|
|
elif status_filter == "upcoming":
|
|
queryset = queryset.filter(starts_at__gt=now, ends_at__isnull=False)
|
|
elif status_filter == "expired":
|
|
queryset = queryset.filter(starts_at__lte=now, ends_at__lt=now)
|
|
|
|
total: int = queryset.count()
|
|
offset: int = (page - 1) * page_size
|
|
campaigns: list[KickDropCampaign] = (
|
|
list(queryset[offset : offset + page_size]) if offset < total else []
|
|
)
|
|
|
|
return JsonResponse({
|
|
"total": total,
|
|
"page": page,
|
|
"page_size": page_size,
|
|
"items": [_serialize_campaign(campaign, now) for campaign in campaigns],
|
|
})
|