This commit is contained in:
2024-08-01 04:48:32 +02:00
parent 354d66f7bc
commit e47de7afd4
8 changed files with 1005 additions and 157 deletions

View File

@ -3,6 +3,8 @@
"allauth", "allauth",
"appendonly", "appendonly",
"asgiref", "asgiref",
"Behaviour",
"cacd",
"forloop", "forloop",
"logdir", "logdir",
"memlock", "memlock",
@ -12,6 +14,7 @@
"requirepass", "requirepass",
"sitewide", "sitewide",
"socialaccount", "socialaccount",
"Stresss",
"ttvdrops", "ttvdrops",
"ulimits", "ulimits",
"xdefiant" "xdefiant"

View File

@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
import hishel import hishel
from django.conf import settings from django.conf import settings
from django.db.models.manager import BaseManager
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.views.generic import ListView from django.views.generic import ListView
@ -15,6 +14,7 @@ from twitch_app.models import Game, RewardCampaign
if TYPE_CHECKING: if TYPE_CHECKING:
from pathlib import Path from pathlib import Path
from django.db.models.manager import BaseManager
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from httpx import Response from httpx import Response

View File

@ -11,7 +11,20 @@ from platformdirs import user_data_dir
from playwright.async_api import Playwright, async_playwright from playwright.async_api import Playwright, async_playwright
from playwright.async_api._generated import Response from playwright.async_api._generated import Response
from twitch_app.models import Game, Image, Reward, RewardCampaign, UnlockRequirements from twitch_app.models import (
Allow,
Benefit,
BenefitEdge,
Channel,
DropCampaign,
Game,
Image,
Owner,
Reward,
RewardCampaign,
TimeBasedDrop,
UnlockRequirements,
)
if TYPE_CHECKING: if TYPE_CHECKING:
from playwright.async_api._generated import BrowserContext, Page from playwright.async_api._generated import BrowserContext, Page
@ -33,92 +46,428 @@ if not data_dir:
logger: logging.Logger = logging.getLogger(__name__) logger: logging.Logger = logging.getLogger(__name__)
async def add_reward_campaign(json_data: dict) -> None: async def add_or_get_game(json_data: dict, name: str) -> tuple[Game | None, bool]:
"""Add data from JSON to the database.""" """Add or get Game from JSON data.
for campaign_data in json_data["data"]["rewardCampaignsAvailableToUser"]:
# Add or get Game
game_data = campaign_data["game"]
if game_data:
game, _ = await sync_to_async(Game.objects.get_or_create)(
id=game_data["id"],
slug=game_data["slug"],
defaults={
"display_name": game_data["displayName"],
"typename": game_data["__typename"],
},
)
else:
logger.warning("%s is not for a game?", campaign_data["name"])
game = None
# Add or get Image Args:
image_data = campaign_data["image"] json_data (dict): JSON data to add to the database.
image, _ = await sync_to_async(Image.objects.get_or_create)( name (str): Name of the drop campaign.
image1_x_url=image_data["image1xURL"],
defaults={"typename": image_data["__typename"]}, Returns:
tuple[Game | None, bool]: Game instance and whether it was created.
"""
if not json_data:
logger.warning("%s is not for a game?", name)
return None, False
game, created = await Game.objects.aupdate_or_create(
id=json_data["id"],
defaults={
"slug": json_data.get("slug"),
"display_name": json_data.get("displayName"),
"typename": json_data.get("__typename"),
},
)
return game, created
async def add_or_get_owner(json_data: dict, name: str) -> tuple[Owner | None, bool]:
"""Add or get Owner from JSON data.
Args:
json_data (dict): JSON data to add to the database.
name (str): Name of the drop campaign.
Returns:
Owner: Owner instance.
"""
if not json_data:
logger.warning("Owner data is missing for %s", name)
return None, False
owner, created = await Owner.objects.aupdate_or_create(
id=json_data["id"],
defaults={
"display_name": json_data.get("name"),
"typename": json_data.get("__typename"),
},
)
return owner, created
async def add_or_get_allow(json_data: dict, name: str) -> tuple[Allow | None, bool]:
"""Add or get Allow from JSON data.
Args:
json_data (dict): JSON data to add to the database.
name (str): Name of the drop campaign.
Returns:
Allow: Allow instance.
"""
if not json_data:
logger.warning("Allow data is missing for %s", name)
return None, False
allow, created = await Allow.objects.aupdate_or_create(
is_enabled=json_data.get("isEnabled"),
typename=json_data.get("__typename"),
)
return allow, created
async def add_or_get_time_based_drops(
time_based_drops_data: list[dict] | None,
owner: Owner | None,
game: Game | None,
) -> list[TimeBasedDrop]:
"""Handle TimeBasedDrops from JSON data.
Args:
time_based_drops_data (list[dict]): Time based drops data from JSON.
owner (Owner): Owner instance.
game (Game): Game instance.
Returns:
list[TimeBasedDrop]: TimeBasedDrop instances.
"""
time_based_drops: list[TimeBasedDrop] = []
if not time_based_drops_data:
logger.warning("No time based drops found")
return []
for time_based_drop_data in time_based_drops_data:
time_based_drop, _ = await TimeBasedDrop.objects.aupdate_or_create(
id=time_based_drop_data["id"],
defaults={
"created_at": time_based_drop_data.get("createdAt"),
"entitlement_limit": time_based_drop_data.get("entitlementLimit"),
"image_asset_url": time_based_drop_data.get("imageAssetURL"),
"is_ios_available": time_based_drop_data.get("isIosAvailable"),
"name": time_based_drop_data.get("name"),
"owner_organization": owner,
"game": game,
"typename": time_based_drop_data.get("__typename"),
},
) )
# Create Reward instances benefit_edges_data: list[dict] = time_based_drop_data.get("benefitEdges", [])
rewards = [] for benefit_edge_data in benefit_edges_data:
for reward_data in campaign_data["rewards"]: benefit_data: dict = benefit_edge_data.get("benefit", {})
banner_image_data = reward_data["bannerImage"] benefit, _ = await Benefit.objects.aupdate_or_create(
id=benefit_data["id"],
defaults={
"created_at": benefit_data.get("createdAt"),
"entitlement_limit": benefit_data.get("entitlementLimit"),
"image_asset_url": benefit_data.get("imageAssetURL"),
"is_ios_available": benefit_data.get("isIosAvailable"),
"name": benefit_data.get("name"),
"owner_organization": owner,
"game": game,
"typename": benefit_data.get("__typename"),
},
)
await BenefitEdge.objects.aupdate_or_create(
benefit=benefit,
defaults={
"entitlement_limit": benefit_edge_data.get("entitlementLimit"),
"typename": benefit_edge_data.get("__typename"),
},
)
time_based_drops.append(time_based_drop)
return time_based_drops
async def add_or_get_drop_campaign(
drop_campaign_data: dict,
game: Game | None,
owner: Owner | None,
) -> tuple[DropCampaign | None, bool]:
"""Handle DropCampaign from JSON data.
Args:
drop_campaign_data (dict): Drop campaign data from JSON.
game (Game): Game instance.
owner (Owner): Owner instance.
Returns:
tuple[DropCampaign, bool]: DropCampaign instance and whether it was created.
"""
if not drop_campaign_data:
logger.warning("No drop campaign data found")
return None, False
drop_campaign, _ = await DropCampaign.objects.aupdate_or_create(
id=drop_campaign_data["id"],
defaults={
# "allow": allow, # We add this later
"account_link_url": drop_campaign_data.get("accountLinkURL"),
"description": drop_campaign_data.get("description"),
"details_url": drop_campaign_data.get("detailsURL"),
"ends_at": drop_campaign_data.get("endAt"),
# event_based_drops = ???? # TODO(TheLovinator): Find out what this is # noqa: TD003
"game": game,
"image_url": drop_campaign_data.get("imageURL"),
"name": drop_campaign_data.get("name"),
"owner": owner,
"starts_at": drop_campaign_data.get("startAt"),
"status": drop_campaign_data.get("status"),
# "time_based_drops": time_based_drops, # We add this later
"typename": drop_campaign_data.get("__typename"),
},
)
return drop_campaign, True
async def add_or_get_channel(json_data: dict) -> tuple[Channel | None, bool]:
"""Add or get Channel from JSON data.
Args:
json_data (dict): JSON data to add to the database.
Returns:
tuple[Channel | None, bool]: Channel instance and whether it was created.
"""
if not json_data:
logger.warning("Channel data is missing")
return None, False
channel, created = await Channel.objects.aupdate_or_create(
id=json_data["id"],
defaults={
"display_name": json_data.get("displayName"),
"name": json_data.get("name"),
"typename": json_data.get("__typename"),
},
)
return channel, created
async def add_drop_campaign(json_data: dict) -> None:
"""Add data from JSON to the database."""
# Get the data from the JSON
user_data: dict = json_data.get("data", {}).get("user", {})
drop_campaign_data: dict = user_data.get("dropCampaign", {})
# Add or get Game
game_data: dict = drop_campaign_data.get("game", {})
game, _ = await add_or_get_game(json_data=game_data, name=drop_campaign_data.get("name", "Unknown Drop Campaign"))
# Add or get Owner
owner_data: dict = drop_campaign_data.get("owner", {})
owner, _ = await add_or_get_owner(
json_data=owner_data,
name=drop_campaign_data.get("name", "Unknown Drop Campaign"),
)
# Add or get Allow
allow_data: dict = drop_campaign_data.get("allow", {})
allow, _ = await add_or_get_allow(
json_data=allow_data,
name=drop_campaign_data.get("name", "Unknown Drop Campaign"),
)
# Add channels to Allow
if allow:
channel_data: list[dict] = allow_data.get("channels", [])
for json_channel in channel_data:
channel, _ = await add_or_get_channel(json_channel)
if channel:
await allow.channels.aadd(channel)
# Add or get TimeBasedDrops
time_based_drops_data = drop_campaign_data.get("timeBasedDrops", [])
time_based_drops: list[TimeBasedDrop] = await add_or_get_time_based_drops(time_based_drops_data, owner, game)
# Add or get DropCampaign
drop_campaign, _ = await add_or_get_drop_campaign(
drop_campaign_data=drop_campaign_data,
game=game,
owner=owner,
)
if drop_campaign:
drop_campaign.allow = allow
await drop_campaign.time_based_drops.aset(time_based_drops)
await drop_campaign.asave()
logger.info("Added Drop Campaign: %s", drop_campaign.name or "Unknown Drop Campaign")
async def add_or_get_image(json_data: dict) -> tuple[Image | None, bool]:
"""Add or get Image from JSON data.
Args:
json_data (dict): JSON data to add to the database.
Returns:
tuple[Image | None, bool]: Image instance and whether it was created.
"""
# TODO(TheLovinator): We should download the image and store it locally # noqa: TD003
if not json_data:
logger.warning("Image data is missing")
return None, False
if not json_data.get("image1xURL"):
logger.warning("Image URL is missing")
return None, False
image, created = await Image.objects.aupdate_or_create(
image1_x_url=json_data.get("image1xURL"),
defaults={
"typename": json_data.get("__typename"),
},
)
return image, created
async def add_or_get_rewards(json_data: dict) -> list[Reward]:
"""Add or get Rewards from JSON data.
Args:
json_data (dict): JSON data to add to the database.
Returns:
list[Reward]: Reward instances
"""
rewards: list[Reward] = []
if not json_data:
logger.warning("No rewards found")
return []
if "rewards" not in json_data:
logger.warning("No rewards found")
return []
rewards_json: list[dict] = json_data.get("rewards", [])
for reward_data in rewards_json:
# Add or get bannerImage
banner_image_data: dict = reward_data.get("bannerImage", {})
if banner_image_data:
banner_image, _ = await sync_to_async(Image.objects.get_or_create)( banner_image, _ = await sync_to_async(Image.objects.get_or_create)(
image1_x_url=banner_image_data["image1xURL"], image1_x_url=banner_image_data["image1xURL"],
defaults={"typename": banner_image_data["__typename"]}, defaults={"typename": banner_image_data["__typename"]},
) )
thumbnail_image_data = reward_data["thumbnailImage"] # Add or get thumbnailImage
thumbnail_image_data = reward_data.get("thumbnailImage", {})
if thumbnail_image_data:
thumbnail_image, _ = await sync_to_async(Image.objects.get_or_create)( thumbnail_image, _ = await sync_to_async(Image.objects.get_or_create)(
image1_x_url=thumbnail_image_data["image1xURL"], image1_x_url=thumbnail_image_data["image1xURL"],
defaults={"typename": thumbnail_image_data["__typename"]}, defaults={"typename": thumbnail_image_data["__typename"]},
) )
reward, _ = await sync_to_async(Reward.objects.get_or_create)( # Convert earnableUntil to a datetime object
id=reward_data["id"], earnable_until: str | None = reward_data.get("earnableUntil")
name=reward_data["name"], earnable_until_date: datetime | None = None
banner_image=banner_image, if earnable_until:
thumbnail_image=thumbnail_image, earnable_until_date = datetime.fromisoformat(earnable_until.replace("Z", "+00:00"))
earnable_until=datetime.fromisoformat(reward_data["earnableUntil"].replace("Z", "+00:00")),
redemption_instructions=reward_data["redemptionInstructions"],
redemption_url=reward_data["redemptionURL"],
typename=reward_data["__typename"],
)
rewards.append(reward)
# Add or get Unlock Requirements reward, _ = await sync_to_async(Reward.objects.get_or_create)(
unlock_requirements_data = campaign_data["unlockRequirements"] id=reward_data["id"],
_, _ = await sync_to_async(UnlockRequirements.objects.get_or_create)(
subs_goal=unlock_requirements_data["subsGoal"],
defaults={ defaults={
"minute_watched_goal": unlock_requirements_data["minuteWatchedGoal"], "name": reward_data.get("name"),
"typename": unlock_requirements_data["__typename"], "banner_image": banner_image,
"thumbnail_image": thumbnail_image,
"earnable_until": earnable_until_date,
"redemption_instructions": reward_data.get("redemptionInstructions"),
"redemption_url": reward_data.get("redemptionURL"),
"typename": reward_data.get("__typename"),
}, },
) )
rewards.append(reward)
return rewards
async def add_or_get_unlock_requirements(json_data: dict) -> tuple[UnlockRequirements | None, bool]:
"""Add or get UnlockRequirements from JSON data.
Args:
json_data (dict): JSON data to add to the database.
Returns:
tuple[UnlockRequirements | None, bool]: UnlockRequirements instance and whether it was created.
"""
if not json_data:
logger.warning("Unlock Requirements data is missing")
return None, False
unlock_requirements, created = await UnlockRequirements.objects.aget_or_create(
subs_goal=json_data["subsGoal"],
defaults={
"minute_watched_goal": json_data["minuteWatchedGoal"],
"typename": json_data["__typename"],
},
)
return unlock_requirements, created
async def add_reward_campaign(json_data: dict) -> None:
"""Add data from JSON to the database.
Args:
json_data (dict): JSON data to add to the database.
Returns:
None: No return value.
"""
campaign_data: list[dict] = json_data["data"]["rewardCampaignsAvailableToUser"]
for campaign in campaign_data:
# Add or get Game
game_data: dict = campaign.get("game", {})
game, _ = await add_or_get_game(json_data=game_data, name=campaign.get("name", "Unknown Reward Campaign"))
# Add or get Image
image_data: dict = campaign.get("image", {})
image, _ = await add_or_get_image(json_data=image_data)
# Add or get Rewards
rewards: list[Reward] = await add_or_get_rewards(campaign)
# Add or get Unlock Requirements
unlock_requirements_data: dict = campaign["unlockRequirements"]
unlock_requirements, _ = await add_or_get_unlock_requirements(unlock_requirements_data)
# Create Reward Campaign # Create Reward Campaign
reward_campaign, _ = await sync_to_async(RewardCampaign.objects.get_or_create)( reward_campaign, _ = await RewardCampaign.objects.aget_or_create(
id=campaign_data["id"], id=campaign["id"],
name=campaign_data["name"], defaults={
brand=campaign_data["brand"], "name": campaign.get("name"),
starts_at=datetime.fromisoformat(campaign_data["startsAt"].replace("Z", "+00:00")), "brand": campaign.get("brand"),
ends_at=datetime.fromisoformat(campaign_data["endsAt"].replace("Z", "+00:00")), "starts_at": campaign.get("startAt"),
status=campaign_data["status"], "ends_at": campaign.get("endAt"),
summary=campaign_data["summary"], "status": campaign.get("status"),
instructions=campaign_data["instructions"], "summary": campaign.get("summary"),
external_url=campaign_data["externalURL"], "instructions": campaign.get("instructions"),
reward_value_url_param=campaign_data["rewardValueURLParam"], "external_url": campaign.get("externalURL"),
about_url=campaign_data["aboutURL"], "reward_value_url_params": campaign.get("rewardValueURLParams"),
is_sitewide=campaign_data["isSitewide"], "about_url": campaign.get("aboutURL"),
game=game, "is_sitewide": campaign.get("isSitewide"),
image=image, "game": game,
typename=campaign_data["__typename"], "unlock_requirements": unlock_requirements,
"image": image,
# "rewards": rewards, # We add this later
"typename": campaign.get("__typename"),
},
) )
# Add Rewards to the Campaign # Add Rewards to the Campaign
for reward in rewards: for reward in rewards:
await sync_to_async(reward_campaign.rewards.add)(reward) await reward_campaign.rewards.aadd(reward)
await sync_to_async(reward_campaign.save)() await reward_campaign.asave()
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -2,83 +2,93 @@
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies: list[tuple[str, str]] = [
dependencies = [ ("twitch_app", "0001_initial"),
('twitch_app', '0001_initial'),
] ]
operations = [ operations: list[Operation] = [
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='about_url', name="about_url",
field=models.URLField(blank=True, null=True), field=models.URLField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='brand', name="brand",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='ends_at', name="ends_at",
field=models.DateTimeField(blank=True, null=True), field=models.DateTimeField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='external_url', name="external_url",
field=models.URLField(blank=True, null=True), field=models.URLField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='game', name="game",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reward_campaigns', to='twitch_app.game'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="reward_campaigns",
to="twitch_app.game",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='image', name="image",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reward_campaigns', to='twitch_app.image'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="reward_campaigns",
to="twitch_app.image",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='instructions', name="instructions",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='is_sitewide', name="is_sitewide",
field=models.BooleanField(blank=True, null=True), field=models.BooleanField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='name', name="name",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='reward_value_url_param', name="reward_value_url_param",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='starts_at', name="starts_at",
field=models.DateTimeField(blank=True, null=True), field=models.DateTimeField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='status', name="status",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='summary', name="summary",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='typename', name="typename",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
] ]

