feat: truncate account

from bscone fork
This commit is contained in:
ZerglingGo 2023-12-02 18:15:40 +09:00 committed by 무라쿠모
parent 219ddedae3
commit 13e4702c07
Signed by: SWREI
GPG Key ID: 139D6573F92DA9F7
20 changed files with 454 additions and 25 deletions

30
locales/index.d.ts vendored
View File

@ -3664,6 +3664,10 @@ export interface Locale extends ILocale {
* *
*/ */
"deleteAccountConfirm": string; "deleteAccountConfirm": string;
/**
*
*/
"truncateAccountConfirm": string;
/** /**
* *
*/ */
@ -3848,6 +3852,10 @@ export interface Locale extends ILocale {
* *
*/ */
"deleteAccount": string; "deleteAccount": string;
/**
*
*/
"truncateAccount": string;
/** /**
* *
*/ */
@ -7292,6 +7300,28 @@ export interface Locale extends ILocale {
*/ */
"youCantUseThisTime": string; "youCantUseThisTime": string;
}; };
"_accountTruncate": {
/**
*
*/
"accountDelete": string;
/**
*
*/
"mayTakeTime": string;
/**
*
*/
"requestAccountTruncate": string;
/**
*
*/
"started": string;
/**
*
*/
"inProgress": string;
};
"_ad": { "_ad": {
/** /**
* *

View File

@ -912,6 +912,7 @@ followingVisibility: "フォローの公開範囲"
followersVisibility: "フォロワーの公開範囲" followersVisibility: "フォロワーの公開範囲"
continueThread: "さらにスレッドを見る" continueThread: "さらにスレッドを見る"
deleteAccountConfirm: "アカウントが削除されます。よろしいですか?" deleteAccountConfirm: "アカウントが削除されます。よろしいですか?"
truncateAccountConfirm: "ダイレクトとピン留めされたノート、関連ドライブのファイルを除くすべてのノートとドライブのファイルが削除されます。続行しますか?"
incorrectPassword: "パスワードが間違っています。" incorrectPassword: "パスワードが間違っています。"
voteConfirm: "「{choice}」に投票しますか?" voteConfirm: "「{choice}」に投票しますか?"
hide: "隠す" hide: "隠す"
@ -958,6 +959,7 @@ requireAdminForView: "閲覧するには管理者アカウントでログイン
isSystemAccount: "システムにより自動で作成・管理されているアカウントです。" isSystemAccount: "システムにより自動で作成・管理されているアカウントです。"
typeToConfirm: "この操作を行うには {x} と入力してください" typeToConfirm: "この操作を行うには {x} と入力してください"
deleteAccount: "アカウント削除" deleteAccount: "アカウント削除"
truncateAccount: "アカウント整理"
document: "ドキュメント" document: "ドキュメント"
numberOfPageCache: "ページキャッシュ数" numberOfPageCache: "ページキャッシュ数"
numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。" numberOfPageCacheDescription: "多くすると利便性が向上しますが、負荷とメモリ使用量が増えます。"
@ -1889,6 +1891,13 @@ _accountDelete:
inProgress: "削除が進行中" inProgress: "削除が進行中"
youCantUseThisTime: "現在、アカウントの削除はできません。" youCantUseThisTime: "現在、アカウントの削除はできません。"
_accountTruncate:
accountDelete: "アカウントの整理"
mayTakeTime: "アカウントの整理は負荷のかかる処理であるため、作成したコンテンツの数やアップロードしたファイルの数が多いと完了までに時間がかかることがあります。"
requestAccountTruncate: "アカウント整理をリクエスト"
started: "整理処理が開始されました。"
inProgress: "整理が進行中"
_ad: _ad:
back: "戻る" back: "戻る"
reduceFrequencyOfThisAd: "この広告の表示頻度を下げる" reduceFrequencyOfThisAd: "この広告の表示頻度を下げる"

View File

@ -909,6 +909,7 @@ followingVisibility: "팔로우 중인 유저를 볼 수 있는 사람"
followersVisibility: "내 팔로워를 볼 수 있는 사람" followersVisibility: "내 팔로워를 볼 수 있는 사람"
continueThread: "글타래 더 보기" continueThread: "글타래 더 보기"
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? " deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
truncateAccountConfirm: "다이렉트 및 프로필 상단에 고정된 노트를 제외한 모든 노트와 파일을 제거하고 복구할 수 없습니다. 그래도 계속하시겠습니까?"
incorrectPassword: "비밀번호가 올바르지 않습니다." incorrectPassword: "비밀번호가 올바르지 않습니다."
voteConfirm: "\"{choice}\"에 투표하시겠습니까?" voteConfirm: "\"{choice}\"에 투표하시겠습니까?"
hide: "숨기기" hide: "숨기기"
@ -948,13 +949,14 @@ noEmailServerWarning: "메일 서버가 설정되어 있지 않습니다."
thereIsUnresolvedAbuseReportWarning: "해결되지 않은 신고가 있습니다." thereIsUnresolvedAbuseReportWarning: "해결되지 않은 신고가 있습니다."
thereIsUnresolvedAbuseReport: "아직 해결되지 않은 신고가 {left}건 있습니다. 제어판에서 확인해주세요." thereIsUnresolvedAbuseReport: "아직 해결되지 않은 신고가 {left}건 있습니다. 제어판에서 확인해주세요."
recommended: "추천" recommended: "추천"
check: "체크" check: "확인"
driveCapOverrideLabel: "이 유저의 드라이브 용량을 변경" driveCapOverrideLabel: "이 유저의 드라이브 용량을 변경"
driveCapOverrideCaption: "0 이하를 지정하면 해제됩니다." driveCapOverrideCaption: "0 이하를 지정하면 해제됩니다."
requireAdminForView: "열람하려면 관리자 계정으로 로그인해야 합니다." requireAdminForView: "열람하려면 관리자 계정으로 로그인해야 합니다."
isSystemAccount: "시스템에 의해 자동으로 생성되어 관리되는 계정입니다." isSystemAccount: "시스템에 의해 자동으로 생성되어 관리되는 계정입니다."
typeToConfirm: "계속하시려면 {x} 을 입력하세요" typeToConfirm: "계속하시려면 {x} 을 입력하세요"
deleteAccount: "계정 삭제" deleteAccount: "계정 삭제"
truncateAccount: "계정 청소"
document: "문서" document: "문서"
numberOfPageCache: "페이지 캐시 수" numberOfPageCache: "페이지 캐시 수"
numberOfPageCacheDescription: "숫자가 클 수록 편리성이 높아지지만, 시스템 자원과 메모리를 더 많이 사용합니다." numberOfPageCacheDescription: "숫자가 클 수록 편리성이 높아지지만, 시스템 자원과 메모리를 더 많이 사용합니다."
@ -1857,6 +1859,12 @@ _accountDelete:
started: "삭제 작업이 시작되었습니다." started: "삭제 작업이 시작되었습니다."
inProgress: "삭제 진행 중" inProgress: "삭제 진행 중"
youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다." youCantUseThisTime: "지금은 계정 삭제를 진행할 수 없습니다."
_accountTruncate:
accountTruncate: "계정 청소"
mayTakeTime: "계정 청소는 서버에 부하를 가하기 때문에, 작성한 콘텐츠나 업로드한 파일의 수가 많으면 완료까지 시간이 걸릴 수 있습니다."
requestAccountTruncate: "계정 청소 요청"
started: "청소 작업이 시작되었습니다."
inProgress: "청소 진행 중"
_ad: _ad:
back: "뒤로" back: "뒤로"
reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기" reduceFrequencyOfThisAd: "이 광고의 표시 빈도 낮추기"

View File

@ -17,6 +17,7 @@ import { CaptchaService } from './CaptchaService.js';
import { CreateSystemUserService } from './CreateSystemUserService.js'; import { CreateSystemUserService } from './CreateSystemUserService.js';
import { CustomEmojiService } from './CustomEmojiService.js'; import { CustomEmojiService } from './CustomEmojiService.js';
import { DeleteAccountService } from './DeleteAccountService.js'; import { DeleteAccountService } from './DeleteAccountService.js';
import { TruncateAccountService } from './TruncateAccountService.js';
import { DownloadService } from './DownloadService.js'; import { DownloadService } from './DownloadService.js';
import { DriveService } from './DriveService.js'; import { DriveService } from './DriveService.js';
import { EmailService } from './EmailService.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 $CreateSystemUserService: Provider = { provide: 'CreateSystemUserService', useExisting: CreateSystemUserService };
const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService }; const $CustomEmojiService: Provider = { provide: 'CustomEmojiService', useExisting: CustomEmojiService };
const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService }; const $DeleteAccountService: Provider = { provide: 'DeleteAccountService', useExisting: DeleteAccountService };
const $TruncateAccountService: Provider = { provide: 'TruncateAccountService', useExisting: TruncateAccountService };
const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService }; const $DownloadService: Provider = { provide: 'DownloadService', useExisting: DownloadService };
const $DriveService: Provider = { provide: 'DriveService', useExisting: DriveService }; const $DriveService: Provider = { provide: 'DriveService', useExisting: DriveService };
const $EmailService: Provider = { provide: 'EmailService', useExisting: EmailService }; const $EmailService: Provider = { provide: 'EmailService', useExisting: EmailService };
@ -297,6 +299,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
TruncateAccountService,
DownloadService, DownloadService,
DriveService, DriveService,
EmailService, EmailService,
@ -433,6 +436,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
$TruncateAccountService,
$DownloadService, $DownloadService,
$DriveService, $DriveService,
$EmailService, $EmailService,
@ -570,6 +574,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
CreateSystemUserService, CreateSystemUserService,
CustomEmojiService, CustomEmojiService,
DeleteAccountService, DeleteAccountService,
TruncateAccountService,
DownloadService, DownloadService,
DriveService, DriveService,
EmailService, EmailService,
@ -705,6 +710,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$CreateSystemUserService, $CreateSystemUserService,
$CustomEmojiService, $CustomEmojiService,
$DeleteAccountService, $DeleteAccountService,
$TruncateAccountService,
$DownloadService, $DownloadService,
$DriveService, $DriveService,
$EmailService, $EmailService,

View File

@ -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 @bindThis
public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) { public createFollowJob(followings: { from: ThinUser, to: ThinUser, requestId?: string, silent?: boolean, withReplies?: boolean }[]) {
const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel)); const jobs = followings.map(rel => this.generateRelationshipJobData('follow', rel));

View 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,
});
}
}

View File

@ -17,6 +17,7 @@ import { CleanChartsProcessorService } from './processors/CleanChartsProcessorSe
import { CleanProcessorService } from './processors/CleanProcessorService.js'; import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js'; import { DeleteDriveFilesProcessorService } from './processors/DeleteDriveFilesProcessorService.js';
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
import { ReindexNotesProcessorService } from './processors/ReindexNotesProcessorService.js'; import { ReindexNotesProcessorService } from './processors/ReindexNotesProcessorService.js';
@ -72,6 +73,7 @@ import { AutoNoteRemovalProcessorService } from './processors/AutoNoteRemovalPro
ImportCustomEmojisProcessorService, ImportCustomEmojisProcessorService,
ImportAntennasProcessorService, ImportAntennasProcessorService,
DeleteAccountProcessorService, DeleteAccountProcessorService,
TruncateAccountProcessorService,
DeleteFileProcessorService, DeleteFileProcessorService,
CleanRemoteFilesProcessorService, CleanRemoteFilesProcessorService,
RelationshipProcessorService, RelationshipProcessorService,

View File

@ -30,6 +30,7 @@ import { ImportUserListsProcessorService } from './processors/ImportUserListsPro
import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js';
import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js'; import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js';
import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js';
import { TruncateAccountProcessorService } from './processors/TruncateAccountProcessorService.js';
import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js'; import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js'; import { DeleteFileProcessorService } from './processors/DeleteFileProcessorService.js';
@ -110,6 +111,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService, private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService,
private importAntennasProcessorService: ImportAntennasProcessorService, private importAntennasProcessorService: ImportAntennasProcessorService,
private deleteAccountProcessorService: DeleteAccountProcessorService, private deleteAccountProcessorService: DeleteAccountProcessorService,
private truncateAccountProcessorService: TruncateAccountProcessorService,
private deleteFileProcessorService: DeleteFileProcessorService, private deleteFileProcessorService: DeleteFileProcessorService,
private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService, private cleanRemoteFilesProcessorService: CleanRemoteFilesProcessorService,
private relationshipProcessorService: RelationshipProcessorService, private relationshipProcessorService: RelationshipProcessorService,
@ -191,6 +193,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'importAntennas': return this.importAntennasProcessorService.process(job); case 'importAntennas': return this.importAntennasProcessorService.process(job);
case 'deleteAccount': return this.deleteAccountProcessorService.process(job); case 'deleteAccount': return this.deleteAccountProcessorService.process(job);
case 'reportAbuse': return this.reportAbuseProcessorService.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`); default: throw new Error(`unrecognized job type ${job.name} for db`);
} }
}, { }, {

View File

@ -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';
}
}

View File

@ -60,6 +60,7 @@ export type DbJobMap = {
importUserLists: DbUserImportJobData; importUserLists: DbUserImportJobData;
importCustomEmojis: DbUserImportJobData; importCustomEmojis: DbUserImportJobData;
deleteAccount: DbUserDeleteJobData; deleteAccount: DbUserDeleteJobData;
truncateAccount: DbUserTruncateJobData;
} }
export type DbJobDataWithUser = { export type DbJobDataWithUser = {
@ -83,6 +84,10 @@ export type DbUserDeleteJobData = {
onlyFiles?: boolean; onlyFiles?: boolean;
}; };
export type DbUserTruncateJobData = {
user: ThinUser;
};
export type DbUserImportJobData = { export type DbUserImportJobData = {
user: ThinUser; user: ThinUser;
fileId: MiDriveFile['id']; fileId: MiDriveFile['id'];

View File

@ -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_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.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_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_exportBlocking from './endpoints/i/export-blocking.js';
import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
import * as ep___i_exportMute from './endpoints/i/export-mute.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_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_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_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_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_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 }; 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_claimAchievement,
$i_changePassword, $i_changePassword,
$i_deleteAccount, $i_deleteAccount,
$i_truncateAccount,
$i_exportBlocking, $i_exportBlocking,
$i_exportFollowing, $i_exportFollowing,
$i_exportMute, $i_exportMute,
@ -1406,6 +1409,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_claimAchievement, $i_claimAchievement,
$i_changePassword, $i_changePassword,
$i_deleteAccount, $i_deleteAccount,
$i_truncateAccount,
$i_exportBlocking, $i_exportBlocking,
$i_exportFollowing, $i_exportFollowing,
$i_exportMute, $i_exportMute,

View File

@ -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_claimAchievement from './endpoints/i/claim-achievement.js';
import * as ep___i_changePassword from './endpoints/i/change-password.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_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_exportBlocking from './endpoints/i/export-blocking.js';
import * as ep___i_exportFollowing from './endpoints/i/export-following.js'; import * as ep___i_exportFollowing from './endpoints/i/export-following.js';
import * as ep___i_exportMute from './endpoints/i/export-mute.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/claim-achievement', ep___i_claimAchievement],
['i/change-password', ep___i_changePassword], ['i/change-password', ep___i_changePassword],
['i/delete-account', ep___i_deleteAccount], ['i/delete-account', ep___i_deleteAccount],
['i/truncate-account', ep___i_truncateAccount],
['i/export-blocking', ep___i_exportBlocking], ['i/export-blocking', ep___i_exportBlocking],
['i/export-following', ep___i_exportFollowing], ['i/export-following', ep___i_exportFollowing],
['i/export-mute', ep___i_exportMute], ['i/export-mute', ep___i_exportMute],

View File

@ -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);
});
}
}

View File

@ -67,13 +67,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.closeAccount }}</template> <template #label>{{ i18n.ts.closeAccount }}</template>
<div v-if="$i.policies.canUseAccountRemoval" class="_gaps_m"> <div v-if="$i.policies.canUseAccountRemoval" class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo> <MkInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</MkInfo>
<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo> <MkInfo>{{ i18n.ts._accountDelete.sendEmail }}</MkInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton> <MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton> <MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div> </div>
<div v-else class="_gaps_m"> <div v-else class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.youCantUseThisTime }}</FormInfo> <MkInfo warn>{{ i18n.ts._accountDelete.youCantUseThisTime }}</MkInfo>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-recycle"></i></template>
<template #label>{{ i18n.ts.truncateAccount }}</template>
<div class="_gaps_m">
<MkInfo warn>{{ i18n.ts._accountTruncate.mayTakeTime }}</MkInfo>
<MkButton v-if="!$i.isDeleted" danger @click="truncateAccount">{{ i18n.ts._accountTruncate.requestAccountTruncate }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountTruncate.inProgress }}</MkButton>
</div> </div>
</MkFolder> </MkFolder>
@ -105,11 +116,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, ref } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import FormLink from '@/components/form/link.vue'; import FormLink from '@/components/form/link.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import FormInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import FormSection from '@/components/form/section.vue';
import MkInput from '@/components/MkInput.vue';
import MkKeyValue from '@/components/MkKeyValue.vue'; import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -118,15 +131,24 @@ import { defaultStore } from '@/store.js';
import { signout, signinRequired } from '@/account.js'; import { signout, signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js'; import { definePageMetadata } from '@/scripts/page-metadata.js';
import { unisonReload } from '@/scripts/unison-reload.js';
import FormSection from '@/components/form/section.vue';
import MkInput from "@/components/MkInput.vue";
const $i = signinRequired(); const $i = signinRequired();
// const reportError = computed(defaultStore.makeGetterSetter('reportError')); // const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const devMode = computed(defaultStore.makeGetterSetter('devMode')); const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies')); const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
const noSpecifiedNotes = ref<boolean>($i.autoRemovalCondition.noSpecifiedNotes);
function saveRemovalCondition() {
misskeyApi('i/update-removal-condition', {
active: autoRemoval.value,
deleteAfter: deleteAfter.value,
noPiningNotes: noPiningNotes.value,
noSpecifiedNotes: noSpecifiedNotes.value,
});
}
async function deleteAccount() { async function deleteAccount() {
{ {
@ -152,6 +174,28 @@ async function deleteAccount() {
await signout(); await signout();
} }
async function truncateAccount() {
{
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.truncateAccountConfirm,
});
if (canceled) return;
}
const auth = await os.authenticateDialog();
if (auth.canceled) return;
await os.apiWithDialog('i/truncate-account', {
password: auth.result.password,
token: auth.result.token,
});
await os.alert({
title: i18n.ts._accountTruncate.started,
});
}
async function updateRepliesAll(withReplies: boolean) { async function updateRepliesAll(withReplies: boolean) {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',

View File

@ -76,8 +76,6 @@ import MkSwitch from '@/components/MkSwitch.vue';
import MkSelect from '@/components/MkSelect.vue'; import MkSelect from '@/components/MkSelect.vue';
import FormSection from '@/components/form/section.vue'; import FormSection from '@/components/form/section.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkInput from '@/components/MkInput.vue';
import MkButton from '@/components/MkButton.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -95,10 +93,6 @@ const hideOnlineStatus = ref($i.hideOnlineStatus);
const publicReactions = ref($i.publicReactions); const publicReactions = ref($i.publicReactions);
const followingVisibility = ref($i.followingVisibility); const followingVisibility = ref($i.followingVisibility);
const followersVisibility = ref($i.followersVisibility); const followersVisibility = ref($i.followersVisibility);
const autoRemoval = ref<boolean>($i.autoRemovalCondition.active);
const deleteAfter = ref<number>($i.autoRemovalCondition.deleteAfter || 7);
const noPiningNotes = ref<boolean>($i.autoRemovalCondition.noPiningNotes);
const noSpecifiedNotes = ref<boolean>($i.autoRemovalCondition.noSpecifiedNotes);
const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility')); const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly')); const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
@ -119,15 +113,6 @@ function save() {
}); });
} }
function saveRemovalCondition() {
misskeyApi('i/update-removal-condition', {
active: autoRemoval.value,
deleteAfter: deleteAfter.value,
noPiningNotes: noPiningNotes.value,
noSpecifiedNotes: noSpecifiedNotes.value,
});
}
const headerActions = computed(() => []); const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);

View File

@ -1526,6 +1526,7 @@ declare namespace entities {
IClaimAchievementRequest, IClaimAchievementRequest,
IChangePasswordRequest, IChangePasswordRequest,
IDeleteAccountRequest, IDeleteAccountRequest,
ITruncateAccountRequest,
IExportFollowingRequest, IExportFollowingRequest,
IFavoritesRequest, IFavoritesRequest,
IFavoritesResponse, IFavoritesResponse,
@ -2314,6 +2315,9 @@ type ISigninHistoryRequest = operations['i___signin-history']['requestBody']['co
// @public (undocumented) // @public (undocumented)
type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json']; type ISigninHistoryResponse = operations['i___signin-history']['responses']['200']['content']['application/json'];
// @public (undocumented)
type ITruncateAccountRequest = operations['i___truncate-account']['requestBody']['content']['application/json'];
// @public (undocumented) // @public (undocumented)
type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json']; type IUnpinRequest = operations['i___unpin']['requestBody']['content']['application/json'];

View File

@ -2397,6 +2397,18 @@ declare module '../api.js' {
credential?: string | null, credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>; ): Promise<SwitchCaseResponseType<E, P>>;
/**
* No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
request<E extends 'i/truncate-account', P extends Endpoints[E]['req']>(
endpoint: E,
params: P,
credential?: string | null,
): Promise<SwitchCaseResponseType<E, P>>;
/** /**
* No description provided. * No description provided.
* *

View File

@ -325,6 +325,7 @@ import type {
IClaimAchievementRequest, IClaimAchievementRequest,
IChangePasswordRequest, IChangePasswordRequest,
IDeleteAccountRequest, IDeleteAccountRequest,
ITruncateAccountRequest,
IExportFollowingRequest, IExportFollowingRequest,
IFavoritesRequest, IFavoritesRequest,
IFavoritesResponse, IFavoritesResponse,
@ -802,6 +803,7 @@ export type Endpoints = {
'i/claim-achievement': { req: IClaimAchievementRequest; res: EmptyResponse }; 'i/claim-achievement': { req: IClaimAchievementRequest; res: EmptyResponse };
'i/change-password': { req: IChangePasswordRequest; res: EmptyResponse }; 'i/change-password': { req: IChangePasswordRequest; res: EmptyResponse };
'i/delete-account': { req: IDeleteAccountRequest; res: EmptyResponse }; 'i/delete-account': { req: IDeleteAccountRequest; res: EmptyResponse };
'i/truncate-account': { req: ITruncateAccountRequest; res: EmptyResponse };
'i/export-blocking': { req: EmptyRequest; res: EmptyResponse }; 'i/export-blocking': { req: EmptyRequest; res: EmptyResponse };
'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse }; 'i/export-following': { req: IExportFollowingRequest; res: EmptyResponse };
'i/export-mute': { req: EmptyRequest; res: EmptyResponse }; 'i/export-mute': { req: EmptyRequest; res: EmptyResponse };

View File

@ -328,6 +328,7 @@ export type IAuthorizedAppsResponse = operations['i___authorized-apps']['respons
export type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json']; export type IClaimAchievementRequest = operations['i___claim-achievement']['requestBody']['content']['application/json'];
export type IChangePasswordRequest = operations['i___change-password']['requestBody']['content']['application/json']; export type IChangePasswordRequest = operations['i___change-password']['requestBody']['content']['application/json'];
export type IDeleteAccountRequest = operations['i___delete-account']['requestBody']['content']['application/json']; export type IDeleteAccountRequest = operations['i___delete-account']['requestBody']['content']['application/json'];
export type ITruncateAccountRequest = operations['i___truncate-account']['requestBody']['content']['application/json'];
export type IExportFollowingRequest = operations['i___export-following']['requestBody']['content']['application/json']; export type IExportFollowingRequest = operations['i___export-following']['requestBody']['content']['application/json'];
export type IFavoritesRequest = operations['i___favorites']['requestBody']['content']['application/json']; export type IFavoritesRequest = operations['i___favorites']['requestBody']['content']['application/json'];
export type IFavoritesResponse = operations['i___favorites']['responses']['200']['content']['application/json']; export type IFavoritesResponse = operations['i___favorites']['responses']['200']['content']['application/json'];

View File

@ -2086,6 +2086,16 @@ export type paths = {
*/ */
post: operations['i___delete-account']; post: operations['i___delete-account'];
}; };
'/i/truncate-account': {
/**
* i/truncate-account
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
post: operations['i___truncate-account'];
};
'/i/export-blocking': { '/i/export-blocking': {
/** /**
* i/export-blocking * i/export-blocking
@ -18224,6 +18234,59 @@ export type operations = {
}; };
}; };
}; };
/**
* i/truncate-account
* @description No description provided.
*
* **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
* **Credential required**: *Yes*
*/
'i___truncate-account': {
requestBody: {
content: {
'application/json': {
password: string;
token?: string | null;
};
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
};
/** @description Client error */
400: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Authentication error */
401: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Forbidden error */
403: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description I'm Ai */
418: {
content: {
'application/json': components['schemas']['Error'];
};
};
/** @description Internal server error */
500: {
content: {
'application/json': components['schemas']['Error'];
};
};
};
};
/** /**
* i/export-blocking * i/export-blocking
* @description No description provided. * @description No description provided.