};
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 0e244931d9..6f2c4ccf70 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -144,7 +144,7 @@ export class MiMeta {
nullable: true,
})
public sidebarLogoUrl: string | null;
-
+
@Column('varchar', {
length: 1024,
nullable: true,
@@ -627,6 +627,11 @@ export class MiMeta {
})
public perUserListTimelineCacheMax: number;
+ @Column('boolean', {
+ default: false,
+ })
+ public enableReactionsBuffering: boolean;
+
@Column('integer', {
default: 0,
})
@@ -682,4 +687,17 @@ export class MiMeta {
comment: 'An array of URL strings or regex that can be used to omit warnings about redirects to external sites. Separate them with spaces to specify AND, and enclose them with slashes to specify regular expressions. Each item is regarded as an OR.',
})
public trustedLinkUrlPatterns: string[];
+
+ @Column('varchar', {
+ length: 128,
+ default: 'all',
+ })
+ public federation: 'all' | 'specified' | 'none';
+
+ @Column('varchar', {
+ length: 1024,
+ array: true,
+ default: '{}',
+ })
+ public federationHosts: string[];
}
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index 4ed71a106c..c4f046c565 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 cbebd0102d..c7ecccf1cf 100644
--- a/packages/backend/src/models/User.ts
+++ b/packages/backend/src/models/User.ts
@@ -179,6 +179,11 @@ export class MiUser {
})
public tags: string[];
+ @Column('integer', {
+ default: 0,
+ })
+ public score: number;
+
@Column('boolean', {
default: false,
comment: 'Whether the User is suspended.',
@@ -341,6 +346,7 @@ 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 listenbrainzSchema = { type: "string", minLength: 1, maxLength: 128 } 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 40ea26f610..9b87130ce1 100644
--- a/packages/backend/src/models/UserProfile.ts
+++ b/packages/backend/src/models/UserProfile.ts
@@ -49,6 +49,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 2a727f86fd..8ef73fa143 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -8,6 +8,7 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction', 'edited'] as const;
+export type WebhookEventTypes = typeof webhookEventTypes[number];
@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 8915436b9e..15e87648ff 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -281,6 +281,10 @@ export const packedMetaLiteSchema = {
optional: false, nullable: false,
},
},
+ 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 3f31cc47ee..990e8957cf 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 504b9b122f..19ea6263c9 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 24b6c50e93..fa93b019bd 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -126,10 +126,6 @@ export const packedUserLiteSchema = {
nullable: false, optional: true,
default: false,
},
- isSilenced: {
- type: 'boolean',
- nullable: false, optional: false,
- },
noindex: {
type: 'boolean',
nullable: false, optional: false,
@@ -277,6 +273,10 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'boolean',
nullable: false, optional: false,
},
+ isSilenced: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
isSuspended: {
type: 'boolean',
nullable: false, optional: false,
@@ -412,6 +412,10 @@ export const packedUserDetailedNotMeOnlySchema = {
ref: 'RoleLite',
},
},
+ followedMessage: {
+ type: 'string',
+ nullable: true, optional: true,
+ },
memo: {
type: 'string',
nullable: true, optional: false,
@@ -484,6 +488,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 7daca687a1..7c6675b15d 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';
@@ -53,6 +54,7 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
ResyncChartsProcessorService,
CleanChartsProcessorService,
CheckExpiredMutingsProcessorService,
+ BakeBufferedReactionsProcessorService,
CleanProcessorService,
DeleteDriveFilesProcessorService,
ExportAccountDataProcessorService,
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index 7a6169bf9c..6841f6ec28 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -40,6 +40,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';
@@ -122,6 +123,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;
@@ -151,6 +153,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 0000000000..d49c99f694
--- /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 4076e9da90..9590a4fe71 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 88c4ea29c0..b3111865ad 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 6ec3c18786..ecc439db69 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 01eab26e96..583ddbb745 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 45087927a5..14d32e78b3 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 7bb626dd31..b81feece01 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 1cc80e66d7..903f962515 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 243b74f2c2..f9867ade29 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 c7611012d7..9e2b678219 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 ee87cff5d3..c483d79854 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 04ad74ee01..17ba71df3d 100644
--- a/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
+++ b/packages/backend/src/queue/processors/ImportCustomEmojisProcessorService.ts
@@ -88,23 +88,30 @@ export class ImportCustomEmojisProcessorService {
await this.emojisRepository.delete({
name: nameNfc,
});
- const driveFile = await this.driveService.addFile({
- user: null,
- path: emojiPath,
- name: record.fileName,
- force: true,
- });
- await this.customEmojiService.add({
- name: nameNfc,
- category: emojiInfo.category?.normalize('NFC'),
- host: null,
- aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')),
- 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: nameNfc,
+ category: emojiInfo.category?.normalize('NFC'),
+ host: null,
+ aliases: emojiInfo.aliases?.map((a: string) => a.normalize('NFC')),
+ 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 641b8b8607..8b3d2ebb50 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}`;
}
@@ -169,9 +179,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 {
@@ -190,11 +199,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);
@@ -202,7 +209,7 @@ export class InboxProcessorService {
this.apRequestChart.inbox();
this.federationChart.inbox(i.host);
- if (meta.enableChartsForFederatedInstances) {
+ if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host);
}
});
@@ -230,4 +237,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 65a8218174..19f082c61c 100644
--- a/packages/backend/src/server/FileServerService.ts
+++ b/packages/backend/src/server/FileServerService.ts
@@ -80,7 +80,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();
});
@@ -145,12 +145,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);
@@ -165,7 +165,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);
}
}
@@ -312,8 +312,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 2c3ed85925..5980609f02 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 39c8f67b8e..216e6b4fb8 100644
--- a/packages/backend/src/server/ServerModule.ts
+++ b/packages/backend/src/server/ServerModule.ts
@@ -49,6 +49,7 @@ import { MastodonApiServerService } from './api/mastodon/MastodonApiServerServic
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: [
@@ -74,6 +75,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 30c133d9ec..43a2a3a2b0 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';
@@ -45,6 +44,9 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -54,7 +56,6 @@ export class ServerService implements OnApplicationShutdown {
@Inject(DI.emojisRepository)
private emojisRepository: EmojisRepository,
- private metaService: MetaService,
private userEntityService: UserEntityService,
private apiServerService: ApiServerService,
private openApiServerService: OpenApiServerService,
@@ -167,8 +168,8 @@ export class ServerService implements OnApplicationShutdown {
}
return await reply.redirect(
- 301,
url.toString(),
+ 301,
);
});
@@ -195,7 +196,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 2307e172d5..016db6ac19 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,
@@ -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;
if (!ip) {
this.logger.warn(`user ${user.id} has a null IP address; please check your network configuration.`);
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index 4a5935f930..709a044601 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 c90a23d7b5..d1df57190a 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -97,6 +97,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';
@@ -494,6 +496,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 };
@@ -663,6 +666,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 };
@@ -895,6 +899,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,
@@ -1064,6 +1069,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,
@@ -1290,6 +1296,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,
@@ -1459,6 +1466,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 0000000000..9ba23c54e2
--- /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 f89c3954f8..c11cd4c448 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -8,9 +8,8 @@ import { Inject, Injectable } from '@nestjs/common';
import * as argon2 from 'argon2';
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';
@@ -31,6 +30,9 @@ export class SignupApiService {
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -48,7 +50,6 @@ export class SignupApiService {
private userEntityService: UserEntityService,
private idService: IdService,
- private metaService: MetaService,
private captchaService: CaptchaService,
private signupService: SignupService,
private signinService: SigninService,
@@ -77,31 +78,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);
});
}
@@ -114,7 +113,7 @@ export class SignupApiService {
const reason = body['reason'];
const emailAddress = body['emailAddress'];
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
if (emailAddress == null || typeof emailAddress !== 'string') {
reply.code(400);
return;
@@ -136,7 +135,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;
@@ -157,7 +156,7 @@ export class SignupApiService {
}
// メアド認証が有効の場合
- if (instance.emailRequiredForSignup) {
+ if (this.meta.emailRequiredForSignup) {
// メアド認証済みならエラー
if (ticket.usedBy) {
reply.code(400);
@@ -175,7 +174,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');
}
@@ -185,7 +184,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 e93e57f907..3987464fcf 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -103,6 +103,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';
@@ -272,6 +273,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';
@@ -498,6 +500,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],
@@ -667,6 +670,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 29c165f87a..21116ba402 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -400,6 +400,10 @@ export const meta = {
type: 'number',
optional: false, nullable: false,
},
+ enableReactionsBuffering: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
notesPerOneAd: {
type: 'number',
optional: false, nullable: false,
@@ -534,6 +538,18 @@ export const meta = {
optional: false, nullable: false,
},
},
+ federation: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ federationHosts: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
},
},
} as const;
@@ -669,6 +685,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,
@@ -678,6 +695,8 @@ export default class extends Endpoint { // eslint-
urlPreviewUserAgent: instance.urlPreviewUserAgent,
urlPreviewSummaryProxyUrl: instance.urlPreviewSummaryProxyUrl,
trustedLinkUrlPatterns: instance.trustedLinkUrlPatterns,
+ federation: instance.federation,
+ federationHosts: instance.federationHosts,
};
});
}
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 7a3410ffa7..f3e440b4cb 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 305ae1af1d..e7589cba81 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 2ba064b9dd..669bffe2dc 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-user.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-user.ts
@@ -36,6 +36,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ followedMessage: {
+ type: 'string',
+ optional: false, nullable: true,
+ },
autoAcceptFollowed: {
type: 'boolean',
optional: false, nullable: false,
@@ -237,6 +241,7 @@ export default class extends Endpoint { // eslint-
emailVerified: profile.emailVerified,
approved: user.approved,
signupReason: user.signupReason,
+ 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 0000000000..fb2ddf4b44
--- /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 cbde554428..1a55dec322 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -151,6 +151,7 @@ export const paramDef = {
perRemoteUserUserTimelineCacheMax: { type: 'integer' },
perUserHomeTimelineCacheMax: { type: 'integer' },
perUserListTimelineCacheMax: { type: 'integer' },
+ enableReactionsBuffering: { type: 'boolean' },
notesPerOneAd: { type: 'integer' },
silencedHosts: {
type: 'array',
@@ -181,6 +182,16 @@ export const paramDef = {
type: 'string',
},
},
+ federation: {
+ type: 'string',
+ enum: ['all', 'none', 'specified'],
+ },
+ federationHosts: {
+ type: 'array',
+ items: {
+ type: 'string',
+ },
+ },
},
required: [],
} as const;
@@ -636,6 +647,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;
}
@@ -674,6 +689,14 @@ export default class extends Endpoint { // eslint-
set.trustedLinkUrlPatterns = ps.trustedLinkUrlPatterns.filter(Boolean);
}
+ if (ps.federation !== undefined) {
+ set.federation = ps.federation;
+ }
+
+ if (Array.isArray(ps.federationHosts)) {
+ set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+ }
+
const before = await this.metaService.fetch(true);
await this.metaService.update(set);
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 577b9e1b1f..e0c8ddcc84 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 0c30bca9e0..10f26b1912 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 ca6789a464..a877d1ce0d 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,10 +110,7 @@ export default class extends Endpoint { // eslint-
*/
@bindThis
private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> {
- // ブロックしてたら中断
- const host = this.utilityService.extractDbHost(uri);
- const fetchedMeta = await this.metaService.fetch();
- if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, host)) return null;
+ if (!this.utilityService.isFederationAllowedUri(uri)) return null;
let local = await this.mergePack(me, ...await Promise.all([
this.apDbResolverService.getUserFromApId(uri),
@@ -123,6 +118,8 @@ export default class extends Endpoint { // eslint-
]));
if (local != null) return local;
+ const host = this.utilityService.extractDbHost(uri);
+
// local object, not found in db? fail
if (this.utilityService.isSelfHost(host)) return null;
diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts
index 9369481649..06130464a9 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';
@@ -65,6 +63,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,
@@ -75,16 +76,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,
});
@@ -95,7 +92,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, withFiles: ps.withFiles, withRenotes: ps.withRenotes }, me), me);
}
diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts
index 7e9b0fa0e1..eb45e29f9e 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 4670392025..b86059b5e7 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 9c17f93ab2..74eb4dded7 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 bc46163e3d..bdf6c065e8 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 2606108539..d7bb6bcd22 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 d5e824df27..e03192d8c6 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 0f5800404e..76b285bb7e 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 bacdd5c88f..76ecfd082c 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 7332026d84..0be8bfb695 100644
--- a/packages/backend/src/server/api/endpoints/i/update-email.ts
+++ b/packages/backend/src/server/api/endpoints/i/update-email.ts
@@ -8,7 +8,7 @@ import ms from 'ms';
//import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
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';
@@ -16,7 +16,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 = {
@@ -71,10 +70,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,
@@ -106,7 +107,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 f9b8061249..026ad614d8 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, listenbrainzSchema, descriptionSchema, locationSchema, nameSchema } from '@/models/User.js';
+import { birthdaySchema, listenbrainzSchema, 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';
@@ -146,6 +145,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 },
listenbrainz: { ...listenbrainzSchema, nullable: true },
@@ -284,6 +284,7 @@ export default class extends Endpoint { // eslint-
}
}
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;
@@ -400,7 +401,7 @@ export default class extends Endpoint { // eslint-
updates.backgroundUrl = null;
updates.backgroundBlurhash = null;
}
-
+
if (ps.avatarDecorations) {
policies ??= await this.roleService.getUserPolicies(user.id);
const decorations = await this.avatarDecorationService.getAll(true);
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 9eb7f5b3a0..6e84603f7a 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 fe07afb2d0..394c178f2a 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 5ddb79caf2..4a0c09ff0c 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 0000000000..2bf6df9ce2
--- /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 a4c2e28129..412491afaa 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 fdc9a77956..75be7b9888 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';
@@ -75,6 +74,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,
@@ -88,7 +90,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) => {
@@ -102,9 +103,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,
@@ -158,7 +157,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,
excludeBots: !ps.withBots,
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 5c3c7ae7d0..d4c806d7e2 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';
@@ -67,6 +65,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,
@@ -74,10 +75,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);
@@ -90,9 +89,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,
@@ -117,7 +114,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 1a14703e6e..d40a04c1b1 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 = {
@@ -57,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,
@@ -70,15 +72,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,
@@ -110,7 +109,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 d6ef655291..234248db5c 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,13 +87,11 @@ export default class extends Endpoint { // eslint-
return;
}
- const instance = await this.metaService.fetch();
-
- if (instance.deeplAuthKey == null && !instance.deeplFreeMode) {
+ if (this.serverSettings.deeplAuthKey == null && !this.serverSettings.deeplFreeMode) {
throw new ApiError(meta.errors.unavailable);
}
- if (instance.deeplFreeMode && !instance.deeplFreeInstance) {
+ if (this.serverSettings.deeplFreeMode && !this.serverSettings.deeplFreeInstance) {
throw new ApiError(meta.errors.unavailable);
}
@@ -98,11 +99,11 @@ export default class extends Endpoint { // eslint-
if (targetLang.includes('-')) targetLang = targetLang.split('-')[0];
const params = new URLSearchParams();
- if (instance.deeplAuthKey) params.append('auth_key', instance.deeplAuthKey);
+ if (this.serverSettings.deeplAuthKey) params.append('auth_key', this.serverSettings.deeplAuthKey);
params.append('text', note.text);
params.append('target_lang', targetLang);
- const endpoint = instance.deeplFreeMode && instance.deeplFreeInstance ? instance.deeplFreeInstance : instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate';
+ const endpoint = this.serverSettings.deeplFreeMode && this.serverSettings.deeplFreeInstance ? this.serverSettings.deeplFreeInstance : 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',
@@ -112,7 +113,7 @@ export default class extends Endpoint { // eslint-
},
body: params.toString(),
});
- if (instance.deeplAuthKey) {
+ if (this.serverSettings.deeplAuthKey) {
const json = (await res.json()) as {
translations: {
detected_source_language: string;
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 43877e61ef..6c7185c9eb 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 15832ef7f8..5b0b656c63 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 c13802eb06..8301c85f2e 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 a9a33149f9..fd76df2d3c 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 affb0996f1..4944be9b05 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 cc76c12f1d..7fc11ba369 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';
@@ -67,6 +66,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,
@@ -75,15 +77,12 @@ 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);
const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null);
const isSelf = me && (me.id === ps.userId);
- 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
@@ -94,7 +93,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 e20707977f..c61ead3de0 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',
@@ -179,7 +179,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');
@@ -242,7 +242,7 @@ export class ClientServerService {
}
});
- const serverAdapter = new FastifyAdapter();
+ const bullBoardServerAdapter = new BullBoardFastifyAdapter();
createBullBoard({
queues: [
@@ -255,11 +255,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, {
@@ -280,15 +280,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();
});
@@ -299,6 +306,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
@@ -443,15 +457,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 ?? 'Sharkey';
+ const name = this.meta.name ?? 'Sharkey';
let content = '';
content += '';
content += `${name}`;
@@ -468,14 +487,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,
});
};
@@ -553,7 +571,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?:/))
@@ -569,7 +586,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 {
// リモートユーザーなので
@@ -607,7 +624,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');
@@ -619,7 +635,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);
@@ -644,7 +660,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 {
@@ -658,7 +673,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);
@@ -674,7 +689,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');
@@ -684,7 +698,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);
@@ -700,7 +714,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');
@@ -710,7 +723,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);
@@ -724,7 +737,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');
@@ -734,7 +746,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);
@@ -749,11 +761,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);
@@ -768,11 +779,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);
@@ -780,7 +790,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 });
@@ -790,21 +800,97 @@ export class ClientServerService {
fastify.get<{ Params: { clip: string; } }>('/user-tags/:tag', async (request, reply) => {
return await renderBase(reply, { noindex: true });
});
- //endregion
+ //#endregion
+
+ //#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) => {
- const meta = await this.metaService.fetch(true);
-
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 ef804b5bfd..981fbb4353 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';
@@ -32,7 +31,9 @@ export class UrlPreviewService {
@Inject(DI.redis)
private redisClient: Redis.Redis,
- private metaService: MetaService,
+ @Inject(DI.meta)
+ private meta: MiMeta,
+
private httpRequestService: HttpRequestService,
private loggerService: LoggerService,
) {
@@ -75,9 +76,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({
@@ -98,14 +97,14 @@ export class UrlPreviewService {
return cached;
}
- this.logger.info(meta.urlPreviewSummaryProxyUrl
+ this.logger.info(this.meta.urlPreviewSummaryProxyUrl
? `(Proxy) Getting preview of ${key} ...`
: `Getting preview of ${key} ...`);
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 0000000000..48d1cd262b
--- /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 38e37ce093..5bef0f1407 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';
// ブロックの中に入れないと、定義した変数がブラウザのグローバルスコープに登録されてしまい邪魔なので
@@ -192,7 +181,7 @@
if (!errorsElement) {
document.body.innerHTML = `
-