diff --git a/.gitignore b/.gitignore
index b9af219..6eaa1fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -162,3 +162,5 @@ cython_debug/
 *.json
 
 staticfiles/
+
+*.sqlite
diff --git a/core/discord.py b/core/discord.py
index 03556aa..4b3a87a 100644
--- a/core/discord.py
+++ b/core/discord.py
@@ -1,42 +1,10 @@
 import logging
-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
-
 logger: logging.Logger = logging.getLogger(__name__)
 
 
-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.
-    """
-    logger.info("Discord message: %s", message)
-
-    webhook_url = webhook_url or str(settings.DISCORD_WEBHOOK_URL)
-    if not webhook_url:
-        logger.error("No webhook URL provided.")
-        return
-
-    webhook = DiscordWebhook(
-        url=webhook_url,
-        content=message,
-        username="TTVDrops",
-        rate_limit_retry=True,
-    )
-    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.
 
@@ -47,47 +15,3 @@ def convert_time_to_discord_timestamp(time: timezone.datetime | None) -> str:
         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
diff --git a/core/management/commands/scrape_twitch.py b/core/management/commands/scrape_twitch.py
index 92ef0b3..50f3e76 100644
--- a/core/management/commands/scrape_twitch.py
+++ b/core/management/commands/scrape_twitch.py
@@ -68,78 +68,99 @@ def save_json(campaign: dict | None, dir_name: str) -> None:
         json.dump(campaign, f, indent=4)
 
 
-async def add_reward_campaign(campaign: dict | None) -> None:  # noqa: C901
+async def add_reward_campaign(campaign: dict | None) -> None:
     """Add a reward campaign to the database.
 
     Args:
         campaign (dict): The reward campaign to add.
     """
-    # sourcery skip: low-code-quality
     if not campaign:
         return
 
-    logger.info("Adding reward campaign to database")
+    logger.info("Adding reward campaign to database %s", campaign["id"])
     if "data" in campaign and "rewardCampaignsAvailableToUser" in campaign["data"]:
-        mappings: dict[str, str] = {
-            "brand": "brand",
-            "createdAt": "created_at",
-            "startsAt": "starts_at",
-            "endsAt": "ends_at",
-            "status": "status",
-            "summary": "summary",
-            "instructions": "instructions",
-            "rewardValueURLParam": "reward_value_url_param",
-            "externalURL": "external_url",
-            "aboutURL": "about_url",
-            "isSitewide": "is_site_wide",
-        }
-
         for reward_campaign in campaign["data"]["rewardCampaignsAvailableToUser"]:
-            defaults = {new_key: reward_campaign[key] for key, new_key in mappings.items() if reward_campaign.get(key)}
+            our_reward_campaign = await handle_reward_campaign(reward_campaign)
 
-            if reward_campaign.get("unlockRequirements", {}).get("subsGoal"):
-                defaults["sub_goal"] = reward_campaign["unlockRequirements"]["subsGoal"]
+            if "rewards" in reward_campaign:
+                for reward in reward_campaign["rewards"]:
+                    await handle_rewards(reward, our_reward_campaign)
 
-            if reward_campaign.get("unlockRequirements", {}).get("minuteWatchedGoal"):
-                defaults["minute_watched_goal"] = reward_campaign["unlockRequirements"]["minuteWatchedGoal"]
 
-            if reward_campaign.get("image"):
-                defaults["image_url"] = reward_campaign["image"]["image1xURL"]
+async def handle_rewards(reward: dict, reward_campaign: RewardCampaign | None) -> None:
+    """Add or update a reward in the database.
 
