feat: truncate account
from bscone fork
This commit is contained in:
parent
219ddedae3
commit
13e4702c07
20 changed files with 454 additions and 25 deletions
|
@ -17,6 +17,7 @@ import { CaptchaService } from './CaptchaService.js';
|
|||
import { CreateSystemUserService } from './CreateSystemUserService.js';
|
||||
import { CustomEmojiService } from './CustomEmojiService.js';
|
||||
import { DeleteAccountService } from './DeleteAccountService.js';
|
||||
import { TruncateAccountService } from './TruncateAccountService.js';
|
||||
import { DownloadService } from './DownloadService.js';
|
||||
import { DriveService } from './DriveService.js';
|
||||
import { EmailService } from './EmailService.js';
|
||||
|
@ -155,6 +156,7 @@ const $CaptchaService: Provider = { provide: 'CaptchaService', useExisting: Capt
|
|||
const $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
|
||||
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
|
||||
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
|
||||
const $TruncateAccountService: Provider = { provide: 'TruncateAccountService', useExisting: TruncateAccountService };
|
||||
const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService };
|
||||
const $DriveService: Provider = { provide: 'DriveService', useExisting: DriveService };
|
||||
const $EmailService: Provider = { provide: 'EmailService', useExisting: EmailService };
|
||||
|
@ -297,6 +299,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
DeleteAccountService,
|
||||
TruncateAccountService,
|
||||
DownloadService,
|
||||
DriveService,
|
||||
EmailService,
|
||||
|
@ -433,6 +436,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
$DeleteAccountService,
|
||||
$TruncateAccountService,
|
||||
$DownloadService,
|
||||
$DriveService,
|
||||
$EmailService,
|
||||
|
@ -570,6 +574,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
CreateSystemUserService,
|
||||
CustomEmojiService,
|
||||
DeleteAccountService,
|
||||
TruncateAccountService,
|
||||
DownloadService,
|
||||
DriveService,
|
||||
EmailService,
|
||||
|
@ -705,6 +710,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
|
|||
$CreateSystemUserService,
|
||||
$CustomEmojiService,
|
||||
$DeleteAccountService,
|
||||
$TruncateAccountService,
|
||||
$DownloadService,
|
||||
$DriveService,
|
||||
$EmailService,
|
||||
|
|
|
@ -389,6 +389,16 @@ export class QueueService {
|
|||
);
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createTruncateAccountJob(user: ThinUser, opts = {}) {
|
||||
return this.dbQueue.add('truncateAccount', {
|
||||
user: { id: user.id },
|
||||
}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
|
||||
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));
|
||||
|
|
33
packages/backend/src/core/TruncateAccountService.ts
Normal file
33
packages/backend/src/core/TruncateAccountService.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository } from '@/models/_.js';
|
||||
import { QueueService } from '@/core/QueueService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
|
||||
@Injectable()
|
||||
export class TruncateAccountService {
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
private queueService: QueueService,
|
||||
) {
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async truncateAccount(user: {
|
||||
id: string;
|
||||
host: string | null;
|
||||
}): Promise<void> {
|
||||
const _user = await this.usersRepository.findOneByOrFail({ id: user.id });
|
||||
|
||||
this.queueService.createTruncateAccountJob(user, {
|
||||
soft: false,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -17,6 +17,7 @@ import { CleanChartsProcessorService } from './processors/CleanChartsProcessorSe
|
|||
import { CleanProcessorService } from './processors/CleanProcessorService.js';
|
||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
||||
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
|
||||
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
|
||||
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
||||
import { ReindexNotesProcessorService } from './processors/ReindexNotesProcessorService.js';
|
||||
|
@ -72,6 +73,7 @@ import { AutoNoteRemovalProcessorService } from './processors/AutoNoteRemovalPro
|
|||
ImportCustomEmojisProcessorService,
|
||||
ImportAntennasProcessorService,
|
||||
DeleteAccountProcessorService,
|
||||
TruncateAccountProcessorService,
|
||||
DeleteFileProcessorService,
|
||||
CleanRemoteFilesProcessorService,
|
||||
RelationshipProcessorService,
|
||||
|
|
|
@ -30,6 +30,7 @@ import { ImportUserListsProcessorService } from './processors/ImportUserListsPro
|
|||
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
|
||||
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
|
||||
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
|
||||
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
|
||||
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
|
||||
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
|
||||
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
|
||||
|
@ -110,6 +111,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
|
||||
private importAntennasProcessorService: ImportAntennasProcessorService,
|
||||
private deleteAccountProcessorService: DeleteAccountProcessorService,
|
||||
private truncateAccountProcessorService: TruncateAccountProcessorService,
|
||||
private deleteFileProcessorService: DeleteFileProcessorService,
|
||||
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
|
||||
private relationshipProcessorService: RelationshipProcessorService,
|
||||
|
@ -191,6 +193,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
|
|||
case 'importAntennas': return this.importAntennasProcessorService.process(job);
|
||||
case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
|
||||
case 'reportAbuse': return this.reportAbuseProcessorService.process(job);
|
||||
case 'truncateAccount': return this.truncateAccountProcessorService.process(job);
|
||||
default: throw new Error(`unrecognized job type ${job.name} for db`);
|
||||
}
|
||||
}, {
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { And, In, MoreThan, Not } from 'typeorm';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { DriveFilesRepository, NotesRepository, UserNotePiningsRepository, UsersRepository } from '@/models/_.js';
|
||||
import type Logger from '@/logger.js';
|
||||
import { DriveService } from '@/core/DriveService.js';
|
||||
import type { MiDriveFile } from '@/models/DriveFile.js';
|
||||
import type { MiNote } from '@/models/Note.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { NoteDeleteService } from '@/core/NoteDeleteService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
import type * as Bull from 'bullmq';
|
||||
import type { DbUserTruncateJobData } from '../types.js';
|
||||
|
||||
@Injectable()
|
||||
export class TruncateAccountProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.notesRepository)
|
||||
private notesRepository: NotesRepository,
|
||||
|
||||
@Inject(DI.userNotePiningsRepository)
|
||||
private userNotePiningsRepository: UserNotePiningsRepository,
|
||||
|
||||
@Inject(DI.driveFilesRepository)
|
||||
private driveFilesRepository: DriveFilesRepository,
|
||||
|
||||
private driveService: DriveService,
|
||||
private queueLoggerService: QueueLoggerService,
|
||||
private noteDeleteService: NoteDeleteService,
|
||||
) {
|
||||
this.logger = this.queueLoggerService.logger.createSubLogger('truncate-account');
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async process(job: Bull.Job<DbUserTruncateJobData>): Promise<string | void> {
|
||||
this.logger.info(`Truncate notes and drives account of ${job.data.user.id} ...`);
|
||||
|
||||
const user = await this.usersRepository.findOneBy({ id: job.data.user.id });
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pinings = await this.userNotePiningsRepository.findBy({ userId: user.id });
|
||||
const piningNoteIds = pinings.map(pining => pining.noteId); // pining.note always undefined (bug?)
|
||||
|
||||
const specifiedNotes = await this.notesRepository.findBy({
|
||||
userId: user.id,
|
||||
visibility: Not(In(['public', 'home', 'followers'])),
|
||||
});
|
||||
const specifiedNoteIds = specifiedNotes.map(note => note.id);
|
||||
|
||||
const keepFileIds = (await Promise.all([...piningNoteIds, ...specifiedNoteIds].map(async (noteId) => {
|
||||
const note = await this.notesRepository.findOneBy({ id: noteId });
|
||||
|
||||
return note?.fileIds;
|
||||
}))).flat().filter((fileId) => fileId !== undefined);
|
||||
|
||||
{ // Delete notes
|
||||
let cursor: MiNote['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const notes = await this.notesRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? {
|
||||
id: And(Not(In([...piningNoteIds, ...specifiedNoteIds])), MoreThan(cursor)),
|
||||
} : {
|
||||
id: Not(In([...piningNoteIds, ...specifiedNoteIds])),
|
||||
}),
|
||||
},
|
||||
take: 100,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
}) as MiNote[];
|
||||
|
||||
if (notes.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = notes.at(-1)?.id ?? null;
|
||||
|
||||
await Promise.all(notes.map((note) => {
|
||||
return this.noteDeleteService.delete(user, note, false, user);
|
||||
}));
|
||||
}
|
||||
|
||||
this.logger.succ('All of notes deleted');
|
||||
}
|
||||
|
||||
{ // Delete files
|
||||
let cursor: MiDriveFile['id'] | null = null;
|
||||
|
||||
while (true) {
|
||||
const files = await this.driveFilesRepository.find({
|
||||
where: {
|
||||
userId: user.id,
|
||||
...(cursor ? {
|
||||
id: And(Not(In(keepFileIds)), MoreThan(cursor)),
|
||||
} : {
|
||||
id: Not(In(keepFileIds)),
|
||||
}),
|
||||
},
|
||||
take: 10,
|
||||
order: {
|
||||
id: 1,
|
||||
},
|
||||
}) as MiDriveFile[];
|
||||
|
||||
if (files.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = files.at(-1)?.id ?? null;
|
||||
|
||||
for (const file of files) {
|
||||
await this.driveService.deleteFileSync(file);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.succ('All of files deleted');
|
||||
}
|
||||
|
||||
return 'Account notes and drives are truncated';
|
||||
}
|
||||
}
|
|
@ -60,6 +60,7 @@ export type DbJobMap = {
|
|||
importUserLists: DbUserImportJobData;
|
||||
importCustomEmojis: DbUserImportJobData;
|
||||
deleteAccount: DbUserDeleteJobData;
|
||||
truncateAccount: DbUserTruncateJobData;
|
||||
}
|
||||
|
||||
export type DbJobDataWithUser = {
|
||||
|
@ -83,6 +84,10 @@ export type DbUserDeleteJobData = {
|
|||
onlyFiles?: boolean;
|
||||
};
|
||||
|
||||
export type DbUserTruncateJobData = {
|
||||
user: ThinUser;
|
||||
};
|
||||
|
||||
export type DbUserImportJobData = {
|
||||
user: ThinUser;
|
||||
fileId: MiDriveFile['id'];
|
||||
|
|
|
@ -222,6 +222,7 @@ import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
|
|||
import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
|
||||
import * as ep___i_changePassword from './endpoints/i/change-password.js';
|
||||
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
|
||||
import * as ep___i_truncateAccount from './endpoints/i/truncate-account.js';
|
||||
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||
|
@ -616,6 +617,7 @@ const $i_authorizedApps: Provider = { provide: 'ep:i/authorized-apps', useClass:
|
|||
const $i_claimAchievement: Provider = { provide: 'ep:i/claim-achievement', useClass: ep___i_claimAchievement.default };
|
||||
const $i_changePassword: Provider = { provide: 'ep:i/change-password', useClass: ep___i_changePassword.default };
|
||||
const $i_deleteAccount: Provider = { provide: 'ep:i/delete-account', useClass: ep___i_deleteAccount.default };
|
||||
const $i_truncateAccount: Provider = { provide: 'ep:i/truncate-account', useClass: ep___i_truncateAccount.default };
|
||||
const $i_exportBlocking: Provider = { provide: 'ep:i/export-blocking', useClass: ep___i_exportBlocking.default };
|
||||
const $i_exportFollowing: Provider = { provide: 'ep:i/export-following', useClass: ep___i_exportFollowing.default };
|
||||
const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_exportMute.default };
|
||||
|
@ -1014,6 +1016,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$i_claimAchievement,
|
||||
$i_changePassword,
|
||||
$i_deleteAccount,
|
||||
$i_truncateAccount,
|
||||
$i_exportBlocking,
|
||||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
|
@ -1406,6 +1409,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
|
|||
$i_claimAchievement,
|
||||
$i_changePassword,
|
||||
$i_deleteAccount,
|
||||
$i_truncateAccount,
|
||||
$i_exportBlocking,
|
||||
$i_exportFollowing,
|
||||
$i_exportMute,
|
||||
|
|
|
@ -222,6 +222,7 @@ import * as ep___i_authorizedApps from './endpoints/i/authorized-apps.js';
|
|||
import * as ep___i_claimAchievement from './endpoints/i/claim-achievement.js';
|
||||
import * as ep___i_changePassword from './endpoints/i/change-password.js';
|
||||
import * as ep___i_deleteAccount from './endpoints/i/delete-account.js';
|
||||
import * as ep___i_truncateAccount from './endpoints/i/truncate-account.js';
|
||||
import * as ep___i_exportBlocking from './endpoints/i/export-blocking.js';
|
||||
import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
|
||||
import * as ep___i_exportMute from './endpoints/i/export-mute.js';
|
||||
|
@ -614,6 +615,7 @@ const eps = [
|
|||
['i/claim-achievement', ep___i_claimAchievement],
|
||||
['i/change-password', ep___i_changePassword],
|
||||
['i/delete-account', ep___i_deleteAccount],
|
||||
['i/truncate-account', ep___i_truncateAccount],
|
||||
['i/export-blocking', ep___i_exportBlocking],
|
||||
['i/export-following', ep___i_exportFollowing],
|
||||
['i/export-mute', ep___i_exportMute],
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import bcrypt from 'bcryptjs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import type { UsersRepository, UserProfilesRepository } from '@/models/_.js';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { TruncateAccountService } from '@/core/TruncateAccountService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { UserAuthService } from '@/core/UserAuthService.js';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
password: { type: 'string' },
|
||||
token: { type: 'string', nullable: true },
|
||||
},
|
||||
required: ['password'],
|
||||
} as const;
|
||||
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
|
||||
constructor(
|
||||
@Inject(DI.usersRepository)
|
||||
private usersRepository: UsersRepository,
|
||||
|
||||
@Inject(DI.userProfilesRepository)
|
||||
private userProfilesRepository: UserProfilesRepository,
|
||||
|
||||
private userAuthService: UserAuthService,
|
||||
private truncateAccountService: TruncateAccountService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
const token = ps.token;
|
||||
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: me.id });
|
||||
|
||||
if (profile.twoFactorEnabled) {
|
||||
if (token == null) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
|
||||
try {
|
||||
await this.userAuthService.twoFactorAuthenticate(profile, token);
|
||||
} catch (e) {
|
||||
throw new Error('authentication failed');
|
||||
}
|
||||
}
|
||||
|
||||
const userDetailed = await this.usersRepository.findOneByOrFail({ id: me.id });
|
||||
if (userDetailed.isDeleted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const passwordMatched = await bcrypt.compare(ps.password, profile.password!);
|
||||
if (!passwordMatched) {
|
||||
throw new Error('incorrect password');
|
||||
}
|
||||
|
||||
await this.truncateAccountService.truncateAccount(me);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue