import logging
from typing import ClassVar
from requests.exceptions import HTTPError
from django.contrib.auth.models import User
from django.db import models
from django.utils.translation import gettext_lazy
from allianceauth.notifications import notify
from . import __title__
from .app_settings import DISCORD_GUILD_ID
from .core import calculate_roles_for_user, create_bot_client, default_bot_client, user_formatted_nick
from .discord_client import DiscordApiBackoff
from .managers import DiscordUserManager
from .utils import LoggerAddTag
logger = LoggerAddTag(logging.getLogger(__name__), __title__)
[docs]
class DiscordUser(models.Model):
"""The Discord user account of an Auth user."""
user = models.OneToOneField(
User,
primary_key=True,
on_delete=models.CASCADE,
related_name="discord",
help_text="Auth user owning this Discord account",
)
uid = models.BigIntegerField(db_index=True, help_text="user's ID on Discord")
username = models.CharField(
max_length=32, default="", blank=True, db_index=True, help_text="user's username on Discord"
)
discriminator = models.CharField(max_length=4, default="", blank=True, help_text="user's discriminator on Discord")
activated = models.DateTimeField(
default=None, null=True, blank=True, help_text="Date & time this service account was activated"
)
objects: ClassVar[DiscordUserManager] = DiscordUserManager()
class Meta:
default_permissions = ()
permissions = (("access_discord", "Can access the Discord service"),)
def __str__(self) -> str:
return f"{self.user.username} - {self.uid}"
def __repr__(self) -> str:
return f"{type(self).__name__}(user='{self.user}', uid={self.uid})"
def update_nickname(self, nickname: str = None) -> bool:
"""Update nickname with formatted name of main character
Params:
- nickname: optional nickname to be used instead of user's main
Returns:
- True on success
- None if user is no longer a member of the Discord server
- False on error or raises exception
"""
if not nickname:
nickname = user_formatted_nick(self.user)
if not nickname:
return False
success = default_bot_client.modify_guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid, nick=nickname)
if success:
logger.info("Nickname for %s has been updated", self.user)
else:
logger.warning("Failed to update nickname for %s", self.user)
return success
def update_groups(self, state_name: str = None) -> bool | None:
"""update groups for a user based on his current group memberships.
Will add or remove roles of a user as needed.
Params:
- state_name: optional state name to be used
Returns:
- True on success
- None if user is no longer a member of the Discord server
- False on error or raises exception
"""
new_roles, is_changed = calculate_roles_for_user(
user=self.user, client=default_bot_client, discord_uid=self.uid, state_name=state_name
)
if is_changed is None:
logger.debug("User is not a member of this guild %s", self.user)
return None
if is_changed:
logger.debug("Need to update roles for user %s", self.user)
success = default_bot_client.modify_guild_member(
guild_id=DISCORD_GUILD_ID, user_id=self.uid, role_ids=list(new_roles.ids())
)
if success:
logger.info("Roles for %s have been updated", self.user)
else:
logger.warning("Failed to update roles for %s", self.user)
return success
logger.info("No need to update roles for user %s", self.user)
return True
def update_username(self) -> bool | None:
"""Updates the username incl. the discriminator
from the Discord server and saves it
Returns:
- True on success
- None if user is no longer a member of the Discord server
"""
member_info = default_bot_client.guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
if not member_info:
logger.warning("%s: User not a guild member", self.user)
return None
self.username = member_info.user.username
self.discriminator = member_info.user.discriminator
self.save()
logger.info("%s: Username has been updated", self.user)
return True
def delete_user(
self, notify_user: bool = False, is_rate_limited: bool = True, handle_api_exceptions: bool = False
) -> bool | None:
"""Deletes the Discount user both on the server and locally
Params:
- notify_user: When True will sent a notification to the user
informing him about the deleting of his account
- is_rate_limited: When False will disable default rate limiting (use with care)
- handle_api_exceptions: When True method will return False
when an API exception occurs
Returns True when successful, otherwise False or raises exceptions
Return None if user does no longer exist
"""
try:
_user = self.user
client = create_bot_client(is_rate_limited=is_rate_limited)
success = client.remove_guild_member(guild_id=DISCORD_GUILD_ID, user_id=self.uid)
if success is not False:
deleted_count, _ = self.delete()
if deleted_count > 0:
if notify_user:
notify(
user=_user,
title=gettext_lazy("Discord Account Disabled"),
message=gettext_lazy(
"Your Discord account was disabled automatically "
"by Auth. If you think this was a mistake, "
"please contact an admin."
),
level="warning",
)
logger.info("Account for user %s was deleted.", _user)
return True
logger.debug("Account for user %s was already deleted.", _user)
return None
logger.warning("Failed to remove user %s from the Discord server", _user)
return False
except (HTTPError, ConnectionError, DiscordApiBackoff) as ex:
if handle_api_exceptions:
logger.exception("Failed to remove user %s from Discord server: %s", self.user, ex)
return False
raise ex