-            our_reward_campaign, created = await RewardCampaign.objects.aupdate_or_create(
-                id=reward_campaign["id"],
-                defaults=defaults,
-            )
-            if created:
-                logger.info("Added reward campaign %s", our_reward_campaign.id)
+    Args:
+        reward (dict): The JSON from Twitch.
+        reward_campaign (RewardCampaign | None): The reward campaign the reward belongs to.
+    """
+    mappings: dict[str, str] = {
+        "name": "name",
+        "earnableUntil": "earnable_until",
+        "redemptionInstructions": "redemption_instructions",
+        "redemptionURL": "redemption_url",
+    }
 
-            if reward_campaign["game"]:
-                # TODO(TheLovinator): Add game to reward campaign  # noqa: TD003
-                # TODO(TheLovinator): Send JSON to Discord # noqa: TD003
-                logger.error("Not implemented: Add game to reward campaign, JSON: %s", reward_campaign["game"])
+    defaults: dict = {new_key: reward[key] for key, new_key in mappings.items() if reward.get(key)}
+    if reward_campaign:
+        defaults["campaign"] = reward_campaign
 
-            # Add rewards
-            mappings: dict[str, str] = {
-                "name": "name",
-                "bannerImage": "banner_image_url",
-                "thumbnailImage": "thumbnail_image_url",
-                "earnableUntil": "earnable_until",
-                "redemptionInstructions": "redemption_instructions",
-                "redemptionURL": "redemption_url",
-            }
-            for reward in reward_campaign["rewards"]:
-                defaults = {new_key: reward[key] for key, new_key in mappings.items() if reward.get(key)}
-                if our_reward_campaign:
-                    defaults["campaign"] = our_reward_campaign
-                    our_reward, created = await Reward.objects.aupdate_or_create(
-                        id=reward["id"],
-                        defaults=defaults,
-                    )
+        if reward.get("bannerImage"):
+            defaults["banner_image_url"] = reward["bannerImage"]["image1xURL"]
 
-                if created:
-                    logger.info("Added reward %s", our_reward.id)
-                else:
-                    logger.info("Updated reward %s", our_reward.id)
+        if reward.get("thumbnailImage"):
+            defaults["thumbnail_image_url"] = reward["thumbnailImage"]["image1xURL"]
+
+        reward_instance, created = await Reward.objects.aupdate_or_create(id=reward["id"], defaults=defaults)
+    if created:
+        logger.info("Added reward %s", reward_instance.id)
+
+
+async def handle_reward_campaign(reward_campaign: dict) -> RewardCampaign:
+    """Add or update a reward campaign in the database.
+
+    Args:
+        reward_campaign (dict): The reward campaign JSON from Twitch.
+
+    Returns:
+        RewardCampaign: The reward campaign that was added or updated.
+    """
+    mappings: dict[str, str] = {
+        "brand": "brand",
+        "createdAt": "created_at",
+        "startsAt": "starts_at",
+        "endsAt": "ends_at",
+        "status": "status",
+        "summary": "summary",
+        "instructions": "instructions",
+        "rewardValueURLParam": "reward_value_url_param",
+        "externalURL": "external_url",
+        "aboutURL": "about_url",
+        "isSitewide": "is_site_wide",
+    }
+
+    defaults: dict = {new_key: reward_campaign[key] for key, new_key in mappings.items() if reward_campaign.get(key)}
+    unlock_requirements: dict = reward_campaign.get("unlockRequirements", {})
+    if unlock_requirements.get("subsGoal"):
+        defaults["sub_goal"] = unlock_requirements["subsGoal"]
+    if unlock_requirements.get("minuteWatchedGoal"):
+        defaults["minute_watched_goal"] = unlock_requirements["minuteWatchedGoal"]
+
+    if reward_campaign.get("image"):
+        defaults["image_url"] = reward_campaign["image"]["image1xURL"]
+
+    reward_campaign_instance, created = await RewardCampaign.objects.aupdate_or_create(
+        id=reward_campaign["id"],
+        defaults=defaults,
+    )
+    if created:
+        logger.info("Added reward campaign %s", reward_campaign_instance.id)
+
+    if reward_campaign["game"]:
+        # TODO(TheLovinator): Add game to reward campaign  # noqa: TD003
+        # TODO(TheLovinator): Send JSON to Discord # noqa: TD003
+        logger.error("Not implemented: Add game to reward campaign, JSON: %s", reward_campaign["game"])
+    return reward_campaign_instance
 
 
 async def add_or_update_game(game_json: dict | None) -> Game | None:
@@ -347,7 +368,7 @@ async def process_json_data(num: int, campaign: dict | None) -> None:
         return
 
     if not isinstance(campaign, dict):
-        logger.warning("Campaign is not a dictionary")
+        logger.warning("Campaign is not a dictionary. %s", campaign)
         return
 
     # This is a Reward Campaign
@@ -356,12 +377,7 @@ async def process_json_data(num: int, campaign: dict | None) -> None:
         await add_reward_campaign(campaign)
 
     if "dropCampaign" in campaign.get("data", {}).get("user", {}):
-        if not campaign["data"]["user"]["dropCampaign"]:
-            logger.warning("No drop campaign found")
-            return
-
         save_json(campaign, "drop_campaign")
-
         if campaign.get("data", {}).get("user", {}).get("dropCampaign"):
             await add_drop_campaign(campaign["data"]["user"]["dropCampaign"])
 
diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
index b993bb5..f506a42 100644
--- a/core/migrations/0001_initial.py
+++ b/core/migrations/0001_initial.py
@@ -1,10 +1,9 @@
-# Generated by Django 5.1 on 2024-08-15 03:42
+# Generated by Django 5.1 on 2024-08-16 02:38
 
 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
 
@@ -98,8 +97,6 @@ class Migration(migrations.Migration):
                     ),
                 ),
                 ("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(
@@ -132,22 +129,6 @@ class Migration(migrations.Migration):
                 ("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=[
@@ -169,28 +150,6 @@ class Migration(migrations.Migration):
                 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",
@@ -201,28 +160,6 @@ class Migration(migrations.Migration):
                 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=[
@@ -319,4 +256,34 @@ class Migration(migrations.Migration):
                 ),
             ],
         ),
+        migrations.CreateModel(
+            name="Webhook",
+            fields=[
+                ("avatar", models.TextField(null=True)),
+                ("channel_id", models.TextField(null=True)),
+                ("guild_id", models.TextField(null=True)),
+                ("id", models.TextField(primary_key=True, serialize=False)),
+                ("name", models.TextField(null=True)),
+                ("type", models.TextField(null=True)),
+                ("token", models.TextField()),
+                ("url", models.TextField()),
+                ("seen_drops", models.ManyToManyField(related_name="seen_drops", to="core.dropcampaign")),
+                (
+                    "subscribed_live_games",
+                    models.ManyToManyField(related_name="subscribed_live_games", to="core.game"),
+                ),
+                (
+                    "subscribed_live_owners",
+                    models.ManyToManyField(related_name="subscribed_live_owners", to="core.owner"),
+                ),
+                ("subscribed_new_games", models.ManyToManyField(related_name="subscribed_new_games", to="core.game")),
+                (
+                    "subscribed_new_owners",
+                    models.ManyToManyField(related_name="subscribed_new_owners", to="core.owner"),
+                ),
+            ],
+            options={
+                "unique_together": {("id", "token")},
+            },
+        ),
     ]
diff --git a/core/models.py b/core/models.py
index 57e128b..54ad39c 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 import logging
+from typing import ClassVar
 
 from django.contrib.auth.models import AbstractUser
 from django.db import models
@@ -8,6 +9,9 @@ from django.db import models
 logger: logging.Logger = logging.getLogger(__name__)
 
 
+class User(AbstractUser): ...
+
+
 class Owner(models.Model):
     """The company or person that owns the game.
 