View File

@ -2,108 +2,118 @@
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies: list[tuple[str, str]] = [
dependencies = [ ("twitch_app", "0002_alter_rewardcampaign_about_url_and_more"),
('twitch_app', '0002_alter_rewardcampaign_about_url_and_more'),
] ]
operations = [ operations: list[Operation] = [
migrations.AlterField( migrations.AlterField(
model_name='game', model_name="game",
name='display_name', name="display_name",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='game', model_name="game",
name='slug', name="slug",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='game', model_name="game",
name='typename', name="typename",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='image', model_name="image",
name='image1_x_url', name="image1_x_url",
field=models.URLField(blank=True, null=True), field=models.URLField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='image', model_name="image",
name='typename', name="typename",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='banner_image', name="banner_image",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='banner_rewards', to='twitch_app.image'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="banner_rewards",
to="twitch_app.image",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='earnable_until', name="earnable_until",
field=models.DateTimeField(null=True), field=models.DateTimeField(null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='name', name="name",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='redemption_instructions', name="redemption_instructions",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='redemption_url', name="redemption_url",
field=models.URLField(blank=True, null=True), field=models.URLField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='thumbnail_image', name="thumbnail_image",
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='thumbnail_rewards', to='twitch_app.image'), field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="thumbnail_rewards",
to="twitch_app.image",
),
), ),
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='typename', name="typename",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='ends_at', name="ends_at",
field=models.DateTimeField(null=True), field=models.DateTimeField(null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='is_sitewide', name="is_sitewide",
field=models.BooleanField(null=True), field=models.BooleanField(null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='rewards', name="rewards",
field=models.ManyToManyField(related_name='reward_campaigns', to='twitch_app.reward'), field=models.ManyToManyField(related_name="reward_campaigns", to="twitch_app.reward"),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='starts_at', name="starts_at",
field=models.DateTimeField(null=True), field=models.DateTimeField(null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='unlockrequirements', model_name="unlockrequirements",
name='minute_watched_goal', name="minute_watched_goal",
field=models.IntegerField(null=True), field=models.IntegerField(null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='unlockrequirements', model_name="unlockrequirements",
name='subs_goal', name="subs_goal",
field=models.IntegerField(null=True), field=models.IntegerField(null=True),
), ),
migrations.AlterField( migrations.AlterField(
model_name='unlockrequirements', model_name="unlockrequirements",
name='typename', name="typename",
field=models.TextField(blank=True, null=True), field=models.TextField(blank=True, null=True),
), ),
] ]

View File

@ -1,23 +1,23 @@
# Generated by Django 5.1rc1 on 2024-07-30 23:44 # Generated by Django 5.1rc1 on 2024-07-30 23:44
from django.db import migrations, models from django.db import migrations, models
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies: list[tuple[str, str]] = [
dependencies = [ ("twitch_app", "0003_alter_game_display_name_alter_game_slug_and_more"),
('twitch_app', '0003_alter_game_display_name_alter_game_slug_and_more'),
] ]
operations = [ operations: list[Operation] = [
migrations.AlterField( migrations.AlterField(
model_name='reward', model_name="reward",
name='id', name="id",
field=models.UUIDField(primary_key=True, serialize=False), field=models.UUIDField(primary_key=True, serialize=False),
), ),
migrations.AlterField( migrations.AlterField(
model_name='rewardcampaign', model_name="rewardcampaign",
name='id', name="id",
field=models.UUIDField(primary_key=True, serialize=False), field=models.UUIDField(primary_key=True, serialize=False),
), ),
] ]

View File

@ -0,0 +1,174 @@
# Generated by Django 5.1rc1 on 2024-08-01 01:00
import django.db.models.deletion
from django.db import migrations, models
from django.db.migrations.operations.base import Operation
class Migration(migrations.Migration):
dependencies: list[tuple[str, str]] = [
("twitch_app", "0004_alter_reward_id_alter_rewardcampaign_id"),
]
operations: list[Operation] = [
migrations.CreateModel(
name="Channel",
fields=[
("id", models.PositiveBigIntegerField(primary_key=True, serialize=False)),
("display_name", models.TextField(blank=True, null=True)),
("name", models.TextField(blank=True, null=True)),
("typename", models.TextField(blank=True, null=True)),
],
),
migrations.CreateModel(
name="Owner",
fields=[
("id", models.PositiveBigIntegerField(primary_key=True, serialize=False)),
("slug", models.TextField(blank=True, null=True)),
("display_name", models.TextField(blank=True, null=True)),
("typename", models.TextField(blank=True, null=True)),
],
),
migrations.AlterField(
model_name="unlockrequirements",
name="minute_watched_goal",
field=models.PositiveBigIntegerField(null=True),
),
migrations.AlterField(
model_name="unlockrequirements",
name="subs_goal",
field=models.PositiveBigIntegerField(null=True),
),
migrations.CreateModel(
name="Benefit",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
("created_at", models.DateTimeField(null=True)),
("entitlement_limit", models.PositiveBigIntegerField(null=True)),
("image_asset_url", models.URLField(blank=True, null=True)),
("is_ios_available", models.BooleanField(null=True)),
("name", models.TextField(blank=True, null=True)),
("typename", models.TextField(blank=True, null=True)),
(
"game",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="benefits",
to="twitch_app.game",
),
),
(
"owner_organization",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="benefits",
to="twitch_app.owner",
),
),
],
),
migrations.CreateModel(
name="BenefitEdge",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("entitlement_limit", models.PositiveBigIntegerField(null=True)),
("typename", models.TextField(blank=True, null=True)),
(
"benefit",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="benefit_edges",
to="twitch_app.benefit",
),
),
],
),
migrations.CreateModel(
name="Allow",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("is_enabled", models.BooleanField(default=True)),
("typename", models.TextField(blank=True, null=True)),
("channels", models.ManyToManyField(related_name="allow", to="twitch_app.channel")),
],
),
migrations.CreateModel(
name="TimeBasedDrop",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
("created_at", models.DateTimeField(null=True)),
("entitlement_limit", models.PositiveBigIntegerField(null=True)),
("image_asset_url", models.URLField(blank=True, null=True)),
("is_ios_available", models.BooleanField(null=True)),
("name", models.TextField(blank=True, null=True)),
("typename", models.TextField(blank=True, null=True)),
(
"game",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="time_based_drops",
to="twitch_app.game",
),
),
(
"owner_organization",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="time_based_drops",
to="twitch_app.owner",
),
),
],
),
migrations.CreateModel(
name="DropCampaign",
fields=[
("id", models.TextField(primary_key=True, serialize=False)),
("account_link_url", models.URLField(blank=True, null=True)),
("description", models.TextField(blank=True, null=True)),
("details_url", models.URLField(blank=True, null=True)),
("ends_at", models.DateTimeField(null=True)),
("image_url", models.URLField(blank=True, null=True)),
("name", models.TextField(blank=True, null=True)),
("starts_at", models.DateTimeField(null=True)),
("status", models.TextField(blank=True, null=True)),
("typename", models.TextField(blank=True, null=True)),
(
"allow",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="drop_campaigns",
to="twitch_app.allow",
),
),
(
"game",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="drop_campaigns",
to="twitch_app.game",
),
),
(
"owner",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="drop_campaigns",
to="twitch_app.owner",
),
),
(
"time_based_drops",
models.ManyToManyField(related_name="drop_campaigns", to="twitch_app.timebaseddrop"),
),
],
),
]

