From 219aee31af46c02953788c24b291a613565abb11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20Hells=C3=A9n?= Date: Mon, 1 Jul 2024 05:56:36 +0200 Subject: [PATCH] WIP --- .vscode/settings.json | 2 + config/settings.py | 19 +- config/urls.py | 8 +- core/admin.py | 6 + core/discord.py | 9 +- core/forms.py | 15 ++ core/migrations/0001_initial.py | 62 ++++++ ...scordsetting_notification_type_and_more.py | 26 +++ core/migrations/0003_subscription.py | 53 +++++ core/models.py | 24 ++ core/templates/add_discord_webhook.html | 206 ++++++++++++++++++ core/templates/index.html | 78 ++++--- core/urls.py | 15 ++ core/views.py | 142 ++++++++++-- requirements.txt | 10 +- ...ptions_alter_dropcampaign_game_and_more.py | 36 --- ...l_added_at_channel_modified_at_and_more.py | 83 ------- .../0004_alter_dropcampaign_options.py | 17 -- {twitch => twitch_app}/__init__.py | 0 twitch_app/admin.py | 9 + {twitch => twitch_app}/api.py | 8 - {twitch => twitch_app}/apps.py | 2 +- {twitch => twitch_app}/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/scrape_twitch.py | 19 +- .../migrations/0001_initial.py | 57 +++-- .../migrations/0002_game_image_url.py | 15 +- {twitch => twitch_app}/migrations/__init__.py | 0 {twitch => twitch_app}/models.py | 21 +- {twitch => twitch_app}/urls.py | 0 30 files changed, 679 insertions(+), 263 deletions(-) create mode 100644 core/admin.py create mode 100644 core/forms.py create mode 100644 core/migrations/0001_initial.py create mode 100644 core/migrations/0002_remove_discordsetting_notification_type_and_more.py create mode 100644 core/migrations/0003_subscription.py create mode 100644 core/models.py create mode 100644 core/templates/add_discord_webhook.html delete mode 100644 twitch/migrations/0002_alter_dropcampaign_options_alter_dropcampaign_game_and_more.py delete mode 100644 twitch/migrations/0003_channel_added_at_channel_modified_at_and_more.py delete mode 100644 twitch/migrations/0004_alter_dropcampaign_options.py rename {twitch => twitch_app}/__init__.py (100%) create mode 100644 twitch_app/admin.py rename {twitch => twitch_app}/api.py (93%) rename {twitch => twitch_app}/apps.py (81%) rename {twitch => twitch_app}/management/__init__.py (100%) rename {twitch => twitch_app}/management/commands/__init__.py (100%) rename {twitch => twitch_app}/management/commands/scrape_twitch.py (92%) rename {twitch => twitch_app}/migrations/0001_initial.py (62%) rename twitch/migrations/0005_game_twitch_url.py => twitch_app/migrations/0002_game_image_url.py (55%) rename {twitch => twitch_app}/migrations/__init__.py (100%) rename {twitch => twitch_app}/models.py (89%) rename {twitch => twitch_app}/urls.py (100%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 157ecfe..11d6358 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,6 @@ { "cSpell.words": [ + "allauth", "appendonly", "asgiref", "logdir", @@ -8,6 +9,7 @@ "PGID", "PUID", "requirepass", + "socialaccount", "ttvdrops", "ulimits" ] diff --git a/config/settings.py b/config/settings.py index ce40619..e0ae7c8 100644 --- a/config/settings.py +++ b/config/settings.py @@ -63,7 +63,7 @@ DISCORD_WEBHOOK_URL: str = os.getenv(key="DISCORD_WEBHOOK_URL", default="") INSTALLED_APPS: list[str] = [ "core.apps.CoreConfig", - "twitch.apps.TwitchConfig", + "twitch_app.apps.TwitchConfig", "whitenoise.runserver_nostatic", "django.contrib.admin", "django.contrib.auth", @@ -72,6 +72,10 @@ INSTALLED_APPS: list[str] = [ "django.contrib.messages", "django.contrib.staticfiles", "ninja", + "allauth", + "allauth.account", + "allauth.socialaccount", + "allauth.socialaccount.providers.twitch", ] MIDDLEWARE: list[str] = [ @@ -83,6 +87,7 @@ MIDDLEWARE: list[str] = [ "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "allauth.account.middleware.AccountMiddleware", ] @@ -155,3 +160,15 @@ LOGGING = { }, }, } + +LOGIN_URL = "/login/" +LOGIN_REDIRECT_URL = "/" +LOGOUT_REDIRECT_URL = "/" +SOCIALACCOUNT_ONLY = True +ACCOUNT_EMAIL_VERIFICATION = "none" + + +AUTHENTICATION_BACKENDS: list[str] = [ + "django.contrib.auth.backends.ModelBackend", + "allauth.account.auth_backends.AuthenticationBackend", +] diff --git a/config/urls.py b/config/urls.py index b6ccf6b..48d16c2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,21 +1,27 @@ +import logging + from django.contrib import admin from django.urls import URLPattern, include, path from django.urls.resolvers import URLResolver from ninja import NinjaAPI -from twitch.api import router as twitch_router +from twitch_app.api import router as twitch_router + +logger: logging.Logger = logging.getLogger(__name__) api = NinjaAPI( title="TTVDrops API", version="1.0.0", description="No rate limits, but don't abuse it.", ) + api.add_router(prefix="/twitch", router=twitch_router) app_name: str = "config" urlpatterns: list[URLPattern | URLResolver] = [ path(route="admin/", view=admin.site.urls), + path(route="accounts/", view=include(arg="allauth.urls")), path(route="", view=include(arg="core.urls")), path(route="api/", view=api.urls), ] diff --git a/core/admin.py b/core/admin.py new file mode 100644 index 0000000..264170b --- /dev/null +++ b/core/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from .models import DiscordSetting, Subscription + +admin.site.register(DiscordSetting) +admin.site.register(Subscription) diff --git a/core/discord.py b/core/discord.py index 59e9529..2053ed1 100644 --- a/core/discord.py +++ b/core/discord.py @@ -10,15 +10,18 @@ if TYPE_CHECKING: logger: logging.Logger = logging.getLogger(__name__) -def send(message: str) -> None: +def send(message: str, webhook_url: str | None = None) -> None: """Send a message to Discord. Args: message: The message to send. + webhook_url: The webhook URL to send the message to. """ - webhook_url = str(settings.DISCORD_WEBHOOK_URL) + logger.info("Discord message: %s", message) + + webhook_url = webhook_url or str(settings.DISCORD_WEBHOOK_URL) if not webhook_url: - logger.error("No Discord webhook URL found.") + logger.error("No webhook URL provided.") return webhook = DiscordWebhook( diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..2a8f165 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,15 @@ +from django import forms + + +class DiscordSettingForm(forms.Form): + name = forms.CharField( + max_length=255, + label="Name", + required=True, + help_text="Friendly name for knowing where the notification goes to.", + ) + webhook_url = forms.URLField( + label="Webhook URL", + required=True, + help_text="The URL to the Discord webhook. The URL can be found by right-clicking on the channel and selecting 'Edit Channel', then 'Integrations', and 'Webhooks'.", # noqa: E501 + ) diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..bdf7d1c --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,62 @@ +# Generated by Django 5.0.6 on 2024-06-30 23:42 + +import django.db.models.deletion +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]] = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations: list[Operation] = [ + migrations.CreateModel( + name="NotificationType", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=255)), + ], + ), + migrations.CreateModel( + name="DiscordSetting", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("webhook_url", models.URLField()), + ("disabled", models.BooleanField(default=False)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "notification_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.notificationtype", + ), + ), + ], + ), + ] diff --git a/core/migrations/0002_remove_discordsetting_notification_type_and_more.py b/core/migrations/0002_remove_discordsetting_notification_type_and_more.py new file mode 100644 index 0000000..3204d7b --- /dev/null +++ b/core/migrations/0002_remove_discordsetting_notification_type_and_more.py @@ -0,0 +1,26 @@ +# Generated by Django 5.0.6 on 2024-07-01 01:10 + +from django.db import migrations, models +from django.db.migrations.operations.base import Operation + + +class Migration(migrations.Migration): + dependencies: list[tuple[str, str]] = [ + ("core", "0001_initial"), + ] + + operations: list[Operation] = [ + migrations.RemoveField( + model_name="discordsetting", + name="notification_type", + ), + migrations.AddField( + model_name="discordsetting", + name="name", + field=models.CharField(default="No name", max_length=255), + preserve_default=False, + ), + migrations.DeleteModel( + name="NotificationType", + ), + ] diff --git a/core/migrations/0003_subscription.py b/core/migrations/0003_subscription.py new file mode 100644 index 0000000..19956e4 --- /dev/null +++ b/core/migrations/0003_subscription.py @@ -0,0 +1,53 @@ +# Generated by Django 5.0.6 on 2024-07-01 03:28 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models +from django.db.migrations.operations.base import Operation + + +class Migration(migrations.Migration): + dependencies: list[tuple[str, str]] = [ + ("core", "0002_remove_discordsetting_notification_type_and_more"), + ("twitch_app", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations: list[Operation] = [ + migrations.CreateModel( + name="Subscription", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ( + "discord_webhook", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="core.discordsetting", + ), + ), + ( + "game", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="twitch_app.game", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/core/models.py b/core/models.py new file mode 100644 index 0000000..b1e63e6 --- /dev/null +++ b/core/models.py @@ -0,0 +1,24 @@ +from django.contrib.auth.models import User +from django.db import models + +from twitch_app.models import Game + + +class DiscordSetting(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + name = models.CharField(max_length=255) + webhook_url = models.URLField() + disabled = models.BooleanField(default=False) + + def __str__(self) -> str: + return f"Discord: {self.user.username} - {self.name}" + + +class Subscription(models.Model): + user = models.ForeignKey(User, on_delete=models.CASCADE) + game = models.ForeignKey(Game, on_delete=models.CASCADE) + created_at = models.DateTimeField(auto_now_add=True) + discord_webhook = models.ForeignKey(DiscordSetting, on_delete=models.CASCADE) + + def __str__(self) -> str: + return f"Subscription: {self.user.username} - {self.game.display_name} - {self.discord_webhook.name}" diff --git a/core/templates/add_discord_webhook.html b/core/templates/add_discord_webhook.html new file mode 100644 index 0000000..ce4a030 --- /dev/null +++ b/core/templates/add_discord_webhook.html @@ -0,0 +1,206 @@ + + + + + + + + + + + Add Discord Webhook + + + + {% if messages %} + + {% endif %} +

+ Twitch Drops +

+ +

Add Discord Webhook

+
+ {% csrf_token %} + {{ form.non_field_errors }} +
+ {{ form.name.errors }} + + {{ form.name }} + {{ form.name.help_text }} +
+
+ {{ form.webhook_url.errors }} + + {{ form.webhook_url }} + {{ form.webhook_url.help_text }} +
+ +
+

Webhooks

+ + + diff --git a/core/templates/index.html b/core/templates/index.html index 59a1bd2..4a1e7ba 100644 --- a/core/templates/index.html +++ b/core/templates/index.html @@ -1,3 +1,4 @@ +{% load socialaccount %} @@ -17,9 +18,13 @@ --border-color: #333; --button-background: #6441a5; --button-hover-background: #503682; + --button-shadow: rgba(0, 0, 0, 0.2); + --button-padding: 0.5rem 1rem; + --button-margin: 0.5rem 0; + --button-radius: 0.5rem; + --button-font-size: 0.875rem; } - /* Set some good defaults for the page */ html { max-width: 88ch; padding: calc(1vmin + 0.5rem); @@ -30,76 +35,67 @@ color: var(--text-color); } - /* Don't underline links and remove the blue/purple color */ a { text-decoration: none; color: inherit; } - - /* Add a gray background for the game name header */ - /* This header also contains the button to subscribe to the game */ + header { - display: flex; - align-items: center; - justify-content: space-between; /* So the button is on the right, and the game name is on the left */ padding: 10px 30px; background: var(--header-background); + display: flex; + flex-direction: column; + align-items: flex-start; } - /* Remove dot in front of list items */ ul { list-style-type: none; padding: 0; } - - /* Add a border around each game to separate them */ + .game { margin-bottom: 1rem; border: 1px solid var(--border-color); } - - /* Move images away from the border */ + img { margin: 10px; } - - /* Button to subscribe to a game */ - /* For example: Subscribe to Rocket League */ + button { background-color: var(--button-background); color: white; border: none; - padding: 0.5rem 1rem; - border-radius: 5px; + padding: var(--button-padding); + margin: var(--button-margin); + border-radius: var(--button-radius); cursor: pointer; - transition: background-color 0.3s ease; - } - - /* Make button darker when hovered */ - button:hover { - background-color: var(--button-hover-background); + transition: background-color 0.3s ease, box-shadow 0.3s ease; + font-size: var(--button-font-size); + box-shadow: 0 2px 4px var(--button-shadow); + } + + button:hover { + background-color: var(--button-hover-background); + box-shadow: 0 3px 6px var(--button-shadow); } - /* Navbar at the top of the page */ .navbar { margin-bottom: 1rem; text-align: center; } - /* Make the logo bigger and bolder and center it */ .logo { text-align: center; font-size: 2.5rem; font-weight: 600; margin: 0; } - - /* Django messages framework */ + .messages { - list-style-type: none; + list-style-type: none; } - /* Make error messages red and success messages green */ .error { color: red; } @@ -121,19 +117,34 @@ {% for organization, org_data in orgs_data.items %}