summaryrefslogtreecommitdiff
path: root/packages/backend
diff options
context:
space:
mode:
Diffstat (limited to 'packages/backend')
-rw-r--r--packages/backend/src/core/QueueService.ts24
-rw-r--r--packages/backend/src/queue/DbQueueProcessorsService.ts6
-rw-r--r--packages/backend/src/queue/QueueProcessorModule.ts4
-rw-r--r--packages/backend/src/queue/processors/ExportAntennasProcessorService.ts99
-rw-r--r--packages/backend/src/queue/processors/ImportAntennasProcessorService.ts93
-rw-r--r--packages/backend/src/queue/types.ts12
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts8
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/i/export-antennas.ts30
-rw-r--r--packages/backend/src/server/api/endpoints/i/import-antennas.ts81
10 files changed, 360 insertions, 1 deletions
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 {
@@ -153,6 +154,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', {
user: { id: user.id },
@@ -236,6 +247,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', {
user: { id: user.id },
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<DBExportAntennasData>, done: () => void): Promise<void> {
+ 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<void> => {
+ 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<DBAntennaImportJobData>, done: () => void): Promise<void> {
+ 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<T extends keyof DbJobMap> = 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<typeof meta, typeof paramDef> { // 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<typeof meta, typeof paramDef> {
+ 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 })[];