View File

@ -4,6 +4,8 @@ from django.db import models
class Game(models.Model): class Game(models.Model):
"""The game that the reward is for. """The game that the reward is for.
Used for reward campaigns (buy subs) and drop campaigns (watch games).
Attributes: Attributes:
id (int): The primary key of the game. id (int): The primary key of the game.
slug (str): The slug identifier of the game. slug (str): The slug identifier of the game.
@ -56,7 +58,7 @@ class Reward(models.Model):
"""The actual reward you get when you complete the requirements. """The actual reward you get when you complete the requirements.
Attributes: Attributes:
id (UUID): The primary key of the reward. id (str): The primary key of the reward.
name (str): The name of the reward. name (str): The name of the reward.
banner_image (Image): The banner image associated with the reward. banner_image (Image): The banner image associated with the reward.
thumbnail_image (Image): The thumbnail image associated with the reward. thumbnail_image (Image): The thumbnail image associated with the reward.
@ -84,7 +86,7 @@ class Reward(models.Model):
} }
""" """
id = models.UUIDField(primary_key=True) id = models.TextField(primary_key=True)
name = models.TextField(null=True, blank=True) name = models.TextField(null=True, blank=True)
banner_image = models.ForeignKey(Image, related_name="banner_rewards", on_delete=models.CASCADE, null=True) banner_image = models.ForeignKey(Image, related_name="banner_rewards", on_delete=models.CASCADE, null=True)
thumbnail_image = models.ForeignKey(Image, related_name="thumbnail_rewards", on_delete=models.CASCADE, null=True) thumbnail_image = models.ForeignKey(Image, related_name="thumbnail_rewards", on_delete=models.CASCADE, null=True)
@ -113,8 +115,8 @@ class UnlockRequirements(models.Model):
} }
""" """
subs_goal = models.IntegerField(null=True) subs_goal = models.PositiveBigIntegerField(null=True)
minute_watched_goal = models.IntegerField(null=True) minute_watched_goal = models.PositiveBigIntegerField(null=True)
typename = models.TextField(null=True, blank=True) typename = models.TextField(null=True, blank=True)
def __str__(self) -> str: def __str__(self) -> str:
@ -125,7 +127,7 @@ class RewardCampaign(models.Model):
"""Represents a reward campaign. """Represents a reward campaign.
Attributes: Attributes:
id (UUID): The primary key of the reward campaign. id (str): The primary key of the reward campaign.
name (str): The name of the reward campaign. name (str): The name of the reward campaign.
brand (str): The brand associated with the campaign. brand (str): The brand associated with the campaign.
starts_at (datetime): The start date and time of the campaign. starts_at (datetime): The start date and time of the campaign.
@ -162,6 +164,11 @@ class RewardCampaign(models.Model):
"displayName": "XDefiant", "displayName": "XDefiant",
"__typename": "Game" "__typename": "Game"
}, },
"unlockRequirements": {
"subsGoal": 2,
"minuteWatchedGoal": 0,
"__typename": "QuestRewardUnlockRequirements"
},
"image": { "image": {
"image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png", "image1xURL": "https://static-cdn.jtvnw.net/twitch-quests-assets/CAMPAIGN/quests_xdefiant_q3_2024/campaign.png",
"__typename": "RewardCampaignImageSet" "__typename": "RewardCampaignImageSet"
@ -188,7 +195,7 @@ class RewardCampaign(models.Model):
} }
""" # noqa: E501 """ # noqa: E501
id = models.UUIDField(primary_key=True) id = models.TextField(primary_key=True)
name = models.TextField(null=True, blank=True) name = models.TextField(null=True, blank=True)
brand = models.TextField(null=True, blank=True) brand = models.TextField(null=True, blank=True)
starts_at = models.DateTimeField(null=True) starts_at = models.DateTimeField(null=True)
@ -201,9 +208,304 @@ class RewardCampaign(models.Model):
about_url = models.URLField(null=True, blank=True) about_url = models.URLField(null=True, blank=True)
is_sitewide = models.BooleanField(null=True) is_sitewide = models.BooleanField(null=True)
game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=True) game = models.ForeignKey(Game, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
unlock_requirements = models.ForeignKey(
UnlockRequirements,
on_delete=models.CASCADE,
related_name="reward_campaigns",
null=True,
)
image = models.ForeignKey(Image, on_delete=models.CASCADE, related_name="reward_campaigns", null=True) image = models.ForeignKey(Image, on_delete=models.CASCADE, related_name="reward_campaigns", null=True)
rewards = models.ManyToManyField(Reward, related_name="reward_campaigns") rewards = models.ManyToManyField(Reward, related_name="reward_campaigns")
typename = models.TextField(null=True, blank=True) typename = models.TextField(null=True, blank=True)
def __str__(self) -> str: def __str__(self) -> str:
return self.name or "Unknown" return self.name or "Unknown"
class Channel(models.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".
JSON example:
{
"id": "25254906",
"displayName": "Stresss",
"name": "stresss",
"__typename": "Channel"
}
"""
id = models.PositiveBigIntegerField(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"
def get_twitch_url(self) -> str:
return f"https://www.twitch.tv/{self.name}"
class Allow(models.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".
JSON example:
"allow": {
"channels": [
{
"id": "25254906",
"displayName": "Stresss",
"name": "stresss",
"__typename": "Channel"
}
],
"isEnabled": false,
"__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(models.Model):
"""Represents the owner of the reward campaign.
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:
"game": {
"id": "491487",
"slug": "dead-by-daylight",
"displayName": "Dead by Daylight",
"__typename": "Game"
},"
"""
id = models.PositiveBigIntegerField(primary_key=True)
slug = models.TextField(null=True, blank=True)
display_name = models.TextField(null=True, blank=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.display_name or "Unknown"
def get_twitch_url(self) -> str:
return f"https://www.twitch.tv/{self.slug}"
class Benefit(models.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.PositiveBigIntegerField(null=True)
game = models.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 = models.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(models.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 = models.ForeignKey(Benefit, on_delete=models.CASCADE, related_name="benefit_edges", null=True)
entitlement_limit = models.PositiveBigIntegerField(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(models.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.PositiveBigIntegerField(null=True)
game = models.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 = models.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(models.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".
"""
id = models.TextField(primary_key=True)
allow = models.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 = models.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 = models.ForeignKey(Owner, on_delete=models.CASCADE, related_name="drop_campaigns", null=True)
starts_at = models.DateTimeField(null=True)
status = models.TextField(null=True, blank=True)
time_based_drops = models.ManyToManyField(TimeBasedDrop, related_name="drop_campaigns", null=True)
typename = models.TextField(null=True, blank=True)
def __str__(self) -> str:
return self.name or "Unknown"