lib.itmens/users/models/user.py

342 lines
11 KiB
Python
Raw Normal View History

import re
from datetime import timedelta
from functools import cached_property
2023-09-16 11:09:57 -04:00
from typing import TYPE_CHECKING, ClassVar
2024-07-05 18:15:10 -04:00
import django_rq
2024-02-20 20:20:43 -05:00
import httpx
2024-04-03 23:10:21 -04:00
from django.conf import settings
2023-08-14 08:15:55 -04:00
from django.contrib.auth.models import AbstractUser, BaseUserManager
2023-08-11 01:43:19 -04:00
from django.contrib.auth.validators import UnicodeUsernameValidator
2023-07-04 17:21:17 -04:00
from django.core.exceptions import ValidationError
2024-02-20 20:20:43 -05:00
from django.core.files.base import ContentFile
2024-07-01 17:29:38 -04:00
from django.db import models, transaction
2024-07-03 00:07:07 -04:00
from django.db.models.functions import Lower
from django.urls import reverse
from django.utils import timezone, translation
from django.utils.deconstruct import deconstructible
2021-12-24 11:56:04 -08:00
from django.utils.translation import gettext_lazy as _
from loguru import logger
2024-07-03 00:07:07 -04:00
from mastodon.models import (
BlueskyAccount,
EmailAccount,
MastodonAccount,
SocialAccount,
ThreadsAccount,
)
2023-07-20 21:59:49 -04:00
from takahe.utils import Takahe
from users.models import preference
if TYPE_CHECKING:
2023-07-20 21:59:49 -04:00
from .apidentity import APIdentity
from .preference import Preference
2023-07-04 17:21:17 -04:00
2023-07-12 01:11:15 -04:00
_RESERVED_USERNAMES = [
2023-07-04 17:21:17 -04:00
"connect",
"__",
"admin",
2024-04-12 20:42:36 -04:00
"administrator",
2024-04-19 20:24:34 -04:00
"service",
"support",
2024-04-12 20:42:36 -04:00
"system",
"user",
"users",
2023-07-04 17:21:17 -04:00
"api",
2024-04-19 20:24:34 -04:00
"bot",
2023-07-04 17:21:17 -04:00
"me",
]
2020-05-01 22:46:15 +08:00
@deconstructible
2023-08-11 01:43:19 -04:00
class UsernameValidator(UnicodeUsernameValidator):
2023-07-04 17:21:17 -04:00
regex = r"^[a-zA-Z0-9_]{2,30}$"
message = _(
"Enter a valid username. This value may contain only unaccented lowercase a-z and uppercase A-Z letters, numbers, and _ characters."
)
flags = re.ASCII
2023-07-04 17:21:17 -04:00
def __call__(self, value):
2023-07-12 01:11:15 -04:00
if value and value.lower() in _RESERVED_USERNAMES:
2023-07-04 17:21:17 -04:00
raise ValidationError(self.message, code=self.code)
return super().__call__(value)
2023-08-14 08:15:55 -04:00
class UserManager(BaseUserManager):
def create_user(self, username, email, password=None):
2024-07-03 00:07:07 -04:00
from mastodon.models import Email
2023-08-14 08:15:55 -04:00
Takahe.get_domain() # ensure configuration is complete
2024-07-03 00:07:07 -04:00
user = User.register(username=username)
e = Email.new_account(email)
if not e:
raise ValueError("Invalid Email")
e.user = user
e.save()
2023-08-14 08:15:55 -04:00
return user
def create_superuser(self, username, email, password=None):
2024-07-03 00:07:07 -04:00
from mastodon.models import Email
2023-08-14 08:15:55 -04:00
from takahe.models import User as TakaheUser
Takahe.get_domain() # ensure configuration is complete
2024-07-03 00:07:07 -04:00
user = User.register(username=username, is_superuser=True)
e = Email.new_account(email)
if not e:
raise ValueError("Invalid Email")
e.user = user
e.save()
2023-08-14 08:15:55 -04:00
tu = TakaheUser.objects.get(pk=user.pk, email="@" + username)
tu.admin = True
tu.save()
return user
2020-05-01 22:46:15 +08:00
class User(AbstractUser):
2023-07-20 21:59:49 -04:00
identity: "APIdentity"
2023-07-12 01:11:15 -04:00
preference: "Preference"
2024-07-01 17:29:38 -04:00
social_accounts: "models.QuerySet[SocialAccount]"
objects: ClassVar[UserManager] = UserManager()
username_validator = UsernameValidator()
username = models.CharField(
_("username"),
2023-07-02 16:56:09 -04:00
max_length=100,
unique=True,
2024-07-06 13:13:05 -04:00
null=False,
blank=False,
help_text=_("Required. 50 characters or fewer. Letters, digits and _ only."),
validators=[username_validator],
error_messages={
"unique": _("A user with that username already exists."),
},
)
2024-07-01 17:29:38 -04:00
language = models.CharField(
_("language"),
max_length=10,
choices=settings.LANGUAGES,
null=False,
default="en",
)
2020-10-22 21:45:05 +02:00
class Meta:
constraints = [
models.UniqueConstraint(
Lower("username"),
name="unique_username",
),
]
2024-07-03 00:07:07 -04:00
indexes = [models.Index("is_active", name="index_user_is_active")]
2020-05-01 22:46:15 +08:00
2024-07-01 17:29:38 -04:00
@cached_property
def mastodon(self) -> "MastodonAccount | None":
return MastodonAccount.objects.filter(user=self).first()
2024-07-03 00:07:07 -04:00
@cached_property
def threads(self) -> "ThreadsAccount | None":
return ThreadsAccount.objects.filter(user=self).first()
@cached_property
def bluesky(self) -> "BlueskyAccount | None":
return BlueskyAccount.objects.filter(user=self).first()
2024-07-01 17:29:38 -04:00
@cached_property
def email_account(self) -> "EmailAccount | None":
return EmailAccount.objects.filter(user=self).first()
2023-07-07 16:54:15 -04:00
@cached_property
def mastodon_acct(self):
2024-07-01 17:29:38 -04:00
return self.mastodon.handle if self.mastodon else ""
2020-10-30 13:18:31 +01:00
2024-07-01 17:29:38 -04:00
@cached_property
2023-07-07 02:02:48 -04:00
def locked(self):
2024-07-01 17:29:38 -04:00
return self.identity.locked
2023-07-07 02:02:48 -04:00
2022-05-30 19:43:29 -04:00
@property
def display_name(self):
2024-07-01 17:29:38 -04:00
return self.identity.display_name
2022-05-30 19:43:29 -04:00
@property
def avatar(self):
2023-08-24 05:48:14 +00:00
return (
self.identity.avatar if self.identity else settings.SITE_INFO["user_icon"]
)
2022-11-08 02:32:35 +00:00
@property
def url(self):
2024-07-01 17:29:38 -04:00
return reverse("journal:user_profile", args=[self.username])
2022-11-08 02:32:35 +00:00
2023-12-23 00:57:00 -05:00
@property
def absolute_url(self):
return settings.SITE_INFO["site_url"] + self.url
def __str__(self):
2024-07-05 18:15:10 -04:00
return f'{self.pk}:{self.username or "<missing>"}'
2023-09-03 20:11:46 +00:00
@property
def registration_complete(self):
return self.username is not None
@property
def last_usage(self):
2024-01-14 22:16:53 -05:00
from journal.models import ShelfMember
2024-01-14 22:16:53 -05:00
p = (
ShelfMember.objects.filter(owner=self.identity)
.order_by("-edited_time")
.first()
)
return p.edited_time if p else None
def clear(self):
2024-07-03 00:07:07 -04:00
if not self.is_active:
return
2024-07-01 17:29:38 -04:00
with transaction.atomic():
2024-07-03 00:07:07 -04:00
accts = [str(a) for a in self.social_accounts.all()]
self.first_name = (";").join(accts)
self.last_name = self.username
2024-07-01 17:29:38 -04:00
self.is_active = False
# self.username = "~removed~" + str(self.pk)
# to get ready for federation, username has to be reserved
self.save()
self.identity.deleted = timezone.now()
self.identity.save()
2024-07-03 00:07:07 -04:00
self.social_accounts.all().delete()
def sync_identity(self):
2024-07-03 00:07:07 -04:00
"""sync display name, bio, and avatar from available sources"""
identity = self.identity.takahe_identity
if identity.deleted:
logger.error(f"Identity {identity} is deleted, skip sync")
return
2024-07-01 17:29:38 -04:00
mastodon = self.mastodon
2024-07-03 00:07:07 -04:00
threads = self.threads
2024-07-05 10:53:43 -04:00
bluesky = self.bluesky
2024-07-03 00:07:07 -04:00
changed = False
name = (
(mastodon.display_name if mastodon else "")
or (threads.username if threads else "")
2024-07-05 10:53:43 -04:00
or (bluesky.display_name if bluesky else "")
2024-07-03 00:07:07 -04:00
or identity.name
or identity.username
)
if identity.name != name:
identity.name = name
changed = True
summary = (
(mastodon.note if mastodon else "")
or (threads.threads_biography if threads else "")
2024-07-05 10:53:43 -04:00
or (bluesky.description if bluesky else "")
2024-07-03 00:07:07 -04:00
or identity.summary
)
if identity.summary != summary:
identity.summary = summary
changed = True
identity.manually_approves_followers = (
mastodon.locked if mastodon else identity.manually_approves_followers
)
# it's tedious to update avatar repeatedly, so only sync it once
if not identity.icon:
url = None
if mastodon and mastodon.avatar:
url = mastodon.avatar
elif threads and threads.threads_profile_picture_url:
url = threads.threads_profile_picture_url
2024-07-05 10:53:43 -04:00
elif bluesky and bluesky.avatar:
url = bluesky.avatar
2024-07-03 00:07:07 -04:00
if url:
2024-02-20 20:20:43 -05:00
try:
2024-07-03 00:07:07 -04:00
r = httpx.get(url)
2024-02-20 20:20:43 -05:00
except Exception as e:
2024-07-13 00:16:47 -04:00
logger.warning(
2024-07-05 18:15:10 -04:00
f"fetch icon failed: {identity} {url}",
2024-05-25 23:38:11 -04:00
extra={"exception": e},
)
2024-07-05 18:15:10 -04:00
r = None
if r:
name = str(self.pk) + "-" + url.split("/")[-1].split("?")[0][-100:]
f = ContentFile(r.content, name=name)
identity.icon.save(name, f, save=False)
changed = True
2024-07-03 00:07:07 -04:00
if changed:
identity.save()
Takahe.update_state(identity, "outdated")
2023-07-07 16:54:15 -04:00
2024-07-05 18:15:10 -04:00
def sync_accounts(self, skip_graph=False, sleep_hours=0):
"""Try refresh account data from 3p server"""
for account in self.social_accounts.all():
account.sync(skip_graph=skip_graph, sleep_hours=sleep_hours)
2024-02-10 23:39:49 -05:00
if not self.preference.mastodon_skip_userinfo:
self.sync_identity()
2024-07-05 18:15:10 -04:00
if skip_graph:
return
2024-02-10 23:39:49 -05:00
if not self.preference.mastodon_skip_relationship:
2024-07-05 19:05:50 -04:00
c = 0
for account in self.social_accounts.all():
c += account.sync_graph()
if c:
logger.debug(f"{self} graph updated with {c} new relationship.")
2024-07-05 18:15:10 -04:00
@staticmethod
def sync_accounts_task(user_id):
user = User.objects.get(pk=user_id)
logger.info(f"{user} accounts sync start")
2024-07-05 19:05:50 -04:00
user.sync_accounts()
logger.info(f"{user} accounts sync end")
2024-07-05 18:15:10 -04:00
def sync_accounts_later(self):
django_rq.get_queue("mastodon").enqueue(User.sync_accounts_task, self.pk)
2024-04-17 00:00:40 -04:00
@cached_property
2023-04-20 13:36:12 -04:00
def unread_announcements(self):
2024-04-17 00:00:40 -04:00
from takahe.utils import Takahe
return Takahe.get_announcements_for_user(self)
2023-04-20 13:36:12 -04:00
2023-07-20 21:59:49 -04:00
@property
def activity_manager(self):
if not self.identity:
raise ValueError("User has no identity")
return self.identity.activity_manager
@property
def shelf_manager(self):
if not self.identity:
raise ValueError("User has no identity")
return self.identity.shelf_manager
@property
def tag_manager(self):
if not self.identity:
raise ValueError("User has no identity")
return self.identity.tag_manager
2022-05-13 00:11:59 -04:00
@classmethod
2024-07-01 17:29:38 -04:00
def register(cls, **param) -> "User":
2023-07-20 21:59:49 -04:00
from .preference import Preference
2024-07-01 17:29:38 -04:00
account = param.pop("account", None)
pref = param.pop("preference", {})
2024-07-01 17:29:38 -04:00
with transaction.atomic():
new_user = cls(**param)
if not new_user.username:
raise ValueError("username is not set")
if "language" not in param:
new_user.language = translation.get_language()
2024-07-03 00:07:07 -04:00
new_user.set_unusable_password()
2024-07-01 17:29:38 -04:00
new_user.save()
pref["user"] = new_user
Preference.objects.create(**pref)
2024-07-01 17:29:38 -04:00
if account:
account.user = new_user
account.save()
Takahe.init_identity_for_local_user(new_user)
new_user.identity.shelf_manager
return new_user
2024-07-03 00:07:07 -04:00
def reconnect_account(self, account: SocialAccount):
with transaction.atomic():
SocialAccount.objects.filter(user=self, type=account.type).delete()
account.user = self
account.save()