From 39748ea0c38c5bfbdadf6cd2772fae636337c470 Mon Sep 17 00:00:00 2001 From: Chocolate Pie <106949016+chocolate-pie@users.noreply.github.com> Date: Sat, 6 May 2023 08:17:55 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=E3=82=A2=E3=83=B3=E3=83=86=E3=83=8A?= =?UTF-8?q?=E3=81=AE=E3=82=A8=E3=82=AF=E3=82=B9=E3=83=9D=E3=83=BC=E3=83=88?= =?UTF-8?q?=E3=83=BB=E3=82=A4=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=88=20(#1075?= =?UTF-8?q?4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: アンテナのエクスポートに対応 (misskey-dev/misskey#10690) * feat: アンテナのインポートに対応 (misskey-dev/misskey#10690) * fix: タイポを修正 * feat: ユーザーリストをサポート * fix: バグを直した * fix: バグを直した * fix: 適当に決めた変数名を変更 * fix * fix: 変数の変更、リファクタリング --- packages/backend/src/core/QueueService.ts | 24 ++++- .../src/queue/DbQueueProcessorsService.ts | 6 ++ .../backend/src/queue/QueueProcessorModule.ts | 4 + .../ExportAntennasProcessorService.ts | 99 +++++++++++++++++++ .../ImportAntennasProcessorService.ts | 93 +++++++++++++++++ packages/backend/src/queue/types.ts | 12 +++ .../backend/src/server/api/EndpointsModule.ts | 8 ++ packages/backend/src/server/api/endpoints.ts | 4 + .../server/api/endpoints/i/export-antennas.ts | 30 ++++++ .../server/api/endpoints/i/import-antennas.ts | 81 +++++++++++++++ .../src/pages/settings/import-export.vue | 24 +++++ 11 files changed, 384 insertions(+), 1 deletion(-) create mode 100644 packages/backend/src/queue/processors/ExportAntennasProcessorService.ts create mode 100644 packages/backend/src/queue/processors/ImportAntennasProcessorService.ts create mode 100644 packages/backend/src/server/api/endpoints/i/export-antennas.ts create mode 100644 packages/backend/src/server/api/endpoints/i/import-antennas.ts diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 3590846d75..b4ffffecc0 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -1,15 +1,16 @@ import { Inject, Injectable } from '@nestjs/common'; import { v4 as uuid } from 'uuid'; +import Bull from 'bull'; import type { IActivity } from '@/core/activitypub/type.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Webhook, webhookEventTypes } from '@/models/entities/Webhook.js'; import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type httpSignature from '@peertube/http-signature'; -import Bull from 'bull'; @Injectable() export class QueueService { @@ -152,6 +153,16 @@ export class QueueService { }); } + @bindThis + public createExportAntennasJob(user: ThinUser) { + return this.dbQueue.add('exportAntennas', { + user: { id: user.id }, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + @bindThis public createImportFollowingJob(user: ThinUser, fileId: DriveFile['id']) { return this.dbQueue.add('importFollowing', { @@ -235,6 +246,17 @@ export class QueueService { }); } + @bindThis + public createImportAntennasJob(user: ThinUser, antenna: Antenna) { + return this.dbQueue.add('importAntennas', { + user: { id: user.id }, + antenna, + }, { + removeOnComplete: true, + removeOnFail: true, + }); + } + @bindThis public createDeleteAccountJob(user: ThinUser, opts: { soft?: boolean; } = {}) { return this.dbQueue.add('deleteAccount', { diff --git a/packages/backend/src/queue/DbQueueProcessorsService.ts b/packages/backend/src/queue/DbQueueProcessorsService.ts index 233a36dd04..df8ac3a301 100644 --- a/packages/backend/src/queue/DbQueueProcessorsService.ts +++ b/packages/backend/src/queue/DbQueueProcessorsService.ts @@ -9,11 +9,13 @@ import { ExportFollowingProcessorService } from './processors/ExportFollowingPro import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportBlockingProcessorService } from './processors/ExportBlockingProcessorService.js'; import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; +import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js'; import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js'; import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js'; import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; +import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js'; import { DeleteAccountProcessorService } from './processors/DeleteAccountProcessorService.js'; import { ExportFavoritesProcessorService } from './processors/ExportFavoritesProcessorService.js'; import type Bull from 'bull'; @@ -32,11 +34,13 @@ export class DbQueueProcessorsService { private exportMutingProcessorService: ExportMutingProcessorService, private exportBlockingProcessorService: ExportBlockingProcessorService, private exportUserListsProcessorService: ExportUserListsProcessorService, + private exportAntennasProcessorService: ExportAntennasProcessorService, private importFollowingProcessorService: ImportFollowingProcessorService, private importMutingProcessorService: ImportMutingProcessorService, private importBlockingProcessorService: ImportBlockingProcessorService, private importUserListsProcessorService: ImportUserListsProcessorService, private importCustomEmojisProcessorService: ImportCustomEmojisProcessorService, + private importAntennasProcessorService: ImportAntennasProcessorService, private deleteAccountProcessorService: DeleteAccountProcessorService, ) { } @@ -51,6 +55,7 @@ export class DbQueueProcessorsService { q.process('exportMuting', (job, done) => this.exportMutingProcessorService.process(job, done)); q.process('exportBlocking', (job, done) => this.exportBlockingProcessorService.process(job, done)); q.process('exportUserLists', (job, done) => this.exportUserListsProcessorService.process(job, done)); + q.process('exportAntennas', (job, done) => this.exportAntennasProcessorService.process(job, done)); q.process('importFollowing', (job, done) => this.importFollowingProcessorService.process(job, done)); q.process('importFollowingToDb', (job) => this.importFollowingProcessorService.processDb(job)); q.process('importMuting', (job, done) => this.importMutingProcessorService.process(job, done)); @@ -58,6 +63,7 @@ export class DbQueueProcessorsService { q.process('importBlockingToDb', (job) => this.importBlockingProcessorService.processDb(job)); q.process('importUserLists', (job, done) => this.importUserListsProcessorService.process(job, done)); q.process('importCustomEmojis', (job, done) => this.importCustomEmojisProcessorService.process(job, done)); + q.process('importAntennas', (job, done) => this.importAntennasProcessorService.process(job, done)); q.process('deleteAccount', (job) => this.deleteAccountProcessorService.process(job)); } } diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 4db9b38547..3d4cc77321 100644 --- a/packages/backend/src/queue/QueueProcessorModule.ts +++ b/packages/backend/src/queue/QueueProcessorModule.ts @@ -24,11 +24,13 @@ import { ExportFollowingProcessorService } from './processors/ExportFollowingPro import { ExportMutingProcessorService } from './processors/ExportMutingProcessorService.js'; import { ExportNotesProcessorService } from './processors/ExportNotesProcessorService.js'; import { ExportUserListsProcessorService } from './processors/ExportUserListsProcessorService.js'; +import { ExportAntennasProcessorService } from './processors/ExportAntennasProcessorService.js'; import { ImportBlockingProcessorService } from './processors/ImportBlockingProcessorService.js'; import { ImportCustomEmojisProcessorService } from './processors/ImportCustomEmojisProcessorService.js'; import { ImportFollowingProcessorService } from './processors/ImportFollowingProcessorService.js'; import { ImportMutingProcessorService } from './processors/ImportMutingProcessorService.js'; import { ImportUserListsProcessorService } from './processors/ImportUserListsProcessorService.js'; +import { ImportAntennasProcessorService } from './processors/ImportAntennasProcessorService.js'; import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js'; import { TickChartsProcessorService } from './processors/TickChartsProcessorService.js'; import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js'; @@ -55,11 +57,13 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor ExportMutingProcessorService, ExportBlockingProcessorService, ExportUserListsProcessorService, + ExportAntennasProcessorService, ImportFollowingProcessorService, ImportMutingProcessorService, ImportBlockingProcessorService, ImportUserListsProcessorService, ImportCustomEmojisProcessorService, + ImportAntennasProcessorService, DeleteAccountProcessorService, DeleteFileProcessorService, CleanRemoteFilesProcessorService, diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts new file mode 100644 index 0000000000..dba764dc35 --- /dev/null +++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts @@ -0,0 +1,99 @@ +import fs from 'node:fs'; +import { Inject, Injectable } from '@nestjs/common'; +import { format as DateFormat } from 'date-fns'; +import { In } from 'typeorm'; +import { DI } from '@/di-symbols.js'; +import type { AntennasRepository, UsersRepository, UserListJoiningsRepository, User } from '@/models/index.js'; +import type { Config } from '@/config.js'; +import Logger from '@/logger.js'; +import { DriveService } from '@/core/DriveService.js'; +import { bindThis } from '@/decorators.js'; +import { createTemp } from '@/misc/create-temp.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import type { DBExportAntennasData } from '../types.js'; +import type Bull from 'bull'; +import { UtilityService } from '@/core/UtilityService.js'; + +@Injectable() +export class ExportAntennasProcessorService { + private logger: Logger; + + constructor ( + @Inject(DI.config) + private config: Config, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + @Inject(DI.antennasRepository) + private antennsRepository: AntennasRepository, + @Inject(DI.userListJoiningsRepository) + private userListJoiningsRepository: UserListJoiningsRepository, + private driveService: DriveService, + private utilityService: UtilityService, + private queueLoggerService: QueueLoggerService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas'); + } + + @bindThis + public async process(job: Bull.Job, done: () => void): Promise { + const user = await this.usersRepository.findOneBy({ id: job.data.user.id }); + if (user == null) { + done(); + return; + } + const [path, cleanup] = await createTemp(); + const stream = fs.createWriteStream(path, { flags: 'a' }); + const write = (input: string): Promise => { + return new Promise((resolve, reject) => { + stream.write(input, err => { + if (err) { + this.logger.error(err); + reject(); + } else { + resolve(); + } + }); + }); + }; + try { + const antennas = await this.antennsRepository.findBy({ userId: job.data.user.id }); + write('['); + for (const [index, antenna] of antennas.entries()) { + let users: User[] | undefined; + if (antenna.userListId !== null) { + const joinings = await this.userListJoiningsRepository.findBy({ userListId: antenna.userListId }); + users = await this.usersRepository.findBy({ + id: In(joinings.map(j => j.userId)), + }); + } + write(JSON.stringify({ + name: antenna.name, + src: antenna.src, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + users: antenna.users, + userListAcct: typeof users !== 'undefined' ? users.map((u) => { + return this.utilityService.getFullApAccount(u.username, u.host); // acct + }) : null, + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, + })); + if (antennas.length - 1 !== index) { + write(', '); + } + } + write(']'); + stream.end(); + + const fileName = 'antennas-' + DateFormat(new Date(), 'yyyy-MM-dd-HH-mm-ss') + '.json'; + const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' }); + this.logger.succ('Exported to: ' + driveFile.id); + } finally { + cleanup(); + done(); + } + } +} + diff --git a/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts new file mode 100644 index 0000000000..67d255bdd3 --- /dev/null +++ b/packages/backend/src/queue/processors/ImportAntennasProcessorService.ts @@ -0,0 +1,93 @@ +import { Injectable, Inject } from '@nestjs/common'; +import Ajv from 'ajv'; +import { IdService } from '@/core/IdService.js'; +import { GlobalEventService } from '@/core/GlobalEventService.js'; +import Logger from '@/logger.js'; +import type { AntennasRepository } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { bindThis } from '@/decorators.js'; +import { QueueLoggerService } from '../QueueLoggerService.js'; +import { DBAntennaImportJobData } from '../types.js'; +import type Bull from 'bull'; + +@Injectable() +export class ImportAntennasProcessorService { + private logger: Logger; + constructor ( + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + private queueLoggerService: QueueLoggerService, + private idService: IdService, + private globalEventService: GlobalEventService, + ) { + this.logger = this.queueLoggerService.logger.createSubLogger('import-antennas'); + } + + @bindThis + public async process(job: Bull.Job, done: () => void): Promise { + const now = new Date(); + try { + const validate = new Ajv().compile({ + type: 'object', + properties: { + name: { type: 'string', minLength: 1, maxLength: 100 }, + src: { type: 'string', enum: ['home', 'all', 'users', 'list'] }, + userListAcct: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + keywords: { type: 'array', items: { + type: 'array', items: { + type: 'string', + }, + } }, + excludeKeywords: { type: 'array', items: { + type: 'array', items: { + type: 'string', + }, + } }, + users: { type: 'array', items: { + type: 'string', + } }, + caseSensitive: { type: 'boolean' }, + withReplies: { type: 'boolean' }, + withFile: { type: 'boolean' }, + notify: { type: 'boolean' }, + }, + required: ['name', 'src', 'keywords', 'excludeKeywords', 'users', 'caseSensitive', 'withReplies', 'withFile', 'notify'], + }); + for (const antenna of job.data.antenna) { + if (antenna.keywords.length === 0 || antenna.keywords[0].every(x => x === '')) continue; + if (!validate(antenna)) { + this.logger.warn('Validation Failed'); + continue; + } + const result = await this.antennasRepository.insert({ + id: this.idService.genId(), + createdAt: now, + lastUsedAt: now, + userId: job.data.user.id, + name: antenna.name, + src: antenna.src === 'list' && antenna.userListAcct ? 'users' : antenna.src, + userListId: null, + keywords: antenna.keywords, + excludeKeywords: antenna.excludeKeywords, + users: (antenna.src === 'list' && antenna.userListAcct !== null ? antenna.userListAcct : antenna.users).filter(Boolean), + caseSensitive: antenna.caseSensitive, + withReplies: antenna.withReplies, + withFile: antenna.withFile, + notify: antenna.notify, + }).then(x => this.antennasRepository.findOneByOrFail(x.identifiers[0])); + this.logger.succ('Antenna created: ' + result.id); + this.globalEventService.publishInternalEvent('antennaCreated', result); + } + } catch (err: any) { + this.logger.error(err); + } finally { + done(); + } + } +} diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 23c973d449..776dd3aa12 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -1,3 +1,4 @@ +import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DriveFile } from '@/models/entities/DriveFile.js'; import type { Note } from '@/models/entities/Note.js'; import type { User } from '@/models/entities/User.js'; @@ -33,12 +34,14 @@ export type DbJobData = DbJobMap[T]; export type DbJobMap = { deleteDriveFiles: DbJobDataWithUser; exportCustomEmojis: DbJobDataWithUser; + exportAntennas: DBExportAntennasData; exportNotes: DbJobDataWithUser; exportFavorites: DbJobDataWithUser; exportFollowing: DbExportFollowingData; exportMuting: DbJobDataWithUser; exportBlocking: DbJobDataWithUser; exportUserLists: DbJobDataWithUser; + importAntennas: DBAntennaImportJobData; importFollowing: DbUserImportJobData; importFollowingToDb: DbUserImportToDbJobData; importMuting: DbUserImportJobData; @@ -59,6 +62,10 @@ export type DbExportFollowingData = { excludeInactive: boolean; }; +export type DBExportAntennasData = { + user: ThinUser +} + export type DbUserDeleteJobData = { user: ThinUser; soft?: boolean; @@ -69,6 +76,11 @@ export type DbUserImportJobData = { fileId: DriveFile['id']; }; +export type DBAntennaImportJobData = { + user: ThinUser, + antenna: Antenna +} + export type DbUserImportToDbJobData = { user: ThinUser; target: string; diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 6dc1313e59..ee1aae5b6c 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -194,6 +194,7 @@ import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; +import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; @@ -202,6 +203,7 @@ import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; import * as ep___i_importMuting from './endpoints/i/import-muting.js'; import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; +import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; import * as ep___i_notifications from './endpoints/i/notifications.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; @@ -530,6 +532,7 @@ const $i_exportMute: Provider = { provide: 'ep:i/export-mute', useClass: ep___i_ const $i_exportNotes: Provider = { provide: 'ep:i/export-notes', useClass: ep___i_exportNotes.default }; const $i_exportFavorites: Provider = { provide: 'ep:i/export-favorites', useClass: ep___i_exportFavorites.default }; const $i_exportUserLists: Provider = { provide: 'ep:i/export-user-lists', useClass: ep___i_exportUserLists.default }; +const $i_exportAntennas: Provider = { provide: 'ep:i/export-antennas', useClass: ep___i_exportAntennas.default }; const $i_favorites: Provider = { provide: 'ep:i/favorites', useClass: ep___i_favorites.default }; const $i_gallery_likes: Provider = { provide: 'ep:i/gallery/likes', useClass: ep___i_gallery_likes.default }; const $i_gallery_posts: Provider = { provide: 'ep:i/gallery/posts', useClass: ep___i_gallery_posts.default }; @@ -538,6 +541,7 @@ const $i_importBlocking: Provider = { provide: 'ep:i/import-blocking', useClass: const $i_importFollowing: Provider = { provide: 'ep:i/import-following', useClass: ep___i_importFollowing.default }; const $i_importMuting: Provider = { provide: 'ep:i/import-muting', useClass: ep___i_importMuting.default }; const $i_importUserLists: Provider = { provide: 'ep:i/import-user-lists', useClass: ep___i_importUserLists.default }; +const $i_importAntennas: Provider = { provide: 'ep:i/import-antennas', useClass: ep___i_importAntennas.default }; const $i_notifications: Provider = { provide: 'ep:i/notifications', useClass: ep___i_notifications.default }; const $i_pageLikes: Provider = { provide: 'ep:i/page-likes', useClass: ep___i_pageLikes.default }; const $i_pages: Provider = { provide: 'ep:i/pages', useClass: ep___i_pages.default }; @@ -870,6 +874,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportNotes, $i_exportFavorites, $i_exportUserLists, + $i_exportAntennas, $i_favorites, $i_gallery_likes, $i_gallery_posts, @@ -878,6 +883,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_importFollowing, $i_importMuting, $i_importUserLists, + $i_importAntennas, $i_notifications, $i_pageLikes, $i_pages, @@ -1204,6 +1210,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_exportNotes, $i_exportFavorites, $i_exportUserLists, + $i_exportAntennas, $i_favorites, $i_gallery_likes, $i_gallery_posts, @@ -1212,6 +1219,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention $i_importFollowing, $i_importMuting, $i_importUserLists, + $i_importAntennas, $i_notifications, $i_pageLikes, $i_pages, diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index acd7f7ec3e..09bd7cbff4 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -194,6 +194,7 @@ import * as ep___i_exportMute from './endpoints/i/export-mute.js'; import * as ep___i_exportNotes from './endpoints/i/export-notes.js'; import * as ep___i_exportFavorites from './endpoints/i/export-favorites.js'; import * as ep___i_exportUserLists from './endpoints/i/export-user-lists.js'; +import * as ep___i_exportAntennas from './endpoints/i/export-antennas.js'; import * as ep___i_favorites from './endpoints/i/favorites.js'; import * as ep___i_gallery_likes from './endpoints/i/gallery/likes.js'; import * as ep___i_gallery_posts from './endpoints/i/gallery/posts.js'; @@ -202,6 +203,7 @@ import * as ep___i_importBlocking from './endpoints/i/import-blocking.js'; import * as ep___i_importFollowing from './endpoints/i/import-following.js'; import * as ep___i_importMuting from './endpoints/i/import-muting.js'; import * as ep___i_importUserLists from './endpoints/i/import-user-lists.js'; +import * as ep___i_importAntennas from './endpoints/i/import-antennas.js'; import * as ep___i_notifications from './endpoints/i/notifications.js'; import * as ep___i_pageLikes from './endpoints/i/page-likes.js'; import * as ep___i_pages from './endpoints/i/pages.js'; @@ -528,6 +530,7 @@ const eps = [ ['i/export-notes', ep___i_exportNotes], ['i/export-favorites', ep___i_exportFavorites], ['i/export-user-lists', ep___i_exportUserLists], + ['i/export-antennas', ep___i_exportAntennas], ['i/favorites', ep___i_favorites], ['i/gallery/likes', ep___i_gallery_likes], ['i/gallery/posts', ep___i_gallery_posts], @@ -536,6 +539,7 @@ const eps = [ ['i/import-following', ep___i_importFollowing], ['i/import-muting', ep___i_importMuting], ['i/import-user-lists', ep___i_importUserLists], + ['i/import-antennas', ep___i_importAntennas], ['i/notifications', ep___i_notifications], ['i/page-likes', ep___i_pageLikes], ['i/pages', ep___i_pages], diff --git a/packages/backend/src/server/api/endpoints/i/export-antennas.ts b/packages/backend/src/server/api/endpoints/i/export-antennas.ts new file mode 100644 index 0000000000..4182c1b247 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/export-antennas.ts @@ -0,0 +1,30 @@ +import { Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; + +export const meta = { + secure: true, + requireCredential: true, + limit: { + duration: ms('1hour'), + max: 1, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: {}, + required: [], +} as const; + +@Injectable() +export default class extends Endpoint { // eslint-disable-line import/no-default-export + constructor ( + private queueService: QueueService, + ) { + super(meta, paramDef, async (ps, me) => { + this.queueService.createExportAntennasJob(me); + }); + } +} diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts new file mode 100644 index 0000000000..676648bde8 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts @@ -0,0 +1,81 @@ +import { Inject, Injectable } from '@nestjs/common'; +import ms from 'ms'; +import { Endpoint } from '@/server/api/endpoint-base.js'; +import { QueueService } from '@/core/QueueService.js'; +import type { AntennasRepository, DriveFilesRepository, UsersRepository, Antenna as _Antenna } from '@/models/index.js'; +import { DI } from '@/di-symbols.js'; +import { ApiError } from '../../error.js'; +import { RoleService } from '@/core/RoleService.js'; +import { DownloadService } from '@/core/DownloadService.js'; + +export const meta = { + secure: true, + requireCredential: true, + prohibitMoved: true, + + limit: { + duration: ms('1hour'), + max: 1, + }, + errors: { + noSuchFile: { + message: 'No such file.', + code: 'NO_SUCH_FILE', + id: '3b71d086-c3fa-431c-b01d-ded65a777172', + }, + noSuchUser: { + message: 'No such user.', + code: 'NO_SUCH_USER', + id: 'e842c379-8ac7-4cf7-b07a-4d4de7e4671c', + }, + emptyFile: { + message: 'That file is empty.', + code: 'EMPTY_FILE', + id: '7f60115d-8d93-4b0f-bd0e-3815dcbb389f', + }, + tooManyAntennas: { + message: 'You cannot create antenna any more.', + code: 'TOO_MANY_ANTENNAS', + id: '600917d4-a4cb-4cc5-8ba8-7ac8ea3c7779', + }, + }, +} as const; + +export const paramDef = { + type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + }, + required: ['fileId'], +} as const; + +@Injectable() // eslint-disable-next-line import/no-default-export +export default class extends Endpoint { + constructor ( + @Inject(DI.driveFilesRepository) + private driveFilesRepository: DriveFilesRepository, + @Inject(DI.antennasRepository) + private antennasRepository: AntennasRepository, + @Inject(DI.usersRepository) + private usersRepository: UsersRepository, + private roleService: RoleService, + private queueService: QueueService, + private downloadService: DownloadService + ) { + super(meta, paramDef, async (ps, me) => { + const users = await this.usersRepository.findOneBy({ id: me.id }); + if (users === null) throw new ApiError(meta.errors.noSuchUser); + const file = await this.driveFilesRepository.findOneBy({ id: ps.fileId }); + if (file === null) throw new ApiError(meta.errors.noSuchFile); + if (file.size === 0) throw new ApiError(meta.errors.emptyFile); + const antennas: (_Antenna & { userListAcct: string[] | null })[] = JSON.parse(await this.downloadService.downloadTextFile(file.url)); + const currentAntennasCount = await this.antennasRepository.countBy({ userId: me.id }); + if (currentAntennasCount + antennas.length > (await this.roleService.getUserPolicies(me.id)).antennaLimit) { + throw new ApiError(meta.errors.tooManyAntennas); + } + this.queueService.createImportAntennasJob(me, antennas); + }); + } +} + +export type Antenna = (_Antenna & { userListAcct: string[] | null })[]; diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue index c883efe691..89b4104020 100644 --- a/packages/frontend/src/pages/settings/import-export.vue +++ b/packages/frontend/src/pages/settings/import-export.vue @@ -84,6 +84,21 @@ + + +
+ + + + {{ i18n.ts.export }} + + + + + {{ i18n.ts.import }} + +
+
@@ -151,6 +166,10 @@ const exportMuting = () => { os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); }; +const exportAntennas = () => { + os.api('i/export-antennas', {}).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); @@ -171,6 +190,11 @@ const importBlocking = async (ev) => { os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; +const importAntennas = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; + const headerActions = $computed(() => []); const headerTabs = $computed(() => []);