feat: improve follow export
This commit is contained in:
		
							parent
							
								
									46c0280764
								
							
						
					
					
						commit
						20134a5367
					
				
					 7 changed files with 156 additions and 84 deletions
				
			
		|  | @ -11,6 +11,8 @@ | |||
| 
 | ||||
| ### Improvements | ||||
| - Added a user-level instance mute in user settings | ||||
| - フォローエクスポートでミュートしているユーザーを含めないオプションを追加 | ||||
| - フォローエクスポートで使われていないアカウントを含めないオプションを追加 | ||||
| 
 | ||||
| ### Bugfixes | ||||
| - クライアント: タッチ機能付きディスプレイを使っていてマウス操作をしている場合に一部機能が動作しない問題を修正 | ||||
|  |  | |||
|  | @ -1318,6 +1318,8 @@ _exportOrImport: | |||
|   muteList: "ミュート" | ||||
|   blockingList: "ブロック" | ||||
|   userLists: "リスト" | ||||
|   excludeMutingUsers: "ミュートしているユーザーを除外" | ||||
|   excludeInactiveUsers: "使われていないアカウントを除外" | ||||
| 
 | ||||
| _charts: | ||||
|   federationInstancesIncDec: "連合の増減" | ||||
|  |  | |||
|  | @ -126,9 +126,11 @@ export function createExportNotesJob(user: ThinUser) { | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export function createExportFollowingJob(user: ThinUser) { | ||||
| export function createExportFollowingJob(user: ThinUser, excludeMuting = false, excludeInactive = false) { | ||||
| 	return dbQueue.add('exportFollowing', { | ||||
| 		user: user, | ||||
| 		excludeMuting, | ||||
| 		excludeInactive, | ||||
| 	}, { | ||||
| 		removeOnComplete: true, | ||||
| 		removeOnFail: true, | ||||
|  |  | |||
|  | @ -6,13 +6,14 @@ import { queueLogger } from '../../logger'; | |||
| import addFile from '@/services/drive/add-file'; | ||||
| import * as dateFormat from 'dateformat'; | ||||
| import { getFullApAccount } from '@/misc/convert-host'; | ||||
| import { Users, Followings } from '@/models/index'; | ||||
| import { MoreThan } from 'typeorm'; | ||||
| import { Users, Followings, Mutings } from '@/models/index'; | ||||
| import { In, MoreThan, Not } from 'typeorm'; | ||||
| import { DbUserJobData } from '@/queue/types'; | ||||
| import { Following } from '@/models/entities/following'; | ||||
| 
 | ||||
| const logger = queueLogger.createSubLogger('export-following'); | ||||
| 
 | ||||
| export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): Promise<void> { | ||||
| export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: () => void): Promise<void> { | ||||
| 	logger.info(`Exporting following of ${job.data.user.id} ...`); | ||||
| 
 | ||||
| 	const user = await Users.findOne(job.data.user.id); | ||||
|  | @ -22,7 +23,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): | |||
| 	} | ||||
| 
 | ||||
| 	// Create temp file
 | ||||
| 	const [path, cleanup] = await new Promise<[string, any]>((res, rej) => { | ||||
| 	const [path, cleanup] = await new Promise<[string, () => void]>((res, rej) => { | ||||
| 		tmp.file((e, path, fd, cleanup) => { | ||||
| 			if (e) return rej(e); | ||||
| 			res([path, cleanup]); | ||||
|  | @ -33,13 +34,17 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): | |||
| 
 | ||||
| 	const stream = fs.createWriteStream(path, { flags: 'a' }); | ||||
| 
 | ||||
| 	let exportedCount = 0; | ||||
| 	let cursor: any = null; | ||||
| 	let cursor: Following['id'] | null = null; | ||||
| 
 | ||||
| 	const mutings = job.data.excludeMuting ? await Mutings.find({ | ||||
| 		muterId: user.id, | ||||
| 	}) : []; | ||||
| 
 | ||||
| 	while (true) { | ||||
| 		const followings = await Followings.find({ | ||||
| 			where: { | ||||
| 				followerId: user.id, | ||||
| 				...(mutings.length > 0 ? { followeeId: Not(In(mutings.map(x => x.muteeId))) } : {}), | ||||
| 				...(cursor ? { id: MoreThan(cursor) } : {}), | ||||
| 			}, | ||||
| 			take: 100, | ||||
|  | @ -49,7 +54,6 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): | |||
| 		}); | ||||
| 
 | ||||
| 		if (followings.length === 0) { | ||||
| 			job.progress(100); | ||||
| 			break; | ||||
| 		} | ||||
| 
 | ||||
|  | @ -58,7 +62,11 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): | |||
| 		for (const following of followings) { | ||||
| 			const u = await Users.findOne({ id: following.followeeId }); | ||||
| 			if (u == null) { | ||||
| 				exportedCount++; continue; | ||||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			if (job.data.excludeInactive && u.updatedAt && (Date.now() - u.updatedAt.getTime() > 1000 * 60 * 60 * 24 * 90)) { | ||||
| 				continue; | ||||
| 			} | ||||
| 
 | ||||
| 			const content = getFullApAccount(u.username, u.host); | ||||
|  | @ -72,14 +80,7 @@ export async function exportFollowing(job: Bull.Job<DbUserJobData>, done: any): | |||
| 					} | ||||
| 				}); | ||||
| 			}); | ||||
| 			exportedCount++; | ||||
| 		} | ||||
| 
 | ||||
| 		const total = await Followings.count({ | ||||
| 			followerId: user.id, | ||||
| 		}); | ||||
| 
 | ||||
| 		job.progress(exportedCount / total); | ||||
| 	} | ||||
| 
 | ||||
| 	stream.end(); | ||||
|  |  | |||
|  | @ -21,6 +21,8 @@ export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobDat | |||
| 
 | ||||
| export type DbUserJobData = { | ||||
| 	user: ThinUser; | ||||
| 	excludeMuting: boolean; | ||||
| 	excludeInactive: boolean; | ||||
| }; | ||||
| 
 | ||||
| export type DbUserDeleteJobData = { | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| import $ from 'cafy'; | ||||
| import define from '../../define'; | ||||
| import { createExportFollowingJob } from '@/queue/index'; | ||||
| import ms from 'ms'; | ||||
|  | @ -9,8 +10,18 @@ export const meta = { | |||
| 		duration: ms('1hour'), | ||||
| 		max: 1, | ||||
| 	}, | ||||
| 	params: { | ||||
| 		excludeMuting: { | ||||
| 			validator: $.optional.bool, | ||||
| 			default: false, | ||||
| 		}, | ||||
| 		excludeInactive: { | ||||
| 			validator: $.optional.bool, | ||||
| 			default: false, | ||||
| 		}, | ||||
| 	}, | ||||
| }; | ||||
| 
 | ||||
| export default define(meta, async (ps, user) => { | ||||
| 	createExportFollowingJob(user); | ||||
| 	createExportFollowingJob(user, ps.excludeMuting, ps.excludeInactive); | ||||
| }); | ||||
|  |  | |||
|  | @ -2,106 +2,158 @@ | |||
| <div class="_formRoot"> | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts._exportOrImport.allNotes }}</template> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('notes')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="exportNotes()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 	</FormSection> | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts._exportOrImport.followingList }}</template> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('following')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="doImport('following', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 		<FormGroup> | ||||
| 			<FormSwitch v-model="excludeMutingUsers" class="_formBlock"> | ||||
| 				{{ $ts._exportOrImport.excludeMutingUsers }} | ||||
| 			</FormSwitch> | ||||
| 			<FormSwitch v-model="excludeInactiveUsers" class="_formBlock"> | ||||
| 				{{ $ts._exportOrImport.excludeInactiveUsers }} | ||||
| 			</FormSwitch> | ||||
| 			<MkButton :class="$style.button" inline @click="exportFollowing()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		</FormGroup> | ||||
| 		<FormGroup> | ||||
| 			<MkButton :class="$style.button" inline @click="importFollowing($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 		</FormGroup> | ||||
| 	</FormSection> | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts._exportOrImport.userLists }}</template> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('user-lists')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="doImport('user-lists', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="exportUserLists()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="importUserLists($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 	</FormSection> | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts._exportOrImport.muteList }}</template> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('muting')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="doImport('muting', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="exportMuting()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="importMuting($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 	</FormSection> | ||||
| 	<FormSection> | ||||
| 		<template #label>{{ $ts._exportOrImport.blockingList }}</template> | ||||
| 		<MkButton :class="$style.button" inline @click="doExport('blocking')"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="doImport('blocking', $event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="exportBlocking()"><i class="fas fa-download"></i> {{ $ts.export }}</MkButton> | ||||
| 		<MkButton :class="$style.button" inline @click="importBlocking($event)"><i class="fas fa-upload"></i> {{ $ts.import }}</MkButton> | ||||
| 	</FormSection> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent } from 'vue'; | ||||
| import { defineComponent, onMounted, ref } from 'vue'; | ||||
| import MkButton from '@/components/ui/button.vue'; | ||||
| import FormSection from '@/components/form/section.vue'; | ||||
| import FormGroup from '@/components/form/group.vue'; | ||||
| import FormSwitch from '@/components/form/switch.vue'; | ||||
| import * as os from '@/os'; | ||||
| import { selectFile } from '@/scripts/select-file'; | ||||
| import * as symbols from '@/symbols'; | ||||
| import { i18n } from '@/i18n'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		FormSection, | ||||
| 		FormGroup, | ||||
| 		FormSwitch, | ||||
| 		MkButton, | ||||
| 	}, | ||||
| 
 | ||||
