From ab807ea1c96cae224ae40a7681a19ebd1f369b39 Mon Sep 17 00:00:00 2001 From: mein Name Date: Sat, 8 Feb 2025 19:57:42 -0500 Subject: [PATCH] improve identity deletion --- journal/models/__init__.py | 4 +-- journal/models/utils.py | 6 +++- neodb-takahe | 2 +- takahe/ap_handlers.py | 12 ++++++++ takahe/utils.py | 24 ++++++++------- users/management/commands/user.py | 37 +++++++++++++++++++++++ users/models/apidentity.py | 11 +++++++ users/models/user.py | 12 +++----- users/views/account.py | 49 +++++++++++++------------------ 9 files changed, 106 insertions(+), 51 deletions(-) diff --git a/journal/models/__init__.py b/journal/models/__init__.py index 1c55698d..1585565b 100644 --- a/journal/models/__init__.py +++ b/journal/models/__init__.py @@ -24,7 +24,7 @@ from .shelf import Shelf, ShelfLogEntry, ShelfManager, ShelfMember, ShelfType from .tag import Tag, TagManager, TagMember from .utils import ( journal_exists_for_item, - remove_data_by_user, + remove_data_by_identity, reset_journal_visibility_for_user, update_journal_for_merged_item, update_journal_for_merged_item_task, @@ -63,7 +63,7 @@ __all__ = [ "TagManager", "TagMember", "journal_exists_for_item", - "remove_data_by_user", + "remove_data_by_identity", "reset_journal_visibility_for_user", "update_journal_for_merged_item", "update_journal_for_merged_item_task", diff --git a/journal/models/utils.py b/journal/models/utils.py index 8ae35d0a..1b8a8324 100644 --- a/journal/models/utils.py +++ b/journal/models/utils.py @@ -4,6 +4,7 @@ from django.db.utils import IntegrityError from loguru import logger from catalog.models import Item +from journal.models.index import JournalIndex from users.models import APIdentity, User from .collection import Collection, CollectionMember, FeaturedCollection @@ -24,7 +25,7 @@ def reset_journal_visibility_for_user(owner: APIdentity, visibility: int): Review.objects.filter(owner=owner).update(visibility=visibility) -def remove_data_by_user(owner: APIdentity): +def remove_data_by_identity(owner: APIdentity): ShelfMember.objects.filter(owner=owner).delete() ShelfLogEntry.objects.filter(owner=owner).delete() Comment.objects.filter(owner=owner).delete() @@ -36,6 +37,9 @@ def remove_data_by_user(owner: APIdentity): CollectionMember.objects.filter(owner=owner).delete() Collection.objects.filter(owner=owner).delete() FeaturedCollection.objects.filter(owner=owner).delete() + index = JournalIndex.instance() + index.delete_by_owner(owner.pk) + logger.info(f"removed journal data by {owner}") def update_journal_for_merged_item_task(editing_user_id: int, legacy_item_uuid: str): diff --git a/neodb-takahe b/neodb-takahe index 549f58eb..40dc9470 160000 --- a/neodb-takahe +++ b/neodb-takahe @@ -1 +1 @@ -Subproject commit 549f58eb472504f8281d20a7000ab8cc6834f611 +Subproject commit 40dc947009e60e429bfe78a92917209f92106112 diff --git a/takahe/ap_handlers.py b/takahe/ap_handlers.py index acea2597..531b0850 100644 --- a/takahe/ap_handlers.py +++ b/takahe/ap_handlers.py @@ -222,6 +222,18 @@ def post_uninteracted(interaction_pk, interaction, post_pk, identity_pk): ).delete() +def identity_deleted(pk): + apid = APIdentity.objects.filter(pk=pk).first() + if not apid: + logger.warning(f"APIdentity {apid} not found") + return + + logger.warning(f"handle deleting identity {apid}") + if apid.user and apid.user.is_active: + apid.user.clear() # for local identity, clear their user as well + apid.clear() + + def identity_fetched(pk): try: identity = Identity.objects.get(pk=pk) diff --git a/takahe/utils.py b/takahe/utils.py index b366806b..38bc1122 100644 --- a/takahe/utils.py +++ b/takahe/utils.py @@ -125,16 +125,20 @@ class Takahe: ).first() @staticmethod - def delete_identity(identity_pk: int): - identity = Identity.objects.filter(pk=identity_pk).first() - if not identity: - logger.warning(f"Cannot find identity {identity_pk}") - return - logger.warning(f"Deleting identity {identity}") - identity.state = "deleted" - identity.deleted = timezone.now() - identity.state_next_attempt = timezone.now() - identity.save() + def request_delete_identity(identity_pk: int): + from journal.models import remove_data_by_identity + from users.models import APIdentity + + i = Identity.objects.filter(pk=identity_pk).first() + if i: + InboxMessage.create_internal( + {"type": "DeleteIdentity", "actor": i.actor_uri} + ) + logger.warning(f"Requested identity {i} deletion") + else: + logger.error(f"Identity {i} not found, force delete APIdentity") + apid = APIdentity.objects.get(pk=identity_pk) + remove_data_by_identity(apid) @staticmethod def create_internal_message(message: dict): diff --git a/users/management/commands/user.py b/users/management/commands/user.py index 29379c81..4d3033b8 100644 --- a/users/management/commands/user.py +++ b/users/management/commands/user.py @@ -1,3 +1,5 @@ +from time import sleep + import httpx from django.core.management.base import BaseCommand from tqdm import tqdm @@ -5,6 +7,7 @@ from tqdm import tqdm from takahe.models import Domain, Identity from takahe.utils import Takahe from users.models import Preference, User +from users.models.apidentity import APIdentity class Command(BaseCommand): @@ -48,6 +51,9 @@ class Command(BaseCommand): self.staff(options["staff"]) if options["active"]: self.set_active(options["active"]) + if options["delete"]: + if input("Are you sure to delete? [Y/N] ").startswith("Y"): + self.delete(options["delete"]) def list(self, users): for user in users: @@ -164,3 +170,34 @@ class Command(BaseCommand): u.is_active = not u.is_active u.save() self.stdout.write(f"update {u} is_active: {u.is_active}") + + def delete(self, v): + for n in v: + try: + apid = APIdentity.get_by_handle(n) + if apid.deleted: + self.stdout.write(f"{apid} already deleted, try anyway") + apid.clear() + if apid.user: + apid.user.clear() + Takahe.request_delete_identity(apid.pk) + count_down = 10 + while count_down > 0: + i = Identity.objects.filter(pk=apid.pk).first() + if i and i.state != "deleted_fanned_out": + self.stdout.write(f"waiting for takahe-stator...{count_down}") + sleep(1) + else: + break + count_down -= 1 + if count_down == 0: + self.stdout.write( + self.style.WARNING( + f"Identity {apid} was deleted, but some data in takahe has not been fully processed yet, make sure takahe-stator is running and wait a bit." + ) + ) + else: + self.stdout.write(f"Deleted identity {apid}") + except APIdentity.DoesNotExist: + self.stdout.write(f"identity {n} not found") + continue diff --git a/users/models/apidentity.py b/users/models/apidentity.py index ea660b15..b7300b97 100644 --- a/users/models/apidentity.py +++ b/users/models/apidentity.py @@ -2,6 +2,8 @@ from functools import cached_property from django.conf import settings from django.db import models +from django.utils import timezone +from loguru import logger from mastodon.models.mastodon import MastodonAccount from takahe.utils import Takahe @@ -288,3 +290,12 @@ class APIdentity(models.Model): from journal.models import TagManager return TagManager(self) + + def clear(self): + """delete data for this identity""" + from journal.models import remove_data_by_identity + + remove_data_by_identity(self) + self.deleted = timezone.now() + self.save() + logger.warning(f"Identity {self} cleared.") diff --git a/users/models/user.py b/users/models/user.py index 34d38503..1bbc37ab 100644 --- a/users/models/user.py +++ b/users/models/user.py @@ -12,7 +12,7 @@ from django.core.files.base import ContentFile from django.db import models, transaction from django.db.models.functions import Lower from django.urls import reverse -from django.utils import timezone, translation +from django.utils import translation from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ from loguru import logger @@ -199,19 +199,15 @@ class User(AbstractUser): return p.edited_time if p else None def clear(self): - if not self.is_active: - return with transaction.atomic(): accts = [str(a) for a in self.social_accounts.all()] - self.first_name = (";").join(accts) + if accts: + self.first_name = (";").join(accts) self.last_name = self.username 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() self.social_accounts.all().delete() + logger.warning(f"User {self} cleared.") def sync_identity(self): """sync display name, bio, and avatar from available sources""" diff --git a/users/views/account.py b/users/views/account.py index 2587ff94..ffc4cd4d 100644 --- a/users/views/account.py +++ b/users/views/account.py @@ -1,6 +1,5 @@ from urllib.parse import quote -import django_rq from django import forms from django.conf import settings from django.contrib import auth, messages @@ -11,11 +10,8 @@ from django.shortcuts import redirect, render from django.urls import reverse from django.utils.translation import gettext as _ from django.views.decorators.http import require_http_methods -from loguru import logger from common.utils import AuthedHttpRequest -from journal.models import remove_data_by_user -from journal.models.index import JournalIndex from mastodon.models import Email, Mastodon from mastodon.models.common import Platform, SocialAccount from mastodon.models.email import EmailAccount @@ -222,33 +218,28 @@ def auth_logout(request): return logout_takahe(redirect("/")) -def clear_data_task(user_id): - user = User.objects.get(pk=user_id) - user_str = str(user) - if user.identity: - remove_data_by_user(user.identity) - Takahe.delete_identity(user.identity.pk) - user.clear() - index = JournalIndex.instance() - index.delete_by_owner(user.identity.pk) - logger.warning(f"User {user_str} data cleared.") - - +@require_http_methods(["POST"]) @login_required def clear_data(request): + # for deletion initiated by local identity in neodb: + # 1. clear user data + # 2. neodb send DeleteIdentity to Takahe + # 3. takahe delete identity and send identity_deleted to neodb + # 4. identity_deleted clear user (if not yet) and identity data + # 5. log web user out + # for deletion initiated by remote/local identity in takahe: + # just 3 & 4 if request.META.get("HTTP_AUTHORIZATION"): raise BadRequest("Only for web login") - if request.method == "POST": - v = request.POST.get("verification", "").strip() - if v: - for acct in request.user.social_accounts.all(): - if acct.handle == v: - django_rq.get_queue("mastodon").enqueue( - clear_data_task, request.user.id - ) - messages.add_message( - request, messages.INFO, _("Account is being deleted.") - ) - return auth_logout(request) - messages.add_message(request, messages.ERROR, _("Account mismatch.")) + v = request.POST.get("verification", "").strip() + if v: + for acct in request.user.social_accounts.all(): + if acct.handle == v: + request.user.clear() + Takahe.request_delete_identity(request.user.identity.pk) + messages.add_message( + request, messages.INFO, _("Account is being deleted.") + ) + return auth_logout(request) + messages.add_message(request, messages.ERROR, _("Account mismatch.")) return redirect(reverse("users:data"))