diff --git a/core/admin.py b/core/admin.py index 605bd55..53449f7 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from .models.twitch import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop +from .models import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop admin.site.register(Game) admin.site.register(Owner) diff --git a/core/management/commands/scrape_twitch.py b/core/management/commands/scrape_twitch.py index 8ec9431..92ef0b3 100644 --- a/core/management/commands/scrape_twitch.py +++ b/core/management/commands/scrape_twitch.py @@ -10,7 +10,7 @@ from platformdirs import user_data_dir from playwright.async_api import Playwright, async_playwright from playwright.async_api._generated import Response -from core.models.twitch import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop +from core.models import Benefit, Channel, DropCampaign, Game, Owner, Reward, RewardCampaign, TimeBasedDrop if TYPE_CHECKING: from playwright.async_api._generated import BrowserContext, Page @@ -74,6 +74,7 @@ async def add_reward_campaign(campaign: dict | None) -> None: # noqa: C901 Args: campaign (dict): The reward campaign to add. """ + # sourcery skip: low-code-quality if not campaign: return diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..bd4b516 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,322 @@ +# Generated by Django 5.1 on 2024-08-15 00:28 + +import django.contrib.auth.models +import django.contrib.auth.validators +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models +from django.db.migrations.operations.base import Operation + + +class Migration(migrations.Migration): + initial = True + + dependencies: list[tuple[str, str]] = [ + ("auth", "0012_alter_user_first_name_max_length"), + ] + + operations: list[Operation] = [ + migrations.CreateModel( + name="DropCampaign", + fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), + ("id", models.TextField(primary_key=True, serialize=False)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("account_link_url", models.URLField(null=True)), + ("description", models.TextField(null=True)), + ("details_url", models.URLField(null=True)), + ("ends_at", models.DateTimeField(null=True)), + ("starts_at", models.DateTimeField(null=True)), + ("image_url", models.URLField(null=True)), + ("name", models.TextField(null=True)), + ("status", models.TextField(null=True)), + ], + ), + migrations.CreateModel( + name="Game", + fields=[ + ("twitch_id", models.TextField(primary_key=True, serialize=False)), + ("game_url", models.URLField(default="https://www.twitch.tv/", null=True)), + ("name", models.TextField(default="Game name unknown", null=True)), + ( + "box_art_url", + models.URLField(default="https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg", null=True), + ), + ("slug", models.TextField(null=True)), + ], + ), + migrations.CreateModel( + name="Owner", + fields=[ + ("id", models.TextField(primary_key=True, serialize=False)), + ("name", models.TextField(null=True)), + ], + ), + migrations.CreateModel( + name="User", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("password", models.CharField(max_length=128, verbose_name="password")), + ("last_login", models.DateTimeField(blank=True, null=True, verbose_name="last login")), + ( + "is_superuser", + models.BooleanField( + default=False, + help_text="Designates that this user has all permissions without explicitly assigning them.", + verbose_name="superuser status", + ), + ), + ( + "username", + models.CharField( + error_messages={"unique": "A user with that username already exists."}, + help_text="Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.", + max_length=150, + unique=True, + validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], + verbose_name="username", + ), + ), + ("first_name", models.CharField(blank=True, max_length=150, verbose_name="first name")), + ("last_name", models.CharField(blank=True, max_length=150, verbose_name="last name")), + ("email", models.EmailField(blank=True, max_length=254, verbose_name="email address")), + ( + "is_staff", + models.BooleanField( + default=False, + help_text="Designates whether the user can log into this admin site.", + verbose_name="staff status", + ), + ), + ( + "is_active", + models.BooleanField( + default=True, + help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.", # noqa: E501 + verbose_name="active", + ), + ), + ("date_joined", models.DateTimeField(default=django.utils.timezone.now, verbose_name="date joined")), + ("subscribe_to_news", models.BooleanField(default=False, help_text="Subscribe to news")), + ("subscribe_to_new_games", models.BooleanField(default=False, help_text="Subscribe to new games")), + ( + "groups", + models.ManyToManyField( + blank=True, + help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", # noqa: E501 + related_name="user_set", + related_query_name="user", + to="auth.group", + verbose_name="groups", + ), + ), + ( + "user_permissions", + models.ManyToManyField( + blank=True, + help_text="Specific permissions for this user.", + related_name="user_set", + related_query_name="user", + to="auth.permission", + verbose_name="user permissions", + ), + ), + ], + options={ + "verbose_name": "user", + "verbose_name_plural": "users", + "abstract": False, + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name="DiscordWebhook", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("url", models.URLField()), + ("name", models.CharField(max_length=255)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="discord_webhooks", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.CreateModel( + name="Channel", + fields=[ + ("twitch_id", models.TextField(primary_key=True, serialize=False)), + ("display_name", models.TextField(default="Channel name unknown", null=True)), + ("name", models.TextField(null=True)), + ("twitch_url", models.URLField(default="https://www.twitch.tv/", null=True)), + ("live", models.BooleanField(default=False)), + ("drop_campaigns", models.ManyToManyField(related_name="channels", to="core.dropcampaign")), + ], + ), + migrations.AddField( + model_name="dropcampaign", + name="game", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="drop_campaigns", + to="core.game", + ), + ), + migrations.CreateModel( + name="GameSubscription", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("game", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.game")), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("webhook", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.discordwebhook")), + ], + options={ + "unique_together": {("user", "game")}, + }, + ), + migrations.AddField( + model_name="user", + name="subscribed_games", + field=models.ManyToManyField( + blank=True, + related_name="subscribed_users", + through="core.GameSubscription", + to="core.game", + ), + ), + migrations.AddField( + model_name="game", + name="org", + field=models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="games", + to="core.owner", + ), + ), + migrations.CreateModel( + name="OwnerSubscription", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("owner", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.owner")), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ("webhook", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="core.discordwebhook")), + ], + options={ + "unique_together": {("user", "owner")}, + }, + ), + migrations.AddField( + model_name="user", + name="subscribed_owners", + field=models.ManyToManyField( + blank=True, + related_name="subscribed_users", + through="core.OwnerSubscription", + to="core.owner", + ), + ), + migrations.CreateModel( + name="RewardCampaign", + fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), + ("id", models.TextField(primary_key=True, serialize=False)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("name", models.TextField(null=True)), + ("brand", models.TextField(null=True)), + ("starts_at", models.DateTimeField(null=True)), + ("ends_at", models.DateTimeField(null=True)), + ("status", models.TextField(null=True)), + ("summary", models.TextField(null=True)), + ("instructions", models.TextField(null=True)), + ("reward_value_url_param", models.TextField(null=True)), + ("external_url", models.URLField(null=True)), + ("about_url", models.URLField(null=True)), + ("is_site_wide", models.BooleanField(null=True)), + ("sub_goal", models.PositiveBigIntegerField(null=True)), + ("minute_watched_goal", models.PositiveBigIntegerField(null=True)), + ("image_url", models.URLField(null=True)), + ( + "game", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="reward_campaigns", + to="core.game", + ), + ), + ], + ), + migrations.CreateModel( + name="Reward", + fields=[ + ("id", models.TextField(primary_key=True, serialize=False)), + ("name", models.TextField(null=True)), + ("banner_image_url", models.URLField(null=True)), + ("thumbnail_image_url", models.URLField(null=True)), + ("earnable_until", models.DateTimeField(null=True)), + ("redemption_instructions", models.TextField(null=True)), + ("redemption_url", models.URLField(null=True)), + ( + "campaign", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="rewards", + to="core.rewardcampaign", + ), + ), + ], + ), + migrations.CreateModel( + name="TimeBasedDrop", + fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), + ("id", models.TextField(primary_key=True, serialize=False)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("required_subs", models.PositiveBigIntegerField(null=True)), + ("ends_at", models.DateTimeField(null=True)), + ("name", models.TextField(null=True)), + ("required_minutes_watched", models.PositiveBigIntegerField(null=True)), + ("starts_at", models.DateTimeField(null=True)), + ( + "drop_campaign", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="drops", + to="core.dropcampaign", + ), + ), + ], + ), + migrations.CreateModel( + name="Benefit", + fields=[ + ("created_at", models.DateTimeField(auto_created=True, null=True)), + ("id", models.TextField(primary_key=True, serialize=False)), + ("modified_at", models.DateTimeField(auto_now=True, null=True)), + ("twitch_created_at", models.DateTimeField(null=True)), + ("entitlement_limit", models.PositiveBigIntegerField(null=True)), + ("image_url", models.URLField(null=True)), + ("is_ios_available", models.BooleanField(null=True)), + ("name", models.TextField(null=True)), + ( + "time_based_drop", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="benefits", + to="core.timebaseddrop", + ), + ), + ], + ), + ] diff --git a/core/migrations/0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more.py b/core/migrations/0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more.py deleted file mode 100644 index 56e9800..0000000 --- a/core/migrations/0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more.py +++ /dev/null @@ -1,202 +0,0 @@ -# Generated by Django 5.1 on 2024-08-12 23:16 - -import django.db.models.deletion -from django.db import migrations, models -from django.db.migrations.operations.base import Operation - - -class Migration(migrations.Migration): - replaces: list[tuple[str, str]] = [ - ("core", "0001_initial"), - ("core", "0002_alter_benefit_time_based_drop_and_more"), - ("core", "0003_alter_benefit_options_alter_channel_options_and_more"), - ] - - initial = True - - dependencies: list[tuple[str, str]] = [] - - operations: list[Operation] = [ - migrations.CreateModel( - name="Owner", - fields=[ - ("id", models.TextField(primary_key=True, serialize=False)), - ("name", models.TextField(null=True)), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="Game", - fields=[ - ("twitch_id", models.TextField(primary_key=True, serialize=False)), - ("game_url", models.URLField(null=True)), - ("name", models.TextField(null=True)), - ("box_art_url", models.URLField(null=True)), - ("slug", models.TextField(null=True)), - ( - "org", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="games", - to="core.owner", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="RewardCampaign", - fields=[ - ("created_at", models.DateTimeField(auto_created=True, null=True)), - ("id", models.TextField(primary_key=True, serialize=False)), - ("modified_at", models.DateTimeField(auto_now=True, null=True)), - ("name", models.TextField(null=True)), - ("brand", models.TextField(null=True)), - ("starts_at", models.DateTimeField(null=True)), - ("ends_at", models.DateTimeField(null=True)), - ("status", models.TextField(null=True)), - ("summary", models.TextField(null=True)), - ("instructions", models.TextField(null=True)), - ("reward_value_url_param", models.TextField(null=True)), - ("external_url", models.URLField(null=True)), - ("about_url", models.URLField(null=True)), - ("is_site_wide", models.BooleanField(null=True)), - ("sub_goal", models.PositiveBigIntegerField(null=True)), - ("minute_watched_goal", models.PositiveBigIntegerField(null=True)), - ("image_url", models.URLField(null=True)), - ( - "game", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="reward_campaigns", - to="core.game", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="DropCampaign", - fields=[ - ("created_at", models.DateTimeField(auto_created=True, null=True)), - ("id", models.TextField(primary_key=True, serialize=False)), - ("modified_at", models.DateTimeField(auto_now=True, null=True)), - ("account_link_url", models.URLField(null=True)), - ("description", models.TextField(null=True)), - ("details_url", models.URLField(null=True)), - ("ends_at", models.DateTimeField(null=True)), - ("starts_at", models.DateTimeField(null=True)), - ("image_url", models.URLField(null=True)), - ("name", models.TextField(null=True)), - ("status", models.TextField(null=True)), - ( - "game", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drop_campaigns", - to="core.game", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="TimeBasedDrop", - fields=[ - ("created_at", models.DateTimeField(auto_created=True, null=True)), - ("id", models.TextField(primary_key=True, serialize=False)), - ("modified_at", models.DateTimeField(auto_now=True, null=True)), - ("required_subs", models.PositiveBigIntegerField(null=True)), - ("ends_at", models.DateTimeField(null=True)), - ("name", models.TextField(null=True)), - ("required_minutes_watched", models.PositiveBigIntegerField(null=True)), - ("starts_at", models.DateTimeField(null=True)), - ( - "drop_campaign", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="drops", - to="core.dropcampaign", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="Channel", - fields=[ - ("twitch_id", models.TextField(primary_key=True, serialize=False)), - ("display_name", models.TextField(null=True)), - ("name", models.TextField(null=True)), - ("twitch_url", models.URLField(null=True)), - ("live", models.BooleanField(default=False)), - ("drop_campaigns", models.ManyToManyField(related_name="channels", to="core.dropcampaign")), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="Benefit", - fields=[ - ("created_at", models.DateTimeField(auto_created=True, null=True)), - ("id", models.TextField(primary_key=True, serialize=False)), - ("modified_at", models.DateTimeField(auto_now=True, null=True)), - ("twitch_created_at", models.DateTimeField(null=True)), - ("entitlement_limit", models.PositiveBigIntegerField(null=True)), - ("image_url", models.URLField(null=True)), - ("is_ios_available", models.BooleanField(null=True)), - ("name", models.TextField(null=True)), - ( - "time_based_drop", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="benefits", - to="core.timebaseddrop", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="Reward", - fields=[ - ("id", models.TextField(primary_key=True, serialize=False)), - ("name", models.TextField(null=True)), - ("banner_image_url", models.URLField(null=True)), - ("thumbnail_image_url", models.URLField(null=True)), - ("earnable_until", models.DateTimeField(null=True)), - ("redemption_instructions", models.TextField(null=True)), - ("redemption_url", models.URLField(null=True)), - ( - "campaign", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="rewards", - to="core.rewardcampaign", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/core/migrations/0004_alter_channel_display_name_alter_channel_twitch_url_and_more.py b/core/migrations/0004_alter_channel_display_name_alter_channel_twitch_url_and_more.py deleted file mode 100644 index 4944620..0000000 --- a/core/migrations/0004_alter_channel_display_name_alter_channel_twitch_url_and_more.py +++ /dev/null @@ -1,37 +0,0 @@ -# Generated by Django 5.1 on 2024-08-13 18:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("core", "0001_squashed_0003_alter_benefit_options_alter_channel_options_and_more"), - ] - - operations = [ - migrations.AlterField( - model_name="channel", - name="display_name", - field=models.TextField(default="Channel name unknown", null=True), - ), - migrations.AlterField( - model_name="channel", - name="twitch_url", - field=models.URLField(default="https://www.twitch.tv/", null=True), - ), - migrations.AlterField( - model_name="game", - name="box_art_url", - field=models.URLField(default="https://static-cdn.jtvnw.net/ttv-static/404_boxart.jpg", null=True), - ), - migrations.AlterField( - model_name="game", - name="game_url", - field=models.URLField(default="https://www.twitch.tv/", null=True), - ), - migrations.AlterField( - model_name="game", - name="name", - field=models.TextField(default="Game name unknown", null=True), - ), - ] diff --git a/core/models/twitch.py b/core/models.py similarity index 79% rename from core/models/twitch.py rename to core/models.py index 453b3f1..57e128b 100644 --- a/core/models/twitch.py +++ b/core/models.py @@ -2,6 +2,7 @@ from __future__ import annotations import logging +from django.contrib.auth.models import AbstractUser from django.db import models logger: logging.Logger = logging.getLogger(__name__) @@ -179,3 +180,64 @@ class Reward(models.Model): def __str__(self) -> str: return self.name or "Reward name unknown" + + +class User(AbstractUser): + """Extended User model to include subscriptions.""" + + subscribed_games = models.ManyToManyField( + "Game", + through="GameSubscription", + related_name="subscribed_users", + blank=True, + ) + subscribed_owners = models.ManyToManyField( + "Owner", + through="OwnerSubscription", + related_name="subscribed_users", + blank=True, + ) + subscribe_to_news = models.BooleanField(default=False, help_text="Subscribe to news") + subscribe_to_new_games = models.BooleanField(default=False, help_text="Subscribe to new games") + + def __str__(self) -> str: + return self.username + + +class DiscordWebhook(models.Model): + """A Discord webhook for sending notifications.""" + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="discord_webhooks") + url = models.URLField() + name = models.CharField(max_length=255) + + def __str__(self) -> str: + return f"{self.name} ({self.user.username})" + + +class GameSubscription(models.Model): + """A subscription to a specific game with a chosen webhook.""" + + user = models.ForeignKey(User, on_delete=models.CASCADE) + game = models.ForeignKey(Game, on_delete=models.CASCADE) + webhook = models.ForeignKey(DiscordWebhook, on_delete=models.CASCADE) + + class Meta: + unique_together = ("user", "game") + + def __str__(self) -> str: + return f"{self.user.username} -> {self.game.name} via {self.webhook.name}" + + +class OwnerSubscription(models.Model): + """A subscription to a specific owner with a chosen webhook.""" + + user = models.ForeignKey(User, on_delete=models.CASCADE) + owner = models.ForeignKey(Owner, on_delete=models.CASCADE) + webhook = models.ForeignKey(DiscordWebhook, on_delete=models.CASCADE) + + class Meta: + unique_together = ("user", "owner") + + def __str__(self) -> str: + return f"{self.user.username} -> {self.owner.name} via {self.webhook.name}" diff --git a/core/models/__init__.py b/core/models/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/core/settings.py b/core/settings.py index 5f47bce..8e186ea 100644 --- a/core/settings.py +++ b/core/settings.py @@ -43,7 +43,7 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" STATICFILES_DIRS: list[Path] = [BASE_DIR / "static"] STATIC_ROOT: Path = BASE_DIR / "staticfiles" STATIC_ROOT.mkdir(exist_ok=True) - +AUTH_USER_MODEL = "core.User" if DEBUG: INTERNAL_IPS: list[str] = ["127.0.0.1"] diff --git a/core/signals.py b/core/signals.py new file mode 100644 index 0000000..2ec2acc --- /dev/null +++ b/core/signals.py @@ -0,0 +1,125 @@ +import logging +from typing import TYPE_CHECKING + +from discord_webhook import DiscordWebhook +from django.db.models.manager import BaseManager +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils import timezone + +from core.models import DropCampaign, Game, GameSubscription, Owner, OwnerSubscription + +if TYPE_CHECKING: + import requests + from django.db.models.manager import BaseManager + +logger: logging.Logger = logging.getLogger(__name__) + + +def convert_time_to_discord_timestamp(time: timezone.datetime | None) -> str: + """Discord uses for timestamps. + + Args: + time: The time to convert to a Discord timestamp. + + Returns: + str: The Discord timestamp string. If time is None, returns "Unknown". + """ + return f"" if time else "Unknown" + + +@receiver(signal=post_save, sender=DropCampaign) +def notify_users_of_new_drop(sender: DropCampaign, instance: DropCampaign, created: bool, **kwargs) -> None: # noqa: ANN003, ARG001, FBT001 + """Notify users of a new drop campaign. + + Args: + sender (DropCampaign): The model we are sending the signal from. + instance (DropCampaign): The instance of the model that was created. + created (bool): Whether the instance was created or updated. + **kwargs: Additional keyword arguments. + """ + if not created: + logger.debug("Drop campaign '%s' was updated.", instance.name) + return + + game: Game | None = instance.game + if not game: + return + + owner: Owner = game.owner # type: ignore # noqa: PGH003 + + # Notify users subscribed to the game + game_subs: BaseManager[GameSubscription] = GameSubscription.objects.filter(game=game) + for sub in game_subs: + if not sub.webhook.url: + logger.error("No webhook URL provided.") + return + + webhook = DiscordWebhook( + url=sub.webhook.url, + content=generate_game_message(instance=instance, game=game, sub=sub), + username=f"{game.name} drop.", + rate_limit_retry=True, + ) + response: requests.Response = webhook.execute() + logger.debug(response) + + # Notify users subscribed to the owner + owner_subs: BaseManager[OwnerSubscription] = OwnerSubscription.objects.filter(owner=owner) + for sub in owner_subs: + if not sub.webhook.url: + logger.error("No webhook URL provided.") + return + + webhook = DiscordWebhook( + url=sub.webhook.url, + content=generate_owner_message(instance=instance, owner=owner, sub=sub), + username=f"{owner.name} drop.", + rate_limit_retry=True, + ) + response: requests.Response = webhook.execute() + logger.debug(response) + + +def generate_game_message(instance: DropCampaign, game: Game, sub: GameSubscription) -> str: + """Generate a message for a drop campaign. + + Args: + instance (DropCampaign): Drop campaign instance. + game (Game): Game instance. + sub (GameSubscription): Game subscription instance. + + Returns: + str: The message to send to Discord. + """ + game_name: str = game.name or "Unknown" + description: str = instance.description or "No description provided." + start_at: str = convert_time_to_discord_timestamp(instance.starts_at) + end_at: str = convert_time_to_discord_timestamp(instance.ends_at) + msg: str = f"{game_name}: {instance.name}\n{description}\nStarts: {start_at}\nEnds: {end_at}" + + logger.info("Discord message: '%s' to '%s'", msg, sub.webhook.url) + + return msg + + +def generate_owner_message(instance: DropCampaign, owner: Owner, sub: OwnerSubscription) -> str: + """Generate a message for a drop campaign. + + Args: + instance (DropCampaign): Drop campaign instance. + owner (Owner): Owner instance. + sub (OwnerSubscription): Owner subscription instance. + + Returns: + str: The message to send to Discord. + """ + owner_name: str = owner.name or "Unknown" + description: str = instance.description or "No description provided." + start_at: str = convert_time_to_discord_timestamp(instance.starts_at) + end_at: str = convert_time_to_discord_timestamp(instance.ends_at) + msg: str = f"{owner_name}: {instance.name}\n{description}\nStarts: {start_at}\nEnds: {end_at}" + + logger.info("Discord message: '%s' to '%s'", msg, sub.webhook.url) + + return msg diff --git a/core/templates/index.html b/core/templates/index.html index 15b93cc..83ba268 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -7,7 +7,10 @@
{% include "partials/info_box.html" %} {% include "partials/news.html" %} -

Reward Campaigns

+

+ Reward campaigns - +
{{ reward_campaigns.count }} campaigns
+

{% for campaign in reward_campaigns %} {% include "partials/reward_campaign_card.html" %} {% endfor %} @@ -16,7 +19,6 @@
{{ games.count }} games
{% for game in games %} - {# Only show games with drop campaigns #} {% if game.drop_campaigns.count > 0 %} {% include "partials/game_card.html" %} {% endif %} diff --git a/core/views.py b/core/views.py index a419f08..6393550 100644 --- a/core/views.py +++ b/core/views.py @@ -9,7 +9,7 @@ from django.db.models.manager import BaseManager from django.template.response import TemplateResponse from django.utils import timezone # type: ignore # noqa: PGH003 -from core.models.twitch import DropCampaign, Game, RewardCampaign +from core.models import DropCampaign, Game, RewardCampaign if TYPE_CHECKING: from django.db.models.manager import BaseManager