| 	emits: ['info'], | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: { | ||||
| 				title: this.$ts.importAndExport, | ||||
| 	setup(props, context) { | ||||
| 		const INFO = { | ||||
| 			title: i18n.locale.importAndExport, | ||||
| 			icon: 'fas fa-boxes', | ||||
| 			bg: 'var(--bg)', | ||||
| 			}, | ||||
| 		} | ||||
| 	}, | ||||
| 		}; | ||||
| 
 | ||||
| 	mounted() { | ||||
| 		this.$emit('info', this[symbols.PAGE_INFO]); | ||||
| 	}, | ||||
| 		const excludeMutingUsers = ref(false); | ||||
| 		const excludeInactiveUsers = ref(false); | ||||
| 
 | ||||
| 	methods: { | ||||
| 		doExport(target) { | ||||
| 			os.api( | ||||
| 				target === 'notes' ? 'i/export-notes' : | ||||
| 				target === 'following' ? 'i/export-following' : | ||||
| 				target === 'blocking' ? 'i/export-blocking' : | ||||
| 				target === 'user-lists' ? 'i/export-user-lists' : | ||||
| 				target === 'muting' ? 'i/export-mute' : | ||||
| 				null, {}) | ||||
| 			.then(() => { | ||||
| 		const onExportSuccess = () => { | ||||
| 			os.alert({ | ||||
| 				type: 'info', | ||||
| 					text: this.$ts.exportRequested | ||||
| 				text: i18n.locale.exportRequested, | ||||
| 			}); | ||||
| 			}).catch((e: any) => { | ||||
| 				os.alert({ | ||||
| 					type: 'error', | ||||
| 					text: e.message | ||||
| 				}); | ||||
| 			}); | ||||
| 		}, | ||||
| 		}; | ||||
| 
 | ||||
| 		async doImport(target, e) { | ||||
| 			const file = await selectFile(e.currentTarget || e.target); | ||||
| 			 | ||||
| 			os.api( | ||||
| 				target === 'following' ? 'i/import-following' : | ||||
| 				target === 'user-lists' ? 'i/import-user-lists' : | ||||
| 				target === 'muting' ? 'i/import-muting' : | ||||
| 				target === 'blocking' ? 'i/import-blocking' : | ||||
| 				null, { | ||||
| 					fileId: file.id | ||||
| 			}).then(() => { | ||||
| 		const onImportSuccess = () => { | ||||
| 			os.alert({ | ||||
| 				type: 'info', | ||||
| 					text: this.$ts.importRequested | ||||
| 				text: i18n.locale.importRequested, | ||||
| 			}); | ||||
| 			}).catch((e: any) => { | ||||
| 		}; | ||||
| 
 | ||||
| 		const onError = (e) => { | ||||
| 			os.alert({ | ||||
| 				type: 'error', | ||||
| 					text: e.message | ||||
| 				text: e.message, | ||||
| 			}); | ||||
| 		}; | ||||
| 
 | ||||
| 		const exportNotes = () => { | ||||
| 			os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const exportFollowing = () => { | ||||
| 			os.api('i/export-following', { | ||||
| 				excludeMuting: excludeMutingUsers.value, | ||||
| 				excludeInactive: excludeInactiveUsers.value, | ||||
| 			}) | ||||
| 			.then(onExportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const exportBlocking = () => { | ||||
| 			os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const exportUserLists = () => { | ||||
| 			os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const exportMuting = () => { | ||||
| 			os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const importFollowing = async (ev) => { | ||||
| 			const file = await selectFile(ev.currentTarget || ev.target); | ||||
| 			os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const importUserLists = async (ev) => { | ||||
| 			const file = await selectFile(ev.currentTarget || ev.target); | ||||
| 			os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const importMuting = async (ev) => { | ||||
| 			const file = await selectFile(ev.currentTarget || ev.target); | ||||
| 			os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		const importBlocking = async (ev) => { | ||||
| 			const file = await selectFile(ev.currentTarget || ev.target); | ||||
| 			os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); | ||||
| 		}; | ||||
| 
 | ||||
| 		onMounted(() => { | ||||
| 			context.emit('info', INFO); | ||||
| 		}); | ||||
| 
 | ||||
| 		return { | ||||
| 			[symbols.PAGE_INFO]: INFO, | ||||
| 			excludeMutingUsers, | ||||
| 			excludeInactiveUsers, | ||||
| 
 | ||||
| 			exportNotes, | ||||
| 			exportFollowing, | ||||
| 			exportBlocking, | ||||
| 			exportUserLists, | ||||
| 			exportMuting, | ||||
| 
 | ||||
| 			importFollowing, | ||||
| 			importUserLists, | ||||
| 			importMuting, | ||||
| 			importBlocking, | ||||
| 		}; | ||||
| 	}, | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue