Resolve #2017
This commit is contained in:
parent
7174a55846
commit
3409a51cca
10 changed files with 253 additions and 29 deletions
|
@ -3,6 +3,7 @@ ChangeLog
|
||||||
|
|
||||||
unreleased
|
unreleased
|
||||||
----------
|
----------
|
||||||
|
* アカウントの削除を試験的に実装
|
||||||
* デッキでメディア投稿のみ表示するオプションが機能していない問題を修正
|
* デッキでメディア投稿のみ表示するオプションが機能していない問題を修正
|
||||||
* デッキでユーザーを表示したときにタイムラインが残存する問題を修正
|
* デッキでユーザーを表示したときにタイムラインが残存する問題を修正
|
||||||
* モバイルのユーザーページで、ユーザーAのタイムラインから他のユーザーBを選択してユーザーBのタイムラインに移動したとき、ユーザーAのタイムラインが残る問題を修正
|
* モバイルのユーザーページで、ユーザーAのタイムラインから他のユーザーBを選択してユーザーBのタイムラインに移動したとき、ユーザーAのタイムラインが残る問題を修正
|
||||||
|
|
|
@ -589,6 +589,10 @@ common/views/components/profile-editor.vue:
|
||||||
mute-list: "ミュート"
|
mute-list: "ミュート"
|
||||||
blocking-list: "ブロック"
|
blocking-list: "ブロック"
|
||||||
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
|
export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。"
|
||||||
|
enter-password: "パスワードを入力してください"
|
||||||
|
danger-zone: "危険な設定"
|
||||||
|
delete-account: "アカウントを削除"
|
||||||
|
account-deleted: "アカウントが削除されました。データが消えるまで時間がかかる場合があります。"
|
||||||
|
|
||||||
common/views/components/user-list-editor.vue:
|
common/views/components/user-list-editor.vue:
|
||||||
users: "ユーザー"
|
users: "ユーザー"
|
||||||
|
|
|
@ -101,6 +101,13 @@
|
||||||
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
|
<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<details>
|
||||||
|
<summary>{{ $t('danger-zone') }}</summary>
|
||||||
|
<ui-button @click="deleteAccount()">{{ $t('delete-account') }}</ui-button>
|
||||||
|
</details>
|
||||||
|
</section>
|
||||||
</ui-card>
|
</ui-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -283,6 +290,25 @@ export default Vue.extend({
|
||||||
type: 'info',
|
type: 'info',
|
||||||
text: this.$t('export-requested')
|
text: this.$t('export-requested')
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteAccount() {
|
||||||
|
const { canceled: canceled, result: password } = await this.$root.dialog({
|
||||||
|
title: this.$t('enter-password'),
|
||||||
|
input: {
|
||||||
|
type: 'password'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (canceled) return;
|
||||||
|
|
||||||
|
this.$root.api('i/delete-account', {
|
||||||
|
password
|
||||||
|
}).then(() => {
|
||||||
|
this.$root.dialog({
|
||||||
|
type: 'success',
|
||||||
|
text: this.$t('account-deleted')
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -55,6 +55,8 @@ type IUserBase = {
|
||||||
emojis?: string[];
|
emojis?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
|
||||||
|
isDeleted: boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 凍結されているか否か
|
* 凍結されているか否か
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -70,6 +70,32 @@ export function processInbox(activity: any, signature: httpSignature.IParsedSign
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createDeleteNotesJob(user: ILocalUser) {
|
||||||
|
const data = {
|
||||||
|
type: 'deleteNotes',
|
||||||
|
user: user
|
||||||
|
};
|
||||||
|
|
||||||
|
if (queueAvailable && enableQueueProcessing) {
|
||||||
|
return queue.createJob(data).save();
|
||||||
|
} else {
|
||||||
|
return handler({ data }, () => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDeleteDriveFilesJob(user: ILocalUser) {
|
||||||
|
const data = {
|
||||||
|
type: 'deleteDriveFiles',
|
||||||
|
user: user
|
||||||
|
};
|
||||||
|
|
||||||
|
if (queueAvailable && enableQueueProcessing) {
|
||||||
|
return queue.createJob(data).save();
|
||||||
|
} else {
|
||||||
|
return handler({ data }, () => {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createExportNotesJob(user: ILocalUser) {
|
export function createExportNotesJob(user: ILocalUser) {
|
||||||
const data = {
|
const data = {
|
||||||
type: 'exportNotes',
|
type: 'exportNotes',
|
||||||
|
|
55
src/queue/processors/delete-drive-files.ts
Normal file
55
src/queue/processors/delete-drive-files.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import * as bq from 'bee-queue';
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
|
||||||
|
import { queueLogger } from '../logger';
|
||||||
|
import User from '../../models/user';
|
||||||
|
import DriveFile from '../../models/drive-file';
|
||||||
|
import deleteFile from '../../services/drive/delete-file';
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger('delete-drive-files');
|
||||||
|
|
||||||
|
export async function deleteDriveFiles(job: bq.Job, done: any): Promise<void> {
|
||||||
|
logger.info(`Deleting drive files of ${job.data.user._id} ...`);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: new mongo.ObjectID(job.data.user._id.toString())
|
||||||
|
});
|
||||||
|
|
||||||
|
let deletedCount = 0;
|
||||||
|
let ended = false;
|
||||||
|
let cursor: any = null;
|
||||||
|
|
||||||
|
while (!ended) {
|
||||||
|
const files = await DriveFile.find({
|
||||||
|
userId: user._id,
|
||||||
|
...(cursor ? { _id: { $gt: cursor } } : {})
|
||||||
|
}, {
|
||||||
|
limit: 100,
|
||||||
|
sort: {
|
||||||
|
_id: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (files.length === 0) {
|
||||||
|
ended = true;
|
||||||
|
if (job.reportProgress) job.reportProgress(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = files[files.length - 1]._id;
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
await deleteFile(file);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await DriveFile.count({
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.reportProgress) job.reportProgress(deletedCount / total);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ(`All drive files (${deletedCount}) of ${user._id} has been deleted.`);
|
||||||
|
done();
|
||||||
|
}
|
55
src/queue/processors/delete-notes.ts
Normal file
55
src/queue/processors/delete-notes.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import * as bq from 'bee-queue';
|
||||||
|
import * as mongo from 'mongodb';
|
||||||
|
|
||||||
|
import { queueLogger } from '../logger';
|
||||||
|
import Note from '../../models/note';
|
||||||
|
import deleteNote from '../../services/note/delete';
|
||||||
|
import User from '../../models/user';
|
||||||
|
|
||||||
|
const logger = queueLogger.createSubLogger('delete-notes');
|
||||||
|
|
||||||
|
export async function deleteNotes(job: bq.Job, done: any): Promise<void> {
|
||||||
|
logger.info(`Deleting notes of ${job.data.user._id} ...`);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: new mongo.ObjectID(job.data.user._id.toString())
|
||||||
|
});
|
||||||
|
|
||||||
|
let deletedCount = 0;
|
||||||
|
let ended = false;
|
||||||
|
let cursor: any = null;
|
||||||
|
|
||||||
|
while (!ended) {
|
||||||
|
const notes = await Note.find({
|
||||||
|
userId: user._id,
|
||||||
|
...(cursor ? { _id: { $gt: cursor } } : {})
|
||||||
|
}, {
|
||||||
|
limit: 100,
|
||||||
|
sort: {
|
||||||
|
_id: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (notes.length === 0) {
|
||||||
|
ended = true;
|
||||||
|
if (job.reportProgress) job.reportProgress(100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = notes[notes.length - 1]._id;
|
||||||
|
|
||||||
|
for (const note of notes) {
|
||||||
|
await deleteNote(user, note, true);
|
||||||
|
deletedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = await Note.count({
|
||||||
|
userId: user._id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (job.reportProgress) job.reportProgress(deletedCount / total);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.succ(`All notes (${deletedCount}) of ${user._id} has been deleted.`);
|
||||||
|
done();
|
||||||
|
}
|
|
@ -1,5 +1,7 @@
|
||||||
import deliver from './http/deliver';
|
import deliver from './http/deliver';
|
||||||
import processInbox from './http/process-inbox';
|
import processInbox from './http/process-inbox';
|
||||||
|
import { deleteNotes } from './delete-notes';
|
||||||
|
import { deleteDriveFiles } from './delete-drive-files';
|
||||||
import { exportNotes } from './export-notes';
|
import { exportNotes } from './export-notes';
|
||||||
import { exportFollowing } from './export-following';
|
import { exportFollowing } from './export-following';
|
||||||
import { exportMute } from './export-mute';
|
import { exportMute } from './export-mute';
|
||||||
|
@ -9,6 +11,8 @@ import { queueLogger } from '../logger';
|
||||||
const handlers: any = {
|
const handlers: any = {
|
||||||
deliver,
|
deliver,
|
||||||
processInbox,
|
processInbox,
|
||||||
|
deleteNotes,
|
||||||
|
deleteDriveFiles,
|
||||||
exportNotes,
|
exportNotes,
|
||||||
exportFollowing,
|
exportFollowing,
|
||||||
exportMute,
|
exportMute,
|
||||||
|
|
49
src/server/api/endpoints/i/delete-account.ts
Normal file
49
src/server/api/endpoints/i/delete-account.ts
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import $ from 'cafy';
|
||||||
|
import * as bcrypt from 'bcryptjs';
|
||||||
|
import User from '../../../../models/user';
|
||||||
|
import define from '../../define';
|
||||||
|
import { createDeleteNotesJob, createDeleteDriveFilesJob } from '../../../../queue';
|
||||||
|
|
||||||
|
export const meta = {
|
||||||
|
requireCredential: true,
|
||||||
|
|
||||||
|
secure: true,
|
||||||
|
|
||||||
|
params: {
|
||||||
|
password: {
|
||||||
|
validator: $.str
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default define(meta, (ps, user) => new Promise(async (res, rej) => {
|
||||||
|
// Compare password
|
||||||
|
const same = await bcrypt.compare(ps.password, user.password);
|
||||||
|
|
||||||
|
if (!same) {
|
||||||
|
return rej('incorrect password');
|
||||||
|
}
|
||||||
|
|
||||||
|
await User.update({ _id: user._id }, {
|
||||||
|
$set: {
|
||||||
|
isDeleted: true,
|
||||||
|
token: null,
|
||||||
|
name: null,
|
||||||
|
description: null,
|
||||||
|
pinnedNoteIds: [],
|
||||||
|
password: null,
|
||||||
|
email: null,
|
||||||
|
twitter: null,
|
||||||
|
github: null,
|
||||||
|
discord: null,
|
||||||
|
profile: {},
|
||||||
|
fields: [],
|
||||||
|
clientSettings: {},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createDeleteNotesJob(user);
|
||||||
|
createDeleteDriveFilesJob(user);
|
||||||
|
|
||||||
|
res();
|
||||||
|
}));
|
|
@ -21,7 +21,7 @@ import instanceChart from '../../services/chart/instance';
|
||||||
* @param user 投稿者
|
* @param user 投稿者
|
||||||
* @param note 投稿
|
* @param note 投稿
|
||||||
*/
|
*/
|
||||||
export default async function(user: IUser, note: INote) {
|
export default async function(user: IUser, note: INote, quiet = false) {
|
||||||
const deletedAt = new Date();
|
const deletedAt = new Date();
|
||||||
|
|
||||||
await Note.update({
|
await Note.update({
|
||||||
|
@ -52,10 +52,6 @@ export default async function(user: IUser, note: INote) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
publishNoteStream(note._id, 'deleted', {
|
|
||||||
deletedAt: deletedAt
|
|
||||||
});
|
|
||||||
|
|
||||||
// この投稿が関わる未読通知を削除
|
// この投稿が関わる未読通知を削除
|
||||||
NoteUnread.find({
|
NoteUnread.find({
|
||||||
noteId: note._id
|
noteId: note._id
|
||||||
|
@ -76,34 +72,40 @@ export default async function(user: IUser, note: INote) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region ローカルの投稿なら削除アクティビティを配送
|
if (!quiet) {
|
||||||
if (isLocalUser(user)) {
|
publishNoteStream(note._id, 'deleted', {
|
||||||
const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user));
|
deletedAt: deletedAt
|
||||||
|
|
||||||
const followings = await Following.find({
|
|
||||||
followeeId: user._id,
|
|
||||||
'_follower.host': { $ne: null }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const following of followings) {
|
//#region ローカルの投稿なら削除アクティビティを配送
|
||||||
deliver(user, content, following._follower.inbox);
|
if (isLocalUser(user)) {
|
||||||
}
|
const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user));
|
||||||
}
|
|
||||||
//#endregion
|
|
||||||
|
|
||||||
// 統計を更新
|
const followings = await Following.find({
|
||||||
notesChart.update(note, false);
|
followeeId: user._id,
|
||||||
perUserNotesChart.update(user, note, false);
|
'_follower.host': { $ne: null }
|
||||||
|
|
||||||
if (isRemoteUser(user)) {
|
|
||||||
registerOrFetchInstanceDoc(user.host).then(i => {
|
|
||||||
Instance.update({ _id: i._id }, {
|
|
||||||
$inc: {
|
|
||||||
notesCount: -1
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
instanceChart.updateNote(i.host, false);
|
for (const following of followings) {
|
||||||
});
|
deliver(user, content, following._follower.inbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
// 統計を更新
|
||||||
|
notesChart.update(note, false);
|
||||||
|
perUserNotesChart.update(user, note, false);
|
||||||
|
|
||||||
|
if (isRemoteUser(user)) {
|
||||||
|
registerOrFetchInstanceDoc(user.host).then(i => {
|
||||||
|
Instance.update({ _id: i._id }, {
|
||||||
|
$inc: {
|
||||||
|
notesCount: -1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
instanceChart.updateNote(i.host, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue