Add Twitch auth with django-allauth
This commit is contained in:
@ -4,3 +4,11 @@ from django.apps import AppConfig
|
||||
class CoreConfig(AppConfig):
|
||||
default_auto_field: str = "django.db.models.BigAutoField"
|
||||
name = "core"
|
||||
|
||||
@staticmethod
|
||||
def ready() -> None:
|
||||
"""Ready runs on app startup.
|
||||
|
||||
We import signals here so that they are registered when the app starts.
|
||||
"""
|
||||
import core.signals # noqa: F401, PLC0415
|
||||
|
@ -3,6 +3,9 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from discord_webhook import DiscordWebhook
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import DropCampaign, Game, GameSubscription, Owner, OwnerSubscription
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from requests import Response
|
||||
@ -32,3 +35,59 @@ def send(message: str, webhook_url: str | None = None) -> None:
|
||||
)
|
||||
response: Response = webhook.execute()
|
||||
logger.debug(response)
|
||||
|
||||
|
||||
def convert_time_to_discord_timestamp(time: timezone.datetime | None) -> str:
|
||||
"""Discord uses <t:UNIX_TIMESTAMP:R> 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"<t:{int(time.timestamp())}:R>" if time else "Unknown"
|
||||
|
||||
|
||||
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
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.1 on 2024-08-15 00:28
|
||||
# Generated by Django 5.1 on 2024-08-15 03:42
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
|
@ -1,5 +1,6 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
import sentry_sdk
|
||||
from django.contrib import messages
|
||||
@ -74,6 +75,11 @@ INSTALLED_APPS: list[str] = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"django.contrib.sites",
|
||||
"allauth",
|
||||
"allauth.account",
|
||||
"allauth.socialaccount",
|
||||
"allauth.socialaccount.providers.twitch",
|
||||
"simple_history",
|
||||
"debug_toolbar",
|
||||
]
|
||||
@ -89,6 +95,7 @@ MIDDLEWARE: list[str] = [
|
||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||
"django.contrib.messages.middleware.MessageMiddleware",
|
||||
"simple_history.middleware.HistoryRequestMiddleware",
|
||||
"allauth.account.middleware.AccountMiddleware",
|
||||
]
|
||||
|
||||
TEMPLATES = [
|
||||
@ -177,3 +184,25 @@ CACHES = {
|
||||
"LOCATION": DATA_DIR / "django_cache",
|
||||
},
|
||||
}
|
||||
SITE_ID = 1
|
||||
AUTHENTICATION_BACKENDS: list[str] = [
|
||||
"django.contrib.auth.backends.ModelBackend",
|
||||
"allauth.account.auth_backends.AuthenticationBackend",
|
||||
]
|
||||
|
||||
SOCIALACCOUNT_PROVIDERS = {
|
||||
"twitch": {
|
||||
"APP": {"client_id": os.environ["TWITCH_CLIENT_ID"], "secret": os.environ["TWITCH_CLIENT_SECRET"], "key": ""},
|
||||
"SCOPE": [],
|
||||
"AUTH_PARAMS": {
|
||||
"force_verify": "true",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
SOCIALACCOUNT_STORE_TOKENS = True
|
||||
SOCIALACCOUNT_ONLY = True
|
||||
ACCOUNT_EMAIL_VERIFICATION: Literal["mandatory", "optional", "none"] = "none"
|
||||
ACCOUNT_SESSION_REMEMBER = True
|
||||
LOGIN_REDIRECT_URL = "/"
|
||||
ACCOUNT_LOGOUT_REDIRECT_URL = "/"
|
||||
|
@ -1,13 +1,14 @@
|
||||
import logging
|
||||
import os
|
||||
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
|
||||
from core.discord import generate_game_message, generate_owner_message
|
||||
from core.models import DropCampaign, Game, GameSubscription, Owner, OwnerSubscription, User
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import requests
|
||||
@ -16,16 +17,35 @@ if TYPE_CHECKING:
|
||||
logger: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def convert_time_to_discord_timestamp(time: timezone.datetime | None) -> str:
|
||||
"""Discord uses <t:UNIX_TIMESTAMP:R> for timestamps.
|
||||
@receiver(signal=post_save, sender=User)
|
||||
def handle_user_signed_up(sender: User, instance: User, created: bool, **kwargs) -> None: # noqa: ANN003, ARG001, FBT001
|
||||
"""Send a message to Discord when a user signs up.
|
||||
|
||||
Webhook URL is read from .env file.
|
||||
|
||||
Args:
|
||||
time: The time to convert to a Discord timestamp.
|
||||
|
||||
Returns:
|
||||
str: The Discord timestamp string. If time is None, returns "Unknown".
|
||||
sender (User): The model we are sending the signal from.
|
||||
instance (User): The instance of the model that was created.
|
||||
created (bool): Whether the instance was created or updated.
|
||||
**kwargs: Additional keyword arguments.
|
||||
"""
|
||||
return f"<t:{int(time.timestamp())}:R>" if time else "Unknown"
|
||||
if not created:
|
||||
logger.debug("User '%s' was updated.", instance.username)
|
||||
return
|
||||
|
||||
webhook_url: str | None = os.getenv("DISCORD_WEBHOOK_URL")
|
||||
if not webhook_url:
|
||||
logger.error("No webhook URL provided.")
|
||||
return
|
||||
|
||||
webhook = DiscordWebhook(
|
||||
url=webhook_url,
|
||||
content=f"New user signed up: '{instance.username}'",
|
||||
username="TTVDrops",
|
||||
rate_limit_retry=True,
|
||||
)
|
||||
response: requests.Response = webhook.execute()
|
||||
logger.debug(response)
|
||||
|
||||
|
||||
@receiver(signal=post_save, sender=DropCampaign)
|
||||
@ -79,47 +99,3 @@ def notify_users_of_new_drop(sender: DropCampaign, instance: DropCampaign, creat
|
||||
)
|
||||
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
|
||||
|
@ -1,3 +1,4 @@
|
||||
{% load socialaccount %}
|
||||
<header class="d-flex justify-content-between align-items-center py-3 border-bottom">
|
||||
<h1 class="h2">
|
||||
<a href='{% url "index" %}' class="text-decoration-none nav-title">Twitch drops</a>
|
||||
@ -16,6 +17,18 @@
|
||||
<li class="nav-item d-none d-sm-block">
|
||||
<a class="nav-link" href="https://github.com/sponsors/TheLovinator1">Donate</a>
|
||||
</li>
|
||||
{% if not user.is_authenticated %}
|
||||
<li>
|
||||
<a class="nav-link" href="{% provider_login_url 'twitch' %}">Login with Twitch</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>
|
||||
<form action="{% url 'account_logout' %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-link nav-link">Logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
|
@ -2,7 +2,7 @@ from __future__ import annotations
|
||||
|
||||
from debug_toolbar.toolbar import debug_toolbar_urls
|
||||
from django.contrib import admin
|
||||
from django.urls import URLPattern, URLResolver, path
|
||||
from django.urls import URLPattern, URLResolver, include, path
|
||||
|
||||
from core.views import game_view, index, reward_campaign_view
|
||||
|
||||
@ -10,6 +10,7 @@ app_name: str = "core"
|
||||
|
||||
urlpatterns: list[URLPattern | URLResolver] = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("accounts/", include("allauth.urls"), name="accounts"),
|
||||
path(route="", view=index, name="index"),
|
||||
path(
|
||||
route="games/",
|
||||
|
Reference in New Issue
Block a user