};
type ObjectSchemaTypeDef =
p['ref'] extends keyof typeof refs ? Packed
:
@@ -232,6 +235,12 @@ export type SchemaTypeDef
=
p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] :
never
) :
+ p['prefixItems'] extends ReadonlyArray ? (
+ p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType
[]] :
+ p['items'] extends false ? ArrayToTuple
:
+ p['unevaluatedItems'] extends false ? ArrayToTuple
:
+ [...ArrayToTuple
, ...unknown[]]
+ ) :
p['items'] extends NonNullable ? SchemaType[] :
any[]
) :
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 81851572d7bb..87c7f0adeb91 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -594,6 +594,11 @@ export class MiMeta {
})
public perUserListTimelineCacheMax: number;
+ @Column('boolean', {
+ default: false,
+ })
+ public enableReactionsBuffering: boolean;
+
@Column('integer', {
default: 0,
})
@@ -631,6 +636,19 @@ export class MiMeta {
})
public urlPreviewUserAgent: string | null;
+ @Column('varchar', {
+ length: 128,
+ default: 'all',
+ })
+ public federation: 'all' | 'specified' | 'none';
+
+ @Column('varchar', {
+ length: 1024,
+ array: true,
+ default: '{}',
+ })
+ public federationHosts: string[];
+
@Column('boolean', {
default: false,
})
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index df88b996364c..c1d3d42134d4 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -7,6 +7,8 @@ import { MiUser } from './User.js';
import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
+import { MiDriveFile } from './DriveFile.js';
+import { userExportableEntities } from '@/types.js';
export type MiNotification = {
type: 'note';
@@ -67,6 +69,7 @@ export type MiNotification = {
id: string;
createdAt: string;
notifierId: MiUser['id'];
+ message: string | null;
} | {
type: 'roleAssigned';
id: string;
@@ -77,6 +80,12 @@ export type MiNotification = {
id: string;
createdAt: string;
achievement: string;
+} | {
+ type: 'exportCompleted';
+ id: string;
+ createdAt: string;
+ exportedEntity: typeof userExportableEntities[number];
+ fileId: MiDriveFile['id'];
} | {
type: 'app';
id: string;
@@ -85,7 +94,7 @@ export type MiNotification = {
/**
* アプリ通知のbody
*/
- customBody: string | null;
+ customBody: string;
/**
* アプリ通知のheader
diff --git a/packages/backend/src/models/User.ts b/packages/backend/src/models/User.ts
index 9e2d7a34447d..805a1e75aebd 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -155,6 +155,11 @@ export class MiUser {
})
public tags: string[];
+ @Column('integer', {
+ default: 0,
+ })
+ public score: number;
+
@Column('boolean', {
default: false,
comment: 'Whether the User is suspended.',
@@ -289,5 +294,6 @@ export const localUsernameSchema = { type: 'string', pattern: /^\w{1,20}$/.toStr
export const passwordSchema = { type: 'string', minLength: 1 } as const;
export const nameSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
export const descriptionSchema = { type: 'string', minLength: 1, maxLength: 1500 } as const;
+export const followedMessageSchema = { type: 'string', minLength: 1, maxLength: 256 } as const;
export const locationSchema = { type: 'string', minLength: 1, maxLength: 50 } as const;
export const birthdaySchema = { type: 'string', pattern: /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/.toString().slice(1, -1) } as const;
diff --git a/packages/backend/src/models/UserProfile.ts b/packages/backend/src/models/UserProfile.ts
index 7dbe0b3717c6..554455529611 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -42,6 +42,14 @@ export class MiUserProfile {
})
public description: string | null;
+ // フォローされた際のメッセージ
+ @Column('varchar', {
+ length: 256, nullable: true,
+ })
+ public followedMessage: string | null;
+
+ // TODO: 鍵アカウントの場合の、フォローリクエスト受信時のメッセージも設定できるようにする
+
@Column('jsonb', {
default: [],
})
diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts
index be42342e3fa6..26cba2e59a5b 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -8,7 +8,8 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
-export type WebhookEventType = (typeof webhookEventTypes)[number] | `note@${string}`;
+export type WebhookEventType = (typeof webhookEventTypes)[number] | `note@${string}`; // TODO: remove this line later
+export type WebhookEventTypes = typeof webhookEventTypes[number] | `note@${string}`;
@Entity('webhook')
export class MiWebhook {
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index 31861a1966aa..00638812822d 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -253,6 +253,10 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
default: 'local',
},
+ maxFileSize: {
+ type: 'number',
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index b4c4442758cd..264501049101 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { notificationTypes } from '@/types.js';
+import { ACHIEVEMENT_TYPES } from '@/core/AchievementService.js';
+import { notificationTypes, userExportableEntities } from '@/types.js';
const baseSchema = {
type: 'object',
@@ -266,6 +267,10 @@ export const packedNotificationSchema = {
optional: false, nullable: false,
format: 'id',
},
+ message: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
},
}, {
type: 'object',
@@ -294,6 +299,27 @@ export const packedNotificationSchema = {
achievement: {
type: 'string',
optional: false, nullable: false,
+ enum: ACHIEVEMENT_TYPES,
+ },
+ },
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: ['exportCompleted'],
+ },
+ exportedEntity: {
+ type: 'string',
+ optional: false, nullable: false,
+ enum: userExportableEntities,
+ },
+ fileId: {
+ type: 'string',
+ optional: false, nullable: false,
+ format: 'id',
},
},
}, {
@@ -311,11 +337,11 @@ export const packedNotificationSchema = {
},
header: {
type: 'string',
- optional: false, nullable: false,
+ optional: false, nullable: true,
},
icon: {
type: 'string',
- optional: false, nullable: false,
+ optional: false, nullable: true,
},
},
}, {
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index bf4cf52c8bf8..b6793357655d 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -276,6 +276,26 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
+ canImportAntennas: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportBlocking: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportFollowing: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportMuting: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportUserLists: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index 947a9317d7a6..16c8a5a097bb 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -370,6 +370,10 @@ export const packedUserDetailedNotMeOnlySchema = {
ref: 'RoleLite',
},
},
+ followedMessage: {
+ type: 'string',
+ nullable: true, optional: true,
+ },
memo: {
type: 'string',
nullable: true, optional: false,
@@ -437,6 +441,10 @@ export const packedMeDetailedOnlySchema = {
nullable: true, optional: false,
format: 'id',
},
+ followedMessage: {
+ type: 'string',
+ nullable: true, optional: false,
+ },
isModerator: {
type: 'boolean',
nullable: true, optional: false,
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index a1fd38fcc58c..0027b5ef3d7a 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -14,6 +14,7 @@ import { InboxProcessorService } from './processors/InboxProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { CleanRemoteFilesProcessorService } from './processors/CleanRemoteFilesProcessorService.js';
@@ -51,6 +52,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
ResyncChartsProcessorService,
CleanChartsProcessorService,
CheckExpiredMutingsProcessorService,
+ BakeBufferedReactionsProcessorService,
CleanProcessorService,
DeleteDriveFilesProcessorService,
ExportCustomEmojisProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 7bd74f3210f8..e9e1c4522469 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -39,6 +39,7 @@ import { TickChartsProcessorService } from './processors/TickChartsProcessorServ
import { ResyncChartsProcessorService } from './processors/ResyncChartsProcessorService.js';
import { CleanChartsProcessorService } from './processors/CleanChartsProcessorService.js';
import { CheckExpiredMutingsProcessorService } from './processors/CheckExpiredMutingsProcessorService.js';
+import { BakeBufferedReactionsProcessorService } from './processors/BakeBufferedReactionsProcessorService.js';
import { CleanProcessorService } from './processors/CleanProcessorService.js';
import { AggregateRetentionProcessorService } from './processors/AggregateRetentionProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
@@ -118,6 +119,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private cleanChartsProcessorService: CleanChartsProcessorService,
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
+ private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
@@ -147,6 +149,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'cleanCharts': return this.cleanChartsProcessorService.process();
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
+ case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts
new file mode 100644
index 000000000000..d49c99f694af
--- /dev/null
+++ b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts
@@ -0,0 +1,42 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+import type * as Bull from 'bullmq';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+
+@Injectable()
+export class BakeBufferedReactionsProcessorService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
+ private reactionsBufferingService: ReactionsBufferingService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions');
+ }
+
+ @bindThis
+ public async process(): Promise {
+ if (!this.meta.enableReactionsBuffering) {
+ this.logger.info('Reactions buffering is disabled. Skipping...');
+ return;
+ }
+
+ this.logger.info('Baking buffered reactions...');
+
+ await this.reactionsBufferingService.bake();
+
+ this.logger.succ('All buffered reactions baked.');
+ }
+}
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 4076e9da9038..9590a4fe71c5 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
import * as Bull from 'bullmq';
import { Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { InstancesRepository } from '@/models/_.js';
+import type { InstancesRepository, MiMeta } from '@/models/_.js';
import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
import { ApRequestService } from '@/core/activitypub/ApRequestService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
@@ -31,10 +30,12 @@ export class DeliverProcessorService {
private latest: string | null;
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.instancesRepository)
private instancesRepository: InstancesRepository,
- private metaService: MetaService,
private utilityService: UtilityService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService,
@@ -52,9 +53,7 @@ export class DeliverProcessorService {
public async process(job: Bull.Job): Promise {
const { host } = new URL(job.data.to);
- // ブロックしてたら中断
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.toPuny(host))) {
+ if (!this.utilityService.isFederationAllowedUri(job.data.to)) {
return 'skip (blocked)';
}
@@ -88,7 +87,7 @@ export class DeliverProcessorService {
this.apRequestChart.deliverSucc();
this.federationChart.deliverd(i.host, true);
- if (meta.enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true);
}
});
@@ -120,7 +119,7 @@ export class DeliverProcessorService {
this.apRequestChart.deliverFail();
this.federationChart.deliverd(i.host, false);
- if (meta.enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, false);
}
});
diff --git a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
index 88c4ea29c0e6..b3111865adc6 100644
--- a/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportAntennasProcessorService.ts
@@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
import { bindThis } from '@/decorators.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { DBExportAntennasData } from '../types.js';
import type * as Bull from 'bullmq';
@@ -35,6 +36,7 @@ export class ExportAntennasProcessorService {
private driveService: DriveService,
private utilityService: UtilityService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-antennas');
}
@@ -95,6 +97,11 @@ export class ExportAntennasProcessorService {
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);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'antenna',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
index 6ec3c18786fe..ecc439db69ce 100644
--- a/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportBlockingProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -30,6 +31,7 @@ export class ExportBlockingProcessorService {
private blockingsRepository: BlockingsRepository,
private utilityService: UtilityService,
+ private notificationService: NotificationService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
) {
@@ -109,6 +111,11 @@ export class ExportBlockingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'blocking',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
index 01eab26e96ab..583ddbb745a4 100644
--- a/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportClipsProcessorService.ts
@@ -19,6 +19,7 @@ import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -43,6 +44,7 @@ export class ExportClipsProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
private idService: IdService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-clips');
}
@@ -79,6 +81,11 @@ export class ExportClipsProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'clip',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
index e4eb4791bd7f..e237cd4975bc 100644
--- a/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportCustomEmojisProcessorService.ts
@@ -16,6 +16,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp, createTempDir } from '@/misc/create-temp.js';
import { DownloadService } from '@/core/DownloadService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -37,6 +38,7 @@ export class ExportCustomEmojisProcessorService {
private driveService: DriveService,
private downloadService: DownloadService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-custom-emojis');
}
@@ -134,6 +136,12 @@ export class ExportCustomEmojisProcessorService {
const driveFile = await this.driveService.addFile({ user, path: archivePath, name: fileName, force: true });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'customEmoji',
+ fileId: driveFile.id,
+ });
+
cleanup();
archiveCleanup();
resolve();
diff --git a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
index 7bb626dd31bf..b81feece018e 100644
--- a/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFavoritesProcessorService.ts
@@ -16,6 +16,7 @@ import type { MiPoll } from '@/models/Poll.js';
import type { MiNote } from '@/models/Note.js';
import { bindThis } from '@/decorators.js';
import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
import type { DbJobDataWithUser } from '../types.js';
@@ -37,6 +38,7 @@ export class ExportFavoritesProcessorService {
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
private idService: IdService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-favorites');
}
@@ -123,6 +125,11 @@ export class ExportFavoritesProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'favorite',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
index 1cc80e66d718..903f96251587 100644
--- a/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportFollowingProcessorService.ts
@@ -14,6 +14,7 @@ import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import type { MiFollowing } from '@/models/Following.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -36,6 +37,7 @@ export class ExportFollowingProcessorService {
private utilityService: UtilityService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-following');
}
@@ -113,6 +115,11 @@ export class ExportFollowingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'following',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
index 243b74f2c2be..f9867ade292a 100644
--- a/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportMutingProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -32,6 +33,7 @@ export class ExportMutingProcessorService {
private utilityService: UtilityService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-muting');
}
@@ -110,6 +112,11 @@ export class ExportMutingProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'muting',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
index c7611012d74b..9e2b678219cf 100644
--- a/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportNotesProcessorService.ts
@@ -18,6 +18,7 @@ import { bindThis } from '@/decorators.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { Packed } from '@/misc/json-schema.js';
import { IdService } from '@/core/IdService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { JsonArrayStream } from '@/misc/JsonArrayStream.js';
import { FileWriterStream } from '@/misc/FileWriterStream.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
@@ -112,6 +113,7 @@ export class ExportNotesProcessorService {
private queueLoggerService: QueueLoggerService,
private driveFileEntityService: DriveFileEntityService,
private idService: IdService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-notes');
}
@@ -150,6 +152,11 @@ export class ExportNotesProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'json' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'note',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
index ee87cff5d360..c483d79854d0 100644
--- a/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
+++ b/packages/backend/src/queue/processors/ExportUserListsProcessorService.ts
@@ -13,6 +13,7 @@ import type Logger from '@/logger.js';
import { DriveService } from '@/core/DriveService.js';
import { createTemp } from '@/misc/create-temp.js';
import { UtilityService } from '@/core/UtilityService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import { bindThis } from '@/decorators.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type * as Bull from 'bullmq';
@@ -35,6 +36,7 @@ export class ExportUserListsProcessorService {
private utilityService: UtilityService,
private driveService: DriveService,
private queueLoggerService: QueueLoggerService,
+ private notificationService: NotificationService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('export-user-lists');
}
@@ -89,6 +91,11 @@ export class ExportUserListsProcessorService {
const driveFile = await this.driveService.addFile({ user, path, name: fileName, force: true, ext: 'csv' });
this.logger.succ(`Exported to: ${driveFile.id}`);
+
+ this.notificationService.createNotification(user.id, 'exportCompleted', {
+ exportedEntity: 'userList',
+ fileId: driveFile.id,
+ });
} finally {
cleanup();
}
diff --git a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
index 171809d25c3a..9e1b8fee7034 100644
--- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
@@ -87,23 +87,30 @@ export class ImportCustomEmojisProcessorService {
await this.emojisRepository.delete({
name: emojiInfo.name,
});
- const driveFile = await this.driveService.addFile({
- user: null,
- path: emojiPath,
- name: record.fileName,
- force: true,
- });
- await this.customEmojiService.add({
- name: emojiInfo.name,
- category: emojiInfo.category,
- host: null,
- aliases: emojiInfo.aliases,
- driveFile,
- license: emojiInfo.license,
- isSensitive: emojiInfo.isSensitive,
- localOnly: emojiInfo.localOnly,
- roleIdsThatCanBeUsedThisEmojiAsReaction: [],
- });
+ try {
+ const driveFile = await this.driveService.addFile({
+ user: null,
+ path: emojiPath,
+ name: record.fileName,
+ force: true,
+ });
+ await this.customEmojiService.add({
+ name: emojiInfo.name,
+ category: emojiInfo.category,
+ host: null,
+ aliases: emojiInfo.aliases,
+ driveFile,
+ license: emojiInfo.license,
+ isSensitive: emojiInfo.isSensitive,
+ localOnly: emojiInfo.localOnly,
+ roleIdsThatCanBeUsedThisEmojiAsReaction: [],
+ });
+ } catch (e) {
+ if (e instanceof Error || typeof e === 'string') {
+ this.logger.error(`couldn't import ${emojiPath} for ${emojiInfo.name}: ${e}`);
+ }
+ continue;
+ }
}
cleanup();
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index f0d3f249a4ed..aaef0b7d3527 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -4,11 +4,10 @@
*/
import { URL } from 'node:url';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common';
import httpSignature from '@peertube/http-signature';
import * as Bull from 'bullmq';
import type Logger from '@/logger.js';
-import { MetaService } from '@/core/MetaService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import InstanceChart from '@/core/chart/charts/instance.js';
@@ -26,16 +25,28 @@ import { JsonLdService } from '@/core/activitypub/JsonLdService.js';
import { ApInboxService } from '@/core/activitypub/ApInboxService.js';
import { bindThis } from '@/decorators.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
+import { CollapsedQueue } from '@/misc/collapsed-queue.js';
+import { MiNote } from '@/models/Note.js';
+import { MiMeta } from '@/models/Meta.js';
+import { DI } from '@/di-symbols.js';
import { QueueLoggerService } from '../QueueLoggerService.js';
import type { InboxJobData } from '../types.js';
+type UpdateInstanceJob = {
+ latestRequestReceivedAt: Date,
+ shouldUnsuspend: boolean,
+};
+
@Injectable()
-export class InboxProcessorService {
+export class InboxProcessorService implements OnApplicationShutdown {
private logger: Logger;
+ private updateInstanceQueue: CollapsedQueue;
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
private utilityService: UtilityService,
- private metaService: MetaService,
private apInboxService: ApInboxService,
private federatedInstanceService: FederatedInstanceService,
private fetchInstanceMetadataService: FetchInstanceMetadataService,
@@ -48,6 +59,7 @@ export class InboxProcessorService {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
+ this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
}
@bindThis
@@ -63,9 +75,7 @@ export class InboxProcessorService {
const host = this.utilityService.toPuny(new URL(signature.keyId).hostname);
- // ブロックしてたら中断
- const meta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) {
+ if (!this.utilityService.isFederationAllowedHost(host)) {
return `Blocked request: ${host}`;
}
@@ -164,9 +174,8 @@ export class InboxProcessorService {
throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${activity.actor})`);
}
- // ブロックしてたら中断
const ldHost = this.utilityService.extractDbHost(authUser.user.uri);
- if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) {
+ if (!this.utilityService.isFederationAllowedHost(ldHost)) {
throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`);
}
} else {
@@ -185,11 +194,9 @@ export class InboxProcessorService {
// Update stats
this.federatedInstanceService.fetch(authUser.user.host).then(i => {
- this.federatedInstanceService.update(i.id, {
+ this.updateInstanceQueue.enqueue(i.id, {
latestRequestReceivedAt: new Date(),
- isNotResponding: false,
- // もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
- suspensionState: i.suspensionState === 'autoSuspendedForNotResponding' ? 'none' : undefined,
+ shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
});
this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
@@ -197,7 +204,7 @@ export class InboxProcessorService {
this.apRequestChart.inbox();
this.federationChart.inbox(i.host);
- if (meta.enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host);
}
});
@@ -226,4 +233,36 @@ export class InboxProcessorService {
}
return 'ok';
}
+
+ @bindThis
+ public collapseUpdateInstanceJobs(oldJob: UpdateInstanceJob, newJob: UpdateInstanceJob) {
+ const latestRequestReceivedAt = oldJob.latestRequestReceivedAt < newJob.latestRequestReceivedAt
+ ? newJob.latestRequestReceivedAt
+ : oldJob.latestRequestReceivedAt;
+ const shouldUnsuspend = oldJob.shouldUnsuspend || newJob.shouldUnsuspend;
+ return {
+ latestRequestReceivedAt,
+ shouldUnsuspend,
+ };
+ }
+
+ @bindThis
+ public async performUpdateInstance(id: string, job: UpdateInstanceJob) {
+ await this.federatedInstanceService.update(id, {
+ latestRequestReceivedAt: new Date(),
+ isNotResponding: false,
+ // もしサーバーが死んでるために配信が止まっていた場合には自動的に復活させてあげる
+ suspensionState: job.shouldUnsuspend ? 'none' : undefined,
+ });
+ }
+
+ @bindThis
+ public async dispose(): Promise {
+ await this.updateInstanceQueue.performAllNow();
+ }
+
+ @bindThis
+ async onApplicationShutdown(signal?: string) {
+ await this.dispose();
+ }
}
diff --git a/packages/backend/src/server/FileServerService.ts b/packages/backend/src/server/FileServerService.ts
index 77a637d89516..41b6d2e83de0 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -82,7 +82,7 @@ export class FileServerService {
.catch(err => this.errorHandler(request, reply, err));
});
fastify.get<{ Params: { key: string; } }>('/files/:key/*', async (request, reply) => {
- return await reply.redirect(301, `${this.config.url}/files/${request.params.key}`);
+ return await reply.redirect(`${this.config.url}/files/${request.params.key}`, 301);
});
done();
});
@@ -147,12 +147,12 @@ export class FileServerService {
url.searchParams.set('static', '1');
file.cleanup();
- return await reply.redirect(301, url.toString());
+ return await reply.redirect(url.toString(), 301);
} else if (file.mime.startsWith('video/')) {
const externalThumbnail = this.videoProcessingService.getExternalVideoThumbnailUrl(file.url);
if (externalThumbnail) {
file.cleanup();
- return await reply.redirect(301, externalThumbnail);
+ return await reply.redirect(externalThumbnail, 301);
}
image = await this.videoProcessingService.generateVideoThumbnail(file.path);
@@ -167,7 +167,7 @@ export class FileServerService {
url.searchParams.set('url', file.url);
file.cleanup();
- return await reply.redirect(301, url.toString());
+ return await reply.redirect(url.toString(), 301);
}
}
@@ -314,8 +314,8 @@ export class FileServerService {
}
return await reply.redirect(
- 301,
url.toString(),
+ 301,
);
}
diff --git a/packages/backend/src/server/HealthServerService.ts b/packages/backend/src/server/HealthServerService.ts
index 2c3ed85925c9..5980609f02bd 100644
--- a/packages/backend/src/server/HealthServerService.ts
+++ b/packages/backend/src/server/HealthServerService.ts
@@ -27,6 +27,9 @@ export class HealthServerService {
@Inject(DI.redisForTimelines)
private redisForTimelines: Redis.Redis,
+ @Inject(DI.redisForReactions)
+ private redisForReactions: Redis.Redis,
+
@Inject(DI.db)
private db: DataSource,
@@ -43,6 +46,7 @@ export class HealthServerService {
this.redisForPub.ping(),
this.redisForSub.ping(),
this.redisForTimelines.ping(),
+ this.redisForReactions.ping(),
this.db.query('SELECT 1'),
...(this.meilisearch ? [this.meilisearch.health()] : []),
]).then(() => 200, () => 503));
diff --git a/packages/backend/src/server/ServerModule.ts b/packages/backend/src/server/ServerModule.ts
index 29bf7c67f92e..eb1aa6f9a46a 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -48,6 +48,7 @@ import { UserListChannelService } from './api/stream/channels/user-list.js';
import { RoleTimelineChannelService } from './api/stream/channels/role-timeline.js';
import { ReversiChannelService } from './api/stream/channels/reversi.js';
import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js';
+import { SigninWithPasskeyApiService } from './api/SigninWithPasskeyApiService.js';
@Module({
imports: [
@@ -73,6 +74,7 @@ import { ReversiGameChannelService } from './api/stream/channels/reversi-game.js
AuthenticateService,
RateLimiterService,
SigninApiService,
+ SigninWithPasskeyApiService,
SigninService,
SignupApiService,
StreamingApiServerService,
diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts
index 9c849480f21f..fd2bd3267d62 100644
--- a/packages/backend/src/server/ServerService.ts
+++ b/packages/backend/src/server/ServerService.ts
@@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body';
import { IsNull } from 'typeorm';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import type { Config } from '@/config.js';
-import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import * as Acct from '@/misc/acct.js';
@@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { bindThis } from '@/decorators.js';
-import { MetaService } from '@/core/MetaService.js';
import { ActivityPubServerService } from './ActivityPubServerService.js';
import { NodeinfoServerService } from './NodeinfoServerService.js';
import { ApiServerService } from './api/ApiServerService.js';
@@ -44,6 +43,9 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -53,7 +55,6 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
- private metaService: MetaService,
private userEntityService: UserEntityService,
private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService,
@@ -165,8 +166,8 @@ export class ServerService implements OnApplicationShutdown {
}
return await reply.redirect(
- 301,
url.toString(),
+ 301,
);
});
@@ -193,7 +194,7 @@ export class ServerService implements OnApplicationShutdown {
reply.header('Content-Type', 'image/png');
reply.header('Cache-Control', 'public, max-age=86400');
- if ((await this.metaService.fetch()).enableIdenticonGeneration) {
+ if (this.meta.enableIdenticonGeneration) {
return await genIdenticon(request.params.x);
} else {
return reply.redirect('/static-assets/avatar.png');
diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts
index 47f64f660939..aad833f1261e 100644
--- a/packages/backend/src/server/api/ApiCallService.ts
+++ b/packages/backend/src/server/api/ApiCallService.ts
@@ -13,8 +13,7 @@ import { getIpHash } from '@/misc/get-ip-hash.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import type { MiAccessToken } from '@/models/AccessToken.js';
import type Logger from '@/logger.js';
-import type { UserIpsRepository } from '@/models/_.js';
-import { MetaService } from '@/core/MetaService.js';
+import type { MiMeta, UserIpsRepository } from '@/models/_.js';
import { createTemp } from '@/misc/create-temp.js';
import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
@@ -40,13 +39,15 @@ export class ApiCallService implements OnApplicationShutdown {
private userIpHistoriesClearIntervalId: NodeJS.Timeout;
constructor(
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.config)
private config: Config,
@Inject(DI.userIpsRepository)
private userIpsRepository: UserIpsRepository,
- private metaService: MetaService,
private authenticateService: AuthenticateService,
private rateLimiterService: RateLimiterService,
private roleService: RoleService,
@@ -64,15 +65,6 @@ export class ApiCallService implements OnApplicationShutdown {
let statusCode = err.httpStatusCode;
if (err.httpStatusCode === 401) {
reply.header('WWW-Authenticate', 'Bearer realm="Misskey"');
- } else if (err.kind === 'client') {
- reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
- statusCode = statusCode ?? 400;
- } else if (err.kind === 'permission') {
- // (ROLE_PERMISSION_DENIEDは関係ない)
- if (err.code === 'PERMISSION_DENIED') {
- reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
- }
- statusCode = statusCode ?? 403;
} else if (err.code === 'RATE_LIMIT_EXCEEDED') {
const info: unknown = err.info;
const unixEpochInSeconds = Date.now();
@@ -83,6 +75,15 @@ export class ApiCallService implements OnApplicationShutdown {
} else {
this.logger.warn(`rate limit information has unexpected type ${typeof(err.info?.reset)}`);
}
+ } else if (err.kind === 'client') {
+ reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="invalid_request", error_description="${err.message}"`);
+ statusCode = statusCode ?? 400;
+ } else if (err.kind === 'permission') {
+ // (ROLE_PERMISSION_DENIEDは関係ない)
+ if (err.code === 'PERMISSION_DENIED') {
+ reply.header('WWW-Authenticate', `Bearer realm="Misskey", error="insufficient_scope", error_description="${err.message}"`);
+ }
+ statusCode = statusCode ?? 403;
} else if (!statusCode) {
statusCode = 500;
}
@@ -199,9 +200,18 @@ export class ApiCallService implements OnApplicationShutdown {
return;
}
- const [path] = await createTemp();
+ const [path, cleanup] = await createTemp();
await stream.pipeline(multipartData.file, fs.createWriteStream(path));
+ // ファイルサイズが制限を超えていた場合
+ // なお truncated はストリームを読み切ってからでないと機能しないため、stream.pipeline より後にある必要がある
+ if (multipartData.file.truncated) {
+ cleanup();
+ reply.code(413);
+ reply.send();
+ return;
+ }
+
const fields = {} as Record;
for (const [k, v] of Object.entries(multipartData.fields)) {
fields[k] = typeof v === 'object' && 'value' in v ? v.value : undefined;
@@ -256,9 +266,8 @@ export class ApiCallService implements OnApplicationShutdown {
}
@bindThis
- private async logIp(request: FastifyRequest, user: MiLocalUser) {
- const meta = await this.metaService.fetch();
- if (!meta.enableIpLogging) return;
+ private logIp(request: FastifyRequest, user: MiLocalUser) {
+ if (!this.meta.enableIpLogging) return;
const ip = request.ip;
const ips = this.userIpHistories.get(user.id);
if (ips == null || !ips.has(ip)) {
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 4a5935f93092..709a0446018a 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -8,6 +8,7 @@ import cors from '@fastify/cors';
import multipart from '@fastify/multipart';
import fastifyCookie from '@fastify/cookie';
import { ModuleRef } from '@nestjs/core';
+import { AuthenticationResponseJSON } from '@simplewebauthn/types';
import type { Config } from '@/config.js';
import type { InstancesRepository, AccessTokensRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
@@ -17,6 +18,7 @@ import endpoints from './endpoints.js';
import { ApiCallService } from './ApiCallService.js';
import { SignupApiService } from './SignupApiService.js';
import { SigninApiService } from './SigninApiService.js';
+import { SigninWithPasskeyApiService } from './SigninWithPasskeyApiService.js';
import type { FastifyInstance, FastifyPluginOptions } from 'fastify';
@Injectable()
@@ -37,6 +39,7 @@ export class ApiServerService {
private apiCallService: ApiCallService,
private signupApiService: SignupApiService,
private signinApiService: SigninApiService,
+ private signinWithPasskeyApiService: SigninWithPasskeyApiService,
) {
//this.createServer = this.createServer.bind(this);
}
@@ -49,7 +52,7 @@ export class ApiServerService {
fastify.register(multipart, {
limits: {
- fileSize: this.config.maxFileSize ?? 262144000,
+ fileSize: this.config.maxFileSize,
files: 1,
},
});
@@ -131,6 +134,12 @@ export class ApiServerService {
};
}>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
+ fastify.post<{
+ Body: {
+ credential?: AuthenticationResponseJSON;
+ };
+ }>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));
+
fastify.post<{ Body: { code: string; } }>('/signup-pending', (request, reply) => this.signupApiService.signupPending(request, reply));
fastify.get('/v1/instance/peers', async (request, reply) => {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 8c112edd761f..f103ac9766f1 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -94,6 +94,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -260,6 +261,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@@ -481,6 +483,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
+const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
@@ -647,6 +650,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
+const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
@@ -872,6 +876,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -1038,6 +1043,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
@@ -1257,6 +1263,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -1423,6 +1430,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
diff --git a/packages/backend/src/server/api/SigninWithPasskeyApiService.ts b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
new file mode 100644
index 000000000000..9ba23c54e226
--- /dev/null
+++ b/packages/backend/src/server/api/SigninWithPasskeyApiService.ts
@@ -0,0 +1,173 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { randomUUID } from 'crypto';
+import { Inject, Injectable } from '@nestjs/common';
+import { IsNull } from 'typeorm';
+import { DI } from '@/di-symbols.js';
+import type {
+ SigninsRepository,
+ UserProfilesRepository,
+ UsersRepository,
+} from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { getIpHash } from '@/misc/get-ip-hash.js';
+import type { MiLocalUser, MiUser } from '@/models/User.js';
+import { IdService } from '@/core/IdService.js';
+import { bindThis } from '@/decorators.js';
+import { WebAuthnService } from '@/core/WebAuthnService.js';
+import Logger from '@/logger.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import type { IdentifiableError } from '@/misc/identifiable-error.js';
+import { RateLimiterService } from './RateLimiterService.js';
+import { SigninService } from './SigninService.js';
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
+import type { FastifyReply, FastifyRequest } from 'fastify';
+
+@Injectable()
+export class SigninWithPasskeyApiService {
+ private logger: Logger;
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
+ @Inject(DI.signinsRepository)
+ private signinsRepository: SigninsRepository,
+
+ private idService: IdService,
+ private rateLimiterService: RateLimiterService,
+ private signinService: SigninService,
+ private webAuthnService: WebAuthnService,
+ private loggerService: LoggerService,
+ ) {
+ this.logger = this.loggerService.getLogger('PasskeyAuth');
+ }
+
+ @bindThis
+ public async signin(
+ request: FastifyRequest<{
+ Body: {
+ credential?: AuthenticationResponseJSON;
+ context?: string;
+ };
+ }>,
+ reply: FastifyReply,
+ ) {
+ reply.header('Access-Control-Allow-Origin', this.config.url);
+ reply.header('Access-Control-Allow-Credentials', 'true');
+
+ const body = request.body;
+ const credential = body['credential'];
+
+ function error(status: number, error: { id: string }) {
+ reply.code(status);
+ return { error };
+ }
+
+ const fail = async (userId: MiUser['id'], status?: number, failure?: { id: string }) => {
+ // Append signin history
+ await this.signinsRepository.insert({
+ id: this.idService.gen(),
+ userId: userId,
+ ip: request.ip,
+ headers: request.headers as any,
+ success: false,
+ });
+ return error(status ?? 500, failure ?? { id: '4e30e80c-e338-45a0-8c8f-44455efa3b76' });
+ };
+
+ try {
+ // Not more than 1 API call per 250ms and not more than 100 attempts per 30min
+ // NOTE: 1 Sign-in require 2 API calls
+ await this.rateLimiterService.limit({ key: 'signin-with-passkey', duration: 60 * 30 * 1000, max: 200, minInterval: 250 }, getIpHash(request.ip));
+ } catch (err) {
+ reply.code(429);
+ return {
+ error: {
+ message: 'Too many failed attempts to sign in. Try again later.',
+ code: 'TOO_MANY_AUTHENTICATION_FAILURES',
+ id: '22d05606-fbcf-421a-a2db-b32610dcfd1b',
+ },
+ };
+ }
+
+ // Initiate Passkey Auth challenge with context
+ if (!credential) {
+ const context = randomUUID();
+ this.logger.info(`Initiate Passkey challenge: context: ${context}`);
+ const authChallengeOptions = {
+ option: await this.webAuthnService.initiateSignInWithPasskeyAuthentication(context),
+ context: context,
+ };
+ reply.code(200);
+ return authChallengeOptions;
+ }
+
+ const context = body.context;
+ if (!context || typeof context !== 'string') {
+ // If try Authentication without context
+ return error(400, {
+ id: '1658cc2e-4495-461f-aee4-d403cdf073c1',
+ });
+ }
+
+ this.logger.debug(`Try Sign-in with Passkey: context: ${context}`);
+
+ let authorizedUserId: MiUser['id'] | null;
+ try {
+ authorizedUserId = await this.webAuthnService.verifySignInWithPasskeyAuthentication(context, credential);
+ } catch (err) {
+ this.logger.warn(`Passkey challenge Verify error! : ${err}`);
+ const errorId = (err as IdentifiableError).id;
+ return error(403, {
+ id: errorId,
+ });
+ }
+
+ if (!authorizedUserId) {
+ return error(403, {
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+ });
+ }
+
+ // Fetch user
+ const user = await this.usersRepository.findOneBy({
+ id: authorizedUserId,
+ host: IsNull(),
+ }) as MiLocalUser | null;
+
+ if (user == null) {
+ return error(403, {
+ id: '652f899f-66d4-490e-993e-6606c8ec04c3',
+ });
+ }
+
+ if (user.isSuspended) {
+ return error(403, {
+ id: 'e03a5f46-d309-4865-9b69-56282d94e1eb',
+ });
+ }
+
+ const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+
+ // Authentication was successful, but passwordless login is not enabled
+ if (!profile.usePasswordLessLogin) {
+ return await fail(user.id, 403, {
+ id: '2d84773e-f7b7-4d0b-8f72-bb69b584c912',
+ });
+ }
+
+ const signinResponse = this.signinService.signin(request, reply, user);
+ return {
+ signinResponse: signinResponse,
+ };
+ }
+}
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index 632b0c62bc5d..c49963801800 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import { IsNull } from 'typeorm';
import { DI } from '@/di-symbols.js';
-import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js';
+import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js';
import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
import { CaptchaService } from '@/core/CaptchaService.js';
import { IdService } from '@/core/IdService.js';
import { SignupService } from '@/core/SignupService.js';
@@ -28,6 +27,9 @@ export class SignupApiService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -45,7 +47,6 @@ export class SignupApiService {
private userEntityService: UserEntityService,
private idService: IdService,
- private metaService: MetaService,
private captchaService: CaptchaService,
private signupService: SignupService,
private signinService: SigninService,
@@ -72,31 +73,29 @@ export class SignupApiService {
) {
const body = request.body;
- const instance = await this.metaService.fetch(true);
-
// Verify *Captcha
// ただしテスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test') {
- if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
- await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+ if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+ await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
- if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) {
- await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+ if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+ await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
- if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
- await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+ if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+ await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
- if (instance.enableTurnstile && instance.turnstileSecretKey) {
- await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => {
+ if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+ await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
throw new FastifyReplyError(400, err);
});
}
@@ -108,7 +107,7 @@ export class SignupApiService {
const invitationCode = body['invitationCode'];
const emailAddress = body['emailAddress'];
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
if (emailAddress == null || typeof emailAddress !== 'string') {
reply.code(400);
return;
@@ -123,7 +122,7 @@ export class SignupApiService {
let ticket: MiRegistrationTicket | null = null;
- if (instance.disableRegistration) {
+ if (this.meta.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400);
return;
@@ -144,7 +143,7 @@ export class SignupApiService {
}
// メアド認証が有効の場合
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
// メアド認証済みならエラー
if (ticket.usedBy) {
reply.code(400);
@@ -162,7 +161,7 @@ export class SignupApiService {
}
}
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
}
@@ -172,7 +171,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, 'USED_USERNAME');
}
- const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
+ const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase());
if (isPreserved) {
throw new FastifyReplyError(400, 'DENIED_USERNAME');
}
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index a0f01f8fb54e..6a432e647a73 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -100,6 +100,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -266,6 +267,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@@ -485,6 +487,7 @@ const eps = [
['admin/system-webhook/list', ep___admin_systemWebhook_list],
['admin/system-webhook/show', ep___admin_systemWebhook_show],
['admin/system-webhook/update', ep___admin_systemWebhook_update],
+ ['admin/system-webhook/test', ep___admin_systemWebhook_test],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
@@ -651,6 +654,7 @@ const eps = [
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
+ ['i/webhooks/test', ep___i_webhooks_test],
['invite/create', ep___invite_create],
['invite/delete', ep___invite_delete],
['invite/list', ep___invite_list],
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 99cd471b738f..d8c536ad096c 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -377,6 +377,10 @@ export const meta = {
type: 'number',
optional: false, nullable: false,
},
+ enableReactionsBuffering: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
@@ -491,6 +495,18 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ federation: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ federationHosts: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
nirilaBlockMentionsFromUnfamiliarRemoteUsers: {
type: 'boolean',
optional: false, nullable: false,
@@ -632,6 +648,7 @@ export default class extends Endpoint { // eslint-
perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
+ enableReactionsBuffering: instance.enableReactionsBuffering,
notesPerOneAd: instance.notesPerOneAd,
summalyProxy: instance.urlPreviewSummaryProxyUrl,
urlPreviewEnabled: instance.urlPreviewEnabled,
@@ -640,6 +657,8 @@ export default class extends Endpoint { // eslint-
urlPreviewRequireContentLength: instance.urlPreviewRequireContentLength,
urlPreviewUserAgent: instance.urlPreviewUserAgent,
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
+ federation: instance.federation,
+ federationHosts: instance.federationHosts,
nirilaBlockMentionsFromUnfamiliarRemoteUsers: instance.nirilaBlockMentionsFromUnfamiliarRemoteUsers,
nirilaAllowedUnfamiliarRemoteUserIds: instance.nirilaAllowedUnfamiliarRemoteUserIds,
};
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
index 7a3410ffa751..f3e440b4cb51 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
- items: {
- anyOf: [
- {
- type: 'string',
- },
- {
- type: 'number',
- },
- ],
- },
+ prefixItems: [
+ {
+ type: 'string',
+ },
+ {
+ type: 'number',
+ },
+ ],
+ unevaluatedItems: false,
},
example: [[
'example.com',
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index 305ae1af1da5..e7589cba8112 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
- items: {
- anyOf: [
- {
- type: 'string',
- },
- {
- type: 'number',
- },
- ],
- },
+ prefixItems: [
+ {
+ type: 'string',
+ },
+ {
+ type: 'number',
+ },
+ ],
+ unevaluatedItems: false,
},
example: [[
'example.com',
diff --git a/packages/backend/src/server/api/endpoints/admin/show-user.ts b/packages/backend/src/server/api/endpoints/admin/show-user.ts
index 5a1c05f41aa6..655bd32bce06 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -31,6 +31,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ followedMessage: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
autoAcceptFollowed: {
type: 'boolean',
optional: false, nullable: false,
@@ -226,6 +230,7 @@ export default class extends Endpoint { // eslint-
return {
email: profile.email,
emailVerified: profile.emailVerified,
+ followedMessage: profile.followedMessage,
autoAcceptFollowed: profile.autoAcceptFollowed,
noCrawle: profile.noCrawle,
preventAiLearning: profile.preventAiLearning,
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
new file mode 100644
index 000000000000..fb2ddf4b446d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+
+export const meta = {
+ tags: ['webhooks'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'read:admin:system-webhook',
+
+ limit: {
+ duration: ms('15min'),
+ max: 60,
+ },
+
+ errors: {
+ noSuchWebhook: {
+ message: 'No such webhook.',
+ code: 'NO_SUCH_WEBHOOK',
+ id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ webhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ type: {
+ type: 'string',
+ enum: systemWebhookEventTypes,
+ },
+ override: {
+ type: 'object',
+ properties: {
+ url: { type: 'string', nullable: false },
+ secret: { type: 'string', nullable: false },
+ },
+ },
+ },
+ required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private webhookTestService: WebhookTestService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ try {
+ await this.webhookTestService.testSystemWebhook({
+ webhookId: ps.webhookId,
+ type: ps.type,
+ override: ps.override,
+ });
+ } catch (e) {
+ if (e instanceof WebhookTestService.NoSuchWebhookError) {
+ throw new ApiError(meta.errors.noSuchWebhook);
+ }
+ throw e;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 4bafd4cdef4c..d829caa718c3 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -143,6 +143,7 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' },
+ enableReactionsBuffering: { type: 'boolean' },
notesPerOneAd: { type: 'integer' },
silencedHosts: {
type: 'array',
@@ -168,6 +169,16 @@ export const paramDef = {
urlPreviewRequireContentLength: { type: 'boolean' },
urlPreviewUserAgent: { type: 'string', nullable: true },
urlPreviewSummaryProxyUrl: { type: 'string', nullable: true },
+ federation: {
+ type: 'string',
+ enum: ['all', 'none', 'specified'],
+ },
+ federationHosts: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
nirilaBlockMentionsFromUnfamiliarRemoteUsers: { type: 'boolean', nullable: false },
nirilaAllowedUnfamiliarRemoteUserIds: {
type: 'array',
@@ -176,7 +187,7 @@ export const paramDef = {
type: 'string',
nullable: false,
},
- }
+ },
},
required: [],
} as const;
@@ -612,6 +623,10 @@ export default class extends Endpoint { // eslint-
set.perUserListTimelineCacheMax = ps.perUserListTimelineCacheMax;
}
+ if (ps.enableReactionsBuffering !== undefined) {
+ set.enableReactionsBuffering = ps.enableReactionsBuffering;
+ }
+
if (ps.notesPerOneAd !== undefined) {
set.notesPerOneAd = ps.notesPerOneAd;
}
@@ -646,6 +661,14 @@ export default class extends Endpoint { // eslint-
set.urlPreviewSummaryProxyUrl = value === '' ? null : value;
}
+ if (ps.federation !== undefined) {
+ set.federation = ps.federation;
+ }
+
+ if (Array.isArray(ps.federationHosts)) {
+ set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+ }
+
if (ps.nirilaBlockMentionsFromUnfamiliarRemoteUsers !== undefined) {
set.nirilaBlockMentionsFromUnfamiliarRemoteUsers = ps.nirilaBlockMentionsFromUnfamiliarRemoteUsers;
}
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 577b9e1b1f8c..e0c8ddcc8478 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -34,6 +34,12 @@ export const meta = {
code: 'TOO_MANY_ANTENNAS',
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
},
+
+ emptyKeyword: {
+ message: 'Either keywords or excludeKeywords is required.',
+ code: 'EMPTY_KEYWORD',
+ id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
+ },
},
res: {
@@ -87,7 +93,7 @@ export default class extends Endpoint { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ throw new ApiError(meta.errors.emptyKeyword);
}
const currentAntennasCount = await this.antennasRepository.countBy({
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 0c30bca9e0bf..10f26b19126e 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -32,6 +32,12 @@ export const meta = {
code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
},
+
+ emptyKeyword: {
+ message: 'Either keywords or excludeKeywords is required.',
+ code: 'EMPTY_KEYWORD',
+ id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
+ },
},
res: {
@@ -85,7 +91,7 @@ export default class extends Endpoint { // eslint-
super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ throw new ApiError(meta.errors.emptyKeyword);
}
}
// Fetch the antenna
diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts
index d3c40dba590e..c52608cefb3f 100644
--- a/packages/backend/src/server/api/endpoints/ap/show.ts
+++ b/packages/backend/src/server/api/endpoints/ap/show.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiNote } from '@/models/Note.js';
@@ -12,7 +12,6 @@ import { isActor, isPost, getApId } from '@/core/activitypub/type.js';
import type { SchemaType } from '@/misc/json-schema.js';
import { ApResolverService } from '@/core/activitypub/ApResolverService.js';
import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js';
-import { MetaService } from '@/core/MetaService.js';
import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js';
import { ApNoteService } from '@/core/activitypub/models/ApNoteService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
@@ -91,7 +90,6 @@ export default class extends Endpoint { // eslint-
private utilityService: UtilityService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
- private metaService: MetaService,
private apResolverService: ApResolverService,
private apDbResolverService: ApDbResolverService,
private apPersonService: ApPersonService,
@@ -112,9 +110,7 @@ export default class extends Endpoint { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> {
- // ブロックしてたら中断
- const fetchedMeta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null;
+ if (!this.utilityService.isFederationAllowedUri(uri)) return null;
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 8c5567359088..d4fd75e04955 100644
--- a/packages/backend/src/server/api/endpoints/channels/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts
@@ -5,14 +5,12 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { ChannelsRepository, NotesRepository } from '@/models/_.js';
+import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js';
import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { IdService } from '@/core/IdService.js';
-import { CacheService } from '@/core/CacheService.js';
-import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { MiLocalUser } from '@/models/User.js';
import { ApiError } from '../../error.js';
@@ -58,6 +56,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -68,16 +69,12 @@ export default class extends Endpoint { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
- private cacheService: CacheService,
private activeUsersChart: ActiveUsersChart,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
- const serverSettings = await this.metaService.fetch();
-
const channel = await this.channelsRepository.findOneBy({
id: ps.channelId,
});
@@ -88,7 +85,7 @@ export default class extends Endpoint { // eslint-
if (me) this.activeUsersChart.read(me);
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me);
}
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 7e9b0fa0e1e4..eb45e29f9e12 100644
--- a/packages/backend/src/server/api/endpoints/drive.ts
+++ b/packages/backend/src/server/api/endpoints/drive.ts
@@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
import { RoleService } from '@/core/RoleService.js';
@@ -41,14 +40,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- private metaService: MetaService,
private driveFileEntityService: DriveFileEntityService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
- const instance = await this.metaService.fetch(true);
-
- // Calculate drive usage
const usage = await this.driveFileEntityService.calcDriveUsageOf(me.id);
const policies = await this.roleService.getUserPolicies(me.id);
diff --git a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
index 467039202524..b86059b5e7ac 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/attached-notes.ts
@@ -10,6 +10,7 @@ import { QueryService } from '@/core/QueryService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+import { RoleService } from '@/core/RoleService.js';
export const meta = {
tags: ['drive', 'notes'],
@@ -61,12 +62,13 @@ export default class extends Endpoint { // eslint-
private noteEntityService: NoteEntityService,
private queryService: QueryService,
+ private roleService: RoleService,
) {
super(meta, paramDef, async (ps, me) => {
// Fetch file
const file = await this.driveFilesRepository.findOneBy({
id: ps.fileId,
- userId: me.id,
+ userId: await this.roleService.isModerator(me) ? undefined : me.id,
});
if (file == null) {
diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts
index 9c17f93ab26b..74eb4dded7ce 100644
--- a/packages/backend/src/server/api/endpoints/drive/files/create.ts
+++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts
@@ -4,14 +4,15 @@
*/
import ms from 'ms';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
-import { MetaService } from '@/core/MetaService.js';
import { DriveService } from '@/core/DriveService.js';
import { ApiError } from '../../../error.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['drive'],
@@ -73,8 +74,10 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private driveFileEntityService: DriveFileEntityService,
- private metaService: MetaService,
private driveService: DriveService,
) {
super(meta, paramDef, async (ps, me, _, file, cleanup, ip, headers) => {
@@ -91,8 +94,6 @@ export default class extends Endpoint { // eslint-
}
}
- const instance = await this.metaService.fetch();
-
try {
// Create file
const driveFile = await this.driveService.addFile({
@@ -103,8 +104,8 @@ export default class extends Endpoint { // eslint-
folderId: ps.folderId,
force: ps.force,
sensitive: ps.isSensitive,
- requestIp: instance.enableIpLogging ? ip : null,
- requestHeaders: instance.enableIpLogging ? headers : null,
+ requestIp: this.serverSettings.enableIpLogging ? ip : null,
+ requestHeaders: this.serverSettings.enableIpLogging ? headers : null,
});
return await this.driveFileEntityService.pack(driveFile, { self: true });
} catch (err) {
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
index bc46163e3d27..bdf6c065e89c 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -16,6 +16,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportAntennas',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index 260610853930..d7bb6bcd2256 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportBlocking',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index d5e824df2740..e03192d8c67b 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportFollowing',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index 0f5800404eaf..76b285bb7e3f 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportMuting',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index bacdd5c88f28..76ecfd082ca3 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportUserLists',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts
index eea657ebbd07..da1faee30d15 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import ms from 'ms';
import bcrypt from 'bcryptjs';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import type { UserProfilesRepository } from '@/models/_.js';
+import type { MiMeta, UserProfilesRepository } from '@/models/_.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { EmailService } from '@/core/EmailService.js';
import type { Config } from '@/config.js';
@@ -15,7 +15,6 @@ import { DI } from '@/di-symbols.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js';
import { UserAuthService } from '@/core/UserAuthService.js';
-import { MetaService } from '@/core/MetaService.js';
import { ApiError } from '../../error.js';
export const meta = {
@@ -70,10 +69,12 @@ export default class extends Endpoint { // eslint-
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
- private metaService: MetaService,
private userEntityService: UserEntityService,
private emailService: EmailService,
private userAuthService: UserAuthService,
@@ -105,7 +106,7 @@ export default class extends Endpoint { // eslint-
if (!res.available) {
throw new ApiError(meta.errors.unavailable);
}
- } else if ((await this.metaService.fetch()).emailRequiredForSignup) {
+ } else if (this.serverSettings.emailRequiredForSignup) {
throw new ApiError(meta.errors.emailRequired);
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 91e5e15e19fd..e1f35cb49934 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -13,9 +13,8 @@ import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
-import { birthdaySchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
+import { birthdaySchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
-import { notificationTypes } from '@/types.js';
import { normalizeForSearch } from '@/misc/normalize-for-search.js';
import { langmap } from '@/misc/langmap.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
@@ -134,6 +133,7 @@ export const paramDef = {
properties: {
name: { ...nameSchema, nullable: true },
description: { ...descriptionSchema, nullable: true },
+ followedMessage: { ...followedMessageSchema, nullable: true },
location: { ...locationSchema, nullable: true },
birthday: { ...birthdaySchema, nullable: true },
lang: { type: 'string', enum: [null, ...Object.keys(langmap)] as string[], nullable: true },
@@ -260,6 +260,7 @@ export default class extends Endpoint { // eslint-
if (ps.name !== undefined) updates.name = ps.name;
if (ps.description !== undefined) profileUpdates.description = ps.description;
+ if (ps.followedMessage !== undefined) profileUpdates.followedMessage = ps.followedMessage;
if (ps.lang !== undefined) profileUpdates.lang = ps.lang;
if (ps.location !== undefined) profileUpdates.location = ps.location;
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index 88a0e6f0cf41..f0b445c6238c 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
index c395fc59e707..faf3aec5b79c 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
@@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks', 'account'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
index 012e3ef94638..fda586e15993 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
@@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
new file mode 100644
index 000000000000..2bf6df9ce2f9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
@@ -0,0 +1,76 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { webhookEventTypes } from '@/models/Webhook.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['webhooks'],
+
+ requireCredential: true,
+ secure: true,
+ kind: 'read:account',
+
+ limit: {
+ duration: ms('15min'),
+ max: 60,
+ },
+
+ errors: {
+ noSuchWebhook: {
+ message: 'No such webhook.',
+ code: 'NO_SUCH_WEBHOOK',
+ id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ webhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ type: {
+ type: 'string',
+ enum: webhookEventTypes,
+ },
+ override: {
+ type: 'object',
+ properties: {
+ url: { type: 'string' },
+ secret: { type: 'string' },
+ },
+ },
+ },
+ required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private webhookTestService: WebhookTestService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ try {
+ await this.webhookTestService.testUserWebhook({
+ webhookId: ps.webhookId,
+ type: ps.type,
+ override: ps.override,
+ }, me);
+ } catch (e) {
+ if (e instanceof WebhookTestService.NoSuchWebhookError) {
+ throw new ApiError(meta.errors.noSuchWebhook);
+ }
+ throw e;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts
index beb77ca7ab02..253a36081514 100644
--- a/packages/backend/src/server/api/endpoints/notes/create.ts
+++ b/packages/backend/src/server/api/endpoints/notes/create.ts
@@ -17,8 +17,6 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { NoteCreateService } from '@/core/NoteCreateService.js';
import { DI } from '@/di-symbols.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
-import { MetaService } from '@/core/MetaService.js';
-import { UtilityService } from '@/core/UtilityService.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
import { ApiError } from '../../error.js';
diff --git a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
index 2a2c6599427d..aed9065bf953 100644
--- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts
@@ -5,7 +5,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
+import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
@@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
import { QueryService } from '@/core/QueryService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@@ -74,6 +73,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -87,7 +89,6 @@ export default class extends Endpoint { // eslint-
private cacheService: CacheService,
private queryService: QueryService,
private userFollowingService: UserFollowingService,
- private metaService: MetaService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -101,9 +102,7 @@ export default class extends Endpoint { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@@ -156,7 +155,7 @@ export default class extends Endpoint { // eslint-
allowPartial: ps.allowPartial,
me,
redisTimelines: timelineConfig,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
noteFilter: note => {
diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
index be82b5a8a749..0b48f2c78bd4 100644
--- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts
@@ -5,16 +5,14 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository } from '@/models/_.js';
+import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { IdService } from '@/core/IdService.js';
-import { CacheService } from '@/core/CacheService.js';
import { QueryService } from '@/core/QueryService.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@@ -66,6 +64,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -73,10 +74,8 @@ export default class extends Endpoint { // eslint-
private roleService: RoleService,
private activeUsersChart: ActiveUsersChart,
private idService: IdService,
- private cacheService: CacheService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -89,9 +88,7 @@ export default class extends Endpoint { // eslint-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@@ -115,7 +112,7 @@ export default class extends Endpoint { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines:
ps.withFiles ? ['localTimelineWithFiles']
: ps.withReplies ? ['localTimeline', 'localTimelineWithReplies']
diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts
index c9b43b535937..7cb11cc1ebfc 100644
--- a/packages/backend/src/server/api/endpoints/notes/timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts
@@ -5,7 +5,7 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js';
+import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { QueryService } from '@/core/QueryService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
@@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js';
import { CacheService } from '@/core/CacheService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { MiLocalUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
export const meta = {
@@ -56,6 +55,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -69,15 +71,12 @@ export default class extends Endpoint { // eslint-
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private userFollowingService: UserFollowingService,
private queryService: QueryService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
@@ -108,7 +107,7 @@ export default class extends Endpoint { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`],
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts
index 38a9660aa239..e9a6a36b0266 100644
--- a/packages/backend/src/server/api/endpoints/notes/translate.ts
+++ b/packages/backend/src/server/api/endpoints/notes/translate.ts
@@ -4,14 +4,15 @@
*/
import { URLSearchParams } from 'node:url';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
-import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import { GetterService } from '@/server/api/GetterService.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '../../error.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
tags: ['notes'],
@@ -59,9 +60,11 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
private noteEntityService: NoteEntityService,
private getterService: GetterService,
- private metaService: MetaService,
private httpRequestService: HttpRequestService,
private roleService: RoleService,
) {
@@ -84,9 +87,7 @@ export default class extends Endpoint { // eslint-
return;
}
- const instance = await this.metaService.fetch();
-
- if (instance.deeplAuthKey == null) {
+ if (this.serverSettings.deeplAuthKey == null) {
throw new ApiError(meta.errors.unavailable);
}
@@ -94,11 +95,11 @@ export default class extends Endpoint { // eslint-
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
const params = new URLSearchParams();
- params.append('auth_key', instance.deeplAuthKey);
+ params.append('auth_key', this.serverSettings.deeplAuthKey);
params.append('text', note.text);
params.append('target_lang', targetLang);
- const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+ const endpoint = this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
const res = await this.httpRequestService.send(endpoint, {
method: 'POST',
diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
index 43877e61efaf..6c7185c9ebba 100644
--- a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
+++ b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts
@@ -5,16 +5,14 @@
import { Inject, Injectable } from '@nestjs/common';
import { Brackets } from 'typeorm';
-import type { MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
+import type { MiMeta, MiUserList, NotesRepository, UserListMembershipsRepository, UserListsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import ActiveUsersChart from '@/core/chart/charts/active-users.js';
import { DI } from '@/di-symbols.js';
-import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js';
import { MiLocalUser } from '@/models/User.js';
-import { MetaService } from '@/core/MetaService.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { ApiError } from '../../error.js';
@@ -69,6 +67,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -80,11 +81,9 @@ export default class extends Endpoint { // eslint-
private noteEntityService: NoteEntityService,
private activeUsersChart: ActiveUsersChart,
- private cacheService: CacheService,
private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
private queryService: QueryService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -99,9 +98,7 @@ export default class extends Endpoint { // eslint-
throw new ApiError(meta.errors.noSuchList);
}
- const serverSettings = await this.metaService.fetch();
-
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb(list, {
untilId,
sinceId,
@@ -124,7 +121,7 @@ export default class extends Endpoint { // eslint-
limit: ps.limit,
allowPartial: ps.allowPartial,
me,
- useDbFallback: serverSettings.enableFanoutTimelineDbFallback,
+ useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback,
redisTimelines: ps.withFiles ? [`userListTimelineWithFiles:${list.id}`] : [`userListTimeline:${list.id}`],
alwaysIncludeMyNotes: true,
excludePureRenotes: !ps.withRenotes,
diff --git a/packages/backend/src/server/api/endpoints/pinned-users.ts b/packages/backend/src/server/api/endpoints/pinned-users.ts
index 15832ef7f8d7..5b0b656c63fe 100644
--- a/packages/backend/src/server/api/endpoints/pinned-users.ts
+++ b/packages/backend/src/server/api/endpoints/pinned-users.ts
@@ -5,11 +5,10 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsersRepository } from '@/models/_.js';
import * as Acct from '@/misc/acct.js';
import type { MiUser } from '@/models/User.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { DI } from '@/di-symbols.js';
@@ -38,16 +37,16 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
- private metaService: MetaService,
private userEntityService: UserEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const meta = await this.metaService.fetch();
-
- const users = await Promise.all(meta.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
+ const users = await Promise.all(this.serverSettings.pinnedUsers.map(acct => Acct.parse(acct)).map(acct => this.usersRepository.findOneBy({
usernameLower: acct.username.toLowerCase(),
host: acct.host ?? IsNull(),
})));
diff --git a/packages/backend/src/server/api/endpoints/server-info.ts b/packages/backend/src/server/api/endpoints/server-info.ts
index c13802eb0689..8301c85f2eb6 100644
--- a/packages/backend/src/server/api/endpoints/server-info.ts
+++ b/packages/backend/src/server/api/endpoints/server-info.ts
@@ -5,9 +5,10 @@
import * as os from 'node:os';
import si from 'systeminformation';
-import { Injectable } from '@nestjs/common';
+import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
+import { MiMeta } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
export const meta = {
requireCredential: false,
@@ -73,10 +74,11 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
- private metaService: MetaService,
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
) {
super(meta, paramDef, async () => {
- if (!(await this.metaService.fetch()).enableServerMachineStats) return {
+ if (!this.serverSettings.enableServerMachineStats) return {
machine: '?',
cpu: {
model: '?',
diff --git a/packages/backend/src/server/api/endpoints/sw/register.ts b/packages/backend/src/server/api/endpoints/sw/register.ts
index a9a33149f95a..fd76df2d3c0b 100644
--- a/packages/backend/src/server/api/endpoints/sw/register.ts
+++ b/packages/backend/src/server/api/endpoints/sw/register.ts
@@ -5,9 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { IdService } from '@/core/IdService.js';
-import type { SwSubscriptionsRepository } from '@/models/_.js';
+import type { MiMeta, SwSubscriptionsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
-import { MetaService } from '@/core/MetaService.js';
import { DI } from '@/di-symbols.js';
import { PushNotificationService } from '@/core/PushNotificationService.js';
@@ -62,11 +61,13 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.swSubscriptionsRepository)
private swSubscriptionsRepository: SwSubscriptionsRepository,
private idService: IdService,
- private metaService: MetaService,
private pushNotificationService: PushNotificationService,
) {
super(meta, paramDef, async (ps, me) => {
@@ -78,12 +79,10 @@ export default class extends Endpoint { // eslint-
publickey: ps.publickey,
});
- const instance = await this.metaService.fetch(true);
-
if (exist != null) {
return {
state: 'already-subscribed' as const,
- key: instance.swPublicKey,
+ key: this.serverSettings.swPublicKey,
userId: me.id,
endpoint: exist.endpoint,
sendReadMessage: exist.sendReadMessage,
@@ -103,7 +102,7 @@ export default class extends Endpoint { // eslint-
return {
state: 'subscribed' as const,
- key: instance.swPublicKey,
+ key: this.serverSettings.swPublicKey,
userId: me.id,
endpoint: ps.endpoint,
sendReadMessage: ps.sendReadMessage,
diff --git a/packages/backend/src/server/api/endpoints/username/available.ts b/packages/backend/src/server/api/endpoints/username/available.ts
index affb0996f182..4944be9b0541 100644
--- a/packages/backend/src/server/api/endpoints/username/available.ts
+++ b/packages/backend/src/server/api/endpoints/username/available.ts
@@ -5,11 +5,10 @@
import { IsNull } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
+import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { localUsernameSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
-import { MetaService } from '@/core/MetaService.js';
export const meta = {
tags: ['users'],
@@ -39,13 +38,14 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@Inject(DI.usedUsernamesRepository)
private usedUsernamesRepository: UsedUsernamesRepository,
-
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const exist = await this.usersRepository.countBy({
@@ -55,8 +55,7 @@ export default class extends Endpoint { // eslint-
const exist2 = await this.usedUsernamesRepository.countBy({ username: ps.username.toLowerCase() });
- const meta = await this.metaService.fetch();
- const isPreserved = meta.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
+ const isPreserved = this.serverSettings.preservedUsernames.map(x => x.toLowerCase()).includes(ps.username.toLowerCase());
return {
available: exist === 0 && exist2 === 0 && !isPreserved,
diff --git a/packages/backend/src/server/api/endpoints/users/notes.ts b/packages/backend/src/server/api/endpoints/users/notes.ts
index 89047b5d7147..1f032064896c 100644
--- a/packages/backend/src/server/api/endpoints/users/notes.ts
+++ b/packages/backend/src/server/api/endpoints/users/notes.ts
@@ -5,14 +5,13 @@
import { Brackets } from 'typeorm';
import { Inject, Injectable } from '@nestjs/common';
-import type { NotesRepository } from '@/models/_.js';
+import type { MiMeta, NotesRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { DI } from '@/di-symbols.js';
import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js';
import { QueryService } from '@/core/QueryService.js';
-import { MetaService } from '@/core/MetaService.js';
import { MiLocalUser } from '@/models/User.js';
import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js';
import { FanoutTimelineName } from '@/core/FanoutTimelineService.js';
@@ -68,6 +67,9 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.meta)
+ private serverSettings: MiMeta,
+
@Inject(DI.notesRepository)
private notesRepository: NotesRepository,
@@ -76,7 +78,6 @@ export default class extends Endpoint { // eslint-
private cacheService: CacheService,
private idService: IdService,
private fanoutTimelineEndpointService: FanoutTimelineEndpointService,
- private metaService: MetaService,
) {
super(meta, paramDef, async (ps, me) => {
const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null);
@@ -84,8 +85,6 @@ export default class extends Endpoint { // eslint-
const isSelf = me && (me.id === ps.userId);
const includeSensitiveChannel = ps.includeSensitiveChannel ?? isSelf ?? false;
- const serverSettings = await this.metaService.fetch();
-
if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles);
// early return if me is blocked by requesting user
@@ -96,7 +95,7 @@ export default class extends Endpoint { // eslint-
}
}
- if (!serverSettings.enableFanoutTimeline) {
+ if (!this.serverSettings.enableFanoutTimeline) {
const timeline = await this.getFromDb({
untilId,
sinceId,
diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index f55790b6363b..dd7bb7823efe 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
import { Inject, Injectable } from '@nestjs/common';
import { createBullBoard } from '@bull-board/api';
import { BullMQAdapter } from '@bull-board/api/bullMQAdapter.js';
-import { FastifyAdapter } from '@bull-board/fastify';
+import { FastifyAdapter as BullBoardFastifyAdapter } from '@bull-board/fastify';
import ms from 'ms';
import sharp from 'sharp';
import pug from 'pug';
@@ -24,7 +24,6 @@ import type { Config } from '@/config.js';
import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js';
-import { MetaService } from '@/core/MetaService.js';
import type {
DbQueue,
DeliverQueue,
@@ -61,7 +60,8 @@ const staticAssets = `${_dirname}/../../../assets/`;
const clientAssets = `${_dirname}/../../../../frontend/assets/`;
const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
-const viteOut = `${_dirname}/../../../../../built/_vite_/`;
+const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
+const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
const tarball = `${_dirname}/../../../../../built/tarball/`;
@Injectable()
@@ -72,6 +72,9 @@ export class ClientServerService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -108,7 +111,6 @@ export class ClientServerService {
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService,
- private metaService: MetaService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
private roleService: RoleService,
@@ -128,32 +130,30 @@ export class ClientServerService {
@bindThis
private async manifestHandler(reply: FastifyReply) {
- const instance = await this.metaService.fetch(true);
-
let manifest = {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'short_name': instance.shortName || instance.name || this.config.host,
+ 'short_name': this.meta.shortName || this.meta.name || this.config.host,
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'name': instance.name || this.config.host,
+ 'name': this.meta.name || this.config.host,
'start_url': '/',
'display': 'standalone',
'background_color': '#313a42',
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'theme_color': instance.themeColor || '#86b300',
+ 'theme_color': this.meta.themeColor || '#86b300',
'icons': [{
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'src': instance.app192IconUrl || '/static-assets/icons/192.png',
+ 'src': this.meta.app192IconUrl || '/static-assets/icons/192.png',
'sizes': '192x192',
'type': 'image/png',
'purpose': 'maskable',
}, {
// 空文字列の場合右辺を使いたいため
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- 'src': instance.app512IconUrl || '/static-assets/icons/512.png',
+ 'src': this.meta.app512IconUrl || '/static-assets/icons/512.png',
'sizes': '512x512',
'type': 'image/png',
'purpose': 'maskable',
@@ -177,7 +177,7 @@ export class ClientServerService {
manifest = {
...manifest,
- ...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride),
+ ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride),
};
reply.header('Cache-Control', 'max-age=300');
@@ -239,7 +239,7 @@ export class ClientServerService {
}
});
- const serverAdapter = new FastifyAdapter();
+ const bullBoardServerAdapter = new BullBoardFastifyAdapter();
createBullBoard({
queues: [
@@ -252,11 +252,11 @@ export class ClientServerService {
this.userWebhookDeliverQueue,
this.systemWebhookDeliverQueue,
].map(q => new BullMQAdapter(q)),
- serverAdapter,
+ serverAdapter: bullBoardServerAdapter,
});
- serverAdapter.setBasePath(bullBoardPath);
- (fastify.register as any)(serverAdapter.registerPlugin(), { prefix: bullBoardPath });
+ bullBoardServerAdapter.setBasePath(bullBoardPath);
+ (fastify.register as any)(bullBoardServerAdapter.registerPlugin(), { prefix: bullBoardPath });
//#endregion
fastify.register(fastifyView, {
@@ -277,15 +277,22 @@ export class ClientServerService {
});
//#region vite assets
- if (this.config.clientManifestExists) {
+ if (this.config.frontendEmbedManifestExists) {
fastify.register((fastify, options, done) => {
fastify.register(fastifyStatic, {
- root: viteOut,
+ root: frontendViteOut,
prefix: '/vite/',
maxAge: ms('30 days'),
immutable: true,
decorateReply: false,
});
+ fastify.register(fastifyStatic, {
+ root: frontendEmbedViteOut,
+ prefix: '/embed_vite/',
+ maxAge: ms('30 days'),
+ immutable: true,
+ decorateReply: false,
+ });
fastify.addHook('onRequest', handleRequestRedirectToOmitSearch);
done();
});
@@ -296,6 +303,13 @@ export class ClientServerService {
prefix: '/vite',
rewritePrefix: '/vite',
});
+
+ const embedPort = (process.env.EMBED_VITE_PORT ?? '5174');
+ fastify.register(fastifyProxy, {
+ upstream: 'http://localhost:' + embedPort,
+ prefix: '/embed_vite',
+ rewritePrefix: '/embed_vite',
+ });
}
//#endregion
@@ -425,15 +439,20 @@ export class ClientServerService {
// Manifest
fastify.get('/manifest.json', async (request, reply) => await this.manifestHandler(reply));
+ // Embed Javascript
+ fastify.get('/embed.js', async (request, reply) => {
+ return await reply.sendFile('/embed.js', staticAssets, {
+ maxAge: ms('1 day'),
+ });
+ });
+
fastify.get('/robots.txt', async (request, reply) => {
return await reply.sendFile('/robots.txt', staticAssets);
});
// OpenSearch XML
fastify.get('/opensearch.xml', async (request, reply) => {
- const meta = await this.metaService.fetch();
-
- const name = meta.name ?? 'Misskey';
+ const name = this.meta.name ?? 'Misskey';
let content = '';
content += '';
content += `${name}`;
@@ -450,14 +469,13 @@ export class ClientServerService {
//#endregion
const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => {
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=30');
return await reply.view('base', {
- img: meta.bannerUrl,
+ img: this.meta.bannerUrl,
url: this.config.url,
- title: meta.name ?? 'Misskey',
- desc: meta.description,
- ...await this.generateCommonPugData(meta),
+ title: this.meta.name ?? 'Misskey',
+ desc: this.meta.description,
+ ...await this.generateCommonPugData(this.meta),
...data,
});
};
@@ -535,7 +553,6 @@ export class ClientServerService {
if (user != null) {
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
- const meta = await this.metaService.fetch();
const me = profile.fields
? profile.fields
.filter(filed => filed.value != null && filed.value.match(/^https?:/))
@@ -551,7 +568,7 @@ export class ClientServerService {
user, profile, me,
avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user),
sub: request.params.sub,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
// リモートユーザーなので
@@ -589,7 +606,6 @@ export class ClientServerService {
if (note) {
const _note = await this.noteEntityService.pack(note);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -601,7 +617,7 @@ export class ClientServerService {
avatarUrl: _note.user.avatarUrl,
// TODO: Let locale changeable by instance setting
summary: getNoteSummary(_note),
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -626,7 +642,6 @@ export class ClientServerService {
if (page) {
const _page = await this.pageEntityService.pack(page);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId });
- const meta = await this.metaService.fetch();
if (['public'].includes(page.visibility)) {
reply.header('Cache-Control', 'public, max-age=15');
} else {
@@ -640,7 +655,7 @@ export class ClientServerService {
page: _page,
profile,
avatarUrl: _page.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -656,7 +671,6 @@ export class ClientServerService {
if (flash) {
const _flash = await this.flashEntityService.pack(flash);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -666,7 +680,7 @@ export class ClientServerService {
flash: _flash,
profile,
avatarUrl: _flash.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -682,7 +696,6 @@ export class ClientServerService {
if (clip && clip.isPublic) {
const _clip = await this.clipEntityService.pack(clip);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -692,7 +705,7 @@ export class ClientServerService {
clip: _clip,
profile,
avatarUrl: _clip.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -706,7 +719,6 @@ export class ClientServerService {
if (post) {
const _post = await this.galleryPostEntityService.pack(post);
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId });
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
if (profile.preventAiLearning) {
reply.header('X-Robots-Tag', 'noimageai');
@@ -716,7 +728,7 @@ export class ClientServerService {
post: _post,
profile,
avatarUrl: _post.user.avatarUrl,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -731,11 +743,10 @@ export class ClientServerService {
if (channel) {
const _channel = await this.channelEntityService.pack(channel);
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=15');
return await reply.view('channel', {
channel: _channel,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -750,11 +761,10 @@ export class ClientServerService {
if (game) {
const _game = await this.reversiGameEntityService.packDetail(game);
- const meta = await this.metaService.fetch();
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('reversi-game', {
game: _game,
- ...await this.generateCommonPugData(meta),
+ ...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
@@ -762,7 +772,7 @@ export class ClientServerService {
});
//#endregion
- //region noindex pages
+ //#region noindex pages
// Tags
fastify.get<{ Params: { clip: string; } }>('/tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
@@ -772,21 +782,97 @@ export class ClientServerService {
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
- //endregion
+ //#endregion
- fastify.get('/_info_card_', async (request, reply) => {
- const meta = await this.metaService.fetch(true);
+ //#region embed pages
+ fastify.get<{ Params: { user: string; } }>('/embed/user-timeline/:user', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+
+ const user = await this.usersRepository.findOneBy({
+ id: request.params.user,
+ });
+
+ if (user == null) return;
+ if (user.host != null) return;
+
+ const _user = await this.userEntityService.pack(user);
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Misskey',
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
+ user: _user,
+ }),
+ });
+ });
+
+ fastify.get<{ Params: { note: string; } }>('/embed/notes/:note', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+ const note = await this.notesRepository.findOneBy({
+ id: request.params.note,
+ });
+
+ if (note == null) return;
+ if (note.visibility !== 'public') return;
+ if (note.userHost != null) return;
+
+ const _note = await this.noteEntityService.pack(note, null, { detail: true });
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Misskey',
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
+ note: _note,
+ }),
+ });
+ });
+
+ fastify.get<{ Params: { clip: string; } }>('/embed/clips/:clip', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+
+ const clip = await this.clipsRepository.findOneBy({
+ id: request.params.clip,
+ });
+
+ if (clip == null) return;
+
+ const _clip = await this.clipEntityService.pack(clip);
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Misskey',
+ ...await this.generateCommonPugData(this.meta),
+ embedCtx: htmlSafeJsonStringify({
+ clip: _clip,
+ }),
+ });
+ });
+
+ fastify.get('/embed/*', async (request, reply) => {
+ reply.removeHeader('X-Frame-Options');
+
+ reply.header('Cache-Control', 'public, max-age=3600');
+ return await reply.view('base-embed', {
+ title: this.meta.name ?? 'Misskey',
+ ...await this.generateCommonPugData(this.meta),
+ });
+ });
+
+ fastify.get('/_info_card_', async (request, reply) => {
reply.removeHeader('X-Frame-Options');
return await reply.view('info-card', {
version: this.config.version,
host: this.config.host,
- meta: meta,
+ meta: this.meta,
originalUsersCount: await this.usersRepository.countBy({ host: IsNull() }),
originalNotesCount: await this.notesRepository.countBy({ userHost: IsNull() }),
});
});
+ //#endregion
fastify.get('/bios', async (request, reply) => {
return await reply.view('bios', {
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index 8f8f08a3051b..5d493c2c46a3 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly';
import { SummalyResult } from '@misskey-dev/summaly/built/summary.js';
import { DI } from '@/di-symbols.js';
import type { Config } from '@/config.js';
-import { MetaService } from '@/core/MetaService.js';
import { HttpRequestService } from '@/core/HttpRequestService.js';
import type Logger from '@/logger.js';
import { query } from '@/misc/prelude/url.js';
@@ -26,7 +25,9 @@ export class UrlPreviewService {
@Inject(DI.config)
private config: Config,
- private metaService: MetaService,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
@@ -62,9 +63,7 @@ export class UrlPreviewService {
return;
}
- const meta = await this.metaService.fetch();
-
- if (!meta.urlPreviewEnabled) {
+ if (!this.meta.urlPreviewEnabled) {
reply.code(403);
return {
error: new ApiError({
@@ -75,14 +74,14 @@ export class UrlPreviewService {
};
}
- this.logger.info(meta.urlPreviewSummaryProxyUrl
+ this.logger.info(this.meta.urlPreviewSummaryProxyUrl
? `(Proxy) Getting preview of ${url}@${lang} ...`
: `Getting preview of ${url}@${lang} ...`);
try {
- const summary = meta.urlPreviewSummaryProxyUrl
- ? await this.fetchSummaryFromProxy(url, meta, lang)
- : await this.fetchSummary(url, meta, lang);
+ const summary = this.meta.urlPreviewSummaryProxyUrl
+ ? await this.fetchSummaryFromProxy(url, this.meta, lang)
+ : await this.fetchSummary(url, this.meta, lang);
this.logger.succ(`Got preview of ${url}: ${summary.title}`);
diff --git a/packages/backend/src/server/web/boot.embed.js b/packages/backend/src/server/web/boot.embed.js
new file mode 100644
index 000000000000..48d1cd262bdc
--- /dev/null
+++ b/packages/backend/src/server/web/boot.embed.js
@@ -0,0 +1,219 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+'use strict';
+
+// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
+(async () => {
+ window.onerror = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED');
+ };
+ window.onunhandledrejection = (e) => {
+ console.error(e);
+ renderError('SOMETHING_HAPPENED_IN_PROMISE');
+ };
+
+ let forceError = localStorage.getItem('forceError');
+ if (forceError != null) {
+ renderError('FORCED_ERROR', 'This error is forced by having forceError in local storage.');
+ return;
+ }
+
+ // パラメータに応じてsplashのスタイルを変更
+ const params = new URLSearchParams(location.search);
+ if (params.has('rounded') && params.get('rounded') === 'false') {
+ document.documentElement.classList.add('norounded');
+ }
+ if (params.has('border') && params.get('border') === 'false') {
+ document.documentElement.classList.add('noborder');
+ }
+
+ //#region Detect language & fetch translations
+ if (!localStorage.hasOwnProperty('locale')) {
+ const supportedLangs = LANGS;
+ let lang = localStorage.getItem('lang');
+ if (lang == null || !supportedLangs.includes(lang)) {
+ if (supportedLangs.includes(navigator.language)) {
+ lang = navigator.language;
+ } else {
+ lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
+
+ // Fallback
+ if (lang == null) lang = 'en-US';
+ }
+ }
+
+ const metaRes = await window.fetch('/api/meta', {
+ method: 'POST',
+ body: JSON.stringify({}),
+ credentials: 'omit',
+ cache: 'no-cache',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ if (metaRes.status !== 200) {
+ renderError('META_FETCH');
+ return;
+ }
+ const meta = await metaRes.json();
+ const v = meta.version;
+ if (v == null) {
+ renderError('META_FETCH_V');
+ return;
+ }
+
+ // for https://github.com/misskey-dev/misskey/issues/10202
+ if (lang == null || lang.toString == null || lang.toString() === 'null') {
+ console.error('invalid lang value detected!!!', typeof lang, lang);
+ lang = 'en-US';
+ }
+
+ const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
+ if (localRes.status === 200) {
+ localStorage.setItem('lang', lang);
+ localStorage.setItem('locale', await localRes.text());
+ localStorage.setItem('localeVersion', v);
+ } else {
+ renderError('LOCALE_FETCH');
+ return;
+ }
+ }
+ //#endregion
+
+ //#region Script
+ async function importAppScript() {
+ await import(`/embed_vite/${CLIENT_ENTRY}`)
+ .catch(async e => {
+ console.error(e);
+ renderError('APP_IMPORT');
+ });
+ }
+
+ // タイミングによっては、この時点でDOMの構築が済んでいる場合とそうでない場合とがある
+ if (document.readyState !== 'loading') {
+ importAppScript();
+ } else {
+ window.addEventListener('DOMContentLoaded', () => {
+ importAppScript();
+ });
+ }
+ //#endregion
+
+ async function addStyle(styleText) {
+ let css = document.createElement('style');
+ css.appendChild(document.createTextNode(styleText));
+ document.head.appendChild(css);
+ }
+
+ async function renderError(code) {
+ // Cannot set property 'innerHTML' of null を回避
+ if (document.readyState === 'loading') {
+ await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
+ }
+ document.body.innerHTML = `
+ 読み込みに失敗しました
+ Failed to initialize Misskey
+ Error Code: ${code}
+ `;
+ addStyle(`
+ #misskey_app,
+ #splash {
+ display: none !important;
+ }
+
+ html,
+ body {
+ margin: 0;
+ }
+
+ body {
+ position: relative;
+ color: #dee7e4;
+ font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ line-height: 1.35;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ min-height: 100vh;
+ margin: 0;
+ padding: 24px;
+ box-sizing: border-box;
+ overflow: hidden;
+
+ border-radius: var(--radius, 12px);
+ border: 1px solid rgba(231, 255, 251, 0.14);
+ }
+
+ body::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #192320;
+ border-radius: var(--radius, 12px);
+ z-index: -1;
+ }
+
+ html.embed.norounded body,
+ html.embed.norounded body::before {
+ border-radius: 0;
+ }
+
+ html.embed.noborder body {
+ border: none;
+ }
+
+ .icon {
+ max-width: 60px;
+ width: 100%;
+ height: auto;
+ margin-bottom: 20px;
+ color: #dec340;
+ }
+
+ .message {
+ text-align: center;
+ font-size: 20px;
+ font-weight: 700;
+ margin-bottom: 20px;
+ }
+
+ .submessage {
+ text-align: center;
+ font-size: 90%;
+ margin-bottom: 7.5px;
+ }
+
+ .submessage:last-of-type {
+ margin-bottom: 20px;
+ }
+
+ button {
+ padding: 7px 14px;
+ min-width: 100px;
+ font-weight: 700;
+ font-family: Hiragino Maru Gothic Pro, BIZ UDGothic, Roboto, HelveticaNeue, Arial, sans-serif;
+ line-height: 1.35;
+ border-radius: 99rem;
+ background-color: #b4e900;
+ color: #192320;
+ border: none;
+ cursor: pointer;
+ -webkit-tap-highlight-color: transparent;
+ }
+
+ button:hover {
+ background-color: #c6ff03;
+ }`);
+ }
+})();
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index 4275dc952715..7c6a5334291d 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -3,17 +3,6 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-/**
- * BOOT LOADER
- * サーバーからレスポンスされるHTMLに埋め込まれるスクリプトで、以下の役割を持ちます。
- * - 翻訳ファイルをフェッチする。
- * - バージョンに基づいて適切なメインスクリプトを読み込む。
- * - キャッシュされたコンパイル済みテーマを適用する。
- * - クライアントの設定値に基づいて対応するHTMLクラス等を設定する。
- * テーマをこの段階で設定するのは、メインスクリプトが読み込まれる間もテーマを適用したいためです。
- * 注: webpackは介さないため、このファイルではrequireやimportは使えません。
- */
-
'use strict';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
@@ -166,7 +155,7 @@
if (!errorsElement) {
document.body.innerHTML = `
-