Files
twitch-drop-notifier/core/models.py
2024-08-10 21:31:21 +02:00

644 lines
25 KiB
Python

from __future__ import annotations
import logging
import typing
import auto_prefetch
from django.db import models
logger: logging.Logger = logging.getLogger(__name__)
class Game(auto_prefetch.Model):
"""The game that the reward is for.
Optional for reward campaigns. Required for drop campaigns.
Attributes:
id (str): The primary key of the game.
slug (str): The slug identifier of the game.
display_name (str): The display name of the game.
typename (str): The type name of the object, typically "Game".
Example JSON data:
{
"data": {
"currentUser": {
"dropCampaigns": [
{
"game": {
"id": "263490",
"slug": "rust",
"displayName": "Rust",
"__typename": "Game"
}
}
]
}
}
}
"""
id = models.TextField(primary_key=True, unique=True, help_text="The game ID.", verbose_name="Game ID")
slug = models.TextField(null=True, blank=True, help_text="Slug used for building URL where all the streams are.")
display_name = models.TextField(
null=True,
blank=True,
help_text="Game name.",
default="Unknown Game",
verbose_name="Game Name",
)
typename = models.TextField(null=True, blank=True, help_text="Always 'Game'.", verbose_name="Type Name")
# Only used for reward campaigns?
box_art_url = models.URLField(
null=True,
blank=True,
help_text="URL to the box art of the game.",
default="https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg",
verbose_name="Box Art URL",
)
def __str__(self) -> str:
return self.display_name or "Unknown"
def get_twitch_url(self) -> str:
if not self.slug:
logger.error("Game %s has no slug", self.display_name)
return "https://www.twitch.tv/"
return f"https://www.twitch.tv/directory/game/{self.slug}"
class Image(auto_prefetch.Model):
"""An image model representing URLs and type.
Attributes:
image1_x_url (str): URL to the image.
typename (str): The type name of the object, typically "RewardCampaignImageSet".
JSON example:
{
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png",
"__typename": "RewardCampaignImageSet"
}
"""
image1_x_url = models.URLField(null=True, blank=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.image1_x_url or "Unknown"
class Reward(auto_prefetch.Model):
"""The actual reward you get when you complete the requirements.
Attributes:
id (str): The primary key of the reward.
name (str): The name of the reward.
banner_image (Image): The banner image associated with the reward.
thumbnail_image (Image): The thumbnail image associated with the reward.
earnable_until (datetime): The date and time until the reward can be earned.
redemption_instructions (str): Instructions on how to redeem the reward.
redemption_url (str): URL for redeeming the reward.
typename (str): The type name of the object, typically "Reward".
JSON example:
{
"id": "374628c6-34b4-11ef-a468-62ece0f03426",
"name": "Twitchy Character Skin",
"bannerImage": {
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
"__typename": "RewardCampaignImageSet"
},
"thumbnailImage": {
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
"__typename": "RewardCampaignImageSet"
},
"earnableUntil": "2024-07-30T06:59:59Z",
"redemptionInstructions": "",
"redemptionURL": "https://redeem.ubisoft.com/xdefiant/",
"__typename": "Reward"
}
"""
id = models.TextField(primary_key=True)
name = models.TextField(null=True, blank=True)
banner_image = auto_prefetch.ForeignKey(Image, related_name="banner_rewards", on_delete=models.CASCADE, null=True)
thumbnail_image = auto_prefetch.ForeignKey(
Image,
related_name="thumbnail_rewards",
on_delete=models.CASCADE,
null=True,
)
earnable_until = models.DateTimeField(null=True)
redemption_instructions = models.TextField(null=True, blank=True)
redemption_url = models.URLField(null=True, blank=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.name or "Unknown"
class UnlockRequirements(auto_prefetch.Model):
"""Requirements to unlock a reward.
Attributes:
subs_goal (int): The number of subscriptions needed to unlock the reward.
minute_watched_goal (int): The number of minutes watched needed to unlock the reward.
typename (str): The type name of the object, typically "QuestRewardUnlockRequirements".
JSON example:
{
"subsGoal": 2,
"minuteWatchedGoal": 0,
"__typename": "QuestRewardUnlockRequirements"
}
"""
subs_goal = models.TextField(null=True)
minute_watched_goal = models.TextField(null=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return f"{self.subs_goal} subs and {self.minute_watched_goal} minutes watched"
class RewardCampaign(auto_prefetch.Model):
"""Represents a reward campaign.
Attributes:
id (str): The primary key of the reward campaign.
name (str): The name of the reward campaign.
brand (str): The brand associated with the campaign.
starts_at (datetime): The start date and time of the campaign.
ends_at (datetime): The end date and time of the campaign.
status (str): The status of the campaign.
summary (str): A brief summary of the campaign.
instructions (str): Instructions for the campaign.
external_url (str): The external URL related to the campaign.
reward_value_url_param (str): URL parameter for the reward value.
about_url (str): URL with more information about the campaign.
is_sitewide (bool): Indicates if the campaign is sitewide.
game (Game): The game associated with the campaign.
image (Image): The image associated with the campaign.
rewards (ManyToManyField): The rewards available in the campaign.
typename (str): The type name of the object, typically "RewardCampaign".
JSON example:
{
"id": "3757a2ae-34b4-11ef-a468-62ece0f03426",
"name": "XDefiant Season 1 Launch",
"brand": "Ubisoft",
"startsAt": "2024-07-02T17:00:00Z",
"endsAt": "2024-07-30T06:59:59Z",
"status": "UNKNOWN",
"summary": "Get a redeemable code for the Twitchy Character Skin in XDefiant for gifting or purchasing 2 subscriptions of any tier to participating channels.",
"instructions": "",
"externalURL": "https://redeem.ubisoft.com/xdefiant/",
"rewardValueURLParam": "",
"aboutURL": "https://xdefiant.com/S1-twitch-rewards",
"isSitewide": false,
"game": {
"id": "780302568",
"slug": "xdefiant",
"displayName": "XDefiant",
"__typename": "Game"
},
"unlockRequirements": {
"subsGoal": 2,
"minuteWatchedGoal": 0,
"__typename": "QuestRewardUnlockRequirements"
},
"image": {
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png",
"__typename": "RewardCampaignImageSet"
},
"rewards": [
{
"id": "374628c6-34b4-11ef-a468-62ece0f03426",
"name": "Twitchy Character Skin",
"bannerImage": {
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
"__typename": "RewardCampaignImageSet"
},
"thumbnailImage": {
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/reward.png",
"__typename": "RewardCampaignImageSet"
},
"earnableUntil": "2024-07-30T06:59:59Z",
"redemptionInstructions": "",
"redemptionURL": "https://redeem.ubisoft.com/xdefiant/",
"__typename": "Reward"
}
],
"__typename": "RewardCampaign"
}
""" # noqa: E501
id = models.TextField(primary_key=True)
name = models.TextField(null=True, blank=True)
brand = models.TextField(null=True, blank=True)
starts_at = models.DateTimeField(null=True)
ends_at = models.DateTimeField(null=True)
status = models.TextField(null=True, blank=True)
summary = models.TextField(null=True, blank=True)
instructions = models.TextField(null=True, blank=True)
external_url = models.URLField(null=True, blank=True)
reward_value_url_param = models.TextField(null=True, blank=True)
about_url = models.URLField(null=True, blank=True)
is_sitewide = models.BooleanField(null=True)
game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
unlock_requirements = auto_prefetch.ForeignKey(
UnlockRequirements,
on_delete=models.CASCADE,
related_name="reward_campaigns",
null=True,
)
image = auto_prefetch.ForeignKey(Image, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
rewards = models.ManyToManyField(Reward, related_name="reward_campaigns")
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.name or "Unknown"
class Channel(auto_prefetch.Model):
"""Represents a Twitch channel.
Attributes:
id (int): The primary key of the channel.
display_name (str): The display name of the channel.
name (str): The name of the channel.
typename (str): The type name of the object, typically "Channel".
Example JSON data:
{
"data": {
"user": {
"dropCampaign": {
"allow": {
"channels": [
{
"id": "464161875",
"displayName": "Valair",
"name": "valair",
"__typename": "Channel"
}
]
}
}
}
}
}
"""
# Used in Drop Campaigns
id = models.TextField(primary_key=True)
display_name = models.TextField(null=True, blank=True)
name = models.TextField(null=True, blank=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.display_name or "Unknown Channel"
def get_twitch_url(self) -> str:
# TODO(TheLovinator): Use a field instead # noqa: TD003
return f"https://www.twitch.tv/{self.name}"
class Allow(auto_prefetch.Model):
"""List of channels that you can watch to earn rewards.
Attributes:
channels (ManyToManyField): The channels that you can watch to earn rewards.
is_enabled (bool): Indicates if the channel is enabled.
typename (str): The type name of the object, typically "RewardCampaignChannelAllow".
Example JSON data:
{
"data": {
"user": {
"dropCampaign": {
"allow": {
"channels": [],
"isEnabled": true,
"__typename": "DropCampaignACL"
}
}
}
}
}
"""
channels = models.ManyToManyField(Channel, related_name="allow")
is_enabled = models.BooleanField(default=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return f"{self.channels.count()} channels"
class Owner(auto_prefetch.Model):
"""Represents the owner of the reward campaign.
Used for:
- Reward campaigns
Attributes:
id (int): The primary key of the owner.
slug (str): The slug identifier of the owner.
display_name (str): The display name of the owner.
typename (str): The type name of the object, typically "Organization".
JSON example:
"owner": {
"id": "a1a51d5a-233d-41c3-9acd-a03bdab35159",
"name": "Out of the Park Developments",
"__typename": "Organization"
},
"""
id = models.TextField(primary_key=True, unique=True, help_text="The owner ID.")
name = models.TextField(null=True, blank=True, help_text="Owner name.")
slug = models.TextField(null=True, blank=True, help_text="Slug used for building URL where all the streams are.")
display_name = models.TextField(null=True, blank=True, help_text="Owner name.")
typename = models.TextField(null=True, blank=True, help_text="Always 'Organization'.")
def __str__(self) -> str:
return self.display_name or "Unknown"
def get_twitch_url(self) -> str:
if not self.slug:
logger.error("Owner %s has no slug", self.display_name)
return "https://www.twitch.tv/"
return f"https://www.twitch.tv/{self.slug}"
class Benefit(auto_prefetch.Model):
"""Represents a benefit that you can earn.
Attributes:
id (int): The primary key of the benefit.
created_at (datetime): The date and time the benefit was created.
entitlement_limit (int): The limit of entitlement.
game (Game): The game associated with the benefit.
image_asset_url (str): URL to the image asset.
is_ios_available (bool): Indicates if the benefit is available on iOS.
name (str): The name of the benefit.
owner_organization (Owner): The owner organization of the benefit.
typename (str): The type name of the object, typically "Benefit".
JSON example:
"benefit": {
"id": "6da09649-1fda-4446-a061-cacd8e21b886_CUSTOM_ID_S29_Torso008_01",
"createdAt": "2024-07-09T12:57:31.072Z",
"entitlementLimit": 1,
"game": {
"id": "491487",
"name": "Dead by Daylight",
"__typename": "Game"
},
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/ed4a7829-cc2b-44d3-90a4-f73ef7d8d636.png",
"isIosAvailable": false,
"name": "Unwanted Attention",
"ownerOrganization": {
"id": "6da09649-1fda-4446-a061-cacd8e21b886",
"name": "Behaviour Interactive Inc.",
"__typename": "Organization"
},
"__typename": "DropBenefit"
}
"""
id = models.TextField(primary_key=True)
created_at = models.DateTimeField(null=True)
entitlement_limit = models.TextField(null=True)
game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="benefits", null=True)
image_asset_url = models.URLField(null=True, blank=True)
is_ios_available = models.BooleanField(null=True)
name = models.TextField(null=True, blank=True)
owner_organization = auto_prefetch.ForeignKey(Owner, on_delete=models.CASCADE, related_name="benefits", null=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.name or "Unknown"
class BenefitEdge(auto_prefetch.Model):
"""Represents a benefit edge.
Attributes:
benefit (Benefit): The benefit associated with the edge.
entitlement_limit (int): The limit of entitlement.
typename (str): The type name of the object, typically "DropBenefitEdge".
JSON example:
"benefitEdges": [
{
"benefit": {
"id": "6da09649-1fda-4446-a061-cacd8e21b886_CUSTOM_ID_S29_Torso008_01",
"createdAt": "2024-07-09T12:57:31.072Z",
"entitlementLimit": 1,
"game": {
"id": "491487",
"name": "Dead by Daylight",
"__typename": "Game"
},
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/ed4a7829-cc2b-44d3-90a4-f73ef7d8d636.png",
"isIosAvailable": false,
"name": "Unwanted Attention",
"ownerOrganization": {
"id": "6da09649-1fda-4446-a061-cacd8e21b886",
"name": "Behaviour Interactive Inc.",
"__typename": "Organization"
},
"__typename": "DropBenefit"
},
"entitlementLimit": 1,
"__typename": "DropBenefitEdge"
}
],
"""
benefit = auto_prefetch.ForeignKey(Benefit, on_delete=models.CASCADE, related_name="benefit_edges", null=True)
entitlement_limit = models.TextField(null=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
benefit_name: str | None = self.benefit.name if self.benefit else "Unknown"
return f"{benefit_name} - {self.entitlement_limit}"
class TimeBasedDrop(auto_prefetch.Model):
"""Represents a time-based drop.
Attributes:
id (int): The primary key of the time-based drop.
name (str): The name of the time-based drop.
starts_at (datetime): The start date and time of the drop.
ends_at (datetime): The end date and time of the drop.
typename (str): The type name of the object, typically "TimeBasedDrop".
JSON example:
{
"id": "0ebeff68-3df3-11ef-b15b-0a58a9feac02",
"requiredSubs": 0,
"benefitEdges": [
{
"benefit": {
"id": "6da09649-1fda-4446-a061-cacd8e21b886_CUSTOM_ID_S29_Legs008_01",
"createdAt": "2024-07-09T12:58:03.654Z",
"entitlementLimit": 1,
"game": {
"id": "491487",
"name": "Dead by Daylight",
"__typename": "Game"
},
"imageAssetURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/REWARD/f46acdf5-9515-41eb-805e-86956db0a9e9.png",
"isIosAvailable": false,
"name": "Back Home",
"ownerOrganization": {
"id": "6da09649-1fda-4446-a061-cacd8e21b886",
"name": "Behaviour Interactive Inc.",
"__typename": "Organization"
},
"__typename": "DropBenefit"
},
"entitlementLimit": 1,
"__typename": "DropBenefitEdge"
}
],
"endAt": "2024-07-30T14:59:59.999Z",
"name": "Back Home",
"preconditionDrops": null,
"requiredMinutesWatched": 360,
"startAt": "2024-07-16T15:00:00Z",
"__typename": "TimeBasedDrop"
},
"""
id = models.TextField(primary_key=True)
created_at = models.DateTimeField(null=True)
entitlement_limit = models.TextField(null=True)
game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="time_based_drops", null=True)
image_asset_url = models.URLField(null=True, blank=True)
is_ios_available = models.BooleanField(null=True)
name = models.TextField(null=True, blank=True)
owner_organization = auto_prefetch.ForeignKey(
Owner,
on_delete=models.CASCADE,
related_name="time_based_drops",
null=True,
)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.name or "Unknown"
class DropCampaign(auto_prefetch.Model):
"""Represents a drop campaign.
Attributes:
id (int): The primary key of the drop campaign.
allow (Allow): The channels that you can watch to earn rewards.
account_link_url (str): URL to link your account.
description (str): The description of the drop campaign.
details_url (str): URL with more details about the drop campaign.
ends_at (datetime): The end date and time of the drop campaign.
game (Game): The game associated with the drop campaign.
image_url (str): URL to the image associated with the drop campaign.
name (str): The name of the drop campaign.
owner (Owner): The owner of the drop campaign.
starts_at (datetime): The start date and time of the drop campaign.
status (str): The status of the drop campaign.
time_based_drops (ManyToManyField): The time-based drops associated with the campaign.
typename (str): The type name of the object, typically "DropCampaign".
"""
STATUS_CHOICES: typing.ClassVar[list[tuple[str, str]]] = [
("ACTIVE", "Active"),
("EXPIRED", "Expired"),
]
id = models.TextField(primary_key=True)
allow = auto_prefetch.ForeignKey(Allow, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
account_link_url = models.URLField(null=True, blank=True)
description = models.TextField(null=True, blank=True)
details_url = models.URLField(null=True, blank=True)
ends_at = models.DateTimeField(null=True)
# event_based_drops = ????
game = auto_prefetch.ForeignKey(Game, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
image_url = models.URLField(null=True, blank=True)
name = models.TextField(null=True, blank=True)
owner = auto_prefetch.ForeignKey(Owner, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
starts_at = models.DateTimeField(null=True)
status = models.TextField(choices=STATUS_CHOICES, null=True, blank=True)
time_based_drops = models.ManyToManyField(TimeBasedDrop, related_name="drop_campaigns")
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.name or "Unknown"
class FrontEndChannel(auto_prefetch.Model):
"""This is the channel we will see on the front end."""
name = models.TextField(null=True, blank=True)
twitch_url = models.URLField(null=True, blank=True)
live = models.BooleanField(default=False)
class FrontEndOrg(auto_prefetch.Model):
"""Drops are group by organization -> by game -> by drop campaign."""
id = models.TextField(primary_key=True)
name = models.TextField(null=True, blank=True)
url = models.TextField(null=True, blank=True)
class FrontEndGame(auto_prefetch.Model):
"""This is the game we will see on the front end."""
twitch_id = models.TextField(primary_key=True)
game_url = models.URLField(null=True, blank=True)
display_name = models.TextField(null=True, blank=True)
org = models.ForeignKey(FrontEndOrg, on_delete=models.CASCADE, related_name="games", null=True)
def __str__(self) -> str:
return self.display_name or "Unknown"
class FrontEndDropCampaign(auto_prefetch.Model):
"""This is the drop campaign we will see on the front end."""
account_link_url = models.URLField(null=True, blank=True)
about_url = models.URLField(null=True, blank=True)
ends_at = models.DateTimeField(null=True)
starts_at = models.DateTimeField(null=True)
game = models.ForeignKey(FrontEndGame, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
channels = models.ManyToManyField(FrontEndChannel, related_name="drop_campaigns")
class FrontEndDrop(auto_prefetch.Model):
"""This is the drop we will see on the front end."""
id = models.TextField(primary_key=True)
created_at = models.DateTimeField(null=True)
name = models.TextField(null=True, blank=True)
image_url = models.URLField(null=True, blank=True)
drop_campaign = models.ForeignKey(FrontEndDropCampaign, on_delete=models.CASCADE, related_name="drops", null=True)
limit = models.PositiveBigIntegerField(null=True)
is_ios_available = models.BooleanField(null=True)
minutes_watched = models.PositiveBigIntegerField(null=True)