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 | ||||
| ---------- | ||||
| * アカウントの削除を試験的に実装 | ||||
| * デッキでメディア投稿のみ表示するオプションが機能していない問題を修正 | ||||
| * デッキでユーザーを表示したときにタイムラインが残存する問題を修正 | ||||
| * モバイルのユーザーページで、ユーザーAのタイムラインから他のユーザーBを選択してユーザーBのタイムラインに移動したとき、ユーザーAのタイムラインが残る問題を修正 | ||||
|  |  | |||
|  | @ -589,6 +589,10 @@ common/views/components/profile-editor.vue: | |||
|     mute-list: "ミュート" | ||||
|     blocking-list: "ブロック" | ||||
|   export-requested: "エクスポートをリクエストしました。これには時間がかかる場合があります。エクスポートが終わると、ドライブにファイルが追加されます。" | ||||
|   enter-password: "パスワードを入力してください" | ||||
|   danger-zone: "危険な設定" | ||||
|   delete-account: "アカウントを削除" | ||||
|   account-deleted: "アカウントが削除されました。データが消えるまで時間がかかる場合があります。" | ||||
| 
 | ||||
| common/views/components/user-list-editor.vue: | ||||
|   users: "ユーザー" | ||||
|  |  | |||
|  | @ -101,6 +101,13 @@ | |||
| 			<ui-button @click="doExport()"><fa :icon="faDownload"/> {{ $t('export') }}</ui-button> | ||||
| 		</div> | ||||
| 	</section> | ||||
| 
 | ||||
| 	<section> | ||||
| 		<details> | ||||
| 			<summary>{{ $t('danger-zone') }}</summary> | ||||
| 			<ui-button @click="deleteAccount()">{{ $t('delete-account') }}</ui-button> | ||||
| 		</details> | ||||
| 	</section> | ||||
| </ui-card> | ||||
| </template> | ||||
| 
 | ||||
|  | @ -283,6 +290,25 @@ export default Vue.extend({ | |||
| 				type: 'info', | ||||
| 				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[]; | ||||
| 	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) { | ||||
| 	const data = { | ||||
| 		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 processInbox from './http/process-inbox'; | ||||
| import { deleteNotes } from './delete-notes'; | ||||
| import { deleteDriveFiles } from './delete-drive-files'; | ||||
| import { exportNotes } from './export-notes'; | ||||
| import { exportFollowing } from './export-following'; | ||||
| import { exportMute } from './export-mute'; | ||||
|  | @ -9,6 +11,8 @@ import { queueLogger } from '../logger'; | |||
| const handlers: any = { | ||||
| 	deliver, | ||||
| 	processInbox, | ||||
| 	deleteNotes, | ||||
| 	deleteDriveFiles, | ||||
| 	exportNotes, | ||||
| 	exportFollowing, | ||||
| 	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 note 投稿 | ||||
|  */ | ||||
| export default async function(user: IUser, note: INote) { | ||||
| export default async function(user: IUser, note: INote, quiet = false) { | ||||
| 	const deletedAt = new Date(); | ||||
| 
 | ||||
| 	await Note.update({ | ||||
|  | @ -52,10 +52,6 @@ export default async function(user: IUser, note: INote) { | |||
| 		}); | ||||
| 	} | ||||
| 
 | ||||
| 	publishNoteStream(note._id, 'deleted', { | ||||
| 		deletedAt: deletedAt | ||||
| 	}); | ||||
| 
 | ||||
| 	// この投稿が関わる未読通知を削除
 | ||||
| 	NoteUnread.find({ | ||||
| 		noteId: note._id | ||||
|  | @ -76,34 +72,40 @@ export default async function(user: IUser, note: INote) { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	//#region ローカルの投稿なら削除アクティビティを配送
 | ||||
| 	if (isLocalUser(user)) { | ||||
| 		const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user)); | ||||
| 
 | ||||
| 		const followings = await Following.find({ | ||||
| 			followeeId: user._id, | ||||
| 			'_follower.host': { $ne: null } | ||||
| 	if (!quiet) { | ||||
| 		publishNoteStream(note._id, 'deleted', { | ||||
| 			deletedAt: deletedAt | ||||
| 		}); | ||||
| 
 | ||||
| 		for (const following of followings) { | ||||
| 			deliver(user, content, following._follower.inbox); | ||||
| 		} | ||||
| 	} | ||||
| 	//#endregion
 | ||||
| 		//#region ローカルの投稿なら削除アクティビティを配送
 | ||||
| 		if (isLocalUser(user)) { | ||||
| 			const content = renderActivity(renderDelete(renderTombstone(`${config.url}/notes/${note._id}`), user)); | ||||
| 
 | ||||
| 	// 統計を更新
 | ||||
| 	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 | ||||
| 				} | ||||
| 			const followings = await Following.find({ | ||||
| 				followeeId: user._id, | ||||
| 				'_follower.host': { $ne: null } | ||||
| 			}); | ||||
| 
 | ||||
| 			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…
	
	Add table
		Add a link
		
	
		Reference in a new issue