@@ -182,62 +186,41 @@ class Reward(models.Model):
         return self.name or "Reward name unknown"
 
 
-class User(AbstractUser):
-    """Extended User model to include subscriptions."""
+class Webhook(models.Model):
+    """Discord webhook."""
 
-    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")
+    avatar = models.TextField(null=True)
+    channel_id = models.TextField(null=True)
+    guild_id = models.TextField(null=True)
+    id = models.TextField(primary_key=True)
+    name = models.TextField(null=True)
+    type = models.TextField(null=True)
+    token = models.TextField()
+    url = models.TextField()
 
-    def __str__(self) -> str:
-        return self.username
+    # Get notified when the site finds a new game.
+    subscribed_new_games = models.ManyToManyField(Game, related_name="subscribed_new_games")
 
+    # Get notified when a drop goes live.
+    subscribed_live_games = models.ManyToManyField(Game, related_name="subscribed_live_games")
 
-class DiscordWebhook(models.Model):
-    """A Discord webhook for sending notifications."""
+    # Get notified when the site finds a new drop campaign for a specific organization.
+    subscribed_new_owners = models.ManyToManyField(Owner, related_name="subscribed_new_owners")
 
-    user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="discord_webhooks")
-    url = models.URLField()
-    name = models.CharField(max_length=255)
+    # Get notified when a drop goes live for a specific organization.
+    subscribed_live_owners = models.ManyToManyField(Owner, related_name="subscribed_live_owners")
 
-    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)
+    # So we don't spam the same drop campaign over and over.
+    seen_drops = models.ManyToManyField(DropCampaign, related_name="seen_drops")
 
     class Meta:
-        unique_together = ("user", "game")
+        unique_together: ClassVar[list[str]] = ["id", "token"]
 
     def __str__(self) -> str:
-        return f"{self.user.username} -> {self.game.name} via {self.webhook.name}"
+        return f"{self.name} - {self.get_webhook_url()}"
 
-
-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}"
+    def get_webhook_url(self) -> str:
+        try:
+            return f"https://discord.com/api/webhooks/{self.id}/{self.token}"
+        except AttributeError:
+            return ""
diff --git a/core/settings.py b/core/settings.py
index 96a1c53..864d908 100644
--- a/core/settings.py
+++ b/core/settings.py
@@ -1,6 +1,5 @@
 import os
 from pathlib import Path
-from typing import Literal
 
 import sentry_sdk
 from django.contrib import messages
@@ -76,10 +75,6 @@ INSTALLED_APPS: list[str] = [
     "django.contrib.messages",
     "django.contrib.staticfiles",
     "django.contrib.sites",
-    "allauth",
-    "allauth.account",
-    "allauth.socialaccount",
-    "allauth.socialaccount.providers.twitch",
     "simple_history",
     "debug_toolbar",
 ]
@@ -95,7 +90,6 @@ MIDDLEWARE: list[str] = [
     "django.contrib.auth.middleware.AuthenticationMiddleware",
     "django.contrib.messages.middleware.MessageMiddleware",
     "simple_history.middleware.HistoryRequestMiddleware",
-    "allauth.account.middleware.AccountMiddleware",
 ]
 
 TEMPLATES = [
@@ -185,24 +179,7 @@ CACHES = {
     },
 }
 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 = "/"
diff --git a/core/signals.py b/core/signals.py
index c5c5d60..71e3d09 100644
--- a/core/signals.py
+++ b/core/signals.py
@@ -3,20 +3,40 @@ 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 core.discord import generate_game_message, generate_owner_message
-from core.models import DropCampaign, Game, GameSubscription, Owner, OwnerSubscription, User
+from core.discord import convert_time_to_discord_timestamp
+from core.models import DropCampaign, Game, Owner, User, Webhook
 
 if TYPE_CHECKING:
     import requests
-    from django.db.models.manager import BaseManager
 
 logger: logging.Logger = logging.getLogger(__name__)
 
 
+def generate_message(game: Game, drop: DropCampaign) -> str:
+    """Generate a message for a game.
+
+    Args:
+        game (Game): The game to generate a message for.
+        drop (DropCampaign): The drop campaign to generate a message for.
+
+    Returns:
+        str: The message.
+    """
+    # TODO(TheLovinator): Add a twitch link to a stream that has drops enabled.  # noqa: TD003
+    game_name: str = game.name or "Unknown game"
+    description: str = drop.description or "No description available."
+    start_at: str = convert_time_to_discord_timestamp(drop.starts_at)
+    end_at: str = convert_time_to_discord_timestamp(drop.ends_at)
+    msg: str = f"**{game_name}**\n\n{description}\n\nStarts: {start_at}\nEnds: {end_at}"
+
+    logger.debug(msg)
+
+    return msg
+
+
 @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.
@@ -48,9 +68,8 @@ def handle_user_signed_up(sender: User, instance: User, created: bool, **kwargs)
     logger.debug(response)
 
 
-@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.
+    """Send message to all webhooks subscribed to new drops.
 
     Args:
         sender (DropCampaign): The model we are sending the signal from.
@@ -64,37 +83,81 @@ def notify_users_of_new_drop(sender: DropCampaign, instance: DropCampaign, creat
 
     game: Game | None = instance.game
     if not game:
+        logger.error("No game found. %s", instance)
         return
 
+    if game.owner:  # type: ignore  # noqa: PGH003
+        handle_owner_drops(instance, game)
+    else:
+        logger.error("No owner found. %s", instance)
+
+    if game := instance.game:
+        handle_game_drops(instance, game)
+    else:
+        logger.error("No game found. %s", instance)
+
+
+def handle_game_drops(instance: DropCampaign, game: Game) -> None:
+    """Send message to all webhooks subscribed to new drops for this game.
+
+    Args:
+        instance (DropCampaign): The drop campaign that was created.
+        game (Game): The game that the drop campaign is for.
+    """
+    webhooks: list[Webhook] = game.subscribed_new_games.all()  # type: ignore  # noqa: PGH003
+    for hook in webhooks:
+        # Don't spam the same drop campaign.
+        if hook in hook.seen_drops.all():
+            logger.error("Already seen drop campaign '%s'.", instance.name)
+            continue
+
+            # Set the webhook as seen so we don't spam it.
+        hook.seen_drops.add(instance)
+
+        # Send the webhook.
+        webhook_url: str = hook.get_webhook_url()
+        if not webhook_url:
+            logger.error("No webhook URL provided.")
+            continue
+
+        webhook = DiscordWebhook(
+            url=webhook_url,
+            content=generate_message(game, instance),
+            username=f"{game.name} Twitch drops",
+            rate_limit_retry=True,
+        )
+        response: requests.Response = webhook.execute()
+        logger.debug(response)
+
+
+def handle_owner_drops(instance: DropCampaign, game: Game) -> None:
+    """Send message to all webhooks subscribed to new drops for this owner/organization.
+
+    Args:
+        instance (DropCampaign): The drop campaign that was created.
+        game (Game): The game that the drop campaign is for.
+    """
     owner: Owner = game.owner  # type: ignore  # noqa: PGH003
+    webhooks: list[Webhook] = owner.subscribed_new_games.all()  # type: ignore  # noqa: PGH003
+    for hook in webhooks:
+        # Don't spam the same drop campaign.
+        if hook in hook.seen_drops.all():
+            logger.error("Already seen drop campaign '%s'.", instance.name)
+            continue
 
-    # 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:
+            # Set the webhook as seen so we don't spam it.
+        hook.seen_drops.add(instance)
+
+        # Send the webhook.
+        webhook_url: str = hook.get_webhook_url()
+        if not webhook_url:
             logger.error("No webhook URL provided.")
-            return
+            continue
 
         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.",
+            url=webhook_url,
+            content=generate_message(game, instance),
+            username=f"{game.name} Twitch drops",
             rate_limit_retry=True,
         )
         response: requests.Response = webhook.execute()
diff --git a/core/templates/partials/game_card.html b/core/templates/partials/game_card.html
index c627590..9e2e29d 100644
--- a/core/templates/partials/game_card.html
+++ b/core/templates/partials/game_card.html
@@ -18,9 +18,7 @@
                     <!-- Insert nice buttons -->
                 </div>
                 {% for campaign in game.drop_campaigns.all %}
-                    {% if not forloop.first %}<br>{% endif %}
                     <div class="mt-3">
-                        <h3 class="h6">{{ campaign.name }}</h3>
                         {% if campaign.details_url == campaign.account_link_url %}
                             <a href="{{ campaign.details_url }}" class="text-decoration-none">Details</a>
                         {% else %}
@@ -31,26 +29,7 @@
                         <p class="mb-2 text-muted">
                             Ends in: <abbr title="{{ campaign.starts_at|date:'l d F H:i' }} - {{ campaign.ends_at|date:'l d F H:i e' }}">{{ campaign.ends_at|timeuntil }}</abbr>
                         </p>
-                        {% if campaign.description != campaign.name %}
-                            {% if campaign.description|length > 300 %}
-                                <p>
-                                    <a class="btn btn-link p-0 text-muted"
-                                       data-bs-toggle="collapse"
-                                       href="#collapseDescription{{ campaign.id }}"
-                                       role="button"
-                                       aria-expanded="false"
-                                       aria-controls="collapseDescription{{ campaign.id }}"
-                                       aria-label="Show Description">Show Description</a>
-                                </p>
-                                <div class="collapse" id="collapseDescription{{ campaign.id }}">
-                                    <div class="card card-body">{{ campaign.description }}</div>
-                                    <br>
-                                </div>
-                            {% else %}
-                                <p>{{ campaign.description }}</p>
-                            {% endif %}
-                        {% endif %}
-                        <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-2">
+                        <div class="row row-cols-1 row-cols-md-2 row-cols-lg-3">
                             {% for drop in campaign.drops.all %}
                                 {% for benefit in drop.benefits.all %}
                                     <div class="col d-flex align-items-center position-relative">
diff --git a/core/templates/partials/header.html b/core/templates/partials/header.html
index 7357a68..06c02a8 100644
--- a/core/templates/partials/header.html
+++ b/core/templates/partials/header.html
@@ -1,4 +1,3 @@
-{% 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>
@@ -17,18 +16,9 @@
             <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 %}
+            <li class="nav-item d-none d-sm-block">
+                <a class="nav-link" href='{% url "webhooks" %}'>Webhooks</a>
+            </li>
         </ul>
     </nav>
 </header>
diff --git a/core/templates/webhooks.html b/core/templates/webhooks.html
index 384d007..4997943 100644
--- a/core/templates/webhooks.html
+++ b/core/templates/webhooks.html
@@ -24,5 +24,27 @@
             </form>
         </div>
         <h2 class="mt-5">Webhooks</h2>
+        {% if webhooks %}
+            <div class="list-group">
+                {% for webhook in webhooks %}
+                    <div class="list-group-item d-flex justify-content-between align-items-center">
+                        <span>
+                            {% if webhook.avatar %}
+                                <img src="https://cdn.discordapp.com/avatars/{{ webhook.id }}/a_{{ webhook.avatar }}.png"
+                                     alt="Avatar of {{ webhook.name }}"
+                                     class="rounded-circle"
+                                     height="32"
+                                     width="32">
+                            {% endif %}
+                            <a href="https://discord.com/api/webhooks/{{ webhook.id }}/{{ webhook.token }}"
+                               target="_blank"
+                               class="text-decoration-none">{{ webhook.name }}</a>
+                        </span>
+                    </div>
+                {% endfor %}
+            </div>
+        {% else %}
+            <div class="alert alert-info">No webhooks added</div>
+        {% endif %}
     </div>
 {% endblock content %}
diff --git a/core/urls.py b/core/urls.py
index 859f866..af38202 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -2,25 +2,17 @@ from __future__ import annotations
 
 from debug_toolbar.toolbar import debug_toolbar_urls
 from django.contrib import admin
-from django.urls import URLPattern, URLResolver, include, path
+from django.urls import URLPattern, URLResolver, path
 
-from core.views import game_view, index, reward_campaign_view
+from core.views import WebhooksView, game_view, index, reward_campaign_view
 
 app_name: str = "core"
 
 urlpatterns: list[URLPattern | URLResolver] = [
-    path("admin/", admin.site.urls),
-    path("accounts/", include("allauth.urls"), name="accounts"),
+    path(route="admin/", view=admin.site.urls),
     path(route="", view=index, name="index"),
-    path(
-        route="games/",
-        view=game_view,
-        name="games",
-    ),
-    path(
-        route="reward_campaigns/",
-        view=reward_campaign_view,
-        name="reward_campaigns",
-    ),
+    path(route="webhooks/", view=WebhooksView.as_view(), name="webhooks"),
+    path(route="games/", view=game_view, name="games"),
+    path(route="reward_campaigns/", view=reward_campaign_view, name="reward_campaigns"),
     *debug_toolbar_urls(),
 ]
diff --git a/core/views.py b/core/views.py
index 6393550..17cb296 100644
--- a/core/views.py
+++ b/core/views.py
@@ -4,16 +4,19 @@ import logging
 from dataclasses import dataclass
 from typing import TYPE_CHECKING
 
+import requests_cache
 from django.db.models import Prefetch
 from django.db.models.manager import BaseManager
+from django.http import HttpRequest, HttpResponse
 from django.template.response import TemplateResponse
-from django.utils import timezone  # type: ignore  # noqa: PGH003
+from django.utils import timezone
+from django.views import View
 
-from core.models import DropCampaign, Game, RewardCampaign
+from core.models import DropCampaign, Game, RewardCampaign, Webhook
 
 if TYPE_CHECKING:
     from django.db.models.manager import BaseManager
-    from django.http import HttpRequest, HttpResponse
+    from django.http import HttpRequest
 
 logger: logging.Logger = logging.getLogger(__name__)
 
@@ -114,3 +117,129 @@ def reward_campaign_view(request: HttpRequest) -> HttpResponse:
     reward_campaigns: BaseManager[RewardCampaign] = RewardCampaign.objects.all()
     context: dict[str, BaseManager[RewardCampaign]] = {"reward_campaigns": reward_campaigns}
     return TemplateResponse(request=request, template="reward_campaigns.html", context=context)
+
+
+def get_webhook_data(webhook_url: str) -> dict[str, str]:
+    """Get the webhook data from the URL.
+
+    Args:
+        webhook_url (str): The webhook URL.
+
+    Returns:
+        dict[str, str]: The webhook data.
+    """
+    session = requests_cache.CachedSession("webhook_cache")
+    response: requests_cache.OriginalResponse | requests_cache.CachedResponse = session.get(webhook_url)
+    return response.json()
+
+
+def split_webhook_url(webhook_url: str) -> tuple[str, str]:
+    """Split the webhook URL into its components.
+
+    Webhooks are in the format:
+        https://discord.com/api/webhooks/{id}/{token}
+
+    Args:
+        webhook_url (str): The webhook URL.
+
+    Returns:
+        tuple[str, str]: The ID and token.
+    """
+    webhook_id: str = webhook_url.split("/")[-2]
+    webhook_token: str = webhook_url.split("/")[-1]
+    return webhook_id, webhook_token
+
+
+class WebhooksView(View):
+    """Render the webhook view page."""
+
+    @staticmethod
+    def post(request: HttpRequest) -> HttpResponse:
+        """Add a webhook to the list of webhooks.
+
+        Args:
+            request (HttpRequest): The request object.
+
+        Returns:
+            HttpResponse: The response object.
+        """
+        webhook_url: str | None = request.POST.get("webhook_url")
+        if not webhook_url:
+            return HttpResponse(content="No webhook URL provided.", status=400)
+
+        # Read webhooks from cookie.
+        webhooks_cookies: str | None = request.COOKIES.get("webhooks")
+        webhooks_list: list[str] = webhooks_cookies.split(",") if webhooks_cookies else []
+
+        # Get webhook data.
+        webhook_id, webhook_token = split_webhook_url(webhook_url)
+        webhook_data: dict[str, str] = get_webhook_data(webhook_url)
+        list_of_json_keys: list[str] = ["avatar", "channel_id", "guild_id", "name", "type", "url"]
+        defaults: dict[str, str | None] = {key: webhook_data.get(key) for key in list_of_json_keys}
+
+        # Warn if JSON has more keys than expected.
+        if len(webhook_data.keys()) > len(list_of_json_keys):
+            logger.warning("Unexpected keys in JSON: %s", webhook_data.keys())
+
+        # Add the webhook to the database.
+        new_webhook, created = Webhook.objects.update_or_create(
+            id=webhook_id,
+            token=webhook_token,
+            defaults=defaults,
+        )
+        if created:
+            logger.info("Created webhook '%s'.", new_webhook)
+
+        # Add the new webhook to the list.
+        webhooks_list.append(webhook_url)
+
+        # Remove duplicates.
+        webhooks_list = list(set(webhooks_list))
+
+        # Save the new list of webhooks to the cookie.
+        response: HttpResponse = HttpResponse()
+        response.set_cookie("webhooks", ",".join(webhooks_list))
+
+        # Redirect to the webhooks page.
+        response["Location"] = "/webhooks/"
+        response.status_code = 302
+        return response
+
+    @staticmethod
+    def get(request: HttpRequest) -> HttpResponse:
+        # Read webhooks from cookie.
+        webhooks_cookies: str | None = request.COOKIES.get("webhooks")
+        webhooks_list: list[str] = webhooks_cookies.split(",") if webhooks_cookies else []
+
+        webhooks_from_db: list[Webhook] = []
+        # Get the webhooks from the database.
+        for webhook_url in webhooks_list:
+            webhook_id, webhook_token = split_webhook_url(webhook_url)
+
+            # Check if the webhook is in the database.
+            if not Webhook.objects.filter(id=webhook_id, token=webhook_token).exists():
+                webhook_data: dict[str, str] = get_webhook_data(webhook_url)
+                list_of_json_keys: list[str] = ["avatar", "channel_id", "guild_id", "name", "type", "url"]
+                defaults: dict[str, str | None] = {key: webhook_data.get(key) for key in list_of_json_keys}
+
+                # Warn if JSON has more keys than expected.
+                if len(webhook_data.keys()) > len(list_of_json_keys):
+                    logger.warning("Unexpected keys in JSON: %s", webhook_data.keys())
+
+                new_webhook, created = Webhook.objects.update_or_create(
+                    id=webhook_id,
+                    token=webhook_token,
+                    defaults=defaults,
+                )
+                if created:
+                    logger.info("Created webhook '%s'.", new_webhook)
+
+                webhooks_from_db.append(new_webhook)
+
+            # If the webhook is in the database, get it from there.
+            else:
+                existing_webhook: Webhook = Webhook.objects.get(id=webhook_id, token=webhook_token)
+                webhooks_from_db.append(existing_webhook)
+
+        context: dict[str, list[Webhook]] = {"webhooks": webhooks_from_db}
+        return TemplateResponse(request=request, template="webhooks.html", context=context)
diff --git a/poetry.lock b/poetry.lock
index 1e5912c..41880ae 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -14,6 +14,25 @@ files = [
 [package.extras]
 tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
 
+[[package]]
+name = "attrs"
+version = "24.2.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.7"
+files = [
+    {file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
+    {file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
+]
+
+[package.extras]
+benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
+tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
+
 [[package]]
 name = "brotli"
 version = "1.1.0"
@@ -106,6 +125,29 @@ files = [
     {file = "Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724"},
 ]
 
+[[package]]
+name = "cattrs"
+version = "23.2.3"
+description = "Composable complex class support for attrs and dataclasses."
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "cattrs-23.2.3-py3-none-any.whl", hash = "sha256:0341994d94971052e9ee70662542699a3162ea1e0c62f7ce1b4a57f563685108"},
+    {file = "cattrs-23.2.3.tar.gz", hash = "sha256:a934090d95abaa9e911dac357e3a8699e0b4b14f8529bcc7d2b1ad9d51672b9f"},
+]
+
+[package.dependencies]
+attrs = ">=23.1.0"
+
+[package.extras]
+bson = ["pymongo (>=4.4.0)"]
+cbor2 = ["cbor2 (>=5.4.6)"]
+msgpack = ["msgpack (>=1.0.5)"]
+orjson = ["orjson (>=3.9.2)"]
+pyyaml = ["pyyaml (>=6.0)"]
+tomlkit = ["tomlkit (>=0.11.8)"]
+ujson = ["ujson (>=5.7.0)"]
+
 [[package]]
 name = "certifi"
 version = "2024.7.4"
@@ -117,85 +159,6 @@ files = [
     {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"},
 ]
 
-[[package]]
-name = "cffi"
-version = "1.17.0"
-description = "Foreign Function Interface for Python calling C code."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "cffi-1.17.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb"},
-    {file = "cffi-1.17.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a"},
-    {file = "cffi-1.17.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42"},
-    {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d"},
-    {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2"},
-    {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab"},
-    {file = "cffi-1.17.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b"},
-    {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206"},
-    {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa"},
-    {file = "cffi-1.17.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f"},
-    {file = "cffi-1.17.0-cp310-cp310-win32.whl", hash = "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc"},
-    {file = "cffi-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2"},
-    {file = "cffi-1.17.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720"},
-    {file = "cffi-1.17.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8"},
-    {file = "cffi-1.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6"},
-    {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91"},
-    {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8"},
-    {file = "cffi-1.17.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb"},
-    {file = "cffi-1.17.0-cp311-cp311-win32.whl", hash = "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9"},
-    {file = "cffi-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0"},
-    {file = "cffi-1.17.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc"},
-    {file = "cffi-1.17.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828"},
-    {file = "cffi-1.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150"},
-    {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a"},
-    {file = "cffi-1.17.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885"},
-    {file = "cffi-1.17.0-cp312-cp312-win32.whl", hash = "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492"},
-    {file = "cffi-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2"},
-    {file = "cffi-1.17.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118"},
-    {file = "cffi-1.17.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204"},
-    {file = "cffi-1.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f"},
-    {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0"},
-    {file = "cffi-1.17.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4"},
-    {file = "cffi-1.17.0-cp313-cp313-win32.whl", hash = "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a"},
-    {file = "cffi-1.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7"},
-    {file = "cffi-1.17.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c"},
-    {file = "cffi-1.17.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e"},
-    {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b"},
-    {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e"},
-    {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401"},
-    {file = "cffi-1.17.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c"},
-    {file = "cffi-1.17.0-cp38-cp38-win32.whl", hash = "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499"},
-    {file = "cffi-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c"},
-    {file = "cffi-1.17.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2"},
-    {file = "cffi-1.17.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759"},
-    {file = "cffi-1.17.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4"},
-    {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82"},
-    {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf"},
-    {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058"},
-    {file = "cffi-1.17.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932"},
-    {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693"},
-    {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3"},
-    {file = "cffi-1.17.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4"},
-    {file = "cffi-1.17.0-cp39-cp39-win32.whl", hash = "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb"},
-    {file = "cffi-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29"},
-    {file = "cffi-1.17.0.tar.gz", hash = "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76"},
-]
-
-[package.dependencies]
-pycparser = "*"
-
 [[package]]
 name = "cfgv"
 version = "3.4.0"
@@ -331,55 +294,6 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
-[[package]]
-name = "cryptography"
-version = "43.0.0"
-description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
-optional = false
-python-versions = ">=3.7"
-files = [
-    {file = "cryptography-43.0.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47"},
-    {file = "cryptography-43.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf"},
-    {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55"},
-    {file = "cryptography-43.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431"},
-    {file = "cryptography-43.0.0-cp37-abi3-win32.whl", hash = "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc"},
-    {file = "cryptography-43.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778"},
-    {file = "cryptography-43.0.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5"},
-    {file = "cryptography-43.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f"},
-    {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0"},
-    {file = "cryptography-43.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b"},
-    {file = "cryptography-43.0.0-cp39-abi3-win32.whl", hash = "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf"},
-    {file = "cryptography-43.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709"},
-    {file = "cryptography-43.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70"},
-    {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66"},
-    {file = "cryptography-43.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f"},
-    {file = "cryptography-43.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f"},
-    {file = "cryptography-43.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2"},
-    {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947"},
-    {file = "cryptography-43.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069"},
-    {file = "cryptography-43.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1"},
-    {file = "cryptography-43.0.0.tar.gz", hash = "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e"},
-]
-
-[package.dependencies]
-cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
-
-[package.extras]
-docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
-docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
-nox = ["nox"]
-pep8test = ["check-sdist", "click", "mypy", "ruff"]
-sdist = ["build"]
-ssh = ["bcrypt (>=3.1.5)"]
-test = ["certifi", "cryptography-vectors (==43.0.0)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
-test-randomorder = ["pytest-randomly"]
-
 [[package]]
 name = "cssbeautifier"
 version = "1.15.1"
@@ -443,29 +357,6 @@ tzdata = {version = "*", markers = "sys_platform == \"win32\""}
 argon2 = ["argon2-cffi (>=19.1.0)"]
 bcrypt = ["bcrypt"]
 
-[[package]]
-name = "django-allauth"
-version = "64.0.0"
-description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication."
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "django_allauth-64.0.0.tar.gz", hash = "sha256:01952c7540160ef475c12dc881f67a69c7c5e533f4bef6c811bba0417a717588"},
-]
-
-[package.dependencies]
-Django = ">=4.2"
-pyjwt = {version = ">=1.7", extras = ["crypto"], optional = true, markers = "extra == \"socialaccount\""}
-requests = {version = ">=2.0.0", optional = true, markers = "extra == \"socialaccount\""}
-requests-oauthlib = {version = ">=0.3.0", optional = true, markers = "extra == \"socialaccount\""}
-
-[package.extras]
-mfa = ["fido2 (>=1.1.2)", "qrcode (>=7.0.0)"]
-openid = ["python3-openid (>=3.0.8)"]
-saml = ["python3-saml (>=1.15.0,<2.0.0)"]
-socialaccount = ["pyjwt[crypto] (>=1.7)", "requests (>=2.0.0)", "requests-oauthlib (>=0.3.0)"]
-steam = ["python3-openid (>=3.0.8)"]
-
 [[package]]
 name = "django-debug-toolbar"
 version = "4.4.6"
@@ -709,22 +600,6 @@ files = [
     {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
 ]
 
-[[package]]
-name = "oauthlib"
-version = "3.2.2"
-description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"},
-    {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"},
-]
-
-[package.extras]
-rsa = ["cryptography (>=3.0.0)"]
-signals = ["blinker (>=1.4.0)"]
-signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
-
 [[package]]
 name = "packaging"
 version = "24.1"
@@ -893,17 +768,6 @@ nodeenv = ">=0.11.1"
 pyyaml = ">=5.1"
 virtualenv = ">=20.10.0"
 
-[[package]]
-name = "pycparser"
-version = "2.22"
-description = "C parser in Python"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
-    {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
-]
-
 [[package]]
 name = "pyee"
 version = "11.0.1"
@@ -921,26 +785,6 @@ typing-extensions = "*"
 [package.extras]
 dev = ["black", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio", "pytest-trio", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"]
 
-[[package]]
-name = "pyjwt"
-version = "2.9.0"
-description = "JSON Web Token implementation in Python"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "PyJWT-2.9.0-py3-none-any.whl", hash = "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850"},
-    {file = "pyjwt-2.9.0.tar.gz", hash = "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c"},
-]
-
-[package.dependencies]
-cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""}
-
-[package.extras]
-crypto = ["cryptography (>=3.4.0)"]
-dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"]
-docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
-tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
-
 [[package]]
 name = "pytest"
 version = "8.3.2"
@@ -1179,22 +1023,34 @@ socks = ["PySocks (>=1.5.6,!=1.5.7)"]
 use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
-name = "requests-oauthlib"
-version = "2.0.0"
-description = "OAuthlib authentication support for Requests."
+name = "requests-cache"
+version = "1.2.1"
+description = "A persistent cache for python requests"
 optional = false
-python-versions = ">=3.4"
+python-versions = ">=3.8"
 files = [
-    {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"},
-    {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"},
+    {file = "requests_cache-1.2.1-py3-none-any.whl", hash = "sha256:1285151cddf5331067baa82598afe2d47c7495a1334bfe7a7d329b43e9fd3603"},
+    {file = "requests_cache-1.2.1.tar.gz", hash = "sha256:68abc986fdc5b8d0911318fbb5f7c80eebcd4d01bfacc6685ecf8876052511d1"},
 ]
 
 [package.dependencies]
-oauthlib = ">=3.0.0"
-requests = ">=2.0.0"
+attrs = ">=21.2"
+cattrs = ">=22.2"
+platformdirs = ">=2.5"
+requests = ">=2.22"
+url-normalize = ">=1.4"
+urllib3 = ">=1.25.5"
 
 [package.extras]
-rsa = ["oauthlib[signedtoken] (>=3.0.0)"]
+all = ["boto3 (>=1.15)", "botocore (>=1.18)", "itsdangerous (>=2.0)", "pymongo (>=3)", "pyyaml (>=6.0.1)", "redis (>=3)", "ujson (>=5.4)"]
+bson = ["bson (>=0.5)"]
+docs = ["furo (>=2023.3,<2024.0)", "linkify-it-py (>=2.0,<3.0)", "myst-parser (>=1.0,<2.0)", "sphinx (>=5.0.2,<6.0.0)", "sphinx-autodoc-typehints (>=1.19)", "sphinx-automodapi (>=0.14)", "sphinx-copybutton (>=0.5)", "sphinx-design (>=0.2)", "sphinx-notfound-page (>=0.8)", "sphinxcontrib-apidoc (>=0.3)", "sphinxext-opengraph (>=0.9)"]
+dynamodb = ["boto3 (>=1.15)", "botocore (>=1.18)"]
+json = ["ujson (>=5.4)"]
+mongodb = ["pymongo (>=3)"]
+redis = ["redis (>=3)"]
+security = ["itsdangerous (>=2.0)"]
+yaml = ["pyyaml (>=6.0.1)"]
 
 [[package]]
 name = "ruff"
@@ -1363,6 +1219,20 @@ files = [
 greenlet = "3.0.1"
 pyee = "11.0.1"
 
+[[package]]
+name = "url-normalize"
+version = "1.4.3"
+description = "URL normalization for Python"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+files = [
+    {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"},
+    {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"},
+]
+
+[package.dependencies]
+six = "*"
+
 [[package]]
 name = "urllib3"
 version = "2.2.2"
@@ -1420,4 +1290,4 @@ brotli = ["brotli"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.12"
-content-hash = "4e9d7d30cf5dae3db8143b3abe1efe4f92d1f24ec5b728ab57c8d568723e24b8"
+content-hash = "e0e08bfb89f04f48832324abd7155f3d199021d83afb7dd934aead158af8a7cb"
diff --git a/pyproject.toml b/pyproject.toml
index 075a244..08042ba 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -10,12 +10,13 @@ package-mode = false
 python = "^3.12"
 discord-webhook = "^1.3.1"
 django = { version = "^5.1", allow-prereleases = true }
-django-allauth = { extras = ["socialaccount"], version = "^64.0.0" }
 django-debug-toolbar = "^4.4.6"
 django-simple-history = "^3.7.0"
 pillow = "^10.4.0"
 platformdirs = "^4.2.2"
 python-dotenv = "^1.0.1"
+requests = "^2.32.3"
+requests-cache = "^1.2.1"
 sentry-sdk = { extras = ["django"], version = "^2.13.0" }
 undetected-playwright-patch = "^1.40.0.post1700587210000"
 whitenoise = { extras = ["brotli"], version = "^6.7.0" }