diff --git a/CHANGELOG.md b/CHANGELOG.md
index 705c093a11..2a3d9a1caa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@
- クライアント: 新しいライトテーマを追加
- API: ユーザーのリアクション一覧を取得する users/reactions を追加
- API: users/search および users/search-by-username-and-host を強化
+- ミュート及びブロックのインポートを行えるように
### Bugfixes
- クライアント: テーマの管理が行えない問題を修正
diff --git a/src/client/pages/settings/import-export.vue b/src/client/pages/settings/import-export.vue
index 2b49996dda..eeaa1f1602 100644
--- a/src/client/pages/settings/import-export.vue
+++ b/src/client/pages/settings/import-export.vue
@@ -16,11 +16,13 @@
{{ $ts._exportOrImport.muteList }}
- {{ $ts.export }}
+ {{ $ts.export }}
+ {{ $ts.import }}
{{ $ts._exportOrImport.blockingList }}
{{ $ts.export }}
+ {{ $ts.import }}
@@ -58,11 +60,11 @@ export default defineComponent({
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 == 'mute' ? 'i/export-mute' :
+ 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(() => {
os.dialog({
@@ -81,8 +83,10 @@ export default defineComponent({
const file = await selectFile(e.currentTarget || e.target);
os.api(
- target == 'following' ? 'i/import-following' :
- target == 'user-lists' ? 'i/import-user-lists' :
+ 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(() => {
diff --git a/src/queue/index.ts b/src/queue/index.ts
index 1e1d5da5a2..43c062bae7 100644
--- a/src/queue/index.ts
+++ b/src/queue/index.ts
@@ -163,6 +163,26 @@ export function createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']
});
}
+export function createImportMutingJob(user: ThinUser, fileId: DriveFile['id']) {
+ return dbQueue.add('importMuting', {
+ user: user,
+ fileId: fileId
+ }, {
+ removeOnComplete: true,
+ removeOnFail: true
+ });
+}
+
+export function createImportBlockingJob(user: ThinUser, fileId: DriveFile['id']) {
+ return dbQueue.add('importBlocking', {
+ user: user,
+ fileId: fileId
+ }, {
+ removeOnComplete: true,
+ removeOnFail: true
+ });
+}
+
export function createImportUserListsJob(user: ThinUser, fileId: DriveFile['id']) {
return dbQueue.add('importUserLists', {
user: user,
diff --git a/src/queue/processors/db/import-blocking.ts b/src/queue/processors/db/import-blocking.ts
new file mode 100644
index 0000000000..ab3b91fc0e
--- /dev/null
+++ b/src/queue/processors/db/import-blocking.ts
@@ -0,0 +1,74 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { parseAcct } from '@/misc/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { Users, DriveFiles, Blockings } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+import block from '@/services/blocking/create';
+
+const logger = queueLogger.createSubLogger('import-blocking');
+
+export async function importBlocking(job: Bull.Job, done: any): Promise {
+ logger.info(`Importing blocking of ${job.data.user.id} ...`);
+
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
+
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
+ });
+ if (file == null) {
+ done();
+ return;
+ }
+
+ const csv = await downloadTextFile(file.url);
+
+ let linenum = 0;
+
+ for (const line of csv.trim().split('\n')) {
+ linenum++;
+
+ try {
+ const acct = line.split(',')[0].trim();
+ const { username, host } = parseAcct(acct);
+
+ let target = isSelfHost(host!) ? await Users.findOne({
+ host: null,
+ usernameLower: username.toLowerCase()
+ }) : await Users.findOne({
+ host: toPuny(host!),
+ usernameLower: username.toLowerCase()
+ });
+
+ if (host == null && target == null) continue;
+
+ if (target == null) {
+ target = await resolveUser(username, host);
+ }
+
+ if (target == null) {
+ throw `cannot resolve user: @${username}@${host}`;
+ }
+
+ // skip myself
+ if (target.id === job.data.user.id) continue;
+
+ logger.info(`Mute[${linenum}] ${target.id} ...`);
+
+ await block(user, target);
+ } catch (e) {
+ logger.warn(`Error in line:${linenum} ${e}`);
+ }
+ }
+
+ logger.succ('Imported');
+ done();
+}
+
diff --git a/src/queue/processors/db/import-muting.ts b/src/queue/processors/db/import-muting.ts
new file mode 100644
index 0000000000..798f03a627
--- /dev/null
+++ b/src/queue/processors/db/import-muting.ts
@@ -0,0 +1,83 @@
+import * as Bull from 'bull';
+
+import { queueLogger } from '../../logger';
+import { parseAcct } from '@/misc/acct';
+import { resolveUser } from '@/remote/resolve-user';
+import { downloadTextFile } from '@/misc/download-text-file';
+import { isSelfHost, toPuny } from '@/misc/convert-host';
+import { Users, DriveFiles, Mutings } from '@/models/index';
+import { DbUserImportJobData } from '@/queue/types';
+import { User } from '@/models/entities/user';
+import { genId } from '@/misc/gen-id';
+
+const logger = queueLogger.createSubLogger('import-muting');
+
+export async function importMuting(job: Bull.Job, done: any): Promise {
+ logger.info(`Importing muting of ${job.data.user.id} ...`);
+
+ const user = await Users.findOne(job.data.user.id);
+ if (user == null) {
+ done();
+ return;
+ }
+
+ const file = await DriveFiles.findOne({
+ id: job.data.fileId
+ });
+ if (file == null) {
+ done();
+ return;
+ }
+
+ const csv = await downloadTextFile(file.url);
+
+ let linenum = 0;
+
+ for (const line of csv.trim().split('\n')) {
+ linenum++;
+
+ try {
+ const acct = line.split(',')[0].trim();
+ const { username, host } = parseAcct(acct);
+
+ let target = isSelfHost(host!) ? await Users.findOne({
+ host: null,
+ usernameLower: username.toLowerCase()
+ }) : await Users.findOne({
+ host: toPuny(host!),
+ usernameLower: username.toLowerCase()
+ });
+
+ if (host == null && target == null) continue;
+
+ if (target == null) {
+ target = await resolveUser(username, host);
+ }
+
+ if (target == null) {
+ throw `cannot resolve user: @${username}@${host}`;
+ }
+
+ // skip myself
+ if (target.id === job.data.user.id) continue;
+
+ logger.info(`Mute[${linenum}] ${target.id} ...`);
+
+ await mute(user, target);
+ } catch (e) {
+ logger.warn(`Error in line:${linenum} ${e}`);
+ }
+ }
+
+ logger.succ('Imported');
+ done();
+}
+
+async function mute(user: User, target: User) {
+ await Mutings.insert({
+ id: genId(),
+ createdAt: new Date(),
+ muterId: user.id,
+ muteeId: target.id,
+ });
+}
diff --git a/src/queue/processors/db/index.ts b/src/queue/processors/db/index.ts
index b051a28e0b..97087642b7 100644
--- a/src/queue/processors/db/index.ts
+++ b/src/queue/processors/db/index.ts
@@ -9,6 +9,8 @@ import { exportUserLists } from './export-user-lists';
import { importFollowing } from './import-following';
import { importUserLists } from './import-user-lists';
import { deleteAccount } from './delete-account';
+import { importMuting } from './import-muting';
+import { importBlocking } from './import-blocking';
const jobs = {
deleteDriveFiles,
@@ -18,6 +20,8 @@ const jobs = {
exportBlocking,
exportUserLists,
importFollowing,
+ importMuting,
+ importBlocking,
importUserLists,
deleteAccount,
} as Record | Bull.ProcessPromiseFunction>;
diff --git a/src/server/api/endpoints/i/import-blocking.ts b/src/server/api/endpoints/i/import-blocking.ts
new file mode 100644
index 0000000000..d44d0b6077
--- /dev/null
+++ b/src/server/api/endpoints/i/import-blocking.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportBlockingJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+ secure: true,
+ requireCredential: true as const,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 1,
+ },
+
+ params: {
+ fileId: {
+ validator: $.type(ID),
+ }
+ },
+
+ errors: {
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: 'ebb53e5f-6574-9c0c-0b92-7ca6def56d7e'
+ },
+
+ unexpectedFileType: {
+ message: 'We need csv file.',
+ code: 'UNEXPECTED_FILE_TYPE',
+ id: 'b6fab7d6-d945-d67c-dfdb-32da1cd12cfe'
+ },
+
+ tooBigFile: {
+ message: 'That file is too big.',
+ code: 'TOO_BIG_FILE',
+ id: 'b7fbf0b1-aeef-3b21-29ef-fadd4cb72ccf'
+ },
+
+ emptyFile: {
+ message: 'That file is empty.',
+ code: 'EMPTY_FILE',
+ id: '6f3a4dcc-f060-a707-4950-806fbdbe60d6'
+ },
+ }
+};
+
+export default define(meta, async (ps, user) => {
+ const file = await DriveFiles.findOne(ps.fileId);
+
+ if (file == null) throw new ApiError(meta.errors.noSuchFile);
+ //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+ if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+ if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+ createImportBlockingJob(user, file.id);
+});
diff --git a/src/server/api/endpoints/i/import-muting.ts b/src/server/api/endpoints/i/import-muting.ts
new file mode 100644
index 0000000000..c17434c587
--- /dev/null
+++ b/src/server/api/endpoints/i/import-muting.ts
@@ -0,0 +1,60 @@
+import $ from 'cafy';
+import { ID } from '@/misc/cafy-id';
+import define from '../../define';
+import { createImportMutingJob } from '@/queue/index';
+import * as ms from 'ms';
+import { ApiError } from '../../error';
+import { DriveFiles } from '@/models/index';
+
+export const meta = {
+ secure: true,
+ requireCredential: true as const,
+
+ limit: {
+ duration: ms('1hour'),
+ max: 1,
+ },
+
+ params: {
+ fileId: {
+ validator: $.type(ID),
+ }
+ },
+
+ errors: {
+ noSuchFile: {
+ message: 'No such file.',
+ code: 'NO_SUCH_FILE',
+ id: 'e674141e-bd2a-ba85-e616-aefb187c9c2a'
+ },
+
+ unexpectedFileType: {
+ message: 'We need csv file.',
+ code: 'UNEXPECTED_FILE_TYPE',
+ id: '568c6e42-c86c-ba09-c004-517f83f9f1a8'
+ },
+
+ tooBigFile: {
+ message: 'That file is too big.',
+ code: 'TOO_BIG_FILE',
+ id: '9b4ada6d-d7f7-0472-0713-4f558bd1ec9c'
+ },
+
+ emptyFile: {
+ message: 'That file is empty.',
+ code: 'EMPTY_FILE',
+ id: 'd2f12af1-e7b4-feac-86a3-519548f2728e'
+ },
+ }
+};
+
+export default define(meta, async (ps, user) => {
+ const file = await DriveFiles.findOne(ps.fileId);
+
+ if (file == null) throw new ApiError(meta.errors.noSuchFile);
+ //if (!file.type.endsWith('/csv')) throw new ApiError(meta.errors.unexpectedFileType);
+ if (file.size > 50000) throw new ApiError(meta.errors.tooBigFile);
+ if (file.size === 0) throw new ApiError(meta.errors.emptyFile);
+
+ createImportMutingJob(user, file.id);
+});