feat: improve follow export

This commit is contained in:
syuilo 2021-12-10 01:22:35 +09:00
parent 94c717375b
commit faacf3ae68
7 changed files with 156 additions and 84 deletions

View File

@ -11,6 +11,8 @@
### Improvements
- Added a user-level instance mute in user settings
- フォローエクスポートでミュートしているユーザーを含めないオプションを追加
- フォローエクスポートで使われていないアカウントを含めないオプションを追加
### Bugfixes
- クライアント: タッチ機能付きディスプレイを使っていてマウス操作をしている場合に一部機能が動作しない問題を修正

View File

@ -1318,6 +1318,8 @@ _exportOrImport:
muteList: "ミュート"
blockingList: "ブロック"
userLists: "リスト"
excludeMutingUsers: "ミュートしているユーザーを除外"
excludeInactiveUsers: "使われていないアカウントを除外"
_charts:
federationInstancesIncDec: "連合の増減"

View File

@ -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,

View File

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

View File

@ -21,6 +21,8 @@ export type DbJobData = DbUserJobData | DbUserImportJobData | DbUserDeleteJobDat
export type DbUserJobData = {
user: ThinUser;
excludeMuting: boolean;
excludeInactive: boolean;
};
export type DbUserDeleteJobData = {

View File

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

View File

@ -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>