diff options
| author | Julia <julia@insertdomain.name> | 2025-05-29 00:07:38 +0000 |
|---|---|---|
| committer | Julia <julia@insertdomain.name> | 2025-05-29 00:07:38 +0000 |
| commit | 6b554c178b81f13f83a69b19d44b72b282a0c119 (patch) | |
| tree | f5537f1a56323a4dd57ba150b3cb84a2d8b5dc63 /packages/backend/src/queue/processors/DeleteAccountProcessorService.ts | |
| parent | merge: Security fixes (!970) (diff) | |
| parent | bump version for release (diff) | |
| download | sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.gz sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.tar.bz2 sharkey-6b554c178b81f13f83a69b19d44b72b282a0c119.zip | |
merge: release 2025.4.2 (!1051)
View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/1051
Approved-by: Hazelnoot <acomputerdog@gmail.com>
Approved-by: Marie <github@yuugi.dev>
Approved-by: Julia <julia@insertdomain.name>
Diffstat (limited to 'packages/backend/src/queue/processors/DeleteAccountProcessorService.ts')
| -rw-r--r-- | packages/backend/src/queue/processors/DeleteAccountProcessorService.ts | 284 |
1 files changed, 238 insertions, 46 deletions
diff --git a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts index 0c70829132..4e9779a41b 100644 --- a/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts +++ b/packages/backend/src/queue/processors/DeleteAccountProcessorService.ts @@ -4,9 +4,9 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import { MoreThan } from 'typeorm'; +import { In, MoreThan } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, NoteReactionsRepository, NotesRepository, UserProfilesRepository, UsersRepository, NoteScheduleRepository, MiNoteSchedule } from '@/models/_.js'; +import type { DriveFilesRepository, NoteReactionsRepository, NotesRepository, UserProfilesRepository, UsersRepository, NoteScheduleRepository, MiNoteSchedule, FollowingsRepository, FollowRequestsRepository, BlockingsRepository, MutingsRepository, ClipsRepository, ClipNotesRepository, LatestNotesRepository, NoteEditRepository, NoteFavoritesRepository, PollVotesRepository, PollsRepository, SigninsRepository, UserIpsRepository, RegistryItemsRepository } from '@/models/_.js'; import type Logger from '@/logger.js'; import { DriveService } from '@/core/DriveService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; @@ -17,10 +17,10 @@ import { bindThis } from '@/decorators.js'; import { SearchService } from '@/core/SearchService.js'; import { ApLogService } from '@/core/ApLogService.js'; import { ReactionService } from '@/core/ReactionService.js'; +import { QueueService } from '@/core/QueueService.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type * as Bull from 'bullmq'; import type { DbUserDeleteJobData } from '../types.js'; -import { QueueService } from '@/core/QueueService.js'; @Injectable() export class DeleteAccountProcessorService { @@ -45,6 +45,48 @@ export class DeleteAccountProcessorService { @Inject(DI.noteScheduleRepository) private noteScheduleRepository: NoteScheduleRepository, + @Inject(DI.followingsRepository) + private readonly followingsRepository: FollowingsRepository, + + @Inject(DI.followRequestsRepository) + private readonly followRequestsRepository: FollowRequestsRepository, + + @Inject(DI.blockingsRepository) + private readonly blockingsRepository: BlockingsRepository, + + @Inject(DI.mutingsRepository) + private readonly mutingsRepository: MutingsRepository, + + @Inject(DI.clipsRepository) + private readonly clipsRepository: ClipsRepository, + + @Inject(DI.clipNotesRepository) + private readonly clipNotesRepository: ClipNotesRepository, + + @Inject(DI.latestNotesRepository) + private readonly latestNotesRepository: LatestNotesRepository, + + @Inject(DI.noteEditRepository) + private readonly noteEditRepository: NoteEditRepository, + + @Inject(DI.noteFavoritesRepository) + private readonly noteFavoritesRepository: NoteFavoritesRepository, + + @Inject(DI.pollVotesRepository) + private readonly pollVotesRepository: PollVotesRepository, + + @Inject(DI.pollsRepository) + private readonly pollsRepository: PollsRepository, + + @Inject(DI.signinsRepository) + private readonly signinsRepository: SigninsRepository, + + @Inject(DI.userIpsRepository) + private readonly userIpsRepository: UserIpsRepository, + + @Inject(DI.registryItemsRepository) + private readonly registryItemsRepository: RegistryItemsRepository, + private queueService: QueueService, private driveService: DriveService, private emailService: EmailService, @@ -65,6 +107,140 @@ export class DeleteAccountProcessorService { return; } + { // Delete user clips + const userClips = await this.clipsRepository.find({ + select: { + id: true, + }, + where: { + userId: user.id, + }, + }) as { id: string }[]; + + // Delete one-at-a-time because there can be a lot + for (const clip of userClips) { + await this.clipNotesRepository.delete({ + id: clip.id, + }); + } + + await this.clipsRepository.delete({ + userId: user.id, + }); + + this.logger.succ('All clips have been deleted.'); + } + + { // Delete favorites + await this.noteFavoritesRepository.delete({ + userId: user.id, + }); + + this.logger.succ('All favorites have been deleted.'); + } + + { // Delete user relations + await this.followingsRepository.delete({ + followerId: user.id, + }); + + await this.followingsRepository.delete({ + followeeId: user.id, + }); + + await this.followRequestsRepository.delete({ + followerId: user.id, + }); + + await this.followRequestsRepository.delete({ + followeeId: user.id, + }); + + await this.blockingsRepository.delete({ + blockerId: user.id, + }); + + await this.blockingsRepository.delete({ + blockeeId: user.id, + }); + + await this.mutingsRepository.delete({ + muterId: user.id, + }); + + await this.mutingsRepository.delete({ + muteeId: user.id, + }); + + this.logger.succ('All user relations have been deleted.'); + } + + { // Delete reactions + let cursor: MiNoteReaction['id'] | null = null; + + while (true) { + const reactions = await this.noteReactionsRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + take: 100, + order: { + id: 1, + }, + relations: { + note: true, + }, + }) as MiNoteReaction[]; + + if (reactions.length === 0) { + break; + } + + cursor = reactions.at(-1)?.id ?? null; + + for (const reaction of reactions) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const note = reaction.note!; + await this.reactionService.delete(user, note, reaction); + } + } + + this.logger.succ('All reactions have been deleted'); + } + + { // Poll votes + let cursor: MiNoteReaction['id'] | null = null; + + while (true) { + const votes = await this.pollVotesRepository.find({ + where: { + userId: user.id, + ...(cursor ? { id: MoreThan(cursor) } : {}), + }, + select: { + id: true, + }, + take: 100, + order: { + id: 1, + }, + }) as { id: string }[]; + + if (votes.length === 0) { + break; + } + + cursor = votes.at(-1)?.id ?? null; + + await this.pollVotesRepository.delete({ + id: In(votes.map(v => v.id)), + }); + } + + this.logger.succ('All poll votes have been deleted'); + } + { // Delete scheduled notes const scheduledNotes = await this.noteScheduleRepository.findBy({ userId: user.id, @@ -82,6 +258,10 @@ export class DeleteAccountProcessorService { } { // Delete notes + await this.latestNotesRepository.delete({ + userId: user.id, + }); + let cursor: MiNote['id'] | null = null; while (true) { @@ -102,7 +282,23 @@ export class DeleteAccountProcessorService { cursor = notes.at(-1)?.id ?? null; - await this.notesRepository.delete(notes.map(note => note.id)); + // Delete associated polls one-at-a-time, since it can cascade to a LOT of vote entries + for (const note of notes) { + if (note.hasPoll) { + await this.pollsRepository.delete({ + noteId: note.id, + }); + } + } + + const ids = notes.map(note => note.id); + + await this.noteEditRepository.delete({ + noteId: In(ids), + }); + await this.notesRepository.delete({ + id: In(ids), + }); for (const note of notes) { await this.searchService.unindexNote(note); @@ -119,37 +315,6 @@ export class DeleteAccountProcessorService { this.logger.succ('All of notes deleted'); } - { // Delete reactions - let cursor: MiNoteReaction['id'] | null = null; - - while (true) { - const reactions = await this.noteReactionsRepository.find({ - where: { - userId: user.id, - ...(cursor ? { id: MoreThan(cursor) } : {}), - }, - take: 100, - order: { - id: 1, - }, - }) as MiNoteReaction[]; - - if (reactions.length === 0) { - break; - } - - cursor = reactions.at(-1)?.id ?? null; - - for (const reaction of reactions) { - const note = await this.notesRepository.findOneBy({ id: reaction.noteId }) as MiNote; - - await this.reactionService.delete(user, note); - } - } - - this.logger.succ('All reactions have been deleted'); - } - { // Delete files let cursor: MiDriveFile['id'] | null = null; @@ -184,22 +349,49 @@ export class DeleteAccountProcessorService { await this.apLogService.deleteObjectLogs(user.uri) .catch(err => this.logger.error(err, `Failed to delete AP logs for user '${user.uri}'`)); } + + await this.apLogService.deleteInboxLogs(user.id) + .catch(err => this.logger.error(err, `Failed to delete AP logs for user '${user.uri}'`)); + + this.logger.succ('All AP logs deleted'); } - { // Send email notification - const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.email && profile.emailVerified) { - this.emailService.sendEmail(profile.email, 'Account deleted', - 'Your account has been deleted.', - 'Your account has been deleted.'); + // Do this BEFORE deleting the account! + const profile = await this.userProfilesRepository.findOneBy({ userId: user.id }); + + { // Delete the actual account + await this.userIpsRepository.delete({ + userId: user.id, + }); + + await this.signinsRepository.delete({ + userId: user.id, + }); + + await this.registryItemsRepository.delete({ + userId: user.id, + }); + + // soft指定されている場合は物理削除しない + if (job.data.soft) { + // nop + } else { + await this.usersRepository.delete(user.id); } + + this.logger.succ('Account data deleted'); } - // soft指定されている場合は物理削除しない - if (job.data.soft) { - // nop - } else { - await this.usersRepository.delete(job.data.user.id); + { // Send email notification + if (profile && profile.email && profile.emailVerified) { + try { + await this.emailService.sendEmail(profile.email, 'Account deleted', + 'Your account has been deleted.', + 'Your account has been deleted.'); + } catch (e) { + this.logger.warn('Failed to send account deletion message:', { e }); + } + } } return 'Account deleted'; |