From d429f810a9ea3fda9efed2ff51483d25a288ecc9 Mon Sep 17 00:00:00 2001 From: Ebise Lutica <7106976+EbiseLutica@users.noreply.github.com> Date: Thu, 13 Apr 2023 00:31:22 +0900 Subject: [PATCH 001/635] Update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41353c346b..df2265727d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,9 @@ ## 13.11.2 +### Note +- 13.11.0または13.11.1から13.11.2以降にアップデートする場合、Redisのカスタム絵文字のキャッシュを削除する必要があります(https://github.com/misskey-dev/misskey/issues/10502#issuecomment-1502790755 参照) + ### General - チャンネルの検索用ページの追加 From 02dfe0a3d56d2a21de6a96f7b6a3228a77619a8e Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 26 Feb 2024 20:49:40 +0000 Subject: [PATCH 002/635] =?UTF-8?q?1.=20ed25519=E3=82=AD=E3=83=BC=E3=83=9A?= =?UTF-8?q?=E3=82=A2=E3=82=92=E7=99=BA=E8=A1=8C=E3=83=BBPerson=E3=81=A8?= =?UTF-8?q?=E3=81=97=E3=81=A6=E5=85=AC=E9=96=8B=E9=8D=B5=E3=82=92=E9=80=81?= =?UTF-8?q?=E5=8F=97=E4=BF=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../migration/1708980134301-APMultipleKeys.js | 43 +++++++++++++++++ .../backend/src/core/GlobalEventService.ts | 1 + .../backend/src/core/UserKeypairService.ts | 45 +++++++++++++++++- .../core/activitypub/ApDbResolverService.ts | 6 +-- .../activitypub/ApDeliverManagerService.ts | 21 ++++++--- .../src/core/activitypub/ApRendererService.ts | 12 +++-- .../activitypub/models/ApPersonService.ts | 46 +++++++++++++++++-- packages/backend/src/core/activitypub/type.ts | 17 +++++-- packages/backend/src/misc/gen-key-pair.ts | 23 ++++++++-- packages/backend/src/models/UserKeypair.ts | 40 ++++++++++++++++ packages/backend/src/models/UserPublickey.ts | 14 +++--- .../src/server/ActivityPubServerService.ts | 2 +- .../backend/test/unit/misc/gen-key-pair.ts | 40 ++++++++++++++++ 13 files changed, 274 insertions(+), 36 deletions(-) create mode 100644 packages/backend/migration/1708980134301-APMultipleKeys.js create mode 100644 packages/backend/test/unit/misc/gen-key-pair.ts diff --git a/packages/backend/migration/1708980134301-APMultipleKeys.js b/packages/backend/migration/1708980134301-APMultipleKeys.js new file mode 100644 index 0000000000..d0687fa7fa --- /dev/null +++ b/packages/backend/migration/1708980134301-APMultipleKeys.js @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class APMultipleKeys1708980134301 { + name = 'APMultipleKeys1708980134301' + + async up(queryRunner) { + await queryRunner.query(`DROP INDEX "public"."IDX_171e64971c780ebd23fae140bb"`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PublicKey" character varying(128)`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PrivateKey" character varying(128)`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PublicKeySignature" character varying(720)`); + await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519SignatureAlgorithm" character varying(32)`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_0db6a5fdb992323449edc8ee421" PRIMARY KEY ("userId", "keyId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_0db6a5fdb992323449edc8ee421"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_171e64971c780ebd23fae140bba" PRIMARY KEY ("keyId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`); + await queryRunner.query(`CREATE INDEX "IDX_10c146e4b39b443ede016f6736" ON "user_publickey" ("userId") `); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`DROP INDEX "public"."IDX_10c146e4b39b443ede016f6736"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_171e64971c780ebd23fae140bba"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_0db6a5fdb992323449edc8ee421" PRIMARY KEY ("userId", "keyId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_0db6a5fdb992323449edc8ee421"`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_10c146e4b39b443ede016f6736d" PRIMARY KEY ("userId")`); + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "FK_10c146e4b39b443ede016f6736d" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" TYPE "public"."user_profile_followersVisibility_enum_old" USING "followersVisibility"::"text"::"public"."user_profile_followersVisibility_enum_old"`); + await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" SET DEFAULT 'public'`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519SignatureAlgorithm"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PublicKeySignature"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PrivateKey"`); + await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PublicKey"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_171e64971c780ebd23fae140bb" ON "user_publickey" ("keyId") `); + } +} diff --git a/packages/backend/src/core/GlobalEventService.ts b/packages/backend/src/core/GlobalEventService.ts index 7c1b34da05..331df4315d 100644 --- a/packages/backend/src/core/GlobalEventService.ts +++ b/packages/backend/src/core/GlobalEventService.ts @@ -240,6 +240,7 @@ export interface InternalEventTypes { unmute: { muterId: MiUser['id']; muteeId: MiUser['id']; }; userListMemberAdded: { userListId: MiUserList['id']; memberId: MiUser['id']; }; userListMemberRemoved: { userListId: MiUserList['id']; memberId: MiUser['id']; }; + userKeypairUpdated: { userId: MiUser['id']; }; } // name/messages(spec) pairs dictionary diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 51ac99179a..7946f410cf 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { sign } from 'node:crypto'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; @@ -11,6 +12,8 @@ import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; +import { ED25519_SIGN_ALGORITHM, genEd25519KeyPair } from '@/misc/gen-key-pair.js'; +import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { @@ -19,9 +22,12 @@ export class UserKeypairService implements OnApplicationShutdown { constructor( @Inject(DI.redis) private redisClient: Redis.Redis, - + @Inject(DI.redisForSub) + private redisForSub: Redis.Redis, @Inject(DI.userKeypairsRepository) private userKeypairsRepository: UserKeypairsRepository, + + private globalEventService: GlobalEventService, ) { this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h @@ -30,6 +36,8 @@ export class UserKeypairService implements OnApplicationShutdown { toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), }); + + this.redisForSub.on('message', this.onMessage); } @bindThis @@ -37,6 +45,41 @@ export class UserKeypairService implements OnApplicationShutdown { return await this.cache.fetch(userId); } + @bindThis + public async refresh(userId: MiUser['id']): Promise { + return await this.cache.refresh(userId); + } + + @bindThis + public async prepareEd25519KeyPair(userId: MiUser['id']): Promise { + await this.refresh(userId); + const keypair = await this.cache.fetch(userId); + if (keypair.ed25519PublicKey != null) return; + const ed25519 = await genEd25519KeyPair(); + const ed25519PublicKeySignature = sign(ED25519_SIGN_ALGORITHM, Buffer.from(ed25519.publicKey), keypair.privateKey).toString('base64'); + await this.userKeypairsRepository.update({ userId }, { + ed25519PublicKey: ed25519.publicKey, + ed25519PrivateKey: ed25519.privateKey, + ed25519PublicKeySignature, + ed25519SignatureAlgorithm: `rsa-${ED25519_SIGN_ALGORITHM}`, + }); + this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); + } + + @bindThis + private async onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'userKeypairUpdated': { + this.refresh(body.userId); + break; + } + } + } + } @bindThis public dispose(): void { this.cache.dispose(); diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index f6b70ead44..913070fdd4 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -36,7 +36,7 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService implements OnApplicationShutdown { private publicKeyCache: MemoryKVCache; - private publicKeyByUserIdCache: MemoryKVCache; + private publicKeyByUserIdCache: MemoryKVCache; constructor( @Inject(DI.config) @@ -55,7 +55,7 @@ export class ApDbResolverService implements OnApplicationShutdown { private apPersonService: ApPersonService, ) { this.publicKeyCache = new MemoryKVCache(Infinity); - this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); + this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); } @bindThis @@ -159,7 +159,7 @@ export class ApDbResolverService implements OnApplicationShutdown { const key = await this.publicKeyByUserIdCache.fetch( user.id, - () => this.userPublickeysRepository.findOneBy({ userId: user.id }), + () => this.userPublickeysRepository.find({ where: { userId: user.id } }), v => v != null, ); diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 5d07cd8e8f..27a9c11dab 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -9,10 +9,10 @@ import { DI } from '@/di-symbols.js'; import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; -import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; +import { UserKeypairService } from '../UserKeypairService.js'; interface IRecipe { type: string; @@ -40,14 +40,14 @@ class DeliverManager { /** * Constructor - * @param userEntityService + * @param userKeypairService * @param followingsRepository * @param queueService * @param actor Actor * @param activity Activity to deliver */ constructor( - private userEntityService: UserEntityService, + private userKeypairService: UserKeypairService, private followingsRepository: FollowingsRepository, private queueService: QueueService, @@ -105,6 +105,13 @@ class DeliverManager { */ @bindThis public async execute(): Promise { + //#region MIGRATION + /** + * ed25519の署名がなければ追加する + */ + await this.userKeypairService.prepareEd25519KeyPair(this.actor.id); + //#endregion + // The value flags whether it is shared or not. // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map(); @@ -154,7 +161,7 @@ export class ApDeliverManagerService { @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, - private userEntityService: UserEntityService, + private userKeypairService: UserKeypairService, private queueService: QueueService, ) { } @@ -167,7 +174,7 @@ export class ApDeliverManagerService { @bindThis public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise { const manager = new DeliverManager( - this.userEntityService, + this.userKeypairService, this.followingsRepository, this.queueService, actor, @@ -186,7 +193,7 @@ export class ApDeliverManagerService { @bindThis public async deliverToUser(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, to: MiRemoteUser): Promise { const manager = new DeliverManager( - this.userEntityService, + this.userKeypairService, this.followingsRepository, this.queueService, actor, @@ -199,7 +206,7 @@ export class ApDeliverManagerService { @bindThis public createDeliverManager(actor: { id: MiUser['id']; host: null; }, activity: IActivity | null): DeliverManager { return new DeliverManager( - this.userEntityService, + this.userKeypairService, this.followingsRepository, this.queueService, diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index d7fb977a99..66322f6870 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -250,15 +250,16 @@ export class ApRendererService { } @bindThis - public renderKey(user: MiLocalUser, key: MiUserKeypair, postfix?: string): IKey { + public renderKey(user: MiLocalUser, publicKey: string, postfix?: string, signature?: IKey['signature']): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', owner: this.userEntityService.genLocalUserUri(user.id), - publicKeyPem: createPublicKey(key.publicKey).export({ + publicKeyPem: createPublicKey(publicKey).export({ type: 'spki', format: 'pem', - }), + }) as string, + signature, }; } @@ -498,7 +499,10 @@ export class ApRendererService { tag, manuallyApprovesFollowers: user.isLocked, discoverable: user.isExplorable, - publicKey: this.renderKey(user, keypair, '#main-key'), + publicKey: this.renderKey(user, keypair.publicKey, '#main-key'), + additionalPublicKeys: [ + ...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key', { type: keypair.ed25519SignatureAlgorithm!, signatureValue: keypair.ed25519PublicKeySignature! })] : []), + ], isCat: user.isCat, attachment: attachment.length ? attachment : undefined, }; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 744b1ea683..c31aa03261 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -3,9 +3,10 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { verify } from 'crypto'; import { Inject, Injectable } from '@nestjs/common'; import promiseLimit from 'promise-limit'; -import { DataSource } from 'typeorm'; +import { DataSource, In, Not } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; @@ -357,10 +358,25 @@ export class ApPersonService implements OnModuleInit { if (person.publicKey) { await transactionalEntityManager.save(new MiUserPublickey({ - userId: user.id, keyId: person.publicKey.id, + userId: user.id, keyPem: person.publicKey.publicKeyPem, })); + + if (person.additionalPublicKeys) { + for (const key of person.additionalPublicKeys) { + if ( + key.signature && key.signature.type && key.signature.signatureValue && + verify(key.signature.type, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) + ) { + await transactionalEntityManager.save(new MiUserPublickey({ + keyId: key.id, + userId: user.id, + keyPem: key.publicKeyPem, + })); + } + } + } } }); } catch (e) { @@ -506,13 +522,35 @@ export class ApPersonService implements OnModuleInit { // Update user await this.usersRepository.update(exist.id, updates); + const availablePublicKeys = new Set(); if (person.publicKey) { - await this.userPublickeysRepository.update({ userId: exist.id }, { - keyId: person.publicKey.id, + await this.userPublickeysRepository.update({ keyId: person.publicKey.id }, { + userId: exist.id, keyPem: person.publicKey.publicKeyPem, }); + availablePublicKeys.add(person.publicKey.id); + + if (person.additionalPublicKeys) { + for (const key of person.additionalPublicKeys) { + if ( + key.signature && key.signature.type && key.signature.signatureValue && + verify(key.signature.type, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) + ) { + await this.userPublickeysRepository.update({ keyId: key.id }, { + userId: exist.id, + keyPem: key.publicKeyPem, + }); + availablePublicKeys.add(key.id); + } + } + } } + this.userPublickeysRepository.delete({ + keyId: Not(In(Array.from(availablePublicKeys))), + userId: exist.id, + }); + let _description: string | null = null; if (person._misskey_summary) { diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index b43dddad61..72d383442e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -168,10 +168,8 @@ export interface IActor extends IObject { discoverable?: boolean; inbox: string; sharedInbox?: string; // 後方互換性のため - publicKey?: { - id: string; - publicKeyPem: string; - }; + publicKey?: IKey; + additionalPublicKeys?: IKey[]; followers?: string | ICollection | IOrderedCollection; following?: string | ICollection | IOrderedCollection; featured?: string | IOrderedCollection; @@ -235,8 +233,17 @@ export const isEmoji = (object: IObject): object is IApEmoji => export interface IKey extends IObject { type: 'Key'; + id: string; owner: string; - publicKeyPem: string | Buffer; + publicKeyPem: string; + + /** + * Signature of publicKeyPem, signed by root privateKey (for additionalPublicKey) + */ + signature?: { + type: string; + signatureValue: string + }; } export interface IApDocument extends IObject { diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 02a303dc0a..7b2e84d991 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -8,7 +8,9 @@ import * as util from 'node:util'; const generateKeyPair = util.promisify(crypto.generateKeyPair); -export async function genRsaKeyPair(modulusLength = 2048) { +export const ED25519_SIGN_ALGORITHM = 'sha256'; + +export async function genRsaKeyPair(modulusLength = 4096) { return await generateKeyPair('rsa', { modulusLength, publicKeyEncoding: { @@ -24,9 +26,8 @@ export async function genRsaKeyPair(modulusLength = 2048) { }); } -export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'secp521r1' | 'curve25519' = 'prime256v1') { - return await generateKeyPair('ec', { - namedCurve, +export async function genEd25519KeyPair() { + return await generateKeyPair('ed25519', { publicKeyEncoding: { type: 'spki', format: 'pem', @@ -39,3 +40,17 @@ export async function genEcKeyPair(namedCurve: 'prime256v1' | 'secp384r1' | 'sec }, }); } + +export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { + const rsa = await genRsaKeyPair(rsaModulusLength); + const ed25519 = await genEd25519KeyPair(); + const ed25519PublicKeySignature = crypto.sign(ED25519_SIGN_ALGORITHM, Buffer.from(ed25519.publicKey), rsa.privateKey).toString('base64'); + return { + publicKey: rsa.publicKey, + privateKey: rsa.privateKey, + ed25519PublicKey: ed25519.publicKey, + ed25519PrivateKey: ed25519.privateKey, + ed25519PublicKeySignature, + ed25519SignatureAlgorithm: `rsa-${ED25519_SIGN_ALGORITHM}`, + }; +} diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts index f5252d126c..47d44af849 100644 --- a/packages/backend/src/models/UserKeypair.ts +++ b/packages/backend/src/models/UserKeypair.ts @@ -18,16 +18,56 @@ export class MiUserKeypair { @JoinColumn() public user: MiUser | null; + /** + * RSA public key + */ @Column('varchar', { length: 4096, }) public publicKey: string; + /** + * RSA private key + */ @Column('varchar', { length: 4096, }) public privateKey: string; + @Column('varchar', { + length: 128, + nullable: true, + default: null, + }) + public ed25519PublicKey: string | null; + + @Column('varchar', { + length: 128, + nullable: true, + default: null, + }) + public ed25519PrivateKey: string | null; + + /** + * Signature of ed25519PublicKey, signed by privateKey. (base64) + */ + @Column('varchar', { + length: 720, + nullable: true, + default: null, + }) + public ed25519PublicKeySignature: string | null; + + /** + * Signature algorithm of ed25519PublicKeySignature. + */ + @Column('varchar', { + length: 32, + nullable: true, + default: null, + }) + public ed25519SignatureAlgorithm: string | null; + constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/src/models/UserPublickey.ts b/packages/backend/src/models/UserPublickey.ts index 6bcd785304..0ecff2bcbe 100644 --- a/packages/backend/src/models/UserPublickey.ts +++ b/packages/backend/src/models/UserPublickey.ts @@ -9,7 +9,13 @@ import { MiUser } from './User.js'; @Entity('user_publickey') export class MiUserPublickey { - @PrimaryColumn(id()) + @PrimaryColumn('varchar', { + length: 256, + }) + public keyId: string; + + @Index() + @Column(id()) public userId: MiUser['id']; @OneToOne(type => MiUser, { @@ -18,12 +24,6 @@ export class MiUserPublickey { @JoinColumn() public user: MiUser | null; - @Index({ unique: true }) - @Column('varchar', { - length: 256, - }) - public keyId: string; - @Column('varchar', { length: 4096, }) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 60366dd5c2..5e6cb0a05a 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -640,7 +640,7 @@ export class ActivityPubServerService { if (this.userEntityService.isLocalUser(user)) { reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); - return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair))); + return (this.apRendererService.addContext(this.apRendererService.renderKey(user, keypair.publicKey))); } else { reply.code(400); return; diff --git a/packages/backend/test/unit/misc/gen-key-pair.ts b/packages/backend/test/unit/misc/gen-key-pair.ts new file mode 100644 index 0000000000..91c62f621d --- /dev/null +++ b/packages/backend/test/unit/misc/gen-key-pair.ts @@ -0,0 +1,40 @@ +import * as crypto from 'node:crypto'; +import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; + +describe(genRSAAndEd25519KeyPair, () => { + test('generates key pair', async () => { + const keyPair = await genRSAAndEd25519KeyPair(); + // 毎回違うキーペアが生成されることを確認するために2回生成して比較してみる + const keyPair2 = await genRSAAndEd25519KeyPair(); + console.log(Object.entries(keyPair).map(([k, v]) => `${k}: ${v.length}`).join('\n')); + console.log(Object.entries(keyPair).map(([k, v]) => `${k}\n${v}`).join('\n')); + + expect(keyPair.publicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/); + expect(keyPair.publicKey).toMatch(/-----END PUBLIC KEY-----\n$/); + expect(keyPair.publicKey).not.toBe(keyPair2.publicKey); + + const publicKeyObj = crypto.createPublicKey(keyPair.publicKey); + expect(publicKeyObj.asymmetricKeyType).toBe('rsa'); + + expect(keyPair.privateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/); + expect(keyPair.privateKey).toMatch(/-----END PRIVATE KEY-----\n$/); + expect(keyPair.privateKey).not.toBe(keyPair2.privateKey); + expect(keyPair.ed25519PublicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/); + expect(keyPair.ed25519PublicKey).toMatch(/-----END PUBLIC KEY-----\n$/); + expect(keyPair.ed25519PublicKey).not.toBe(keyPair2.ed25519PublicKey); + + const ed25519PublicKeyObj = crypto.createPublicKey(keyPair.ed25519PublicKey); + expect(ed25519PublicKeyObj.asymmetricKeyType).toBe('ed25519'); + + expect(keyPair.ed25519PrivateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/); + expect(keyPair.ed25519PrivateKey).toMatch(/-----END PRIVATE KEY-----\n$/); + expect(keyPair.ed25519PrivateKey).not.toBe(keyPair2.ed25519PrivateKey); + expect(keyPair.ed25519PublicKeySignature).toBe( + crypto.sign(keyPair.ed25519SignatureAlgorithm.split('-').pop(), Buffer.from(keyPair.ed25519PublicKey), keyPair.privateKey).toString('base64'), + ); + expect(crypto.verify(keyPair.ed25519SignatureAlgorithm, Buffer.from(keyPair.ed25519PublicKey), keyPair.publicKey, Buffer.from(keyPair.ed25519PublicKeySignature, 'base64'))).toBe(true); + expect(keyPair.ed25519PublicKeySignature).not.toBe(keyPair2.ed25519PublicKeySignature); + + //const imported = await webCrypto.subtle.importKey('spki', Buffer.from(keyPair.publicKey).buffer, { name: 'rsa-pss', hash: 'sha-256' }, false, ['verify']); + }); +}); From 18353973859d20c4be96d7b9ea91da35cb92cf52 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 26 Feb 2024 21:06:05 +0000 Subject: [PATCH 003/635] validate additionalPublicKeys --- .../core/activitypub/ApDbResolverService.ts | 2 +- .../activitypub/models/ApPersonService.ts | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 913070fdd4..6cce3951bd 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -152,7 +152,7 @@ export class ApDbResolverService implements OnApplicationShutdown { @bindThis public async getAuthUserFromApId(uri: string): Promise<{ user: MiRemoteUser; - key: MiUserPublickey | null; + key: MiUserPublickey[] | null; } | null> { const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; if (user.isDeleted) return null; diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index c31aa03261..4c1f79e1b1 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -194,6 +194,37 @@ export class ApPersonService implements OnModuleInit { } } + if (x.additionalPublicKeys) { + if (!x.publicKey) { + throw new Error('invalid Actor: additionalPublicKeys is set but publicKey is not'); + } + + if (!Array.isArray(x.additionalPublicKeys)) { + throw new Error('invalid Actor: additionalPublicKeys is not an array'); + } + + for (const key of x.additionalPublicKeys) { + if (typeof key.id !== 'string') { + throw new Error('invalid Actor: additionalPublicKeys.id is not a string'); + } + + const keyIdHost = this.punyHost(key.id); + if (keyIdHost !== expectHost) { + throw new Error('invalid Actor: additionalPublicKeys.id has different host'); + } + + if (!key.signature) { + throw new Error('invalid Actor: additionalPublicKeys.signature is not set'); + } + if (typeof key.signature.type !== 'string') { + throw new Error('invalid Actor: additionalPublicKeys.signature.type is not a string'); + } + if (typeof key.signature.signatureValue !== 'string') { + throw new Error('invalid Actor: additionalPublicKeys.signature.signatureValue is not a string'); + } + } + } + return x; } From 5b7b8503cd4f1b76b65d18b8f291e10db12143b3 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 26 Feb 2024 21:27:50 +0000 Subject: [PATCH 004/635] =?UTF-8?q?getAuthUserFromApId=E3=81=AFmain?= =?UTF-8?q?=E3=82=92=E9=81=B8=E3=81=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/activitypub/ApDbResolverService.ts | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 6cce3951bd..fe211157b9 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -152,17 +152,36 @@ export class ApDbResolverService implements OnApplicationShutdown { @bindThis public async getAuthUserFromApId(uri: string): Promise<{ user: MiRemoteUser; - key: MiUserPublickey[] | null; + key: MiUserPublickey | null; } | null> { const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; if (user.isDeleted) return null; - const key = await this.publicKeyByUserIdCache.fetch( + const keys = await this.publicKeyByUserIdCache.fetch( user.id, () => this.userPublickeysRepository.find({ where: { userId: user.id } }), v => v != null, ); + if (keys == null || keys.length === 8) return null; + + // 公開鍵は複数あるが、mainっぽいのを選ぶ + const key = keys.length === 1 ? + keys[0] : + keys.find(x => { + try { + const url = new URL(x.keyId); + if ( + url.hash.toLowerCase().includes('main') || + url.pathname.split('/').pop()?.toLowerCase().includes('main') + ) { + return true; + } + } catch { /* noop */ } + + return false; + }) ?? keys[0]; + return { user, key, From 00738b90c2e02d648422ac495859e169562af25c Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 26 Feb 2024 21:31:43 +0000 Subject: [PATCH 005/635] :v: --- .../src/core/activitypub/ApDbResolverService.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index fe211157b9..0cb3b78941 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -171,10 +171,12 @@ export class ApDbResolverService implements OnApplicationShutdown { keys.find(x => { try { const url = new URL(x.keyId); - if ( - url.hash.toLowerCase().includes('main') || - url.pathname.split('/').pop()?.toLowerCase().includes('main') - ) { + const path = url.pathname.split('/').pop()?.toLowerCase(); + if (url.hash) { + if (url.hash.toLowerCase().includes('main')) { + return true; + } + } else if (path?.includes('main') || path === 'publickey') { return true; } } catch { /* noop */ } From 172546f3efb0ef96812d988d18ac7f3eadff0be8 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 26 Feb 2024 21:44:33 +0000 Subject: [PATCH 006/635] fix --- .../src/core/CreateSystemUserService.ts | 7 +++---- packages/backend/src/core/SignupService.ts | 21 +++---------------- 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/packages/backend/src/core/CreateSystemUserService.ts b/packages/backend/src/core/CreateSystemUserService.ts index 6c5b0f6a36..60ddc9cde2 100644 --- a/packages/backend/src/core/CreateSystemUserService.ts +++ b/packages/backend/src/core/CreateSystemUserService.ts @@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull, DataSource } from 'typeorm'; -import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; +import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; import { MiUser } from '@/models/User.js'; import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; @@ -38,7 +38,7 @@ export class CreateSystemUserService { // Generate secret const secret = generateNativeUserToken(); - const keyPair = await genRsaKeyPair(); + const keyPair = await genRSAAndEd25519KeyPair(); let account!: MiUser; @@ -64,9 +64,8 @@ export class CreateSystemUserService { }).then(x => transactionalEntityManager.findOneByOrFail(MiUser, x.identifiers[0])); await transactionalEntityManager.insert(MiUserKeypair, { - publicKey: keyPair.publicKey, - privateKey: keyPair.privateKey, userId: account.id, + ...keyPair, }); await transactionalEntityManager.insert(MiUserProfile, { diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 5522ecd6cc..78a77d2adf 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -21,6 +21,7 @@ import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; import { MetaService } from '@/core/MetaService.js'; +import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; @Injectable() export class SignupService { @@ -93,22 +94,7 @@ export class SignupService { } } - const keyPair = await new Promise((res, rej) => - generateKeyPair('rsa', { - modulusLength: 2048, - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - cipher: undefined, - passphrase: undefined, - }, - }, (err, publicKey, privateKey) => - err ? rej(err) : res([publicKey, privateKey]), - )); + const keyPair = await genRSAAndEd25519KeyPair(); let account!: MiUser; @@ -131,9 +117,8 @@ export class SignupService { })); await transactionalEntityManager.save(new MiUserKeypair({ - publicKey: keyPair[0], - privateKey: keyPair[1], userId: account.id, + ...keyPair, })); await transactionalEntityManager.save(new MiUserProfile({ From 1d780ac010875314928fcba257d4ca34ade3ea15 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 01:41:34 +0000 Subject: [PATCH 007/635] signatureAlgorithm --- .../src/core/activitypub/ApRendererService.ts | 3 ++- .../src/core/activitypub/models/ApPersonService.ts | 12 ++++++------ packages/backend/src/core/activitypub/type.ts | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 66322f6870..66a15b2d18 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -501,7 +501,7 @@ export class ApRendererService { discoverable: user.isExplorable, publicKey: this.renderKey(user, keypair.publicKey, '#main-key'), additionalPublicKeys: [ - ...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key', { type: keypair.ed25519SignatureAlgorithm!, signatureValue: keypair.ed25519PublicKeySignature! })] : []), + ...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key', { signatureAlgorithm: keypair.ed25519SignatureAlgorithm!, signatureValue: keypair.ed25519PublicKeySignature! })] : []), ], isCat: user.isCat, attachment: attachment.length ? attachment : undefined, @@ -649,6 +649,7 @@ export class ApRendererService { '_misskey_votes': 'misskey:_misskey_votes', '_misskey_summary': 'misskey:_misskey_summary', 'isCat': 'misskey:isCat', + additionalPublicKeys: 'misskey:additionalPublicKeys', // vcard vcard: 'http://www.w3.org/2006/vcard/ns#', }, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 4c1f79e1b1..36fad4c759 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -216,8 +216,8 @@ export class ApPersonService implements OnModuleInit { if (!key.signature) { throw new Error('invalid Actor: additionalPublicKeys.signature is not set'); } - if (typeof key.signature.type !== 'string') { - throw new Error('invalid Actor: additionalPublicKeys.signature.type is not a string'); + if (typeof key.signature.signatureAlgorithm !== 'string') { + throw new Error('invalid Actor: additionalPublicKeys.signature.signatureAlgorithm is not a string'); } if (typeof key.signature.signatureValue !== 'string') { throw new Error('invalid Actor: additionalPublicKeys.signature.signatureValue is not a string'); @@ -397,8 +397,8 @@ export class ApPersonService implements OnModuleInit { if (person.additionalPublicKeys) { for (const key of person.additionalPublicKeys) { if ( - key.signature && key.signature.type && key.signature.signatureValue && - verify(key.signature.type, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) + key.signature && key.signature.signatureAlgorithm && key.signature.signatureValue && + verify(key.signature.signatureAlgorithm, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) ) { await transactionalEntityManager.save(new MiUserPublickey({ keyId: key.id, @@ -564,8 +564,8 @@ export class ApPersonService implements OnModuleInit { if (person.additionalPublicKeys) { for (const key of person.additionalPublicKeys) { if ( - key.signature && key.signature.type && key.signature.signatureValue && - verify(key.signature.type, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) + key.signature && key.signature.signatureAlgorithm && key.signature.signatureValue && + verify(key.signature.signatureAlgorithm, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) ) { await this.userPublickeysRepository.update({ keyId: key.id }, { userId: exist.id, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 72d383442e..ff0d340b6b 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -241,7 +241,7 @@ export interface IKey extends IObject { * Signature of publicKeyPem, signed by root privateKey (for additionalPublicKey) */ signature?: { - type: string; + signatureAlgorithm: string; signatureValue: string }; } From 13e0a64a7744d369983e6ec0ee8a56075945e95f Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 01:57:35 +0000 Subject: [PATCH 008/635] set publicKeyCache lifetime --- packages/backend/src/core/activitypub/ApDbResolverService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 0cb3b78941..273572e0da 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -54,8 +54,8 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new MemoryKVCache(Infinity); - this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); + this.publicKeyCache = new MemoryKVCache(1e3 * 60 * 20); // 20分 + this.publicKeyByUserIdCache = new MemoryKVCache(1e3 * 60 * 20); // 20分 } @bindThis From e2a8f4f88091554bc0fca8ec67f38673ffa1c822 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 02:15:49 +0000 Subject: [PATCH 009/635] refresh --- .../src/core/activitypub/ApDbResolverService.ts | 12 +++++++++++- .../src/queue/processors/InboxProcessorService.ts | 3 ++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 273572e0da..904c89b990 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { MiUser, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { MemoryKVCache } from '@/misc/cache.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; @@ -190,6 +190,16 @@ export class ApDbResolverService implements OnApplicationShutdown { }; } + @bindThis + public refreshCacheByUserId(userId: MiUser['id']): void { + this.publicKeyByUserIdCache.delete(userId); + for (const [k, v] of this.publicKeyCache.cache.entries()) { + if (v.value?.userId === userId) { + this.publicKeyCache.delete(k); + } + } + } + @bindThis public dispose(): void { this.publicKeyCache.dispose(); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 0a713149e5..bc593147bb 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -119,7 +119,8 @@ export class InboxProcessorService { // みたいになっててUserを引っ張れば公開キーも入ることを期待する if (activity.signature.creator) { const candicate = activity.signature.creator.replace(/#.*/, ''); - await this.apPersonService.resolvePerson(candicate).catch(() => null); + const user = await this.apPersonService.resolvePerson(candicate).catch(() => null); + if (user) this.apDbResolverService.refreshCacheByUserId(user.id); } // keyIdからLD-Signatureのユーザーを取得 From 5876a28f1e815069ddf312f32d097562b1ad8dcf Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 03:01:07 +0000 Subject: [PATCH 010/635] httpMessageSignatureAcceptable --- .../src/server/NodeinfoServerService.ts | 51 ++++++++++++++++--- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index c1e5af08c9..f9a9ce0449 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -37,12 +37,12 @@ export class NodeinfoServerService { @bindThis public getLinks() { return [{ - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', - href: this.config.url + nodeinfo2_1path - }, { - rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', - href: this.config.url + nodeinfo2_0path, - }]; + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.1', + href: this.config.url + nodeinfo2_1path, + }, { + rel: 'http://nodeinfo.diaspora.software/ns/schema/2.0', + href: this.config.url + nodeinfo2_0path, + }]; } @bindThis @@ -94,6 +94,45 @@ export class NodeinfoServerService { localComments: 0, }, metadata: { + httpMessageSignatureAcceptable: [ + { + version: 'draft-ietf-httpbis-message-signatures-02', + /** + * https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-02#name-initial-contents + */ + algorithms: [ + 'hs2019', + // 'rsa-sha1', [Deprecated] + 'rsa-sha256', + 'hmac-sha256', + 'ecdsa-sha256', + ], + /** + * https://datatracker.ietf.org/doc/html/rfc9421#section-6.2 + */ + hs2019: [ + 'ecdsa-p384-sha384', + 'ed25519', + ], + }, + /** + * rfc9421 algorithms: + * https://datatracker.ietf.org/doc/html/rfc9421#section-6.2 + * Misskeyはnode:crypto.verifyに食わせるだけなので、だいたいなんでもいけるはず + */ + /*{ + version: 'rfc9421', + algorithms: [ + 'rsa-pss-sha512', + 'rsa-v1_5-sha256', + 'hmac-sha256', + 'ecdsa-p256-sha256', + 'ecdsa-p384-sha384', + 'ed25519', + ], + },*/ + ], + nodeName: meta.name, nodeDescription: meta.description, nodeAdmins: [{ From eb8bef486d82618c3a6b2ee6d7094eea685fb1f1 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 03:06:19 +0000 Subject: [PATCH 011/635] ED25519_SIGNED_ALGORITHM --- packages/backend/src/core/UserKeypairService.ts | 6 +++--- packages/backend/src/misc/gen-key-pair.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 7946f410cf..78a18b438d 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -12,7 +12,7 @@ import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { ED25519_SIGN_ALGORITHM, genEd25519KeyPair } from '@/misc/gen-key-pair.js'; +import { ED25519_SIGNED_ALGORITHM, genEd25519KeyPair } from '@/misc/gen-key-pair.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; @Injectable() @@ -56,12 +56,12 @@ export class UserKeypairService implements OnApplicationShutdown { const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) return; const ed25519 = await genEd25519KeyPair(); - const ed25519PublicKeySignature = sign(ED25519_SIGN_ALGORITHM, Buffer.from(ed25519.publicKey), keypair.privateKey).toString('base64'); + const ed25519PublicKeySignature = sign(ED25519_SIGNED_ALGORITHM, Buffer.from(ed25519.publicKey), keypair.privateKey).toString('base64'); await this.userKeypairsRepository.update({ userId }, { ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, ed25519PublicKeySignature, - ed25519SignatureAlgorithm: `rsa-${ED25519_SIGN_ALGORITHM}`, + ed25519SignatureAlgorithm: `rsa-${ED25519_SIGNED_ALGORITHM}`, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); } diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 7b2e84d991..51ea093b29 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -8,7 +8,7 @@ import * as util from 'node:util'; const generateKeyPair = util.promisify(crypto.generateKeyPair); -export const ED25519_SIGN_ALGORITHM = 'sha256'; +export const ED25519_SIGNED_ALGORITHM = 'sha256'; export async function genRsaKeyPair(modulusLength = 4096) { return await generateKeyPair('rsa', { @@ -44,13 +44,13 @@ export async function genEd25519KeyPair() { export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { const rsa = await genRsaKeyPair(rsaModulusLength); const ed25519 = await genEd25519KeyPair(); - const ed25519PublicKeySignature = crypto.sign(ED25519_SIGN_ALGORITHM, Buffer.from(ed25519.publicKey), rsa.privateKey).toString('base64'); + const ed25519PublicKeySignature = crypto.sign(ED25519_SIGNED_ALGORITHM, Buffer.from(ed25519.publicKey), rsa.privateKey).toString('base64'); return { publicKey: rsa.publicKey, privateKey: rsa.privateKey, ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, ed25519PublicKeySignature, - ed25519SignatureAlgorithm: `rsa-${ED25519_SIGN_ALGORITHM}`, + ed25519SignatureAlgorithm: `rsa-${ED25519_SIGNED_ALGORITHM}`, }; } From 437e69cfc48f51b5be168cddc9a61f7b5dbfb4d7 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 03:08:55 +0000 Subject: [PATCH 012/635] ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM --- packages/backend/src/core/UserKeypairService.ts | 6 +++--- packages/backend/src/misc/gen-key-pair.ts | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 78a18b438d..246b747576 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -12,7 +12,7 @@ import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { ED25519_SIGNED_ALGORITHM, genEd25519KeyPair } from '@/misc/gen-key-pair.js'; +import { ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM, genEd25519KeyPair } from '@/misc/gen-key-pair.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; @Injectable() @@ -56,12 +56,12 @@ export class UserKeypairService implements OnApplicationShutdown { const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) return; const ed25519 = await genEd25519KeyPair(); - const ed25519PublicKeySignature = sign(ED25519_SIGNED_ALGORITHM, Buffer.from(ed25519.publicKey), keypair.privateKey).toString('base64'); + const ed25519PublicKeySignature = sign(ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM, Buffer.from(ed25519.publicKey), keypair.privateKey).toString('base64'); await this.userKeypairsRepository.update({ userId }, { ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, ed25519PublicKeySignature, - ed25519SignatureAlgorithm: `rsa-${ED25519_SIGNED_ALGORITHM}`, + ed25519SignatureAlgorithm: `rsa-${ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM}`, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); } diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 51ea093b29..c4b44597c5 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -8,7 +8,7 @@ import * as util from 'node:util'; const generateKeyPair = util.promisify(crypto.generateKeyPair); -export const ED25519_SIGNED_ALGORITHM = 'sha256'; +export const ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM = 'sha256'; export async function genRsaKeyPair(modulusLength = 4096) { return await generateKeyPair('rsa', { @@ -44,13 +44,13 @@ export async function genEd25519KeyPair() { export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { const rsa = await genRsaKeyPair(rsaModulusLength); const ed25519 = await genEd25519KeyPair(); - const ed25519PublicKeySignature = crypto.sign(ED25519_SIGNED_ALGORITHM, Buffer.from(ed25519.publicKey), rsa.privateKey).toString('base64'); + const ed25519PublicKeySignature = crypto.sign(ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM, Buffer.from(ed25519.publicKey), rsa.privateKey).toString('base64'); return { publicKey: rsa.publicKey, privateKey: rsa.privateKey, ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, ed25519PublicKeySignature, - ed25519SignatureAlgorithm: `rsa-${ED25519_SIGNED_ALGORITHM}`, + ed25519SignatureAlgorithm: `rsa-${ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM}`, }; } From 9705ec4a47544f6c8cedb90479f6183faaf87007 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 03:33:50 +0000 Subject: [PATCH 013/635] remove sign additionalPublicKeys signature requirements --- .../migration/1708980134301-APMultipleKeys.js | 4 -- .../backend/src/core/UserKeypairService.ts | 6 +-- .../src/core/activitypub/ApRendererService.ts | 5 +-- .../activitypub/models/ApPersonService.ts | 40 +++++-------------- packages/backend/src/core/activitypub/type.ts | 8 ---- packages/backend/src/misc/gen-key-pair.ts | 5 --- packages/backend/src/models/UserKeypair.ts | 20 ---------- .../backend/test/unit/misc/gen-key-pair.ts | 6 --- 8 files changed, 13 insertions(+), 81 deletions(-) diff --git a/packages/backend/migration/1708980134301-APMultipleKeys.js b/packages/backend/migration/1708980134301-APMultipleKeys.js index d0687fa7fa..ca55526c6e 100644 --- a/packages/backend/migration/1708980134301-APMultipleKeys.js +++ b/packages/backend/migration/1708980134301-APMultipleKeys.js @@ -10,8 +10,6 @@ export class APMultipleKeys1708980134301 { await queryRunner.query(`DROP INDEX "public"."IDX_171e64971c780ebd23fae140bb"`); await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PublicKey" character varying(128)`); await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PrivateKey" character varying(128)`); - await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519PublicKeySignature" character varying(720)`); - await queryRunner.query(`ALTER TABLE "user_keypair" ADD "ed25519SignatureAlgorithm" character varying(32)`); await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "FK_10c146e4b39b443ede016f6736d"`); await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "PK_10c146e4b39b443ede016f6736d"`); await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "PK_0db6a5fdb992323449edc8ee421" PRIMARY KEY ("userId", "keyId")`); @@ -34,8 +32,6 @@ export class APMultipleKeys1708980134301 { await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" DROP DEFAULT`); await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" TYPE "public"."user_profile_followersVisibility_enum_old" USING "followersVisibility"::"text"::"public"."user_profile_followersVisibility_enum_old"`); await queryRunner.query(`ALTER TABLE "user_profile" ALTER COLUMN "followersVisibility" SET DEFAULT 'public'`); - await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519SignatureAlgorithm"`); - await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PublicKeySignature"`); await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PrivateKey"`); await queryRunner.query(`ALTER TABLE "user_keypair" DROP COLUMN "ed25519PublicKey"`); await queryRunner.query(`CREATE UNIQUE INDEX "IDX_171e64971c780ebd23fae140bb" ON "user_publickey" ("keyId") `); diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 246b747576..10818cfc7f 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { sign } from 'node:crypto'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; import type { MiUser } from '@/models/User.js'; @@ -12,7 +11,7 @@ import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM, genEd25519KeyPair } from '@/misc/gen-key-pair.js'; +import { genEd25519KeyPair } from '@/misc/gen-key-pair.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; @Injectable() @@ -56,12 +55,9 @@ export class UserKeypairService implements OnApplicationShutdown { const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) return; const ed25519 = await genEd25519KeyPair(); - const ed25519PublicKeySignature = sign(ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM, Buffer.from(ed25519.publicKey), keypair.privateKey).toString('base64'); await this.userKeypairsRepository.update({ userId }, { ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, - ed25519PublicKeySignature, - ed25519SignatureAlgorithm: `rsa-${ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM}`, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 66a15b2d18..d929fa4ee1 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -250,7 +250,7 @@ export class ApRendererService { } @bindThis - public renderKey(user: MiLocalUser, publicKey: string, postfix?: string, signature?: IKey['signature']): IKey { + public renderKey(user: MiLocalUser, publicKey: string, postfix?: string): IKey { return { id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, type: 'Key', @@ -259,7 +259,6 @@ export class ApRendererService { type: 'spki', format: 'pem', }) as string, - signature, }; } @@ -501,7 +500,7 @@ export class ApRendererService { discoverable: user.isExplorable, publicKey: this.renderKey(user, keypair.publicKey, '#main-key'), additionalPublicKeys: [ - ...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key', { signatureAlgorithm: keypair.ed25519SignatureAlgorithm!, signatureValue: keypair.ed25519PublicKeySignature! })] : []), + ...(keypair.ed25519PublicKey ? [this.renderKey(user, keypair.ed25519PublicKey, '#ed25519-key')] : []), ], isCat: user.isCat, attachment: attachment.length ? attachment : undefined, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 36fad4c759..5100a24600 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -212,16 +212,6 @@ export class ApPersonService implements OnModuleInit { if (keyIdHost !== expectHost) { throw new Error('invalid Actor: additionalPublicKeys.id has different host'); } - - if (!key.signature) { - throw new Error('invalid Actor: additionalPublicKeys.signature is not set'); - } - if (typeof key.signature.signatureAlgorithm !== 'string') { - throw new Error('invalid Actor: additionalPublicKeys.signature.signatureAlgorithm is not a string'); - } - if (typeof key.signature.signatureValue !== 'string') { - throw new Error('invalid Actor: additionalPublicKeys.signature.signatureValue is not a string'); - } } } @@ -396,16 +386,11 @@ export class ApPersonService implements OnModuleInit { if (person.additionalPublicKeys) { for (const key of person.additionalPublicKeys) { - if ( - key.signature && key.signature.signatureAlgorithm && key.signature.signatureValue && - verify(key.signature.signatureAlgorithm, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) - ) { - await transactionalEntityManager.save(new MiUserPublickey({ - keyId: key.id, - userId: user.id, - keyPem: key.publicKeyPem, - })); - } + await transactionalEntityManager.save(new MiUserPublickey({ + keyId: key.id, + userId: user.id, + keyPem: key.publicKeyPem, + })); } } } @@ -563,16 +548,11 @@ export class ApPersonService implements OnModuleInit { if (person.additionalPublicKeys) { for (const key of person.additionalPublicKeys) { - if ( - key.signature && key.signature.signatureAlgorithm && key.signature.signatureValue && - verify(key.signature.signatureAlgorithm, Buffer.from(key.publicKeyPem), person.publicKey.publicKeyPem, Buffer.from(key.signature.signatureValue, 'base64')) - ) { - await this.userPublickeysRepository.update({ keyId: key.id }, { - userId: exist.id, - keyPem: key.publicKeyPem, - }); - availablePublicKeys.add(key.id); - } + await this.userPublickeysRepository.update({ keyId: key.id }, { + userId: exist.id, + keyPem: key.publicKeyPem, + }); + availablePublicKeys.add(key.id); } } } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index ff0d340b6b..0b34fd7664 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -236,14 +236,6 @@ export interface IKey extends IObject { id: string; owner: string; publicKeyPem: string; - - /** - * Signature of publicKeyPem, signed by root privateKey (for additionalPublicKey) - */ - signature?: { - signatureAlgorithm: string; - signatureValue: string - }; } export interface IApDocument extends IObject { diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index c4b44597c5..a214591a81 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -8,8 +8,6 @@ import * as util from 'node:util'; const generateKeyPair = util.promisify(crypto.generateKeyPair); -export const ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM = 'sha256'; - export async function genRsaKeyPair(modulusLength = 4096) { return await generateKeyPair('rsa', { modulusLength, @@ -44,13 +42,10 @@ export async function genEd25519KeyPair() { export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { const rsa = await genRsaKeyPair(rsaModulusLength); const ed25519 = await genEd25519KeyPair(); - const ed25519PublicKeySignature = crypto.sign(ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM, Buffer.from(ed25519.publicKey), rsa.privateKey).toString('base64'); return { publicKey: rsa.publicKey, privateKey: rsa.privateKey, ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, - ed25519PublicKeySignature, - ed25519SignatureAlgorithm: `rsa-${ED25519_PUBLIC_KEY_SIGNATURE_ALGORITHM}`, }; } diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts index 47d44af849..59b37bb9e9 100644 --- a/packages/backend/src/models/UserKeypair.ts +++ b/packages/backend/src/models/UserKeypair.ts @@ -48,26 +48,6 @@ export class MiUserKeypair { }) public ed25519PrivateKey: string | null; - /** - * Signature of ed25519PublicKey, signed by privateKey. (base64) - */ - @Column('varchar', { - length: 720, - nullable: true, - default: null, - }) - public ed25519PublicKeySignature: string | null; - - /** - * Signature algorithm of ed25519PublicKeySignature. - */ - @Column('varchar', { - length: 32, - nullable: true, - default: null, - }) - public ed25519SignatureAlgorithm: string | null; - constructor(data: Partial) { if (data == null) return; diff --git a/packages/backend/test/unit/misc/gen-key-pair.ts b/packages/backend/test/unit/misc/gen-key-pair.ts index 91c62f621d..771147d299 100644 --- a/packages/backend/test/unit/misc/gen-key-pair.ts +++ b/packages/backend/test/unit/misc/gen-key-pair.ts @@ -29,12 +29,6 @@ describe(genRSAAndEd25519KeyPair, () => { expect(keyPair.ed25519PrivateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/); expect(keyPair.ed25519PrivateKey).toMatch(/-----END PRIVATE KEY-----\n$/); expect(keyPair.ed25519PrivateKey).not.toBe(keyPair2.ed25519PrivateKey); - expect(keyPair.ed25519PublicKeySignature).toBe( - crypto.sign(keyPair.ed25519SignatureAlgorithm.split('-').pop(), Buffer.from(keyPair.ed25519PublicKey), keyPair.privateKey).toString('base64'), - ); - expect(crypto.verify(keyPair.ed25519SignatureAlgorithm, Buffer.from(keyPair.ed25519PublicKey), keyPair.publicKey, Buffer.from(keyPair.ed25519PublicKeySignature, 'base64'))).toBe(true); - expect(keyPair.ed25519PublicKeySignature).not.toBe(keyPair2.ed25519PublicKeySignature); - //const imported = await webCrypto.subtle.importKey('spki', Buffer.from(keyPair.publicKey).buffer, { name: 'rsa-pss', hash: 'sha-256' }, false, ['verify']); }); }); From f6b7872a0285f0578d302c82ff516e759c7bfc71 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 03:37:02 +0000 Subject: [PATCH 014/635] httpMessageSignaturesSupported --- packages/backend/src/server/NodeinfoServerService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index f9a9ce0449..1b50d71839 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -94,7 +94,7 @@ export class NodeinfoServerService { localComments: 0, }, metadata: { - httpMessageSignatureAcceptable: [ + httpMessageSignaturesSupported: [ { version: 'draft-ietf-httpbis-message-signatures-02', /** From 8579cb222f269ce490512112f7f2157a7dbabf86 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 04:25:51 +0000 Subject: [PATCH 015/635] httpMessageSignaturesImplementationLevel --- .../src/server/NodeinfoServerService.ts | 44 +++---------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 1b50d71839..3b7ee79ca3 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -94,44 +94,12 @@ export class NodeinfoServerService { localComments: 0, }, metadata: { - httpMessageSignaturesSupported: [ - { - version: 'draft-ietf-httpbis-message-signatures-02', - /** - * https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-02#name-initial-contents - */ - algorithms: [ - 'hs2019', - // 'rsa-sha1', [Deprecated] - 'rsa-sha256', - 'hmac-sha256', - 'ecdsa-sha256', - ], - /** - * https://datatracker.ietf.org/doc/html/rfc9421#section-6.2 - */ - hs2019: [ - 'ecdsa-p384-sha384', - 'ed25519', - ], - }, - /** - * rfc9421 algorithms: - * https://datatracker.ietf.org/doc/html/rfc9421#section-6.2 - * Misskeyはnode:crypto.verifyに食わせるだけなので、だいたいなんでもいけるはず - */ - /*{ - version: 'rfc9421', - algorithms: [ - 'rsa-pss-sha512', - 'rsa-v1_5-sha256', - 'hmac-sha256', - 'ecdsa-p256-sha256', - 'ecdsa-p384-sha384', - 'ed25519', - ], - },*/ - ], + /** + * 00: Draft, RSA only + * 01: Draft, Ed25519 suported + * 11: RFC 9421, Ed25519 supported + */ + httpMessageSignaturesImplementationLevel: 1, nodeName: meta.name, nodeDescription: meta.description, From 59ae73516976da0f1d34dd320a2bb76f6df14d69 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 27 Feb 2024 04:52:40 +0000 Subject: [PATCH 016/635] httpMessageSignaturesImplementationLevel: '01' --- packages/backend/src/server/NodeinfoServerService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/server/NodeinfoServerService.ts b/packages/backend/src/server/NodeinfoServerService.ts index 3b7ee79ca3..a5d4fdbddc 100644 --- a/packages/backend/src/server/NodeinfoServerService.ts +++ b/packages/backend/src/server/NodeinfoServerService.ts @@ -95,11 +95,11 @@ export class NodeinfoServerService { }, metadata: { /** - * 00: Draft, RSA only - * 01: Draft, Ed25519 suported - * 11: RFC 9421, Ed25519 supported + * '00': Draft, RSA only + * '01': Draft, Ed25519 suported + * '11': RFC 9421, Ed25519 supported */ - httpMessageSignaturesImplementationLevel: 1, + httpMessageSignaturesImplementationLevel: '01', nodeName: meta.name, nodeDescription: meta.description, From aaacfabc1b33c8ea95c0cd6d232abb3767dc77a0 Mon Sep 17 00:00:00 2001 From: MeiMei <30769358+mei23@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:44:01 +0900 Subject: [PATCH 017/635] perf(federation): Use hint for getAuthUserFromApId (#13470) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Hint for getAuthUserFromApId * とどのつまりこれでいいのか? --- .../core/activitypub/ApDbResolverService.ts | 41 +------------------ .../queue/processors/InboxProcessorService.ts | 32 +++++---------- 2 files changed, 13 insertions(+), 60 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 904c89b990..4013eaddc0 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -35,7 +35,6 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService implements OnApplicationShutdown { - private publicKeyCache: MemoryKVCache; private publicKeyByUserIdCache: MemoryKVCache; constructor( @@ -54,7 +53,6 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, ) { - this.publicKeyCache = new MemoryKVCache(1e3 * 60 * 20); // 20分 this.publicKeyByUserIdCache = new MemoryKVCache(1e3 * 60 * 20); // 20分 } @@ -116,41 +114,11 @@ export class ApDbResolverService implements OnApplicationShutdown { } } - /** - * AP KeyId => Misskey User and Key - */ - @bindThis - public async getAuthUserFromKeyId(keyId: string): Promise<{ - user: MiRemoteUser; - key: MiUserPublickey; - } | null> { - const key = await this.publicKeyCache.fetch(keyId, async () => { - const key = await this.userPublickeysRepository.findOneBy({ - keyId, - }); - - if (key == null) return null; - - return key; - }, key => key != null); - - if (key == null) return null; - - const user = await this.cacheService.findUserById(key.userId).catch(() => null) as MiRemoteUser | null; - if (user == null) return null; - if (user.isDeleted) return null; - - return { - user, - key, - }; - } - /** * AP Actor id => Misskey User and Key */ @bindThis - public async getAuthUserFromApId(uri: string): Promise<{ + public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{ user: MiRemoteUser; key: MiUserPublickey | null; } | null> { @@ -170,6 +138,7 @@ export class ApDbResolverService implements OnApplicationShutdown { keys[0] : keys.find(x => { try { + if (x.keyId === keyId) return true; const url = new URL(x.keyId); const path = url.pathname.split('/').pop()?.toLowerCase(); if (url.hash) { @@ -193,16 +162,10 @@ export class ApDbResolverService implements OnApplicationShutdown { @bindThis public refreshCacheByUserId(userId: MiUser['id']): void { this.publicKeyByUserIdCache.delete(userId); - for (const [k, v] of this.publicKeyCache.cache.entries()) { - if (v.value?.userId === userId) { - this.publicKeyCache.delete(k); - } - } } @bindThis public dispose(): void { - this.publicKeyCache.dispose(); this.publicKeyByUserIdCache.dispose(); } diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index bc593147bb..ac7f8e6cd9 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -77,20 +77,18 @@ export class InboxProcessorService { let authUser: { user: MiRemoteUser; key: MiUserPublickey | null; - } | null = await this.apDbResolverService.getAuthUserFromKeyId(signature.keyId); + } | null = null; // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 - if (authUser == null) { - try { - authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor)); - } catch (err) { - // 対象が4xxならスキップ - if (err instanceof StatusError) { - if (!err.isRetryable) { - throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); - } - throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`); + try { + authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor), signature.keyId); + } catch (err) { + // 対象が4xxならスキップ + if (err instanceof StatusError) { + if (!err.isRetryable) { + throw new Bull.UnrecoverableError(`skip: Ignored deleted actors on both ends ${activity.actor} - ${err.statusCode}`); } + throw new Error(`Error in actor ${activity.actor} - ${err.statusCode}`); } } @@ -110,21 +108,13 @@ export class InboxProcessorService { // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る - if (activity.signature) { + if (activity.signature?.creator) { if (activity.signature.type !== 'RsaSignature2017') { throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); } - // activity.signature.creator: https://example.oom/users/user#main-key - // みたいになっててUserを引っ張れば公開キーも入ることを期待する - if (activity.signature.creator) { - const candicate = activity.signature.creator.replace(/#.*/, ''); - const user = await this.apPersonService.resolvePerson(candicate).catch(() => null); - if (user) this.apDbResolverService.refreshCacheByUserId(user.id); - } + authUser = await this.apDbResolverService.getAuthUserFromApId(activity.signature.creator.replace(/#.*/, '')); - // keyIdからLD-Signatureのユーザーを取得 - authUser = await this.apDbResolverService.getAuthUserFromKeyId(activity.signature.creator); if (authUser == null) { throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); } From a1e6cb02b85e68eab23aa30e09751257186d033b Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 29 Feb 2024 21:05:31 +0000 Subject: [PATCH 018/635] use @misskey-dev/node-http-message-signatures --- packages/backend/package.json | 2 +- .../backend/src/@types/http-signature.d.ts | 82 ------------------- packages/backend/src/core/QueueService.ts | 6 +- .../queue/processors/InboxProcessorService.ts | 6 +- packages/backend/src/queue/types.ts | 20 ++++- .../src/server/ActivityPubServerService.ts | 60 +++----------- packages/backend/test/unit/ap-request.ts | 61 -------------- pnpm-lock.yaml | 19 ++--- 8 files changed, 45 insertions(+), 211 deletions(-) delete mode 100644 packages/backend/src/@types/http-signature.d.ts delete mode 100644 packages/backend/test/unit/ap-request.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 9b38fd6228..072b9ece14 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,12 +79,12 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", + "@misskey-dev/node-http-message-signatures": "0.0.0-alpha.7", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.2.10", "@nestjs/core": "10.2.10", "@nestjs/testing": "10.2.10", - "@peertube/http-signature": "1.7.0", "@simplewebauthn/server": "9.0.3", "@sinonjs/fake-timers": "11.2.2", "@smithy/node-http-handler": "2.1.10", diff --git a/packages/backend/src/@types/http-signature.d.ts b/packages/backend/src/@types/http-signature.d.ts deleted file mode 100644 index 75b62e55f0..0000000000 --- a/packages/backend/src/@types/http-signature.d.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -declare module '@peertube/http-signature' { - import type { IncomingMessage, ClientRequest } from 'node:http'; - - interface ISignature { - keyId: string; - algorithm: string; - headers: string[]; - signature: string; - } - - interface IOptions { - headers?: string[]; - algorithm?: string; - strict?: boolean; - authorizationHeaderName?: string; - } - - interface IParseRequestOptions extends IOptions { - clockSkew?: number; - } - - interface IParsedSignature { - scheme: string; - params: ISignature; - signingString: string; - algorithm: string; - keyId: string; - } - - type RequestSignerConstructorOptions = - IRequestSignerConstructorOptionsFromProperties | - IRequestSignerConstructorOptionsFromFunction; - - interface IRequestSignerConstructorOptionsFromProperties { - keyId: string; - key: string | Buffer; - algorithm?: string; - } - - interface IRequestSignerConstructorOptionsFromFunction { - sign?: (data: string, cb: (err: any, sig: ISignature) => void) => void; - } - - class RequestSigner { - constructor(options: RequestSignerConstructorOptions); - - public writeHeader(header: string, value: string): string; - - public writeDateHeader(): string; - - public writeTarget(method: string, path: string): void; - - public sign(cb: (err: any, authz: string) => void): void; - } - - interface ISignRequestOptions extends IOptions { - keyId: string; - key: string; - httpVersion?: string; - } - - export function parse(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; - export function parseRequest(request: IncomingMessage, options?: IParseRequestOptions): IParsedSignature; - - export function sign(request: ClientRequest, options: ISignRequestOptions): boolean; - export function signRequest(request: ClientRequest, options: ISignRequestOptions): boolean; - export function createSigner(): RequestSigner; - export function isSigner(obj: any): obj is RequestSigner; - - export function sshKeyToPEM(key: string): string; - export function sshKeyFingerprint(key: string): string; - export function pemToRsaSSHKey(pem: string, comment: string): string; - - export function verify(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; - export function verifySignature(parsedSignature: IParsedSignature, pubkey: string | Buffer): boolean; - export function verifyHMAC(parsedSignature: IParsedSignature, secret: string): boolean; -} diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index c258a22927..ef2ec0d2ff 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -12,11 +12,11 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; +import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; -import type httpSignature from '@peertube/http-signature'; import type * as Bull from 'bullmq'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; +import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; @Injectable() export class QueueService { @@ -136,7 +136,7 @@ export class QueueService { } @bindThis - public inbox(activity: IActivity, signature: httpSignature.IParsedSignature) { + public inbox(activity: IActivity, signature: ParsedSignature) { const data = { activity: activity, signature, diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index ac7f8e6cd9..85ce6417b8 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -5,8 +5,8 @@ import { URL } from 'node:url'; import { Injectable } from '@nestjs/common'; -import httpSignature from '@peertube/http-signature'; import * as Bull from 'bullmq'; +import { verifyDraftSignature } from '@misskey-dev/node-http-message-signatures'; import type Logger from '@/logger.js'; import { MetaService } from '@/core/MetaService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; @@ -51,7 +51,7 @@ export class InboxProcessorService { @bindThis public async process(job: Bull.Job): Promise { - const signature = job.data.signature; // HTTP-signature + const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; const activity = job.data.activity; //#region Log @@ -103,7 +103,7 @@ export class InboxProcessorService { } // HTTP-Signatureの検証 - const httpSignatureValidated = httpSignature.verifySignature(signature, authUser.key.keyPem); + const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem); // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index ce57ba745e..135bccb60c 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -9,7 +9,23 @@ import type { MiNote } from '@/models/Note.js'; import type { MiUser } from '@/models/User.js'; import type { MiWebhook } from '@/models/Webhook.js'; import type { IActivity } from '@/core/activitypub/type.js'; -import type httpSignature from '@peertube/http-signature'; +import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; + +/** + * @peertube/http-signature 時代の古いデータにも対応しておく + */ +export interface OldParsedSignature { + scheme: 'Signature'; + params: { + keyId: string; + algorithm: string; + headers: string[]; + signature: string; + }; + signingString: string; + algorithm: string; + keyId: string; +} export type DeliverJobData = { /** Actor */ @@ -26,7 +42,7 @@ export type DeliverJobData = { export type InboxJobData = { activity: IActivity; - signature: httpSignature.IParsedSignature; + signature: ParsedSignature | OldParsedSignature; }; export type RelationshipJobData = { diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 5e6cb0a05a..269bc3fb11 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -7,7 +7,7 @@ import * as crypto from 'node:crypto'; import { IncomingMessage } from 'node:http'; import { Inject, Injectable } from '@nestjs/common'; import fastifyAccepts from '@fastify/accepts'; -import httpSignature from '@peertube/http-signature'; +import { verifyDigestHeader, parseRequestSignature } from '@misskey-dev/node-http-message-signatures'; import { Brackets, In, IsNull, LessThan, Not } from 'typeorm'; import accepts from 'accepts'; import vary from 'vary'; @@ -103,63 +103,29 @@ export class ActivityPubServerService { private inbox(request: FastifyRequest, reply: FastifyReply) { let signature; + const verifyDigest = verifyDigestHeader(request.raw, request.rawBody || '', true); + if (!verifyDigest) { + reply.code(401); + return; + } + try { - signature = httpSignature.parseRequest(request.raw, { 'headers': [] }); + signature = parseRequestSignature(request.raw); } catch (e) { reply.code(401); return; } - if (signature.params.headers.indexOf('host') === -1 - || request.headers.host !== this.config.host) { - // Host not specified or not match. + if (!signature) { reply.code(401); return; } - if (signature.params.headers.indexOf('digest') === -1) { - // Digest not found. + if (signature.value.params.headers.indexOf('host') === -1 + || request.headers.host !== this.config.host) { + // Host not specified or not match. reply.code(401); - } else { - const digest = request.headers.digest; - - if (typeof digest !== 'string') { - // Huh? - reply.code(401); - return; - } - - const re = /^([a-zA-Z0-9\-]+)=(.+)$/; - const match = digest.match(re); - - if (match == null) { - // Invalid digest - reply.code(401); - return; - } - - const algo = match[1].toUpperCase(); - const digestValue = match[2]; - - if (algo !== 'SHA-256') { - // Unsupported digest algorithm - reply.code(401); - return; - } - - if (request.rawBody == null) { - // Bad request - reply.code(400); - return; - } - - const hash = crypto.createHash('sha256').update(request.rawBody).digest('base64'); - - if (hash !== digestValue) { - // Invalid digest - reply.code(401); - return; - } + return; } this.queueService.inbox(request.body as IActivity, signature); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts deleted file mode 100644 index d3d39240dc..0000000000 --- a/packages/backend/test/unit/ap-request.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as assert from 'assert'; -import httpSignature from '@peertube/http-signature'; - -import { genRsaKeyPair } from '@/misc/gen-key-pair.js'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; - -export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { - return { - scheme: 'Signature', - params: { - keyId: 'KeyID', // dummy, not used for verify - algorithm: algorithm, - headers: ['(request-target)', 'date', 'host', 'digest'], // dummy, not used for verify - signature: signature, - }, - signingString: signingString, - algorithm: algorithm.toUpperCase(), - keyId: 'KeyID', // dummy, not used for verify - }; -}; - -describe('ap-request', () => { - test('createSignedPost with verify', async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/inbox'; - const activity = { a: 1 }; - const body = JSON.stringify(activity); - const headers = { - 'User-Agent': 'UA', - }; - - const req = ApRequestCreator.createSignedPost({ key, url, body, additionalHeaders: headers }); - - const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); - - test('createSignedGet with verify', async () => { - const keypair = await genRsaKeyPair(); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/outbox'; - const headers = { - 'User-Agent': 'UA', - }; - - const req = ApRequestCreator.createSignedGet({ key, url, additionalHeaders: headers }); - - const parsed = buildParsedSignature(req.signingString, req.signature, 'rsa-sha256'); - - const result = httpSignature.verifySignature(parsed, keypair.publicKey); - assert.deepStrictEqual(result, true); - }); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26add9a11e..922234f34d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: '@fastify/view': specifier: 8.2.0 version: 8.2.0 + '@misskey-dev/node-http-message-signatures': + specifier: 0.0.0-alpha.7 + version: 0.0.0-alpha.7 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -125,9 +128,6 @@ importers: '@nestjs/testing': specifier: 10.2.10 version: 10.2.10(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/platform-express@10.3.3) - '@peertube/http-signature': - specifier: 1.7.0 - version: 1.7.0 '@simplewebauthn/server': specifier: 9.0.3 version: 9.0.3 @@ -4735,6 +4735,10 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.18.1)(eslint@8.56.0) dev: true + /@misskey-dev/node-http-message-signatures@0.0.0-alpha.7: + resolution: {integrity: sha512-iM1nZ3YT+G4AEhbUnsK7PqnMY9MjBP5JomQAgi2OyxDtZ/wBpgLP6MCVz3ElCqZ8NQS1f+c4E1m6/dSN8MtU9Q==} + dev: false + /@misskey-dev/sharp-read-bmp@1.2.0: resolution: {integrity: sha512-er4pRakXzHYfEgOFAFfQagqDouG+wLm+kwNq1I30oSdIHDa0wM3KjFpfIGQ25Fks4GcmOl1s7Zh6xoQu5dNjTw==} dependencies: @@ -5057,15 +5061,6 @@ packages: tslib: 2.6.2 dev: false - /@peertube/http-signature@1.7.0: - resolution: {integrity: sha512-aGQIwo6/sWtyyqhVK4e1MtxYz4N1X8CNt6SOtCc+Wnczs5S5ONaLHDDR8LYaGn0MgOwvGgXyuZ5sJIfd7iyoUw==} - engines: {node: '>=0.10'} - dependencies: - assert-plus: 1.0.0 - jsprim: 1.4.2 - sshpk: 1.17.0 - dev: false - /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} From fc20ef018198ac0b5781062940af3349d555a40f Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 29 Feb 2024 21:18:46 +0000 Subject: [PATCH 019/635] fix --- .../src/server/api/endpoints/admin/queue/inbox-delayed.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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..7839d8b6f5 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 @@ -56,7 +56,8 @@ export default class extends Endpoint { // eslint- const res = [] as [string, number][]; for (const job of jobs) { - const host = new URL(job.data.signature.keyId).host; + const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; + const host = new URL(signature.keyId).host; if (res.find(x => x[0] === host)) { res.find(x => x[0] === host)![1]++; } else { From 735714d61ccd943d07b0831de44384cdcfadb7d4 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 29 Feb 2024 22:20:48 +0000 Subject: [PATCH 020/635] signedPost, signedGet --- .../migration/1709242519122-HttpSignImplLv.js | 16 ++ .../src/core/FetchInstanceMetadataService.ts | 6 +- .../src/core/activitypub/ApRequestService.ts | 175 ++++++------------ .../src/core/activitypub/ApResolverService.ts | 8 +- packages/backend/src/models/Instance.ts | 5 + .../processors/DeliverProcessorService.ts | 29 +-- 6 files changed, 109 insertions(+), 130 deletions(-) create mode 100644 packages/backend/migration/1709242519122-HttpSignImplLv.js diff --git a/packages/backend/migration/1709242519122-HttpSignImplLv.js b/packages/backend/migration/1709242519122-HttpSignImplLv.js new file mode 100644 index 0000000000..7748bae006 --- /dev/null +++ b/packages/backend/migration/1709242519122-HttpSignImplLv.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class HttpSignImplLv1709242519122 { + name = 'HttpSignImplLv1709242519122' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" ADD "httpMessageSignaturesImplementationLevel" character varying(16) NOT NULL DEFAULT '00'`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "instance" DROP COLUMN "httpMessageSignaturesImplementationLevel"`); + } +} diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index bc270bd28f..63168afd7d 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -24,6 +24,7 @@ type NodeInfo = { version?: unknown; }; metadata?: { + httpMessageSignaturesImplementationLevel?: unknown, name?: unknown; nodeName?: unknown; nodeDescription?: unknown; @@ -70,7 +71,7 @@ export class FetchInstanceMetadataService { if (!force) { const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); - if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { + if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 3)) { // unlock at the finally caluse return; } @@ -104,6 +105,9 @@ export class FetchInstanceMetadataService { updates.openRegistrations = info.openRegistrations; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; + if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel) { + updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel.toString() ?? '00'; + } } if (name) updates.name = name; diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 93ac8ce9a7..717e47d4b4 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as crypto from 'node:crypto'; import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; +import { genRFC3230DigestHeader, RequestLike, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiUser } from '@/models/User.js'; @@ -16,12 +16,6 @@ import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -type Request = { - url: string; - method: string; - headers: Record; -}; - type Signed = { request: Request; signingString: string; @@ -34,103 +28,51 @@ type PrivateKey = { keyId: string; }; -export class ApRequestCreator { - static createSignedPost(args: { key: PrivateKey, url: string, body: string, digest?: string, additionalHeaders: Record }): Signed { - const u = new URL(args.url); - const digestHeader = args.digest ?? this.createDigest(args.body); +export function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string; additionalHeaders: Record }) { + const u = new URL(args.url); + const request: RequestLike = { + url: u.href, + method: 'POST', + headers: { + 'Date': new Date().toUTCString(), + 'Host': u.host, + 'Content-Type': 'application/activity+json', + ...args.additionalHeaders, + }, + }; - const request: Request = { - url: u.href, - method: 'POST', - headers: this.#objectAssignWithLcKey({ - 'Date': new Date().toUTCString(), - 'Host': u.host, - 'Content-Type': 'application/activity+json', - 'Digest': digestHeader, - }, args.additionalHeaders), - }; + // TODO: levelによって処理を分ける + const digestHeader = args.digest ?? genRFC3230DigestHeader(args.body); + request.headers['Digest'] = digestHeader; - const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); + const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; - } + return { + request, + ...result, + }; +} - static createDigest(body: string) { - return `SHA-256=${crypto.createHash('sha256').update(body).digest('base64')}`; - } +export function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record }) { + const u = new URL(args.url); + const request: RequestLike = { + url: u.href, + method: 'GET', + headers: { + 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', + 'Date': new Date().toUTCString(), + 'Host': new URL(args.url).host, + ...args.additionalHeaders, + }, + }; - static createSignedGet(args: { key: PrivateKey, url: string, additionalHeaders: Record }): Signed { - const u = new URL(args.url); + // TODO: levelによって処理を分ける + const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); - const request: Request = { - url: u.href, - method: 'GET', - headers: this.#objectAssignWithLcKey({ - 'Accept': 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"', - 'Date': new Date().toUTCString(), - 'Host': new URL(args.url).host, - }, args.additionalHeaders), - }; - - const result = this.#signToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); - - return { - request, - signingString: result.signingString, - signature: result.signature, - signatureHeader: result.signatureHeader, - }; - } - - static #signToRequest(request: Request, key: PrivateKey, includeHeaders: string[]): Signed { - const signingString = this.#genSigningString(request, includeHeaders); - const signature = crypto.sign('sha256', Buffer.from(signingString), key.privateKeyPem).toString('base64'); - const signatureHeader = `keyId="${key.keyId}",algorithm="rsa-sha256",headers="${includeHeaders.join(' ')}",signature="${signature}"`; - - request.headers = this.#objectAssignWithLcKey(request.headers, { - Signature: signatureHeader, - }); - // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! - delete request.headers['host']; - - return { - request, - signingString, - signature, - signatureHeader, - }; - } - - static #genSigningString(request: Request, includeHeaders: string[]): string { - request.headers = this.#lcObjectKey(request.headers); - - const results: string[] = []; - - for (const key of includeHeaders.map(x => x.toLowerCase())) { - if (key === '(request-target)') { - results.push(`(request-target): ${request.method.toLowerCase()} ${new URL(request.url).pathname}`); - } else { - results.push(`${key}: ${request.headers[key]}`); - } - } - - return results.join('\n'); - } - - static #lcObjectKey(src: Record): Record { - const dst: Record = {}; - for (const key of Object.keys(src).filter(x => x !== '__proto__' && typeof src[x] === 'string')) dst[key.toLowerCase()] = src[key]; - return dst; - } - - static #objectAssignWithLcKey(a: Record, b: Record): Record { - return Object.assign(this.#lcObjectKey(a), this.#lcObjectKey(b)); - } + return { + request, + ...result, + }; } @Injectable() @@ -150,16 +92,25 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, digest?: string): Promise { + private async getPrivateKey(userId: MiUser['id'], level: string): Promise { + const keypair = await this.userKeypairService.getUserKeypair(userId); + + return (level !== '00' && keypair.ed25519PrivateKey) ? { + privateKeyPem: keypair.ed25519PrivateKey, + keyId: `${this.config.url}/users/${userId}#ed25519-key`, + } : { + privateKeyPem: keypair.privateKey, + keyId: `${this.config.url}/users/${userId}#main-key`, + }; + } + + @bindThis + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); - const keypair = await this.userKeypairService.getUserKeypair(user.id); - - const req = ApRequestCreator.createSignedPost({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${this.config.url}/users/${user.id}#main-key`, - }, + const req = createSignedPost({ + level, + key: await this.getPrivateKey(user.id, level), url, body, digest, @@ -180,14 +131,10 @@ export class ApRequestService { * @param url URL to fetch */ @bindThis - public async signedGet(url: string, user: { id: MiUser['id'] }): Promise { - const keypair = await this.userKeypairService.getUserKeypair(user.id); - - const req = ApRequestCreator.createSignedGet({ - key: { - privateKeyPem: keypair.privateKey, - keyId: `${this.config.url}/users/${user.id}#main-key`, - }, + public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise { + const req = createSignedGet({ + level, + key: await this.getPrivateKey(user.id, level), url, additionalHeaders: { }, diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index bb3c40f093..727ff6f956 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -16,6 +16,7 @@ import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; +import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { isCollectionOrOrderedCollection } from './type.js'; import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; @@ -41,6 +42,7 @@ export class Resolver { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, + private federatedInstanceService: FederatedInstanceService, private loggerService: LoggerService, private recursionLimit = 100, ) { @@ -103,8 +105,10 @@ export class Resolver { this.user = await this.instanceActorService.getInstanceActor(); } + const server = await this.federatedInstanceService.fetch(host); + const object = (this.user - ? await this.apRequestService.signedGet(value, this.user) as IObject + ? await this.apRequestService.signedGet(value, this.user, server.httpMessageSignaturesImplementationLevel) as IObject : await this.httpRequestService.getActivityJson(value)) as IObject; if ( @@ -200,6 +204,7 @@ export class ApResolverService { private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, private apDbResolverService: ApDbResolverService, + private federatedInstanceService: FederatedInstanceService, private loggerService: LoggerService, ) { } @@ -220,6 +225,7 @@ export class ApResolverService { this.httpRequestService, this.apRendererService, this.apDbResolverService, + this.federatedInstanceService, this.loggerService, ); } diff --git a/packages/backend/src/models/Instance.ts b/packages/backend/src/models/Instance.ts index 9863c9d75d..0c94f9b277 100644 --- a/packages/backend/src/models/Instance.ts +++ b/packages/backend/src/models/Instance.ts @@ -149,4 +149,9 @@ export class MiInstance { length: 16384, default: '', }) public moderationNote: string; + + @Column('varchar', { + length: 16, default: '00', nullable: false, + }) + public httpMessageSignaturesImplementationLevel: string; } diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 5fed070929..8f3782a048 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -72,24 +72,25 @@ export class DeliverProcessorService { } try { - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest); + const _server = await this.federatedInstanceService.fetch(host); + await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {}); + const server = await this.federatedInstanceService.fetch(host); + + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel, job.data.digest); // Update stats - this.federatedInstanceService.fetch(host).then(i => { - if (i.isNotResponding) { - this.federatedInstanceService.update(i.id, { - isNotResponding: false, - }); - } + if (server.isNotResponding) { + this.federatedInstanceService.update(server.id, { + isNotResponding: false, + }); + } - this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - this.apRequestChart.deliverSucc(); - this.federationChart.deliverd(i.host, true); + this.apRequestChart.deliverSucc(); + this.federationChart.deliverd(server.host, true); - if (meta.enableChartsForFederatedInstances) { - this.instanceChart.requestSent(i.host, true); - } - }); + if (meta.enableChartsForFederatedInstances) { + this.instanceChart.requestSent(server.host, true); + } return 'Success'; } catch (res) { From 434520a14ec1b85c3e05dc38a912667e34b6b392 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 29 Feb 2024 22:36:19 +0000 Subject: [PATCH 021/635] =?UTF-8?q?ap-request.ts=E3=82=92=E5=BE=A9?= =?UTF-8?q?=E6=B4=BB=E3=81=95=E3=81=9B=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/test/unit/ap-request.ts | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 packages/backend/test/unit/ap-request.ts diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts new file mode 100644 index 0000000000..2aeeed1ebc --- /dev/null +++ b/packages/backend/test/unit/ap-request.ts @@ -0,0 +1,71 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as assert from 'assert'; +import { verifyDraftSignature, parseRequestSignature } from '@misskey-dev/node-http-message-signatures'; +import { genEd25519KeyPair, genRsaKeyPair } from '@/misc/gen-key-pair.js'; +import { createSignedGet, createSignedPost } from '@/core/activitypub/ApRequestService.js'; + +export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { + return { + scheme: 'Signature', + params: { + keyId: 'KeyID', // dummy, not used for verify + algorithm: algorithm, + headers: ['(request-target)', 'date', 'host', 'digest'], // dummy, not used for verify + signature: signature, + }, + signingString: signingString, + algorithm: algorithm.toUpperCase(), + keyId: 'KeyID', // dummy, not used for verify + }; +}; + +async function getKeyPair(level: string) { + if (level === '00') { + return await genRsaKeyPair(); + } else if (level === '01') { + return await genEd25519KeyPair(); + } + throw new Error('Invalid level'); +} + +describe('ap-request', () => { + describe.each(['00', '01'])('createSignedPost with verify', async (level) => { + const keypair = await getKeyPair(level); + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const url = 'https://example.com/inbox'; + const activity = { a: 1 }; + const body = JSON.stringify(activity); + const headers = { + 'User-Agent': 'UA', + }; + + const req = createSignedPost({ level, key, url, body, additionalHeaders: headers }); + + const parsed = parseRequestSignature(req.request); + expect(parsed?.version).toBe('draft'); + if (!parsed) return; + const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); + assert.deepStrictEqual(verify, true); + }); + + describe.each(['00', '01'])('createSignedGet with verify', async (level) => { + const keypair = await getKeyPair(level); + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const url = 'https://example.com/outbox'; + const headers = { + 'User-Agent': 'UA', + }; + + const req = createSignedGet({ level, key, url, additionalHeaders: headers }); + + const parsed = parseRequestSignature(req.request); + expect(parsed?.version).toBe('draft'); + if (!parsed) return; + const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); + assert.deepStrictEqual(verify, true); + }); +}); From 5f89b0a2a38f96629cd2e06659a1128db59c9734 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 29 Feb 2024 22:41:45 +0000 Subject: [PATCH 022/635] remove digest prerender --- packages/backend/src/core/QueueService.ts | 5 ----- packages/backend/src/core/activitypub/ApRequestService.ts | 7 +++---- .../src/queue/processors/DeliverProcessorService.ts | 2 +- packages/backend/src/queue/types.ts | 2 -- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index ef2ec0d2ff..7ba1853f84 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -12,7 +12,6 @@ import type { Config } from '@/config.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; -import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type * as Bull from 'bullmq'; @@ -76,14 +75,12 @@ export class QueueService { if (to == null) return null; const contentBody = JSON.stringify(content); - const digest = ApRequestCreator.createDigest(contentBody); const data: DeliverJobData = { user: { id: user.id, }, content: contentBody, - digest, to, isSharedInbox, }; @@ -109,7 +106,6 @@ export class QueueService { public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map) { if (content == null) return null; const contentBody = JSON.stringify(content); - const digest = ApRequestCreator.createDigest(contentBody); const opts = { attempts: this.config.deliverJobMaxAttempts ?? 12, @@ -125,7 +121,6 @@ export class QueueService { data: { user, content: contentBody, - digest, to: d[0], isSharedInbox: d[1], } as DeliverJobData, diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 717e47d4b4..cc2f05aba2 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -28,7 +28,7 @@ type PrivateKey = { keyId: string; }; -export function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string; additionalHeaders: Record }) { +export function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; additionalHeaders: Record }) { const u = new URL(args.url); const request: RequestLike = { url: u.href, @@ -42,7 +42,7 @@ export function createSignedPost(args: { level: string; key: PrivateKey; url: st }; // TODO: levelによって処理を分ける - const digestHeader = args.digest ?? genRFC3230DigestHeader(args.body); + const digestHeader = genRFC3230DigestHeader(args.body); request.headers['Digest'] = digestHeader; const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); @@ -105,7 +105,7 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string): Promise { + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); const req = createSignedPost({ @@ -113,7 +113,6 @@ export class ApRequestService { key: await this.getPrivateKey(user.id, level), url, body, - digest, additionalHeaders: { }, }); diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 8f3782a048..6bbe7a4973 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -76,7 +76,7 @@ export class DeliverProcessorService { await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {}); const server = await this.federatedInstanceService.fetch(host); - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel, job.data.digest); + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel); // Update stats if (server.isNotResponding) { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 135bccb60c..fcae4c2596 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -32,8 +32,6 @@ export type DeliverJobData = { user: ThinUser; /** Activity */ content: string; - /** Digest header */ - digest: string; /** inbox URL to deliver */ to: string; /** whether it is sharedInbox */ From 66c0942d7e67bea66988f2b437214f1ed31e10d3 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 29 Feb 2024 22:50:46 +0000 Subject: [PATCH 023/635] fix test? --- packages/backend/test/misc/mock-resolver.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 3c7e796700..485506ee64 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -14,6 +14,7 @@ import type { InstanceActorService } from '@/core/InstanceActorService.js'; import type { LoggerService } from '@/core/LoggerService.js'; import type { MetaService } from '@/core/MetaService.js'; import type { UtilityService } from '@/core/UtilityService.js'; +import type { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { bindThis } from '@/decorators.js'; import type { FollowRequestsRepository, @@ -47,6 +48,7 @@ export class MockResolver extends Resolver { {} as HttpRequestService, {} as ApRendererService, {} as ApDbResolverService, + {} as FederatedInstanceService, loggerService, ); } From 7751d8005649993a80abe7d9df96488921ec1d44 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 29 Feb 2024 22:57:10 +0000 Subject: [PATCH 024/635] fix test --- packages/backend/test/unit/ap-request.ts | 60 +++++++++++++----------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index 2aeeed1ebc..d04de68001 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -33,39 +33,43 @@ async function getKeyPair(level: string) { } describe('ap-request', () => { - describe.each(['00', '01'])('createSignedPost with verify', async (level) => { - const keypair = await getKeyPair(level); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/inbox'; - const activity = { a: 1 }; - const body = JSON.stringify(activity); - const headers = { - 'User-Agent': 'UA', - }; + describe.each(['00', '01'])('createSignedPost with verify', (level) => { + test('pass', async () => { + const keypair = await getKeyPair(level); + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const url = 'https://example.com/inbox'; + const activity = { a: 1 }; + const body = JSON.stringify(activity); + const headers = { + 'User-Agent': 'UA', + }; - const req = createSignedPost({ level, key, url, body, additionalHeaders: headers }); + const req = createSignedPost({ level, key, url, body, additionalHeaders: headers }); - const parsed = parseRequestSignature(req.request); - expect(parsed?.version).toBe('draft'); - if (!parsed) return; - const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); - assert.deepStrictEqual(verify, true); + const parsed = parseRequestSignature(req.request); + expect(parsed?.version).toBe('draft'); + if (!parsed) return; + const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); + assert.deepStrictEqual(verify, true); + }); }); - describe.each(['00', '01'])('createSignedGet with verify', async (level) => { - const keypair = await getKeyPair(level); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; - const url = 'https://example.com/outbox'; - const headers = { - 'User-Agent': 'UA', - }; + describe.each(['00', '01'])('createSignedGet with verify', (level) => { + test('pass', async () => { + const keypair = await getKeyPair(level); + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const url = 'https://example.com/outbox'; + const headers = { + 'User-Agent': 'UA', + }; - const req = createSignedGet({ level, key, url, additionalHeaders: headers }); + const req = createSignedGet({ level, key, url, additionalHeaders: headers }); - const parsed = parseRequestSignature(req.request); - expect(parsed?.version).toBe('draft'); - if (!parsed) return; - const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); - assert.deepStrictEqual(verify, true); + const parsed = parseRequestSignature(req.request); + expect(parsed?.version).toBe('draft'); + if (!parsed) return; + const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); + assert.deepStrictEqual(verify, true); + }); }); }); From 16cea7d3b66a8cf3b2e1827fae6f64483b1df862 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 04:38:42 +0000 Subject: [PATCH 025/635] add httpMessageSignaturesImplementationLevel to FederationInstance --- packages/backend/src/core/entities/InstanceEntityService.ts | 1 + .../backend/src/models/json-schema/federation-instance.ts | 4 ++++ packages/misskey-js/src/autogen/types.ts | 1 + 3 files changed, 6 insertions(+) diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index e46bd8b963..1482a22a6c 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -55,6 +55,7 @@ export class InstanceEntityService { infoUpdatedAt: instance.infoUpdatedAt ? instance.infoUpdatedAt.toISOString() : null, latestRequestReceivedAt: instance.latestRequestReceivedAt ? instance.latestRequestReceivedAt.toISOString() : null, moderationNote: iAmModerator ? instance.moderationNote : null, + httpMessageSignaturesImplementationLevel: instance.httpMessageSignaturesImplementationLevel, }; } diff --git a/packages/backend/src/models/json-schema/federation-instance.ts b/packages/backend/src/models/json-schema/federation-instance.ts index 42d98fe523..5da3f87d42 100644 --- a/packages/backend/src/models/json-schema/federation-instance.ts +++ b/packages/backend/src/models/json-schema/federation-instance.ts @@ -111,5 +111,9 @@ export const packedFederationInstanceSchema = { type: 'string', optional: true, nullable: true, }, + httpMessageSignaturesImplementationLevel: { + type: 'string', + optional: false, nullable: false, + }, }, } as const; diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 227295fbb8..0e32c91484 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4492,6 +4492,7 @@ export type components = { /** Format: date-time */ latestRequestReceivedAt: string | null; moderationNote?: string | null; + httpMessageSignaturesImplementationLevel: string; }; GalleryPost: { /** From 87ded2bd1c2c96bcd5719f244998100a65f65feb Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 05:04:02 +0000 Subject: [PATCH 026/635] ManyToOne --- .../1709269211718-APMultipleKeysFix1.js | 16 ++++++++++++++++ packages/backend/src/models/UserKeypair.ts | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 packages/backend/migration/1709269211718-APMultipleKeysFix1.js diff --git a/packages/backend/migration/1709269211718-APMultipleKeysFix1.js b/packages/backend/migration/1709269211718-APMultipleKeysFix1.js new file mode 100644 index 0000000000..d2011802f2 --- /dev/null +++ b/packages/backend/migration/1709269211718-APMultipleKeysFix1.js @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export class APMultipleKeys1709269211718 { + name = 'APMultipleKeys1709269211718' + + async up(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_publickey" DROP CONSTRAINT "UQ_10c146e4b39b443ede016f6736d"`); + } + + async down(queryRunner) { + await queryRunner.query(`ALTER TABLE "user_publickey" ADD CONSTRAINT "UQ_10c146e4b39b443ede016f6736d" UNIQUE ("userId")`); + } +} diff --git a/packages/backend/src/models/UserKeypair.ts b/packages/backend/src/models/UserKeypair.ts index 59b37bb9e9..afa74ef11a 100644 --- a/packages/backend/src/models/UserKeypair.ts +++ b/packages/backend/src/models/UserKeypair.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { PrimaryColumn, Entity, JoinColumn, Column, OneToOne } from 'typeorm'; +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne } from 'typeorm'; import { id } from './util/id.js'; import { MiUser } from './User.js'; @@ -12,7 +12,7 @@ export class MiUserKeypair { @PrimaryColumn(id()) public userId: MiUser['id']; - @OneToOne(type => MiUser, { + @ManyToOne(type => MiUser, { onDelete: 'CASCADE', }) @JoinColumn() From 54fe8ca600bbecb15236a5ccddef5f5f7a9e2fb8 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 06:49:38 +0000 Subject: [PATCH 027/635] fetchPersonWithRenewal --- .../src/core/FetchInstanceMetadataService.ts | 23 ++++++++++++------- .../core/activitypub/ApDbResolverService.ts | 2 +- .../src/core/activitypub/ApInboxService.ts | 10 -------- .../src/core/activitypub/ApRequestService.ts | 23 ++++++++++++++++--- .../activitypub/models/ApPersonService.ts | 20 ++++++++++++++-- .../queue/processors/InboxProcessorService.ts | 12 ++++++++-- 6 files changed, 64 insertions(+), 26 deletions(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 63168afd7d..21e84a7454 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -40,6 +40,7 @@ type NodeInfo = { @Injectable() export class FetchInstanceMetadataService { private logger: Logger; + private httpColon = 'https://'; constructor( private httpRequestService: HttpRequestService, @@ -49,6 +50,7 @@ export class FetchInstanceMetadataService { private redisClient: Redis.Redis, ) { this.logger = this.loggerService.getLogger('metadata', 'cyan'); + this.httpColon = 'http://'; } @bindThis @@ -72,8 +74,7 @@ export class FetchInstanceMetadataService { const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 3)) { - // unlock at the finally caluse - return; + throw new Error('Skip because updated recently'); } } @@ -119,6 +120,12 @@ export class FetchInstanceMetadataService { await this.federatedInstanceService.update(instance.id, updates); this.logger.succ(`Successfuly updated metadata of ${instance.host}`); + this.logger.debug('Updated metadata:', { + info: !!info, + dom: !!dom, + manifest: !!manifest, + updates, + }); } catch (e) { this.logger.error(`Failed to update metadata of ${instance.host}: ${e}`); } finally { @@ -131,7 +138,7 @@ export class FetchInstanceMetadataService { this.logger.info(`Fetching nodeinfo of ${instance.host} ...`); try { - const wellknown = await this.httpRequestService.getJson('https://' + instance.host + '/.well-known/nodeinfo') + const wellknown = await this.httpRequestService.getJson(this.httpColon + instance.host + '/.well-known/nodeinfo') .catch(err => { if (err.statusCode === 404) { throw new Error('No nodeinfo provided'); @@ -174,7 +181,7 @@ export class FetchInstanceMetadataService { private async fetchDom(instance: MiInstance): Promise { this.logger.info(`Fetching HTML of ${instance.host} ...`); - const url = 'https://' + instance.host; + const url = this.httpColon + instance.host; const html = await this.httpRequestService.getHtml(url); @@ -186,7 +193,7 @@ export class FetchInstanceMetadataService { @bindThis private async fetchManifest(instance: MiInstance): Promise | null> { - const url = 'https://' + instance.host; + const url = this.httpColon + instance.host; const manifestUrl = url + '/manifest.json'; @@ -197,7 +204,7 @@ export class FetchInstanceMetadataService { @bindThis private async fetchFaviconUrl(instance: MiInstance, doc: DOMWindow['document'] | null): Promise { - const url = 'https://' + instance.host; + const url = this.httpColon + instance.host; if (doc) { // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 @@ -224,12 +231,12 @@ export class FetchInstanceMetadataService { @bindThis private async fetchIconUrl(instance: MiInstance, doc: DOMWindow['document'] | null, manifest: Record | null): Promise { if (manifest && manifest.icons && manifest.icons.length > 0 && manifest.icons[0].src) { - const url = 'https://' + instance.host; + const url = this.httpColon + instance.host; return (new URL(manifest.icons[0].src, url)).href; } if (doc) { - const url = 'https://' + instance.host; + const url = this.httpColon + instance.host; // https://github.com/misskey-dev/misskey/pull/8220#issuecomment-1025104043 const links = Array.from(doc.getElementsByTagName('link')).reverse(); diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 4013eaddc0..d95bd3bbda 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -122,7 +122,7 @@ export class ApDbResolverService implements OnApplicationShutdown { user: MiRemoteUser; key: MiUserPublickey | null; } | null> { - const user = await this.apPersonService.resolvePerson(uri) as MiRemoteUser; + const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser; if (user.isDeleted) return null; const keys = await this.publicKeyByUserIdCache.fetch( diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index b0f56a5d82..13b86c2920 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -36,7 +36,6 @@ import { ApResolverService } from './ApResolverService.js'; import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.js'; -import { CacheService } from '@/core/CacheService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Resolver } from './ApResolverService.js'; import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove } from './type.js'; @@ -109,15 +108,6 @@ export class ApInboxService { } else { await this.performOneActivity(actor, activity); } - - // ついでにリモートユーザーの情報が古かったら更新しておく - if (actor.uri) { - if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - setImmediate(() => { - this.apPersonService.updatePerson(actor.uri); - }); - } - } } @bindThis diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index cc2f05aba2..36a4b34de1 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -107,16 +107,24 @@ export class ApRequestService { @bindThis public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); - + const key = await this.getPrivateKey(user.id, level); const req = createSignedPost({ level, - key: await this.getPrivateKey(user.id, level), + key, url, body, additionalHeaders: { + 'User-Agent': this.config.userAgent, }, }); + this.logger.debug('create signed post', { + version: 'draft', + level, + url, + keyId: key.keyId, + }); + await this.httpRequestService.send(url, { method: req.request.method, headers: req.request.headers, @@ -131,14 +139,23 @@ export class ApRequestService { */ @bindThis public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise { + const key = await this.getPrivateKey(user.id, level); const req = createSignedGet({ level, - key: await this.getPrivateKey(user.id, level), + key, url, additionalHeaders: { + 'User-Agent': this.config.userAgent, }, }); + this.logger.debug('create signed get', { + version: 'draft', + level, + url, + keyId: key.keyId, + }); + const res = await this.httpRequestService.send(url, { method: req.request.method, headers: req.request.headers, diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 5100a24600..da4a42f73e 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -248,6 +248,22 @@ export class ApPersonService implements OnModuleInit { return null; } + @bindThis + async fetchPersonWithRenewal(uri: string): Promise { + const exist = await this.fetchPerson(uri); + if (exist == null) return null; + + // ついでにリモートユーザーの情報が古かったら更新しておく + if (this.userEntityService.isRemoteUser(exist)) { + if (exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > 1000 * 60 * 60 * 3) { + await this.updatePerson(exist.uri); + return await this.fetchPerson(uri); + } + } + + return exist; + } + private async resolveAvatarAndBanner(user: MiRemoteUser, icon: any, image: any): Promise>> { if (user == null) throw new Error('failed to create user: user is null'); @@ -624,9 +640,9 @@ export class ApPersonService implements OnModuleInit { * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。 */ @bindThis - public async resolvePerson(uri: string, resolver?: Resolver): Promise { + public async resolvePerson(uri: string, resolver?: Resolver, withRenewal = false): Promise { //#region このサーバーに既に登録されていたらそれを返す - const exist = await this.fetchPerson(uri); + const exist = withRenewal ? await this.fetchPersonWithRenewal(uri) : await this.fetchPerson(uri); if (exist) return exist; //#endregion diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 85ce6417b8..4d7f76c0d9 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -19,6 +19,7 @@ import type { MiRemoteUser } from '@/models/User.js'; import type { MiUserPublickey } from '@/models/UserPublickey.js'; import { ApDbResolverService } from '@/core/activitypub/ApDbResolverService.js'; import { StatusError } from '@/misc/status-error.js'; +import * as Acct from '@/misc/acct.js'; import { UtilityService } from '@/core/UtilityService.js'; import { ApPersonService } from '@/core/activitypub/models/ApPersonService.js'; import { LdSignatureService } from '@/core/activitypub/LdSignatureService.js'; @@ -79,7 +80,6 @@ export class InboxProcessorService { key: MiUserPublickey | null; } | null = null; - // keyIdでわからなければ、activity.actorを元にDBから取得 || activity.actorを元にリモートから取得 try { authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor), signature.keyId); } catch (err) { @@ -103,7 +103,15 @@ export class InboxProcessorService { } // HTTP-Signatureの検証 - const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem); + const errorLogger = (ms: any) => this.logger.error(ms); + const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem, errorLogger); + this.logger.debug('Inbox message validation: ', { + userId: authUser.user.id, + userAcct: Acct.toString(authUser.user), + parsedKeyId: signature.keyId, + foundKeyId: authUser.key.keyId, + httpSignatureValidated, + }); // また、signatureのsignerは、activity.actorと一致する必要がある if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { From bec6159b4a19905efa79acce105e63a80f0d1100 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 06:58:43 +0000 Subject: [PATCH 028/635] exactKey --- .../core/activitypub/ApDbResolverService.ts | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index d95bd3bbda..25a37ca096 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -131,31 +131,43 @@ export class ApDbResolverService implements OnApplicationShutdown { v => v != null, ); - if (keys == null || keys.length === 8) return null; + if (keys == null || !Array.isArray(keys)) return null; + + if (keys.length === 0) { + return { + user, + key: keys[0], + }; + } + + const exactKey = keys.find(x => x.keyId === keyId); + if (exactKey) { + return { + user, + key: exactKey, + }; + } // 公開鍵は複数あるが、mainっぽいのを選ぶ - const key = keys.length === 1 ? - keys[0] : - keys.find(x => { - try { - if (x.keyId === keyId) return true; - const url = new URL(x.keyId); - const path = url.pathname.split('/').pop()?.toLowerCase(); - if (url.hash) { - if (url.hash.toLowerCase().includes('main')) { - return true; - } - } else if (path?.includes('main') || path === 'publickey') { + const mainKey = keys.find(x => { + try { + if (x.keyId === keyId) return true; + const url = new URL(x.keyId); + const path = url.pathname.split('/').pop()?.toLowerCase(); + if (url.hash) { + if (url.hash.toLowerCase().includes('main')) { return true; } - } catch { /* noop */ } - - return false; - }) ?? keys[0]; + } else if (path?.includes('main') || path === 'publickey') { + return true; + } + } catch { /* noop */ } + return false; + }); return { user, - key, + key: mainKey ?? keys[0], }; } From 743b74077583b7a14881618de567563fb7992c60 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 07:18:16 +0000 Subject: [PATCH 029/635] :v: --- CONTRIBUTING.md | 2 +- packages/backend/src/core/FetchInstanceMetadataService.ts | 2 +- packages/backend/src/core/WebfingerService.ts | 2 +- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3263bf6aa..b155e4403b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -185,7 +185,7 @@ TODO ## Environment Variable - `MISSKEY_CONFIG_YML`: Specify the file path of config.yml instead of default.yml (e.g. `2nd.yml`). -- `MISSKEY_WEBFINGER_USE_HTTP`: If it's set true, WebFinger requests will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION. +- `MISSKEY_USE_HTTP`: If it's set true, federation requests (like nodeinfo and webfinger) will be http instead of https, useful for testing federation between servers in localhost. NEVER USE IN PRODUCTION. (was `MISSKEY_WEBFINGER_USE_HTTP`) ## Continuous integration Misskey uses GitHub Actions for executing automated tests. diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 21e84a7454..fc4f335d54 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -50,7 +50,7 @@ export class FetchInstanceMetadataService { private redisClient: Redis.Redis, ) { this.logger = this.loggerService.getLogger('metadata', 'cyan'); - this.httpColon = 'http://'; + this.httpColon = process.env.MISSKEY_USE_HTTP?.toLowerCase() === 'true' ? 'http://' : 'https://'; } @bindThis diff --git a/packages/backend/src/core/WebfingerService.ts b/packages/backend/src/core/WebfingerService.ts index 374536a741..aa1144778c 100644 --- a/packages/backend/src/core/WebfingerService.ts +++ b/packages/backend/src/core/WebfingerService.ts @@ -46,7 +46,7 @@ export class WebfingerService { const m = query.match(mRegex); if (m) { const hostname = m[2]; - const useHttp = process.env.MISSKEY_WEBFINGER_USE_HTTP && process.env.MISSKEY_WEBFINGER_USE_HTTP.toLowerCase() === 'true'; + const useHttp = process.env.MISSKEY_USE_HTTP && process.env.MISSKEY_USE_HTTP.toLowerCase() === 'true'; return `http${useHttp ? '' : 's'}://${hostname}/.well-known/webfinger?${urlQuery({ resource: `acct:${query}` })}`; } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index da4a42f73e..000d1d4144 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -256,9 +256,11 @@ export class ApPersonService implements OnModuleInit { // ついでにリモートユーザーの情報が古かったら更新しておく if (this.userEntityService.isRemoteUser(exist)) { if (exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > 1000 * 60 * 60 * 3) { + this.logger.debug('fetchPersonWithRenewal: renew', { uri, lastFetchedAt: exist.lastFetchedAt }); await this.updatePerson(exist.uri); return await this.fetchPerson(uri); } + this.logger.debug('fetchPersonWithRenewal: use cache', { uri, lastFetchedAt: exist.lastFetchedAt }); } return exist; From fd71ad7a5f641abfb7316965e7ecedce01264a19 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 07:27:43 +0000 Subject: [PATCH 030/635] use const --- packages/backend/src/const.ts | 5 +++++ packages/backend/src/core/FetchInstanceMetadataService.ts | 3 ++- .../backend/src/core/activitypub/models/ApPersonService.ts | 5 +++-- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts index a238f4973a..8579f5d187 100644 --- a/packages/backend/src/const.ts +++ b/packages/backend/src/const.ts @@ -8,6 +8,11 @@ export const MAX_NOTE_TEXT_LENGTH = 3000; export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days +export const REMOTE_USER_CACHE_TTL = 1000 * 60 * 60 * 3; // 3hours +export const REMOTE_USER_MOVE_COOLDOWN = 1000 * 60 * 60 * 24 * 14; // 14days + +export const REMOTE_SERVER_CACHE_TTL = 1000 * 60 * 60 * 3; // 3hours + //#region hard limits // If you change DB_* values, you must also change the DB schema. diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index fc4f335d54..d038646d0d 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -15,6 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; +import { REMOTE_SERVER_CACHE_TTL } from '@/const'; import type { DOMWindow } from 'jsdom'; type NodeInfo = { @@ -73,7 +74,7 @@ export class FetchInstanceMetadataService { if (!force) { const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); - if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 3)) { + if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) { throw new Error('Skip because updated recently'); } } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 000d1d4144..97abfcbde8 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -40,6 +40,7 @@ import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.j import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; import { isNotNull } from '@/misc/is-not-null.js'; +import { REMOTE_USER_CACHE_TTL, REMOTE_USER_MOVE_COOLDOWN } from '@/const.js'; import { getApId, getApType, getOneApHrefNullable, isActor, isCollection, isCollectionOrOrderedCollection, isPropertyValue } from '../type.js'; import { extractApHashtags } from './tag.js'; import type { OnModuleInit } from '@nestjs/common'; @@ -255,7 +256,7 @@ export class ApPersonService implements OnModuleInit { // ついでにリモートユーザーの情報が古かったら更新しておく if (this.userEntityService.isRemoteUser(exist)) { - if (exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > 1000 * 60 * 60 * 3) { + if (exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > REMOTE_USER_CACHE_TTL) { this.logger.debug('fetchPersonWithRenewal: renew', { uri, lastFetchedAt: exist.lastFetchedAt }); await this.updatePerson(exist.uri); return await this.fetchPerson(uri); @@ -619,7 +620,7 @@ export class ApPersonService implements OnModuleInit { exist.movedAt == null || // 以前のmovingから14日以上経過した場合のみ移行処理を許可 // (Mastodonのクールダウン期間は30日だが若干緩めに設定しておく) - exist.movedAt.getTime() + 1000 * 60 * 60 * 24 * 14 < updated.movedAt.getTime() + exist.movedAt.getTime() + REMOTE_USER_MOVE_COOLDOWN < updated.movedAt.getTime() )) { this.logger.info(`Start to process Move of @${updated.username}@${updated.host} (${uri})`); return this.processRemoteMove(updated, movePreventUris) From 67758d2d1e8c5686db242af0c059c8d6ca3bef44 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 12:52:46 +0000 Subject: [PATCH 031/635] use gen-key-pair fn. from '@misskey-dev/node-http-message-signatures' --- .../backend/src/core/UserKeypairService.ts | 2 +- packages/backend/src/misc/gen-key-pair.ts | 36 +------------------ packages/backend/test/unit/ap-request.ts | 7 ++-- 3 files changed, 5 insertions(+), 40 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 10818cfc7f..4cc1fa78d6 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -11,7 +11,7 @@ import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { genEd25519KeyPair } from '@/misc/gen-key-pair.js'; +import { genEd25519KeyPair } from '@misskey-dev/node-http-message-signatures'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; @Injectable() diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index a214591a81..58e3091369 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -3,41 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as crypto from 'node:crypto'; -import * as util from 'node:util'; - -const generateKeyPair = util.promisify(crypto.generateKeyPair); - -export async function genRsaKeyPair(modulusLength = 4096) { - return await generateKeyPair('rsa', { - modulusLength, - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - cipher: undefined, - passphrase: undefined, - }, - }); -} - -export async function genEd25519KeyPair() { - return await generateKeyPair('ed25519', { - publicKeyEncoding: { - type: 'spki', - format: 'pem', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'pem', - cipher: undefined, - passphrase: undefined, - }, - }); -} +import { genEd25519KeyPair, genRsaKeyPair } from '@misskey-dev/node-http-message-signatures'; export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { const rsa = await genRsaKeyPair(rsaModulusLength); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index d04de68001..fc3855534d 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -4,8 +4,7 @@ */ import * as assert from 'assert'; -import { verifyDraftSignature, parseRequestSignature } from '@misskey-dev/node-http-message-signatures'; -import { genEd25519KeyPair, genRsaKeyPair } from '@/misc/gen-key-pair.js'; +import { verifyDraftSignature, parseRequestSignature, genEd25519KeyPair, genRsaKeyPair } from '@misskey-dev/node-http-message-signatures'; import { createSignedGet, createSignedPost } from '@/core/activitypub/ApRequestService.js'; export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { @@ -49,7 +48,7 @@ describe('ap-request', () => { const parsed = parseRequestSignature(req.request); expect(parsed?.version).toBe('draft'); if (!parsed) return; - const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); + const verify = verifyDraftSignature(parsed.value, keypair.publicKey); assert.deepStrictEqual(verify, true); }); }); @@ -68,7 +67,7 @@ describe('ap-request', () => { const parsed = parseRequestSignature(req.request); expect(parsed?.version).toBe('draft'); if (!parsed) return; - const verify = verifyDraftSignature(parsed?.value, keypair.publicKey); + const verify = verifyDraftSignature(parsed.value, keypair.publicKey); assert.deepStrictEqual(verify, true); }); }); From 86c9f0b0fba5c5bcc2b5684a5feecb3ae64a8ea3 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 15:08:12 +0000 Subject: [PATCH 032/635] update node-http-message-signatures --- packages/backend/package.json | 2 +- packages/backend/src/server/ActivityPubServerService.ts | 7 +------ pnpm-lock.yaml | 8 ++++---- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index c07f31c777..b80956e5bc 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.0-alpha.7", + "@misskey-dev/node-http-message-signatures": "0.0.0-alpha.10", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 269bc3fb11..204b9e7aa7 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -101,7 +101,7 @@ export class ActivityPubServerService { @bindThis private inbox(request: FastifyRequest, reply: FastifyReply) { - let signature; + let signature: ReturnType; const verifyDigest = verifyDigestHeader(request.raw, request.rawBody || '', true); if (!verifyDigest) { @@ -116,11 +116,6 @@ export class ActivityPubServerService { return; } - if (!signature) { - reply.code(401); - return; - } - if (signature.value.params.headers.indexOf('host') === -1 || request.headers.host !== this.config.host) { // Host not specified or not match. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b989c05254..2d2d6a23d1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.0-alpha.7 - version: 0.0.0-alpha.7 + specifier: 0.0.0-alpha.10 + version: 0.0.0-alpha.10 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4750,8 +4750,8 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.0-alpha.7: - resolution: {integrity: sha512-iM1nZ3YT+G4AEhbUnsK7PqnMY9MjBP5JomQAgi2OyxDtZ/wBpgLP6MCVz3ElCqZ8NQS1f+c4E1m6/dSN8MtU9Q==} + /@misskey-dev/node-http-message-signatures@0.0.0-alpha.10: + resolution: {integrity: sha512-4weLJVBm06bx4fLJHL9YtKXQ8ofKSNDI4UAaoI0JkzbbU8pyJHK4LFGfERrpUX9C+WsvSwVFZAgAzOxFwJ7z2w==} dev: false /@misskey-dev/sharp-read-bmp@1.2.0: From 65bd187d85b64ce95ccee88cd7136d2b88d97490 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 15:51:09 +0000 Subject: [PATCH 033/635] fix --- packages/backend/src/core/FetchInstanceMetadataService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index d038646d0d..a1313fc257 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -15,7 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { REMOTE_SERVER_CACHE_TTL } from '@/const'; +import { REMOTE_SERVER_CACHE_TTL } from '@/const.js'; import type { DOMWindow } from 'jsdom'; type NodeInfo = { From 9111b5c4826021bbede6ba4fde1f4e68b90990a0 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 16:33:08 +0000 Subject: [PATCH 034/635] @misskey-dev/node-http-message-signatures@0.0.0-alpha.11 --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index b80956e5bc..1d3f2fcd2a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.0-alpha.10", + "@misskey-dev/node-http-message-signatures": "0.0.0-alpha.11", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2d2d6a23d1..52efc4c217 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.0-alpha.10 - version: 0.0.0-alpha.10 + specifier: 0.0.0-alpha.11 + version: 0.0.0-alpha.11 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4750,8 +4750,8 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.0-alpha.10: - resolution: {integrity: sha512-4weLJVBm06bx4fLJHL9YtKXQ8ofKSNDI4UAaoI0JkzbbU8pyJHK4LFGfERrpUX9C+WsvSwVFZAgAzOxFwJ7z2w==} + /@misskey-dev/node-http-message-signatures@0.0.0-alpha.11: + resolution: {integrity: sha512-DeLzSIquddDQp+hqD9/zS/IkpJg8Zs1h/4viavCFe37CW2W4ABFumXhYJPB07n775QEihZNuvdipoMNXl83jzw==} dev: false /@misskey-dev/sharp-read-bmp@1.2.0: From d86b8c8752bb9eb5f6b4431403ed4bb0bc615c59 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 18:29:30 +0000 Subject: [PATCH 035/635] =?UTF-8?q?getAuthUserFromApId=E3=81=A7updatePerso?= =?UTF-8?q?n=E3=81=AE=E9=A0=BB=E5=BA=A6=E3=82=92=E5=A2=97=E3=82=84?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/activitypub/ApDbResolverService.ts | 80 ++++++++++--------- .../activitypub/models/ApPersonService.ts | 9 +-- 2 files changed, 48 insertions(+), 41 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 25a37ca096..9111906538 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -116,6 +116,8 @@ export class ApDbResolverService implements OnApplicationShutdown { /** * AP Actor id => Misskey User and Key + * @param uri AP Actor id + * @param keyId Key id to find. If not specified, main key will be selected. */ @bindThis public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{ @@ -125,50 +127,56 @@ export class ApDbResolverService implements OnApplicationShutdown { const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser; if (user.isDeleted) return null; - const keys = await this.publicKeyByUserIdCache.fetch( - user.id, - () => this.userPublickeysRepository.find({ where: { userId: user.id } }), - v => v != null, - ); + const keys = await this.getPublicKeyByUserId(user.id); - if (keys == null || !Array.isArray(keys)) return null; + if (keys == null || !Array.isArray(keys)) return { user, key: null }; - if (keys.length === 0) { - return { - user, - key: keys[0], - }; + if (!keyId) { + // mainっぽいのを選ぶ + const mainKey = keys.find(x => { + try { + const url = new URL(x.keyId); + const path = url.pathname.split('/').pop()?.toLowerCase(); + if (url.hash) { + if (url.hash.toLowerCase().includes('main')) { + return true; + } + } else if (path?.includes('main') || path === 'publickey') { + return true; + } + } catch { /* noop */ } + + return false; + }); + return { user, key: mainKey ?? keys[0] }; } const exactKey = keys.find(x => x.keyId === keyId); - if (exactKey) { - return { - user, - key: exactKey, - }; + if (exactKey) return { user, key: exactKey }; + + // keyIdで見つからない場合、lastFetchedAtでの更新制限を弱めて再取得 + if (user.lastFetchedAt == null || user.lastFetchedAt < new Date(Date.now() - 1000 * 60 * 12)) { + const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0); + if (renewed == null || renewed.isDeleted) return null; + + this.refreshCacheByUserId(user.id); + const keys = await this.getPublicKeyByUserId(user.id); + if (keys == null || !Array.isArray(keys)) return null; + + const exactKey = keys.find(x => x.keyId === keyId); + if (exactKey) return { user, key: exactKey }; } - // 公開鍵は複数あるが、mainっぽいのを選ぶ - const mainKey = keys.find(x => { - try { - if (x.keyId === keyId) return true; - const url = new URL(x.keyId); - const path = url.pathname.split('/').pop()?.toLowerCase(); - if (url.hash) { - if (url.hash.toLowerCase().includes('main')) { - return true; - } - } else if (path?.includes('main') || path === 'publickey') { - return true; - } - } catch { /* noop */ } + return { user, key: null }; + } - return false; - }); - return { - user, - key: mainKey ?? keys[0], - }; + @bindThis + public async getPublicKeyByUserId(userId: MiUser['id']): Promise { + return await this.publicKeyByUserIdCache.fetch( + userId, + () => this.userPublickeysRepository.find({ where: { userId } }), + v => v != null, + ); } @bindThis diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 97abfcbde8..e331ea5395 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -250,18 +250,17 @@ export class ApPersonService implements OnModuleInit { } @bindThis - async fetchPersonWithRenewal(uri: string): Promise { + async fetchPersonWithRenewal(uri: string, TTL = REMOTE_USER_CACHE_TTL): Promise { const exist = await this.fetchPerson(uri); if (exist == null) return null; - // ついでにリモートユーザーの情報が古かったら更新しておく if (this.userEntityService.isRemoteUser(exist)) { - if (exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > REMOTE_USER_CACHE_TTL) { - this.logger.debug('fetchPersonWithRenewal: renew', { uri, lastFetchedAt: exist.lastFetchedAt }); + if (TTL === 0 || exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > TTL) { + this.logger.debug('fetchPersonWithRenewal: renew', { uri, TTL, lastFetchedAt: exist.lastFetchedAt }); await this.updatePerson(exist.uri); return await this.fetchPerson(uri); } - this.logger.debug('fetchPersonWithRenewal: use cache', { uri, lastFetchedAt: exist.lastFetchedAt }); + this.logger.debug('fetchPersonWithRenewal: use cache', { uri, TTL, lastFetchedAt: exist.lastFetchedAt }); } return exist; From ea6c38cc6b095e268e2825c8765b80756a6a3bf4 Mon Sep 17 00:00:00 2001 From: tamaina Date: Fri, 1 Mar 2024 18:38:31 +0000 Subject: [PATCH 036/635] cacheRaw.date --- .../src/core/activitypub/ApDbResolverService.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 9111906538..1495ba7a95 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -53,7 +53,7 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, ) { - this.publicKeyByUserIdCache = new MemoryKVCache(1e3 * 60 * 20); // 20分 + this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); } @bindThis @@ -154,7 +154,19 @@ export class ApDbResolverService implements OnApplicationShutdown { const exactKey = keys.find(x => x.keyId === keyId); if (exactKey) return { user, key: exactKey }; - // keyIdで見つからない場合、lastFetchedAtでの更新制限を弱めて再取得 + // keyIdで見つからない場合 + // まずはキャッシュを更新して再取得 + const cacheRaw = this.publicKeyByUserIdCache.cache.get(user.id); + if (cacheRaw && cacheRaw.date > Date.now() - 1000 * 60 * 12) { + this.refreshCacheByUserId(user.id); + const keys = await this.getPublicKeyByUserId(user.id); + if (keys == null || !Array.isArray(keys)) return null; + + const exactKey = keys.find(x => x.keyId === keyId); + if (exactKey) return { user, key: exactKey }; + } + + // lastFetchedAtでの更新制限を弱めて再取得 if (user.lastFetchedAt == null || user.lastFetchedAt < new Date(Date.now() - 1000 * 60 * 12)) { const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0); if (renewed == null || renewed.isDeleted) return null; From 1357b076d02a54dbbbde3c5f19ec1a829a81ebb7 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 2 Mar 2024 13:53:52 +0000 Subject: [PATCH 037/635] use requiredInputs https://github.com/misskey-dev/misskey/pull/13464#discussion_r1509964359 --- packages/backend/src/server/ActivityPubServerService.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 204b9e7aa7..ba0adac03b 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -110,7 +110,11 @@ export class ActivityPubServerService { } try { - signature = parseRequestSignature(request.raw); + signature = parseRequestSignature(request.raw, { + requiredInputs: { + draft: ['(request-target)', 'digest', 'host', 'date'], + }, + }); } catch (e) { reply.code(401); return; From a405b6282729a3aac18f80777f134a95d4fca945 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 21:02:23 +0000 Subject: [PATCH 038/635] update @misskey-dev/node-http-message-signatures --- packages/backend/package.json | 2 +- .../src/core/activitypub/ApRequestService.ts | 14 ++++++------- .../queue/processors/InboxProcessorService.ts | 2 +- .../src/server/ActivityPubServerService.ts | 12 ++--------- packages/backend/test/unit/ap-request.ts | 8 ++++---- pnpm-lock.yaml | 20 ++++++++++++++----- 6 files changed, 30 insertions(+), 28 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 1d3f2fcd2a..82675fafc2 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.0-alpha.11", + "@misskey-dev/node-http-message-signatures": "0.0.1", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 36a4b34de1..9dda59543e 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -28,7 +28,7 @@ type PrivateKey = { keyId: string; }; -export function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; additionalHeaders: Record }) { +export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; additionalHeaders: Record }) { const u = new URL(args.url); const request: RequestLike = { url: u.href, @@ -42,10 +42,10 @@ export function createSignedPost(args: { level: string; key: PrivateKey; url: st }; // TODO: levelによって処理を分ける - const digestHeader = genRFC3230DigestHeader(args.body); + const digestHeader = await genRFC3230DigestHeader(args.body, 'SHA-256'); request.headers['Digest'] = digestHeader; - const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); + const result = await signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); return { request, @@ -53,7 +53,7 @@ export function createSignedPost(args: { level: string; key: PrivateKey; url: st }; } -export function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record }) { +export async function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record }) { const u = new URL(args.url); const request: RequestLike = { url: u.href, @@ -67,7 +67,7 @@ export function createSignedGet(args: { level: string; key: PrivateKey; url: str }; // TODO: levelによって処理を分ける - const result = signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); + const result = await signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); return { request, @@ -108,7 +108,7 @@ export class ApRequestService { public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); const key = await this.getPrivateKey(user.id, level); - const req = createSignedPost({ + const req = await createSignedPost({ level, key, url, @@ -140,7 +140,7 @@ export class ApRequestService { @bindThis public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise { const key = await this.getPrivateKey(user.id, level); - const req = createSignedGet({ + const req = await createSignedGet({ level, key, url, diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 3f10a128ed..94163a2495 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -104,7 +104,7 @@ export class InboxProcessorService { // HTTP-Signatureの検証 const errorLogger = (ms: any) => this.logger.error(ms); - const httpSignatureValidated = verifyDraftSignature(signature, authUser.key.keyPem, errorLogger); + const httpSignatureValidated = await verifyDraftSignature(signature, authUser.key.keyPem, errorLogger); this.logger.debug('Inbox message validation: ', { userId: authUser.user.id, userAcct: Acct.toString(authUser.user), diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index ba0adac03b..1dbb94a2c2 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import * as crypto from 'node:crypto'; import { IncomingMessage } from 'node:http'; import { Inject, Injectable } from '@nestjs/common'; import fastifyAccepts from '@fastify/accepts'; @@ -100,10 +99,10 @@ export class ActivityPubServerService { } @bindThis - private inbox(request: FastifyRequest, reply: FastifyReply) { + private async inbox(request: FastifyRequest, reply: FastifyReply) { let signature: ReturnType; - const verifyDigest = verifyDigestHeader(request.raw, request.rawBody || '', true); + const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true); if (!verifyDigest) { reply.code(401); return; @@ -120,13 +119,6 @@ export class ActivityPubServerService { return; } - if (signature.value.params.headers.indexOf('host') === -1 - || request.headers.host !== this.config.host) { - // Host not specified or not match. - reply.code(401); - return; - } - this.queueService.inbox(request.body as IActivity, signature); reply.code(202); diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index fc3855534d..797ef73e62 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -43,12 +43,12 @@ describe('ap-request', () => { 'User-Agent': 'UA', }; - const req = createSignedPost({ level, key, url, body, additionalHeaders: headers }); + const req = await createSignedPost({ level, key, url, body, additionalHeaders: headers }); const parsed = parseRequestSignature(req.request); expect(parsed?.version).toBe('draft'); if (!parsed) return; - const verify = verifyDraftSignature(parsed.value, keypair.publicKey); + const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); assert.deepStrictEqual(verify, true); }); }); @@ -62,12 +62,12 @@ describe('ap-request', () => { 'User-Agent': 'UA', }; - const req = createSignedGet({ level, key, url, additionalHeaders: headers }); + const req = await createSignedGet({ level, key, url, additionalHeaders: headers }); const parsed = parseRequestSignature(req.request); expect(parsed?.version).toBe('draft'); if (!parsed) return; - const verify = verifyDraftSignature(parsed.value, keypair.publicKey); + const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); assert.deepStrictEqual(verify, true); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52efc4c217..5853c00bb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.0-alpha.11 - version: 0.0.0-alpha.11 + specifier: 0.0.1 + version: 0.0.1 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4621,6 +4621,10 @@ packages: resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} dev: false + /@lapo/asn1js@1.2.4: + resolution: {integrity: sha512-mdInpQZaYUWu5QbKIB2+Vd+j6Y7cc6xQYNwYBPC9jri2rwy3tbxom0IhhT4G5WOKWO7Iht10SxYpKq+AfuH6dw==} + dev: false + /@levischuck/tiny-cbor@0.2.2: resolution: {integrity: sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw==} dev: false @@ -4750,8 +4754,10 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.0-alpha.11: - resolution: {integrity: sha512-DeLzSIquddDQp+hqD9/zS/IkpJg8Zs1h/4viavCFe37CW2W4ABFumXhYJPB07n775QEihZNuvdipoMNXl83jzw==} + /@misskey-dev/node-http-message-signatures@0.0.1: + resolution: {integrity: sha512-BaMYEOSBwBtDW0NJcljE4S39gFpgNigVzbVFlVsKzu4+k7snL72mXEaAoMCHWCw7XmdY3+fKADbQoqc4SFrpLw==} + dependencies: + '@lapo/asn1js': 1.2.4 dev: false /@misskey-dev/sharp-read-bmp@1.2.0: @@ -6641,7 +6647,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.3.3) - vue-component-type-helpers: 1.8.27 + vue-component-type-helpers: 2.0.3 transitivePeerDependencies: - encoding - supports-color @@ -19440,6 +19446,10 @@ packages: resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==} dev: true + /vue-component-type-helpers@2.0.3: + resolution: {integrity: sha512-dVPXmrwul+lLt2ErNqwfIGTWkZoPQjlarFx5pwXnCwdJ80ZaJ2nmqtt3KcUgVaKBwBQBpJqOlJPaMxICBay+Cg==} + dev: true + /vue-demi@0.14.7(vue@3.4.21): resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} engines: {node: '>=12'} From 13af6f2313ff5cda466c50ead25bb91edb9fbbd2 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 21:22:47 +0000 Subject: [PATCH 039/635] clean up --- packages/backend/src/core/SignupService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index 78a77d2adf..54c6170062 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { generateKeyPair } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; From aabdb666b7b84a259d48a39012710d9b95178c3b Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 22:03:25 +0000 Subject: [PATCH 040/635] err msg --- packages/backend/src/core/FetchInstanceMetadataService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index a1313fc257..4dc81cd6be 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -75,7 +75,7 @@ export class FetchInstanceMetadataService { const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) { - throw new Error('Skip because updated recently'); + throw new Error(`Skip because updated recently ${_instance.infoUpdatedAt.toJSON()}`); } } From 25d5a8cb7e905221b464316893d9aa79926e6c39 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:04:33 +0000 Subject: [PATCH 041/635] =?UTF-8?q?fix(backend):=20fetchInstanceMetadata?= =?UTF-8?q?=E3=81=AELock=E3=81=8C=E6=B0=B8=E9=81=A0=E3=81=AB=E8=A7=A3?= =?UTF-8?q?=E9=99=A4=E3=81=95=E3=82=8C=E3=81=AA=E3=81=84=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com> --- .../src/core/FetchInstanceMetadataService.ts | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index bc270bd28f..7e5185e960 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -51,23 +51,33 @@ export class FetchInstanceMetadataService { } @bindThis - public async tryLock(host: string): Promise { - const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'GET'); - return mutex !== '1'; + private async tryLock(host: string): Promise { + // TODO: マイグレーションなのであとで消す (2024.3.1) + this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`); + + return await this.redisClient.set( + `fetchInstanceMetadata:mutex:v2:${host}`, '1', + 'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395 + 'GET' // 古い値を返す(なかったらnull) + ); } @bindThis - public unlock(host: string): Promise<'OK'> { - return this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '0'); + private unlock(host: string): Promise { + return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`); } @bindThis public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { const host = instance.host; - // Acquire mutex to ensure no parallel runs - if (!await this.tryLock(host)) return; + try { if (!force) { + if (await this.tryLock(host) === '1') { + // 1が返ってきていたらロックされている = 何もしない + return; + } + const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { From 89e1ff699adc8bacce521d8d73e22d7439390e01 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:19:13 +0000 Subject: [PATCH 042/635] fix httpMessageSignaturesImplementationLevel validation --- .../backend/src/core/FetchInstanceMetadataService.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 4dc81cd6be..2398d94819 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -17,6 +17,7 @@ import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { REMOTE_SERVER_CACHE_TTL } from '@/const.js'; import type { DOMWindow } from 'jsdom'; +import { el } from 'date-fns/locale'; type NodeInfo = { openRegistrations?: unknown; @@ -107,8 +108,13 @@ export class FetchInstanceMetadataService { updates.openRegistrations = info.openRegistrations; updates.maintainerName = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.name ?? null) : null : null; updates.maintainerEmail = info.metadata ? info.metadata.maintainer ? (info.metadata.maintainer.email ?? null) : null : null; - if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel) { - updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel.toString() ?? '00'; + if (info.metadata && info.metadata.httpMessageSignaturesImplementationLevel && ( + info.metadata.httpMessageSignaturesImplementationLevel === '01' || + info.metadata.httpMessageSignaturesImplementationLevel === '11' + )) { + updates.httpMessageSignaturesImplementationLevel = info.metadata.httpMessageSignaturesImplementationLevel; + } else { + updates.httpMessageSignaturesImplementationLevel = '00'; } } From 2dde845738846e021fee6e6c3c0f625cd0ce1e47 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:26:35 +0000 Subject: [PATCH 043/635] fix test --- packages/backend/src/core/FetchInstanceMetadataService.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 7e5185e960..d8be1153ce 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -51,7 +51,8 @@ export class FetchInstanceMetadataService { } @bindThis - private async tryLock(host: string): Promise { + // public for test + public async tryLock(host: string): Promise { // TODO: マイグレーションなのであとで消す (2024.3.1) this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`); @@ -63,7 +64,8 @@ export class FetchInstanceMetadataService { } @bindThis - private unlock(host: string): Promise { + // public for test + public unlock(host: string): Promise { return this.redisClient.del(`fetchInstanceMetadata:mutex:v2:${host}`); } From 41a461edbe1dfe347e9a8c7394c2d6ab9e869973 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:33:08 +0000 Subject: [PATCH 044/635] fix --- .../backend/src/core/FetchInstanceMetadataService.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index d8be1153ce..d937605b3c 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -73,13 +73,14 @@ export class FetchInstanceMetadataService { public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { const host = instance.host; + // unlockされてしまうのでtry内でロックチェックをしない + if (!force && await this.tryLock(host) === '1') { + // 1が返ってきていたらロックされているという意味なので、何もしない + return; + } + try { if (!force) { - if (await this.tryLock(host) === '1') { - // 1が返ってきていたらロックされている = 何もしない - return; - } - const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) { From 2926f68d8e3cd15865f4969778ac4fed00cdb4bf Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:33:25 +0000 Subject: [PATCH 045/635] comment --- packages/backend/src/core/FetchInstanceMetadataService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index d937605b3c..dc3780e270 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -73,7 +73,7 @@ export class FetchInstanceMetadataService { public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { const host = instance.host; - // unlockされてしまうのでtry内でロックチェックをしない + // finallyでunlockされてしまうのでtry内でロックチェックをしない if (!force && await this.tryLock(host) === '1') { // 1が返ってきていたらロックされているという意味なので、何もしない return; From 64fcf736cccafc6d548a5bbd4bda29a4154ed686 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:36:03 +0000 Subject: [PATCH 046/635] comment --- packages/backend/src/core/FetchInstanceMetadataService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index dc3780e270..8d173855f3 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -74,6 +74,7 @@ export class FetchInstanceMetadataService { const host = instance.host; // finallyでunlockされてしまうのでtry内でロックチェックをしない + // (returnであってもfinallyは実行される) if (!force && await this.tryLock(host) === '1') { // 1が返ってきていたらロックされているという意味なので、何もしない return; From 7eb19d5a8e413ceb8f134fc53c2917e1a1a91fa2 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:45:47 +0000 Subject: [PATCH 047/635] improve test --- .../test/unit/FetchInstanceMetadataService.ts | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index 510b84b680..bf8f3ab0e3 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -56,6 +56,7 @@ describe('FetchInstanceMetadataService', () => { } else if (token === DI.redis) { return mockRedis; } + return null; }) .compile(); @@ -78,6 +79,7 @@ describe('FetchInstanceMetadataService', () => { httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); @@ -92,6 +94,7 @@ describe('FetchInstanceMetadataService', () => { httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); @@ -104,13 +107,30 @@ describe('FetchInstanceMetadataService', () => { const now = Date.now(); federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); + await fetchInstanceMetadataService.tryLock('example.com'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); - await fetchInstanceMetadataService.tryLock('example.com'); + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); - expect(tryLockSpy).toHaveBeenCalledTimes(2); + expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(0); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); }); + + test('Do when lock not acquired but forced', async () => { + redisClient.set = mockRedis(); + const now = Date.now(); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); + httpRequestService.getJson.mockImplementation(() => { throw Error(); }); + await fetchInstanceMetadataService.tryLock('example.com'); + const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); + const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); + + await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true); + expect(tryLockSpy).toHaveBeenCalledTimes(0); + expect(unlockSpy).toHaveBeenCalledTimes(1); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); + expect(httpRequestService.getJson).toHaveBeenCalled(); + }); }); From c7eed1c360cfec165757aa714fbd075346bbbb7a Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:49:04 +0000 Subject: [PATCH 048/635] fix --- packages/backend/src/core/FetchInstanceMetadataService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 54dc925540..e8d9cec804 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -17,7 +17,6 @@ import { bindThis } from '@/decorators.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { REMOTE_SERVER_CACHE_TTL } from '@/const.js'; import type { DOMWindow } from 'jsdom'; -import { el } from 'date-fns/locale'; type NodeInfo = { openRegistrations?: unknown; From 6a56aea4227d3459d37c17ae67ad6eb686f391d1 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:53:30 +0000 Subject: [PATCH 049/635] use Promise.all in genRSAAndEd25519KeyPair --- packages/backend/src/misc/gen-key-pair.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 58e3091369..0b033ec33e 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -6,8 +6,7 @@ import { genEd25519KeyPair, genRsaKeyPair } from '@misskey-dev/node-http-message-signatures'; export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { - const rsa = await genRsaKeyPair(rsaModulusLength); - const ed25519 = await genEd25519KeyPair(); + const [rsa, ed25519] = await Promise.all([genRsaKeyPair(rsaModulusLength), genEd25519KeyPair()]); return { publicKey: rsa.publicKey, privateKey: rsa.privateKey, From d772eacfa144ce4e2ac943c42248dcc78735b666 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 3 Mar 2024 23:57:30 +0000 Subject: [PATCH 050/635] refreshAndprepareEd25519KeyPair --- packages/backend/src/core/UserKeypairService.ts | 4 ++-- .../backend/src/core/activitypub/ApDeliverManagerService.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 4cc1fa78d6..c27f8fc71a 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -5,13 +5,13 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; +import { genEd25519KeyPair } from '@misskey-dev/node-http-message-signatures'; import type { MiUser } from '@/models/User.js'; import type { UserKeypairsRepository } from '@/models/_.js'; import { RedisKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { genEd25519KeyPair } from '@misskey-dev/node-http-message-signatures'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; @Injectable() @@ -50,7 +50,7 @@ export class UserKeypairService implements OnApplicationShutdown { } @bindThis - public async prepareEd25519KeyPair(userId: MiUser['id']): Promise { + public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { await this.refresh(userId); const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) return; diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 27a9c11dab..cd3b6a8795 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -109,7 +109,7 @@ class DeliverManager { /** * ed25519の署名がなければ追加する */ - await this.userKeypairService.prepareEd25519KeyPair(this.actor.id); + await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); //#endregion // The value flags whether it is shared or not. From 941aed6a141d12f3abc33ce07d5f7930c3d50881 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 00:05:48 +0000 Subject: [PATCH 051/635] refreshAndfindKey --- .../core/activitypub/ApDbResolverService.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 1495ba7a95..61fcc6731d 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -114,6 +114,15 @@ export class ApDbResolverService implements OnApplicationShutdown { } } + @bindThis + private async refreshAndfindKey(userId: MiUser['id'], keyId: string): Promise { + this.refreshCacheByUserId(userId); + const keys = await this.getPublicKeyByUserId(userId); + if (keys == null || !Array.isArray(keys)) return null; + + return keys.find(x => x.keyId === keyId) ?? null; + } + /** * AP Actor id => Misskey User and Key * @param uri AP Actor id @@ -158,11 +167,7 @@ export class ApDbResolverService implements OnApplicationShutdown { // まずはキャッシュを更新して再取得 const cacheRaw = this.publicKeyByUserIdCache.cache.get(user.id); if (cacheRaw && cacheRaw.date > Date.now() - 1000 * 60 * 12) { - this.refreshCacheByUserId(user.id); - const keys = await this.getPublicKeyByUserId(user.id); - if (keys == null || !Array.isArray(keys)) return null; - - const exactKey = keys.find(x => x.keyId === keyId); + const exactKey = await this.refreshAndfindKey(user.id, keyId); if (exactKey) return { user, key: exactKey }; } @@ -171,12 +176,7 @@ export class ApDbResolverService implements OnApplicationShutdown { const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0); if (renewed == null || renewed.isDeleted) return null; - this.refreshCacheByUserId(user.id); - const keys = await this.getPublicKeyByUserId(user.id); - if (keys == null || !Array.isArray(keys)) return null; - - const exactKey = keys.find(x => x.keyId === keyId); - if (exactKey) return { user, key: exactKey }; + return { user, key: await this.refreshAndfindKey(user.id, keyId) }; } return { user, key: null }; From 83f635835eb4ed4abed1d7afdc480dbe82c7cad7 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 00:12:11 +0000 Subject: [PATCH 052/635] commetn --- packages/backend/src/core/activitypub/ApRequestService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 9dda59543e..0e96a80165 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -41,7 +41,7 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u }, }; - // TODO: levelによって処理を分ける + // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする const digestHeader = await genRFC3230DigestHeader(args.body, 'SHA-256'); request.headers['Digest'] = digestHeader; @@ -66,7 +66,7 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur }, }; - // TODO: levelによって処理を分ける + // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする const result = await signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); return { From 25cc9e0bf10b6f9204ad9d3c8cee2d63b0e9117a Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 00:34:37 +0000 Subject: [PATCH 053/635] refactor public keys add --- .../activitypub/models/ApPersonService.ts | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index e331ea5395..983bf0ebc5 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -50,7 +50,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; -import type { IActor, IObject } from '../type.js'; +import type { IActor, IKey, IObject } from '../type.js'; const nameLength = 128; const summaryLength = 2048; @@ -396,20 +396,17 @@ export class ApPersonService implements OnModuleInit { })); if (person.publicKey) { - await transactionalEntityManager.save(new MiUserPublickey({ - keyId: person.publicKey.id, - userId: user.id, - keyPem: person.publicKey.publicKeyPem, - })); + const keys = new Map([ + ...(person.additionalPublicKeys ? person.additionalPublicKeys.map(key => [key.id, key] as const) : []), + [person.publicKey.id, person.publicKey], + ]); - if (person.additionalPublicKeys) { - for (const key of person.additionalPublicKeys) { - await transactionalEntityManager.save(new MiUserPublickey({ - keyId: key.id, - userId: user.id, - keyPem: key.publicKeyPem, - })); - } + for (const key of keys.values()) { + await transactionalEntityManager.save(new MiUserPublickey({ + keyId: key.id, + userId: user.id, + keyPem: key.publicKeyPem, + })); } } }); @@ -556,27 +553,21 @@ export class ApPersonService implements OnModuleInit { // Update user await this.usersRepository.update(exist.id, updates); - const availablePublicKeys = new Set(); + const publicKeys = new Map(); if (person.publicKey) { - await this.userPublickeysRepository.update({ keyId: person.publicKey.id }, { - userId: exist.id, - keyPem: person.publicKey.publicKeyPem, - }); - availablePublicKeys.add(person.publicKey.id); + (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); + publicKeys.set(person.publicKey.id, person.publicKey); - if (person.additionalPublicKeys) { - for (const key of person.additionalPublicKeys) { - await this.userPublickeysRepository.update({ keyId: key.id }, { - userId: exist.id, - keyPem: key.publicKeyPem, - }); - availablePublicKeys.add(key.id); - } + for (const key of publicKeys.values()) { + await this.userPublickeysRepository.update({ keyId: key.id }, { + userId: exist.id, + keyPem: key.publicKeyPem, + }); } } this.userPublickeysRepository.delete({ - keyId: Not(In(Array.from(availablePublicKeys))), + keyId: Not(In(Array.from(publicKeys.keys()))), userId: exist.id, }); From eefca034fc622db86c2527bed034bb9b64d7ba77 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 02:20:09 +0000 Subject: [PATCH 054/635] =?UTF-8?q?digest=E3=83=97=E3=83=AA=E3=83=AC?= =?UTF-8?q?=E3=83=B3=E3=83=80=E3=82=92=E5=BE=A9=E6=B4=BB=E3=81=95=E3=81=9B?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC実装時にどうするか考える --- packages/backend/src/core/QueueService.ts | 3 ++- .../backend/src/core/activitypub/ApRequestService.ts | 9 +++++---- .../src/queue/processors/DeliverProcessorService.ts | 2 +- packages/backend/src/queue/types.ts | 2 ++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 7ba1853f84..1e96adedfd 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -15,7 +15,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type * as Bull from 'bullmq'; -import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; +import { genRFC3230DigestHeader, type ParsedSignature } from '@misskey-dev/node-http-message-signatures'; @Injectable() export class QueueService { @@ -81,6 +81,7 @@ export class QueueService { id: user.id, }, content: contentBody, + digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'), to, isSharedInbox, }; diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 0e96a80165..8469dfbce4 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -28,7 +28,7 @@ type PrivateKey = { keyId: string; }; -export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; additionalHeaders: Record }) { +export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record }) { const u = new URL(args.url); const request: RequestLike = { url: u.href, @@ -40,9 +40,9 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u ...args.additionalHeaders, }, }; - +c // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする - const digestHeader = await genRFC3230DigestHeader(args.body, 'SHA-256'); + const digestHeader = args.digest ?? await genRFC3230DigestHeader(args.body, 'SHA-256'); request.headers['Digest'] = digestHeader; const result = await signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); @@ -105,7 +105,7 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string): Promise { + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); const key = await this.getPrivateKey(user.id, level); const req = await createSignedPost({ @@ -116,6 +116,7 @@ export class ApRequestService { additionalHeaders: { 'User-Agent': this.config.userAgent, }, + digest, }); this.logger.debug('create signed post', { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 6bbe7a4973..8f3782a048 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -76,7 +76,7 @@ export class DeliverProcessorService { await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {}); const server = await this.federatedInstanceService.fetch(host); - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel); + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel, job.data.digest); // Update stats if (server.isNotResponding) { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index fcae4c2596..135bccb60c 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -32,6 +32,8 @@ export type DeliverJobData = { user: ThinUser; /** Activity */ content: string; + /** Digest header */ + digest: string; /** inbox URL to deliver */ to: string; /** whether it is sharedInbox */ From 79249a0514e955d4f6cffebdd03620873e8bcbe7 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 02:26:16 +0000 Subject: [PATCH 055/635] fix, async --- packages/backend/src/core/QueueService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 1e96adedfd..6177aba32e 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -70,7 +70,7 @@ export class QueueService { } @bindThis - public deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) { + public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) { if (content == null) return null; if (to == null) return null; From 7a334a5e281a696416b707945a42d7a57cb8e512 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 03:12:17 +0000 Subject: [PATCH 056/635] fix --- packages/backend/src/core/activitypub/ApRequestService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 8469dfbce4..6c58bee211 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -40,7 +40,7 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u ...args.additionalHeaders, }, }; -c + // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする const digestHeader = args.digest ?? await genRFC3230DigestHeader(args.body, 'SHA-256'); request.headers['Digest'] = digestHeader; From 821a79ff28492ae1142a064a33e1a314464a1e3b Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 09:33:31 +0000 Subject: [PATCH 057/635] !== true --- packages/backend/src/queue/processors/InboxProcessorService.ts | 2 +- packages/backend/src/server/ActivityPubServerService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 94163a2495..23e377d790 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -114,7 +114,7 @@ export class InboxProcessorService { }); // また、signatureのsignerは、activity.actorと一致する必要がある - if (!httpSignatureValidated || authUser.user.uri !== activity.actor) { + if (httpSignatureValidated !== true || authUser.user.uri !== activity.actor) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る if (activity.signature?.creator) { if (activity.signature.type !== 'RsaSignature2017') { diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 1dbb94a2c2..78b89e8684 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -103,7 +103,7 @@ export class ActivityPubServerService { let signature: ReturnType; const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true); - if (!verifyDigest) { + if (verifyDigest !== true) { reply.code(401); return; } From 1af1bc87bd58e7655e72473fd82c21394f52a0a0 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 10:05:45 +0000 Subject: [PATCH 058/635] use save --- .../src/core/activitypub/models/ApPersonService.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 983bf0ebc5..a977c9251d 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -558,12 +558,11 @@ export class ApPersonService implements OnModuleInit { (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); publicKeys.set(person.publicKey.id, person.publicKey); - for (const key of publicKeys.values()) { - await this.userPublickeysRepository.update({ keyId: key.id }, { - userId: exist.id, - keyPem: key.publicKeyPem, - }); - } + await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({ + keyId: key.id, + userId: exist.id, + keyPem: key.publicKeyPem, + }))); } this.userPublickeysRepository.delete({ From 7d77c7044e49e753d2bbecaabd0ff4b832d48231 Mon Sep 17 00:00:00 2001 From: tamaina Date: Mon, 4 Mar 2024 18:47:07 +0000 Subject: [PATCH 059/635] Deliver update person when new key generated (not tested) https://github.com/misskey-dev/misskey/pull/13464#issuecomment-1977049061 --- .../backend/src/core/AccountUpdateService.ts | 13 +++++++--- packages/backend/src/core/QueueService.ts | 7 +++-- packages/backend/src/core/RelayService.ts | 4 +-- .../activitypub/ApDeliverManagerService.ts | 26 +++++++++++++------ .../src/core/activitypub/ApRequestService.ts | 7 ++++- .../processors/DeliverProcessorService.ts | 11 +++++++- packages/backend/src/queue/types.ts | 2 ++ 7 files changed, 53 insertions(+), 17 deletions(-) diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 69a57b4854..4c89f3a048 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -27,15 +27,22 @@ export class AccountUpdateService { } @bindThis - public async publishToFollowers(userId: MiUser['id']) { + /** + * ユーザーのアップデートをフォロワーに配信する + * @param userId ユーザーID + * @param isKeyUpdation Ed25519キーの作成など公開鍵のアップデートによる呼び出しか? trueにするとメインキーを使うようになる + */ + public async publishToFollowers(userId: MiUser['id'], isKeyUpdation: boolean = false) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); // フォロワーがリモートユーザーかつ投稿者がローカルユーザーならUpdateを配信 if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); - this.apDeliverManagerService.deliverToFollowers(user, content); - this.relayService.deliverToRelays(user, content); + await Promise.allSettled([ + this.apDeliverManagerService.deliverToFollowers(user, content, isKeyUpdation), + this.relayService.deliverToRelays(user, content, isKeyUpdation), + ]); } } } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index 6177aba32e..cb5dca2d39 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -70,7 +70,7 @@ export class QueueService { } @bindThis - public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean) { + public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, forceMainKey?: boolean) { if (content == null) return null; if (to == null) return null; @@ -84,6 +84,7 @@ export class QueueService { digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'), to, isSharedInbox, + forceMainKey, }; return this.deliverQueue.add(to, data, { @@ -101,10 +102,11 @@ export class QueueService { * @param user `{ id: string; }` この関数ではThinUserに変換しないので前もって変換してください * @param content IActivity | null * @param inboxes `Map` / key: to (inbox url), value: isSharedInbox (whether it is sharedInbox) + * @param forceMainKey boolean | undefined, force to use main (rsa) key * @returns void */ @bindThis - public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map) { + public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map, forceMainKey?: boolean) { if (content == null) return null; const contentBody = JSON.stringify(content); @@ -124,6 +126,7 @@ export class QueueService { content: contentBody, to: d[0], isSharedInbox: d[1], + forceMainKey, } as DeliverJobData, opts, }))); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index e9dc9b57af..52bf5d8e6a 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -111,7 +111,7 @@ export class RelayService { } @bindThis - public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any): Promise { + public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, forceMainKey?: boolean): Promise { if (activity == null) return; const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ @@ -125,7 +125,7 @@ export class RelayService { const signed = await this.apRendererService.attachLdSignature(copy, user); for (const relay of relays) { - this.queueService.deliver(user, signed, relay.inbox, false); + this.queueService.deliver(user, signed, relay.inbox, false, forceMainKey); } } } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index cd3b6a8795..cf3dce465e 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -13,6 +13,7 @@ import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { UserKeypairService } from '../UserKeypairService.js'; +import { AccountUpdateService } from '@/core/AccountUpdateService.js'; interface IRecipe { type: string; @@ -50,6 +51,7 @@ class DeliverManager { private userKeypairService: UserKeypairService, private followingsRepository: FollowingsRepository, private queueService: QueueService, + private accountUpdateService: AccountUpdateService, actor: { id: MiUser['id']; host: null; }, activity: IActivity | null, @@ -104,12 +106,16 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute(): Promise { + public async execute(opts?: { forceMainKey?: boolean }): Promise { //#region MIGRATION - /** - * ed25519の署名がなければ追加する - */ - await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); + if (opts?.forceMainKey !== true) { + /** + * ed25519の署名がなければ追加する + */ + await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); + // リモートに配信 + await this.accountUpdateService.publishToFollowers(this.actor.id, true); + } //#endregion // The value flags whether it is shared or not. @@ -163,6 +169,7 @@ export class ApDeliverManagerService { private userKeypairService: UserKeypairService, private queueService: QueueService, + private accountUpdateService: AccountUpdateService, ) { } @@ -170,18 +177,20 @@ export class ApDeliverManagerService { * Deliver activity to followers * @param actor * @param activity Activity + * @param forceMainKey Force to use main (rsa) key */ @bindThis - public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity): Promise { + public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, forceMainKey?: boolean): Promise { const manager = new DeliverManager( this.userKeypairService, this.followingsRepository, this.queueService, + this.accountUpdateService, actor, activity, ); manager.addFollowersRecipe(); - await manager.execute(); + await manager.execute({ forceMainKey }); } /** @@ -196,6 +205,7 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, + this.accountUpdateService, actor, activity, ); @@ -209,7 +219,7 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, - + this.accountUpdateService, actor, activity, ); diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 6c58bee211..132593679e 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -91,11 +91,16 @@ export class ApRequestService { this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる } + /** + * Get private key by user id and implementation level + * @param userId User id + * @param level Implementation level + */ @bindThis private async getPrivateKey(userId: MiUser['id'], level: string): Promise { const keypair = await this.userKeypairService.getUserKeypair(userId); - return (level !== '00' && keypair.ed25519PrivateKey) ? { + return (level !== '00' && level !== '10' && keypair.ed25519PrivateKey) ? { privateKeyPem: keypair.ed25519PrivateKey, keyId: `${this.config.url}/users/${userId}#ed25519-key`, } : { diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 8f3782a048..2a4dd5f997 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -76,7 +76,16 @@ export class DeliverProcessorService { await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {}); const server = await this.federatedInstanceService.fetch(host); - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, server.httpMessageSignaturesImplementationLevel, job.data.digest); + /** + * RSAキーを強制するかでレベルを変える + */ + const level = job.data.forceMainKey ? + server.httpMessageSignaturesImplementationLevel === '11' ? + '10' : + '00' + : server.httpMessageSignaturesImplementationLevel; + + await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, level, job.data.digest); // Update stats if (server.isNotResponding) { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 135bccb60c..74e0c0a66e 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -38,6 +38,8 @@ export type DeliverJobData = { to: string; /** whether it is sharedInbox */ isSharedInbox: boolean; + /** force to use main (rsa) key */ + forceMainKey?: boolean; }; export type InboxJobData = { From 15782f7f47320dd7e64b011f92073a1fa52ae770 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 03:31:13 +0000 Subject: [PATCH 060/635] =?UTF-8?q?=E5=BE=AA=E7=92=B0=E5=8F=82=E7=85=A7?= =?UTF-8?q?=E3=81=A7=E8=90=BD=E3=81=A1=E3=82=8B=E3=81=AE=E3=82=92=E8=A7=A3?= =?UTF-8?q?=E6=B6=88=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/AccountUpdateService.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 4c89f3a048..fd790913b4 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, OnModuleInit } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; import type { UsersRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; @@ -14,18 +15,24 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; @Injectable() -export class AccountUpdateService { +export class AccountUpdateService implements OnModuleInit { + private apDeliverManagerService: ApDeliverManagerService; constructor( + private moduleRef: ModuleRef, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, private userEntityService: UserEntityService, private apRendererService: ApRendererService, - private apDeliverManagerService: ApDeliverManagerService, private relayService: RelayService, ) { } + async onModuleInit() { + this.apDeliverManagerService = this.moduleRef.get(ApDeliverManagerService.name); + } + @bindThis /** * ユーザーのアップデートをフォロワーに配信する From 0082f6f8e8c5d5febd14933ba9a1ac643f70ca92 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 05:15:56 +0000 Subject: [PATCH 061/635] fix? --- packages/backend/src/core/AccountUpdateService.ts | 2 ++ packages/backend/src/core/UserKeypairService.ts | 5 +++++ .../src/core/activitypub/ApDeliverManagerService.ts | 8 -------- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index fd790913b4..5102d0508d 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -30,6 +30,8 @@ export class AccountUpdateService implements OnModuleInit { } async onModuleInit() { + // Circular dependency + // AccountUpdateService - ApDeliverManagerSevice( - DeliverManager) - UserKeypairService - AccountUpdateService this.apDeliverManagerService = this.moduleRef.get(ApDeliverManagerService.name); } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index c27f8fc71a..34199a68f4 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -13,6 +13,7 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; +import { AccountUpdateService } from '@/core/AccountUpdateService.js'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { @@ -27,6 +28,7 @@ export class UserKeypairService implements OnApplicationShutdown { private userKeypairsRepository: UserKeypairsRepository, private globalEventService: GlobalEventService, + private accountUpdateService: AccountUpdateService, ) { this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h @@ -54,12 +56,15 @@ export class UserKeypairService implements OnApplicationShutdown { await this.refresh(userId); const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) return; + const ed25519 = await genEd25519KeyPair(); await this.userKeypairsRepository.update({ userId }, { ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); + // リモートに配信 + await this.accountUpdateService.publishToFollowers(userId, true); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index cf3dce465e..1ba6416ad4 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -13,7 +13,6 @@ import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { UserKeypairService } from '../UserKeypairService.js'; -import { AccountUpdateService } from '@/core/AccountUpdateService.js'; interface IRecipe { type: string; @@ -51,7 +50,6 @@ class DeliverManager { private userKeypairService: UserKeypairService, private followingsRepository: FollowingsRepository, private queueService: QueueService, - private accountUpdateService: AccountUpdateService, actor: { id: MiUser['id']; host: null; }, activity: IActivity | null, @@ -113,8 +111,6 @@ class DeliverManager { * ed25519の署名がなければ追加する */ await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); - // リモートに配信 - await this.accountUpdateService.publishToFollowers(this.actor.id, true); } //#endregion @@ -169,7 +165,6 @@ export class ApDeliverManagerService { private userKeypairService: UserKeypairService, private queueService: QueueService, - private accountUpdateService: AccountUpdateService, ) { } @@ -185,7 +180,6 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, - this.accountUpdateService, actor, activity, ); @@ -205,7 +199,6 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, - this.accountUpdateService, actor, activity, ); @@ -219,7 +212,6 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, - this.accountUpdateService, actor, activity, ); From 2a622b02dc86add6f84319115b45e4e64df71282 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 05:20:37 +0000 Subject: [PATCH 062/635] Revert "fix?" This reverts commit 0082f6f8e8c5d5febd14933ba9a1ac643f70ca92. --- packages/backend/src/core/AccountUpdateService.ts | 2 -- packages/backend/src/core/UserKeypairService.ts | 5 ----- .../src/core/activitypub/ApDeliverManagerService.ts | 8 ++++++++ 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 5102d0508d..fd790913b4 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -30,8 +30,6 @@ export class AccountUpdateService implements OnModuleInit { } async onModuleInit() { - // Circular dependency - // AccountUpdateService - ApDeliverManagerSevice( - DeliverManager) - UserKeypairService - AccountUpdateService this.apDeliverManagerService = this.moduleRef.get(ApDeliverManagerService.name); } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 34199a68f4..c27f8fc71a 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -13,7 +13,6 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; -import { AccountUpdateService } from '@/core/AccountUpdateService.js'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { @@ -28,7 +27,6 @@ export class UserKeypairService implements OnApplicationShutdown { private userKeypairsRepository: UserKeypairsRepository, private globalEventService: GlobalEventService, - private accountUpdateService: AccountUpdateService, ) { this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h @@ -56,15 +54,12 @@ export class UserKeypairService implements OnApplicationShutdown { await this.refresh(userId); const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) return; - const ed25519 = await genEd25519KeyPair(); await this.userKeypairsRepository.update({ userId }, { ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); - // リモートに配信 - await this.accountUpdateService.publishToFollowers(userId, true); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 1ba6416ad4..cf3dce465e 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -13,6 +13,7 @@ import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { UserKeypairService } from '../UserKeypairService.js'; +import { AccountUpdateService } from '@/core/AccountUpdateService.js'; interface IRecipe { type: string; @@ -50,6 +51,7 @@ class DeliverManager { private userKeypairService: UserKeypairService, private followingsRepository: FollowingsRepository, private queueService: QueueService, + private accountUpdateService: AccountUpdateService, actor: { id: MiUser['id']; host: null; }, activity: IActivity | null, @@ -111,6 +113,8 @@ class DeliverManager { * ed25519の署名がなければ追加する */ await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); + // リモートに配信 + await this.accountUpdateService.publishToFollowers(this.actor.id, true); } //#endregion @@ -165,6 +169,7 @@ export class ApDeliverManagerService { private userKeypairService: UserKeypairService, private queueService: QueueService, + private accountUpdateService: AccountUpdateService, ) { } @@ -180,6 +185,7 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, + this.accountUpdateService, actor, activity, ); @@ -199,6 +205,7 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, + this.accountUpdateService, actor, activity, ); @@ -212,6 +219,7 @@ export class ApDeliverManagerService { this.userKeypairService, this.followingsRepository, this.queueService, + this.accountUpdateService, actor, activity, ); From 31bf1dbc95953fe481975fcf1edbd4b092be1f18 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 05:22:09 +0000 Subject: [PATCH 063/635] a --- packages/backend/src/core/UserKeypairService.ts | 13 +++++++++++-- .../src/core/activitypub/ApDeliverManagerService.ts | 10 ++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index c27f8fc71a..2341976068 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -49,17 +49,26 @@ export class UserKeypairService implements OnApplicationShutdown { return await this.cache.refresh(userId); } + /** + * + * @param userId user id + * @returns Promise true if keypair is created, false if keypair is already exists + */ @bindThis - public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { + public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { await this.refresh(userId); const keypair = await this.cache.fetch(userId); - if (keypair.ed25519PublicKey != null) return; + if (keypair.ed25519PublicKey != null) { + return false; + } + const ed25519 = await genEd25519KeyPair(); await this.userKeypairsRepository.update({ userId }, { ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); + return true; } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index cf3dce465e..e161ac9973 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -12,8 +12,8 @@ import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; -import { UserKeypairService } from '../UserKeypairService.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; +import { UserKeypairService } from '../UserKeypairService.js'; interface IRecipe { type: string; @@ -112,9 +112,11 @@ class DeliverManager { /** * ed25519の署名がなければ追加する */ - await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); - // リモートに配信 - await this.accountUpdateService.publishToFollowers(this.actor.id, true); + const created = await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); + if (created) { + // リモートに配信 + await this.accountUpdateService.publishToFollowers(this.actor.id, true); + } } //#endregion From 4b9ffb8dc0a6c05a2a7c9e7ddb0c5a44df6b83ba Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 05:38:27 +0000 Subject: [PATCH 064/635] logger --- .../backend/src/core/activitypub/ApDeliverManagerService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index e161ac9973..2046d980d4 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -14,6 +14,7 @@ import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import { UserKeypairService } from '../UserKeypairService.js'; +import Logger from '@/logger.js'; interface IRecipe { type: string; @@ -23,6 +24,8 @@ interface IFollowersRecipe extends IRecipe { type: 'Followers'; } +const logger = new Logger('deliver-manager', 'azure'); + interface IDirectRecipe extends IRecipe { type: 'Direct'; to: MiRemoteUser; @@ -114,6 +117,7 @@ class DeliverManager { */ const created = await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); if (created) { + logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); // リモートに配信 await this.accountUpdateService.publishToFollowers(this.actor.id, true); } @@ -160,6 +164,7 @@ class DeliverManager { // deliver await this.queueService.deliverMany(this.actor, this.activity, inboxes); + logger.info(`Deliver queues dispatched to ${inboxes.size} inboxes`); } } From ac4336db43ff6cdc86b9b5a99b6a7dd454ebc1a3 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 05:41:11 +0000 Subject: [PATCH 065/635] log --- .../backend/src/core/activitypub/ApDeliverManagerService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 2046d980d4..cbe17dca4a 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -164,7 +164,7 @@ class DeliverManager { // deliver await this.queueService.deliverMany(this.actor, this.activity, inboxes); - logger.info(`Deliver queues dispatched to ${inboxes.size} inboxes`); + logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor?.id} activityId=${this.activity?.id}`); } } From 6e4357c3784a9b24b407886d6c855c7c0baefe52 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 05:49:12 +0000 Subject: [PATCH 066/635] change logger --- .../core/activitypub/ApDeliverManagerService.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index cbe17dca4a..449524c5ff 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -13,8 +13,9 @@ import { bindThis } from '@/decorators.js'; import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; +import type Logger from '@/logger.js'; import { UserKeypairService } from '../UserKeypairService.js'; -import Logger from '@/logger.js'; +import { ApLoggerService } from './ApLoggerService.js'; interface IRecipe { type: string; @@ -24,8 +25,6 @@ interface IFollowersRecipe extends IRecipe { type: 'Followers'; } -const logger = new Logger('deliver-manager', 'azure'); - interface IDirectRecipe extends IRecipe { type: 'Direct'; to: MiRemoteUser; @@ -55,6 +54,7 @@ class DeliverManager { private followingsRepository: FollowingsRepository, private queueService: QueueService, private accountUpdateService: AccountUpdateService, + private logger: Logger, actor: { id: MiUser['id']; host: null; }, activity: IActivity | null, @@ -117,7 +117,7 @@ class DeliverManager { */ const created = await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); if (created) { - logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); + this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); // リモートに配信 await this.accountUpdateService.publishToFollowers(this.actor.id, true); } @@ -164,12 +164,14 @@ class DeliverManager { // deliver await this.queueService.deliverMany(this.actor, this.activity, inboxes); - logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor?.id} activityId=${this.activity?.id}`); + this.logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor.id} activityId=${this.activity?.id}`); } } @Injectable() export class ApDeliverManagerService { + private logger: Logger; + constructor( @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @@ -177,7 +179,9 @@ export class ApDeliverManagerService { private userKeypairService: UserKeypairService, private queueService: QueueService, private accountUpdateService: AccountUpdateService, + private apLoggerService: ApLoggerService, ) { + this.logger = this.apLoggerService.logger.createSubLogger('deliver-manager'); } /** @@ -193,6 +197,7 @@ export class ApDeliverManagerService { this.followingsRepository, this.queueService, this.accountUpdateService, + this.logger, actor, activity, ); @@ -213,6 +218,7 @@ export class ApDeliverManagerService { this.followingsRepository, this.queueService, this.accountUpdateService, + this.logger, actor, activity, ); @@ -227,6 +233,7 @@ export class ApDeliverManagerService { this.followingsRepository, this.queueService, this.accountUpdateService, + this.logger, actor, activity, ); From 430f0b7911066d9f74ae70d975ba8cb9db98422e Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 11:57:43 +0000 Subject: [PATCH 067/635] =?UTF-8?q?=E7=A7=98=E5=AF=86=E9=8D=B5=E3=81=AE?= =?UTF-8?q?=E5=A4=89=E6=9B=B4=E3=81=AF=E3=80=81=E3=83=95=E3=83=A9=E3=82=B0?= =?UTF-8?q?=E3=81=A7=E3=81=AF=E3=81=AA=E3=81=8F=E9=8D=B5=E3=82=92=E5=BC=95?= =?UTF-8?q?=E3=81=8D=E5=9B=9E=E3=81=99=E3=82=88=E3=81=86=E3=81=AB=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/AccountUpdateService.ts | 7 +-- packages/backend/src/core/QueueService.ts | 10 ++--- packages/backend/src/core/RelayService.ts | 5 ++- .../backend/src/core/UserKeypairService.ts | 43 +++++++++++++++++-- .../activitypub/ApDeliverManagerService.ts | 16 ++++--- .../src/core/activitypub/ApRequestService.ts | 27 +++--------- packages/backend/src/core/activitypub/type.ts | 5 +++ .../processors/DeliverProcessorService.ts | 18 ++++---- packages/backend/src/queue/types.ts | 4 +- 9 files changed, 82 insertions(+), 53 deletions(-) diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index fd790913b4..208131ed6e 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -13,6 +13,7 @@ import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import type { PrivateKey } from './activitypub/type.js'; @Injectable() export class AccountUpdateService implements OnModuleInit { @@ -39,7 +40,7 @@ export class AccountUpdateService implements OnModuleInit { * @param userId ユーザーID * @param isKeyUpdation Ed25519キーの作成など公開鍵のアップデートによる呼び出しか? trueにするとメインキーを使うようになる */ - public async publishToFollowers(userId: MiUser['id'], isKeyUpdation: boolean = false) { + public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKey) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); @@ -47,8 +48,8 @@ export class AccountUpdateService implements OnModuleInit { if (this.userEntityService.isLocalUser(user)) { const content = this.apRendererService.addContext(this.apRendererService.renderUpdate(await this.apRendererService.renderPerson(user), user)); await Promise.allSettled([ - this.apDeliverManagerService.deliverToFollowers(user, content, isKeyUpdation), - this.relayService.deliverToRelays(user, content, isKeyUpdation), + this.apDeliverManagerService.deliverToFollowers(user, content, deliverKey), + this.relayService.deliverToRelays(user, content, deliverKey), ]); } } diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index cb5dca2d39..b73d0de4d0 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import type { IActivity } from '@/core/activitypub/type.js'; +import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { Config } from '@/config.js'; @@ -70,7 +70,7 @@ export class QueueService { } @bindThis - public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, forceMainKey?: boolean) { + public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, privateKey?: PrivateKey) { if (content == null) return null; if (to == null) return null; @@ -84,7 +84,7 @@ export class QueueService { digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'), to, isSharedInbox, - forceMainKey, + privateKey, }; return this.deliverQueue.add(to, data, { @@ -106,7 +106,7 @@ export class QueueService { * @returns void */ @bindThis - public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map, forceMainKey?: boolean) { + public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map, privateKey?: PrivateKey) { if (content == null) return null; const contentBody = JSON.stringify(content); @@ -126,7 +126,7 @@ export class QueueService { content: contentBody, to: d[0], isSharedInbox: d[1], - forceMainKey, + privateKey, } as DeliverJobData, opts, }))); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index 52bf5d8e6a..c5099a5425 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -16,6 +16,7 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; +import { PrivateKey } from './activitypub/type.js'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -111,7 +112,7 @@ export class RelayService { } @bindThis - public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, forceMainKey?: boolean): Promise { + public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, privateKey?: PrivateKey): Promise { if (activity == null) return; const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ @@ -125,7 +126,7 @@ export class RelayService { const signed = await this.apRendererService.attachLdSignature(copy, user); for (const relay of relays) { - this.queueService.deliver(user, signed, relay.inbox, false, forceMainKey); + this.queueService.deliver(user, signed, relay.inbox, false, privateKey); } } } diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 2341976068..26190024cd 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -13,6 +13,7 @@ import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; +import { UserEntityService } from '@/core/entities/UserEntityService.js'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { @@ -27,6 +28,7 @@ export class UserKeypairService implements OnApplicationShutdown { private userKeypairsRepository: UserKeypairsRepository, private globalEventService: GlobalEventService, + private userEntityService: UserEntityService, ) { this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h @@ -44,6 +46,35 @@ export class UserKeypairService implements OnApplicationShutdown { return await this.cache.fetch(userId); } + /** + * + * @param userIdOrHint user id or MiUserKeypair + * @param preferType 'main' or 'ed25519'; If 'ed25519' is specified and ed25519 keypair is not exists, it will return main keypair + * @returns + */ + @bindThis + public async getLocalUserKeypairWithKeyId( + userIdOrHint: MiUser['id'] | MiUserKeypair, preferType: 'main' | 'ed25519' + ): Promise<{ keyId: string; publicKey: string; privateKey: string; }> { + const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint; + if (preferType === 'ed25519' && keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) { + return { + keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#ed25519-key`, + publicKey: keypair.ed25519PublicKey, + privateKey: keypair.ed25519PrivateKey, + }; + } + if (preferType === 'main') { + return { + keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`, + publicKey: keypair.publicKey, + privateKey: keypair.privateKey, + }; + } + + throw new Error('invalid type'); + } + @bindThis public async refresh(userId: MiUser['id']): Promise { return await this.cache.refresh(userId); @@ -52,14 +83,14 @@ export class UserKeypairService implements OnApplicationShutdown { /** * * @param userId user id - * @returns Promise true if keypair is created, false if keypair is already exists + * @returns MiUserKeypair if keypair is created, void if keypair is already exists */ @bindThis - public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { + public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { await this.refresh(userId); const keypair = await this.cache.fetch(userId); if (keypair.ed25519PublicKey != null) { - return false; + return; } const ed25519 = await genEd25519KeyPair(); @@ -68,7 +99,11 @@ export class UserKeypairService implements OnApplicationShutdown { ed25519PrivateKey: ed25519.privateKey, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); - return true; + return { + ...keypair, + ed25519PublicKey: ed25519.publicKey, + ed25519PrivateKey: ed25519.privateKey, + }; } @bindThis diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 449524c5ff..7f22f167af 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -10,7 +10,7 @@ import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; -import type { IActivity } from '@/core/activitypub/type.js'; +import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import type Logger from '@/logger.js'; @@ -109,17 +109,19 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute(opts?: { forceMainKey?: boolean }): Promise { + public async execute(opts?: { privateKey?: PrivateKey }): Promise { //#region MIGRATION - if (opts?.forceMainKey !== true) { + if (!opts?.privateKey) { /** * ed25519の署名がなければ追加する */ const created = await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); if (created) { + // createdが存在するということは新規作成されたということなので、フォロワーに配信する this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); // リモートに配信 - await this.accountUpdateService.publishToFollowers(this.actor.id, true); + const keyPair = await this.userKeypairService.getLocalUserKeypairWithKeyId(created, 'ed25519'); + await this.accountUpdateService.publishToFollowers(this.actor.id, { keyId: keyPair.keyId, privateKeyPem: keyPair.privateKey }); } } //#endregion @@ -163,7 +165,7 @@ class DeliverManager { } // deliver - await this.queueService.deliverMany(this.actor, this.activity, inboxes); + await this.queueService.deliverMany(this.actor, this.activity, inboxes, opts?.privateKey); this.logger.info(`Deliver queues dispatched: inboxes=${inboxes.size} actorId=${this.actor.id} activityId=${this.activity?.id}`); } } @@ -191,7 +193,7 @@ export class ApDeliverManagerService { * @param forceMainKey Force to use main (rsa) key */ @bindThis - public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, forceMainKey?: boolean): Promise { + public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, privateKey?: PrivateKey): Promise { const manager = new DeliverManager( this.userKeypairService, this.followingsRepository, @@ -202,7 +204,7 @@ export class ApDeliverManagerService { activity, ); manager.addFollowersRecipe(); - await manager.execute({ forceMainKey }); + await manager.execute({ privateKey }); } /** diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 132593679e..a6b9053706 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -15,18 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; - -type Signed = { - request: Request; - signingString: string; - signature: string; - signatureHeader: string; -}; - -type PrivateKey = { - privateKeyPem: string; - keyId: string; -}; +import type { PrivateKey } from './type.js'; export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record }) { const u = new URL(args.url); @@ -98,21 +87,19 @@ export class ApRequestService { */ @bindThis private async getPrivateKey(userId: MiUser['id'], level: string): Promise { - const keypair = await this.userKeypairService.getUserKeypair(userId); + const type = level === '00' || level === '10' ? 'ed25519' : 'main'; + const keypair = await this.userKeypairService.getLocalUserKeypairWithKeyId(userId, type); - return (level !== '00' && level !== '10' && keypair.ed25519PrivateKey) ? { - privateKeyPem: keypair.ed25519PrivateKey, - keyId: `${this.config.url}/users/${userId}#ed25519-key`, - } : { + return { + keyId: keypair.keyId, privateKeyPem: keypair.privateKey, - keyId: `${this.config.url}/users/${userId}#main-key`, }; } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string): Promise { + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKey): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); - const key = await this.getPrivateKey(user.id, level); + key = key ?? await this.getPrivateKey(user.id, level); const req = await createSignedPost({ level, key, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 0b34fd7664..54d1c52dc0 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -326,3 +326,8 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; + +export type PrivateKey = { + privateKeyPem: string; + keyId: string; +}; diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 2a4dd5f997..47a69e7622 100644 --- a/packages/backend/src/queue/processors/DeliverProcessorService.ts +++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts @@ -76,16 +76,14 @@ export class DeliverProcessorService { await this.fetchInstanceMetadataService.fetchInstanceMetadata(_server).then(() => {}); const server = await this.federatedInstanceService.fetch(host); - /** - * RSAキーを強制するかでレベルを変える - */ - const level = job.data.forceMainKey ? - server.httpMessageSignaturesImplementationLevel === '11' ? - '10' : - '00' - : server.httpMessageSignaturesImplementationLevel; - - await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, level, job.data.digest); + await this.apRequestService.signedPost( + job.data.user, + job.data.to, + job.data.content, + server.httpMessageSignaturesImplementationLevel, + job.data.digest, + job.data.privateKey, + ); // Update stats if (server.isNotResponding) { diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 74e0c0a66e..12e3136270 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -8,7 +8,7 @@ import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiUser } from '@/models/User.js'; import type { MiWebhook } from '@/models/Webhook.js'; -import type { IActivity } from '@/core/activitypub/type.js'; +import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; /** @@ -39,7 +39,7 @@ export type DeliverJobData = { /** whether it is sharedInbox */ isSharedInbox: boolean; /** force to use main (rsa) key */ - forceMainKey?: boolean; + privateKey?: PrivateKey; }; export type InboxJobData = { From e4fea42436e82f3e08411bac253f8c3fe8d5f27c Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 12:30:22 +0000 Subject: [PATCH 068/635] addAllKnowingSharedInboxRecipe --- .../backend/src/core/UserKeypairService.ts | 23 ++++--- .../backend/src/core/UserSuspendService.ts | 66 +++++-------------- .../activitypub/ApDeliverManagerService.ts | 39 ++++++++++- .../src/core/activitypub/ApRequestService.ts | 3 +- 4 files changed, 65 insertions(+), 66 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 26190024cd..6ec9a8b333 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -49,30 +49,29 @@ export class UserKeypairService implements OnApplicationShutdown { /** * * @param userIdOrHint user id or MiUserKeypair - * @param preferType 'main' or 'ed25519'; If 'ed25519' is specified and ed25519 keypair is not exists, it will return main keypair + * @param preferType If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists. Otherwise, main keypair is returned. * @returns */ @bindThis public async getLocalUserKeypairWithKeyId( - userIdOrHint: MiUser['id'] | MiUserKeypair, preferType: 'main' | 'ed25519' + userIdOrHint: MiUser['id'] | MiUserKeypair, preferType?: string, ): Promise<{ keyId: string; publicKey: string; privateKey: string; }> { const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint; - if (preferType === 'ed25519' && keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) { + if ( + preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase()) && + keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null + ) { return { keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#ed25519-key`, publicKey: keypair.ed25519PublicKey, privateKey: keypair.ed25519PrivateKey, }; } - if (preferType === 'main') { - return { - keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`, - publicKey: keypair.publicKey, - privateKey: keypair.privateKey, - }; - } - - throw new Error('invalid type'); + return { + keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`, + publicKey: keypair.publicKey, + privateKey: keypair.privateKey, + }; } @bindThis diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index d594a223f4..af42e4405b 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -3,27 +3,23 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Inject, Injectable } from '@nestjs/common'; -import { Not, IsNull } from 'typeorm'; -import type { FollowingsRepository } from '@/models/_.js'; +import { Injectable } from '@nestjs/common'; import type { MiUser } from '@/models/User.js'; -import { QueueService } from '@/core/QueueService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; -import { DI } from '@/di-symbols.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; +import { UserKeypairService } from './UserKeypairService.js'; +import { ApDeliverManagerService } from './activitypub/ApDeliverManagerService.js'; @Injectable() export class UserSuspendService { constructor( - @Inject(DI.followingsRepository) - private followingsRepository: FollowingsRepository, - private userEntityService: UserEntityService, - private queueService: QueueService, private globalEventService: GlobalEventService, private apRendererService: ApRendererService, + private userKeypairService: UserKeypairService, + private apDeliverManagerService: ApDeliverManagerService, ) { } @@ -32,28 +28,12 @@ export class UserSuspendService { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: true }); if (this.userEntityService.isLocalUser(user)) { - // 知り得る全SharedInboxにDelete配信 const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); - - const queue: string[] = []; - - const followings = await this.followingsRepository.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ['followerSharedInbox', 'followeeSharedInbox'], - }); - - const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - this.queueService.deliver(user, content, inbox, true); - } + const manager = this.apDeliverManagerService.createDeliverManager(user, content); + manager.addAllKnowingSharedInboxRecipe(); + // process delivre時にはキーペアが消去されているはずなので、ここで挿入する + const keypairs = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); + manager.execute({ privateKey: { keyId: keypairs.keyId, privateKeyPem: keypairs.privateKey } }); } } @@ -62,28 +42,12 @@ export class UserSuspendService { this.globalEventService.publishInternalEvent('userChangeSuspendedState', { id: user.id, isSuspended: false }); if (this.userEntityService.isLocalUser(user)) { - // 知り得る全SharedInboxにUndo Delete配信 const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); - - const queue: string[] = []; - - const followings = await this.followingsRepository.find({ - where: [ - { followerSharedInbox: Not(IsNull()) }, - { followeeSharedInbox: Not(IsNull()) }, - ], - select: ['followerSharedInbox', 'followeeSharedInbox'], - }); - - const inboxes = followings.map(x => x.followerSharedInbox ?? x.followeeSharedInbox); - - for (const inbox of inboxes) { - if (inbox != null && !queue.includes(inbox)) queue.push(inbox); - } - - for (const inbox of queue) { - this.queueService.deliver(user as any, content, inbox, true); - } + const manager = this.apDeliverManagerService.createDeliverManager(user, content); + manager.addAllKnowingSharedInboxRecipe(); + // process delivre時にはキーペアが消去されているはずなので、ここで挿入する + const keypairs = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); + manager.execute({ privateKey: { keyId: keypairs.keyId, privateKeyPem: keypairs.privateKey } }); } } } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 7f22f167af..1c533ef7c3 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -30,12 +30,19 @@ interface IDirectRecipe extends IRecipe { to: MiRemoteUser; } +interface IAllKnowingSharedInboxRecipe extends IRecipe { + type: 'AllKnowingSharedInbox'; +} + const isFollowers = (recipe: IRecipe): recipe is IFollowersRecipe => recipe.type === 'Followers'; const isDirect = (recipe: IRecipe): recipe is IDirectRecipe => recipe.type === 'Direct'; +const isAllKnowingSharedInbox = (recipe: IRecipe): recipe is IAllKnowingSharedInboxRecipe => + recipe.type === 'AllKnowingSharedInbox'; + class DeliverManager { private actor: ThinUser; private activity: IActivity | null; @@ -96,6 +103,18 @@ class DeliverManager { this.addRecipe(recipe); } + /** + * Add recipe for all-knowing shared inbox deliver + */ + @bindThis + public addAllKnowingSharedInboxRecipe(): void { + const deliver: IAllKnowingSharedInboxRecipe = { + type: 'AllKnowingSharedInbox', + }; + + this.addRecipe(deliver); + } + /** * Add recipe * @param recipe Recipe @@ -120,16 +139,33 @@ class DeliverManager { // createdが存在するということは新規作成されたということなので、フォロワーに配信する this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); // リモートに配信 - const keyPair = await this.userKeypairService.getLocalUserKeypairWithKeyId(created, 'ed25519'); + const keyPair = await this.userKeypairService.getLocalUserKeypairWithKeyId(created, 'main'); await this.accountUpdateService.publishToFollowers(this.actor.id, { keyId: keyPair.keyId, privateKeyPem: keyPair.privateKey }); } } //#endregion + //#region correct inboxes by recipes // The value flags whether it is shared or not. // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map(); + if (this.recipes.some(r => isAllKnowingSharedInbox(r))) { + // all-knowing shared inbox + const followings = await this.followingsRepository.find({ + where: [ + { followerSharedInbox: Not(IsNull()) }, + { followeeSharedInbox: Not(IsNull()) }, + ], + select: ['followerSharedInbox', 'followeeSharedInbox'], + }); + + for (const following of followings) { + if (following.followeeSharedInbox) inboxes.set(following.followeeSharedInbox, true); + if (following.followerSharedInbox) inboxes.set(following.followerSharedInbox, true); + } + } + // build inbox list // Process follower recipes first to avoid duplication when processing direct recipes later. if (this.recipes.some(r => isFollowers(r))) { @@ -163,6 +199,7 @@ class DeliverManager { inboxes.set(recipe.to.inbox, false); } + //#endregion // deliver await this.queueService.deliverMany(this.actor, this.activity, inboxes, opts?.privateKey); diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index a6b9053706..6c0df36d7e 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -87,8 +87,7 @@ export class ApRequestService { */ @bindThis private async getPrivateKey(userId: MiUser['id'], level: string): Promise { - const type = level === '00' || level === '10' ? 'ed25519' : 'main'; - const keypair = await this.userKeypairService.getLocalUserKeypairWithKeyId(userId, type); + const keypair = await this.userKeypairService.getLocalUserKeypairWithKeyId(userId, level); return { keyId: keypair.keyId, From 021801c721bb4406460a10fb40c6822b6a23e1dd Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 12:55:30 +0000 Subject: [PATCH 069/635] nanka meccha kaeta --- packages/backend/src/core/QueueService.ts | 4 +-- packages/backend/src/core/RelayService.ts | 12 +++---- .../backend/src/core/UserSuspendService.ts | 8 ++--- .../activitypub/ApDeliverManagerService.ts | 2 +- .../src/core/activitypub/ApRendererService.ts | 8 ++--- .../src/core/activitypub/ApRequestService.ts | 31 +++++++------------ packages/backend/src/core/activitypub/type.ts | 2 +- packages/backend/test/unit/ap-request.ts | 4 +-- 8 files changed, 31 insertions(+), 40 deletions(-) diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index b73d0de4d0..afea871575 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -84,7 +84,7 @@ export class QueueService { digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'), to, isSharedInbox, - privateKey, + privateKey: privateKey && { keyId: privateKey.keyId, privateKey: privateKey.privateKey }, }; return this.deliverQueue.add(to, data, { @@ -126,7 +126,7 @@ export class QueueService { content: contentBody, to: d[0], isSharedInbox: d[1], - privateKey, + privateKey: privateKey && { keyId: privateKey.keyId, privateKey: privateKey.privateKey }, } as DeliverJobData, opts, }))); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index c5099a5425..f106ae1669 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -16,7 +16,8 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; -import { PrivateKey } from './activitypub/type.js'; +import { UserKeypairService } from './UserKeypairService.js'; +import type { PrivateKey } from './activitypub/type.js'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -35,6 +36,7 @@ export class RelayService { private queueService: QueueService, private createSystemUserService: CreateSystemUserService, private apRendererService: ApRendererService, + private userKeypairService: UserKeypairService, ) { this.relaysCache = new MemorySingleCache(1000 * 60 * 10); } @@ -122,11 +124,9 @@ export class RelayService { const copy = deepClone(activity); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; + privateKey = privateKey ?? await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id); + const signed = await this.apRendererService.attachLdSignature(copy, user, privateKey); - const signed = await this.apRendererService.attachLdSignature(copy, user); - - for (const relay of relays) { - this.queueService.deliver(user, signed, relay.inbox, false, privateKey); - } + this.queueService.deliverMany(user, signed, new Map(relays.map(({ inbox }) => [inbox, false])), privateKey); } } diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index af42e4405b..042e13a540 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -32,8 +32,8 @@ export class UserSuspendService { const manager = this.apDeliverManagerService.createDeliverManager(user, content); manager.addAllKnowingSharedInboxRecipe(); // process delivre時にはキーペアが消去されているはずなので、ここで挿入する - const keypairs = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); - manager.execute({ privateKey: { keyId: keypairs.keyId, privateKeyPem: keypairs.privateKey } }); + const privateKey = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); + manager.execute({ privateKey }); } } @@ -46,8 +46,8 @@ export class UserSuspendService { const manager = this.apDeliverManagerService.createDeliverManager(user, content); manager.addAllKnowingSharedInboxRecipe(); // process delivre時にはキーペアが消去されているはずなので、ここで挿入する - const keypairs = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); - manager.execute({ privateKey: { keyId: keypairs.keyId, privateKeyPem: keypairs.privateKey } }); + const privateKey = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); + manager.execute({ privateKey }); } } } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 1c533ef7c3..4b0dd1403d 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -140,7 +140,7 @@ class DeliverManager { this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); // リモートに配信 const keyPair = await this.userKeypairService.getLocalUserKeypairWithKeyId(created, 'main'); - await this.accountUpdateService.publishToFollowers(this.actor.id, { keyId: keyPair.keyId, privateKeyPem: keyPair.privateKey }); + await this.accountUpdateService.publishToFollowers(this.actor.id, keyPair); } } //#endregion diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index d929fa4ee1..e4b3074abd 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -30,7 +30,7 @@ import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; -import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; +import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate, PrivateKey } from './type.js'; @Injectable() export class ApRendererService { @@ -657,12 +657,10 @@ export class ApRendererService { } @bindThis - public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }): Promise { - const keypair = await this.userKeypairService.getUserKeypair(user.id); - + public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }, key: PrivateKey): Promise { const ldSignature = this.ldSignatureService.use(); ldSignature.debug = false; - activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`); + activity = await ldSignature.signRsaSignature2017(activity, key.privateKey, key.keyId); return activity; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 6c0df36d7e..c9bcbd961a 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -34,7 +34,11 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u const digestHeader = args.digest ?? await genRFC3230DigestHeader(args.body, 'SHA-256'); request.headers['Digest'] = digestHeader; - const result = await signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'digest']); + const result = await signAsDraftToRequest( + request, + { keyId: args.key.keyId, privateKeyPem: args.key.privateKey }, + ['(request-target)', 'date', 'host', 'digest'], + ); return { request, @@ -56,7 +60,11 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur }; // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする - const result = await signAsDraftToRequest(request, args.key, ['(request-target)', 'date', 'host', 'accept']); + const result = await signAsDraftToRequest( + request, + { keyId: args.key.keyId, privateKeyPem: args.key.privateKey }, + ['(request-target)', 'date', 'host', 'accept'], + ); return { request, @@ -80,25 +88,10 @@ export class ApRequestService { this.logger = this.loggerService?.getLogger('ap-request'); // なぜか TypeError: Cannot read properties of undefined (reading 'getLogger') と言われる } - /** - * Get private key by user id and implementation level - * @param userId User id - * @param level Implementation level - */ - @bindThis - private async getPrivateKey(userId: MiUser['id'], level: string): Promise { - const keypair = await this.userKeypairService.getLocalUserKeypairWithKeyId(userId, level); - - return { - keyId: keypair.keyId, - privateKeyPem: keypair.privateKey, - }; - } - @bindThis public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKey): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); - key = key ?? await this.getPrivateKey(user.id, level); + key = key ?? await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, level); const req = await createSignedPost({ level, key, @@ -131,7 +124,7 @@ export class ApRequestService { */ @bindThis public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise { - const key = await this.getPrivateKey(user.id, level); + const key = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, level); const req = await createSignedGet({ level, key, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 54d1c52dc0..6fd5ada9fe 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -328,6 +328,6 @@ export const isFlag = (object: IObject): object is IFlag => getApType(object) == export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; export type PrivateKey = { - privateKeyPem: string; + privateKey: string; keyId: string; }; diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index 797ef73e62..1c0159ed95 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -35,7 +35,7 @@ describe('ap-request', () => { describe.each(['00', '01'])('createSignedPost with verify', (level) => { test('pass', async () => { const keypair = await getKeyPair(level); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const key = { keyId: 'x', 'privateKey': keypair.privateKey }; const url = 'https://example.com/inbox'; const activity = { a: 1 }; const body = JSON.stringify(activity); @@ -56,7 +56,7 @@ describe('ap-request', () => { describe.each(['00', '01'])('createSignedGet with verify', (level) => { test('pass', async () => { const keypair = await getKeyPair(level); - const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; + const key = { keyId: 'x', 'privateKey': keypair.privateKey }; const url = 'https://example.com/outbox'; const headers = { 'User-Agent': 'UA', From 6b02efac32b4068f67196705bd076d9f3e6d8835 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 13:18:00 +0000 Subject: [PATCH 070/635] delivre --- packages/backend/src/core/UserSuspendService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 042e13a540..7abeb3d46d 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -31,7 +31,7 @@ export class UserSuspendService { const content = this.apRendererService.addContext(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user)); const manager = this.apDeliverManagerService.createDeliverManager(user, content); manager.addAllKnowingSharedInboxRecipe(); - // process delivre時にはキーペアが消去されているはずなので、ここで挿入する + // process deliver時にはキーペアが消去されているはずなので、ここで挿入する const privateKey = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); manager.execute({ privateKey }); } @@ -45,7 +45,7 @@ export class UserSuspendService { const content = this.apRendererService.addContext(this.apRendererService.renderUndo(this.apRendererService.renderDelete(this.userEntityService.genLocalUserUri(user.id), user), user)); const manager = this.apDeliverManagerService.createDeliverManager(user, content); manager.addAllKnowingSharedInboxRecipe(); - // process delivre時にはキーペアが消去されているはずなので、ここで挿入する + // process deliver時にはキーペアが消去されているはずなので、ここで挿入する const privateKey = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); manager.execute({ privateKey }); } From 0e509c440e7ede74639333fc4a36dde91732da67 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 13:58:29 +0000 Subject: [PATCH 071/635] =?UTF-8?q?=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7?= =?UTF-8?q?=E3=83=A5=E6=9C=89=E5=8A=B9=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF?= =?UTF-8?q?=E3=81=AF=E3=83=AD=E3=83=83=E3=82=AF=E5=8F=96=E5=BE=97=E5=89=8D?= =?UTF-8?q?=E3=81=AB=E8=A1=8C=E3=81=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/FetchInstanceMetadataService.ts | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index e8d9cec804..9d1ce3ac5d 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -63,7 +63,7 @@ export class FetchInstanceMetadataService { return await this.redisClient.set( `fetchInstanceMetadata:mutex:v2:${host}`, '1', 'EX', 30, // 30秒したら自動でロック解除 https://github.com/misskey-dev/misskey/issues/13506#issuecomment-1975375395 - 'GET' // 古い値を返す(なかったらnull) + 'GET', // 古い値を返す(なかったらnull) ); } @@ -77,22 +77,24 @@ export class FetchInstanceMetadataService { public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise { const host = instance.host; - // finallyでunlockされてしまうのでtry内でロックチェックをしない - // (returnであってもfinallyは実行される) - if (!force && await this.tryLock(host) === '1') { - // 1が返ってきていたらロックされているという意味なので、何もしない - return; + if (!force) { + // キャッシュ有効チェックはロック取得前に行う + const _instance = await this.federatedInstanceService.fetch(host); + const now = Date.now(); + if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) { + this.logger.debug(`Skip because updated recently ${_instance.infoUpdatedAt.toJSON()}`); + return; + } + + // finallyでunlockされてしまうのでtry内でロックチェックをしない + // (returnであってもfinallyは実行される) + if (await this.tryLock(host) === '1') { + // 1が返ってきていたら他にロックされているという意味なので、何もしない + return; + } } try { - if (!force) { - const _instance = await this.federatedInstanceService.fetch(host); - const now = Date.now(); - if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) { - throw new Error(`Skip because updated recently ${_instance.infoUpdatedAt.toJSON()}`); - } - } - this.logger.info(`Fetching metadata of ${instance.host} ...`); const [info, dom, manifest] = await Promise.all([ From 834f46537d06b1b6e17ea2e36668811302749ff7 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 15:26:25 +0000 Subject: [PATCH 072/635] @misskey-dev/node-http-message-signatures@0.0.3 --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 82675fafc2..8b310c2d61 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.1", + "@misskey-dev/node-http-message-signatures": "0.0.3", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5853c00bb7..1ba0cfcf74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.1 - version: 0.0.1 + specifier: 0.0.3 + version: 0.0.3 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4754,8 +4754,9 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.1: - resolution: {integrity: sha512-BaMYEOSBwBtDW0NJcljE4S39gFpgNigVzbVFlVsKzu4+k7snL72mXEaAoMCHWCw7XmdY3+fKADbQoqc4SFrpLw==} + /@misskey-dev/node-http-message-signatures@0.0.3: + resolution: {integrity: sha512-IDsdj01d0Jz/D5L4ubHGOeBLEjbIpHpGIrS7p0t2Z36ahP5cvzeawkxZW2ZuqyWQI4JendJWhGnSmDrH4N3IFg==} + engines: {node: '>=18.4.0'} dependencies: '@lapo/asn1js': 1.2.4 dev: false @@ -6647,7 +6648,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.3.3) - vue-component-type-helpers: 2.0.3 + vue-component-type-helpers: 2.0.5 transitivePeerDependencies: - encoding - supports-color @@ -19446,8 +19447,8 @@ packages: resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==} dev: true - /vue-component-type-helpers@2.0.3: - resolution: {integrity: sha512-dVPXmrwul+lLt2ErNqwfIGTWkZoPQjlarFx5pwXnCwdJ80ZaJ2nmqtt3KcUgVaKBwBQBpJqOlJPaMxICBay+Cg==} + /vue-component-type-helpers@2.0.5: + resolution: {integrity: sha512-v9N4ufDSnd8YHcDq/vURPjxDyBVak5ZVAQ6aGNIrf7ZAj/VxRKpLZXFHEaqt9yHkWi0/TZp76Jmf8yNJxDQi4g==} dev: true /vue-demi@0.14.7(vue@3.4.21): From 689a9ce5f977b00d3a2a47cfcce086ff6fa8582a Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 15:53:24 +0000 Subject: [PATCH 073/635] PrivateKeyPem --- packages/backend/package.json | 2 +- packages/backend/src/core/AccountUpdateService.ts | 4 ++-- packages/backend/src/core/QueueService.ts | 12 ++++++------ packages/backend/src/core/RelayService.ts | 6 +++--- packages/backend/src/core/UserKeypairService.ts | 12 +++++------- packages/backend/src/core/UserSuspendService.ts | 4 ++-- .../src/core/activitypub/ApDeliverManagerService.ts | 9 +++++---- .../src/core/activitypub/ApRendererService.ts | 8 ++++---- .../backend/src/core/activitypub/ApRequestService.ts | 12 ++++++------ packages/backend/src/core/activitypub/type.ts | 5 ----- packages/backend/src/queue/types.ts | 6 +++--- pnpm-lock.yaml | 8 ++++---- 12 files changed, 41 insertions(+), 47 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 8b310c2d61..912768c934 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.3", + "@misskey-dev/node-http-message-signatures": "0.0.4", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index 208131ed6e..d4a7b14554 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -13,7 +13,7 @@ import { RelayService } from '@/core/RelayService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; -import type { PrivateKey } from './activitypub/type.js'; +import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; @Injectable() export class AccountUpdateService implements OnModuleInit { @@ -40,7 +40,7 @@ export class AccountUpdateService implements OnModuleInit { * @param userId ユーザーID * @param isKeyUpdation Ed25519キーの作成など公開鍵のアップデートによる呼び出しか? trueにするとメインキーを使うようになる */ - public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKey) { + public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKeyWithPem) { const user = await this.usersRepository.findOneBy({ id: userId }); if (user == null) throw new Error('user not found'); diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index afea871575..a0ec9987a7 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; +import type { IActivity } from '@/core/activitypub/type.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { Config } from '@/config.js'; @@ -15,7 +15,7 @@ import type { Antenna } from '@/server/api/endpoints/i/import-antennas.js'; import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, RelationshipQueue, SystemQueue, WebhookDeliverQueue } from './QueueModule.js'; import type { DbJobData, DeliverJobData, RelationshipJobData, ThinUser } from '../queue/types.js'; import type * as Bull from 'bullmq'; -import { genRFC3230DigestHeader, type ParsedSignature } from '@misskey-dev/node-http-message-signatures'; +import { genRFC3230DigestHeader, type PrivateKeyWithPem, type ParsedSignature } from '@misskey-dev/node-http-message-signatures'; @Injectable() export class QueueService { @@ -70,7 +70,7 @@ export class QueueService { } @bindThis - public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, privateKey?: PrivateKey) { + public async deliver(user: ThinUser, content: IActivity | null, to: string | null, isSharedInbox: boolean, privateKey?: PrivateKeyWithPem) { if (content == null) return null; if (to == null) return null; @@ -84,7 +84,7 @@ export class QueueService { digest: await genRFC3230DigestHeader(contentBody, 'SHA-256'), to, isSharedInbox, - privateKey: privateKey && { keyId: privateKey.keyId, privateKey: privateKey.privateKey }, + privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem }, }; return this.deliverQueue.add(to, data, { @@ -106,7 +106,7 @@ export class QueueService { * @returns void */ @bindThis - public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map, privateKey?: PrivateKey) { + public async deliverMany(user: ThinUser, content: IActivity | null, inboxes: Map, privateKey?: PrivateKeyWithPem) { if (content == null) return null; const contentBody = JSON.stringify(content); @@ -126,7 +126,7 @@ export class QueueService { content: contentBody, to: d[0], isSharedInbox: d[1], - privateKey: privateKey && { keyId: privateKey.keyId, privateKey: privateKey.privateKey }, + privateKey: privateKey && { keyId: privateKey.keyId, privateKeyPem: privateKey.privateKeyPem }, } as DeliverJobData, opts, }))); diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index f106ae1669..5e4b6d36ae 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -17,7 +17,7 @@ import { DI } from '@/di-symbols.js'; import { deepClone } from '@/misc/clone.js'; import { bindThis } from '@/decorators.js'; import { UserKeypairService } from './UserKeypairService.js'; -import type { PrivateKey } from './activitypub/type.js'; +import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; const ACTOR_USERNAME = 'relay.actor' as const; @@ -114,7 +114,7 @@ export class RelayService { } @bindThis - public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, privateKey?: PrivateKey): Promise { + public async deliverToRelays(user: { id: MiUser['id']; host: null; }, activity: any, privateKey?: PrivateKeyWithPem): Promise { if (activity == null) return; const relays = await this.relaysCache.fetch(() => this.relaysRepository.findBy({ @@ -124,7 +124,7 @@ export class RelayService { const copy = deepClone(activity); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; - privateKey = privateKey ?? await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id); + privateKey = privateKey ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id); const signed = await this.apRendererService.attachLdSignature(copy, user, privateKey); this.queueService.deliverMany(user, signed, new Map(relays.map(({ inbox }) => [inbox, false])), privateKey); diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 6ec9a8b333..00f97143c8 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { genEd25519KeyPair } from '@misskey-dev/node-http-message-signatures'; +import { genEd25519KeyPair, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; import type { MiUser } from '@/models/User.js'; import type { UserKeypairsRepository } from '@/models/_.js'; import { RedisKVCache } from '@/misc/cache.js'; @@ -53,9 +53,9 @@ export class UserKeypairService implements OnApplicationShutdown { * @returns */ @bindThis - public async getLocalUserKeypairWithKeyId( + public async getLocalUserPrivateKeyPem( userIdOrHint: MiUser['id'] | MiUserKeypair, preferType?: string, - ): Promise<{ keyId: string; publicKey: string; privateKey: string; }> { + ): Promise { const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint; if ( preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase()) && @@ -63,14 +63,12 @@ export class UserKeypairService implements OnApplicationShutdown { ) { return { keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#ed25519-key`, - publicKey: keypair.ed25519PublicKey, - privateKey: keypair.ed25519PrivateKey, + privateKeyPem: keypair.ed25519PrivateKey, }; } return { keyId: `${this.userEntityService.genLocalUserUri(keypair.userId)}#main-key`, - publicKey: keypair.publicKey, - privateKey: keypair.privateKey, + privateKeyPem: keypair.privateKey, }; } diff --git a/packages/backend/src/core/UserSuspendService.ts b/packages/backend/src/core/UserSuspendService.ts index 7abeb3d46d..fc5a68c72e 100644 --- a/packages/backend/src/core/UserSuspendService.ts +++ b/packages/backend/src/core/UserSuspendService.ts @@ -32,7 +32,7 @@ export class UserSuspendService { const manager = this.apDeliverManagerService.createDeliverManager(user, content); manager.addAllKnowingSharedInboxRecipe(); // process deliver時にはキーペアが消去されているはずなので、ここで挿入する - const privateKey = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); + const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main'); manager.execute({ privateKey }); } } @@ -46,7 +46,7 @@ export class UserSuspendService { const manager = this.apDeliverManagerService.createDeliverManager(user, content); manager.addAllKnowingSharedInboxRecipe(); // process deliver時にはキーペアが消去されているはずなので、ここで挿入する - const privateKey = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, 'main'); + const privateKey = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, 'main'); manager.execute({ privateKey }); } } diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 4b0dd1403d..e09b548d3a 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -10,12 +10,13 @@ import type { FollowingsRepository } from '@/models/_.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; import { QueueService } from '@/core/QueueService.js'; import { bindThis } from '@/decorators.js'; -import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; +import type { IActivity } from '@/core/activitypub/type.js'; import { ThinUser } from '@/queue/types.js'; import { AccountUpdateService } from '@/core/AccountUpdateService.js'; import type Logger from '@/logger.js'; import { UserKeypairService } from '../UserKeypairService.js'; import { ApLoggerService } from './ApLoggerService.js'; +import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; interface IRecipe { type: string; @@ -128,7 +129,7 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute(opts?: { privateKey?: PrivateKey }): Promise { + public async execute(opts?: { privateKey?: PrivateKeyWithPem }): Promise { //#region MIGRATION if (!opts?.privateKey) { /** @@ -139,7 +140,7 @@ class DeliverManager { // createdが存在するということは新規作成されたということなので、フォロワーに配信する this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); // リモートに配信 - const keyPair = await this.userKeypairService.getLocalUserKeypairWithKeyId(created, 'main'); + const keyPair = await this.userKeypairService.getLocalUserPrivateKeyPem(created, 'main'); await this.accountUpdateService.publishToFollowers(this.actor.id, keyPair); } } @@ -230,7 +231,7 @@ export class ApDeliverManagerService { * @param forceMainKey Force to use main (rsa) key */ @bindThis - public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, privateKey?: PrivateKey): Promise { + public async deliverToFollowers(actor: { id: MiLocalUser['id']; host: null; }, activity: IActivity, privateKey?: PrivateKeyWithPem): Promise { const manager = new DeliverManager( this.userKeypairService, this.followingsRepository, diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index e4b3074abd..5d9ca74fe1 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -22,7 +22,6 @@ import { UserKeypairService } from '@/core/UserKeypairService.js'; import { MfmService } from '@/core/MfmService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; -import type { MiUserKeypair } from '@/models/UserKeypair.js'; import type { UsersRepository, UserProfilesRepository, NotesRepository, DriveFilesRepository, PollsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { CustomEmojiService } from '@/core/CustomEmojiService.js'; @@ -30,7 +29,8 @@ import { isNotNull } from '@/misc/is-not-null.js'; import { IdService } from '@/core/IdService.js'; import { LdSignatureService } from './LdSignatureService.js'; import { ApMfmService } from './ApMfmService.js'; -import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate, PrivateKey } from './type.js'; +import type { IAccept, IActivity, IAdd, IAnnounce, IApDocument, IApEmoji, IApHashtag, IApImage, IApMention, IBlock, ICreate, IDelete, IFlag, IFollow, IKey, ILike, IMove, IObject, IPost, IQuestion, IReject, IRemove, ITombstone, IUndo, IUpdate } from './type.js'; +import type { PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; @Injectable() export class ApRendererService { @@ -657,10 +657,10 @@ export class ApRendererService { } @bindThis - public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }, key: PrivateKey): Promise { + public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }, key: PrivateKeyWithPem): Promise { const ldSignature = this.ldSignatureService.use(); ldSignature.debug = false; - activity = await ldSignature.signRsaSignature2017(activity, key.privateKey, key.keyId); + activity = await ldSignature.signRsaSignature2017(activity, key.privateKeyPem, key.keyId); return activity; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index c9bcbd961a..892e11b2d0 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -15,7 +15,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import type Logger from '@/logger.js'; import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js'; -import type { PrivateKey } from './type.js'; +import type { PrivateKeyWithPem, PrivateKey } from '@misskey-dev/node-http-message-signatures'; export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record }) { const u = new URL(args.url); @@ -36,7 +36,7 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u const result = await signAsDraftToRequest( request, - { keyId: args.key.keyId, privateKeyPem: args.key.privateKey }, + args.key, ['(request-target)', 'date', 'host', 'digest'], ); @@ -62,7 +62,7 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする const result = await signAsDraftToRequest( request, - { keyId: args.key.keyId, privateKeyPem: args.key.privateKey }, + args.key, ['(request-target)', 'date', 'host', 'accept'], ); @@ -89,9 +89,9 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKey): Promise { + public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); - key = key ?? await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, level); + key = key ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, level); const req = await createSignedPost({ level, key, @@ -124,7 +124,7 @@ export class ApRequestService { */ @bindThis public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise { - const key = await this.userKeypairService.getLocalUserKeypairWithKeyId(user.id, level); + const key = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, level); const req = await createSignedGet({ level, key, diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 6fd5ada9fe..0b34fd7664 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -326,8 +326,3 @@ export const isAnnounce = (object: IObject): object is IAnnounce => getApType(ob export const isBlock = (object: IObject): object is IBlock => getApType(object) === 'Block'; export const isFlag = (object: IObject): object is IFlag => getApType(object) === 'Flag'; export const isMove = (object: IObject): object is IMove => getApType(object) === 'Move'; - -export type PrivateKey = { - privateKey: string; - keyId: string; -}; diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 12e3136270..85b1c4924b 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -8,8 +8,8 @@ import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiNote } from '@/models/Note.js'; import type { MiUser } from '@/models/User.js'; import type { MiWebhook } from '@/models/Webhook.js'; -import type { IActivity, PrivateKey } from '@/core/activitypub/type.js'; -import type { ParsedSignature } from '@misskey-dev/node-http-message-signatures'; +import type { IActivity } from '@/core/activitypub/type.js'; +import type { ParsedSignature, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; /** * @peertube/http-signature 時代の古いデータにも対応しておく @@ -39,7 +39,7 @@ export type DeliverJobData = { /** whether it is sharedInbox */ isSharedInbox: boolean; /** force to use main (rsa) key */ - privateKey?: PrivateKey; + privateKey?: PrivateKeyWithPem; }; export type InboxJobData = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1ba0cfcf74..b9f77b46d4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.3 - version: 0.0.3 + specifier: 0.0.4 + version: 0.0.4 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4754,8 +4754,8 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.3: - resolution: {integrity: sha512-IDsdj01d0Jz/D5L4ubHGOeBLEjbIpHpGIrS7p0t2Z36ahP5cvzeawkxZW2ZuqyWQI4JendJWhGnSmDrH4N3IFg==} + /@misskey-dev/node-http-message-signatures@0.0.4: + resolution: {integrity: sha512-hAQFaJZwJsg63JAY1RZ/yvcaxHbpZg8fLvNdJFucS270TZGunKYv7I16tuNQMKV7+BYLQNxiE7uL11KVZ9jfrg==} engines: {node: '>=18.4.0'} dependencies: '@lapo/asn1js': 1.2.4 From 0127f8929893ea5d337dace2502c5a5a6da71d8f Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 16:27:13 +0000 Subject: [PATCH 074/635] getLocalUserPrivateKey --- .../backend/src/core/UserKeypairService.ts | 78 ++++++++++++++++--- .../src/core/activitypub/ApRendererService.ts | 2 +- .../src/core/activitypub/ApRequestService.ts | 5 +- packages/backend/src/misc/cache.ts | 3 + 4 files changed, 74 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 00f97143c8..74d1257bc3 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -5,19 +5,21 @@ import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import * as Redis from 'ioredis'; -import { genEd25519KeyPair, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; +import { genEd25519KeyPair, importPrivateKey, PrivateKey, PrivateKeyWithPem } from '@misskey-dev/node-http-message-signatures'; import type { MiUser } from '@/models/User.js'; import type { UserKeypairsRepository } from '@/models/_.js'; -import { RedisKVCache } from '@/misc/cache.js'; +import { RedisKVCache, MemoryKVCache } from '@/misc/cache.js'; import type { MiUserKeypair } from '@/models/UserKeypair.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { GlobalEventService, GlobalEvents } from '@/core/GlobalEventService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; +import type { webcrypto } from 'node:crypto'; @Injectable() export class UserKeypairService implements OnApplicationShutdown { - private cache: RedisKVCache; + private keypairEntityCache: RedisKVCache; + private privateKeyObjectCache: MemoryKVCache; constructor( @Inject(DI.redis) @@ -30,26 +32,29 @@ export class UserKeypairService implements OnApplicationShutdown { private globalEventService: GlobalEventService, private userEntityService: UserEntityService, ) { - this.cache = new RedisKVCache(this.redisClient, 'userKeypair', { + this.keypairEntityCache = new RedisKVCache(this.redisClient, 'userKeypair', { lifetime: 1000 * 60 * 60 * 24, // 24h memoryCacheLifetime: Infinity, fetcher: (key) => this.userKeypairsRepository.findOneByOrFail({ userId: key }), toRedisConverter: (value) => JSON.stringify(value), fromRedisConverter: (value) => JSON.parse(value), }); + this.privateKeyObjectCache = new MemoryKVCache(1000 * 60 * 60 * 1); this.redisForSub.on('message', this.onMessage); } @bindThis public async getUserKeypair(userId: MiUser['id']): Promise { - return await this.cache.fetch(userId); + return await this.keypairEntityCache.fetch(userId); } /** - * + * Get private key [Only PrivateKeyWithPem for queue data etc.] * @param userIdOrHint user id or MiUserKeypair - * @param preferType If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists. Otherwise, main keypair is returned. + * @param preferType + * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists. + * Otherwise, main keypair is returned. * @returns */ @bindThis @@ -72,9 +77,62 @@ export class UserKeypairService implements OnApplicationShutdown { }; } + /** + * Get private key [Only PrivateKey for ap request] + * Using cache due to performance reasons of `crypto.subtle.importKey` + * @param userIdOrHint user id, MiUserKeypair, or PrivateKeyWithPem + * @param preferType + * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists. + * Otherwise, main keypair is returned. (ignored if userIdOrHint is PrivateKeyWithPem) + * @returns + */ + @bindThis + public async getLocalUserPrivateKey( + userIdOrHint: MiUser['id'] | MiUserKeypair | PrivateKeyWithPem, preferType?: string, + ): Promise { + if (typeof userIdOrHint === 'object' && 'privateKeyPem' in userIdOrHint) { + // userIdOrHint is PrivateKeyWithPem + return { + keyId: userIdOrHint.keyId, + privateKey: await this.privateKeyObjectCache.fetch(userIdOrHint.keyId, async () => { + return await importPrivateKey(userIdOrHint.privateKeyPem); + }), + }; + } + + const userId = typeof userIdOrHint === 'string' ? userIdOrHint : userIdOrHint.userId; + const getKeypair = () => typeof userIdOrHint === 'string' ? this.getUserKeypair(userId) : userIdOrHint; + + if (preferType && ['01', '11', 'ed25519'].includes(preferType.toLowerCase())) { + const keyId = `${this.userEntityService.genLocalUserUri(userId)}#ed25519-key`; + const fetched = await this.privateKeyObjectCache.fetchMaybe(keyId, async () => { + const keypair = await getKeypair(); + if (keypair.ed25519PublicKey != null && keypair.ed25519PrivateKey != null) { + return await importPrivateKey(keypair.ed25519PrivateKey); + } + return; + }); + if (fetched) { + return { + keyId, + privateKey: fetched, + }; + } + } + + const keyId = `${this.userEntityService.genLocalUserUri(userId)}#main-key`; + return { + keyId, + privateKey: await this.privateKeyObjectCache.fetch(keyId, async () => { + const keypair = await getKeypair(); + return await importPrivateKey(keypair.privateKey); + }), + }; + } + @bindThis public async refresh(userId: MiUser['id']): Promise { - return await this.cache.refresh(userId); + return await this.keypairEntityCache.refresh(userId); } /** @@ -85,7 +143,7 @@ export class UserKeypairService implements OnApplicationShutdown { @bindThis public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { await this.refresh(userId); - const keypair = await this.cache.fetch(userId); + const keypair = await this.keypairEntityCache.fetch(userId); if (keypair.ed25519PublicKey != null) { return; } @@ -119,7 +177,7 @@ export class UserKeypairService implements OnApplicationShutdown { } @bindThis public dispose(): void { - this.cache.dispose(); + this.keypairEntityCache.dispose(); } @bindThis diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 5d9ca74fe1..d191bfd089 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -252,7 +252,7 @@ export class ApRendererService { @bindThis public renderKey(user: MiLocalUser, publicKey: string, postfix?: string): IKey { return { - id: `${this.config.url}/users/${user.id}${postfix ?? '/publickey'}`, + id: `${this.userEntityService.genLocalUserUri(user.id)}${postfix ?? '/publickey'}`, type: 'Key', owner: this.userEntityService.genLocalUserUri(user.id), publicKeyPem: createPublicKey(publicKey).export({ diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 892e11b2d0..8338a8ca10 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -91,10 +91,9 @@ export class ApRequestService { @bindThis public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); - key = key ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, level); const req = await createSignedPost({ level, - key, + key: await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level), url, body, additionalHeaders: { @@ -124,7 +123,7 @@ export class ApRequestService { */ @bindThis public async signedGet(url: string, user: { id: MiUser['id'] }, level: string): Promise { - const key = await this.userKeypairService.getLocalUserPrivateKeyPem(user.id, level); + const key = await this.userKeypairService.getLocalUserPrivateKey(user.id, level); const req = await createSignedGet({ level, key, diff --git a/packages/backend/src/misc/cache.ts b/packages/backend/src/misc/cache.ts index bba64a06ef..f498c110bf 100644 --- a/packages/backend/src/misc/cache.ts +++ b/packages/backend/src/misc/cache.ts @@ -195,6 +195,9 @@ export class MemoryKVCache { private lifetime: number; private gcIntervalHandle: NodeJS.Timeout; + /** + * @param lifetime キャッシュの生存期間 (ms) + */ constructor(lifetime: MemoryKVCache['lifetime']) { this.cache = new Map(); this.lifetime = lifetime; From 01b8d2fdb12e26be852475313d36bdfd7c111be4 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 16:44:11 +0000 Subject: [PATCH 075/635] fix test --- .../src/core/FetchInstanceMetadataService.ts | 2 +- .../src/core/activitypub/ApRequestService.ts | 5 ++-- .../test/unit/FetchInstanceMetadataService.ts | 23 +++++++++---------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts index 9d1ce3ac5d..ad68e575cf 100644 --- a/packages/backend/src/core/FetchInstanceMetadataService.ts +++ b/packages/backend/src/core/FetchInstanceMetadataService.ts @@ -81,7 +81,7 @@ export class FetchInstanceMetadataService { // キャッシュ有効チェックはロック取得前に行う const _instance = await this.federatedInstanceService.fetch(host); const now = Date.now(); - if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) { + if (_instance && _instance.infoUpdatedAt != null && (now - _instance.infoUpdatedAt.getTime() < REMOTE_SERVER_CACHE_TTL)) { this.logger.debug(`Skip because updated recently ${_instance.infoUpdatedAt.toJSON()}`); return; } diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 8338a8ca10..631430e46a 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -91,9 +91,10 @@ export class ApRequestService { @bindThis public async signedPost(user: { id: MiUser['id'] }, url: string, object: unknown, level: string, digest?: string, key?: PrivateKeyWithPem): Promise { const body = typeof object === 'string' ? object : JSON.stringify(object); + const keyFetched = await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level); const req = await createSignedPost({ level, - key: await this.userKeypairService.getLocalUserPrivateKey(key ?? user.id, level), + key: keyFetched, url, body, additionalHeaders: { @@ -106,7 +107,7 @@ export class ApRequestService { version: 'draft', level, url, - keyId: key.keyId, + keyId: keyFetched.keyId, }); await this.httpRequestService.send(url, { diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index bf8f3ab0e3..f87a6765a9 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -75,62 +75,61 @@ describe('FetchInstanceMetadataService', () => { test('Lock and update', async () => { redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date(now - 10 * 1000 * 60 * 60 * 24) } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(httpRequestService.getJson).toHaveBeenCalled(); }); - test('Lock and don\'t update', async () => { + test('Don\'t lock and update when recently updated', async () => { redisClient.set = mockRedis(); - const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date() } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); - expect(tryLockSpy).toHaveBeenCalledTimes(1); - expect(unlockSpy).toHaveBeenCalledTimes(1); expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); + expect(tryLockSpy).toHaveBeenCalledTimes(0); + expect(unlockSpy).toHaveBeenCalledTimes(0); expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); }); test('Do nothing when lock not acquired', async () => { redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date(now - 10 * 1000 * 60 * 60 * 24) } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); await fetchInstanceMetadataService.tryLock('example.com'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1); expect(tryLockSpy).toHaveBeenCalledTimes(1); expect(unlockSpy).toHaveBeenCalledTimes(0); - expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); expect(httpRequestService.getJson).toHaveBeenCalledTimes(0); }); - test('Do when lock not acquired but forced', async () => { + test('Do when forced', async () => { redisClient.set = mockRedis(); const now = Date.now(); - federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any); + federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date(now - 10 * 1000 * 60 * 60 * 24) } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); await fetchInstanceMetadataService.tryLock('example.com'); const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock'); const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock'); await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true); + expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); expect(tryLockSpy).toHaveBeenCalledTimes(0); expect(unlockSpy).toHaveBeenCalledTimes(1); - expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0); expect(httpRequestService.getJson).toHaveBeenCalled(); }); }); From ab29cbab41f303025e35f6070c0e6455f793d4fa Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 16:44:32 +0000 Subject: [PATCH 076/635] if --- packages/backend/test/unit/FetchInstanceMetadataService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts index f87a6765a9..2e66b81fcd 100644 --- a/packages/backend/test/unit/FetchInstanceMetadataService.ts +++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts @@ -87,7 +87,7 @@ describe('FetchInstanceMetadataService', () => { expect(httpRequestService.getJson).toHaveBeenCalled(); }); - test('Don\'t lock and update when recently updated', async () => { + test('Don\'t lock and update if recently updated', async () => { redisClient.set = mockRedis(); federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: new Date() } as any); httpRequestService.getJson.mockImplementation(() => { throw Error(); }); From a701fed9e5c945261cb31d7eb80502d30ca8c445 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 5 Mar 2024 16:50:26 +0000 Subject: [PATCH 077/635] fix ap-request --- packages/backend/test/unit/ap-request.ts | 41 ++++++++++++++++-------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index 1c0159ed95..8406cb7ec2 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -4,7 +4,7 @@ */ import * as assert from 'assert'; -import { verifyDraftSignature, parseRequestSignature, genEd25519KeyPair, genRsaKeyPair } from '@misskey-dev/node-http-message-signatures'; +import { verifyDraftSignature, parseRequestSignature, genEd25519KeyPair, genRsaKeyPair, importPrivateKey } from '@misskey-dev/node-http-message-signatures'; import { createSignedGet, createSignedPost } from '@/core/activitypub/ApRequestService.js'; export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => { @@ -31,32 +31,47 @@ async function getKeyPair(level: string) { throw new Error('Invalid level'); } -describe('ap-request', () => { +describe('ap-request post', () => { + const url = 'https://example.com/inbox'; + const activity = { a: 1 }; + const body = JSON.stringify(activity); + const headers = { + 'User-Agent': 'UA', + }; + describe.each(['00', '01'])('createSignedPost with verify', (level) => { - test('pass', async () => { + test('pem', async () => { const keypair = await getKeyPair(level); - const key = { keyId: 'x', 'privateKey': keypair.privateKey }; - const url = 'https://example.com/inbox'; - const activity = { a: 1 }; - const body = JSON.stringify(activity); - const headers = { - 'User-Agent': 'UA', - }; + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; const req = await createSignedPost({ level, key, url, body, additionalHeaders: headers }); const parsed = parseRequestSignature(req.request); - expect(parsed?.version).toBe('draft'); + expect(parsed.version).toBe('draft'); + if (!parsed) return; + const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); + assert.deepStrictEqual(verify, true); + }); + test('imported', async () => { + const keypair = await getKeyPair(level); + const key = { keyId: 'x', 'privateKey': await importPrivateKey(keypair.privateKey) }; + + const req = await createSignedPost({ level, key, url, body, additionalHeaders: headers }); + + const parsed = parseRequestSignature(req.request); + expect(parsed.version).toBe('draft'); if (!parsed) return; const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); assert.deepStrictEqual(verify, true); }); }); +}); +describe('ap-request get', () => { describe.each(['00', '01'])('createSignedGet with verify', (level) => { test('pass', async () => { const keypair = await getKeyPair(level); - const key = { keyId: 'x', 'privateKey': keypair.privateKey }; + const key = { keyId: 'x', 'privateKeyPem': keypair.privateKey }; const url = 'https://example.com/outbox'; const headers = { 'User-Agent': 'UA', @@ -65,7 +80,7 @@ describe('ap-request', () => { const req = await createSignedGet({ level, key, url, additionalHeaders: headers }); const parsed = parseRequestSignature(req.request); - expect(parsed?.version).toBe('draft'); + expect(parsed.version).toBe('draft'); if (!parsed) return; const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); assert.deepStrictEqual(verify, true); From 39fba74dd12aa0fd22c1b3f59ec053858443f2f8 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 6 Mar 2024 06:48:03 +0000 Subject: [PATCH 078/635] update node-http-message-signatures --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 912768c934..daf00e3238 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.4", + "@misskey-dev/node-http-message-signatures": "0.0.5", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b9f77b46d4..5b98adc1c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.4 - version: 0.0.4 + specifier: 0.0.5 + version: 0.0.5 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4754,8 +4754,8 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.4: - resolution: {integrity: sha512-hAQFaJZwJsg63JAY1RZ/yvcaxHbpZg8fLvNdJFucS270TZGunKYv7I16tuNQMKV7+BYLQNxiE7uL11KVZ9jfrg==} + /@misskey-dev/node-http-message-signatures@0.0.5: + resolution: {integrity: sha512-lULO/ekgFA3p8K9Tc8HqKf/0jUFn3pR6SWB845OL44qKrIa8wsPAbMJN3u5NLBeFvPAGXhQfHJLcWfuTwlLB6Q==} engines: {node: '>=18.4.0'} dependencies: '@lapo/asn1js': 1.2.4 From fef9ebfe06113e211d21520faf65dc6e68ca0b56 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 6 Mar 2024 12:56:03 +0000 Subject: [PATCH 079/635] fix type error --- .../backend/src/queue/processors/InboxProcessorService.ts | 4 ++++ .../src/server/api/endpoints/admin/queue/inbox-delayed.ts | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 23e377d790..e790267a04 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -53,6 +53,10 @@ export class InboxProcessorService { @bindThis public async process(job: Bull.Job): Promise { const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; + if (Array.isArray(signature)) { + // RFC 9401はsignatureが配列になるが、とりあえずエラーにする + throw new Error('signature is array'); + } const activity = job.data.activity; //#region Log 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 7839d8b6f5..42afa08485 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 @@ -57,7 +57,7 @@ export default class extends Endpoint { // eslint- for (const job of jobs) { const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; - const host = new URL(signature.keyId).host; + const host = Array.isArray(signature) ? 'TODO' : new URL(signature.keyId).host; if (res.find(x => x[0] === host)) { res.find(x => x[0] === host)![1]++; } else { From 844feb1bb398f1037084dd0fbcf18b73b8462d73 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 7 Mar 2024 10:52:38 +0000 Subject: [PATCH 080/635] update package --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 20 +++++++++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index daf00e3238..2cf1fc55b3 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.5", + "@misskey-dev/node-http-message-signatures": "0.0.6", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b98adc1c8..20aab29678 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.5 - version: 0.0.5 + specifier: 0.0.6 + version: 0.0.6 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4754,11 +4754,12 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.5: - resolution: {integrity: sha512-lULO/ekgFA3p8K9Tc8HqKf/0jUFn3pR6SWB845OL44qKrIa8wsPAbMJN3u5NLBeFvPAGXhQfHJLcWfuTwlLB6Q==} + /@misskey-dev/node-http-message-signatures@0.0.6: + resolution: {integrity: sha512-Yw/bPzdXs0q/W1FkCOWNFl3woDwAr3wA2eg9ujfk5GlyfdrosWOdep3Vcc9rj4s+POr+2F2EAvTWNrduBWHkNQ==} engines: {node: '>=18.4.0'} dependencies: '@lapo/asn1js': 1.2.4 + structured-headers: 1.0.1 dev: false /@misskey-dev/sharp-read-bmp@1.2.0: @@ -6648,7 +6649,7 @@ packages: ts-dedent: 2.2.0 type-fest: 2.19.0 vue: 3.4.21(typescript@5.3.3) - vue-component-type-helpers: 2.0.5 + vue-component-type-helpers: 2.0.6 transitivePeerDependencies: - encoding - supports-color @@ -18286,6 +18287,11 @@ packages: peek-readable: 5.0.0 dev: false + /structured-headers@1.0.1: + resolution: {integrity: sha512-QYBxdBtA4Tl5rFPuqmbmdrS9kbtren74RTJTcs0VSQNVV5iRhJD4QlYTLD0+81SBwUQctjEQzjTRI3WG4DzICA==} + engines: {node: '>= 14', npm: '>=6'} + dev: false + /stylehacks@6.0.3(postcss@8.4.35): resolution: {integrity: sha512-KzBqjnqktc8/I0ERCb+lGq06giF/JxDbw2r9kEVhen9noHeIDRtMWUp9r62sOk+/2bbX6sFG1GhsS7ToXG0PEg==} engines: {node: ^14 || ^16 || >=18.0} @@ -19447,8 +19453,8 @@ packages: resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==} dev: true - /vue-component-type-helpers@2.0.5: - resolution: {integrity: sha512-v9N4ufDSnd8YHcDq/vURPjxDyBVak5ZVAQ6aGNIrf7ZAj/VxRKpLZXFHEaqt9yHkWi0/TZp76Jmf8yNJxDQi4g==} + /vue-component-type-helpers@2.0.6: + resolution: {integrity: sha512-qdGXCtoBrwqk1BT6r2+1Wcvl583ZVkuSZ3or7Y1O2w5AvWtlvvxwjGhmz5DdPJS9xqRdDlgTJ/38ehWnEi0tFA==} dev: true /vue-demi@0.14.7(vue@3.4.21): From e543ffe368e842d4501d6d46d918c8c9203a9867 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 7 Mar 2024 11:38:23 +0000 Subject: [PATCH 081/635] fix type --- packages/backend/src/core/activitypub/ApRequestService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 631430e46a..420844cc17 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -19,7 +19,7 @@ import type { PrivateKeyWithPem, PrivateKey } from '@misskey-dev/node-http-messa export async function createSignedPost(args: { level: string; key: PrivateKey; url: string; body: string; digest?: string, additionalHeaders: Record }) { const u = new URL(args.url); - const request: RequestLike = { + const request = { url: u.href, method: 'POST', headers: { @@ -27,7 +27,7 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u 'Host': u.host, 'Content-Type': 'application/activity+json', ...args.additionalHeaders, - }, + } as Record, }; // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする @@ -48,7 +48,7 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u export async function createSignedGet(args: { level: string; key: PrivateKey; url: string; additionalHeaders: Record }) { const u = new URL(args.url); - const request: RequestLike = { + const request = { url: u.href, method: 'GET', headers: { @@ -56,7 +56,7 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur 'Date': new Date().toUTCString(), 'Host': new URL(args.url).host, ...args.additionalHeaders, - }, + } as Record, }; // TODO: httpMessageSignaturesImplementationLevelによって新規格で通信をするようにする From 74c8f0a483d020e7b1b60fb3a44d33043b18bb0c Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 7 Mar 2024 15:21:07 +0000 Subject: [PATCH 082/635] update package --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index e2f4180d1c..57f718210f 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.6", + "@misskey-dev/node-http-message-signatures": "0.0.7", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 20aab29678..f853e68f6a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.6 - version: 0.0.6 + specifier: 0.0.7 + version: 0.0.7 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4754,8 +4754,8 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.6: - resolution: {integrity: sha512-Yw/bPzdXs0q/W1FkCOWNFl3woDwAr3wA2eg9ujfk5GlyfdrosWOdep3Vcc9rj4s+POr+2F2EAvTWNrduBWHkNQ==} + /@misskey-dev/node-http-message-signatures@0.0.7: + resolution: {integrity: sha512-NmXxPZGHtNniKMaAhLDoA+jrnubLqYmjdbzDzQVieaCTqAT0sn0Mg+IyHbfDR4+kePUSKMBQIeM1YNJWextFtA==} engines: {node: '>=18.4.0'} dependencies: '@lapo/asn1js': 1.2.4 From 6907b6505afc7eeee0efcde9232ffa433934ea98 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 10:24:55 +0000 Subject: [PATCH 083/635] retry no key --- .../backend/src/queue/processors/InboxProcessorService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index e790267a04..b1468a2a93 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -103,7 +103,9 @@ export class InboxProcessorService { // publicKey がなくても終了 if (authUser.key == null) { - throw new Bull.UnrecoverableError('skip: failed to resolve user publicKey'); + // publicKeyがないのはpublicKeyの変更(主にmain→ed25519)に + // 対応しきれていない場合があるためリトライする + throw new Error('skip: failed to resolve user publicKey'); } // HTTP-Signatureの検証 From d0da9f32dc91546eb8aab19e6a609045693d9bf8 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 10:25:58 +0000 Subject: [PATCH 084/635] @misskey-dev/node-http-message-signatures@0.0.8 --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index 57f718210f..76a17cc88c 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.1.0", "@fastify/static": "6.12.0", "@fastify/view": "8.2.0", - "@misskey-dev/node-http-message-signatures": "0.0.7", + "@misskey-dev/node-http-message-signatures": "0.0.8", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.0.3", "@nestjs/common": "10.3.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f853e68f6a..99195bbfc7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,8 +111,8 @@ importers: specifier: 8.2.0 version: 8.2.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.7 - version: 0.0.7 + specifier: 0.0.8 + version: 0.0.8 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -4754,11 +4754,12 @@ packages: eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.1.0)(eslint@8.57.0) dev: true - /@misskey-dev/node-http-message-signatures@0.0.7: - resolution: {integrity: sha512-NmXxPZGHtNniKMaAhLDoA+jrnubLqYmjdbzDzQVieaCTqAT0sn0Mg+IyHbfDR4+kePUSKMBQIeM1YNJWextFtA==} + /@misskey-dev/node-http-message-signatures@0.0.8: + resolution: {integrity: sha512-ymyjluFpY2xK/3UYyj+MiRfA7752BIWwSb+uFU0mFK1Yq2fGI6lAGZg3+D2lD8W/OkltnMQm1gSOXzE3CNIgtA==} engines: {node: '>=18.4.0'} dependencies: '@lapo/asn1js': 1.2.4 + rfc4648: 1.5.3 structured-headers: 1.0.1 dev: false @@ -17349,6 +17350,10 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + /rfc4648@1.5.3: + resolution: {integrity: sha512-MjOWxM065+WswwnmNONOT+bD1nXzY9Km6u3kzvnx8F8/HXGZdz3T6e6vZJ8Q/RIMUSp/nxqjH3GwvJDy8ijeQQ==} + dev: false + /rfdc@1.3.0: resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} From 75a2f1c1e870657b86a985404d92ee53cf9325dc Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 12:27:52 +0000 Subject: [PATCH 085/635] fix type error --- packages/backend/test/unit/ap-request.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/test/unit/ap-request.ts b/packages/backend/test/unit/ap-request.ts index 8406cb7ec2..50894c8b81 100644 --- a/packages/backend/test/unit/ap-request.ts +++ b/packages/backend/test/unit/ap-request.ts @@ -48,8 +48,8 @@ describe('ap-request post', () => { const parsed = parseRequestSignature(req.request); expect(parsed.version).toBe('draft'); - if (!parsed) return; - const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); + expect(Array.isArray(parsed.value)).toBe(false); + const verify = await verifyDraftSignature(parsed.value as any, keypair.publicKey); assert.deepStrictEqual(verify, true); }); test('imported', async () => { @@ -60,8 +60,8 @@ describe('ap-request post', () => { const parsed = parseRequestSignature(req.request); expect(parsed.version).toBe('draft'); - if (!parsed) return; - const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); + expect(Array.isArray(parsed.value)).toBe(false); + const verify = await verifyDraftSignature(parsed.value as any, keypair.publicKey); assert.deepStrictEqual(verify, true); }); }); @@ -81,8 +81,8 @@ describe('ap-request get', () => { const parsed = parseRequestSignature(req.request); expect(parsed.version).toBe('draft'); - if (!parsed) return; - const verify = await verifyDraftSignature(parsed.value, keypair.publicKey); + expect(Array.isArray(parsed.value)).toBe(false); + const verify = await verifyDraftSignature(parsed.value as any, keypair.publicKey); assert.deepStrictEqual(verify, true); }); }); From 4310229ca52add406a06ef84b047ec6f539d7e9c Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 16:39:01 +0000 Subject: [PATCH 086/635] log keyid --- packages/backend/src/queue/processors/InboxProcessorService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index b1468a2a93..dcaa24e8b7 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -105,7 +105,7 @@ export class InboxProcessorService { if (authUser.key == null) { // publicKeyがないのはpublicKeyの変更(主にmain→ed25519)に // 対応しきれていない場合があるためリトライする - throw new Error('skip: failed to resolve user publicKey'); + throw new Error(`skip: failed to resolve user publicKey: keyId=${signature.keyId}`); } // HTTP-Signatureの検証 From d168ec7dd58d179679b9459bdcdcdf75ef841155 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 17:22:16 +0000 Subject: [PATCH 087/635] logger --- .../core/activitypub/ApDbResolverService.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 61fcc6731d..93f0e1bb54 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -13,8 +13,10 @@ import { CacheService } from '@/core/CacheService.js'; import type { MiNote } from '@/models/Note.js'; import { bindThis } from '@/decorators.js'; import { MiLocalUser, MiRemoteUser } from '@/models/User.js'; +import Logger from '@/logger.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; +import { ApLoggerService } from './ApLoggerService.js'; import type { IObject } from './type.js'; export type UriParseResult = { @@ -36,6 +38,7 @@ export type UriParseResult = { @Injectable() export class ApDbResolverService implements OnApplicationShutdown { private publicKeyByUserIdCache: MemoryKVCache; + private logger: Logger; constructor( @Inject(DI.config) @@ -52,8 +55,10 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, + private apLoggerService: ApLoggerService, ) { this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); + this.logger = this.apLoggerService.logger; } @bindThis @@ -118,9 +123,14 @@ export class ApDbResolverService implements OnApplicationShutdown { private async refreshAndfindKey(userId: MiUser['id'], keyId: string): Promise { this.refreshCacheByUserId(userId); const keys = await this.getPublicKeyByUserId(userId); - if (keys == null || !Array.isArray(keys)) return null; - - return keys.find(x => x.keyId === keyId) ?? null; + if (keys == null || !Array.isArray(keys) || keys.length === 0) { + this.logger.warn(`No key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${keys}`); + return null; + } + const exactKey = keys.find(x => x.keyId === keyId); + if (exactKey) return exactKey; + this.logger.warn(`No exact key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${keys}`); + return null; } /** @@ -138,7 +148,10 @@ export class ApDbResolverService implements OnApplicationShutdown { const keys = await this.getPublicKeyByUserId(user.id); - if (keys == null || !Array.isArray(keys)) return { user, key: null }; + if (keys == null || !Array.isArray(keys) || keys.length === 0) { + this.logger.warn(`No key found uri=${uri} userId=${user.id} keys=${keys}`); + return { user, key: null }; + } if (!keyId) { // mainっぽいのを選ぶ @@ -173,12 +186,14 @@ export class ApDbResolverService implements OnApplicationShutdown { // lastFetchedAtでの更新制限を弱めて再取得 if (user.lastFetchedAt == null || user.lastFetchedAt < new Date(Date.now() - 1000 * 60 * 12)) { + this.logger.info(`Fetching user to find public key uri=${uri} userId=${user.id} keyId=${keyId}`); const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0); if (renewed == null || renewed.isDeleted) return null; return { user, key: await this.refreshAndfindKey(user.id, keyId) }; } + this.logger.warn(`No key found uri=${uri} userId=${user.id} keyId=${keyId}`); return { user, key: null }; } From 1690e0617e232707929d1423e31fd19eb5abc804 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 17:25:24 +0000 Subject: [PATCH 088/635] db-resolver --- packages/backend/src/core/activitypub/ApDbResolverService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 93f0e1bb54..677f8c2cfa 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -58,7 +58,7 @@ export class ApDbResolverService implements OnApplicationShutdown { private apLoggerService: ApLoggerService, ) { this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); - this.logger = this.apLoggerService.logger; + this.logger = this.apLoggerService.logger.createSubLogger('db-resolver'); } @bindThis From da4a44b337717b3b51671b75c7586df308c839f0 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 17:29:23 +0000 Subject: [PATCH 089/635] JSON.stringify --- .../backend/src/core/activitypub/ApDbResolverService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 677f8c2cfa..8509f04c09 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -124,12 +124,12 @@ export class ApDbResolverService implements OnApplicationShutdown { this.refreshCacheByUserId(userId); const keys = await this.getPublicKeyByUserId(userId); if (keys == null || !Array.isArray(keys) || keys.length === 0) { - this.logger.warn(`No key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${keys}`); + this.logger.warn(`No key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); return null; } const exactKey = keys.find(x => x.keyId === keyId); if (exactKey) return exactKey; - this.logger.warn(`No exact key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${keys}`); + this.logger.warn(`No exact key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); return null; } @@ -149,7 +149,7 @@ export class ApDbResolverService implements OnApplicationShutdown { const keys = await this.getPublicKeyByUserId(user.id); if (keys == null || !Array.isArray(keys) || keys.length === 0) { - this.logger.warn(`No key found uri=${uri} userId=${user.id} keys=${keys}`); + this.logger.warn(`No key found uri=${uri} userId=${user.id} keys=${JSON.stringify(keys)}`); return { user, key: null }; } From 8104963e1dcd1ba1a3dec7f526321b11c7459920 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 19:06:59 +0000 Subject: [PATCH 090/635] =?UTF-8?q?HTTP=20Signature=E3=81=8C=E3=81=AA?= =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=9F=E3=82=8A=E4=BD=BF=E3=81=88=E3=81=AA?= =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=9F=E3=82=8A=E3=81=97=E3=81=9D=E3=81=86?= =?UTF-8?q?=E3=81=AA=E5=A0=B4=E5=90=88=E3=81=ABLD=20Signature=E3=82=92?= =?UTF-8?q?=E6=B4=BB=E7=94=A8=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/QueueService.ts | 2 +- .../core/activitypub/ApDbResolverService.ts | 31 ++++++- .../queue/processors/InboxProcessorService.ts | 90 ++++++++++--------- packages/backend/src/queue/types.ts | 2 +- .../src/server/ActivityPubServerService.ts | 24 ++++- 5 files changed, 101 insertions(+), 48 deletions(-) diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts index a0ec9987a7..6488687867 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -135,7 +135,7 @@ export class QueueService { } @bindThis - public inbox(activity: IActivity, signature: ParsedSignature) { + public inbox(activity: IActivity, signature: ParsedSignature | null) { const data = { activity: activity, signature, diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 8509f04c09..16804a240d 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -137,14 +137,41 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Actor id => Misskey User and Key * @param uri AP Actor id * @param keyId Key id to find. If not specified, main key will be selected. + * keyIdがURLライクの場合、ハッシュを削除したkeyIdはuriと同一であることが期待される + * @returns + * 1. uriとkeyIdが一致しない場合`null` + * 2. userが見つからない場合`{ user: null, key: null }` + * 3. keyが見つからない場合`{ user, key: null }` */ @bindThis public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{ user: MiRemoteUser; key: MiUserPublickey | null; - } | null> { + } | { + user: null; + key: null; + } | + null> { + if (keyId) { + try { + const actorUrl = new URL(uri); + const keyUrl = new URL(keyId); + actorUrl.hash = ''; + keyUrl.hash = ''; + if (actorUrl.href !== keyUrl.href) { + // uriとkeyId(のhashなし)が一致しない場合、actorと鍵の所有者が一致していないということである + // その場合、そもそも署名は有効といえないのでキーの検索は無意味 + this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`); + return null; + } + } catch (err) { + // キーがURLっぽくない場合はエラーになるはず。そういった場合はとりあえずキー検索してみる + this.logger.warn(`maybe actor uri or keyId are not url like: uri=${uri} keyId=${keyId}`, { err }); + } + } + const user = await this.apPersonService.resolvePerson(uri, undefined, true) as MiRemoteUser; - if (user.isDeleted) return null; + if (user.isDeleted) return { user: null, key: null }; const keys = await this.getPublicKeyByUserId(user.id); diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index dcaa24e8b7..07f36f4d7a 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -52,12 +52,15 @@ export class InboxProcessorService { @bindThis public async process(job: Bull.Job): Promise { - const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; + const signature = job.data.signature ? + 'version' in job.data.signature ? job.data.signature.value : job.data.signature + : null; if (Array.isArray(signature)) { // RFC 9401はsignatureが配列になるが、とりあえずエラーにする throw new Error('signature is array'); } const activity = job.data.activity; + const actorUri = getApId(activity.actor); //#region Log const info = Object.assign({}, activity); @@ -65,7 +68,7 @@ export class InboxProcessorService { this.logger.debug(JSON.stringify(info, null, 2)); //#endregion - const host = this.utilityService.toPuny(new URL(signature.keyId).hostname); + const host = this.utilityService.toPuny(new URL(activity.actor).hostname); // ブロックしてたら中断 const meta = await this.metaService.fetch(); @@ -73,19 +76,12 @@ export class InboxProcessorService { return `Blocked request: ${host}`; } - const keyIdLower = signature.keyId.toLowerCase(); - if (keyIdLower.startsWith('acct:')) { - return `Old keyId is no longer supported. ${keyIdLower}`; - } - // HTTP-Signature keyIdを元にDBから取得 - let authUser: { - user: MiRemoteUser; - key: MiUserPublickey | null; - } | null = null; + let authUser: Awaited> = null; + let httpSignatureIsValid = null as boolean | null; try { - authUser = await this.apDbResolverService.getAuthUserFromApId(getApId(activity.actor), signature.keyId); + authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, signature?.keyId); } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { @@ -96,45 +92,58 @@ export class InboxProcessorService { } } - // それでもわからなければ終了 - if (authUser == null) { + // authUser.userがnullならスキップ + if (authUser != null && authUser.user == null) { throw new Bull.UnrecoverableError('skip: failed to resolve user'); } - // publicKey がなくても終了 - if (authUser.key == null) { - // publicKeyがないのはpublicKeyの変更(主にmain→ed25519)に - // 対応しきれていない場合があるためリトライする - throw new Error(`skip: failed to resolve user publicKey: keyId=${signature.keyId}`); + if (signature != null && authUser != null) { + if (signature.keyId.toLowerCase().startsWith('acct:')) { + this.logger.warn(`Old keyId is no longer supported. lowerKeyId=${signature.keyId.toLowerCase()}`); + } else if (authUser.key != null) { + // keyがなかったらLD Signatureで検証するべき + // HTTP-Signatureの検証 + const errorLogger = (ms: any) => this.logger.error(ms); + httpSignatureIsValid = await verifyDraftSignature(signature, authUser.key.keyPem, errorLogger); + this.logger.debug('Inbox message validation: ', { + userId: authUser.user.id, + userAcct: Acct.toString(authUser.user), + parsedKeyId: signature.keyId, + foundKeyId: authUser.key.keyId, + httpSignatureValid: httpSignatureIsValid, + }); + } } - // HTTP-Signatureの検証 - const errorLogger = (ms: any) => this.logger.error(ms); - const httpSignatureValidated = await verifyDraftSignature(signature, authUser.key.keyPem, errorLogger); - this.logger.debug('Inbox message validation: ', { - userId: authUser.user.id, - userAcct: Acct.toString(authUser.user), - parsedKeyId: signature.keyId, - foundKeyId: authUser.key.keyId, - httpSignatureValidated, - }); - - // また、signatureのsignerは、activity.actorと一致する必要がある - if (httpSignatureValidated !== true || authUser.user.uri !== activity.actor) { + if ( + authUser == null || + httpSignatureIsValid !== true || + authUser.user.uri !== actorUri // 一応チェック + ) { // 一致しなくても、でもLD-Signatureがありそうならそっちも見る if (activity.signature?.creator) { if (activity.signature.type !== 'RsaSignature2017') { throw new Bull.UnrecoverableError(`skip: unsupported LD-signature type ${activity.signature.type}`); } - authUser = await this.apDbResolverService.getAuthUserFromApId(activity.signature.creator.replace(/#.*/, '')); - - if (authUser == null) { - throw new Bull.UnrecoverableError('skip: LD-Signatureのユーザーが取得できませんでした'); + if (activity.signature.creator.toLowerCase().startsWith('acct:')) { + throw new Bull.UnrecoverableError(`old key not supported ${activity.signature.creator}`); } + authUser = await this.apDbResolverService.getAuthUserFromApId(actorUri, activity.signature.creator); + + if (authUser == null) { + throw new Bull.UnrecoverableError(`skip: LD-Signatureのactorとcreatorが一致しませんでした uri=${actorUri} creator=${activity.signature.creator}`); + } + if (authUser.user == null) { + throw new Bull.UnrecoverableError(`skip: LD-Signatureのユーザーが取得できませんでした uri=${actorUri} creator=${activity.signature.creator}`); + } + // 一応actorチェック + if (authUser.user.uri !== actorUri) { + throw new Bull.UnrecoverableError(`skip: LD-Signature user(${authUser.user.uri}) !== activity.actor(${actorUri})`); + } if (authUser.key == null) { - throw new Bull.UnrecoverableError('skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした'); + throw new Bull.UnrecoverableError(`skip: LD-SignatureのユーザーはpublicKeyを持っていませんでした uri=${actorUri} creator=${activity.signature.creator}`); } // LD-Signature検証 @@ -144,18 +153,13 @@ export class InboxProcessorService { throw new Bull.UnrecoverableError('skip: LD-Signatureの検証に失敗しました'); } - // もう一度actorチェック - if (authUser.user.uri !== activity.actor) { - 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)) { throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { - throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. keyId=${signature.keyId}`); + throw new Bull.UnrecoverableError(`skip: http-signature verification failed and no LD-Signature. http_signature_keyId=${signature?.keyId}`); } } diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index 85b1c4924b..eaa836d965 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -44,7 +44,7 @@ export type DeliverJobData = { export type InboxJobData = { activity: IActivity; - signature: ParsedSignature | OldParsedSignature; + signature: ParsedSignature | OldParsedSignature | null; }; export type RelationshipJobData = { diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 78b89e8684..4a24ddf53c 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -30,12 +30,17 @@ import { IActivity } from '@/core/activitypub/type.js'; import { isPureRenote } from '@/misc/is-pure-renote.js'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FindOptionsWhere } from 'typeorm'; +import { LoggerService } from '@/core/LoggerService.js'; +import Logger from '@/logger.js'; const ACTIVITY_JSON = 'application/activity+json; charset=utf-8'; const LD_JSON = 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"; charset=utf-8'; @Injectable() export class ActivityPubServerService { + private logger: Logger; + private inboxLogger: Logger; + constructor( @Inject(DI.config) private config: Config, @@ -70,8 +75,11 @@ export class ActivityPubServerService { private queueService: QueueService, private userKeypairService: UserKeypairService, private queryService: QueryService, + private loggerService: LoggerService, ) { //this.createServer = this.createServer.bind(this); + this.logger = this.loggerService.getLogger('server-ap', 'gray', false); + this.inboxLogger = this.logger.createSubLogger('inbox', 'gray', false); } @bindThis @@ -100,10 +108,17 @@ export class ActivityPubServerService { @bindThis private async inbox(request: FastifyRequest, reply: FastifyReply) { + if (request.body == null) { + this.inboxLogger.warn('request body is empty'); + reply.code(400); + return; + } + let signature: ReturnType; const verifyDigest = await verifyDigestHeader(request.raw, request.rawBody || '', true); if (verifyDigest !== true) { + this.inboxLogger.warn('digest verification failed'); reply.code(401); return; } @@ -115,12 +130,19 @@ export class ActivityPubServerService { }, }); } catch (e) { + if (typeof request.body === 'object' && 'signature' in request.body) { + // LD SignatureがあればOK + this.queueService.inbox(request.body as IActivity, null); + reply.code(202); + return; + } + + this.inboxLogger.warn('signature header parsing failed and LD signature not found'); reply.code(401); return; } this.queueService.inbox(request.body as IActivity, signature); - reply.code(202); } From 154a2026eae812e2ecdceac463ab7e0b557c4d69 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 19:15:40 +0000 Subject: [PATCH 091/635] inbox-delayed use actor if no signature --- .../src/server/api/endpoints/admin/queue/inbox-delayed.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 42afa08485..bfe230da8d 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 @@ -56,8 +56,8 @@ export default class extends Endpoint { // eslint- const res = [] as [string, number][]; for (const job of jobs) { - const signature = 'version' in job.data.signature ? job.data.signature.value : job.data.signature; - const host = Array.isArray(signature) ? 'TODO' : new URL(signature.keyId).host; + const signature = job.data.signature ? 'version' in job.data.signature ? job.data.signature.value : job.data.signature : null; + const host = signature ? Array.isArray(signature) ? 'TODO' : new URL(signature.keyId).host : new URL(job.data.activity.actor).host; if (res.find(x => x[0] === host)) { res.find(x => x[0] === host)![1]++; } else { From eb8495648eac739412bcf551451c2fa275ebb1a8 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 20:01:07 +0000 Subject: [PATCH 092/635] =?UTF-8?q?=E3=83=A6=E3=83=BC=E3=82=B6=E3=83=BC?= =?UTF-8?q?=E3=81=A8=E3=82=AD=E3=83=BC=E3=81=AE=E5=90=8C=E4=B8=80=E6=80=A7?= =?UTF-8?q?=E3=83=81=E3=82=A7=E3=83=83=E3=82=AF=E3=81=AFhost=E3=81=AE?= =?UTF-8?q?=E4=B8=80=E8=87=B4=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/activitypub/ApDbResolverService.ts | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 16804a240d..66f649c9bd 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -18,6 +18,7 @@ import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApLoggerService } from './ApLoggerService.js'; import type { IObject } from './type.js'; +import { UtilityService } from '../UtilityService.js'; export type UriParseResult = { /** wether the URI was generated by us */ @@ -56,11 +57,18 @@ export class ApDbResolverService implements OnApplicationShutdown { private cacheService: CacheService, private apPersonService: ApPersonService, private apLoggerService: ApLoggerService, + private utilityService: UtilityService, ) { this.publicKeyByUserIdCache = new MemoryKVCache(Infinity); this.logger = this.apLoggerService.logger.createSubLogger('db-resolver'); } + private punyHost(url: string): string { + const urlObj = new URL(url); + const host = `${this.utilityService.toPuny(urlObj.hostname)}${urlObj.port.length > 0 ? ':' + urlObj.port : ''}`; + return host; + } + @bindThis public parseUri(value: string | IObject): UriParseResult { const separator = '/'; @@ -137,9 +145,8 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Actor id => Misskey User and Key * @param uri AP Actor id * @param keyId Key id to find. If not specified, main key will be selected. - * keyIdがURLライクの場合、ハッシュを削除したkeyIdはuriと同一であることが期待される * @returns - * 1. uriとkeyIdが一致しない場合`null` + * 1. ユーザーとキーのホストが一致しない場合`null` * 2. userが見つからない場合`{ user: null, key: null }` * 3. keyが見つからない場合`{ user, key: null }` */ @@ -153,20 +160,19 @@ export class ApDbResolverService implements OnApplicationShutdown { } | null> { if (keyId) { - try { - const actorUrl = new URL(uri); - const keyUrl = new URL(keyId); - actorUrl.hash = ''; - keyUrl.hash = ''; - if (actorUrl.href !== keyUrl.href) { - // uriとkeyId(のhashなし)が一致しない場合、actorと鍵の所有者が一致していないということである - // その場合、そもそも署名は有効といえないのでキーの検索は無意味 - this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`); - return null; - } - } catch (err) { - // キーがURLっぽくない場合はエラーになるはず。そういった場合はとりあえずキー検索してみる - this.logger.warn(`maybe actor uri or keyId are not url like: uri=${uri} keyId=${keyId}`, { err }); + if (this.punyHost(uri) !== this.punyHost(keyId)) { + /** + * keyIdはURL形式かつkeyIdのホストはuriのホストと一致するはず + * (ApPersonService.validateActorに由来) + * + * ただ、Mastodonはリプライ関連で他人のノートをHTTP Signature署名して送ってくることがある + * そのような署名は有効性に疑問があるので無視することにする + * ここではuriとkeyIdのホストが一致しない場合は無視する + * ハッシュをなくしたkeyIdとuriの同一性を比べてみてもいいが、`uri#*-key`というkeyIdを設定するのが + * 決まりごとというわけでもないため幅を持たせることにする + */ + this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`); + return null; } } From 9bfa38e6017efe5659830b7ad015ea45d5c7ab96 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 9 Mar 2024 20:02:33 +0000 Subject: [PATCH 093/635] log signature parse err --- packages/backend/src/server/ActivityPubServerService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 4a24ddf53c..a63d346da6 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -129,7 +129,9 @@ export class ActivityPubServerService { draft: ['(request-target)', 'digest', 'host', 'date'], }, }); - } catch (e) { + } catch (err) { + this.inboxLogger.warn('signature header parsing failed', { err }); + if (typeof request.body === 'object' && 'signature' in request.body) { // LD SignatureがあればOK this.queueService.inbox(request.body as IActivity, null); From e2b574a97c3e9f717fd0b71a5f16f7bf92271381 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 10 Mar 2024 16:00:25 +0000 Subject: [PATCH 094/635] save array --- .../src/core/activitypub/models/ApPersonService.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index a977c9251d..814c4646b6 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -396,18 +396,16 @@ export class ApPersonService implements OnModuleInit { })); if (person.publicKey) { - const keys = new Map([ + const publicKeys = new Map([ ...(person.additionalPublicKeys ? person.additionalPublicKeys.map(key => [key.id, key] as const) : []), [person.publicKey.id, person.publicKey], ]); - for (const key of keys.values()) { - await transactionalEntityManager.save(new MiUserPublickey({ - keyId: key.id, - userId: user.id, - keyPem: key.publicKeyPem, - })); - } + await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({ + keyId: key.id, + userId: user!.id, + keyPem: key.publicKeyPem, + }))); } }); } catch (e) { From 76487de5ed7d77aa1f3c4b9882530dfec7b7477f Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 10 Mar 2024 16:17:09 +0000 Subject: [PATCH 095/635] =?UTF-8?q?=E3=81=A8=E3=82=8A=E3=81=82=E3=81=88?= =?UTF-8?q?=E3=81=9Atry=E3=81=A7=E5=9B=B2=E3=81=A3=E3=81=A6=E3=81=8A?= =?UTF-8?q?=E3=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitypub/models/ApPersonService.ts | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 814c4646b6..1b01b9e2a9 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -551,23 +551,29 @@ export class ApPersonService implements OnModuleInit { // Update user await this.usersRepository.update(exist.id, updates); - const publicKeys = new Map(); - if (person.publicKey) { - (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); - publicKeys.set(person.publicKey.id, person.publicKey); + try { + // Deleteアクティビティ受信時にもここが走ってsaveがuserforeign key制約エラーを吐くことがある + // とりあえずtry-catchで囲っておく + const publicKeys = new Map(); + if (person.publicKey) { + (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); + publicKeys.set(person.publicKey.id, person.publicKey); - await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({ - keyId: key.id, + await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({ + keyId: key.id, + userId: exist.id, + keyPem: key.publicKeyPem, + }))); + } + + this.userPublickeysRepository.delete({ + keyId: Not(In(Array.from(publicKeys.keys()))), userId: exist.id, - keyPem: key.publicKeyPem, - }))); + }); + } catch (err) { + this.logger.error('something happened while updating remote user public keys:', { err }); } - this.userPublickeysRepository.delete({ - keyId: Not(In(Array.from(publicKeys.keys()))), - userId: exist.id, - }); - let _description: string | null = null; if (person._misskey_summary) { From d7c32cef70133179039695d2b5feb6a77856bddb Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 10 Mar 2024 16:38:53 +0000 Subject: [PATCH 096/635] =?UTF-8?q?fetchPersonWithRenewal=E3=81=A7?= =?UTF-8?q?=E3=82=A8=E3=83=A9=E3=83=BC=E3=81=8C=E8=B5=B7=E3=81=8D=E3=81=9F?= =?UTF-8?q?=E3=82=89=E5=8F=A4=E3=81=84=E3=83=87=E3=83=BC=E3=82=BF=E3=82=92?= =?UTF-8?q?=E8=BF=94=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/activitypub/models/ApPersonService.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 1b01b9e2a9..545a8af4ec 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -249,6 +249,12 @@ export class ApPersonService implements OnModuleInit { return null; } + /** + * uriからUser(Person)をフェッチします。 + * + * Misskeyに対象のPersonが登録されていればそれを返し、登録がなければnullを返します。 + * また、TTLが0でない場合、TTLを過ぎていた場合はupdatePersonを実行します。 + */ @bindThis async fetchPersonWithRenewal(uri: string, TTL = REMOTE_USER_CACHE_TTL): Promise { const exist = await this.fetchPerson(uri); @@ -257,8 +263,12 @@ export class ApPersonService implements OnModuleInit { if (this.userEntityService.isRemoteUser(exist)) { if (TTL === 0 || exist.lastFetchedAt == null || Date.now() - exist.lastFetchedAt.getTime() > TTL) { this.logger.debug('fetchPersonWithRenewal: renew', { uri, TTL, lastFetchedAt: exist.lastFetchedAt }); - await this.updatePerson(exist.uri); - return await this.fetchPerson(uri); + try { + await this.updatePerson(exist.uri); + return await this.fetchPerson(uri); + } catch (err) { + this.logger.error('error occurred while renewing user', { err }); + } } this.logger.debug('fetchPersonWithRenewal: use cache', { uri, TTL, lastFetchedAt: exist.lastFetchedAt }); } From aa5181cdfc698d2e0d39f09ddbd464708aa2d775 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sun, 10 Mar 2024 16:44:05 +0000 Subject: [PATCH 097/635] use transactionalEntityManager --- packages/backend/src/core/activitypub/models/ApPersonService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 545a8af4ec..f315e49a54 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -411,7 +411,7 @@ export class ApPersonService implements OnModuleInit { [person.publicKey.id, person.publicKey], ]); - await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({ + await transactionalEntityManager.save(Array.from(publicKeys.values(), key => new MiUserPublickey({ keyId: key.id, userId: user!.id, keyPem: key.publicKeyPem, From c58b4f8c24b67ebb1eeb575db4d071ccf7db72e7 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 2 May 2024 15:38:21 +0900 Subject: [PATCH 098/635] fix spdx --- packages/backend/test/unit/misc/gen-key-pair.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/backend/test/unit/misc/gen-key-pair.ts b/packages/backend/test/unit/misc/gen-key-pair.ts index 771147d299..9d76c68009 100644 --- a/packages/backend/test/unit/misc/gen-key-pair.ts +++ b/packages/backend/test/unit/misc/gen-key-pair.ts @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + import * as crypto from 'node:crypto'; import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; From be102f262207b24c6af1bdd1652628c08ffe95e9 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 8 May 2024 22:03:14 +0900 Subject: [PATCH 099/635] @misskey-dev/node-http-message-signatures@0.0.10 --- packages/backend/package.json | 2 +- pnpm-lock.yaml | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/backend/package.json b/packages/backend/package.json index db8a695db7..34f76695d4 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -79,7 +79,7 @@ "@fastify/multipart": "8.2.0", "@fastify/static": "7.0.3", "@fastify/view": "9.1.0", - "@misskey-dev/node-http-message-signatures": "0.0.8", + "@misskey-dev/node-http-message-signatures": "0.0.10", "@misskey-dev/sharp-read-bmp": "1.2.0", "@misskey-dev/summaly": "5.1.0", "@napi-rs/canvas": "^0.1.52", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 27bdb521f8..e73ccc9edc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,8 +120,8 @@ importers: specifier: 9.1.0 version: 9.1.0 '@misskey-dev/node-http-message-signatures': - specifier: 0.0.8 - version: 0.0.8 + specifier: 0.0.10 + version: 0.0.10 '@misskey-dev/sharp-read-bmp': specifier: 1.2.0 version: 1.2.0 @@ -3012,9 +3012,10 @@ packages: '@kurkle/color@0.3.2': resolution: {integrity: sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==} - '@lapo/asn1js@1.3.0': - resolution: {integrity: sha512-guWBAktMu8vcPvNUCZ5YWmEf4W4vWocjh2VSMQnMa84Ojqar3a2tkJHHCKxKpWXyC6PMYpT0IjFXmxwBPe9w1A==} + '@lapo/asn1js@2.0.4': + resolution: {integrity: sha512-KJD3wQAZxozcraJdWp3utDU6DEZgAVBGp9INCdptUpZaXCEYkpwNb7h7wyYh5y6DxtpvIud8k0suhWJ/z2rKvw==} engines: {node: '>=12.20.0'} + hasBin: true '@levischuck/tiny-cbor@0.2.2': resolution: {integrity: sha512-f5CnPw997Y2GQ8FAvtuVVC19FX8mwNNC+1XJcIi16n/LTJifKO6QBgGLgN3YEmqtGMk17SKSuoWES3imJVxAVw==} @@ -3067,8 +3068,8 @@ packages: eslint: '>= 3' eslint-plugin-import: '>= 2' - '@misskey-dev/node-http-message-signatures@0.0.8': - resolution: {integrity: sha512-ymyjluFpY2xK/3UYyj+MiRfA7752BIWwSb+uFU0mFK1Yq2fGI6lAGZg3+D2lD8W/OkltnMQm1gSOXzE3CNIgtA==} + '@misskey-dev/node-http-message-signatures@0.0.10': + resolution: {integrity: sha512-HiAuc//tOU077KFUJhHYLAPWku9enTpOFIqQiK6l2i2mIizRvv7HhV7Y+yuav5quDOAz+WZGK/i5C9OR5fkKIg==} engines: {node: '>=18.4.0'} '@misskey-dev/sharp-read-bmp@1.2.0': @@ -13441,7 +13442,7 @@ snapshots: '@kurkle/color@0.3.2': {} - '@lapo/asn1js@1.3.0': {} + '@lapo/asn1js@2.0.4': {} '@levischuck/tiny-cbor@0.2.2': {} @@ -13535,9 +13536,9 @@ snapshots: eslint: 8.57.0 eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.7.1(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0) - '@misskey-dev/node-http-message-signatures@0.0.8': + '@misskey-dev/node-http-message-signatures@0.0.10': dependencies: - '@lapo/asn1js': 1.3.0 + '@lapo/asn1js': 2.0.4 rfc4648: 1.5.3 structured-headers: 1.0.1 From f31996eb42df2834fab105b0b4ccfcfe21069a72 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 11 Jun 2024 14:37:22 +0900 Subject: [PATCH 100/635] add comment --- .../backend/src/core/activitypub/ApInboxService.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index de3178b482..62287615b7 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -115,15 +115,8 @@ export class ApInboxService { result = await this.performOneActivity(actor, activity); } - // ついでにリモートユーザーの情報が古かったら更新しておく - if (actor.uri) { - if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { - setImmediate(() => { - this.apPersonService.updatePerson(actor.uri); - }); - } - } - return result; + // ついでにリモートユーザーの情報が古かったら更新しておく? + // → No, この関数が呼び出される前に署名検証で更新されているはず } @bindThis From 3717ff35a3bc464c32d08584e0251ddd061ccf67 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 11 Jun 2024 14:45:09 +0900 Subject: [PATCH 101/635] fix --- packages/backend/src/server/ActivityPubServerService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index 7745c2a448..753eaad047 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -78,8 +78,8 @@ export class ActivityPubServerService { private loggerService: LoggerService, ) { //this.createServer = this.createServer.bind(this); - this.logger = this.loggerService.getLogger('server-ap', 'gray', false); - this.inboxLogger = this.logger.createSubLogger('inbox', 'gray', false); + this.logger = this.loggerService.getLogger('server-ap', 'gray'); + this.inboxLogger = this.logger.createSubLogger('inbox', 'gray'); } @bindThis From 64004fdea244f039c34e9f59a103be9b9e164f77 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 11 Jun 2024 15:32:55 +0900 Subject: [PATCH 102/635] =?UTF-8?q?publicKey=E3=81=AB=E9=85=8D=E5=88=97?= =?UTF-8?q?=E3=81=8C=E5=85=A5=E3=81=A3=E3=81=A6=E3=82=82=E3=81=84=E3=81=84?= =?UTF-8?q?=E3=82=88=E3=81=86=E3=81=AB=E3=81=99=E3=82=8B=20https://github.?= =?UTF-8?q?com/misskey-dev/misskey/pull/13950?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../activitypub/models/ApPersonService.ts | 21 +++++++++++++------ packages/backend/src/core/activitypub/type.ts | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f315e49a54..a712ca95ca 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -184,15 +184,24 @@ export class ApPersonService implements OnModuleInit { throw new Error('invalid Actor: id has different host'); } - if (x.publicKey) { - if (typeof x.publicKey.id !== 'string') { - throw new Error('invalid Actor: publicKey.id is not a string'); - } - + if (x.publicKey && typeof x.publicKey.id !== 'string') { const publicKeyIdHost = this.punyHost(x.publicKey.id); if (publicKeyIdHost !== expectHost) { throw new Error('invalid Actor: publicKey.id has different host'); } + } else if (x.publicKey && Array.isArray(x.publicKey)) { + for (const publicKey of x.publicKey) { + if (typeof publicKey.id !== 'string') { + throw new Error('invalid Actor: publicKey.id is not a string'); + } + + const publicKeyIdHost = this.punyHost(publicKey.id); + if (publicKeyIdHost !== expectHost) { + throw new Error('invalid Actor: publicKey.id has different host'); + } + } + } else if (x.publicKey) { + throw new Error('invalid Actor: publicKey is not an object or an array'); } if (x.additionalPublicKeys) { @@ -408,7 +417,7 @@ export class ApPersonService implements OnModuleInit { if (person.publicKey) { const publicKeys = new Map([ ...(person.additionalPublicKeys ? person.additionalPublicKeys.map(key => [key.id, key] as const) : []), - [person.publicKey.id, person.publicKey], + ...(Array.isArray(person.publicKey) ? person.publicKey.map(key => [key.id, key] as const) : [[person.publicKey.id, person.publicKey]] as const), ]); await transactionalEntityManager.save(Array.from(publicKeys.values(), key => new MiUserPublickey({ diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 0cd941ae19..368650a83d 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -169,7 +169,7 @@ export interface IActor extends IObject { discoverable?: boolean; inbox: string; sharedInbox?: string; // 後方互換性のため - publicKey?: IKey; + publicKey?: IKey | IKey[]; additionalPublicKeys?: IKey[]; followers?: string | ICollection | IOrderedCollection; following?: string | ICollection | IOrderedCollection; From 133970a184fe8ab955055def3248665b7e0710cf Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 11 Jun 2024 15:42:17 +0900 Subject: [PATCH 103/635] define additionalPublicKeys --- packages/backend/src/core/activitypub/misc/contexts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index feb8c42c56..fc4e3e3bef 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -134,6 +134,7 @@ const security_v1 = { 'privateKey': { '@id': 'sec:privateKey', '@type': '@id' }, 'privateKeyPem': 'sec:privateKeyPem', 'publicKey': { '@id': 'sec:publicKey', '@type': '@id' }, + 'additionalPublicKeys': { '@id': 'sec:publicKey', '@type': '@id' }, 'publicKeyBase58': 'sec:publicKeyBase58', 'publicKeyPem': 'sec:publicKeyPem', 'publicKeyWif': 'sec:publicKeyWif', From a3d4eae99d847466fa041810c0066a284dfd32e5 Mon Sep 17 00:00:00 2001 From: tamaina Date: Tue, 11 Jun 2024 15:50:55 +0900 Subject: [PATCH 104/635] fix --- .../activitypub/models/ApPersonService.ts | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index a712ca95ca..7ff1254660 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -184,13 +184,10 @@ export class ApPersonService implements OnModuleInit { throw new Error('invalid Actor: id has different host'); } - if (x.publicKey && typeof x.publicKey.id !== 'string') { - const publicKeyIdHost = this.punyHost(x.publicKey.id); - if (publicKeyIdHost !== expectHost) { - throw new Error('invalid Actor: publicKey.id has different host'); - } - } else if (x.publicKey && Array.isArray(x.publicKey)) { - for (const publicKey of x.publicKey) { + if (x.publicKey) { + const publicKeys = Array.isArray(x.publicKey) ? x.publicKey : [x.publicKey]; + + for (const publicKey of publicKeys) { if (typeof publicKey.id !== 'string') { throw new Error('invalid Actor: publicKey.id is not a string'); } @@ -200,8 +197,6 @@ export class ApPersonService implements OnModuleInit { throw new Error('invalid Actor: publicKey.id has different host'); } } - } else if (x.publicKey) { - throw new Error('invalid Actor: publicKey is not an object or an array'); } if (x.additionalPublicKeys) { @@ -415,10 +410,9 @@ export class ApPersonService implements OnModuleInit { })); if (person.publicKey) { - const publicKeys = new Map([ - ...(person.additionalPublicKeys ? person.additionalPublicKeys.map(key => [key.id, key] as const) : []), - ...(Array.isArray(person.publicKey) ? person.publicKey.map(key => [key.id, key] as const) : [[person.publicKey.id, person.publicKey]] as const), - ]); + const publicKeys = new Map(); + (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); + (Array.isArray(person.publicKey) ? person.publicKey : [person.publicKey]).forEach(key => publicKeys.set(key.id, key)); await transactionalEntityManager.save(Array.from(publicKeys.values(), key => new MiUserPublickey({ keyId: key.id, @@ -576,7 +570,7 @@ export class ApPersonService implements OnModuleInit { const publicKeys = new Map(); if (person.publicKey) { (person.additionalPublicKeys ?? []).forEach(key => publicKeys.set(key.id, key)); - publicKeys.set(person.publicKey.id, person.publicKey); + (Array.isArray(person.publicKey) ? person.publicKey : [person.publicKey]).forEach(key => publicKeys.set(key.id, key)); await this.userPublickeysRepository.save(Array.from(publicKeys.values(), key => ({ keyId: key.id, From 7353c7397f6064e265c4233aa500b237d312c4b2 Mon Sep 17 00:00:00 2001 From: tamaina Date: Sat, 29 Jun 2024 09:24:14 +0900 Subject: [PATCH 105/635] merge fix --- packages/backend/src/core/activitypub/ApRequestService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 420844cc17..9359dccc86 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -5,7 +5,7 @@ import { URL } from 'node:url'; import { Inject, Injectable } from '@nestjs/common'; -import { genRFC3230DigestHeader, RequestLike, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures'; +import { genRFC3230DigestHeader, signAsDraftToRequest } from '@misskey-dev/node-http-message-signatures'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { MiUser } from '@/models/User.js'; From c80b16cdf8140d8d1fcb41e2fd190ff78eef4237 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 15:29:13 +0900 Subject: [PATCH 106/635] =?UTF-8?q?refreshAndprepareEd25519KeyPair=20?= =?UTF-8?q?=E2=86=92=20refreshAndPrepareEd25519KeyPair?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/core/UserKeypairService.ts | 2 +- .../backend/src/core/activitypub/ApDeliverManagerService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 74d1257bc3..9edc8ecc27 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -141,7 +141,7 @@ export class UserKeypairService implements OnApplicationShutdown { * @returns MiUserKeypair if keypair is created, void if keypair is already exists */ @bindThis - public async refreshAndprepareEd25519KeyPair(userId: MiUser['id']): Promise { + public async refreshAndPrepareEd25519KeyPair(userId: MiUser['id']): Promise { await this.refresh(userId); const keypair = await this.keypairEntityCache.fetch(userId); if (keypair.ed25519PublicKey != null) { diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index e09b548d3a..decb021561 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -135,7 +135,7 @@ class DeliverManager { /** * ed25519の署名がなければ追加する */ - const created = await this.userKeypairService.refreshAndprepareEd25519KeyPair(this.actor.id); + const created = await this.userKeypairService.refreshAndPrepareEd25519KeyPair(this.actor.id); if (created) { // createdが存在するということは新規作成されたということなので、フォロワーに配信する this.logger.info(`ed25519 key pair created for user ${this.actor.id} and publishing to followers`); From 7e2c3e4439a6539aeea014af43e9f005ef806a7b Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:03:44 +0900 Subject: [PATCH 107/635] remove gen-key-pair.ts --- .../backend/test/unit/misc/gen-key-pair.ts | 39 ------------------- 1 file changed, 39 deletions(-) delete mode 100644 packages/backend/test/unit/misc/gen-key-pair.ts diff --git a/packages/backend/test/unit/misc/gen-key-pair.ts b/packages/backend/test/unit/misc/gen-key-pair.ts deleted file mode 100644 index 9d76c68009..0000000000 --- a/packages/backend/test/unit/misc/gen-key-pair.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import * as crypto from 'node:crypto'; -import { genRSAAndEd25519KeyPair } from '@/misc/gen-key-pair.js'; - -describe(genRSAAndEd25519KeyPair, () => { - test('generates key pair', async () => { - const keyPair = await genRSAAndEd25519KeyPair(); - // 毎回違うキーペアが生成されることを確認するために2回生成して比較してみる - const keyPair2 = await genRSAAndEd25519KeyPair(); - console.log(Object.entries(keyPair).map(([k, v]) => `${k}: ${v.length}`).join('\n')); - console.log(Object.entries(keyPair).map(([k, v]) => `${k}\n${v}`).join('\n')); - - expect(keyPair.publicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/); - expect(keyPair.publicKey).toMatch(/-----END PUBLIC KEY-----\n$/); - expect(keyPair.publicKey).not.toBe(keyPair2.publicKey); - - const publicKeyObj = crypto.createPublicKey(keyPair.publicKey); - expect(publicKeyObj.asymmetricKeyType).toBe('rsa'); - - expect(keyPair.privateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/); - expect(keyPair.privateKey).toMatch(/-----END PRIVATE KEY-----\n$/); - expect(keyPair.privateKey).not.toBe(keyPair2.privateKey); - expect(keyPair.ed25519PublicKey).toMatch(/^-----BEGIN PUBLIC KEY-----/); - expect(keyPair.ed25519PublicKey).toMatch(/-----END PUBLIC KEY-----\n$/); - expect(keyPair.ed25519PublicKey).not.toBe(keyPair2.ed25519PublicKey); - - const ed25519PublicKeyObj = crypto.createPublicKey(keyPair.ed25519PublicKey); - expect(ed25519PublicKeyObj.asymmetricKeyType).toBe('ed25519'); - - expect(keyPair.ed25519PrivateKey).toMatch(/^-----BEGIN PRIVATE KEY-----/); - expect(keyPair.ed25519PrivateKey).toMatch(/-----END PRIVATE KEY-----\n$/); - expect(keyPair.ed25519PrivateKey).not.toBe(keyPair2.ed25519PrivateKey); - //const imported = await webCrypto.subtle.importKey('spki', Buffer.from(keyPair.publicKey).buffer, { name: 'rsa-pss', hash: 'sha-256' }, false, ['verify']); - }); -}); From f2c412c18057a9300540794ccbe4dfbf6d259ed6 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:14:24 +0900 Subject: [PATCH 108/635] defaultMaxListeners = 512 --- packages/backend/src/boot/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index 25375c3015..aef999b683 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -22,7 +22,7 @@ import 'reflect-metadata'; process.title = `Misskey (${cluster.isPrimary ? 'master' : 'worker'})`; Error.stackTraceLimit = Infinity; -EventEmitter.defaultMaxListeners = 128; +EventEmitter.defaultMaxListeners = 512; const logger = new Logger('core', 'cyan'); const clusterLogger = logger.createSubLogger('cluster', 'orange'); From 57bfffedae107864ee8b305cf3e3cd6de6958ef3 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:17:09 +0900 Subject: [PATCH 109/635] Revert "defaultMaxListeners = 512" This reverts commit f2c412c18057a9300540794ccbe4dfbf6d259ed6. --- packages/backend/src/boot/entry.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/boot/entry.ts b/packages/backend/src/boot/entry.ts index aef999b683..25375c3015 100644 --- a/packages/backend/src/boot/entry.ts +++ b/packages/backend/src/boot/entry.ts @@ -22,7 +22,7 @@ import 'reflect-metadata'; process.title = `Misskey (${cluster.isPrimary ? 'master' : 'worker'})`; Error.stackTraceLimit = Infinity; -EventEmitter.defaultMaxListeners = 512; +EventEmitter.defaultMaxListeners = 128; const logger = new Logger('core', 'cyan'); const clusterLogger = logger.createSubLogger('cluster', 'orange'); From d0aada55c1ed5aa98f18731ec82f3ac5eb5a6c16 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:20:39 +0900 Subject: [PATCH 110/635] =?UTF-8?q?genRSAAndEd25519KeyPair=E3=81=A7?= =?UTF-8?q?=E3=81=AF=E3=82=AD=E3=83=BC=E3=82=92=E7=9B=B4=E5=88=97=E3=81=AB?= =?UTF-8?q?=E7=94=9F=E6=88=90=E3=81=99=E3=82=8B=3F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/src/misc/gen-key-pair.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 0b033ec33e..58e3091369 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -6,7 +6,8 @@ import { genEd25519KeyPair, genRsaKeyPair } from '@misskey-dev/node-http-message-signatures'; export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { - const [rsa, ed25519] = await Promise.all([genRsaKeyPair(rsaModulusLength), genEd25519KeyPair()]); + const rsa = await genRsaKeyPair(rsaModulusLength); + const ed25519 = await genEd25519KeyPair(); return { publicKey: rsa.publicKey, privateKey: rsa.privateKey, From 613c1273b84be74d80743afd0d203f99384296c5 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:30:54 +0900 Subject: [PATCH 111/635] maxConcurrency: 8 --- packages/backend/jest.config.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 5a4aa4e15a..1552fa3620 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -216,5 +216,5 @@ module.exports = { logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) - maxConcurrency: 32, + maxConcurrency: 8, }; From a80a7f6458ac20bad167b6ba7804c3c12db4a2a3 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:53:34 +0900 Subject: [PATCH 112/635] maxConcurrency: 16 --- packages/backend/jest.config.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 1552fa3620..d5565c8412 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -216,5 +216,5 @@ module.exports = { logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) - maxConcurrency: 8, + maxConcurrency: 16, }; From c2d084bac4857b877c7f9deded389906242485d5 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:58:55 +0900 Subject: [PATCH 113/635] maxConcurrency: 8 --- packages/backend/jest.config.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index d5565c8412..1552fa3620 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -216,5 +216,5 @@ module.exports = { logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) - maxConcurrency: 16, + maxConcurrency: 8, }; From d3280fe7b365aa1f4577765623cfacbe15ae2448 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 16:59:01 +0900 Subject: [PATCH 114/635] =?UTF-8?q?Revert=20"genRSAAndEd25519KeyPair?= =?UTF-8?q?=E3=81=A7=E3=81=AF=E3=82=AD=E3=83=BC=E3=82=92=E7=9B=B4=E5=88=97?= =?UTF-8?q?=E3=81=AB=E7=94=9F=E6=88=90=E3=81=99=E3=82=8B=3F"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d0aada55c1ed5aa98f18731ec82f3ac5eb5a6c16. --- packages/backend/src/misc/gen-key-pair.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/backend/src/misc/gen-key-pair.ts b/packages/backend/src/misc/gen-key-pair.ts index 58e3091369..0b033ec33e 100644 --- a/packages/backend/src/misc/gen-key-pair.ts +++ b/packages/backend/src/misc/gen-key-pair.ts @@ -6,8 +6,7 @@ import { genEd25519KeyPair, genRsaKeyPair } from '@misskey-dev/node-http-message-signatures'; export async function genRSAAndEd25519KeyPair(rsaModulusLength = 4096) { - const rsa = await genRsaKeyPair(rsaModulusLength); - const ed25519 = await genEd25519KeyPair(); + const [rsa, ed25519] = await Promise.all([genRsaKeyPair(rsaModulusLength), genEd25519KeyPair()]); return { publicKey: rsa.publicKey, privateKey: rsa.privateKey, From 9e0a93f110456320d6485a871f014f7cdab29b33 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 17:08:08 +0900 Subject: [PATCH 115/635] maxWorkers: '90%' --- packages/backend/jest.config.cjs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 1552fa3620..a4bd6ea9a2 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -23,7 +23,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'], // The directory where Jest should output its coverage files - coverageDirectory: "coverage", + coverageDirectory: 'coverage', // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ @@ -31,7 +31,7 @@ module.exports = { // ], // Indicates which provider should be used to instrument code for coverage - coverageProvider: "v8", + coverageProvider: 'v8', // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ @@ -129,7 +129,7 @@ module.exports = { // A list of paths to directories that Jest should use to search for files in roots: [ - "" + '', ], // Allows you to use a custom runner instead of Jest's default test runner @@ -148,7 +148,7 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: "node", + testEnvironment: 'node', // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, @@ -158,8 +158,8 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ - "/test/unit/**/*.ts", - "/src/**/*.test.ts", + '/test/unit/**/*.ts', + '/src/**/*.test.ts', ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped @@ -184,7 +184,7 @@ module.exports = { // A map from regular expressions to paths to transformers transform: { - "^.+\\.(t|j)sx?$": ["@swc/jest"], + '^.+\\.(t|j)sx?$': ['@swc/jest'], }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation @@ -212,7 +212,7 @@ module.exports = { // Let Jest kill the test worker whenever it grows too much // (It seems there's a known memory leak issue in Node.js' vm.Script used by Jest) // https://github.com/facebook/jest/issues/11956 - maxWorkers: 1, // Make it use worker (that can be killed and restarted) + maxWorkers: '90%', // Make it use worker (that can be killed and restarted) logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) From 38a5e09a3693e57bc65e27b45457a6812b0727d1 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 17:10:10 +0900 Subject: [PATCH 116/635] Revert "maxWorkers: '90%'" This reverts commit 9e0a93f110456320d6485a871f014f7cdab29b33. --- packages/backend/jest.config.cjs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index a4bd6ea9a2..1552fa3620 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -23,7 +23,7 @@ module.exports = { collectCoverageFrom: ['src/**/*.ts', '!src/**/*.test.ts'], // The directory where Jest should output its coverage files - coverageDirectory: 'coverage', + coverageDirectory: "coverage", // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ @@ -31,7 +31,7 @@ module.exports = { // ], // Indicates which provider should be used to instrument code for coverage - coverageProvider: 'v8', + coverageProvider: "v8", // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ @@ -129,7 +129,7 @@ module.exports = { // A list of paths to directories that Jest should use to search for files in roots: [ - '', + "" ], // Allows you to use a custom runner instead of Jest's default test runner @@ -148,7 +148,7 @@ module.exports = { // snapshotSerializers: [], // The test environment that will be used for testing - testEnvironment: 'node', + testEnvironment: "node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, @@ -158,8 +158,8 @@ module.exports = { // The glob patterns Jest uses to detect test files testMatch: [ - '/test/unit/**/*.ts', - '/src/**/*.test.ts', + "/test/unit/**/*.ts", + "/src/**/*.test.ts", ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped @@ -184,7 +184,7 @@ module.exports = { // A map from regular expressions to paths to transformers transform: { - '^.+\\.(t|j)sx?$': ['@swc/jest'], + "^.+\\.(t|j)sx?$": ["@swc/jest"], }, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation @@ -212,7 +212,7 @@ module.exports = { // Let Jest kill the test worker whenever it grows too much // (It seems there's a known memory leak issue in Node.js' vm.Script used by Jest) // https://github.com/facebook/jest/issues/11956 - maxWorkers: '90%', // Make it use worker (that can be killed and restarted) + maxWorkers: 1, // Make it use worker (that can be killed and restarted) logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) From 44f00643010bfc96b1c603cc80579d0baed283b6 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 17:33:27 +0900 Subject: [PATCH 117/635] =?UTF-8?q?e2e/timelines.ts=E3=81=A7=E5=80=8B?= =?UTF-8?q?=E3=80=85=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E3=81=99=E3=82=8Btimeout=E3=82=92=E5=89=8A=E9=99=A4,=20maxConc?= =?UTF-8?q?urrency:=2032?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/jest.config.cjs | 2 +- packages/backend/test/e2e/timelines.ts | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/jest.config.cjs b/packages/backend/jest.config.cjs index 1552fa3620..5a4aa4e15a 100644 --- a/packages/backend/jest.config.cjs +++ b/packages/backend/jest.config.cjs @@ -216,5 +216,5 @@ module.exports = { logHeapUsage: true, // To debug when out-of-memory happens on CI workerIdleMemoryLimit: '1GiB', // Limit the worker to 1GB (GitHub Workflows dies at 2GB) - maxConcurrency: 8, + maxConcurrency: 32, }; diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index 540b866b28..fce1eacf00 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -378,7 +378,7 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); assert.strictEqual(res.body.some(note => note.id === carolNote1.id), false); assert.strictEqual(res.body.some(note => note.id === carolNote2.id), false); - }, 1000 * 10); + }); test.concurrent('フォローしているユーザーのチャンネル投稿が含まれない', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); @@ -672,7 +672,7 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); + }); }); describe('Social TL', () => { @@ -812,7 +812,7 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); + }); }); describe('User List TL', () => { @@ -1025,7 +1025,7 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); + }); test.concurrent('リスインしているユーザーの自身宛ての visibility: specified なノートが含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); @@ -1184,7 +1184,7 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote1.id), false); assert.strictEqual(res.body.some(note => note.id === bobNote2.id), true); - }, 1000 * 10); + }); test.concurrent('[withChannelNotes: true] チャンネル投稿が含まれる', async () => { const [alice, bob] = await Promise.all([signup(), signup()]); From 09b2e71e62179ff102120ce478ec7efa1ffbf111 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 17:57:45 +0900 Subject: [PATCH 118/635] better error handling of this.userPublickeysRepository.delete --- .../backend/src/core/activitypub/models/ApPersonService.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index bea9d02abd..c41fc713d5 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -589,9 +589,11 @@ export class ApPersonService implements OnModuleInit { this.userPublickeysRepository.delete({ keyId: Not(In(Array.from(publicKeys.keys()))), userId: exist.id, + }).catch(err => { + this.logger.error('something happened while deleting remote user public keys:', { userId: exist.id, err }); }); } catch (err) { - this.logger.error('something happened while updating remote user public keys:', { err }); + this.logger.error('something happened while updating remote user public keys:', { userId: exist.id, err }); } let _description: string | null = null; From 41883c451d88253705d6133a4dad2ecb19b1b26a Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 18:01:03 +0900 Subject: [PATCH 119/635] better comment --- packages/backend/src/core/AccountUpdateService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/AccountUpdateService.ts b/packages/backend/src/core/AccountUpdateService.ts index d4a7b14554..ca0864f679 100644 --- a/packages/backend/src/core/AccountUpdateService.ts +++ b/packages/backend/src/core/AccountUpdateService.ts @@ -36,9 +36,9 @@ export class AccountUpdateService implements OnModuleInit { @bindThis /** - * ユーザーのアップデートをフォロワーに配信する - * @param userId ユーザーID - * @param isKeyUpdation Ed25519キーの作成など公開鍵のアップデートによる呼び出しか? trueにするとメインキーを使うようになる + * Deliver account update to followers + * @param userId user id + * @param deliverKey optional. Private key to sign the deliver. */ public async publishToFollowers(userId: MiUser['id'], deliverKey?: PrivateKeyWithPem) { const user = await this.usersRepository.findOneBy({ id: userId }); From 5afc659afab86e35542804b715af096498b1601d Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 18:15:17 +0900 Subject: [PATCH 120/635] set result to keypairEntityCache --- packages/backend/src/core/UserKeypairService.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 9edc8ecc27..de8f7c17da 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -136,7 +136,8 @@ export class UserKeypairService implements OnApplicationShutdown { } /** - * + * If DB has ed25519 keypair, refresh cache and return it. + * If not, create, save and return ed25519 keypair. * @param userId user id * @returns MiUserKeypair if keypair is created, void if keypair is already exists */ @@ -154,11 +155,13 @@ export class UserKeypairService implements OnApplicationShutdown { ed25519PrivateKey: ed25519.privateKey, }); this.globalEventService.publishInternalEvent('userKeypairUpdated', { userId }); - return { + const result = { ...keypair, ed25519PublicKey: ed25519.publicKey, ed25519PrivateKey: ed25519.privateKey, }; + this.keypairEntityCache.set(userId, result); + return result; } @bindThis From ffd12d0539785ccb9d51bd7423abeb529f95d4e0 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 18:36:04 +0900 Subject: [PATCH 121/635] deliverJobConcurrency: 16, deliverJobPerSec: 1024, inboxJobConcurrency: 4 --- .config/docker_example.yml | 4 ++-- .config/example.yml | 6 +++--- .devcontainer/devcontainer.yml | 6 +++--- chart/files/default.yml | 6 +++--- packages/backend/src/core/HttpRequestService.ts | 2 +- packages/backend/src/queue/QueueProcessorService.ts | 6 +++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.config/docker_example.yml b/.config/docker_example.yml index d347882d1a..985ba989c4 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -164,8 +164,8 @@ id: 'aidx' #clusterLimit: 1 # Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 +# deliverJobConcurrency: 16 +# inboxJobConcurrency: 4 # Job rate limiter # deliverJobPerSec: 128 diff --git a/.config/example.yml b/.config/example.yml index b11cbd1373..54f21aa0f9 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -230,14 +230,14 @@ id: 'aidx' #clusterLimit: 1 # Job concurrency per worker -#deliverJobConcurrency: 128 -#inboxJobConcurrency: 16 +#deliverJobConcurrency: 16 +#inboxJobConcurrency: 4 #relationshipJobConcurrency: 16 # What's relationshipJob?: # Follow, unfollow, block and unblock(ings) while following-imports, etc. or account migrations. # Job rate limiter -#deliverJobPerSec: 128 +#deliverJobPerSec: 1024 #inboxJobPerSec: 32 #relationshipJobPerSec: 64 diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index beefcfd0a2..42c81588d6 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -157,11 +157,11 @@ id: 'aidx' #clusterLimit: 1 # Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 +# deliverJobConcurrency: 16 +# inboxJobConcurrency: 4 # Job rate limiter -# deliverJobPerSec: 128 +# deliverJobPerSec: 1024 # inboxJobPerSec: 32 # Job attempts diff --git a/chart/files/default.yml b/chart/files/default.yml index f98b8ebfee..7606238048 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -178,11 +178,11 @@ id: "aidx" #clusterLimit: 1 # Job concurrency per worker -# deliverJobConcurrency: 128 -# inboxJobConcurrency: 16 +# deliverJobConcurrency: 16 +# inboxJobConcurrency: 4 # Job rate limiter -# deliverJobPerSec: 128 +# deliverJobPerSec: 1024 # inboxJobPerSec: 32 # Job attempts diff --git a/packages/backend/src/core/HttpRequestService.ts b/packages/backend/src/core/HttpRequestService.ts index 7f3cac7c58..4249c158d7 100644 --- a/packages/backend/src/core/HttpRequestService.ts +++ b/packages/backend/src/core/HttpRequestService.ts @@ -70,7 +70,7 @@ export class HttpRequestService { localAddress: config.outgoingAddress, }); - const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 128); + const maxSockets = Math.max(256, config.deliverJobConcurrency ?? 16); this.httpAgent = config.proxy ? new HttpProxyAgent({ diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index 7bd74f3210..df56391e9e 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -250,9 +250,9 @@ export class QueueProcessorService implements OnApplicationShutdown { }, { ...baseQueueOptions(this.config, QUEUE.DELIVER), autorun: false, - concurrency: this.config.deliverJobConcurrency ?? 128, + concurrency: this.config.deliverJobConcurrency ?? 16, limiter: { - max: this.config.deliverJobPerSec ?? 128, + max: this.config.deliverJobPerSec ?? 1024, duration: 1000, }, settings: { @@ -290,7 +290,7 @@ export class QueueProcessorService implements OnApplicationShutdown { }, { ...baseQueueOptions(this.config, QUEUE.INBOX), autorun: false, - concurrency: this.config.inboxJobConcurrency ?? 16, + concurrency: this.config.inboxJobConcurrency ?? 4, limiter: { max: this.config.inboxJobPerSec ?? 32, duration: 1000, From fe77f216c38802200f1ba4dca58d6e1a6663d49a Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 22:49:07 +0900 Subject: [PATCH 122/635] inboxJobPerSec: 64 --- .config/docker_example.yml | 2 +- .config/example.yml | 2 +- .devcontainer/devcontainer.yml | 2 +- chart/files/default.yml | 2 +- packages/backend/src/queue/QueueProcessorService.ts | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.config/docker_example.yml b/.config/docker_example.yml index 985ba989c4..bd0ad2872a 100644 --- a/.config/docker_example.yml +++ b/.config/docker_example.yml @@ -169,7 +169,7 @@ id: 'aidx' # Job rate limiter # deliverJobPerSec: 128 -# inboxJobPerSec: 32 +# inboxJobPerSec: 64 # Job attempts # deliverJobMaxAttempts: 12 diff --git a/.config/example.yml b/.config/example.yml index 54f21aa0f9..0d525f61c4 100644 --- a/.config/example.yml +++ b/.config/example.yml @@ -238,7 +238,7 @@ id: 'aidx' # Job rate limiter #deliverJobPerSec: 1024 -#inboxJobPerSec: 32 +#inboxJobPerSec: 64 #relationshipJobPerSec: 64 # Job attempts diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml index 42c81588d6..d74d741e02 100644 --- a/.devcontainer/devcontainer.yml +++ b/.devcontainer/devcontainer.yml @@ -162,7 +162,7 @@ id: 'aidx' # Job rate limiter # deliverJobPerSec: 1024 -# inboxJobPerSec: 32 +# inboxJobPerSec: 64 # Job attempts # deliverJobMaxAttempts: 12 diff --git a/chart/files/default.yml b/chart/files/default.yml index 7606238048..4017588fa0 100644 --- a/chart/files/default.yml +++ b/chart/files/default.yml @@ -183,7 +183,7 @@ id: "aidx" # Job rate limiter # deliverJobPerSec: 1024 -# inboxJobPerSec: 32 +# inboxJobPerSec: 64 # Job attempts # deliverJobMaxAttempts: 12 diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts index df56391e9e..169b22c3f5 100644 --- a/packages/backend/src/queue/QueueProcessorService.ts +++ b/packages/backend/src/queue/QueueProcessorService.ts @@ -292,7 +292,7 @@ export class QueueProcessorService implements OnApplicationShutdown { autorun: false, concurrency: this.config.inboxJobConcurrency ?? 4, limiter: { - max: this.config.inboxJobPerSec ?? 32, + max: this.config.inboxJobPerSec ?? 64, duration: 1000, }, settings: { From b7349e5771ad29377e97b7a1ed3c46f3960f196e Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 23:03:30 +0900 Subject: [PATCH 123/635] delete request.headers['host']; --- packages/backend/src/core/activitypub/ApRequestService.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 9359dccc86..1b0ae6b5aa 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -40,6 +40,8 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u ['(request-target)', 'date', 'host', 'digest'], ); + delete request.headers['host']; + return { request, ...result, @@ -66,6 +68,8 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur ['(request-target)', 'date', 'host', 'accept'], ); + delete request.headers['host']; + return { request, ...result, From 1f0e7a40b6a120555fd908f9f305c086af717e12 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 23:26:57 +0900 Subject: [PATCH 124/635] fix --- packages/backend/src/core/activitypub/ApRequestService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 1b0ae6b5aa..e48c1865d9 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -40,7 +40,7 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u ['(request-target)', 'date', 'host', 'digest'], ); - delete request.headers['host']; + delete request.headers['Host']; return { request, @@ -68,7 +68,7 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur ['(request-target)', 'date', 'host', 'accept'], ); - delete request.headers['host']; + delete request.headers['Host']; return { request, From de677a5b1fbf6ab9d2438f94e373c836c71f240b Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 23:28:37 +0900 Subject: [PATCH 125/635] // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! --- packages/backend/src/core/activitypub/ApRequestService.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index e48c1865d9..352a45fcfc 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -40,6 +40,7 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u ['(request-target)', 'date', 'host', 'digest'], ); + // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! delete request.headers['Host']; return { @@ -68,6 +69,7 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur ['(request-target)', 'date', 'host', 'accept'], ); + // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! delete request.headers['Host']; return { From e602f2efdaad1e677b3e1e2d69824a22367ff550 Mon Sep 17 00:00:00 2001 From: tamaina Date: Wed, 17 Jul 2024 23:55:39 +0900 Subject: [PATCH 126/635] move delete host --- .../backend/src/core/activitypub/ApRequestService.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 352a45fcfc..0cae91316b 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -40,9 +40,6 @@ export async function createSignedPost(args: { level: string; key: PrivateKey; u ['(request-target)', 'date', 'host', 'digest'], ); - // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! - delete request.headers['Host']; - return { request, ...result, @@ -69,9 +66,6 @@ export async function createSignedGet(args: { level: string; key: PrivateKey; ur ['(request-target)', 'date', 'host', 'accept'], ); - // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! - delete request.headers['Host']; - return { request, ...result, @@ -109,6 +103,9 @@ export class ApRequestService { digest, }); + // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! + delete req.request.headers['Host']; + this.logger.debug('create signed post', { version: 'draft', level, @@ -140,6 +137,9 @@ export class ApRequestService { }, }); + // node-fetch will generate this for us. if we keep 'Host', it won't change with redirects! + delete req.request.headers['Host']; + this.logger.debug('create signed get', { version: 'draft', level, From 72cda5ca809171982cded34b256ac1a210990536 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:02:36 +0900 Subject: [PATCH 127/635] modify comment --- packages/backend/src/core/UserKeypairService.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index de8f7c17da..902f008660 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -53,13 +53,14 @@ export class UserKeypairService implements OnApplicationShutdown { * Get private key [Only PrivateKeyWithPem for queue data etc.] * @param userIdOrHint user id or MiUserKeypair * @param preferType - * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists. - * Otherwise, main keypair is returned. + * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists. + * Otherwise, main keypair will be returned. * @returns */ @bindThis public async getLocalUserPrivateKeyPem( - userIdOrHint: MiUser['id'] | MiUserKeypair, preferType?: string, + userIdOrHint: MiUser['id'] | MiUserKeypair, + preferType?: string, ): Promise { const keypair = typeof userIdOrHint === 'string' ? await this.getUserKeypair(userIdOrHint) : userIdOrHint; if ( From 95918607f4ee404ddcdd1db8cde7a6d92331f965 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:04:35 +0900 Subject: [PATCH 128/635] modify comment --- packages/backend/src/core/UserKeypairService.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/backend/src/core/UserKeypairService.ts b/packages/backend/src/core/UserKeypairService.ts index 902f008660..aa90f1e209 100644 --- a/packages/backend/src/core/UserKeypairService.ts +++ b/packages/backend/src/core/UserKeypairService.ts @@ -83,13 +83,14 @@ export class UserKeypairService implements OnApplicationShutdown { * Using cache due to performance reasons of `crypto.subtle.importKey` * @param userIdOrHint user id, MiUserKeypair, or PrivateKeyWithPem * @param preferType - * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair is returned if exists. - * Otherwise, main keypair is returned. (ignored if userIdOrHint is PrivateKeyWithPem) + * If ed25519-like(`ed25519`, `01`, `11`) is specified, ed25519 keypair will be returned if exists. + * Otherwise, main keypair will be returned. (ignored if userIdOrHint is PrivateKeyWithPem) * @returns */ @bindThis public async getLocalUserPrivateKey( - userIdOrHint: MiUser['id'] | MiUserKeypair | PrivateKeyWithPem, preferType?: string, + userIdOrHint: MiUser['id'] | MiUserKeypair | PrivateKeyWithPem, + preferType?: string, ): Promise { if (typeof userIdOrHint === 'object' && 'privateKeyPem' in userIdOrHint) { // userIdOrHint is PrivateKeyWithPem From cd19ad694c520dcdef1499254da20df2cee942b9 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:09:20 +0900 Subject: [PATCH 129/635] =?UTF-8?q?fix=20correct=20=E2=86=92=20collect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../backend/src/core/activitypub/ApDeliverManagerService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index decb021561..db3302e6ff 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -146,7 +146,7 @@ class DeliverManager { } //#endregion - //#region correct inboxes by recipes + //#region collect inboxes by recipes // The value flags whether it is shared or not. // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map(); From 99113d59f454e1d9107cc8dd63232e029d445b2c Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:12:21 +0900 Subject: [PATCH 130/635] =?UTF-8?q?refreshAndfindKey=20=E2=86=92=20refresh?= =?UTF-8?q?AndFindKey?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/core/activitypub/ApDbResolverService.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 66f649c9bd..75c2241233 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -128,16 +128,16 @@ export class ApDbResolverService implements OnApplicationShutdown { } @bindThis - private async refreshAndfindKey(userId: MiUser['id'], keyId: string): Promise { + private async refreshAndFindKey(userId: MiUser['id'], keyId: string): Promise { this.refreshCacheByUserId(userId); const keys = await this.getPublicKeyByUserId(userId); if (keys == null || !Array.isArray(keys) || keys.length === 0) { - this.logger.warn(`No key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); + this.logger.warn(`No key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); return null; } const exactKey = keys.find(x => x.keyId === keyId); if (exactKey) return exactKey; - this.logger.warn(`No exact key found (refreshAndfindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); + this.logger.warn(`No exact key found (refreshAndFindKey) userId=${userId} keyId=${keyId} keys=${JSON.stringify(keys)}`); return null; } @@ -213,7 +213,7 @@ export class ApDbResolverService implements OnApplicationShutdown { // まずはキャッシュを更新して再取得 const cacheRaw = this.publicKeyByUserIdCache.cache.get(user.id); if (cacheRaw && cacheRaw.date > Date.now() - 1000 * 60 * 12) { - const exactKey = await this.refreshAndfindKey(user.id, keyId); + const exactKey = await this.refreshAndFindKey(user.id, keyId); if (exactKey) return { user, key: exactKey }; } @@ -223,7 +223,7 @@ export class ApDbResolverService implements OnApplicationShutdown { const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0); if (renewed == null || renewed.isDeleted) return null; - return { user, key: await this.refreshAndfindKey(user.id, keyId) }; + return { user, key: await this.refreshAndFindKey(user.id, keyId) }; } this.logger.warn(`No key found uri=${uri} userId=${user.id} keyId=${keyId}`); From c00b61e90b3a68248e028190f918a9b335a2743b Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:24:47 +0900 Subject: [PATCH 131/635] modify comment --- .../core/activitypub/ApDbResolverService.ts | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 75c2241233..973394683f 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -146,9 +146,10 @@ export class ApDbResolverService implements OnApplicationShutdown { * @param uri AP Actor id * @param keyId Key id to find. If not specified, main key will be selected. * @returns - * 1. ユーザーとキーのホストが一致しない場合`null` - * 2. userが見つからない場合`{ user: null, key: null }` - * 3. keyが見つからない場合`{ user, key: null }` + * 1. `null` if the user and key host do not match + * 2. `{ user: null, key: null }` if the user is not found + * 3. `{ user: MiRemoteUser, key: null }` if key is not found + * 4. `{ user: MiRemoteUser, key: MiUserPublickey }` if both are found */ @bindThis public async getAuthUserFromApId(uri: string, keyId?: string): Promise<{ @@ -165,11 +166,21 @@ export class ApDbResolverService implements OnApplicationShutdown { * keyIdはURL形式かつkeyIdのホストはuriのホストと一致するはず * (ApPersonService.validateActorに由来) * - * ただ、Mastodonはリプライ関連で他人のノートをHTTP Signature署名して送ってくることがある + * ただ、Mastodonはリプライ関連で他人のトゥートをHTTP Signature署名して送ってくることがある * そのような署名は有効性に疑問があるので無視することにする * ここではuriとkeyIdのホストが一致しない場合は無視する * ハッシュをなくしたkeyIdとuriの同一性を比べてみてもいいが、`uri#*-key`というkeyIdを設定するのが * 決まりごとというわけでもないため幅を持たせることにする + * + * + * The keyId should be in URL format and its host should match the host of the uri + * (derived from ApPersonService.validateActor) + * + * However, Mastodon sometimes sends toots from other users with HTTP Signature signing for reply-related purposes + * Such signatures are of questionable validity, so we choose to ignore them + * Here, we ignore cases where the hosts of uri and keyId do not match + * We could also compare the equality of keyId without the hash and uri, but since setting a keyId like `uri#*-key` + * is not a strict rule, we decide to allow for some flexibility */ this.logger.warn(`actor uri and keyId are not matched uri=${uri} keyId=${keyId}`); return null; @@ -187,7 +198,7 @@ export class ApDbResolverService implements OnApplicationShutdown { } if (!keyId) { - // mainっぽいのを選ぶ + // Choose the main-like const mainKey = keys.find(x => { try { const url = new URL(x.keyId); @@ -209,15 +220,20 @@ export class ApDbResolverService implements OnApplicationShutdown { const exactKey = keys.find(x => x.keyId === keyId); if (exactKey) return { user, key: exactKey }; - // keyIdで見つからない場合 - // まずはキャッシュを更新して再取得 + /** + * keyIdで見つからない場合、まずはキャッシュを更新して再取得 + * If not found with keyId, update cache and reacquire + */ const cacheRaw = this.publicKeyByUserIdCache.cache.get(user.id); if (cacheRaw && cacheRaw.date > Date.now() - 1000 * 60 * 12) { const exactKey = await this.refreshAndFindKey(user.id, keyId); if (exactKey) return { user, key: exactKey }; } - // lastFetchedAtでの更新制限を弱めて再取得 + /** + * lastFetchedAtでの更新制限を弱めて再取得 + * Reacquisition with weakened update limit at lastFetchedAt + */ if (user.lastFetchedAt == null || user.lastFetchedAt < new Date(Date.now() - 1000 * 60 * 12)) { this.logger.info(`Fetching user to find public key uri=${uri} userId=${user.id} keyId=${keyId}`); const renewed = await this.apPersonService.fetchPersonWithRenewal(uri, 0); From 29d9bbf05b9ef5f65004915ae2242db0da89eadf Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:29:15 +0900 Subject: [PATCH 132/635] modify attachLdSignature --- packages/backend/src/core/RelayService.ts | 2 +- packages/backend/src/core/activitypub/ApRendererService.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/backend/src/core/RelayService.ts b/packages/backend/src/core/RelayService.ts index edf4ffc2e1..ad01f98902 100644 --- a/packages/backend/src/core/RelayService.ts +++ b/packages/backend/src/core/RelayService.ts @@ -125,7 +125,7 @@ export class RelayService { const copy = deepClone(activity); if (!copy.to) copy.to = ['https://www.w3.org/ns/activitystreams#Public']; privateKey = privateKey ?? await this.userKeypairService.getLocalUserPrivateKeyPem(user.id); - const signed = await this.apRendererService.attachLdSignature(copy, user, privateKey); + const signed = await this.apRendererService.attachLdSignature(copy, privateKey); this.queueService.deliverMany(user, signed, new Map(relays.map(({ inbox }) => [inbox, false])), privateKey); } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 34a9826fb3..5d7419f934 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -625,9 +625,7 @@ export class ApRendererService { } @bindThis - public async attachLdSignature(activity: any, user: { id: MiUser['id']; host: null; }, key: PrivateKeyWithPem): Promise { - const keypair = await this.userKeypairService.getUserKeypair(user.id); - + public async attachLdSignature(activity: any, key: PrivateKeyWithPem): Promise { const jsonLd = this.jsonLdService.use(); jsonLd.debug = false; activity = await jsonLd.signRsaSignature2017(activity, key.privateKeyPem, key.keyId); From aed28060e7d596261df723a419fdc1d49c903455 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:42:47 +0900 Subject: [PATCH 133/635] getApId, InboxProcessorService --- packages/backend/src/core/activitypub/type.ts | 2 +- packages/backend/src/queue/processors/InboxProcessorService.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 368650a83d..1d55971660 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -55,7 +55,7 @@ export function getOneApId(value: ApObject): string { export function getApId(value: string | IObject): string { if (typeof value === 'string') return value; if (typeof value.id === 'string') return value.id; - throw new Error('cannot detemine id'); + throw new Error('cannot determine id'); } /** diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 183399c14c..935c623df1 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -69,7 +69,7 @@ export class InboxProcessorService { this.logger.debug(JSON.stringify(info, null, 2)); //#endregion - const host = this.utilityService.toPuny(new URL(activity.actor).hostname); + const host = this.utilityService.toPuny(new URL(actorUri).hostname); // ブロックしてたら中断 const meta = await this.metaService.fetch(); From 8c76c7b8b5ce740cb9cd37c57f55ce77602d2259 Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 00:43:31 +0900 Subject: [PATCH 134/635] TODO --- packages/backend/src/queue/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/backend/src/queue/types.ts b/packages/backend/src/queue/types.ts index a2566d0cde..f2466f2e3d 100644 --- a/packages/backend/src/queue/types.ts +++ b/packages/backend/src/queue/types.ts @@ -13,6 +13,7 @@ import type { ParsedSignature, PrivateKeyWithPem } from '@misskey-dev/node-http- /** * @peertube/http-signature 時代の古いデータにも対応しておく + * TODO: 2026年ぐらいには消す */ export interface OldParsedSignature { scheme: 'Signature'; From 61d904e8f1ec1196f84a3f791725563391ecce9f Mon Sep 17 00:00:00 2001 From: tamaina Date: Thu, 18 Jul 2024 01:08:14 +0900 Subject: [PATCH 135/635] [skip ci] add CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f739f546..19e3cbae15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Fix: Dockerコンテナの立ち上げ時に`pnpm`のインストールで固まることがある問題 - Fix: デフォルトテーマに無効なテーマコードを入力するとUIが使用できなくなる問題を修正 - Enhance: Allow negative delay for MFM animation elements (`tada`, `jelly`, `twitch`, `shake`, `spin`, `jump`, `bounce`, `rainbow`) +- Feat: 連合に使うHTTP SignaturesがEd25519鍵に対応するように #13464 + - Ed25519署名に対応するサーバーが増えると、deliverで要求されるサーバーリソースが削減されます ### Client - Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善 From 3331f3972a02f8b662a3bf3e5a94f7b0a45ebbb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 18 Jul 2024 01:22:24 +0900 Subject: [PATCH 136/635] =?UTF-8?q?fix(frontend):=20=E3=80=8C=E3=82=A2?= =?UTF-8?q?=E3=83=8B=E3=83=A1=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E7=94=BB?= =?UTF-8?q?=E5=83=8F=E3=82=92=E5=86=8D=E7=94=9F=E3=81=97=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=80=8D=E3=81=8C=E3=82=AA=E3=83=B3=E3=81=AE=E3=81=A8=E3=81=8D?= =?UTF-8?q?=E3=81=AB=E3=83=90=E3=83=8A=E3=83=BC=E7=94=BB=E5=83=8F=E3=83=BB?= =?UTF-8?q?=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC=E8=83=8C=E6=99=AF=E7=94=BB?= =?UTF-8?q?=E5=83=8F=E3=81=8C=E3=82=A2=E3=83=8B=E3=83=A1=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=A7=E3=83=B3=E3=81=97=E3=81=AA=E3=81=84=E3=82=88=E3=81=86?= =?UTF-8?q?=E3=81=AB=20(#14243)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: stop animating banner and backgrounds when stop showing animated images is enabled (cherry picked from commit 8fe2596316e9688509745706ea424f0b4bfd4136) * chore: nest ternary (cherry picked from commit 2783fe5f5bd7c0647db9f9b6fb5e000e4f411092) * chore: flip ternary (cherry picked from commit b9d66f824cff373cc53bfa846a56c16f456a6d5b) * update changelog --------- Co-authored-by: Marie --- CHANGELOG.md | 2 ++ packages/frontend/src/components/MkUserInfo.vue | 4 +++- packages/frontend/src/components/MkUserPopup.vue | 3 ++- packages/frontend/src/pages/user/home.vue | 11 +++++++++-- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02497883f0..9eea70d3b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ - Fix: ショートカットキーが連打できる問題を修正 (Cherry-picked from https://github.com/taiyme/misskey/pull/234) - Fix: MkSignin.vueのcredentialRequestからReactivityを削除(ProxyがPasskey認証処理に渡ることを避けるため) +- Fix: 「アニメーション画像を再生しない」がオンのときでもサーバーのバナー画像・背景画像がアニメーションしてしまう問題を修正 + (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/574) ### Server - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue index d6f1ae453c..f0b9606590 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue index 5c3c6aa51d..16cf5b1b75 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -36,7 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only
@@ -67,11 +72,16 @@ type Input = { maxLength?: number; }; +type SelectItem = { + value: any; + text: string; +}; + type Select = { - items: { - value: any; - text: string; - }[]; + items: (SelectItem | { + sectionTitle: string; + items: SelectItem[]; + })[]; default: string | null; }; diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index 3085f33e21..a8dd99c854 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -447,15 +447,20 @@ export function authenticateDialog(): Promise<{ }); } +type SelectItem = { + value: C; + text: string; +}; + // default が指定されていたら result は null になり得ないことを保証する overload function export function select(props: { title?: string; text?: string; default: string; - items: { - value: C; - text: string; - }[]; + items: (SelectItem | { + sectionTitle: string; + items: SelectItem[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -465,10 +470,10 @@ export function select(props: { title?: string; text?: string; default?: string | null; - items: { - value: C; - text: string; - }[]; + items: (SelectItem | { + sectionTitle: string; + items: SelectItem[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -478,10 +483,10 @@ export function select(props: { title?: string; text?: string; default?: string | null; - items: { - value: C; - text: string; - }[]; + items: (SelectItem | { + sectionTitle: string; + items: SelectItem[]; + } | undefined)[]; }): Promise<{ canceled: true; result: undefined; } | { @@ -492,7 +497,7 @@ export function select(props: { title: props.title, text: props.text, select: { - items: props.items, + items: props.items.filter(x => x !== undefined), default: props.default ?? null, }, }, { diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue index 2d026d2fa9..2b8518747f 100644 --- a/packages/frontend/src/pages/my-antennas/create.vue +++ b/packages/frontend/src/pages/my-antennas/create.vue @@ -4,43 +4,33 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue index 9471be8575..9f927cd1a0 100644 --- a/packages/frontend/src/pages/my-antennas/edit.vue +++ b/packages/frontend/src/pages/my-antennas/edit.vue @@ -4,15 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only --> diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts index 4e39a0fa06..9794a300da 100644 --- a/packages/frontend/src/scripts/merge.ts +++ b/packages/frontend/src/scripts/merge.ts @@ -6,7 +6,7 @@ import { deepClone } from './clone.js'; import type { Cloneable } from './clone.js'; -type DeepPartial = { +export type DeepPartial = { [P in keyof T]?: T[P] extends Record ? DeepPartial : T[P]; }; diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue index bdb62dca15..af46b0641d 100644 --- a/packages/frontend/src/ui/deck.vue +++ b/packages/frontend/src/ui/deck.vue @@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only :ref="id" :key="id" :class="$style.column" - :column="columns.find(c => c.id === id)" + :column="columns.find(c => c.id === id)!" :isStacked="ids.length > 1" @headerWheel="onWheel" /> @@ -95,7 +95,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue'; import { v4 as uuid } from 'uuid'; import XCommon from './_common_/common.vue'; -import { deckStore, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; +import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js'; +import type { ColumnType } from './deck/deck-store.js'; import XSidebar from '@/ui/_common_/navbar.vue'; import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue'; import MkButton from '@/components/MkButton.vue'; @@ -152,10 +153,12 @@ window.addEventListener('resize', () => { const snapScroll = deviceKind === 'smartphone' || deviceKind === 'tablet'; const drawerMenuShowing = ref(false); +/* const route = 'TODO'; watch(route, () => { drawerMenuShowing.value = false; }); +*/ const columns = deckStore.reactiveState.columns; const layout = deckStore.reactiveState.layout; @@ -174,32 +177,20 @@ function showSettings() { const columnsEl = shallowRef(); const addColumn = async (ev) => { - const columns = [ - 'main', - 'widgets', - 'notifications', - 'tl', - 'antenna', - 'list', - 'channel', - 'mentions', - 'direct', - 'roleTimeline', - ]; - const { canceled, result: column } = await os.select({ title: i18n.ts._deck.addColumn, - items: columns.map(column => ({ + items: columnTypes.map(column => ({ value: column, text: i18n.ts._deck._columns[column], })), }); - if (canceled) return; + if (canceled || column == null) return; addColumnToStore({ type: column, id: uuid(), name: i18n.ts._deck._columns[column], width: 330, + soundSetting: { type: null, volume: 1 }, }); }; @@ -211,7 +202,7 @@ const onContextmenu = (ev) => { }; function onWheel(ev: WheelEvent) { - if (ev.deltaX === 0) { + if (ev.deltaX === 0 && columnsEl.value != null) { columnsEl.value.scrollLeft += ev.deltaY; } } @@ -242,7 +233,7 @@ function changeProfile(ev: MouseEvent) { title: i18n.ts._deck.profile, minLength: 1, }); - if (canceled) return; + if (canceled || name == null) return; deckStore.set('profile', name); unisonReload(); diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue index c3dc1e4fce..987bd4db55 100644 --- a/packages/frontend/src/ui/deck/antenna-column.vue +++ b/packages/frontend/src/ui/deck/antenna-column.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> + diff --git a/packages/frontend/src/pages/search.stories.impl.ts b/packages/frontend/src/pages/search.stories.impl.ts new file mode 100644 index 0000000000..0110a7ab8e --- /dev/null +++ b/packages/frontend/src/pages/search.stories.impl.ts @@ -0,0 +1,88 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import { HttpResponse, http } from 'msw'; +import search_ from './search.vue'; +import { userDetailed } from '@/../.storybook/fakes.js'; +import { commonHandlers } from '@/../.storybook/mocks.js'; + +const localUser = userDetailed('someuserid', 'miskist', null, 'Local Misskey User'); + +export const Default = { + render(args) { + return { + components: { + search_, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '', + }; + }, + args: { + ignoreNotesSearchAvailable: true, + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/users/show', () => { + return HttpResponse.json(userDetailed()); + }), + http.post('/api/users/search', () => { + return HttpResponse.json([userDetailed(), localUser]); + }), + ], + }, + }, +} satisfies StoryObj; + +export const NoteSearchDisabled = { + ...Default, + args: {}, +} satisfies StoryObj; + +export const WithUsernameLocal = { + ...Default, + + args: { + ...Default.args, + username: localUser.username, + host: localUser.host, + }, + parameters: { + layout: 'fullscreen', + msw: { + handlers: [ + ...commonHandlers, + http.post('/api/users/show', () => { + return HttpResponse.json(localUser); + }), + http.post('/api/users/search', () => { + return HttpResponse.json([userDetailed(), localUser]); + }), + ], + }, + }, +} satisfies StoryObj; + +export const WithUserType = { + ...Default, + args: { + type: 'user', + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue index b9c2704bc7..85d869d9cb 100644 --- a/packages/frontend/src/pages/search.user.vue +++ b/packages/frontend/src/pages/search.user.vue @@ -25,7 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index d0a6bba47b..853c1d6b0b 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only -
- +
+
@@ -239,6 +239,7 @@ async function del() { .footer { position: sticky; + z-index: 10000; bottom: 0; left: 0; padding: 12px; From 3137c104f23ac966340a66f7452f3a903d134633 Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Wed, 31 Jul 2024 07:23:38 +0900 Subject: [PATCH 214/635] =?UTF-8?q?test:=20=E3=83=95=E3=82=A9=E3=83=AD?= =?UTF-8?q?=E3=83=BC=E3=81=97=E3=81=A6=E3=81=84=E3=81=AA=E3=81=84=E3=83=A6?= =?UTF-8?q?=E3=83=BC=E3=82=B6=E3=83=BC=E3=81=8B=E3=82=89=E3=81=AE=E8=87=AA?= =?UTF-8?q?=E5=88=86=E3=81=B8=E3=81=AE=E8=BF=94=E4=BF=A1=E3=81=8C=E5=90=AB?= =?UTF-8?q?=E3=81=BE=E3=82=8C=E3=82=8B=E3=81=93=E3=81=A8=E3=82=92=E7=A2=BA?= =?UTF-8?q?=E8=AA=8D=E3=81=99=E3=82=8B=E3=83=86=E3=82=B9=E3=83=88=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=20(#14333)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/backend/test/e2e/timelines.ts | 30 ++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/backend/test/e2e/timelines.ts b/packages/backend/test/e2e/timelines.ts index b7069dd82c..d12be2a9ac 100644 --- a/packages/backend/test/e2e/timelines.ts +++ b/packages/backend/test/e2e/timelines.ts @@ -703,6 +703,21 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); + test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/local-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); + test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); @@ -899,6 +914,21 @@ describe('Timelines', () => { assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); }); + test.concurrent('withReplies: false でフォローしていないユーザーからの自分への返信が含まれる', async () => { + const [alice, bob] = await Promise.all([signup(), signup()]); + + await setTimeout(1000); + const aliceNote = await post(alice, { text: 'hi' }); + const bobNote = await post(bob, { text: 'hi', replyId: aliceNote.id }); + + await waitForPushToTl(); + + const res = await api('notes/local-timeline', { limit: 100 }, alice); + + assert.strictEqual(res.body.some(note => note.id === aliceNote.id), true); + assert.strictEqual(res.body.some(note => note.id === bobNote.id), true); + }); + test.concurrent('[withReplies: true] 他人の他人への返信が含まれる', async () => { const [alice, bob, carol] = await Promise.all([signup(), signup(), signup()]); From 9dacc20d67777185c729dd71899885db72874692 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 31 Jul 2024 07:23:58 +0900 Subject: [PATCH 215/635] New Crowdin updates (#14331) * New translations ja-jp.yml (Chinese Simplified) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Chinese Simplified) * New translations ja-jp.yml (English) --- locales/en-US.yml | 10 ++++++++-- locales/zh-CN.yml | 10 ++++++++-- locales/zh-TW.yml | 6 ++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/locales/en-US.yml b/locales/en-US.yml index cfb5783a95..451300d973 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -167,7 +167,7 @@ emojiUrl: "Emoji URL" addEmoji: "Add an emoji" settingGuide: "Recommended settings" cacheRemoteFiles: "Cache remote files" -cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote instance. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." +cacheRemoteFilesDescription: "When this setting is disabled, remote files are loaded directly from the remote servers. Disabling this will decrease storage usage, but increase traffic, as thumbnails will not be generated." youCanCleanRemoteFilesCache: "You can clear the cache by clicking the 🗑️ button in the file management view." cacheRemoteSensitiveFiles: "Cache sensitive remote files" cacheRemoteSensitiveFilesDescription: "When this setting is disabled, sensitive remote files are loaded directly from the remote instance without caching." @@ -212,6 +212,7 @@ perDay: "Per Day" stopActivityDelivery: "Stop sending activities" blockThisInstance: "Block this instance" silenceThisInstance: "Silence this instance" +mediaSilenceThisInstance: "Media-silence this server" operations: "Operations" software: "Software" version: "Version" @@ -232,7 +233,9 @@ clearCachedFilesConfirm: "Are you sure that you want to delete all cached remote blockedInstances: "Blocked Instances" blockedInstancesDescription: "List the hostnames of the instances you want to block separated by linebreaks. Listed instances will no longer be able to communicate with this instance." silencedInstances: "Silenced instances" -silencedInstancesDescription: "List the hostnames of the instances that you want to silence. All accounts of the listed instances will be treated as silenced, can only make follow requests, and cannot mention local accounts if not followed. This will not affect blocked instances." +silencedInstancesDescription: "List the host names of the servers that you want to silence, separated by a new line. All accounts belonging to the listed servers will be treated as silenced, and can only make follow requests, and cannot mention local accounts if not followed. This will not affect the blocked servers." +mediaSilencedInstances: "Media-silenced servers" +mediaSilencedInstancesDescription: "List the host names of the servers that you want to media-silence, separated by a new line. All accounts belonging to the listed servers will be treated as sensitive, and can't use custom emojis. This will not affect the blocked servers." muteAndBlock: "Mutes and Blocks" mutedUsers: "Muted users" blockedUsers: "Blocked users" @@ -1121,6 +1124,8 @@ preventAiLearning: "Reject usage in Machine Learning (Generative AI)" preventAiLearningDescription: "Requests crawlers to not use posted text or image material etc. in machine learning (Predictive / Generative AI) data sets. This is achieved by adding a \"noai\" HTML-Response flag to the respective content. A complete prevention can however not be achieved through this flag, as it may simply be ignored." options: "Options" specifyUser: "Specific user" +lookupConfirm: "Do you want to look up?" +openTagPageConfirm: "Do you want to open a hashtag page?" specifyHost: "Specify a host" failedToPreviewUrl: "Could not preview" update: "Update" @@ -1962,6 +1967,7 @@ _soundSettings: driveFileTypeWarnDescription: "Select an audio file" driveFileDurationWarn: "The audio is too long." driveFileDurationWarnDescription: "Long audio may disrupt using Misskey. Still continue?" + driveFileError: "It couldn't load the sound. Please change the setting." _ago: future: "Future" justNow: "Just now" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 8eae32ca32..0a868aab44 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -212,6 +212,7 @@ perDay: "每天" stopActivityDelivery: "停止发送活动" blockThisInstance: "阻止此服务器向本服务器推流" silenceThisInstance: "使服务器静音" +mediaSilenceThisInstance: "隐藏此服务器的媒体文件" operations: "操作" software: "软件" version: "版本" @@ -230,9 +231,11 @@ clearQueueConfirmText: "未送达的帖子将不会被投递。 通常无需执 clearCachedFiles: "清除缓存" clearCachedFilesConfirm: "确定要清除所有缓存的远程文件?" blockedInstances: "被封锁的服务器" -blockedInstancesDescription: "设定要封锁的服务器,以换行来进行分割。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。" +blockedInstancesDescription: "设定要封锁的服务器,以换行分隔。被封锁的服务器将无法与本服务器进行交换通讯。子域名也同样会被封锁。" silencedInstances: "被静音的服务器" -silencedInstancesDescription: "设置要静音的服务器,以换行符分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。" +silencedInstancesDescription: "设置要静音的服务器,以换行分隔。被静音的服务器内所有的账户将默认处于「静音」状态,仅能发送关注请求,并且在未关注状态下无法提及本地账户。被阻止的实例不受影响。" +mediaSilencedInstances: "已隐藏媒体文件的服务器" +mediaSilencedInstancesDescription: "设置要隐藏媒体文件的服务器,以换行分隔。被设置为隐藏媒体文件服务器内所有账号的文件均按照「敏感内容」处理,且将无法使用自定义表情符号。被阻止的实例不受影响。" muteAndBlock: "静音/拉黑" mutedUsers: "已静音用户" blockedUsers: "已拉黑的用户" @@ -1121,6 +1124,8 @@ preventAiLearning: "拒绝接受生成式 AI 的学习" preventAiLearningDescription: "要求文章生成 AI 或图像生成 AI 不能够以发布的帖子和图像等内容作为学习对象。这是通过在 HTML 响应中包含 noai 标志来实现的,这不能完全阻止 AI 学习你的发布内容,并不是所有 AI 都会遵守这类请求。" options: "选项" specifyUser: "用户指定" +lookupConfirm: "确定查询?" +openTagPageConfirm: "确定打开话题标签页面?" specifyHost: "指定主机名" failedToPreviewUrl: "无法预览" update: "更新" @@ -1961,6 +1966,7 @@ _soundSettings: driveFileTypeWarnDescription: "请选择音频文件" driveFileDurationWarn: "音频过长" driveFileDurationWarnDescription: "使用长音频可能会影响 Misskey 的使用。即使这样也要继续吗?" + driveFileError: "无法读取声音。请更改设置。" _ago: future: "未来" justNow: "最近" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 8b53273c3b..476ccd8799 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -212,6 +212,7 @@ perDay: "每日" stopActivityDelivery: "停止發送活動" blockThisInstance: "封鎖此伺服器" silenceThisInstance: "禁言此伺服器" +mediaSilenceThisInstance: "將這個伺服器的媒體設為禁言" operations: "操作" software: "軟體" version: "版本" @@ -233,6 +234,8 @@ blockedInstances: "已封鎖的伺服器" blockedInstancesDescription: "請逐行輸入需要封鎖的伺服器。已封鎖的伺服器將無法與本伺服器進行通訊。" silencedInstances: "被禁言的伺服器" silencedInstancesDescription: "設定要禁言的伺服器主機名稱,以換行分隔。隸屬於禁言伺服器的所有帳戶都將被視為「禁言帳戶」,只能發出「追隨請求」,而且無法提及未追隨的本地帳戶。這不會影響已封鎖的實例。" +mediaSilencedInstances: "媒體被禁言的伺服器" +mediaSilencedInstancesDescription: "設定您想要對媒體設定禁言的伺服器,以換行符號區隔。來自被媒體禁言的伺服器所屬帳戶的所有檔案都會被視為敏感檔案,且自訂表情符號不能使用。被封鎖的伺服器不受影響。" muteAndBlock: "靜音和封鎖" mutedUsers: "被靜音的使用者" blockedUsers: "被封鎖的使用者" @@ -1121,6 +1124,8 @@ preventAiLearning: "拒絕接受生成式AI的訓練" preventAiLearningDescription: "要求站外生成式 AI 不使用您發佈的內容訓練模型。此功能會使伺服器於 HTML 回應新增「noai」標籤,而因為要視乎 AI 會否遵守該標籤,所以此功能無法完全阻止所有 AI 使用您的內容。" options: "選項" specifyUser: "指定使用者" +lookupConfirm: "要查詢嗎?" +openTagPageConfirm: "要開啟標籤的頁面嗎?" specifyHost: "指定主機" failedToPreviewUrl: "無法預覽" update: "更新" @@ -1962,6 +1967,7 @@ _soundSettings: driveFileTypeWarnDescription: "請選擇音效檔案" driveFileDurationWarn: "音效太長了" driveFileDurationWarnDescription: "使用長音效檔可能會影響 Misskey 的使用體驗。仍要使用此檔案嗎?" + driveFileError: "無法載入語音。請更改設定" _ago: future: "未來" justNow: "剛剛" From d63b854f96d9437f9764f9170c3ed3537cc98a2c Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:12:35 +0900 Subject: [PATCH 216/635] tweak localization --- locales/ja-JP.yml | 2 +- packages/frontend/src/components/MkSystemWebhookEditor.vue | 2 +- packages/frontend/src/pages/settings/webhook.edit.vue | 2 +- packages/frontend/src/pages/settings/webhook.new.vue | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 522ad7e22a..b493183974 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -2492,7 +2492,7 @@ _webhookSettings: modifyWebhook: "Webhookを編集" name: "名前" secret: "シークレット" - events: "Webhookを実行するタイミング" + trigger: "トリガー" active: "有効" _events: follow: "フォローしたとき" diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue index cad2e99e4d..f5c7a3160b 100644 --- a/packages/frontend/src/components/MkSystemWebhookEditor.vue +++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue @@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue index e9fb1e471e..058ef69c35 100644 --- a/packages/frontend/src/pages/settings/webhook.edit.vue +++ b/packages/frontend/src/pages/settings/webhook.edit.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
{{ i18n.ts._webhookSettings._events.follow }} diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue index 5bf85e48f4..d62357caaf 100644 --- a/packages/frontend/src/pages/settings/webhook.new.vue +++ b/packages/frontend/src/pages/settings/webhook.new.vue @@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
{{ i18n.ts._webhookSettings._events.follow }} From 4b04b2989b3c8957cee292326846b3e019b4a1c6 Mon Sep 17 00:00:00 2001 From: taichan <40626578+tai-cha@users.noreply.github.com> Date: Wed, 31 Jul 2024 17:22:51 +0900 Subject: [PATCH 217/635] chore(locale): update index.d.ts (#14339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/misskey-dev/misskey/commit/d63b854f96d9437f9764f9170c3ed3537cc98a2c での更新漏れ --- locales/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/locales/index.d.ts b/locales/index.d.ts index c58e4ace7b..91d36a14a6 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -9402,9 +9402,9 @@ export interface Locale extends ILocale { */ "secret": string; /** - * Webhookを実行するタイミング + * トリガー */ - "events": string; + "trigger": string; /** * 有効 */ From d6ba12e24c78611d80a005efef4340681949931b Mon Sep 17 00:00:00 2001 From: taichan <40626578+tai-cha@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:10:36 +0900 Subject: [PATCH 218/635] =?UTF-8?q?Fix(frontend):=20LTL=E7=84=A1=E5=8A=B9?= =?UTF-8?q?=E6=99=82=E3=81=AB=E3=83=98=E3=83=83=E3=83=80=E3=83=BC=E3=81=AB?= =?UTF-8?q?STL=E3=81=8C=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=A6?= =?UTF-8?q?=E3=81=97=E3=81=BE=E3=81=86=20&=20=E3=83=87=E3=83=95=E3=82=A9?= =?UTF-8?q?=E3=83=AB=E3=83=88=E3=80=81=E3=82=AF=E3=83=A9=E3=82=B7=E3=83=83?= =?UTF-8?q?=E3=82=AF=E3=81=A7=E3=83=AA=E3=82=B9=E3=83=88=E3=81=8C=E6=B6=88?= =?UTF-8?q?=E3=81=88=E3=81=A6=E3=81=84=E3=82=8B=20(#14337)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix condition of STL available * Fix: condition of stl * Listがタイムラインのヘッダーから消えている --- packages/frontend/src/pages/timeline.vue | 5 +++++ packages/frontend/src/timelines.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue index c5905c7ada..32f6dd0e5a 100644 --- a/packages/frontend/src/pages/timeline.vue +++ b/packages/frontend/src/pages/timeline.vue @@ -287,6 +287,11 @@ const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserList icon: basicTimelineIconClass(tl), iconOnly: true, })), { + icon: 'ti ti-list', + title: i18n.ts.lists, + iconOnly: true, + onClick: chooseList, +}, { icon: 'ti ti-antenna', title: i18n.ts.antennas, iconOnly: true, diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts index 3ef95fd272..94eda3545e 100644 --- a/packages/frontend/src/timelines.ts +++ b/packages/frontend/src/timelines.ts @@ -39,7 +39,7 @@ export function isAvailableBasicTimeline(timeline: BasicTimelineType | undefined case 'local': return ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); case 'social': - return $i != null && instance.policies.ltlAvailable; + return $i != null && $i.policies.ltlAvailable; case 'global': return ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); default: From 1a521a44c0c0e3f5a8156a2cf4b2f8001ff3d004 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:13:20 +0900 Subject: [PATCH 219/635] New Crowdin updates (#14335) * New translations ja-jp.yml (Japanese, Kansai) * New translations ja-jp.yml (Chinese Traditional) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Chinese Simplified) * New translations ja-jp.yml (Vietnamese) * New translations ja-jp.yml (Spanish) * New translations ja-jp.yml (Czech) * New translations ja-jp.yml (German) * New translations ja-jp.yml (Italian) * New translations ja-jp.yml (Korean) * New translations ja-jp.yml (Polish) * New translations ja-jp.yml (Indonesian) * New translations ja-jp.yml (Thai) * New translations ja-jp.yml (English) * New translations ja-jp.yml (English) * New translations ja-jp.yml (Chinese Simplified) * New translations ja-jp.yml (Chinese Traditional) --- locales/cs-CZ.yml | 1 - locales/de-DE.yml | 1 - locales/en-US.yml | 4 ++-- locales/es-ES.yml | 1 - locales/id-ID.yml | 1 - locales/it-IT.yml | 1 - locales/ja-KS.yml | 1 - locales/ko-KR.yml | 1 - locales/pl-PL.yml | 1 - locales/th-TH.yml | 22 ++++++++++++++++++++-- locales/vi-VN.yml | 1 - locales/zh-CN.yml | 2 +- locales/zh-TW.yml | 2 +- 13 files changed, 24 insertions(+), 15 deletions(-) diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index aa970f823a..7db7424762 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -2010,7 +2010,6 @@ _webhookSettings: createWebhook: "Vytvořit Webhook" name: "Jméno" secret: "Tajné" - events: "Události Webhook" active: "Zapnuto" _events: follow: "Při sledování uživatele" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index a319af56cb..8e44a3bbd4 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -2191,7 +2191,6 @@ _webhookSettings: createWebhook: "Webhook erstellen" name: "Name" secret: "Secret" - events: "Webhook-Ereignisse" active: "Aktiviert" _events: follow: "Wenn du jemandem folgst" diff --git a/locales/en-US.yml b/locales/en-US.yml index 451300d973..2cb76fa746 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -398,7 +398,7 @@ mcaptcha: "mCaptcha" enableMcaptcha: "Enable mCaptcha" mcaptchaSiteKey: "Site key" mcaptchaSecretKey: "Secret key" -mcaptchaInstanceUrl: "mCaptcha instance URL" +mcaptchaInstanceUrl: "mCaptcha server URL" recaptcha: "reCAPTCHA" enableRecaptcha: "Enable reCAPTCHA" recaptchaSiteKey: "Site key" @@ -2426,7 +2426,7 @@ _webhookSettings: modifyWebhook: "Modify Webhook" name: "Name" secret: "Secret" - events: "Webhook Events" + trigger: "Trigger" active: "Enabled" _events: follow: "When following a user" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index 3ae3ba3b8a..ef066a37ed 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -2382,7 +2382,6 @@ _webhookSettings: createWebhook: "Crear Webhook" name: "Nombre" secret: "Secreto" - events: "Eventos de webhook" active: "Activado" _events: follow: "Cuando se sigue a alguien" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index de50569e89..24f7482fca 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -2403,7 +2403,6 @@ _webhookSettings: modifyWebhook: "Sunting Webhook" name: "Nama" secret: "Secret" - events: "Webhook Events" active: "Aktif" _events: follow: "Ketika mengikuti pengguna" diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 64cd26878e..2b4b1e425e 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -2412,7 +2412,6 @@ _webhookSettings: modifyWebhook: "Modifica Webhook" name: "Nome" secret: "Segreto" - events: "Quando eseguire il Webhook" active: "Attivo" _events: follow: "Quando segui un profilo" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 774031f6f5..5969082cf2 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -2391,7 +2391,6 @@ _webhookSettings: createWebhook: "Webhookをつくる" name: "名前" secret: "シークレット" - events: "Webhookを投げるタイミング" active: "有効" _events: follow: "フォローしたとき~!" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 7fcb681c9a..34c1cc3ebf 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -2411,7 +2411,6 @@ _webhookSettings: modifyWebhook: "Webhook 수정" name: "이름" secret: "시크릿" - events: "Webhook을 실행할 타이밍" active: "활성화" _events: follow: "누군가를 팔로우했을 때" diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 7513d056b4..73eff0941a 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1544,7 +1544,6 @@ _webhookSettings: createWebhook: "Stwórz Webhook" name: "Nazwa" secret: "Sekret" - events: "Uruchomienie Webhooka" active: "Właczono" _events: follow: "Po zaobserwowaniu użytkownika" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index eb7cdc2365..63f2793428 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -60,6 +60,7 @@ copyFileId: "คัดลอกไฟล์ ID" copyFolderId: "คัดลอกโฟลเดอร์ ID" copyProfileUrl: "คัดลอกโปรไฟล์ URL" searchUser: "ค้นหาผู้ใช้" +searchThisUsersNotes: "ค้นหาโน้ตของผู้ใช้" reply: "ตอบกลับ" loadMore: "แสดงเพิ่มเติม" showMore: "แสดงเพิ่มเติม" @@ -154,6 +155,7 @@ editList: "แก้ไขรายชื่อ" selectChannel: "เลือกช่อง" selectAntenna: "เลือกเสาอากาศ" editAntenna: "แก้ไขเสาอากาศ" +createAntenna: "สร้างเสาอากาศ" selectWidget: "เลือกวิดเจ็ต" editWidgets: "แก้ไขวิดเจ็ต" editWidgetsExit: "เรียบร้อย" @@ -194,6 +196,7 @@ followConfirm: "ต้องการติดตาม {name} ใช่ไห proxyAccount: "บัญชีพร็อกซี่" proxyAccountDescription: "บัญชีพร็อกซี คือ บัญชีที่ทำหน้าที่ติดตาม(ผู้ใช้)ระยะไกลภายใต้เงื่อนไขบางประการ ตัวอย่างเช่น เมื่อผู้ใช้ท้องถิ่นเพิ่มผู้ใช้ระยะไกลลงรายชื่อ หากไม่มีใครติดตามผู้ใช้ระยะไกลในรายชื่อนั้น กิจกรรมก็จะไม่ถูกส่งมายังเซิร์ฟเวอร์ ดังนั้นจึงมีบัญชีพร็อกซีไว้ติดตามผู้ใช้ระยะไกลเหล่านั้น" host: "โฮสต์" +selectSelf: "เลือกตัวเอง" selectUser: "เลือกผู้ใช้งาน" recipient: "ผู้รับ" annotation: "หมายเหตุประกอบ" @@ -209,6 +212,7 @@ perDay: "ต่อวัน" stopActivityDelivery: "หยุดส่งกิจกรรม" blockThisInstance: "บล็อกเซิร์ฟเวอร์นี้" silenceThisInstance: "ปิดปากเซิร์ฟเวอร์นี้" +mediaSilenceThisInstance: "ปิดปากสื่อของเซิร์ฟเวอร์นี้" operations: "ดำเนินการ" software: "ซอฟต์แวร์" version: "เวอร์ชั่น" @@ -230,6 +234,8 @@ blockedInstances: "เซิร์ฟเวอร์ที่ถูกบล็ blockedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการบล็อก คั่นด้วยการขึ้นบรรทัดใหม่ เซิร์ฟเวอร์ที่ถูกบล็อกจะไม่สามารถติดต่อกับอินสแตนซ์นี้ได้" silencedInstances: "ปิดปากเซิร์ฟเวอร์นี้แล้ว" silencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปาก คั่นด้วยการขึ้นบรรทัดใหม่, บัญชีทั้งหมดของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปากเช่นกัน ทำได้เฉพาะคำขอติดตามเท่านั้น และไม่สามารถกล่าวถึงบัญชีในเซิร์ฟเวอร์นี้ได้หากไม่ได้ถูกติดตามกลับ | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" +mediaSilencedInstances: "เซิร์ฟเวอร์ที่ถูกปิดปากสื่อ" +mediaSilencedInstancesDescription: "ระบุโฮสต์ของเซิร์ฟเวอร์ที่ต้องการปิดปากสื่อ คั่นด้วยการขึ้นบรรทัดใหม่, ไฟล์ที่ถูกส่งจากบัญชีของเซิร์ฟเวอร์ดังกล่าวจะถือว่าถูกปิดปาก แล้วจะถูกติดเครื่องหมายว่ามีเนื้อหาละเอียดอ่อน และเอโมจิแบบกำหนดเองก็จะใช้ไม่ได้ด้วย | สิ่งนี้ไม่มีผลต่ออินสแตนซ์ที่ถูกบล็อก" muteAndBlock: "ปิดเสียงและบล็อก" mutedUsers: "ผู้ใช้ที่ถูกปิดเสียง" blockedUsers: "ผู้ใช้ที่ถูกบล็อก" @@ -1106,6 +1112,8 @@ preservedUsernames: "ชื่อผู้ใช้ที่สงวนไว preservedUsernamesDescription: "ระบุชื่อผู้ใช้ที่จะสงวนชื่อไว้ คั่นด้วยการขึ้นบรรทัดใหม่ ชื่อผู้ใช้ที่ระบุที่นี่จะไม่สามารถใช้งานได้อีกต่อไปเมื่อสร้างบัญชีใหม่ ยกเว้นเมื่อผู้ดูแลระบบสร้างบัญชี นอกจากนี้ บัญชีที่มีอยู่แล้วจะไม่ได้รับผลกระทบ" createNoteFromTheFile: "เรียบเรียงโน้ตจากไฟล์นี้" archive: "เก็บถาวร" +archived: "เก็บถาวรแล้ว" +unarchive: "เลิกการเก็บถาวร" channelArchiveConfirmTitle: "ต้องการเก็บถาวรเจ้า {name} ใช่ไหม?" channelArchiveConfirmDescription: "เมื่อเก็บถาวรแล้ว จะไม่ปรากฏในรายการช่องหรือผลการค้นหาอีกต่อไป และจะไม่สามารถโพสต์ใหม่ได้อีกต่อไป" thisChannelArchived: "ช่องนี้ถูกเก็บถาวรแล้วนะ" @@ -1116,6 +1124,9 @@ preventAiLearning: "ปฏิเสธการเรียนรู้ด้ว preventAiLearningDescription: "ส่งคำร้องขอไม่ให้ใช้ ข้อความในโน้ตที่โพสต์, หรือเนื้อหารูปภาพ ฯลฯ ในการเรียนรู้ของเครื่อง(machine learning) / Predictive AI / Generative AI โดยการเพิ่มแฟล็ก “noai” ลง HTML-Response ให้กับเนื้อหาที่เกี่ยวข้อง แต่ทั้งนี้ ไม่ได้ป้องกัน AI จากการเรียนรู้ได้อย่างสมบูรณ์ เนื่องจากมี AI บางตัวเท่านั้นที่จะเคารพคำขอดังกล่าว" options: "ตัวเลือกบทบาท" specifyUser: "ผู้ใช้เฉพาะ" +lookupConfirm: "ต้องการเรียกดูข้อมูลใช่ไหม?" +openTagPageConfirm: "ต้องการเปิดหน้าแฮชแท็กใช่ไหม?" +specifyHost: "ระบุโฮสต์" failedToPreviewUrl: "ไม่สามารถดูตัวอย่างได้" update: "อัปเดต" rolesThatCanBeUsedThisEmojiAsReaction: "บทบาทที่สามารถใช้เอโมจินี้เป็นรีแอคชั่นได้" @@ -1250,6 +1261,8 @@ inquiry: "ติดต่อเรา" tryAgain: "โปรดลองอีกครั้ง" confirmWhenRevealingSensitiveMedia: "ตรวจสอบก่อนแสดงสื่อที่มีเนื้อหาละเอียดอ่อน" sensitiveMediaRevealConfirm: "สื่อนี้มีเนื้อหาละเอียดอ่อน, ต้องการแสดงใช่ไหม?" +createdLists: "รายชื่อที่ถูกสร้าง" +createdAntennas: "เสาอากาศที่ถูกสร้าง" _delivery: status: "สถานะการจัดส่ง" stop: "ระงับการส่ง" @@ -1954,6 +1967,7 @@ _soundSettings: driveFileTypeWarnDescription: "กรุณาเลือกไฟล์เสียง" driveFileDurationWarn: "เสียงยาวเกินไป" driveFileDurationWarnDescription: "การใช้เสียงที่ยาว อาจรบกวนการใช้งาน Misskey, ต้องการดำเนินการต่อใช่ไหม?" + driveFileError: "ไม่สามารถโหลดไฟล์เสียงได้ กรุณาเปลี่ยนแปลงการตั้งค่า" _ago: future: "อนาคต" justNow: "เมื่อกี๊นี้" @@ -2412,7 +2426,6 @@ _webhookSettings: modifyWebhook: "แก้ไข Webhook" name: "ชื่อ" secret: "ความลับ" - events: "อีเว้นท์ Webhook" active: "เปิดใช้งาน" _events: follow: "เมื่อกำลังติดตามผู้ใช้" @@ -2536,7 +2549,7 @@ _externalResourceInstaller: description: "เกิดปัญหาระหว่างการติดตั้งธีม กรุณาลองอีกครั้ง. รายละเอียดข้อผิดพลาดสามารถดูได้ในคอนโซล Javascript" _dataSaver: _media: - title: "โหลดมีเดีย" + title: "โหลดสื่อ" description: "กันไม่ให้ภาพและวิดีโอโหลดโดยอัตโนมัติ แตะรูปภาพ/วิดีโอที่ซ่อนอยู่เพื่อโหลด" _avatar: title: "รูปไอคอน" @@ -2616,3 +2629,8 @@ _mediaControls: pip: "รูปภาพในรูปภาม" playbackRate: "ความเร็วในการเล่น" loop: "เล่นวนซ้ำ" +_contextMenu: + title: "เมนูเนื้อหา" + app: "แอปพลิเคชัน" + appWithShift: "แอปฟลิเคชันด้วยปุ่มยกแคร่ (Shift)" + native: "UI ของเบราว์เซอร์" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 317dea5150..aadbf8b16f 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1918,7 +1918,6 @@ _webhookSettings: createWebhook: "Tạo Webhook" name: "Tên" secret: "Mã bí mật" - events: "Sự kiện Webhook" active: "Đã bật" _events: reaction: "Khi nhận được sự kiện" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 0a868aab44..1deb0effc3 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -2425,7 +2425,7 @@ _webhookSettings: modifyWebhook: "编辑 webhook" name: "名称" secret: "密钥" - events: "何时运行 Webhook" + trigger: "触发器" active: "已启用" _events: follow: "关注时" diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 476ccd8799..16dc464e35 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -2426,7 +2426,7 @@ _webhookSettings: modifyWebhook: "編輯 Webhook" name: "名字" secret: "密鑰" - events: "何時運行 Webhook" + trigger: "觸發器" active: "已啟用" _events: follow: "當你追隨時" From 59e2e43a68ee39d40f7a95a068cb9cb4f235cfed Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 Jul 2024 11:20:28 +0000 Subject: [PATCH 220/635] Release: 2024.7.0 --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 42751be196..4bf7b0a918 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.7.0-rc.8", + "version": "2024.7.0", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 45f42e1846..f0e8733e67 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.7.0-rc.8", + "version": "2024.7.0", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 008a66d73f58956b6a9c30f2d48cd554df865ac9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 31 Jul 2024 11:20:33 +0000 Subject: [PATCH 221/635] [skip ci] Update CHANGELOG.md (prepend template) --- CHANGELOG.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b996216ac1..86e33a8272 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +## Unreleased + +### General +- + +### Client +- + +### Server +- + + ## 2024.7.0 ### Note From 6e3e7d7df1efb7d74d905b8ebdf704573ff2f930 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:22:25 +0900 Subject: [PATCH 222/635] Update about-misskey.vue --- packages/frontend/src/pages/about-misskey.vue | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index 8db71c8881..9b5b4a5000 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -264,6 +264,9 @@ const patronsWithIcon = [{ }, { name: 'ささくれりょう', icon: 'https://assets.misskey-hub.net/patrons/cf55022cee6c41da8e70a43587aaad9a.jpg', +}, { + name: 'Macop', + icon: 'https://assets.misskey-hub.net/patrons/ee052bf550014d36a643ce3dce595640.jpg', }]; const patrons = [ From 820becb4e47ba88e181ad6a225a5a904ea821376 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Fri, 9 Aug 2024 10:51:18 +0900 Subject: [PATCH 223/635] fix import --- packages/backend/src/core/ModerationLogService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/backend/src/core/ModerationLogService.ts b/packages/backend/src/core/ModerationLogService.ts index 6c155c9a62..2c02af217d 100644 --- a/packages/backend/src/core/ModerationLogService.ts +++ b/packages/backend/src/core/ModerationLogService.ts @@ -9,7 +9,8 @@ import type { ModerationLogsRepository } from '@/models/_.js'; import type { MiUser } from '@/models/User.js'; import { IdService } from '@/core/IdService.js'; import { bindThis } from '@/decorators.js'; -import { ModerationLogPayloads, moderationLogTypes } from '@/types.js'; +import type { ModerationLogPayloads } from '@/types.js'; +import { moderationLogTypes } from '@/types.js'; @Injectable() export class ModerationLogService { From f244d425005ae662cfb94f2a18e38cedadfb3c0a Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 9 Aug 2024 12:05:28 +0900 Subject: [PATCH 224/635] ci: change prerelease channels to alpha, beta, and rc (#14376) --- .github/workflows/release-edit-with-push.yml | 2 +- .github/workflows/release-with-dispatch.yml | 14 +++++++++++--- .github/workflows/release-with-ready.yml | 2 ++ 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release-edit-with-push.yml b/.github/workflows/release-edit-with-push.yml index f86c1948f8..57657a4ba7 100644 --- a/.github/workflows/release-edit-with-push.yml +++ b/.github/workflows/release-edit-with-push.yml @@ -6,7 +6,7 @@ on: - develop paths: - 'CHANGELOG.md' - # - .github/workflows/release-edit-with-push.yml + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-with-dispatch.yml b/.github/workflows/release-with-dispatch.yml index 0936bc0ae8..ed2f822269 100644 --- a/.github/workflows/release-with-dispatch.yml +++ b/.github/workflows/release-with-dispatch.yml @@ -17,6 +17,10 @@ on: type: boolean description: 'MERGE RELEASE BRANCH TO MAIN' default: false + start-rc: + type: boolean + description: 'Start Release Candidate' + default: false env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -56,13 +60,13 @@ jobs: ### General - - + ### Client - - + ### Server - - + use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} indent: ${{ vars.INDENT }} secrets: @@ -79,6 +83,9 @@ jobs: package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} indent: ${{ vars.INDENT }} + draft_prerelease_channel: alpha + ready_start_prerelease_channel: beta + prerelease_channel: ${{ inputs.start-rc && 'rc' || '' }} secrets: RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} @@ -122,6 +129,7 @@ jobs: use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} indent: ${{ vars.INDENT }} stable_branch: ${{ vars.STABLE_BRANCH }} + draft_prerelease_channel: alpha secrets: RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} diff --git a/.github/workflows/release-with-ready.yml b/.github/workflows/release-with-ready.yml index 79b6ade012..e863b5e2e8 100644 --- a/.github/workflows/release-with-ready.yml +++ b/.github/workflows/release-with-ready.yml @@ -39,6 +39,8 @@ jobs: package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} indent: ${{ vars.INDENT }} + draft_prerelease_channel: alpha + ready_start_prerelease_channel: beta secrets: RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} From 0d508db8a7a36218d38231af4e718aff0e94d9bc Mon Sep 17 00:00:00 2001 From: Daiki Mizukami Date: Fri, 9 Aug 2024 12:10:51 +0900 Subject: [PATCH 225/635] fix(backend): check visibility of following/followers of remote users / feat: moderators can see following/followers of all users (#14375) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend): check visibility of following/followers of remote users Resolves https://github.com/misskey-dev/misskey/issues/13362. * test(backend): add tests for visibility of following/followers of remote users * docs(changelog): update CHANGELOG.md * feat: moderators can see following/followers of all users * docs(changelog): update CHANGELOG.md * refactor(backend): minor refactoring `createPerson`と`if`の条件を統一するとともに、異常系の 処理をearly returnに追い出すための変更。 * feat(backend): moderators can see following/followers count of all users As per https://github.com/misskey-dev/misskey/pull/14375#issuecomment-2275044908. --- CHANGELOG.md | 3 +- .../activitypub/models/ApPersonService.ts | 50 ++++++++++++++++- packages/backend/src/core/activitypub/type.ts | 6 ++- .../src/core/entities/UserEntityService.ts | 4 +- .../server/api/endpoints/users/followers.ts | 34 ++++++------ .../server/api/endpoints/users/following.ts | 34 ++++++------ packages/backend/test/unit/activitypub.ts | 53 ++++++++++++++++++- .../frontend/src/scripts/isFfVisibleForMe.ts | 4 +- 8 files changed, 149 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e33a8272..c18cccc44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ ## Unreleased ### General -- +- Fix: リモートユーザのフォロー・フォロワーの一覧が非公開設定の場合も表示できてしまう問題を修正 +- Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように ### Client - diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index 457205e023..f3ddf3952c 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -48,7 +48,7 @@ import type { ApResolverService, Resolver } from '../ApResolverService.js'; import type { ApLoggerService } from '../ApLoggerService.js'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports import type { ApImageService } from './ApImageService.js'; -import type { IActor, IObject } from '../type.js'; +import type { IActor, ICollection, IObject, IOrderedCollection } from '../type.js'; const nameLength = 128; const summaryLength = 2048; @@ -296,6 +296,21 @@ export class ApPersonService implements OnModuleInit { const isBot = getApType(object) === 'Service' || getApType(object) === 'Application'; + const [followingVisibility, followersVisibility] = await Promise.all( + [ + this.isPublicCollection(person.following, resolver), + this.isPublicCollection(person.followers, resolver), + ].map((p): Promise<'public' | 'private'> => p + .then(isPublic => isPublic ? 'public' : 'private') + .catch(err => { + if (!(err instanceof StatusError) || err.isRetryable) { + this.logger.error('error occurred while fetching following/followers collection', { stack: err }); + } + return 'private'; + }) + ) + ); + const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const url = getOneApHrefNullable(person.url); @@ -357,6 +372,8 @@ export class ApPersonService implements OnModuleInit { description: _description, url, fields, + followingVisibility, + followersVisibility, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, userHost: host, @@ -464,6 +481,23 @@ export class ApPersonService implements OnModuleInit { const tags = extractApHashtags(person.tag).map(normalizeForSearch).splice(0, 32); + const [followingVisibility, followersVisibility] = await Promise.all( + [ + this.isPublicCollection(person.following, resolver), + this.isPublicCollection(person.followers, resolver), + ].map((p): Promise<'public' | 'private' | undefined> => p + .then(isPublic => isPublic ? 'public' : 'private') + .catch(err => { + if (!(err instanceof StatusError) || err.isRetryable) { + this.logger.error('error occurred while fetching following/followers collection', { stack: err }); + // Do not update the visibiility on transient errors. + return undefined; + } + return 'private'; + }) + ) + ); + const bday = person['vcard:bday']?.match(/^\d{4}-\d{2}-\d{2}/); const url = getOneApHrefNullable(person.url); @@ -532,6 +566,8 @@ export class ApPersonService implements OnModuleInit { url, fields, description: _description, + followingVisibility, + followersVisibility, birthday: bday?.[0] ?? null, location: person['vcard:Address'] ?? null, }); @@ -703,4 +739,16 @@ export class ApPersonService implements OnModuleInit { return 'ok'; } + + @bindThis + private async isPublicCollection(collection: string | ICollection | IOrderedCollection | undefined, resolver: Resolver): Promise { + if (collection) { + const resolved = await resolver.resolveCollection(collection); + if (resolved.first || (resolved as ICollection).items || (resolved as IOrderedCollection).orderedItems) { + return true; + } + } + + return false; + } } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 5b6c6c8ca6..131c518c0a 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -97,13 +97,15 @@ export interface IActivity extends IObject { export interface ICollection extends IObject { type: 'Collection'; totalItems: number; - items: ApObject; + first?: IObject | string; + items?: ApObject; } export interface IOrderedCollection extends IObject { type: 'OrderedCollection'; totalItems: number; - orderedItems: ApObject; + first?: IObject | string; + orderedItems?: ApObject; } export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video', 'Event']; diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts index 7fd093c191..9bf568bc90 100644 --- a/packages/backend/src/core/entities/UserEntityService.ts +++ b/packages/backend/src/core/entities/UserEntityService.ts @@ -454,12 +454,12 @@ export class UserEntityService implements OnModuleInit { } const followingCount = profile == null ? null : - (profile.followingVisibility === 'public') || isMe ? user.followingCount : + (profile.followingVisibility === 'public') || isMe || iAmModerator ? user.followingCount : (profile.followingVisibility === 'followers') && (relation && relation.isFollowing) ? user.followingCount : null; const followersCount = profile == null ? null : - (profile.followersVisibility === 'public') || isMe ? user.followersCount : + (profile.followersVisibility === 'public') || isMe || iAmModerator ? user.followersCount : (profile.followersVisibility === 'followers') && (relation && relation.isFollowing) ? user.followersCount : null; diff --git a/packages/backend/src/server/api/endpoints/users/followers.ts b/packages/backend/src/server/api/endpoints/users/followers.ts index 7ce7734f53..a8b4319a61 100644 --- a/packages/backend/src/server/api/endpoints/users/followers.ts +++ b/packages/backend/src/server/api/endpoints/users/followers.ts @@ -11,6 +11,7 @@ import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -81,6 +82,7 @@ export default class extends Endpoint { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy(ps.userId != null @@ -93,23 +95,25 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followersVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.followersVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { + if (profile.followersVisibility !== 'public' && !await this.roleService.isModerator(me)) { + if (profile.followersVisibility === 'private') { + if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } + } else if (profile.followersVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { + throw new ApiError(meta.errors.forbidden); + } + } } } diff --git a/packages/backend/src/server/api/endpoints/users/following.ts b/packages/backend/src/server/api/endpoints/users/following.ts index 6b3389f0b2..feda5bb353 100644 --- a/packages/backend/src/server/api/endpoints/users/following.ts +++ b/packages/backend/src/server/api/endpoints/users/following.ts @@ -12,6 +12,7 @@ import { QueryService } from '@/core/QueryService.js'; import { FollowingEntityService } from '@/core/entities/FollowingEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; +import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -90,6 +91,7 @@ export default class extends Endpoint { // eslint- private utilityService: UtilityService, private followingEntityService: FollowingEntityService, private queryService: QueryService, + private roleService: RoleService, ) { super(meta, paramDef, async (ps, me) => { const user = await this.usersRepository.findOneBy(ps.userId != null @@ -102,23 +104,25 @@ export default class extends Endpoint { // eslint- const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - if (profile.followingVisibility === 'private') { - if (me == null || (me.id !== user.id)) { - throw new ApiError(meta.errors.forbidden); - } - } else if (profile.followingVisibility === 'followers') { - if (me == null) { - throw new ApiError(meta.errors.forbidden); - } else if (me.id !== user.id) { - const isFollowing = await this.followingsRepository.exists({ - where: { - followeeId: user.id, - followerId: me.id, - }, - }); - if (!isFollowing) { + if (profile.followingVisibility !== 'public' && !await this.roleService.isModerator(me)) { + if (profile.followingVisibility === 'private') { + if (me == null || (me.id !== user.id)) { throw new ApiError(meta.errors.forbidden); } + } else if (profile.followingVisibility === 'followers') { + if (me == null) { + throw new ApiError(meta.errors.forbidden); + } else if (me.id !== user.id) { + const isFollowing = await this.followingsRepository.exists({ + where: { + followeeId: user.id, + followerId: me.id, + }, + }); + if (!isFollowing) { + throw new ApiError(meta.errors.forbidden); + } + } } } diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 6962608106..763ce2b336 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -20,7 +20,8 @@ import { CoreModule } from '@/core/CoreModule.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { LoggerService } from '@/core/LoggerService.js'; import type { IActor, IApDocument, ICollection, IObject, IPost } from '@/core/activitypub/type.js'; -import { MiMeta, MiNote } from '@/models/_.js'; +import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DownloadService } from '@/core/DownloadService.js'; import { MetaService } from '@/core/MetaService.js'; @@ -86,6 +87,7 @@ async function createRandomRemoteUser( } describe('ActivityPub', () => { + let userProfilesRepository: UserProfilesRepository; let imageService: ApImageService; let noteService: ApNoteService; let personService: ApPersonService; @@ -127,6 +129,8 @@ describe('ActivityPub', () => { await app.init(); app.enableShutdownHooks(); + userProfilesRepository = app.get(DI.userProfilesRepository); + noteService = app.get(ApNoteService); personService = app.get(ApPersonService); rendererService = app.get(ApRendererService); @@ -205,6 +209,53 @@ describe('ActivityPub', () => { }); }); + describe('Collection visibility', () => { + test('Public following/followers', async () => { + const actor = createRandomActor(); + actor.following = { + id: `${actor.id}/following`, + type: 'OrderedCollection', + totalItems: 0, + first: `${actor.id}/following?page=1`, + }; + actor.followers = `${actor.id}/followers`; + + resolver.register(actor.id, actor); + resolver.register(actor.followers, { + id: actor.followers, + type: 'OrderedCollection', + totalItems: 0, + first: `${actor.followers}?page=1`, + }); + + const user = await personService.createPerson(actor.id, resolver); + const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id }); + + assert.deepStrictEqual(userProfile.followingVisibility, 'public'); + assert.deepStrictEqual(userProfile.followersVisibility, 'public'); + }); + + test('Private following/followers', async () => { + const actor = createRandomActor(); + actor.following = { + id: `${actor.id}/following`, + type: 'OrderedCollection', + totalItems: 0, + // first: … + }; + actor.followers = `${actor.id}/followers`; + + resolver.register(actor.id, actor); + //resolver.register(actor.followers, { … }); + + const user = await personService.createPerson(actor.id, resolver); + const userProfile = await userProfilesRepository.findOneByOrFail({ userId: user.id }); + + assert.deepStrictEqual(userProfile.followingVisibility, 'private'); + assert.deepStrictEqual(userProfile.followersVisibility, 'private'); + }); + }); + describe('Renderer', () => { test('Render an announce with visibility: followers', () => { rendererService.renderAnnounce('https://example.com/notes/00example', { diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index 406404c462..e28e5725bc 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js'; import { $i } from '@/account.js'; export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && $i.id === user.id) return true; + if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; if (user.followingVisibility === 'private') return false; if (user.followingVisibility === 'followers' && !user.isFollowing) return false; @@ -15,7 +15,7 @@ export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): bo return true; } export function isFollowersVisibleForMe(user: Misskey.entities.UserDetailed): boolean { - if ($i && $i.id === user.id) return true; + if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true; if (user.followersVisibility === 'private') return false; if (user.followersVisibility === 'followers' && !user.isFollowing) return false; From f50941389d8724442ce2d7326afe9fbdadd3b58e Mon Sep 17 00:00:00 2001 From: anatawa12 Date: Fri, 9 Aug 2024 16:04:41 +0900 Subject: [PATCH 226/635] fix: readAllNotifications message not working (#14374) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: add and use isJsonObject * fix: readNotification message without body is not working * docs(changelog): WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 * Update CHANGELOG.md Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --------- Co-authored-by: Sayamame-beans <61457993+Sayamame-beans@users.noreply.github.com> --- CHANGELOG.md | 4 ++- packages/backend/src/misc/json-value.ts | 4 +++ .../src/server/api/stream/Connection.ts | 27 +++++++++++-------- .../server/api/stream/channels/queue-stats.ts | 3 ++- .../api/stream/channels/reversi-game.ts | 7 ++--- .../api/stream/channels/server-stats.ts | 3 ++- 6 files changed, 31 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c18cccc44e..fa0bc6282b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,9 @@ - ### Server -- +- Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374 + - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。 + - これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。 ## 2024.7.0 diff --git a/packages/backend/src/misc/json-value.ts b/packages/backend/src/misc/json-value.ts index 7994441791..bd7fe12058 100644 --- a/packages/backend/src/misc/json-value.ts +++ b/packages/backend/src/misc/json-value.ts @@ -6,3 +6,7 @@ export type JsonValue = JsonArray | JsonObject | string | number | boolean | null; export type JsonObject = {[K in string]?: JsonValue}; export type JsonArray = JsonValue[]; + +export function isJsonObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/backend/src/server/api/stream/Connection.ts b/packages/backend/src/server/api/stream/Connection.ts index 96082827f8..7773150b74 100644 --- a/packages/backend/src/server/api/stream/Connection.ts +++ b/packages/backend/src/server/api/stream/Connection.ts @@ -14,7 +14,8 @@ import { CacheService } from '@/core/CacheService.js'; import { MiFollowing, MiUserProfile } from '@/models/_.js'; import type { StreamEventEmitter, GlobalEvents } from '@/core/GlobalEventService.js'; import { ChannelFollowingService } from '@/core/ChannelFollowingService.js'; -import type { JsonObject } from '@/misc/json-value.js'; +import { isJsonObject } from '@/misc/json-value.js'; +import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import type { ChannelsService } from './ChannelsService.js'; import type { EventEmitter } from 'events'; import type Channel from './channel.js'; @@ -112,8 +113,6 @@ export default class Connection { const { type, body } = obj; - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; - switch (type) { case 'readNotification': this.onReadNotification(body); break; case 'subNote': this.onSubscribeNote(body); break; @@ -154,7 +153,8 @@ export default class Connection { } @bindThis - private readNote(body: JsonObject) { + private readNote(body: JsonValue | undefined) { + if (!isJsonObject(body)) return; const id = body.id; const note = this.cachedNotes.find(n => n.id === id); @@ -166,7 +166,7 @@ export default class Connection { } @bindThis - private onReadNotification(payload: JsonObject) { + private onReadNotification(payload: JsonValue | undefined) { this.notificationService.readAllNotification(this.user!.id); } @@ -174,7 +174,8 @@ export default class Connection { * 投稿購読要求時 */ @bindThis - private onSubscribeNote(payload: JsonObject) { + private onSubscribeNote(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; if (!payload.id || typeof payload.id !== 'string') return; const current = this.subscribingNotes[payload.id] ?? 0; @@ -190,7 +191,8 @@ export default class Connection { * 投稿購読解除要求時 */ @bindThis - private onUnsubscribeNote(payload: JsonObject) { + private onUnsubscribeNote(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; if (!payload.id || typeof payload.id !== 'string') return; const current = this.subscribingNotes[payload.id]; @@ -216,12 +218,13 @@ export default class Connection { * チャンネル接続要求時 */ @bindThis - private onChannelConnectRequested(payload: JsonObject) { + private onChannelConnectRequested(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; const { channel, id, params, pong } = payload; if (typeof id !== 'string') return; if (typeof channel !== 'string') return; if (typeof pong !== 'boolean' && typeof pong !== 'undefined' && pong !== null) return; - if (typeof params !== 'undefined' && (typeof params !== 'object' || params === null || Array.isArray(params))) return; + if (typeof params !== 'undefined' && !isJsonObject(params)) return; this.connectChannel(id, params, channel, pong ?? undefined); } @@ -229,7 +232,8 @@ export default class Connection { * チャンネル切断要求時 */ @bindThis - private onChannelDisconnectRequested(payload: JsonObject) { + private onChannelDisconnectRequested(payload: JsonValue | undefined) { + if (!isJsonObject(payload)) return; const { id } = payload; if (typeof id !== 'string') return; this.disconnectChannel(id); @@ -297,7 +301,8 @@ export default class Connection { * @param data メッセージ */ @bindThis - private onChannelMessageRequested(data: JsonObject) { + private onChannelMessageRequested(data: JsonValue | undefined) { + if (!isJsonObject(data)) return; if (typeof data.id !== 'string') return; if (typeof data.type !== 'string') return; if (typeof data.body === 'undefined') return; diff --git a/packages/backend/src/server/api/stream/channels/queue-stats.ts b/packages/backend/src/server/api/stream/channels/queue-stats.ts index ff7e740226..91b62255b4 100644 --- a/packages/backend/src/server/api/stream/channels/queue-stats.ts +++ b/packages/backend/src/server/api/stream/channels/queue-stats.ts @@ -6,6 +6,7 @@ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { isJsonObject } from '@/misc/json-value.js'; import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -36,7 +37,7 @@ class QueueStatsChannel extends Channel { public onMessage(type: string, body: JsonValue) { switch (type) { case 'requestLog': - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; + if (!isJsonObject(body)) return; if (typeof body.id !== 'string') return; if (typeof body.length !== 'number') return; ev.once(`queueStatsLog:${body.id}`, statsLog => { diff --git a/packages/backend/src/server/api/stream/channels/reversi-game.ts b/packages/backend/src/server/api/stream/channels/reversi-game.ts index 17823a164a..c6f4a4ae3b 100644 --- a/packages/backend/src/server/api/stream/channels/reversi-game.ts +++ b/packages/backend/src/server/api/stream/channels/reversi-game.ts @@ -9,6 +9,7 @@ import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; import { ReversiService } from '@/core/ReversiService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; +import { isJsonObject } from '@/misc/json-value.js'; import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -44,16 +45,16 @@ class ReversiGameChannel extends Channel { this.ready(body); break; case 'updateSettings': - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; + if (!isJsonObject(body)) return; if (typeof body.key !== 'string') return; - if (typeof body.value !== 'object' || body.value === null || Array.isArray(body.value)) return; + if (!isJsonObject(body.value)) return; this.updateSettings(body.key, body.value); break; case 'cancel': this.cancelGame(); break; case 'putStone': - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; + if (!isJsonObject(body)) return; if (typeof body.pos !== 'number') return; if (typeof body.id !== 'string') return; this.putStone(body.pos, body.id); diff --git a/packages/backend/src/server/api/stream/channels/server-stats.ts b/packages/backend/src/server/api/stream/channels/server-stats.ts index 6258afba35..ec5352d12d 100644 --- a/packages/backend/src/server/api/stream/channels/server-stats.ts +++ b/packages/backend/src/server/api/stream/channels/server-stats.ts @@ -6,6 +6,7 @@ import Xev from 'xev'; import { Injectable } from '@nestjs/common'; import { bindThis } from '@/decorators.js'; +import { isJsonObject } from '@/misc/json-value.js'; import type { JsonObject, JsonValue } from '@/misc/json-value.js'; import Channel, { type MiChannelService } from '../channel.js'; @@ -36,7 +37,7 @@ class ServerStatsChannel extends Channel { public onMessage(type: string, body: JsonValue) { switch (type) { case 'requestLog': - if (typeof body !== 'object' || body === null || Array.isArray(body)) return; + if (!isJsonObject(body)) return; ev.once(`serverStatsLog:${body.id}`, statsLog => { this.send('statsLog', statsLog); }); From 01a815f8a716f65dbd977d533d638eb69561136a Mon Sep 17 00:00:00 2001 From: zyoshoka <107108195+zyoshoka@users.noreply.github.com> Date: Sat, 10 Aug 2024 09:34:49 +0900 Subject: [PATCH 227/635] fix(general): some fixes and improvements of Play visibility (#14384) * fix(backend): missing `visibility` param in packing flash * fix(frontend): use `visibility` value got from API * enhance(frontend): change preview appearance of private Play * Update CHANGELOG.md --- CHANGELOG.md | 4 +- .../src/core/entities/FlashEntityService.ts | 1 + .../backend/src/models/json-schema/flash.ts | 5 ++ packages/frontend/.storybook/fakes.ts | 35 ++++++++++++ packages/frontend/.storybook/generate.tsx | 1 + .../components/MkFlashPreview.stories.impl.ts | 53 +++++++++++++++++++ .../src/components/MkFlashPreview.vue | 12 +++-- .../frontend/src/pages/flash/flash-edit.vue | 2 +- packages/misskey-js/src/autogen/types.ts | 2 + 9 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 packages/frontend/src/components/MkFlashPreview.stories.impl.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0bc6282b..7128499c79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,14 @@ - Enhance: モデレーターはすべてのユーザーのフォロー・フォロワーの一覧を見られるように ### Client -- +- Enhance: 「自分のPlay」ページにおいてPlayが非公開かどうかが一目でわかるように +- Fix: Play編集時に公開範囲が「パブリック」にリセットされる問題を修正 ### Server - Fix: WSの`readAllNotifications` メッセージが `body` を持たない場合に動作しない問題 #14374 - 通知ページや通知カラム(デッキ)を開いている状態において、新たに発生した通知が既読されない問題が修正されます。 - これにより、プッシュ通知が有効な同条件下の環境において、プッシュ通知が常に発生してしまう問題も修正されます。 +- Fix: Play各種エンドポイントの返り値に`visibility`が含まれていない問題を修正 ## 2024.7.0 diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts index d110f7afc6..4aa7104c1e 100644 --- a/packages/backend/src/core/entities/FlashEntityService.ts +++ b/packages/backend/src/core/entities/FlashEntityService.ts @@ -49,6 +49,7 @@ export class FlashEntityService { title: flash.title, summary: flash.summary, script: flash.script, + visibility: flash.visibility, likedCount: flash.likedCount, isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined, }); diff --git a/packages/backend/src/models/json-schema/flash.ts b/packages/backend/src/models/json-schema/flash.ts index 952df649ad..42b2172409 100644 --- a/packages/backend/src/models/json-schema/flash.ts +++ b/packages/backend/src/models/json-schema/flash.ts @@ -44,6 +44,11 @@ export const packedFlashSchema = { type: 'string', optional: false, nullable: false, }, + visibility: { + type: 'string', + optional: false, nullable: false, + enum: ['private', 'public'], + }, likedCount: { type: 'number', optional: false, nullable: true, diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index ab04d3e60c..fc3b0334e4 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -3,6 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { AISCRIPT_VERSION } from '@syuilo/aiscript'; import type { entities } from 'misskey-js' export function abuseUserReport() { @@ -114,6 +115,40 @@ export function file(isSensitive = false) { }; } +const script = `/// @ ${AISCRIPT_VERSION} + +var name = "" + +Ui:render([ + Ui:C:textInput({ + label: "Your name" + onInput: @(v) { name = v } + }) + Ui:C:button({ + text: "Hello" + onClick: @() { + Mk:dialog(null, \`Hello, {name}!\`) + } + }) +]) +`; + +export function flash(): entities.Flash { + return { + id: 'someflashid', + createdAt: '2016-12-28T22:49:51.000Z', + updatedAt: '2016-12-28T22:49:51.000Z', + userId: 'someuserid', + user: userLite(), + title: 'Some Play title', + summary: 'Some Play summary', + script, + visibility: 'public', + likedCount: 0, + isLiked: false, + }; +} + export function folder(id = 'somefolderid', name = 'Some Folder', parentId: string | null = null): entities.DriveFolder { return { id, diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx index 52c01aaf70..490a441b70 100644 --- a/packages/frontend/.storybook/generate.tsx +++ b/packages/frontend/.storybook/generate.tsx @@ -398,6 +398,7 @@ function toStories(component: string): Promise { glob('src/components/global/Mk*.vue'), glob('src/components/global/RouterView.vue'), glob('src/components/Mk[A-E]*.vue'), + glob('src/components/MkFlashPreview.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkUserSetupDialog.vue'), diff --git a/packages/frontend/src/components/MkFlashPreview.stories.impl.ts b/packages/frontend/src/components/MkFlashPreview.stories.impl.ts new file mode 100644 index 0000000000..fa5288b73d --- /dev/null +++ b/packages/frontend/src/components/MkFlashPreview.stories.impl.ts @@ -0,0 +1,53 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { StoryObj } from '@storybook/vue3'; +import MkFlashPreview from './MkFlashPreview.vue'; +import { flash } from './../../.storybook/fakes.js'; +export const Public = { + render(args) { + return { + components: { + MkFlashPreview, + }, + setup() { + return { + args, + }; + }, + computed: { + props() { + return { + ...this.args, + }; + }, + }, + template: '', + }; + }, + args: { + flash: { + ...flash(), + visibility: 'public', + }, + }, + parameters: { + layout: 'fullscreen', + }, + decorators: [ + () => ({ + template: '
', + }), + ], +} satisfies StoryObj; +export const Private = { + ...Public, + args: { + flash: { + ...flash(), + visibility: 'private', + }, + }, +} satisfies StoryObj; diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue index 6783804cc5..8a2a438624 100644 --- a/packages/frontend/src/components/MkFlashPreview.vue +++ b/packages/frontend/src/components/MkFlashPreview.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> + + From cdb0566c5b823f0ce4ecc493bd459cb726431be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 6 Sep 2024 16:12:14 +0900 Subject: [PATCH 302/635] =?UTF-8?q?refactor(frontend):=20scss=20deprecated?= =?UTF-8?q?=20=E8=AD=A6=E5=91=8A=E3=81=AB=E5=AF=BE=E5=BF=9C=20(#14513)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../frontend/src/components/MkModalWindow.vue | 6 +++--- packages/frontend/src/components/MkSuperMenu.vue | 6 +++--- packages/frontend/src/components/MkWindow.vue | 8 ++++---- .../frontend/src/pages/admin/overview.users.vue | 8 ++++---- packages/frontend/src/pages/page.vue | 3 +-- packages/frontend/src/style.scss | 8 ++++---- packages/frontend/src/ui/_common_/statusbars.vue | 16 ++++++++-------- packages/frontend/src/ui/deck/column.vue | 6 +++--- 8 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue index c3c7812036..f26959888b 100644 --- a/packages/frontend/src/components/MkModalWindow.vue +++ b/packages/frontend/src/components/MkModalWindow.vue @@ -94,12 +94,12 @@ defineExpose({ --root-margin: 24px; + --headerHeight: 46px; + --headerHeightNarrow: 42px; + @media (max-width: 500px) { --root-margin: 16px; } - - --headerHeight: 46px; - --headerHeightNarrow: 42px; } .header { diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 1a880170be..3746ffd8f3 100644 --- a/packages/frontend/src/components/MkSuperMenu.vue +++ b/packages/frontend/src/components/MkSuperMenu.vue @@ -100,14 +100,14 @@ defineProps<{ &.grid { > .group { + margin-left: 0; + margin-right: 0; + & + .group { padding-top: 0; border-top: none; } - margin-left: 0; - margin-right: 0; - > .title { font-size: 1em; opacity: 0.7; diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue index 303e49de00..26ba598498 100644 --- a/packages/frontend/src/components/MkWindow.vue +++ b/packages/frontend/src/components/MkWindow.vue @@ -508,10 +508,6 @@ defineExpose({ .header { --height: 39px; - &.mini { - --height: 32px; - } - display: flex; position: relative; z-index: 1; @@ -524,6 +520,10 @@ defineExpose({ //border-bottom: solid 1px var(--divider); font-size: 90%; font-weight: bold; + + &.mini { + --height: 32px; + } } .headerButton { diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue index 408be88d47..a7dd4c0a48 100644 --- a/packages/frontend/src/pages/admin/overview.users.vue +++ b/packages/frontend/src/pages/admin/overview.users.vue @@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, { .root { &:global { > .users { - .chart-move { - transition: transform 1s ease; - } - display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 12px; + .chart-move { + transition: transform 1s ease; + } + > .user:hover { text-decoration: none; } diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue index cb1ce9b918..7ae61236e8 100644 --- a/packages/frontend/src/pages/page.vue +++ b/packages/frontend/src/pages/page.vue @@ -433,13 +433,12 @@ definePageMetadata(() => ({ .pageBannerTitleUser { --height: 32px; flex-shrink: 0; + line-height: var(--height); .avatar { height: var(--height); width: var(--height); } - - line-height: var(--height); } .pageBannerTitleSubActions { diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss index 44ef740a2e..caaf9fca6f 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -17,10 +17,6 @@ --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px))); --minBottomSpacing: var(--minBottomSpacingMobile); - @media (max-width: 500px) { - --margin: var(--marginHalf); - } - //--ad: rgb(255 169 0 / 10%); --eventFollow: #36aed2; --eventRenote: #36d298; @@ -29,6 +25,10 @@ --eventReaction: #e99a0b; --eventAchievement: #cb9a11; --eventOther: #88a6b7; + + @media (max-width: 500px) { + --margin: var(--marginHalf); + } } ::selection { diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue index 872c69810c..690366307b 100644 --- a/packages/frontend/src/ui/_common_/statusbars.vue +++ b/packages/frontend/src/ui/_common_/statusbars.vue @@ -40,6 +40,14 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') --nameMargin: 10px; font-size: 0.85em; + display: flex; + vertical-align: bottom; + width: 100%; + line-height: var(--height); + height: var(--height); + overflow: clip; + contain: strict; + &.verySmall { --nameMargin: 7px; --height: 16px; @@ -64,14 +72,6 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue') font-size: 0.9em; } - display: flex; - vertical-align: bottom; - width: 100%; - line-height: var(--height); - height: var(--height); - overflow: clip; - contain: strict; - &.black { background: #000; color: #fff; diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue index e96402d13b..893301122e 100644 --- a/packages/frontend/src/ui/deck/column.vue +++ b/packages/frontend/src/ui/deck/column.vue @@ -324,11 +324,11 @@ function onDrop(ev) { > .body { background: transparent !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: transparent; } - scrollbar-color: var(--scrollbarHandle) transparent; } } @@ -338,11 +338,11 @@ function onDrop(ev) { > .body { background: var(--bg) !important; overflow-y: scroll !important; + scrollbar-color: var(--scrollbarHandle) transparent; &::-webkit-scrollbar-track { background: inherit; } - scrollbar-color: var(--scrollbarHandle) transparent; } } } @@ -423,10 +423,10 @@ function onDrop(ev) { box-sizing: border-box; container-type: size; background-color: var(--bg); + scrollbar-color: var(--scrollbarHandle) var(--panel); &::-webkit-scrollbar-track { background: var(--panel); } - scrollbar-color: var(--scrollbarHandle) var(--panel); } From 8d19bdbb65c79e2425bf5c73fd8b6310670a8c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:22:45 +0900 Subject: [PATCH 303/635] =?UTF-8?q?fix(misskey-js):=20content-type?= =?UTF-8?q?=E3=81=AFapplication/json=E3=81=A7=E3=81=AA=E3=81=84=E3=82=82?= =?UTF-8?q?=E3=81=AE=E3=81=AE=E3=81=BF=E3=82=92=E8=A8=98=E9=8C=B2=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#14508)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../misskey-js/generator/src/generator.ts | 21 +- packages/misskey-js/src/api.ts | 9 +- packages/misskey-js/src/autogen/endpoint.ts | 385 +----------------- 3 files changed, 22 insertions(+), 393 deletions(-) diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts index 4ae00a4522..88f2ae9ee9 100644 --- a/packages/misskey-js/generator/src/generator.ts +++ b/packages/misskey-js/generator/src/generator.ts @@ -96,15 +96,11 @@ async function generateEndpoints( endpoint.request = req; const reqType = new EndpointReqMediaType(path, req); - endpointReqMediaTypesSet.add(reqType.getMediaType()); - endpointReqMediaTypes.push(reqType); - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); + if (reqType.getMediaType() !== 'application/json') { + endpointReqMediaTypesSet.add(reqType.getMediaType()); + endpointReqMediaTypes.push(reqType); + } } - } else { - endpointReqMediaTypesSet.add('application/json'); - endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json')); } if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) { @@ -158,16 +154,19 @@ async function generateEndpoints( endpointOutputLine.push(''); function generateEndpointReqMediaTypesType() { - return `Record `'${t}'`).join(' | ')}>`; + return `{ [K in keyof Endpoints]?: ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}; }`; } - endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`); + endpointOutputLine.push(`/** + * NOTE: The content-type for all endpoints not listed here is application/json. + */`); + endpointOutputLine.push('export const endpointReqTypes = {'); endpointOutputLine.push( ...endpointReqMediaTypes.map(it => '\t' + it.toLine()), ); - endpointOutputLine.push('};'); + endpointOutputLine.push(`} as const satisfies ${generateEndpointReqMediaTypesType()};`); endpointOutputLine.push(''); await writeFile(endpointOutputPath, endpointOutputLine.join('\n')); diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts index ea1df57f3d..659a29a221 100644 --- a/packages/misskey-js/src/api.ts +++ b/packages/misskey-js/src/api.ts @@ -56,6 +56,10 @@ export class APIClient { return obj !== null && typeof obj === 'object' && !Array.isArray(obj); } + private assertSpecialEpReqType(ep: keyof Endpoints): ep is keyof typeof endpointReqTypes { + return ep in endpointReqTypes; + } + public request( endpoint: E, params: P = {} as P, @@ -63,9 +67,10 @@ export class APIClient { ): Promise> { return new Promise((resolve, reject) => { let mediaType = 'application/json'; - if (endpoint in endpointReqTypes) { + if (this.assertSpecialEpReqType(endpoint) && endpointReqTypes[endpoint] != null) { mediaType = endpointReqTypes[endpoint]; } + let payload: FormData | string = '{}'; if (mediaType === 'application/json') { @@ -100,7 +105,7 @@ export class APIClient { method: 'POST', body: payload, headers: { - 'Content-Type': endpointReqTypes[endpoint], + 'Content-Type': mediaType, }, credentials: 'omit', cache: 'no-cache', diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts index be41951e4d..8fbdbbb629 100644 --- a/packages/misskey-js/src/autogen/endpoint.ts +++ b/packages/misskey-js/src/autogen/endpoint.ts @@ -955,384 +955,9 @@ export type Endpoints = { 'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse }; } -export const endpointReqTypes: Record = { - 'admin/meta': 'application/json', - 'admin/abuse-user-reports': 'application/json', - 'admin/abuse-report/notification-recipient/list': 'application/json', - 'admin/abuse-report/notification-recipient/show': 'application/json', - 'admin/abuse-report/notification-recipient/create': 'application/json', - 'admin/abuse-report/notification-recipient/update': 'application/json', - 'admin/abuse-report/notification-recipient/delete': 'application/json', - 'admin/accounts/create': 'application/json', - 'admin/accounts/delete': 'application/json', - 'admin/accounts/find-by-email': 'application/json', - 'admin/ad/create': 'application/json', - 'admin/ad/delete': 'application/json', - 'admin/ad/list': 'application/json', - 'admin/ad/update': 'application/json', - 'admin/announcements/create': 'application/json', - 'admin/announcements/delete': 'application/json', - 'admin/announcements/list': 'application/json', - 'admin/announcements/update': 'application/json', - 'admin/avatar-decorations/create': 'application/json', - 'admin/avatar-decorations/delete': 'application/json', - 'admin/avatar-decorations/list': 'application/json', - 'admin/avatar-decorations/update': 'application/json', - 'admin/delete-all-files-of-a-user': 'application/json', - 'admin/unset-user-avatar': 'application/json', - 'admin/unset-user-banner': 'application/json', - 'admin/drive/clean-remote-files': 'application/json', - 'admin/drive/cleanup': 'application/json', - 'admin/drive/files': 'application/json', - 'admin/drive/show-file': 'application/json', - 'admin/emoji/add-aliases-bulk': 'application/json', - 'admin/emoji/add': 'application/json', - 'admin/emoji/copy': 'application/json', - 'admin/emoji/delete-bulk': 'application/json', - 'admin/emoji/delete': 'application/json', - 'admin/emoji/import-zip': 'application/json', - 'admin/emoji/list-remote': 'application/json', - 'admin/emoji/list': 'application/json', - 'admin/emoji/remove-aliases-bulk': 'application/json', - 'admin/emoji/set-aliases-bulk': 'application/json', - 'admin/emoji/set-category-bulk': 'application/json', - 'admin/emoji/set-license-bulk': 'application/json', - 'admin/emoji/update': 'application/json', - 'admin/federation/delete-all-files': 'application/json', - 'admin/federation/refresh-remote-instance-metadata': 'application/json', - 'admin/federation/remove-all-following': 'application/json', - 'admin/federation/update-instance': 'application/json', - 'admin/get-index-stats': 'application/json', - 'admin/get-table-stats': 'application/json', - 'admin/get-user-ips': 'application/json', - 'admin/invite/create': 'application/json', - 'admin/invite/list': 'application/json', - 'admin/promo/create': 'application/json', - 'admin/queue/clear': 'application/json', - 'admin/queue/deliver-delayed': 'application/json', - 'admin/queue/inbox-delayed': 'application/json', - 'admin/queue/promote': 'application/json', - 'admin/queue/stats': 'application/json', - 'admin/relays/add': 'application/json', - 'admin/relays/list': 'application/json', - 'admin/relays/remove': 'application/json', - 'admin/reset-password': 'application/json', - 'admin/resolve-abuse-user-report': 'application/json', - 'admin/send-email': 'application/json', - 'admin/server-info': 'application/json', - 'admin/show-moderation-logs': 'application/json', - 'admin/show-user': 'application/json', - 'admin/show-users': 'application/json', - 'admin/suspend-user': 'application/json', - 'admin/unsuspend-user': 'application/json', - 'admin/update-meta': 'application/json', - 'admin/delete-account': 'application/json', - 'admin/update-user-note': 'application/json', - 'admin/roles/create': 'application/json', - 'admin/roles/delete': 'application/json', - 'admin/roles/list': 'application/json', - 'admin/roles/show': 'application/json', - 'admin/roles/update': 'application/json', - 'admin/roles/assign': 'application/json', - 'admin/roles/unassign': 'application/json', - 'admin/roles/update-default-policies': 'application/json', - 'admin/roles/users': 'application/json', - 'admin/system-webhook/create': 'application/json', - 'admin/system-webhook/delete': 'application/json', - 'admin/system-webhook/list': 'application/json', - 'admin/system-webhook/show': 'application/json', - 'admin/system-webhook/update': 'application/json', - 'announcements': 'application/json', - 'announcements/show': 'application/json', - 'antennas/create': 'application/json', - 'antennas/delete': 'application/json', - 'antennas/list': 'application/json', - 'antennas/notes': 'application/json', - 'antennas/show': 'application/json', - 'antennas/update': 'application/json', - 'ap/get': 'application/json', - 'ap/show': 'application/json', - 'app/create': 'application/json', - 'app/show': 'application/json', - 'auth/accept': 'application/json', - 'auth/session/generate': 'application/json', - 'auth/session/show': 'application/json', - 'auth/session/userkey': 'application/json', - 'blocking/create': 'application/json', - 'blocking/delete': 'application/json', - 'blocking/list': 'application/json', - 'channels/create': 'application/json', - 'channels/featured': 'application/json', - 'channels/follow': 'application/json', - 'channels/followed': 'application/json', - 'channels/owned': 'application/json', - 'channels/show': 'application/json', - 'channels/timeline': 'application/json', - 'channels/unfollow': 'application/json', - 'channels/update': 'application/json', - 'channels/favorite': 'application/json', - 'channels/unfavorite': 'application/json', - 'channels/my-favorites': 'application/json', - 'channels/search': 'application/json', - 'charts/active-users': 'application/json', - 'charts/ap-request': 'application/json', - 'charts/drive': 'application/json', - 'charts/federation': 'application/json', - 'charts/instance': 'application/json', - 'charts/notes': 'application/json', - 'charts/user/drive': 'application/json', - 'charts/user/following': 'application/json', - 'charts/user/notes': 'application/json', - 'charts/user/pv': 'application/json', - 'charts/user/reactions': 'application/json', - 'charts/users': 'application/json', - 'clips/add-note': 'application/json', - 'clips/remove-note': 'application/json', - 'clips/create': 'application/json', - 'clips/delete': 'application/json', - 'clips/list': 'application/json', - 'clips/notes': 'application/json', - 'clips/show': 'application/json', - 'clips/update': 'application/json', - 'clips/favorite': 'application/json', - 'clips/unfavorite': 'application/json', - 'clips/my-favorites': 'application/json', - 'drive': 'application/json', - 'drive/files': 'application/json', - 'drive/files/attached-notes': 'application/json', - 'drive/files/check-existence': 'application/json', +/** + * NOTE: The content-type for all endpoints not listed here is application/json. + */ +export const endpointReqTypes = { 'drive/files/create': 'multipart/form-data', - 'drive/files/delete': 'application/json', - 'drive/files/find-by-hash': 'application/json', - 'drive/files/find': 'application/json', - 'drive/files/show': 'application/json', - 'drive/files/update': 'application/json', - 'drive/files/upload-from-url': 'application/json', - 'drive/folders': 'application/json', - 'drive/folders/create': 'application/json', - 'drive/folders/delete': 'application/json', - 'drive/folders/find': 'application/json', - 'drive/folders/show': 'application/json', - 'drive/folders/update': 'application/json', - 'drive/stream': 'application/json', - 'email-address/available': 'application/json', - 'endpoint': 'application/json', - 'endpoints': 'application/json', - 'export-custom-emojis': 'application/json', - 'federation/followers': 'application/json', - 'federation/following': 'application/json', - 'federation/instances': 'application/json', - 'federation/show-instance': 'application/json', - 'federation/update-remote-user': 'application/json', - 'federation/users': 'application/json', - 'federation/stats': 'application/json', - 'following/create': 'application/json', - 'following/delete': 'application/json', - 'following/update': 'application/json', - 'following/update-all': 'application/json', - 'following/invalidate': 'application/json', - 'following/requests/accept': 'application/json', - 'following/requests/cancel': 'application/json', - 'following/requests/list': 'application/json', - 'following/requests/reject': 'application/json', - 'gallery/featured': 'application/json', - 'gallery/popular': 'application/json', - 'gallery/posts': 'application/json', - 'gallery/posts/create': 'application/json', - 'gallery/posts/delete': 'application/json', - 'gallery/posts/like': 'application/json', - 'gallery/posts/show': 'application/json', - 'gallery/posts/unlike': 'application/json', - 'gallery/posts/update': 'application/json', - 'get-online-users-count': 'application/json', - 'get-avatar-decorations': 'application/json', - 'hashtags/list': 'application/json', - 'hashtags/search': 'application/json', - 'hashtags/show': 'application/json', - 'hashtags/trend': 'application/json', - 'hashtags/users': 'application/json', - 'i': 'application/json', - 'i/2fa/done': 'application/json', - 'i/2fa/key-done': 'application/json', - 'i/2fa/password-less': 'application/json', - 'i/2fa/register-key': 'application/json', - 'i/2fa/register': 'application/json', - 'i/2fa/update-key': 'application/json', - 'i/2fa/remove-key': 'application/json', - 'i/2fa/unregister': 'application/json', - 'i/apps': 'application/json', - 'i/authorized-apps': 'application/json', - 'i/claim-achievement': 'application/json', - 'i/change-password': 'application/json', - 'i/delete-account': 'application/json', - 'i/export-blocking': 'application/json', - 'i/export-following': 'application/json', - 'i/export-mute': 'application/json', - 'i/export-notes': 'application/json', - 'i/export-clips': 'application/json', - 'i/export-favorites': 'application/json', - 'i/export-user-lists': 'application/json', - 'i/export-antennas': 'application/json', - 'i/favorites': 'application/json', - 'i/gallery/likes': 'application/json', - 'i/gallery/posts': 'application/json', - 'i/import-blocking': 'application/json', - 'i/import-following': 'application/json', - 'i/import-muting': 'application/json', - 'i/import-user-lists': 'application/json', - 'i/import-antennas': 'application/json', - 'i/notifications': 'application/json', - 'i/notifications-grouped': 'application/json', - 'i/page-likes': 'application/json', - 'i/pages': 'application/json', - 'i/pin': 'application/json', - 'i/read-all-unread-notes': 'application/json', - 'i/read-announcement': 'application/json', - 'i/regenerate-token': 'application/json', - 'i/registry/get-all': 'application/json', - 'i/registry/get-detail': 'application/json', - 'i/registry/get': 'application/json', - 'i/registry/keys-with-type': 'application/json', - 'i/registry/keys': 'application/json', - 'i/registry/remove': 'application/json', - 'i/registry/scopes-with-domain': 'application/json', - 'i/registry/set': 'application/json', - 'i/revoke-token': 'application/json', - 'i/signin-history': 'application/json', - 'i/unpin': 'application/json', - 'i/update-email': 'application/json', - 'i/update': 'application/json', - 'i/move': 'application/json', - 'i/webhooks/create': 'application/json', - 'i/webhooks/list': 'application/json', - 'i/webhooks/show': 'application/json', - 'i/webhooks/update': 'application/json', - 'i/webhooks/delete': 'application/json', - 'invite/create': 'application/json', - 'invite/delete': 'application/json', - 'invite/list': 'application/json', - 'invite/limit': 'application/json', - 'meta': 'application/json', - 'emojis': 'application/json', - 'emoji': 'application/json', - 'miauth/gen-token': 'application/json', - 'mute/create': 'application/json', - 'mute/delete': 'application/json', - 'mute/list': 'application/json', - 'renote-mute/create': 'application/json', - 'renote-mute/delete': 'application/json', - 'renote-mute/list': 'application/json', - 'my/apps': 'application/json', - 'notes': 'application/json', - 'notes/children': 'application/json', - 'notes/clips': 'application/json', - 'notes/conversation': 'application/json', - 'notes/create': 'application/json', - 'notes/delete': 'application/json', - 'notes/favorites/create': 'application/json', - 'notes/favorites/delete': 'application/json', - 'notes/featured': 'application/json', - 'notes/global-timeline': 'application/json', - 'notes/hybrid-timeline': 'application/json', - 'notes/local-timeline': 'application/json', - 'notes/mentions': 'application/json', - 'notes/polls/recommendation': 'application/json', - 'notes/polls/vote': 'application/json', - 'notes/reactions': 'application/json', - 'notes/reactions/create': 'application/json', - 'notes/reactions/delete': 'application/json', - 'notes/renotes': 'application/json', - 'notes/replies': 'application/json', - 'notes/search-by-tag': 'application/json', - 'notes/search': 'application/json', - 'notes/show': 'application/json', - 'notes/state': 'application/json', - 'notes/thread-muting/create': 'application/json', - 'notes/thread-muting/delete': 'application/json', - 'notes/timeline': 'application/json', - 'notes/translate': 'application/json', - 'notes/unrenote': 'application/json', - 'notes/user-list-timeline': 'application/json', - 'notifications/create': 'application/json', - 'notifications/flush': 'application/json', - 'notifications/mark-all-as-read': 'application/json', - 'notifications/test-notification': 'application/json', - 'page-push': 'application/json', - 'pages/create': 'application/json', - 'pages/delete': 'application/json', - 'pages/featured': 'application/json', - 'pages/like': 'application/json', - 'pages/show': 'application/json', - 'pages/unlike': 'application/json', - 'pages/update': 'application/json', - 'flash/create': 'application/json', - 'flash/delete': 'application/json', - 'flash/featured': 'application/json', - 'flash/like': 'application/json', - 'flash/show': 'application/json', - 'flash/unlike': 'application/json', - 'flash/update': 'application/json', - 'flash/my': 'application/json', - 'flash/my-likes': 'application/json', - 'ping': 'application/json', - 'pinned-users': 'application/json', - 'promo/read': 'application/json', - 'roles/list': 'application/json', - 'roles/show': 'application/json', - 'roles/users': 'application/json', - 'roles/notes': 'application/json', - 'request-reset-password': 'application/json', - 'reset-db': 'application/json', - 'reset-password': 'application/json', - 'server-info': 'application/json', - 'stats': 'application/json', - 'sw/show-registration': 'application/json', - 'sw/update-registration': 'application/json', - 'sw/register': 'application/json', - 'sw/unregister': 'application/json', - 'test': 'application/json', - 'username/available': 'application/json', - 'users': 'application/json', - 'users/clips': 'application/json', - 'users/followers': 'application/json', - 'users/following': 'application/json', - 'users/gallery/posts': 'application/json', - 'users/get-frequently-replied-users': 'application/json', - 'users/featured-notes': 'application/json', - 'users/lists/create': 'application/json', - 'users/lists/delete': 'application/json', - 'users/lists/list': 'application/json', - 'users/lists/pull': 'application/json', - 'users/lists/push': 'application/json', - 'users/lists/show': 'application/json', - 'users/lists/favorite': 'application/json', - 'users/lists/unfavorite': 'application/json', - 'users/lists/update': 'application/json', - 'users/lists/create-from-public': 'application/json', - 'users/lists/update-membership': 'application/json', - 'users/lists/get-memberships': 'application/json', - 'users/notes': 'application/json', - 'users/pages': 'application/json', - 'users/flashs': 'application/json', - 'users/reactions': 'application/json', - 'users/recommendation': 'application/json', - 'users/relation': 'application/json', - 'users/report-abuse': 'application/json', - 'users/search-by-username-and-host': 'application/json', - 'users/search': 'application/json', - 'users/show': 'application/json', - 'users/achievements': 'application/json', - 'users/update-memo': 'application/json', - 'fetch-rss': 'application/json', - 'fetch-external-resources': 'application/json', - 'retention': 'application/json', - 'bubble-game/register': 'application/json', - 'bubble-game/ranking': 'application/json', - 'reversi/cancel-match': 'application/json', - 'reversi/games': 'application/json', - 'reversi/match': 'application/json', - 'reversi/invitations': 'application/json', - 'reversi/show-game': 'application/json', - 'reversi/surrender': 'application/json', - 'reversi/verify': 'application/json', -}; +} as const satisfies { [K in keyof Endpoints]?: 'multipart/form-data'; }; From 567acea2a3a040dbde69748deb2112e3ff2b92b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Fri, 6 Sep 2024 17:23:40 +0900 Subject: [PATCH 304/635] =?UTF-8?q?fix(frontend):=20instance=20info?= =?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=A7=E4=B8=8D=E5=BF=85=E8=A6=81?= =?UTF-8?q?=E3=81=AAapi=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=8C=E9=A3=9B=E3=81=B6=E3=81=AE=E3=82=92=E6=8A=91=E6=AD=A2?= =?UTF-8?q?=20(#14515)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): instance infoページで不必要なapiリクエストが飛ぶのを抑止 * fix --- packages/frontend/src/components/MkChart.vue | 50 ++++++++++--------- packages/frontend/src/pages/instance-info.vue | 25 +++++++--- 2 files changed, 43 insertions(+), 32 deletions(-) diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue index 4b24562249..57d325b11a 100644 --- a/packages/frontend/src/components/MkChart.vue +++ b/packages/frontend/src/components/MkChart.vue @@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
- + + diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue new file mode 100644 index 0000000000..07315e6a8b --- /dev/null +++ b/packages/frontend-embed/src/components/EmAcct.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/frontend-embed/src/components/EmAvatar.vue b/packages/frontend-embed/src/components/EmAvatar.vue new file mode 100644 index 0000000000..58c35c8ef0 --- /dev/null +++ b/packages/frontend-embed/src/components/EmAvatar.vue @@ -0,0 +1,250 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue new file mode 100644 index 0000000000..e4149cf363 --- /dev/null +++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue @@ -0,0 +1,101 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmEmoji.vue b/packages/frontend-embed/src/components/EmEmoji.vue new file mode 100644 index 0000000000..224979707b --- /dev/null +++ b/packages/frontend-embed/src/components/EmEmoji.vue @@ -0,0 +1,26 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmError.vue b/packages/frontend-embed/src/components/EmError.vue new file mode 100644 index 0000000000..d376b29a7f --- /dev/null +++ b/packages/frontend-embed/src/components/EmError.vue @@ -0,0 +1,43 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue new file mode 100644 index 0000000000..d19cd08d0a --- /dev/null +++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue @@ -0,0 +1,240 @@ + + + + + + + + + diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue new file mode 100644 index 0000000000..eeeaee528e --- /dev/null +++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue @@ -0,0 +1,87 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmLink.vue b/packages/frontend-embed/src/components/EmLink.vue new file mode 100644 index 0000000000..319ad72399 --- /dev/null +++ b/packages/frontend-embed/src/components/EmLink.vue @@ -0,0 +1,40 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue new file mode 100644 index 0000000000..49d8ace37b --- /dev/null +++ b/packages/frontend-embed/src/components/EmLoading.vue @@ -0,0 +1,112 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue new file mode 100644 index 0000000000..435da238a4 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaBanner.vue @@ -0,0 +1,55 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue new file mode 100644 index 0000000000..fe1aa5a877 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaImage.vue @@ -0,0 +1,154 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaList.vue b/packages/frontend-embed/src/components/EmMediaList.vue new file mode 100644 index 0000000000..0b2d835abe --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaList.vue @@ -0,0 +1,146 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue new file mode 100644 index 0000000000..ce751f9acd --- /dev/null +++ b/packages/frontend-embed/src/components/EmMediaVideo.vue @@ -0,0 +1,64 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue new file mode 100644 index 0000000000..5eadf828c7 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMention.vue @@ -0,0 +1,46 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts new file mode 100644 index 0000000000..7543d3cd54 --- /dev/null +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -0,0 +1,461 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { VNode, h, SetupContext, provide } from 'vue'; +import * as mfm from 'mfm-js'; +import * as Misskey from 'misskey-js'; +import EmUrl from '@/components/EmUrl.vue'; +import EmTime from '@/components/EmTime.vue'; +import EmLink from '@/components/EmLink.vue'; +import EmMention from '@/components/EmMention.vue'; +import EmEmoji from '@/components/EmEmoji.vue'; +import EmCustomEmoji from '@/components/EmCustomEmoji.vue'; +import EmA from '@/components/EmA.vue'; +import { host } from '@/config.js'; + +function safeParseFloat(str: unknown): number | null { + if (typeof str !== 'string' || str === '') return null; + const num = parseFloat(str); + if (isNaN(num)) return null; + return num; +} + +const QUOTE_STYLE = ` +display: block; +margin: 8px; +padding: 6px 0 6px 12px; +color: var(--fg); +border-left: solid 3px var(--fg); +opacity: 0.7; +`.split('\n').join(' '); + +type MfmProps = { + text: string; + plain?: boolean; + nowrap?: boolean; + author?: Misskey.entities.UserLite; + isNote?: boolean; + emojiUrls?: Record; + rootScale?: number; + nyaize?: boolean | 'respect'; + parsedNodes?: mfm.MfmNode[] | null; + enableEmojiMenu?: boolean; + enableEmojiMenuReaction?: boolean; + linkNavigationBehavior?: string; +}; + +type MfmEvents = { + clickEv(id: string): void; +}; + +// eslint-disable-next-line import/no-default-export +export default function (props: MfmProps, { emit }: { emit: SetupContext['emit'] }) { + provide('linkNavigationBehavior', props.linkNavigationBehavior); + + const isNote = props.isNote ?? true; + const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false; + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.text == null || props.text === '') return; + + const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text); + + const validTime = (t: string | boolean | null | undefined) => { + if (t == null) return null; + if (typeof t === 'boolean') return null; + return t.match(/^\-?[0-9.]+s$/) ? t : null; + }; + + const validColor = (c: unknown): string | null => { + if (typeof c !== 'string') return null; + return c.match(/^[0-9a-f]{3,6}$/i) ? c : null; + }; + + const useAnim = true; + + /** + * Gen Vue Elements from MFM AST + * @param ast MFM AST + * @param scale How times large the text is + * @param disableNyaize Whether nyaize is disabled or not + */ + const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => { + switch (token.type) { + case 'text': { + let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n'); + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + + if (!props.plain) { + const res: (VNode | string)[] = []; + for (const t of text.split('\n')) { + res.push(h('br')); + res.push(t); + } + res.shift(); + return res; + } else { + return [text.replace(/\n/g, ' ')]; + } + } + + case 'bold': { + return [h('b', genEl(token.children, scale))]; + } + + case 'strike': { + return [h('del', genEl(token.children, scale))]; + } + + case 'italic': { + return h('i', { + style: 'font-style: oblique;', + }, genEl(token.children, scale)); + } + + case 'fn': { + // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる + let style: string | undefined; + switch (token.props.name) { + case 'tada': { + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : ''); + break; + } + case 'jelly': { + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : ''); + break; + } + case 'twitch': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : ''; + break; + } + case 'shake': { + const speed = validTime(token.props.args.speed) ?? '0.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : ''; + break; + } + case 'spin': { + const direction = + token.props.args.left ? 'reverse' : + token.props.args.alternate ? 'alternate' : + 'normal'; + const anime = + token.props.args.x ? 'mfm-spinX' : + token.props.args.y ? 'mfm-spinY' : + 'mfm-spin'; + const speed = validTime(token.props.args.speed) ?? '1.5s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : ''; + break; + } + case 'jump': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : ''; + break; + } + case 'bounce': { + const speed = validTime(token.props.args.speed) ?? '0.75s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : ''; + break; + } + case 'flip': { + const transform = + (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' : + token.props.args.v ? 'scaleY(-1)' : + 'scaleX(-1)'; + style = `transform: ${transform};`; + break; + } + case 'x2': { + return h('span', { + class: 'mfm-x2', + }, genEl(token.children, scale * 2)); + } + case 'x3': { + return h('span', { + class: 'mfm-x3', + }, genEl(token.children, scale * 3)); + } + case 'x4': { + return h('span', { + class: 'mfm-x4', + }, genEl(token.children, scale * 4)); + } + case 'font': { + const family = + token.props.args.serif ? 'serif' : + token.props.args.monospace ? 'monospace' : + token.props.args.cursive ? 'cursive' : + token.props.args.fantasy ? 'fantasy' : + token.props.args.emoji ? 'emoji' : + token.props.args.math ? 'math' : + null; + if (family) style = `font-family: ${family};`; + break; + } + case 'blur': { + return h('span', { + class: '_mfm_blur_', + }, genEl(token.children, scale)); + } + case 'rainbow': { + if (!useAnim) { + return h('span', { + class: '_mfm_rainbow_fallback_', + }, genEl(token.children, scale)); + } + const speed = validTime(token.props.args.speed) ?? '1s'; + const delay = validTime(token.props.args.delay) ?? '0s'; + style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`; + break; + } + case 'sparkle': { + return genEl(token.children, scale); + } + case 'rotate': { + const degrees = safeParseFloat(token.props.args.deg) ?? 90; + style = `transform: rotate(${degrees}deg); transform-origin: center center;`; + break; + } + case 'position': { + const x = safeParseFloat(token.props.args.x) ?? 0; + const y = safeParseFloat(token.props.args.y) ?? 0; + style = `transform: translateX(${x}em) translateY(${y}em);`; + break; + } + case 'scale': { + const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5); + const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5); + style = `transform: scale(${x}, ${y});`; + scale = scale * Math.max(x, y); + break; + } + case 'fg': { + let color = validColor(token.props.args.color); + color = color ?? 'f00'; + style = `color: #${color}; overflow-wrap: anywhere;`; + break; + } + case 'bg': { + let color = validColor(token.props.args.color); + color = color ?? 'f00'; + style = `background-color: #${color}; overflow-wrap: anywhere;`; + break; + } + case 'border': { + let color = validColor(token.props.args.color); + color = color ? `#${color}` : 'var(--accent)'; + let b_style = token.props.args.style; + if ( + typeof b_style !== 'string' || + !['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset'] + .includes(b_style) + ) b_style = 'solid'; + const width = safeParseFloat(token.props.args.width) ?? 1; + const radius = safeParseFloat(token.props.args.radius) ?? 0; + style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`; + break; + } + case 'ruby': { + if (token.children.length === 1) { + const child = token.children[0]; + let text = child.type === 'text' ? child.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]); + } else { + const rt = token.children.at(-1)!; + let text = rt.type === 'text' ? rt.props.text : ''; + if (!disableNyaize && shouldNyaize) { + text = Misskey.nyaize(text); + } + return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]); + } + } + case 'unixtime': { + const child = token.children[0]; + const unixtime = parseInt(child.type === 'text' ? child.props.text : ''); + return h('span', { + style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;', + }, [ + h('i', { + class: 'ti ti-clock', + style: 'margin-right: 0.25em;', + }), + h(EmTime, { + key: Math.random(), + time: unixtime * 1000, + mode: 'detail', + }), + ]); + } + case 'clickable': { + return h('span', { onClick(ev: MouseEvent): void { + ev.stopPropagation(); + ev.preventDefault(); + const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : ''; + emit('clickEv', clickEv); + } }, genEl(token.children, scale)); + } + } + if (style === undefined) { + return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']); + } else { + return h('span', { + style: 'display: inline-block; ' + style, + }, genEl(token.children, scale)); + } + } + + case 'small': { + return [h('small', { + style: 'opacity: 0.7;', + }, genEl(token.children, scale))]; + } + + case 'center': { + return [h('div', { + style: 'text-align:center;', + }, genEl(token.children, scale))]; + } + + case 'url': { + return [h(EmUrl, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + })]; + } + + case 'link': { + return [h(EmLink, { + key: Math.random(), + url: token.props.url, + rel: 'nofollow noopener', + }, genEl(token.children, scale, true))]; + } + + case 'mention': { + return [h(EmMention, { + key: Math.random(), + host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host, + username: token.props.username, + })]; + } + + case 'hashtag': { + return [h(EmA, { + key: Math.random(), + to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`, + style: 'color:var(--hashtag);', + }, `#${token.props.hashtag}`)]; + } + + case 'blockCode': { + return [h('code', { + key: Math.random(), + lang: token.props.lang ?? undefined, + }, token.props.code)]; + } + + case 'inlineCode': { + return [h('code', { + key: Math.random(), + }, token.props.code)]; + } + + case 'quote': { + if (!props.nowrap) { + return [h('div', { + style: QUOTE_STYLE, + }, genEl(token.children, scale, true))]; + } else { + return [h('span', { + style: QUOTE_STYLE, + }, genEl(token.children, scale, true))]; + } + } + + case 'emojiCode': { + if (props.author?.host == null) { + return [h(EmCustomEmoji, { + key: Math.random(), + name: token.props.name, + normal: props.plain, + host: null, + useOriginalSize: scale >= 2.5, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, + fallbackToImage: false, + })]; + } else { + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) { + return [h('span', `:${token.props.name}:`)]; + } else { + return [h(EmCustomEmoji, { + key: Math.random(), + name: token.props.name, + url: props.emojiUrls && props.emojiUrls[token.props.name], + normal: props.plain, + host: props.author.host, + useOriginalSize: scale >= 2.5, + })]; + } + } + } + + case 'unicodeEmoji': { + return [h(EmEmoji, { + key: Math.random(), + emoji: token.props.emoji, + menu: props.enableEmojiMenu, + menuReaction: props.enableEmojiMenuReaction, + })]; + } + + case 'mathInline': { + return [h('code', token.props.formula)]; + } + + case 'mathBlock': { + return [h('code', token.props.formula)]; + } + + case 'search': { + return [h('div', { + key: Math.random(), + }, token.props.query)]; + } + + case 'plain': { + return [h('span', genEl(token.children, scale, true))]; + } + + default: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + console.error('unrecognized ast type:', (token as any).type); + + return []; + } + } + }).flat(Infinity) as (VNode | string)[]; + + return h('span', { + // https://codeday.me/jp/qa/20190424/690106.html + style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;', + }, genEl(rootAst, props.rootScale ?? 1)); +} diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue new file mode 100644 index 0000000000..7c4d591066 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNote.vue @@ -0,0 +1,609 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue new file mode 100644 index 0000000000..74a26856c8 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -0,0 +1,486 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue new file mode 100644 index 0000000000..e4add9501f --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteHeader.vue @@ -0,0 +1,104 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue new file mode 100644 index 0000000000..828b6cd2e2 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteSimple.vue @@ -0,0 +1,105 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue new file mode 100644 index 0000000000..c98b956805 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNoteSub.vue @@ -0,0 +1,149 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue new file mode 100644 index 0000000000..3970d05098 --- /dev/null +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -0,0 +1,48 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue new file mode 100644 index 0000000000..5d5317a912 --- /dev/null +++ b/packages/frontend-embed/src/components/EmPagination.vue @@ -0,0 +1,504 @@ + + + + + + + + diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue new file mode 100644 index 0000000000..a2b1203449 --- /dev/null +++ b/packages/frontend-embed/src/components/EmPoll.vue @@ -0,0 +1,82 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmReactionIcon.vue b/packages/frontend-embed/src/components/EmReactionIcon.vue new file mode 100644 index 0000000000..5c38ecb0ed --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionIcon.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue new file mode 100644 index 0000000000..2e43eb8d17 --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue @@ -0,0 +1,99 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.vue b/packages/frontend-embed/src/components/EmReactionsViewer.vue new file mode 100644 index 0000000000..014dd1c935 --- /dev/null +++ b/packages/frontend-embed/src/components/EmReactionsViewer.vue @@ -0,0 +1,104 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue new file mode 100644 index 0000000000..382e39e492 --- /dev/null +++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue @@ -0,0 +1,113 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue new file mode 100644 index 0000000000..a8627e02c8 --- /dev/null +++ b/packages/frontend-embed/src/components/EmTime.vue @@ -0,0 +1,107 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue new file mode 100644 index 0000000000..6c30b1102d --- /dev/null +++ b/packages/frontend-embed/src/components/EmTimelineContainer.vue @@ -0,0 +1,39 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue new file mode 100644 index 0000000000..a96bfdb493 --- /dev/null +++ b/packages/frontend-embed/src/components/EmUrl.vue @@ -0,0 +1,96 @@ + + + + + + + diff --git a/packages/frontend-embed/src/components/EmUserName.vue b/packages/frontend-embed/src/components/EmUserName.vue new file mode 100644 index 0000000000..c0c7c443ca --- /dev/null +++ b/packages/frontend-embed/src/components/EmUserName.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue new file mode 100644 index 0000000000..b621110ec9 --- /dev/null +++ b/packages/frontend-embed/src/components/I18n.vue @@ -0,0 +1,51 @@ + + + + + diff --git a/packages/frontend-embed/src/config.ts b/packages/frontend-embed/src/config.ts new file mode 100644 index 0000000000..f9850ba461 --- /dev/null +++ b/packages/frontend-embed/src/config.ts @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const address = new URL(document.querySelector('meta[property="instance_url"]')?.content || location.href); +const siteName = document.querySelector('meta[property="og:site_name"]')?.content; + +export const host = address.host; +export const hostname = address.hostname; +export const url = address.origin; +export const apiUrl = location.origin + '/api'; +export const lang = localStorage.getItem('lang') ?? 'en-US'; +export const langs = _LANGS_; +const preParseLocale = localStorage.getItem('locale'); +export const locale = preParseLocale ? JSON.parse(preParseLocale) : null; +export const instanceName = siteName === 'Misskey' || siteName == null ? host : siteName; +export const debug = localStorage.getItem('debug') === 'true'; diff --git a/packages/frontend-embed/src/custom-emojis.ts b/packages/frontend-embed/src/custom-emojis.ts new file mode 100644 index 0000000000..d5b40885c1 --- /dev/null +++ b/packages/frontend-embed/src/custom-emojis.ts @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { shallowRef, watch } from 'vue'; +import * as Misskey from 'misskey-js'; +import { misskeyApi, misskeyApiGet } from '@/misskey-api.js'; + +function get(key: string) { + const value = localStorage.getItem(key); + if (value === null) return null; + return JSON.parse(value); +} + +function set(key: string, value: any) { + localStorage.setItem(key, JSON.stringify(value)); +} + +const storageCache = await get('emojis'); +export const customEmojis = shallowRef(Array.isArray(storageCache) ? storageCache : []); + +export const customEmojisMap = new Map(); +watch(customEmojis, emojis => { + customEmojisMap.clear(); + for (const emoji of emojis) { + customEmojisMap.set(emoji.name, emoji); + } +}, { immediate: true }); + +export async function fetchCustomEmojis(force = false) { + const now = Date.now(); + + let res; + if (force) { + res = await misskeyApi('emojis', {}); + } else { + const lastFetchedAt = await get('lastEmojisFetchedAt'); + if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return; + res = await misskeyApiGet('emojis', {}); + } + + customEmojis.value = res.emojis; + set('emojis', res.emojis); + set('lastEmojisFetchedAt', now); +} + +let cachedTags; +export function getCustomEmojiTags() { + if (cachedTags) return cachedTags; + + const tags = new Set(); + for (const emoji of customEmojis.value) { + for (const tag of emoji.aliases) { + tags.add(tag); + } + } + const res = Array.from(tags); + cachedTags = res; + return res; +} diff --git a/packages/frontend-embed/src/di.ts b/packages/frontend-embed/src/di.ts new file mode 100644 index 0000000000..799bbed598 --- /dev/null +++ b/packages/frontend-embed/src/di.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import type { InjectionKey } from 'vue'; +import * as Misskey from 'misskey-js'; +import { MediaProxy } from '@@/js/media-proxy.js'; +import type { ParsedEmbedParams } from '@@/js/embed-page.js'; + +export const DI = { + serverMetadata: Symbol() as InjectionKey, + embedParams: Symbol() as InjectionKey, + mediaProxy: Symbol() as InjectionKey, +}; diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts new file mode 100644 index 0000000000..17e787f9fc --- /dev/null +++ b/packages/frontend-embed/src/i18n.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { markRaw } from 'vue'; +import { I18n } from '@@/js/i18n.js'; +import type { Locale } from '../../../locales/index.js'; +import { locale } from '@/config.js'; + +export const i18n = markRaw(new I18n(locale, _DEV_)); + +export function updateI18n(newLocale: Locale) { + i18n.locale = newLocale; +} diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html new file mode 100644 index 0000000000..47b0b0e84e --- /dev/null +++ b/packages/frontend-embed/src/index.html @@ -0,0 +1,36 @@ + + + + + + + + + [DEV] Loading... + + + + + + + +
+ + + diff --git a/packages/frontend-embed/src/misskey-api.ts b/packages/frontend-embed/src/misskey-api.ts new file mode 100644 index 0000000000..13630590b6 --- /dev/null +++ b/packages/frontend-embed/src/misskey-api.ts @@ -0,0 +1,99 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { ref } from 'vue'; +import { apiUrl } from '@/config.js'; + +export const pendingApiRequestsCount = ref(0); + +// Implements Misskey.api.ApiClient.request +export function misskeyApi< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, +>( + endpoint: E, + data: P = {} as any, + signal?: AbortSignal, +): Promise<_ResT> { + if (endpoint.includes('://')) throw new Error('invalid endpoint'); + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const promise = new Promise<_ResT>((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: 'omit', + cache: 'no-cache', + headers: { + 'Content-Type': 'application/json', + }, + signal, + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(undefined as _ResT); // void -> undefined + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} + +// Implements Misskey.api.ApiClient.request +export function misskeyApiGet< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT, +>( + endpoint: E, + data: P = {} as any, +): Promise<_ResT> { + pendingApiRequestsCount.value++; + + const onFinally = () => { + pendingApiRequestsCount.value--; + }; + + const query = new URLSearchParams(data as any); + + const promise = new Promise<_ResT>((resolve, reject) => { + // Send request + window.fetch(`${apiUrl}/${endpoint}?${query}`, { + method: 'GET', + credentials: 'omit', + cache: 'default', + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(undefined as _ResT); // void -> undefined + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; +} diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue new file mode 100644 index 0000000000..6564eecd75 --- /dev/null +++ b/packages/frontend-embed/src/pages/clip.vue @@ -0,0 +1,140 @@ + + + + + + + diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue new file mode 100644 index 0000000000..bbb03b4e64 --- /dev/null +++ b/packages/frontend-embed/src/pages/not-found.vue @@ -0,0 +1,24 @@ + + + + + diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue new file mode 100644 index 0000000000..86aebe072a --- /dev/null +++ b/packages/frontend-embed/src/pages/note.vue @@ -0,0 +1,48 @@ + + + + + + + diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue new file mode 100644 index 0000000000..d69555287a --- /dev/null +++ b/packages/frontend-embed/src/pages/tag.vue @@ -0,0 +1,125 @@ + + + + + + + diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue new file mode 100644 index 0000000000..d590f6e650 --- /dev/null +++ b/packages/frontend-embed/src/pages/user-timeline.vue @@ -0,0 +1,138 @@ + + + + + + + diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts new file mode 100644 index 0000000000..fd8eb8a5d2 --- /dev/null +++ b/packages/frontend-embed/src/post-message.ts @@ -0,0 +1,49 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export const postMessageEventTypes = [ + 'misskey:embed:ready', + 'misskey:embed:changeHeight', +] as const; + +export type PostMessageEventType = typeof postMessageEventTypes[number]; + +export interface PostMessageEventPayload extends Record { + 'misskey:embed:ready': undefined; + 'misskey:embed:changeHeight': { + height: number; + }; +} + +export type MiPostMessageEvent = { + type: T; + iframeId?: string; + payload?: PostMessageEventPayload[T]; +} + +let defaultIframeId: string | null = null; + +export function setIframeId(id: string): void { + if (defaultIframeId != null) return; + + if (_DEV_) console.log('setIframeId', id); + defaultIframeId = id; +} + +/** + * 親フレームにイベントを送信 + */ +export function postMessageToParentWindow(type: T, payload?: PostMessageEventPayload[T], iframeId: string | null = null): void { + let _iframeId = iframeId; + if (_iframeId == null) { + _iframeId = defaultIframeId; + } + if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload); + window.parent.postMessage({ + type, + iframeId: _iframeId, + payload, + }, '*'); +} diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts new file mode 100644 index 0000000000..2bd57a0990 --- /dev/null +++ b/packages/frontend-embed/src/server-metadata.ts @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { misskeyApi } from '@/misskey-api.js'; + +const providedMetaEl = document.getElementById('misskey_meta'); + +const _serverMetadata = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null; + +// NOTE: devモードのときしか _serverMetadata が null になることは無い +export const serverMetadata = _serverMetadata ?? await misskeyApi('meta', { + detail: true, +}); diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss new file mode 100644 index 0000000000..02008ddbd0 --- /dev/null +++ b/packages/frontend-embed/src/style.scss @@ -0,0 +1,453 @@ +@charset "utf-8"; + +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +:root { + --radius: 12px; + --marginFull: 14px; + --marginHalf: 10px; + + --margin: var(--marginFull); +} + +html { + background-color: transparent; + color-scheme: light dark; + color: var(--fg); + accent-color: var(--accent); + overflow: clip; + overflow-wrap: break-word; + font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; + font-size: 14px; + line-height: 1.35; + text-size-adjust: 100%; + tab-size: 2; + -webkit-text-size-adjust: 100%; + + &, * { + scrollbar-color: var(--scrollbarHandle) transparent; + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-track { + background: inherit; + } + + &::-webkit-scrollbar-thumb { + background: var(--scrollbarHandle); + + &:hover { + background: var(--scrollbarHandleHover); + } + + &:active { + background: var(--accent); + } + } + } +} + +html, body { + height: 100%; + touch-action: manipulation; + margin: 0; + padding: 0; + scroll-behavior: smooth; +} + +#misskey_app { + height: 100%; +} + +a { + text-decoration: none; + cursor: pointer; + color: inherit; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + -webkit-touch-callout: none; + + &:focus-visible { + outline-offset: 2px; + } + + &:hover { + text-decoration: underline; + } + + &[target="_blank"] { + -webkit-touch-callout: default; + } +} + +rt { + white-space: initial; +} + +:focus-visible { + outline: var(--focus) solid 2px; + outline-offset: -2px; + + &:hover { + text-decoration: none; + } +} + +.ti { + width: 1.28em; + vertical-align: -12%; + line-height: 1em; + + &::before { + font-size: 128%; + } +} + +.ti-fw { + display: inline-block; + text-align: center; +} + +._nowrap { + white-space: pre !important; + word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html + overflow: hidden; + text-overflow: ellipsis; +} + +._button { + user-select: none; + -webkit-user-select: none; + -webkit-touch-callout: none; + appearance: none; + display: inline-block; + padding: 0; + margin: 0; // for Safari + background: none; + border: none; + cursor: pointer; + color: inherit; + touch-action: manipulation; + tap-highlight-color: transparent; + -webkit-tap-highlight-color: transparent; + font-size: 1em; + font-family: inherit; + line-height: inherit; + max-width: 100%; + + &:disabled { + opacity: 0.5; + cursor: default; + } +} + +._buttonGray { + @extend ._button; + background: var(--buttonBg); + + &:not(:disabled):hover { + background: var(--buttonHoverBg); + } +} + +._buttonPrimary { + @extend ._button; + color: var(--fgOnAccent); + background: var(--accent); + + &:not(:disabled):hover { + background: hsl(from var(--accent) h s calc(l + 5)); + } + + &:not(:disabled):active { + background: hsl(from var(--accent) h s calc(l - 5)); + } +} + +._buttonGradate { + @extend ._buttonPrimary; + color: var(--fgOnAccent); + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + + &:not(:disabled):hover { + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + } + + &:not(:disabled):active { + background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5))); + } +} + +._buttonRounded { + font-size: 0.95em; + padding: 0.5em 1em; + min-width: 100px; + border-radius: 99rem; + + &._buttonPrimary, + &._buttonGradate { + font-weight: 700; + } +} + +._help { + color: var(--accent); + cursor: help; +} + +._textButton { + @extend ._button; + color: var(--accent); + + &:focus-visible { + outline-offset: 2px; + } + + &:not(:disabled):hover { + text-decoration: underline; + } +} + +._panel { + background: var(--panel); + border-radius: var(--radius); + overflow: clip; +} + +._margin { + margin: var(--margin) 0; +} + +._gaps_m { + display: flex; + flex-direction: column; + gap: 1.5em; +} + +._gaps_s { + display: flex; + flex-direction: column; + gap: 0.75em; +} + +._gaps { + display: flex; + flex-direction: column; + gap: var(--margin); +} + +._buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +._buttonsCenter { + @extend ._buttons; + + justify-content: center; +} + +._borderButton { + @extend ._button; + display: block; + width: 100%; + padding: 10px; + box-sizing: border-box; + text-align: center; + border: solid 0.5px var(--divider); + border-radius: var(--radius); + + &:active { + border-color: var(--accent); + } +} + +._popup { + background: var(--popup); + border-radius: var(--radius); + contain: content; +} + +._acrylic { + background: var(--acrylicPanel); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); +} + +._fullinfo { + padding: 64px 32px; + text-align: center; + + > img { + vertical-align: bottom; + height: 128px; + margin-bottom: 16px; + border-radius: 16px; + } +} + +._link { + color: var(--link); +} + +._caption { + font-size: 0.8em; + opacity: 0.7; +} + +._monospace { + font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important; +} + +// MFM ----------------------------- + +._mfm_blur_ { + filter: blur(6px); + transition: filter 0.3s; + + &:hover { + filter: blur(0px); + } +} + +.mfm-x2 { + --mfm-zoom-size: 200%; +} + +.mfm-x3 { + --mfm-zoom-size: 400%; +} + +.mfm-x4 { + --mfm-zoom-size: 600%; +} + +.mfm-x2, .mfm-x3, .mfm-x4 { + font-size: var(--mfm-zoom-size); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* only half effective */ + font-size: calc(var(--mfm-zoom-size) / 2 + 50%); + + .mfm-x2, .mfm-x3, .mfm-x4 { + /* disabled */ + font-size: 100%; + } + } +} + +._mfm_rainbow_fallback_ { + background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} + +@keyframes mfm-spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +@keyframes mfm-spinX { + 0% { transform: perspective(128px) rotateX(0deg); } + 100% { transform: perspective(128px) rotateX(360deg); } +} + +@keyframes mfm-spinY { + 0% { transform: perspective(128px) rotateY(0deg); } + 100% { transform: perspective(128px) rotateY(360deg); } +} + +@keyframes mfm-jump { + 0% { transform: translateY(0); } + 25% { transform: translateY(-16px); } + 50% { transform: translateY(0); } + 75% { transform: translateY(-8px); } + 100% { transform: translateY(0); } +} + +@keyframes mfm-bounce { + 0% { transform: translateY(0) scale(1, 1); } + 25% { transform: translateY(-16px) scale(1, 1); } + 50% { transform: translateY(0) scale(1, 1); } + 75% { transform: translateY(0) scale(1.5, 0.75); } + 100% { transform: translateY(0) scale(1, 1); } +} + +// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-twitch { + 0% { transform: translate(7px, -2px) } + 5% { transform: translate(-3px, 1px) } + 10% { transform: translate(-7px, -1px) } + 15% { transform: translate(0px, -1px) } + 20% { transform: translate(-8px, 6px) } + 25% { transform: translate(-4px, -3px) } + 30% { transform: translate(-4px, -6px) } + 35% { transform: translate(-8px, -8px) } + 40% { transform: translate(4px, 6px) } + 45% { transform: translate(-3px, 1px) } + 50% { transform: translate(2px, -10px) } + 55% { transform: translate(-7px, 0px) } + 60% { transform: translate(-2px, 4px) } + 65% { transform: translate(3px, -8px) } + 70% { transform: translate(6px, 7px) } + 75% { transform: translate(-7px, -2px) } + 80% { transform: translate(-7px, -8px) } + 85% { transform: translate(9px, 3px) } + 90% { transform: translate(-3px, -2px) } + 95% { transform: translate(-10px, 2px) } + 100% { transform: translate(-2px, -6px) } +} + +// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`; +// let css = ''; +// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; } +@keyframes mfm-shake { + 0% { transform: translate(-3px, -1px) rotate(-8deg) } + 5% { transform: translate(0px, -1px) rotate(-10deg) } + 10% { transform: translate(1px, -3px) rotate(0deg) } + 15% { transform: translate(1px, 1px) rotate(11deg) } + 20% { transform: translate(-2px, 1px) rotate(1deg) } + 25% { transform: translate(-1px, -2px) rotate(-2deg) } + 30% { transform: translate(-1px, 2px) rotate(-3deg) } + 35% { transform: translate(2px, 1px) rotate(6deg) } + 40% { transform: translate(-2px, -3px) rotate(-9deg) } + 45% { transform: translate(0px, -1px) rotate(-12deg) } + 50% { transform: translate(1px, 2px) rotate(10deg) } + 55% { transform: translate(0px, -3px) rotate(8deg) } + 60% { transform: translate(1px, -1px) rotate(8deg) } + 65% { transform: translate(0px, -1px) rotate(-7deg) } + 70% { transform: translate(-1px, -3px) rotate(6deg) } + 75% { transform: translate(0px, -2px) rotate(4deg) } + 80% { transform: translate(-2px, -1px) rotate(3deg) } + 85% { transform: translate(1px, -3px) rotate(-10deg) } + 90% { transform: translate(1px, 0px) rotate(3deg) } + 95% { transform: translate(-2px, 0px) rotate(-3deg) } + 100% { transform: translate(2px, 1px) rotate(2deg) } +} + +@keyframes mfm-rubberBand { + from { transform: scale3d(1, 1, 1); } + 30% { transform: scale3d(1.25, 0.75, 1); } + 40% { transform: scale3d(0.75, 1.25, 1); } + 50% { transform: scale3d(1.15, 0.85, 1); } + 65% { transform: scale3d(0.95, 1.05, 1); } + 75% { transform: scale3d(1.05, 0.95, 1); } + to { transform: scale3d(1, 1, 1); } +} + +@keyframes mfm-rainbow { + 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); } + 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); } +} diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts new file mode 100644 index 0000000000..050d8cf63b --- /dev/null +++ b/packages/frontend-embed/src/theme.ts @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; +import type { BundledTheme } from 'shiki/themes'; + +export type Theme = { + id: string; + name: string; + author: string; + desc?: string; + base?: 'dark' | 'light'; + props: Record; + codeHighlighter?: { + base: BundledTheme; + overrides?: Record; + } | { + base: '_none_'; + overrides: Record; + }; +}; + +let timeout: number | null = null; + +export function applyTheme(theme: Theme, persist = true) { + if (timeout) window.clearTimeout(timeout); + + document.documentElement.classList.add('_themeChanging_'); + + timeout = window.setTimeout(() => { + document.documentElement.classList.remove('_themeChanging_'); + }, 1000); + + const colorScheme = theme.base === 'dark' ? 'dark' : 'light'; + + // Deep copy + const _theme = JSON.parse(JSON.stringify(theme)); + + if (_theme.base) { + const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); + if (base) _theme.props = Object.assign({}, base.props, _theme.props); + } + + const props = compile(_theme); + + for (const tag of document.head.children) { + if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') { + tag.setAttribute('content', props['htmlThemeColor']); + break; + } + } + + for (const [k, v] of Object.entries(props)) { + document.documentElement.style.setProperty(`--${k}`, v.toString()); + } + + document.documentElement.style.setProperty('color-scheme', colorScheme); +} + +function compile(theme: Theme): Record { + function getColor(val: string): tinycolor.Instance { + if (val[0] === '@') { // ref (prop) + return getColor(theme.props[val.substring(1)]); + } else if (val[0] === '$') { // ref (const) + return getColor(theme.props[val]); + } else if (val[0] === ':') { // func + const parts = val.split('<'); + const func = parts.shift().substring(1); + const arg = parseFloat(parts.shift()); + const color = getColor(parts.join('<')); + + switch (func) { + case 'darken': return color.darken(arg); + case 'lighten': return color.lighten(arg); + case 'alpha': return color.setAlpha(arg); + case 'hue': return color.spin(arg); + case 'saturate': return color.saturate(arg); + } + } + + // other case + return tinycolor(val); + } + + const props = {}; + + for (const [k, v] of Object.entries(theme.props)) { + if (k.startsWith('$')) continue; // ignore const + + props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v)); + } + + return props; +} + +function genValue(c: tinycolor.Instance): string { + return c.toRgbString(); +} diff --git a/packages/frontend-embed/src/to-be-shared/collapsed.ts b/packages/frontend-embed/src/to-be-shared/collapsed.ts new file mode 100644 index 0000000000..4ec88a3c65 --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/collapsed.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; + +export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean { + const collapsed = note.cw == null && ( + note.text != null && ( + (note.text.includes('$[x2')) || + (note.text.includes('$[x3')) || + (note.text.includes('$[x4')) || + (note.text.includes('$[scale')) || + (note.text.split('\n').length > 9) || + (note.text.length > 500) || + (urls.length >= 4) + ) || note.files.length >= 5 + ); + + return collapsed; +} diff --git a/packages/frontend-embed/src/to-be-shared/intl-const.ts b/packages/frontend-embed/src/to-be-shared/intl-const.ts new file mode 100644 index 0000000000..aaa4f0a86e --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/intl-const.ts @@ -0,0 +1,50 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { lang } from '@/config.js'; + +export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP'); + +let _dateTimeFormat: Intl.DateTimeFormat; +try { + _dateTimeFormat = new Intl.DateTimeFormat(versatileLang, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }); +} catch (err) { + console.warn(err); + if (_DEV_) console.log('[Intl] Fallback to en-US'); + + // Fallback to en-US + _dateTimeFormat = new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }); +} +export const dateTimeFormat = _dateTimeFormat; + +export const timeZone = dateTimeFormat.resolvedOptions().timeZone; + +export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N'; + +let _numberFormat: Intl.NumberFormat; +try { + _numberFormat = new Intl.NumberFormat(versatileLang); +} catch (err) { + console.warn(err); + if (_DEV_) console.log('[Intl] Fallback to en-US'); + + // Fallback to en-US + _numberFormat = new Intl.NumberFormat('en-US'); +} +export const numberFormat = _numberFormat; diff --git a/packages/frontend-embed/src/to-be-shared/is-link.ts b/packages/frontend-embed/src/to-be-shared/is-link.ts new file mode 100644 index 0000000000..946f86400e --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/is-link.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isLink(el: HTMLElement) { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; +} diff --git a/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts new file mode 100644 index 0000000000..6b3fcd9383 --- /dev/null +++ b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts @@ -0,0 +1,82 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +function defaultUseWorkerNumber(prev: number, totalWorkers: number) { + return prev + 1; +} + +export class WorkerMultiDispatch { + private symbol = Symbol('WorkerMultiDispatch'); + private workers: Worker[] = []; + private terminated = false; + private prevWorkerNumber = 0; + private getUseWorkerNumber = defaultUseWorkerNumber; + private finalizationRegistry: FinalizationRegistry; + + constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) { + this.getUseWorkerNumber = getUseWorkerNumber; + for (let i = 0; i < concurrency; i++) { + this.workers.push(workerConstructor()); + } + + this.finalizationRegistry = new FinalizationRegistry(() => { + this.terminate(); + }); + this.finalizationRegistry.register(this, this.symbol); + + if (_DEV_) console.log('WorkerMultiDispatch: Created', this); + } + + public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) { + let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length); + workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length; + if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber); + this.prevWorkerNumber = workerNumber; + + // 不毛だがunionをoverloadに突っ込めない + // https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error + // https://github.com/microsoft/TypeScript/issues/14107 + if (Array.isArray(options)) { + this.workers[workerNumber].postMessage(message, options); + } else { + this.workers[workerNumber].postMessage(message, options); + } + return workerNumber; + } + + public addListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { + this.workers.forEach(worker => { + worker.addEventListener('message', callback, options); + }); + } + + public removeListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) { + this.workers.forEach(worker => { + worker.removeEventListener('message', callback, options); + }); + } + + public terminate() { + this.terminated = true; + if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this); + this.workers.forEach(worker => { + worker.terminate(); + }); + this.workers = []; + this.finalizationRegistry.unregister(this); + } + + public isTerminated() { + return this.terminated; + } + + public getWorkers() { + return this.workers; + } + + public getSymbol() { + return this.symbol; + } +} diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue new file mode 100644 index 0000000000..3b8449dac8 --- /dev/null +++ b/packages/frontend-embed/src/ui.vue @@ -0,0 +1,96 @@ + + + + + + + diff --git a/packages/frontend-embed/src/utils.ts b/packages/frontend-embed/src/utils.ts new file mode 100644 index 0000000000..9a2fd0beef --- /dev/null +++ b/packages/frontend-embed/src/utils.ts @@ -0,0 +1,23 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { url } from '@/config.js'; + +export const acct = (user: Misskey.Acct) => { + return Misskey.acct.toString(user); +}; + +export const userName = (user: Misskey.entities.User) => { + return user.name || user.username; +}; + +export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => { + return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`; +}; + +export const notePage = note => { + return `/notes/${note.id}`; +}; diff --git a/packages/frontend-embed/src/workers/draw-blurhash.ts b/packages/frontend-embed/src/workers/draw-blurhash.ts new file mode 100644 index 0000000000..22de6cd3a8 --- /dev/null +++ b/packages/frontend-embed/src/workers/draw-blurhash.ts @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { render } from 'buraha'; + +const canvas = new OffscreenCanvas(64, 64); + +onmessage = (event) => { + // console.log(event.data); + if (!('id' in event.data && typeof event.data.id === 'string')) { + return; + } + if (!('hash' in event.data && typeof event.data.hash === 'string')) { + return; + } + + render(event.data.hash, canvas); + const bitmap = canvas.transferToImageBitmap(); + postMessage({ id: event.data.id, bitmap }); +}; diff --git a/packages/frontend-embed/src/workers/test-webgl2.ts b/packages/frontend-embed/src/workers/test-webgl2.ts new file mode 100644 index 0000000000..b203ebe666 --- /dev/null +++ b/packages/frontend-embed/src/workers/test-webgl2.ts @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const canvas = globalThis.OffscreenCanvas && new OffscreenCanvas(1, 1); +// 環境によってはOffscreenCanvasが存在しないため +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +const gl = canvas?.getContext('webgl2'); +if (gl) { + postMessage({ result: true }); +} else { + postMessage({ result: false }); +} diff --git a/packages/frontend-embed/src/workers/tsconfig.json b/packages/frontend-embed/src/workers/tsconfig.json new file mode 100644 index 0000000000..8ee8930465 --- /dev/null +++ b/packages/frontend-embed/src/workers/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["esnext", "webworker"], + } +} diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json new file mode 100644 index 0000000000..3701343623 --- /dev/null +++ b/packages/frontend-embed/tsconfig.json @@ -0,0 +1,53 @@ +{ + "compilerOptions": { + "allowJs": true, + "noEmitOnError": false, + "noImplicitAny": false, + "noImplicitReturns": true, + "noUnusedParameters": false, + "noUnusedLocals": false, + "noFallthroughCasesInSwitch": true, + "declaration": false, + "sourceMap": false, + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "removeComments": false, + "noLib": false, + "strict": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@@/*": ["../frontend-shared/*"] + }, + "typeRoots": [ + "./@types", + "./node_modules/@types", + "./node_modules/@vue-macros", + "./node_modules" + ], + "types": [ + "vite/client", + ], + "lib": [ + "esnext", + "dom", + "dom.iterable" + ], + "jsx": "preserve" + }, + "compileOnSave": false, + "include": [ + "./**/*.ts", + "./**/*.vue" + ], + "exclude": [ + ".storybook/**/*" + ] +} diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts new file mode 100644 index 0000000000..bf2f478887 --- /dev/null +++ b/packages/frontend-embed/vite.config.local-dev.ts @@ -0,0 +1,96 @@ +import dns from 'dns'; +import { readFile } from 'node:fs/promises'; +import type { IncomingMessage } from 'node:http'; +import { defineConfig } from 'vite'; +import type { UserConfig } from 'vite'; +import * as yaml from 'js-yaml'; +import locales from '../../locales/index.js'; +import { getConfig } from './vite.config.js'; + +dns.setDefaultResultOrder('ipv4first'); + +const defaultConfig = getConfig(); + +const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8')); + +const httpUrl = `http://localhost:${port}/`; +const websocketUrl = `ws://localhost:${port}/`; + +// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す +function varyHandler(req: IncomingMessage) { + if (req.headers.accept?.includes('application/activity+json')) { + return null; + } + return '/index.html'; +} + +const devConfig: UserConfig = { + // 基本の設定は vite.config.js から引き継ぐ + ...defaultConfig, + root: 'src', + publicDir: '../assets', + base: '/embed', + server: { + host: 'localhost', + port: 5174, + proxy: { + '/api': { + changeOrigin: true, + target: httpUrl, + }, + '/assets': httpUrl, + '/static-assets': httpUrl, + '/client-assets': httpUrl, + '/files': httpUrl, + '/twemoji': httpUrl, + '/fluent-emoji': httpUrl, + '/sw.js': httpUrl, + '/streaming': { + target: websocketUrl, + ws: true, + }, + '/favicon.ico': httpUrl, + '/robots.txt': httpUrl, + '/embed.js': httpUrl, + '/identicon': { + target: httpUrl, + rewrite(path) { + return path.replace('@localhost:5173', ''); + }, + }, + '/url': httpUrl, + '/proxy': httpUrl, + '/_info_card_': httpUrl, + '/bios': httpUrl, + '/cli': httpUrl, + '/inbox': httpUrl, + '/emoji/': httpUrl, + '/notes': { + target: httpUrl, + bypass: varyHandler, + }, + '/users': { + target: httpUrl, + bypass: varyHandler, + }, + '/.well-known': { + target: httpUrl, + }, + }, + }, + build: { + ...defaultConfig.build, + rollupOptions: { + ...defaultConfig.build?.rollupOptions, + input: 'index.html', + }, + }, + + define: { + ...defaultConfig.define, + _LANGS_FULL_: JSON.stringify(Object.entries(locales)), + }, +}; + +export default defineConfig(({ command, mode }) => devConfig); + diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts new file mode 100644 index 0000000000..64e67401c2 --- /dev/null +++ b/packages/frontend-embed/vite.config.ts @@ -0,0 +1,156 @@ +import path from 'path'; +import pluginVue from '@vitejs/plugin-vue'; +import { type UserConfig, defineConfig } from 'vite'; + +import locales from '../../locales/index.js'; +import meta from '../../package.json'; +import packageInfo from './package.json' with { type: 'json' }; +import pluginJson5 from './vite.json5.js'; + +const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue']; + +/** + * Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。 + * CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK + */ +const externalPackages = [ + // shiki(コードブロックのシンタックスハイライトで使用中)はテーマ・言語の定義の容量が大きいため、それらはCDNから読み込む + { + name: 'shiki', + match: /^shiki\/(?(langs|themes))$/, + path(id: string, pattern: RegExp): string { + const match = pattern.exec(id)?.groups; + return match + ? `https://esm.sh/shiki@${packageInfo.dependencies.shiki}/${match['subPkg']}` + : id; + }, + }, +]; + +const hash = (str: string, seed = 0): number => { + let h1 = 0xdeadbeef ^ seed, + h2 = 0x41c6ce57 ^ seed; + for (let i = 0, ch; i < str.length; i++) { + ch = str.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909); + + return 4294967296 * (2097151 & h2) + (h1 >>> 0); +}; + +const BASE62_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + +function toBase62(n: number): string { + if (n === 0) { + return '0'; + } + let result = ''; + while (n > 0) { + result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result; + n = Math.floor(n / BASE62_DIGITS.length); + } + + return result; +} + +export function getConfig(): UserConfig { + return { + base: '/embed_vite/', + + server: { + port: 5174, + }, + + plugins: [ + pluginVue(), + pluginJson5(), + ], + + resolve: { + extensions, + alias: { + '@/': __dirname + '/src/', + '@@/': __dirname + '/../frontend-shared/', + '/client-assets/': __dirname + '/assets/', + '/static-assets/': __dirname + '/../backend/assets/' + }, + }, + + css: { + modules: { + generateScopedName(name, filename, _css): string { + const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, ''); + if (process.env.NODE_ENV === 'production') { + return 'x' + toBase62(hash(id)).substring(0, 4); + } else { + return id; + } + }, + }, + }, + + define: { + _VERSION_: JSON.stringify(meta.version), + _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])), + _ENV_: JSON.stringify(process.env.NODE_ENV), + _DEV_: process.env.NODE_ENV !== 'production', + _PERF_PREFIX_: JSON.stringify('Misskey:'), + __VUE_OPTIONS_API__: false, + __VUE_PROD_DEVTOOLS__: false, + }, + + build: { + target: [ + 'chrome116', + 'firefox116', + 'safari16', + ], + manifest: 'manifest.json', + rollupOptions: { + input: { + app: './src/boot.ts', + }, + external: externalPackages.map(p => p.match), + output: { + manualChunks: { + vue: ['vue'], + }, + chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js', + assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]', + paths(id) { + for (const p of externalPackages) { + if (p.match.test(id)) { + return p.path(id, p.match); + } + } + + return id; + }, + }, + }, + cssCodeSplit: true, + outDir: __dirname + '/../../built/_frontend_embed_vite_', + assetsDir: '.', + emptyOutDir: false, + sourcemap: process.env.NODE_ENV === 'development', + reportCompressedSize: false, + + // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies + commonjsOptions: { + include: [/misskey-js/, /node_modules/], + }, + }, + + worker: { + format: 'es', + }, + }; +} + +const config = defineConfig(({ command, mode }) => getConfig()); + +export default config; diff --git a/packages/frontend-embed/vite.json5.ts b/packages/frontend-embed/vite.json5.ts new file mode 100644 index 0000000000..87b67c2142 --- /dev/null +++ b/packages/frontend-embed/vite.json5.ts @@ -0,0 +1,48 @@ +// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json + +import JSON5 from 'json5'; +import { Plugin } from 'rollup'; +import { createFilter, dataToEsm } from '@rollup/pluginutils'; +import { RollupJsonOptions } from '@rollup/plugin-json'; + +// json5 extends SyntaxError with additional fields (without subclassing) +// https://github.com/json5/json5/blob/de344f0619bda1465a6e25c76f1c0c3dda8108d9/lib/parse.js#L1111-L1112 +interface Json5SyntaxError extends SyntaxError { + lineNumber: number; + columnNumber: number; +} + +export default function json5(options: RollupJsonOptions = {}): Plugin { + const filter = createFilter(options.include, options.exclude); + const indent = 'indent' in options ? options.indent : '\t'; + + return { + name: 'json5', + + // eslint-disable-next-line no-shadow + transform(json, id) { + if (id.slice(-6) !== '.json5' || !filter(id)) return null; + + try { + const parsed = JSON5.parse(json); + return { + code: dataToEsm(parsed, { + preferConst: options.preferConst, + compact: options.compact, + namedExports: options.namedExports, + indent, + }), + map: { mappings: '' }, + }; + } catch (err) { + if (!(err instanceof SyntaxError)) { + throw err; + } + const message = 'Could not parse JSON5 file'; + const { lineNumber, columnNumber } = err as Json5SyntaxError; + this.warn({ message, id, loc: { line: lineNumber, column: columnNumber } }); + return null; + } + }, + }; +} diff --git a/packages/frontend-embed/vue-shims.d.ts b/packages/frontend-embed/vue-shims.d.ts new file mode 100644 index 0000000000..eba994772d --- /dev/null +++ b/packages/frontend-embed/vue-shims.d.ts @@ -0,0 +1,6 @@ +/* eslint-disable */ +declare module "*.vue" { + import { defineComponent } from "vue"; + const component: ReturnType; + export default component; +} diff --git a/packages/frontend-shared/.gitignore b/packages/frontend-shared/.gitignore new file mode 100644 index 0000000000..5f6be09d7c --- /dev/null +++ b/packages/frontend-shared/.gitignore @@ -0,0 +1,2 @@ +/storybook-static +js-built diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js new file mode 100644 index 0000000000..17b6da8d30 --- /dev/null +++ b/packages/frontend-shared/build.js @@ -0,0 +1,106 @@ +import fs from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname } from 'node:path'; +import * as esbuild from 'esbuild'; +import { build } from 'esbuild'; +import { globSync } from 'glob'; +import { execa } from 'execa'; + +const _filename = fileURLToPath(import.meta.url); +const _dirname = dirname(_filename); +const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8')); + +const entryPoints = globSync('./js/**/**.{ts,tsx}'); + +/** @type {import('esbuild').BuildOptions} */ +const options = { + entryPoints, + minify: process.env.NODE_ENV === 'production', + outdir: './js-built', + target: 'es2022', + platform: 'browser', + format: 'esm', + sourcemap: 'linked', +}; + +// js-built配下をすべて削除する +fs.rmSync('./js-built', { recursive: true, force: true }); + +if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) { + await watchSrc(); +} else { + await buildSrc(); +} + +async function buildSrc() { + console.log(`[${_package.name}] start building...`); + + await build(options) + .then(() => { + console.log(`[${_package.name}] build succeeded.`); + }) + .catch((err) => { + process.stderr.write(err.stderr); + process.exit(1); + }); + + if (process.env.NODE_ENV === 'production') { + console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`); + } else { + await buildDts(); + } + + fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json'); + + console.log(`[${_package.name}] finish building.`); +} + +function buildDts() { + return execa( + 'tsc', + [ + '--project', 'tsconfig.json', + '--outDir', 'js-built', + '--declaration', 'true', + '--emitDeclarationOnly', 'true', + ], + { + stdout: process.stdout, + stderr: process.stderr, + }, + ); +} + +async function watchSrc() { + const plugins = [{ + name: 'gen-dts', + setup(build) { + build.onStart(() => { + console.log(`[${_package.name}] detect changed...`); + }); + build.onEnd(async result => { + if (result.errors.length > 0) { + console.error(`[${_package.name}] watch build failed:`, result); + return; + } + await buildDts(); + }); + }, + }]; + + console.log(`[${_package.name}] start watching...`); + + const context = await esbuild.context({ ...options, plugins }); + await context.watch(); + + await new Promise((resolve, reject) => { + process.on('SIGHUP', resolve); + process.on('SIGINT', resolve); + process.on('SIGTERM', resolve); + process.on('uncaughtException', reject); + process.on('exit', resolve); + }).finally(async () => { + await context.dispose(); + console.log(`[${_package.name}] finish watching.`); + }); +} diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js new file mode 100644 index 0000000000..a15fb29e37 --- /dev/null +++ b/packages/frontend-shared/eslint.config.js @@ -0,0 +1,96 @@ +import globals from 'globals'; +import tsParser from '@typescript-eslint/parser'; +import parser from 'vue-eslint-parser'; +import pluginVue from 'eslint-plugin-vue'; +import pluginMisskey from '@misskey-dev/eslint-plugin'; +import sharedConfig from '../shared/eslint.config.js'; + +// eslint-disable-next-line import/no-default-export +export default [ + ...sharedConfig, + { + files: ['**/*.vue'], + ...pluginMisskey.configs.typescript, + }, + ...pluginVue.configs['flat/recommended'], + { + files: ['js/**/*.{ts,vue}', '**/*.vue'], + languageOptions: { + globals: { + ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])), + ...globals.browser, + + // Node.js + module: false, + require: false, + __dirname: false, + + // Misskey + _DEV_: false, + _LANGS_: false, + _VERSION_: false, + _ENV_: false, + _PERF_PREFIX_: false, + _DATA_TRANSFER_DRIVE_FILE_: false, + _DATA_TRANSFER_DRIVE_FOLDER_: false, + _DATA_TRANSFER_DECK_COLUMN_: false, + }, + parser, + parserOptions: { + extraFileExtensions: ['.vue'], + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + rules: { + '@typescript-eslint/no-empty-interface': ['error', { + allowSingleExtends: true, + }], + // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため + // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため + 'id-denylist': ['error', 'window', 'e'], + 'no-shadow': ['warn'], + 'vue/attributes-order': ['error', { + alphabetical: false, + }], + 'vue/no-use-v-if-with-v-for': ['error', { + allowUsingIterationVar: false, + }], + 'vue/no-ref-as-operand': 'error', + 'vue/no-multi-spaces': ['error', { + ignoreProperties: false, + }], + 'vue/no-v-html': 'warn', + 'vue/order-in-components': 'error', + 'vue/html-indent': ['warn', 'tab', { + attribute: 1, + baseIndent: 0, + closeBracket: 0, + alignAttributesVertically: true, + ignores: [], + }], + 'vue/html-closing-bracket-spacing': ['warn', { + startTag: 'never', + endTag: 'never', + selfClosingTag: 'never', + }], + 'vue/multi-word-component-names': 'warn', + 'vue/require-v-for-key': 'warn', + 'vue/no-unused-components': 'warn', + 'vue/no-unused-vars': 'warn', + 'vue/no-dupe-keys': 'warn', + 'vue/valid-v-for': 'warn', + 'vue/return-in-computed-property': 'warn', + 'vue/no-setup-props-reactivity-loss': 'warn', + 'vue/max-attributes-per-line': 'off', + 'vue/html-self-closing': 'off', + 'vue/singleline-html-element-content-newline': 'off', + 'vue/v-on-event-hyphenation': ['error', 'never', { + autofix: true, + }], + 'vue/attribute-hyphenation': ['error', 'never'], + }, + }, +]; diff --git a/packages/frontend/src/const.ts b/packages/frontend-shared/js/const.ts similarity index 98% rename from packages/frontend/src/const.ts rename to packages/frontend-shared/js/const.ts index e135bc69a0..8391fb638c 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -127,7 +127,7 @@ export const MFM_PARAMS: Record = { position: ['x=', 'y='], fg: ['color='], bg: ['color='], - border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], + border: ['width=', 'style=', 'color=', 'radius=', 'noclip'], font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'], blur: [], rainbow: ['speed=', 'delay='], diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts new file mode 100644 index 0000000000..d5555a98c3 --- /dev/null +++ b/packages/frontend-shared/js/embed-page.ts @@ -0,0 +1,97 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +//#region Embed関連の定義 + +/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */ +const embeddableEntities = [ + 'notes', + 'user-timeline', + 'clips', + 'tags', +] as const; + +/** 埋め込みの対象となるエンティティ */ +export type EmbeddableEntity = typeof embeddableEntities[number]; + +/** 内部でスクロールがあるページ */ +export const embedRouteWithScrollbar: EmbeddableEntity[] = [ + 'clips', + 'tags', + 'user-timeline', +]; + +/** 埋め込みコードのパラメータ */ +export type EmbedParams = { + maxHeight?: number; + colorMode?: 'light' | 'dark'; + rounded?: boolean; + border?: boolean; + autoload?: boolean; + header?: boolean; +}; + +/** 正規化されたパラメータ */ +export type ParsedEmbedParams = Required> & Pick; + +/** パラメータのデフォルトの値 */ +export const defaultEmbedParams = { + maxHeight: undefined, + colorMode: undefined, + rounded: true, + border: true, + autoload: false, + header: true, +} as const satisfies EmbedParams; + +//#endregion + +/** + * パラメータを正規化する(埋め込みページ初期化用) + * @param searchParams URLSearchParamsもしくはクエリ文字列 + * @returns 正規化されたパラメータ + */ +export function parseEmbedParams(searchParams: URLSearchParams | string): ParsedEmbedParams { + let _searchParams: URLSearchParams; + if (typeof searchParams === 'string') { + _searchParams = new URLSearchParams(searchParams); + } else if (searchParams instanceof URLSearchParams) { + _searchParams = searchParams; + } else { + throw new Error('searchParams must be URLSearchParams or string'); + } + + function convertBoolean(value: string | null): boolean | undefined { + if (value === 'true') { + return true; + } else if (value === 'false') { + return false; + } + return undefined; + } + + function convertNumber(value: string | null): number | undefined { + if (value != null && !isNaN(Number(value))) { + return Number(value); + } + return undefined; + } + + function convertColorMode(value: string | null): 'light' | 'dark' | undefined { + if (value != null && ['light', 'dark'].includes(value)) { + return value as 'light' | 'dark'; + } + return undefined; + } + + return { + maxHeight: convertNumber(_searchParams.get('maxHeight')) ?? defaultEmbedParams.maxHeight, + colorMode: convertColorMode(_searchParams.get('colorMode')) ?? defaultEmbedParams.colorMode, + rounded: convertBoolean(_searchParams.get('rounded')) ?? defaultEmbedParams.rounded, + border: convertBoolean(_searchParams.get('border')) ?? defaultEmbedParams.border, + autoload: convertBoolean(_searchParams.get('autoload')) ?? defaultEmbedParams.autoload, + header: convertBoolean(_searchParams.get('header')) ?? defaultEmbedParams.header, + }; +} diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend-shared/js/emoji-base.ts similarity index 100% rename from packages/frontend/src/scripts/emoji-base.ts rename to packages/frontend-shared/js/emoji-base.ts diff --git a/packages/frontend/src/emojilist.json b/packages/frontend-shared/js/emojilist.json similarity index 100% rename from packages/frontend/src/emojilist.json rename to packages/frontend-shared/js/emojilist.json diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend-shared/js/emojilist.ts similarity index 96% rename from packages/frontend/src/scripts/emojilist.ts rename to packages/frontend-shared/js/emojilist.ts index 6565feba97..bde30a864f 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend-shared/js/emojilist.ts @@ -12,12 +12,12 @@ export type UnicodeEmojiDef = { } // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; +import _emojilist from './emojilist.json'; export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ name: x[1] as string, char: x[0] as string, - category: unicodeEmojiCategories[x[2]], + category: unicodeEmojiCategories[x[2] as number], })); const unicodeEmojisMap = new Map( diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts similarity index 100% rename from packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts rename to packages/frontend-shared/js/extract-avg-color-from-blurhash.ts diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend-shared/js/i18n.ts similarity index 91% rename from packages/frontend/src/scripts/i18n.ts rename to packages/frontend-shared/js/i18n.ts index b258a2a678..18232691fa 100644 --- a/packages/frontend/src/scripts/i18n.ts +++ b/packages/frontend-shared/js/i18n.ts @@ -2,7 +2,10 @@ * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; +import type { ILocale, ParameterizedString } from '../../../locales/index.js'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type TODO = any; type FlattenKeys = keyof { [K in keyof T as T[K] extends ILocale @@ -32,15 +35,18 @@ type Tsx = { export class I18n { private tsxCache?: Tsx; + private devMode: boolean; + + constructor(public locale: T, devMode = false) { + this.devMode = devMode; - constructor(public locale: T) { //#region BIND this.t = this.t.bind(this); //#endregion } public get ts(): T { - if (_DEV_) { + if (this.devMode) { class Handler implements ProxyHandler { get(target: TTarget, p: string | symbol): unknown { const value = target[p as keyof TTarget]; @@ -72,7 +78,7 @@ export class I18n { } public get tsx(): Tsx { - if (_DEV_) { + if (this.devMode) { if (this.tsxCache) { return this.tsxCache; } @@ -113,7 +119,7 @@ export class I18n { return () => value; } - return (arg) => { + return (arg: TODO) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -152,7 +158,7 @@ export class I18n { const value = target[k as keyof typeof target]; if (typeof value === 'object') { - result[k] = build(value as ILocale); + (result as TODO)[k] = build(value as ILocale); } else if (typeof value === 'string') { const quasis: string[] = []; const expressions: string[] = []; @@ -179,7 +185,7 @@ export class I18n { continue; } - result[k] = (arg) => { + (result as TODO)[k] = (arg: TODO) => { let str = quasis[0]; for (let i = 0; i < expressions.length; i++) { @@ -208,9 +214,9 @@ export class I18n { let str: string | ParameterizedString | ILocale = this.locale; for (const k of key.split('.')) { - str = str[k]; + str = (str as TODO)[k]; - if (_DEV_) { + if (this.devMode) { if (typeof str === 'undefined') { console.error(`Unexpected locale key: ${key}`); return key; @@ -219,7 +225,7 @@ export class I18n { } if (args) { - if (_DEV_) { + if (this.devMode) { const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); if (missing.length) { @@ -230,7 +236,7 @@ export class I18n { for (const [k, v] of Object.entries(args)) { const search = `{${k}}`; - if (_DEV_) { + if (this.devMode) { if (!(str as string).includes(search)) { console.error(`Unexpected locale parameter: ${k} at ${key}`); } diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts new file mode 100644 index 0000000000..2837870c9a --- /dev/null +++ b/packages/frontend-shared/js/media-proxy.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import * as Misskey from 'misskey-js'; +import { query } from './url.js'; + +export class MediaProxy { + private serverMetadata: Misskey.entities.MetaDetailed; + private url: string; + + constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) { + this.serverMetadata = serverMetadata; + this.url = url; + } + + public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { + const localProxy = `${this.url}/proxy`; + let _imageUrl = imageUrl; + + if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { + // もう既にproxyっぽそうだったらurlを取り出す + _imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; + } + + return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${ + type === 'preview' ? 'preview.webp' + : 'image.webp' + }?${query({ + url: _imageUrl, + ...(!noFallback ? { 'fallback': '1' } : {}), + ...(type ? { [type]: '1' } : {}), + ...(mustOrigin ? { origin: '1' } : {}), + })}`; + } + + public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { + if (imageUrl == null) return null; + return this.getProxiedImageUrl(imageUrl, type); + } + + public getStaticImageUrl(baseUrl: string): string { + const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url); + + if (u.href.startsWith(`${this.url}/emoji/`)) { + // もう既にemojiっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; + } + + if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) { + // もう既にproxyっぽそうだったらsearchParams付けるだけ + u.searchParams.set('static', '1'); + return u.href; + } + + return `${this.serverMetadata.mediaProxy}/static.webp?${query({ + url: u.href, + static: '1', + })}`; + } +} diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend-shared/js/scroll.ts similarity index 98% rename from packages/frontend/src/scripts/scroll.ts rename to packages/frontend-shared/js/scroll.ts index f0274034b5..1062e5252f 100644 --- a/packages/frontend/src/scripts/scroll.ts +++ b/packages/frontend-shared/js/scroll.ts @@ -45,7 +45,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, o const container = getScrollContainer(el) ?? window; - const onScroll = ev => { + const onScroll = () => { if (!document.body.contains(el)) return; if (isTopVisible(el, tolerance)) { cb(); @@ -69,7 +69,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1 } const containerOrWindow = container ?? window; - const onScroll = ev => { + const onScroll = () => { if (!document.body.contains(el)) return; if (isBottomVisible(el, 1, container)) { cb(); diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend-shared/js/url.ts similarity index 70% rename from packages/frontend/src/scripts/url.ts rename to packages/frontend-shared/js/url.ts index 5a8265af9e..eb830b1eea 100644 --- a/packages/frontend/src/scripts/url.ts +++ b/packages/frontend-shared/js/url.ts @@ -8,18 +8,18 @@ * 2. プロパティがundefinedの時はクエリを付けない * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) */ -export function query(obj: Record): string { +export function query(obj: Record): string { const params = Object.entries(obj) - .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) - .reduce((a, [k, v]) => (a[k] = v, a), {} as Record); + .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition + .reduce>((a, [k, v]) => (a[k] = v, a), {}); return Object.entries(params) .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) .join('&'); } -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; +export function appendQuery(url: string, queryString: string): string { + return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`; } export function extractDomain(url: string) { diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts similarity index 85% rename from packages/frontend/src/scripts/use-document-visibility.ts rename to packages/frontend-shared/js/use-document-visibility.ts index a8f4d5e03a..b1197e68da 100644 --- a/packages/frontend/src/scripts/use-document-visibility.ts +++ b/packages/frontend-shared/js/use-document-visibility.ts @@ -3,7 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { onMounted, onUnmounted, ref, Ref } from 'vue'; +import { onMounted, onUnmounted, ref } from 'vue'; +import type { Ref } from 'vue'; export function useDocumentVisibility(): Ref { const visibility = ref(document.visibilityState); diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend-shared/js/use-interval.ts similarity index 100% rename from packages/frontend/src/scripts/use-interval.ts rename to packages/frontend-shared/js/use-interval.ts diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json new file mode 100644 index 0000000000..9981d10dd2 --- /dev/null +++ b/packages/frontend-shared/package.json @@ -0,0 +1,39 @@ +{ + "name": "frontend-shared", + "type": "module", + "main": "./js-built/index.js", + "types": "./js-built/index.d.ts", + "exports": { + ".": { + "import": "./js-built/index.js", + "types": "./js-built/index.d.ts" + }, + "./*": { + "import": "./js-built/*", + "types": "./js-built/*" + } + }, + "scripts": { + "build": "node ./build.js", + "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"", + "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", + "typecheck": "tsc --noEmit", + "lint": "pnpm typecheck && pnpm eslint" + }, + "devDependencies": { + "@types/node": "20.14.12", + "@typescript-eslint/eslint-plugin": "7.17.0", + "@typescript-eslint/parser": "7.17.0", + "esbuild": "0.23.0", + "eslint-plugin-vue": "9.27.0", + "typescript": "5.5.4", + "vue-eslint-parser": "9.4.3" + }, + "files": [ + "js-built" + ], + "dependencies": { + "misskey-js": "workspace:*", + "vue": "3.4.37" + } +} diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 similarity index 100% rename from packages/frontend/src/themes/_dark.json5 rename to packages/frontend-shared/themes/_dark.json5 diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5 similarity index 100% rename from packages/frontend/src/themes/_light.json5 rename to packages/frontend-shared/themes/_light.json5 diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5 similarity index 100% rename from packages/frontend/src/themes/d-astro.json5 rename to packages/frontend-shared/themes/d-astro.json5 diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend-shared/themes/d-botanical.json5 similarity index 100% rename from packages/frontend/src/themes/d-botanical.json5 rename to packages/frontend-shared/themes/d-botanical.json5 diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend-shared/themes/d-cherry.json5 similarity index 100% rename from packages/frontend/src/themes/d-cherry.json5 rename to packages/frontend-shared/themes/d-cherry.json5 diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend-shared/themes/d-dark.json5 similarity index 100% rename from packages/frontend/src/themes/d-dark.json5 rename to packages/frontend-shared/themes/d-dark.json5 diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend-shared/themes/d-future.json5 similarity index 100% rename from packages/frontend/src/themes/d-future.json5 rename to packages/frontend-shared/themes/d-future.json5 diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend-shared/themes/d-green-lime.json5 similarity index 100% rename from packages/frontend/src/themes/d-green-lime.json5 rename to packages/frontend-shared/themes/d-green-lime.json5 diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend-shared/themes/d-green-orange.json5 similarity index 100% rename from packages/frontend/src/themes/d-green-orange.json5 rename to packages/frontend-shared/themes/d-green-orange.json5 diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend-shared/themes/d-ice.json5 similarity index 100% rename from packages/frontend/src/themes/d-ice.json5 rename to packages/frontend-shared/themes/d-ice.json5 diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend-shared/themes/d-persimmon.json5 similarity index 100% rename from packages/frontend/src/themes/d-persimmon.json5 rename to packages/frontend-shared/themes/d-persimmon.json5 diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5 similarity index 100% rename from packages/frontend/src/themes/d-u0.json5 rename to packages/frontend-shared/themes/d-u0.json5 diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend-shared/themes/l-apricot.json5 similarity index 100% rename from packages/frontend/src/themes/l-apricot.json5 rename to packages/frontend-shared/themes/l-apricot.json5 diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend-shared/themes/l-botanical.json5 similarity index 100% rename from packages/frontend/src/themes/l-botanical.json5 rename to packages/frontend-shared/themes/l-botanical.json5 diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend-shared/themes/l-cherry.json5 similarity index 100% rename from packages/frontend/src/themes/l-cherry.json5 rename to packages/frontend-shared/themes/l-cherry.json5 diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend-shared/themes/l-coffee.json5 similarity index 100% rename from packages/frontend/src/themes/l-coffee.json5 rename to packages/frontend-shared/themes/l-coffee.json5 diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend-shared/themes/l-light.json5 similarity index 100% rename from packages/frontend/src/themes/l-light.json5 rename to packages/frontend-shared/themes/l-light.json5 diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend-shared/themes/l-rainy.json5 similarity index 100% rename from packages/frontend/src/themes/l-rainy.json5 rename to packages/frontend-shared/themes/l-rainy.json5 diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend-shared/themes/l-sushi.json5 similarity index 100% rename from packages/frontend/src/themes/l-sushi.json5 rename to packages/frontend-shared/themes/l-sushi.json5 diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5 similarity index 100% rename from packages/frontend/src/themes/l-u0.json5 rename to packages/frontend-shared/themes/l-u0.json5 diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5 similarity index 100% rename from packages/frontend/src/themes/l-vivid.json5 rename to packages/frontend-shared/themes/l-vivid.json5 diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json new file mode 100644 index 0000000000..fa0b765534 --- /dev/null +++ b/packages/frontend-shared/tsconfig.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "declaration": true, + "declarationMap": true, + "sourceMap": false, + "outDir": "./js-built/", + "removeComments": true, + "resolveJsonModule": true, + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "experimentalDecorators": true, + "noImplicitReturns": true, + "esModuleInterop": true, + "typeRoots": [ + "./node_modules/@types" + ], + "lib": [ + "esnext", + "dom" + ] + }, + "include": [ + "js/**/*" + ], + "exclude": [ + "node_modules", + "test/**/*" + ] +} diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts index fb93d7be13..e5573f2ac3 100644 --- a/packages/frontend/.storybook/preload-theme.ts +++ b/packages/frontend/.storybook/preload-theme.ts @@ -30,7 +30,7 @@ const keys = [ 'd-u0', ] -await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { +await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => { writeFile( new URL('./themes.ts', import.meta.url), `export default ${JSON.stringify( diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts index 0a7281898d..70afc356c1 100644 --- a/packages/frontend/@types/theme.d.ts +++ b/packages/frontend/@types/theme.d.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -declare module '@/themes/*.json5' { +declare module '@@/themes/*.json5' { import { Theme } from '@/scripts/theme.js'; const theme: Theme; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 1464be18a7..67be7f0598 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -55,6 +55,7 @@ "misskey-bubble-game": "workspace:*", "misskey-js": "workspace:*", "misskey-reversi": "workspace:*", + "frontend-shared": "workspace:*", "photoswipe": "5.4.4", "punycode": "2.3.1", "rollup": "4.19.1", diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts index d86ae18ffe..19d30f64ce 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { deckStore } from '@/ui/deck/deck-store.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; -import { setupRouter } from '@/router/definition.js'; +import { setupRouter } from '@/router/main.js'; +import { createMainRouter } from '@/router/definition.js'; export async function common(createVue: () => App) { console.info(`Misskey v${version}`); @@ -239,7 +240,7 @@ export async function common(createVue: () => App) { const app = createVue(); - setupRouter(app); + setupRouter(app, createMainRouter); if (_DEV_) { app.config.performance = true; diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts index 3e7c4f26f8..b31281dcf2 100644 --- a/packages/frontend/src/boot/main-boot.ts +++ b/packages/frontend/src/boot/main-boot.ts @@ -22,6 +22,7 @@ import { deckStore } from '@/ui/deck/deck-store.js'; import { emojiPicker } from '@/scripts/emoji-picker.js'; import { mainRouter } from '@/router/main.js'; import { type Keymap, makeHotkey } from '@/scripts/hotkey.js'; +import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js'; export async function mainBoot() { const { isClientUpdated } = await common(() => createApp( @@ -62,6 +63,18 @@ export async function mainBoot() { } }); + stream.on('emojiAdded', emojiData => { + addCustomEmoji(emojiData.emoji); + }); + + stream.on('emojiUpdated', emojiData => { + updateCustomEmojis(emojiData.emojis); + }); + + stream.on('emojiDeleted', emojiData => { + removeCustomEmojis(emojiData.emojis); + }); + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { import('@/plugin.js').then(async ({ install }) => { // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index 932c4ecb2e..f547991369 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue index c13164c296..fca7aa2f4e 100644 --- a/packages/frontend/src/components/MkEmojiPicker.section.vue +++ b/packages/frontend/src/components/MkEmojiPicker.section.vue @@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only `, + ]; + return iframeCode.join('\n'); +} + +/** + * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) + * + * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください + */ +export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { + const _params = { ...params }; + + if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { + _params.maxHeight = 700; + } + + // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー + if (window.innerWidth < MOBILE_THRESHOLD) { + copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); + os.success(); + } else { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { + entity, + id, + params: _params, + }, { + closed: () => dispose(), + }); + } +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b5d7350a41..e0ccea813d 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -21,6 +21,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -156,6 +157,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): }; } +function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { + if (note.url != null || note.uri != null) return undefined; + if (['specified', 'followers'].includes(note.visibility)) return undefined; + + return { + icon: 'ti ti-code', + text, + action: (): void => { + genEmbedCode('notes', note.id); + }, + }; +} + export function getNoteMenu(props: { note: Misskey.entities.Note; translation: Ref; @@ -310,7 +324,7 @@ export function getNoteMenu(props: { action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined, + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode), ...(isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, @@ -443,14 +457,14 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? { + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink), + (appearNote.url || appearNote.uri) ? { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined] + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)] .filter(x => x !== undefined); } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 33f16a68aa..035abc7bd0 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -17,6 +17,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; import { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { @@ -179,7 +180,17 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if (user.url == null) return; window.open(user.url, '_blank', 'noopener'); }, - }] : []), { + }] : [{ + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + type: 'parent' as const, + children: [{ + text: i18n.ts.noteOfThisUser, + action: () => { + genEmbedCode('user-timeline', user.id); + }, + }], // TODO: ユーザーカードの埋め込みなど + }]), { icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 6b511f2a5f..20f51660c7 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,10 +10,11 @@ import { set as iset, del as idel, } from 'idb-keyval'; +import { miLocalStorage } from '@/local-storage.js'; -const fallbackName = (key: string) => `idbfallback::${key}`; +const PREFIX = 'idbfallback::'; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと @@ -38,15 +39,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return JSON.parse(window.localStorage.getItem(fallbackName(key))); + return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); + return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); } export async function del(key: string) { if (idbAvailable) return idel(key); - return window.localStorage.removeItem(fallbackName(key)); + return miLocalStorage.removeItem(`${PREFIX}${key}`); } diff --git a/packages/frontend/src/scripts/is-link.ts b/packages/frontend/src/scripts/is-link.ts new file mode 100644 index 0000000000..946f86400e --- /dev/null +++ b/packages/frontend/src/scripts/is-link.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isLink(el: HTMLElement) { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; +} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 099a22163a..68a5a1dcf8 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -3,51 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { query } from '@/scripts/url.js'; +import { MediaProxy } from '@@/js/media-proxy.js'; import { url } from '@/config.js'; import { instance } from '@/instance.js'; -export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { - const localProxy = `${url}/proxy`; +let _mediaProxy: MediaProxy | null = null; - if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { - // もう既にproxyっぽそうだったらurlを取り出す - imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; +export function getProxiedImageUrl(...args: Parameters): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${mustOrigin ? localProxy : instance.mediaProxy}/${ - type === 'preview' ? 'preview.webp' - : 'image.webp' - }?${query({ - url: imageUrl, - ...(!noFallback ? { 'fallback': '1' } : {}), - ...(type ? { [type]: '1' } : {}), - ...(mustOrigin ? { origin: '1' } : {}), - })}`; + return _mediaProxy.getProxiedImageUrl(...args); } -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} - -export function getStaticImageUrl(baseUrl: string): string { - const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); - - if (u.href.startsWith(`${url}/emoji/`)) { - // もう既にemojiっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; +export function getProxiedImageUrlNullable(...args: Parameters): string | null { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - if (u.href.startsWith(instance.mediaProxy + '/')) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; + return _mediaProxy.getProxiedImageUrlNullable(...args); +} + +export function getStaticImageUrl(...args: Parameters): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${instance.mediaProxy}/static.webp?${query({ - url: u.href, - static: '1', - })}`; + return _mediaProxy.getStaticImageUrl(...args); } diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 9938e534c1..bf59fe98a0 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -6,7 +6,7 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@/const.js'; +import { MFM_TAGS } from '@@/js/const.js'; import type { MenuItem } from '@/types/menu.js'; /** diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 1caa2dfc21..ed49611b4f 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from './url.js'; +import { appendQuery } from '@@/js/url.js'; import * as config from '@/config.js'; export function popout(path: string, w?: HTMLElement) { diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 31a9ac1ad9..11b6f52ddd 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -18,7 +18,7 @@ export type MiPostMessageEvent = { * 親フレームにイベントを送信 */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.postMessage({ + window.parent.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts deleted file mode 100644 index 6bfcef6c36..0000000000 --- a/packages/frontend/src/scripts/safe-parse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeParseFloat(str: unknown): number | null { - if (typeof str !== 'string' || str === '') return null; - const num = parseFloat(str); - if (isNaN(num)) return null; - return num; -} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 0edf4e9eba..0000000000 --- a/packages/frontend/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts new file mode 100644 index 0000000000..cb0e607fcb --- /dev/null +++ b/packages/frontend/src/scripts/stream-mock.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Misskey from 'misskey-js'; +import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; + +type AnyOf> = T[keyof T]; +type OmitFirst = T extends [any, ...infer R] ? R : never; + +/** + * Websocket無効化時に使うStreamのモック(なにもしない) + */ +export class StreamMock extends EventEmitter implements IStream { + public readonly state = 'initializing'; + + constructor(...args: ConstructorParameters) { + super(); + // do nothing + } + + public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock { + return new ChannelConnectionMock(this, channel, name); + } + + public removeSharedConnection(connection: any): void { + // do nothing + } + + public removeSharedConnectionPool(pool: any): void { + // do nothing + } + + public disconnectToChannel(): void { + // do nothing + } + + public send(typeOrPayload: string): void + public send(typeOrPayload: string, payload: any): void + public send(typeOrPayload: Record | any[]): void + public send(typeOrPayload: string | Record | any[], payload?: any): void { + // do nothing + } + + public ping(): void { + // do nothing + } + + public heartbeat(): void { + // do nothing + } + + public close(): void { + // do nothing + } +} + +class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection { + public id = ''; + public name?: string; // for debug + public inCount = 0; // for debug + public outCount = 0; // for debug + public channel: string; + + constructor(stream: IStream, ...args: OmitFirst>>) { + super(); + + this.channel = args[0]; + this.name = args[1]; + } + + public send(type: T, body: Channel['receives'][T]): void { + // do nothing + } + + public dispose(): void { + // do nothing + } +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index c7f8b3d596..9b9f1f030c 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,11 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 437314074a..0bf499bb4d 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -458,10 +458,10 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: false, }, - contextMenu: { + contextMenu: { where: 'device', default: 'app' as 'app' | 'appWithShift' | 'native', - }, + }, sound_masterVolume: { where: 'device', @@ -520,8 +520,8 @@ interface Watcher { /** * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ) */ -import lightTheme from '@/themes/l-light.json5'; -import darkTheme from '@/themes/d-green-lime.json5'; +import lightTheme from '@@/themes/l-light.json5'; +import darkTheme from '@@/themes/d-green-lime.json5'; export class ColdDeviceStorage { public static default = { @@ -558,7 +558,7 @@ export class ColdDeviceStorage { public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void { // 呼び出し側のバグ等で undefined が来ることがある // undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視 - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value === undefined) { console.error(`attempt to store undefined value for key '${key}'`); return; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index 0d5bd78b09..9d7edce890 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -7,17 +7,20 @@ import * as Misskey from 'misskey-js'; import { markRaw } from 'vue'; import { $i } from '@/account.js'; import { wsOrigin } from '@/config.js'; +// TODO: No WebsocketモードでStreamMockが使えそう +//import { StreamMock } from '@/scripts/stream-mock.js'; // heart beat interval in ms const HEART_BEAT_INTERVAL = 1000 * 60; -let stream: Misskey.Stream | null = null; -let timeoutHeartBeat: ReturnType | null = null; +let stream: Misskey.IStream | null = null; +let timeoutHeartBeat: number | null = null; let lastHeartbeatCall = 0; -export function useStream(): Misskey.Stream { +export function useStream(): Misskey.IStream { if (stream) return stream; + // TODO: No Websocketモードもここで判定 stream = markRaw(new Misskey.Stream(wsOrigin, $i ? { token: $i.token, } : null)); diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue index 8dad666623..e234bb3a33 100644 --- a/packages/frontend/src/ui/_common_/statusbar-federation.vue +++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue @@ -35,7 +35,7 @@ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue index 6e1d06eec1..550fc39b00 100644 --- a/packages/frontend/src/ui/_common_/statusbar-rss.vue +++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { shuffle } from '@/scripts/shuffle.js'; const props = defineProps<{ diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue index 67f8b109c4..078b595dca 100644 --- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue +++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue @@ -35,7 +35,7 @@ import { ref, watch } from 'vue'; import * as Misskey from 'misskey-js'; import MarqueeText from '@/components/MkMarquee.vue'; import { misskeyApi } from '@/scripts/misskey-api.js'; -import { useInterval } from '@/scripts/use-interval.js'; +import { useInterval } from '@@/js/use-interval.js'; import { getNoteSummary } from '@/scripts/get-note-summary.js'; import { notePage } from '@/filters/note.js'; diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue index 79c9671917..e7ecf7fd20 100644 --- a/packages/frontend/src/ui/deck/main-column.vue +++ b/packages/frontend/src/ui/deck/main-column.vue @@ -26,7 +26,7 @@ import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { useScrollPositionManager } from '@/nirax.js'; -import { getScrollContainer } from '@/scripts/scroll.js'; +import { getScrollContainer } from '@@/js/scroll.js'; import { mainRouter } from '@/router/main.js'; defineProps<{ diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue index 073acbd4db..00a6811fc9 100644 --- a/packages/frontend/src/ui/universal.vue +++ b/packages/frontend/src/ui/universal.vue @@ -108,7 +108,7 @@ import { $i } from '@/account.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { deviceKind } from '@/scripts/device-kind.js'; import { miLocalStorage } from '@/local-storage.js'; -import { CURRENT_STICKY_BOTTOM } from '@/const.js'; +import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js'; import { useScrollPositionManager } from '@/nirax.js'; import { mainRouter } from '@/router/main.js'; diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue index 49fd103d37..bcfaaf00ab 100644 --- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue +++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue @@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index b0ffac93d7..6710d9826e 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getHighlighterCore, loadWasm } from 'shiki/core'; +import { createHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; @@ -69,7 +69,7 @@ async function initHighlighter() { ]); const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript'); - const highlighter = await getHighlighterCore({ + const highlighter = await createHighlighterCore({ themes, langs: [ ...(jsLangInfo ? [async () => await jsLangInfo.import()] : []), From 6b2072f4b1e6a191634b51b448442aaf57df5434 Mon Sep 17 00:00:00 2001 From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:13:46 +0900 Subject: [PATCH 315/635] =?UTF-8?q?fix(backend/antenna):=20=E3=82=AD?= =?UTF-8?q?=E3=83=BC=E3=83=AF=E3=83=BC=E3=83=89=E3=81=8C=E4=B8=8E=E3=81=88?= =?UTF-8?q?=E3=82=89=E3=82=8C=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E5=A0=B4?= =?UTF-8?q?=E5=90=88=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92ApiError?= =?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=A6=E6=8A=95=E3=81=92=E3=82=8B=20(#1449?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(backend/antenna): report validation failure as ApiError on update * test(backend/antenna): reflect change in previous commit * fix(backend/antenna): report validation failure as ApiError on create * test(backend/antenna): reflect change in previous commit * test(backend/antenna): semi * test(backend/antenna): bring being spread parameters first in object literal * chore: add CHANGELOG entry --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 4 +++- .../server/api/endpoints/antennas/create.ts | 8 ++++++- .../server/api/endpoints/antennas/update.ts | 8 ++++++- packages/backend/test/e2e/antennas.ts | 23 +++++++++++++++++++ 4 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc2d9f102e..62d9d4defa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,11 +12,13 @@ - Fix: 月の違う同じ日はセパレータが表示されないのを修正 ### Server +- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 +- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように + - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます - Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 - Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正 (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8) - ## 2024.8.0 ### General 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/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts index 6ac14cd8dc..a544db955a 100644 --- a/packages/backend/test/e2e/antennas.ts +++ b/packages/backend/test/e2e/antennas.ts @@ -228,6 +228,17 @@ describe('アンテナ', () => { assert.deepStrictEqual(response, expected); }); + test('を作成する時キーワードが指定されていないとエラーになる', async () => { + await failedApiCall({ + endpoint: 'antennas/create', + parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] }, + user: alice + }, { + status: 400, + code: 'EMPTY_KEYWORD', + id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a' + }) + }); //#endregion //#region 更新(antennas/update) @@ -255,6 +266,18 @@ describe('アンテナ', () => { id: '1c6b35c9-943e-48c2-81e4-2844989407f7', }); }); + test('を変更する時キーワードが指定されていないとエラーになる', async () => { + const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice }); + await failedApiCall({ + endpoint: 'antennas/update', + parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] }, + user: alice + }, { + status: 400, + code: 'EMPTY_KEYWORD', + id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4' + }) + }); //#endregion //#region 表示(antennas/show) From 366b79e4595b709f5a6b8b4700eb93510d41072a Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 15 Sep 2024 15:14:13 +0900 Subject: [PATCH 316/635] Update CHANGELOG.md --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62d9d4defa..ffe03c13c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,6 @@ - Fix: 月の違う同じ日はセパレータが表示されないのを修正 ### Server -- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 - Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます - Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正 From 07f26bc8dd199ff366e6278a8ac1521497922b95 Mon Sep 17 00:00:00 2001 From: Juan Aguilar Santillana Date: Sun, 15 Sep 2024 10:43:24 +0200 Subject: [PATCH 317/635] refactor(backend): use Reflet for autobind deco (#14482) Using Reflect.defineProperty instead of Object.defineProperty gives a more consistent behavior with the rest of the modern JavaScript features. --- packages/backend/src/decorators.ts | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts index 21777657d1..42f925e125 100644 --- a/packages/backend/src/decorators.ts +++ b/packages/backend/src/decorators.ts @@ -10,8 +10,9 @@ * The getter will return a .bind version of the function * and memoize the result against a symbol on the instance */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any export function bindThis(target: any, key: string, descriptor: any) { - let fn = descriptor.value; + const fn = descriptor.value; if (typeof fn !== 'function') { throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`); @@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) { configurable: true, get() { // eslint-disable-next-line no-prototype-builtins - if (this === target.prototype || this.hasOwnProperty(key) || - typeof fn !== 'function') { + if (this === target.prototype || this.hasOwnProperty(key)) { return fn; } const boundFn = fn.bind(this); - Object.defineProperty(this, key, { + Reflect.defineProperty(this, key, { + value: boundFn, configurable: true, - get() { - return boundFn; - }, - set(value) { - fn = value; - delete this[key]; - }, + writable: true, }); + return boundFn; }, - set(value: any) { - fn = value; - }, }; } From 0e4b6d1dade90673af58af3480081c95984c0274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 15 Sep 2024 17:50:25 +0900 Subject: [PATCH 318/635] =?UTF-8?q?enhance(frontend):=20admin=E3=81=AE?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=83=AA=E3=82=B9=E3=83=88?= =?UTF-8?q?=E3=81=A7=E3=82=BB=E3=83=B3=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AB=E6=9E=A0=E7=B7=9A?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(#14510)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * enhance(frontend): adminのファイルリストでセンシティブファイルに枠線を追加 * Update Changelog --- CHANGELOG.md | 1 + .../src/components/MkDriveFileThumbnail.vue | 21 ++++++++++++++++++- .../src/components/MkFileListForAdmin.vue | 2 +- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe03c13c7..c01d284bdb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です - Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように - Enhance: アイコンデコレーション管理画面にプレビューを追加 +- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく - Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正 - Fix: 月の違う同じ日はセパレータが表示されないのを修正 diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 2c47a70970..eb93aaab6e 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only --> -
+
+ + + +
{{ i18n.ts.edit }} @@ -40,6 +44,7 @@ import { toRefs } from 'vue'; import MkFolder from '@/components/MkFolder.vue'; import { i18n } from '@/i18n.js'; import MkButton from '@/components/MkButton.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; const emit = defineEmits<{ (ev: 'edit', value: entities.SystemWebhook): void; From 023fa30280e561e9921a2c83138af4cac01068ab Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 12:53:13 +0900 Subject: [PATCH 349/635] refactor/perf(backend): provide metadata statically (#14601) * wip * Update ReactionService.ts * Update ApiCallService.ts * Update timeline.ts * Update GlobalModule.ts * Update GlobalModule.ts * Update NoteEntityService.ts * wip * wip * wip * Update ApPersonService.ts * wip * Update GlobalModule.ts * Update mock-resolver.ts * Update RoleService.ts * Update activitypub.ts * Update activitypub.ts * Update activitypub.ts * Update activitypub.ts * Update activitypub.ts * clean up * Update utils.ts * Update UtilityService.ts * Revert "Update utils.ts" This reverts commit a27d4be764b78c1b5a9eac685e261fee49331d89. * Revert "Update UtilityService.ts" This reverts commit e5fd9e004c482cf099252201c0c1aa888e001430. * vuwa- * Revert "vuwa-" This reverts commit 0c3bd12472b4b9938cdff2d6f131e6800bc3724c. * Update entry.ts * Update entry.ts * Update entry.ts * Update entry.ts * Update jest.setup.ts --- packages/backend/src/GlobalModule.ts | 63 +++++++++++++++++- .../core/AbuseReportNotificationService.ts | 12 ++-- .../backend/src/core/AccountMoveService.ts | 9 +-- packages/backend/src/core/DriveService.ts | 61 ++++++++--------- packages/backend/src/core/EmailService.ts | 43 ++++++------ packages/backend/src/core/HashtagService.ts | 12 ++-- .../backend/src/core/NoteCreateService.ts | 60 ++++++++--------- .../backend/src/core/NoteDeleteService.ts | 15 ++--- .../backend/src/core/ProxyAccountService.ts | 13 ++-- .../src/core/PushNotificationService.ts | 16 ++--- packages/backend/src/core/ReactionService.ts | 19 +++--- packages/backend/src/core/RoleService.ts | 10 ++- packages/backend/src/core/SignupService.ts | 10 +-- .../backend/src/core/UserFollowingService.ts | 20 +++--- packages/backend/src/core/WebAuthnService.ts | 30 ++++----- .../src/core/activitypub/ApInboxService.ts | 10 +-- .../src/core/activitypub/ApResolverService.ts | 14 ++-- .../core/activitypub/models/ApImageService.ts | 11 ++-- .../core/activitypub/models/ApNoteService.ts | 12 ++-- .../activitypub/models/ApPersonService.ts | 12 ++-- .../src/core/chart/charts/federation.ts | 19 +++--- .../core/entities/InstanceEntityService.ts | 16 +++-- .../src/core/entities/MetaEntityService.ts | 9 +-- .../src/core/entities/NoteEntityService.ts | 16 ++--- .../backend/src/daemons/ServerStatsService.ts | 10 +-- packages/backend/src/di-symbols.ts | 1 + .../BakeBufferedReactionsProcessorService.ts | 10 +-- .../processors/DeliverProcessorService.ts | 14 ++-- .../queue/processors/InboxProcessorService.ts | 16 +++-- packages/backend/src/server/ServerService.ts | 9 +-- .../backend/src/server/api/ApiCallService.ts | 12 ++-- .../src/server/api/SignupApiService.ts | 35 +++++----- .../src/server/api/endpoints/ap/show.ts | 14 ++-- .../server/api/endpoints/channels/timeline.ts | 13 ++-- .../backend/src/server/api/endpoints/drive.ts | 5 -- .../api/endpoints/drive/files/create.ts | 15 +++-- .../server/api/endpoints/i/update-email.ts | 9 +-- .../src/server/api/endpoints/notes/create.ts | 2 - .../api/endpoints/notes/hybrid-timeline.ts | 13 ++-- .../api/endpoints/notes/local-timeline.ts | 15 ++--- .../server/api/endpoints/notes/timeline.ts | 13 ++-- .../server/api/endpoints/notes/translate.ts | 17 ++--- .../api/endpoints/notes/user-list-timeline.ts | 15 ++--- .../src/server/api/endpoints/pinned-users.ts | 11 ++-- .../src/server/api/endpoints/server-info.ts | 10 +-- .../src/server/api/endpoints/sw/register.ts | 13 ++-- .../api/endpoints/username/available.ts | 11 ++-- .../src/server/api/endpoints/users/notes.ts | 11 ++-- .../src/server/web/ClientServerService.ts | 66 +++++++------------ .../src/server/web/UrlPreviewService.ts | 17 +++-- packages/backend/test-server/entry.ts | 22 ++++++- packages/backend/test/jest.setup.ts | 6 +- packages/backend/test/misc/mock-resolver.ts | 3 +- packages/backend/test/unit/RoleService.ts | 45 +++++-------- packages/backend/test/unit/activitypub.ts | 21 +++--- 55 files changed, 499 insertions(+), 487 deletions(-) diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts index 2ecc1f4742..0f69cf93a9 100644 --- a/packages/backend/src/GlobalModule.ts +++ b/packages/backend/src/GlobalModule.ts @@ -13,6 +13,8 @@ import { createPostgresDataSource } from './postgres.js'; import { RepositoryModule } from './models/RepositoryModule.js'; import { allSettled } from './misc/promise-tracker.js'; import type { Provider, OnApplicationShutdown } from '@nestjs/common'; +import { MiMeta } from '@/models/Meta.js'; +import { GlobalEvents } from './core/GlobalEventService.js'; const $config: Provider = { provide: DI.config, @@ -86,11 +88,68 @@ const $redisForReactions: Provider = { inject: [DI.config], }; +const $meta: Provider = { + provide: DI.meta, + useFactory: async (db: DataSource, redisForSub: Redis.Redis) => { + const meta = await db.transaction(async transactionalEntityManager => { + // 過去のバグでレコードが複数出来てしまっている可能性があるので新しいIDを優先する + const metas = await transactionalEntityManager.find(MiMeta, { + order: { + id: 'DESC', + }, + }); + + const meta = metas[0]; + + if (meta) { + return meta; + } else { + // metaが空のときfetchMetaが同時に呼ばれるとここが同時に呼ばれてしまうことがあるのでフェイルセーフなupsertを使う + const saved = await transactionalEntityManager + .upsert( + MiMeta, + { + id: 'x', + }, + ['id'], + ) + .then((x) => transactionalEntityManager.findOneByOrFail(MiMeta, x.identifiers[0])); + + return saved; + } + }); + + async function onMessage(_: string, data: string): Promise { + const obj = JSON.parse(data); + + if (obj.channel === 'internal') { + const { type, body } = obj.message as GlobalEvents['internal']['payload']; + switch (type) { + case 'metaUpdated': { + for (const key in body) { + (meta as any)[key] = (body as any)[key]; + } + meta.proxyAccount = null; // joinなカラムは通常取ってこないので + break; + } + default: + break; + } + } + } + + redisForSub.on('message', onMessage); + + return meta; + }, + inject: [DI.db, DI.redisForSub], +}; + @Global() @Module({ imports: [RepositoryModule], - providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions], - exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule], + providers: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions], + exports: [$config, $db, $meta, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule], }) export class GlobalModule implements OnApplicationShutdown { constructor( diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts index 7be5335885..fe2c63e7d6 100644 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -14,10 +14,10 @@ import type { AbuseReportNotificationRecipientRepository, MiAbuseReportNotificationRecipient, MiAbuseUserReport, + MiMeta, MiUser, } from '@/models/_.js'; import { EmailService } from '@/core/EmailService.js'; -import { MetaService } from '@/core/MetaService.js'; import { RoleService } from '@/core/RoleService.js'; import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; @@ -27,15 +27,19 @@ import { IdService } from './IdService.js'; @Injectable() export class AbuseReportNotificationService implements OnApplicationShutdown { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.abuseReportNotificationRecipientRepository) private abuseReportNotificationRecipientRepository: AbuseReportNotificationRecipientRepository, + @Inject(DI.redisForSub) private redisForSub: Redis.Redis, + private idService: IdService, private roleService: RoleService, private systemWebhookService: SystemWebhookService, private emailService: EmailService, - private metaService: MetaService, private moderationLogService: ModerationLogService, private globalEventService: GlobalEventService, ) { @@ -93,10 +97,8 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { .filter(x => x != null), ); - // 送信先の鮮度を保つため、毎回取得する - const meta = await this.metaService.fetch(true); recipientEMailAddresses.push( - ...(meta.email ? [meta.email] : []), + ...(this.meta.email ? [this.meta.email] : []), ); if (recipientEMailAddresses.length <= 0) { diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts index b6b591d240..6e3125044c 100644 --- a/packages/backend/src/core/AccountMoveService.ts +++ b/packages/backend/src/core/AccountMoveService.ts @@ -9,7 +9,7 @@ import { IsNull, In, MoreThan, Not } from 'typeorm'; import { bindThis } from '@/decorators.js'; import { DI } from '@/di-symbols.js'; import type { MiLocalUser, MiRemoteUser, MiUser } from '@/models/User.js'; -import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; +import type { BlockingsRepository, FollowingsRepository, InstancesRepository, MiMeta, MutingsRepository, UserListMembershipsRepository, UsersRepository } from '@/models/_.js'; import type { RelationshipJobData, ThinUser } from '@/queue/types.js'; import { IdService } from '@/core/IdService.js'; @@ -22,13 +22,15 @@ import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; -import { MetaService } from '@/core/MetaService.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; @Injectable() export class AccountMoveService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -57,7 +59,6 @@ export class AccountMoveService { private perUserFollowingChart: PerUserFollowingChart, private federatedInstanceService: FederatedInstanceService, private instanceChart: InstanceChart, - private metaService: MetaService, private relayService: RelayService, private queueService: QueueService, ) { @@ -276,7 +277,7 @@ export class AccountMoveService { if (this.userEntityService.isRemoteUser(oldAccount)) { this.federatedInstanceService.fetch(oldAccount.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, false); } }); diff --git a/packages/backend/src/core/DriveService.ts b/packages/backend/src/core/DriveService.ts index 8aa04b4da7..c332e5a0a8 100644 --- a/packages/backend/src/core/DriveService.ts +++ b/packages/backend/src/core/DriveService.ts @@ -11,11 +11,10 @@ import { sharpBmp } from '@misskey-dev/sharp-read-bmp'; import { IsNull } from 'typeorm'; import { DeleteObjectCommandInput, PutObjectCommandInput, NoSuchKey } from '@aws-sdk/client-s3'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, UsersRepository, DriveFoldersRepository, UserProfilesRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import Logger from '@/logger.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiDriveFile } from '@/models/DriveFile.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; @@ -99,6 +98,9 @@ export class DriveService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -115,7 +117,6 @@ export class DriveService { private userEntityService: UserEntityService, private driveFileEntityService: DriveFileEntityService, private idService: IdService, - private metaService: MetaService, private downloadService: DownloadService, private internalStorageService: InternalStorageService, private s3Service: S3Service, @@ -149,9 +150,7 @@ export class DriveService { // thunbnail, webpublic を必要なら生成 const alts = await this.generateAlts(path, type, !file.uri); - const meta = await this.metaService.fetch(); - - if (meta.useObjectStorage) { + if (this.meta.useObjectStorage) { //#region ObjectStorage params let [ext] = (name.match(/\.([a-zA-Z0-9_-]+)$/) ?? ['']); @@ -170,11 +169,11 @@ export class DriveService { ext = ''; } - const baseUrl = meta.objectStorageBaseUrl - ?? `${ meta.objectStorageUseSSL ? 'https' : 'http' }://${ meta.objectStorageEndpoint }${ meta.objectStoragePort ? `:${meta.objectStoragePort}` : '' }/${ meta.objectStorageBucket }`; + const baseUrl = this.meta.objectStorageBaseUrl + ?? `${ this.meta.objectStorageUseSSL ? 'https' : 'http' }://${ this.meta.objectStorageEndpoint }${ this.meta.objectStoragePort ? `:${this.meta.objectStoragePort}` : '' }/${ this.meta.objectStorageBucket }`; // for original - const key = `${meta.objectStoragePrefix}/${randomUUID()}${ext}`; + const key = `${this.meta.objectStoragePrefix}/${randomUUID()}${ext}`; const url = `${ baseUrl }/${ key }`; // for alts @@ -191,7 +190,7 @@ export class DriveService { ]; if (alts.webpublic) { - webpublicKey = `${meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; + webpublicKey = `${this.meta.objectStoragePrefix}/webpublic-${randomUUID()}.${alts.webpublic.ext}`; webpublicUrl = `${ baseUrl }/${ webpublicKey }`; this.registerLogger.info(`uploading webpublic: ${webpublicKey}`); @@ -199,7 +198,7 @@ export class DriveService { } if (alts.thumbnail) { - thumbnailKey = `${meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; + thumbnailKey = `${this.meta.objectStoragePrefix}/thumbnail-${randomUUID()}.${alts.thumbnail.ext}`; thumbnailUrl = `${ baseUrl }/${ thumbnailKey }`; this.registerLogger.info(`uploading thumbnail: ${thumbnailKey}`); @@ -376,10 +375,8 @@ export class DriveService { if (type === 'image/apng') type = 'image/png'; if (!FILE_TYPE_BROWSERSAFE.includes(type)) type = 'application/octet-stream'; - const meta = await this.metaService.fetch(); - const params = { - Bucket: meta.objectStorageBucket, + Bucket: this.meta.objectStorageBucket, Key: key, Body: stream, ContentType: type, @@ -392,9 +389,9 @@ export class DriveService { // 許可されているファイル形式でしか拡張子をつけない ext ? correctFilename(filename, ext) : filename, ); - if (meta.objectStorageSetPublicRead) params.ACL = 'public-read'; + if (this.meta.objectStorageSetPublicRead) params.ACL = 'public-read'; - await this.s3Service.upload(meta, params) + await this.s3Service.upload(this.meta, params) .then( result => { if ('Bucket' in result) { // CompleteMultipartUploadCommandOutput @@ -460,32 +457,31 @@ export class DriveService { ext = null, }: AddFileArgs): Promise { let skipNsfwCheck = false; - const instance = await this.metaService.fetch(); const userRoleNSFW = user && (await this.roleService.getUserPolicies(user.id)).alwaysMarkNsfw; if (user == null) { skipNsfwCheck = true; } else if (userRoleNSFW) { skipNsfwCheck = true; } - if (instance.sensitiveMediaDetection === 'none') skipNsfwCheck = true; - if (user && instance.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; - if (user && instance.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; + if (this.meta.sensitiveMediaDetection === 'none') skipNsfwCheck = true; + if (user && this.meta.sensitiveMediaDetection === 'local' && this.userEntityService.isRemoteUser(user)) skipNsfwCheck = true; + if (user && this.meta.sensitiveMediaDetection === 'remote' && this.userEntityService.isLocalUser(user)) skipNsfwCheck = true; const info = await this.fileInfoService.getFileInfo(path, { skipSensitiveDetection: skipNsfwCheck, sensitiveThreshold: // 感度が高いほどしきい値は低くすることになる - instance.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : - instance.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : - instance.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : - instance.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : + this.meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 0.1 : + this.meta.sensitiveMediaDetectionSensitivity === 'high' ? 0.3 : + this.meta.sensitiveMediaDetectionSensitivity === 'low' ? 0.7 : + this.meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0.9 : 0.5, sensitiveThresholdForPorn: 0.75, - enableSensitiveMediaDetectionForVideos: instance.enableSensitiveMediaDetectionForVideos, + enableSensitiveMediaDetectionForVideos: this.meta.enableSensitiveMediaDetectionForVideos, }); this.registerLogger.info(`${JSON.stringify(info)}`); // 現状 false positive が多すぎて実用に耐えない - //if (info.porn && instance.disallowUploadWhenPredictedAsPorn) { + //if (info.porn && this.meta.disallowUploadWhenPredictedAsPorn) { // throw new IdentifiableError('282f77bf-5816-4f72-9264-aa14d8261a21', 'Detected as porn.'); //} @@ -589,9 +585,9 @@ export class DriveService { sensitive ?? false : false; - if (user && this.utilityService.isMediaSilencedHost(instance.mediaSilencedHosts, user.host)) file.isSensitive = true; + if (user && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) file.isSensitive = true; if (info.sensitive && profile!.autoSensitive) file.isSensitive = true; - if (info.sensitive && instance.setSensitiveFlagAutomatically) file.isSensitive = true; + if (info.sensitive && this.meta.setSensitiveFlagAutomatically) file.isSensitive = true; if (userRoleNSFW) file.isSensitive = true; if (url !== null) { @@ -652,7 +648,7 @@ export class DriveService { // ローカルユーザーのみ this.perUserDriveChart.update(file, true); } else { - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, true); } } @@ -798,7 +794,7 @@ export class DriveService { // ローカルユーザーのみ this.perUserDriveChart.update(file, false); } else { - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateDrive(file, false); } } @@ -820,14 +816,13 @@ export class DriveService { @bindThis public async deleteObjectStorageFile(key: string) { - const meta = await this.metaService.fetch(); try { const param = { - Bucket: meta.objectStorageBucket, + Bucket: this.meta.objectStorageBucket, Key: key, } as DeleteObjectCommandInput; - await this.s3Service.delete(meta, param); + await this.s3Service.delete(this.meta, param); } catch (err: any) { if (err.name === 'NoSuchKey') { this.deleteLogger.warn(`The object storage had no such key to delete: ${key}. Skipping this.`, err as Error); diff --git a/packages/backend/src/core/EmailService.ts b/packages/backend/src/core/EmailService.ts index 37fa58bb65..a176474b95 100644 --- a/packages/backend/src/core/EmailService.ts +++ b/packages/backend/src/core/EmailService.ts @@ -8,16 +8,14 @@ import * as nodemailer from 'nodemailer'; import juice from 'juice'; import { Inject, Injectable } from '@nestjs/common'; import { validate as validateEmail } from 'deep-email-validator'; -import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type Logger from '@/logger.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; -import { QueueService } from '@/core/QueueService.js'; @Injectable() export class EmailService { @@ -27,38 +25,37 @@ export class EmailService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, private loggerService: LoggerService, private utilityService: UtilityService, private httpRequestService: HttpRequestService, - private queueService: QueueService, ) { this.logger = this.loggerService.getLogger('email'); } @bindThis public async sendEmail(to: string, subject: string, html: string, text: string) { - const meta = await this.metaService.fetch(true); - - if (!meta.enableEmail) return; + if (!this.meta.enableEmail) return; const iconUrl = `${this.config.url}/static-assets/mi-white.png`; const emailSettingUrl = `${this.config.url}/settings/email`; - const enableAuth = meta.smtpUser != null && meta.smtpUser !== ''; + const enableAuth = this.meta.smtpUser != null && this.meta.smtpUser !== ''; const transporter = nodemailer.createTransport({ - host: meta.smtpHost, - port: meta.smtpPort, - secure: meta.smtpSecure, + host: this.meta.smtpHost, + port: this.meta.smtpPort, + secure: this.meta.smtpSecure, ignoreTLS: !enableAuth, proxy: this.config.proxySmtp, auth: enableAuth ? { - user: meta.smtpUser, - pass: meta.smtpPass, + user: this.meta.smtpUser, + pass: this.meta.smtpPass, } : undefined, } as any); @@ -127,7 +124,7 @@ export class EmailService {
- +

${ subject }

@@ -148,7 +145,7 @@ export class EmailService { try { // TODO: htmlサニタイズ const info = await transporter.sendMail({ - from: meta.email!, + from: this.meta.email!, to: to, subject: subject, text: text, @@ -167,8 +164,6 @@ export class EmailService { available: boolean; reason: null | 'used' | 'format' | 'disposable' | 'mx' | 'smtp' | 'banned' | 'network' | 'blacklist'; }> { - const meta = await this.metaService.fetch(); - const exist = await this.userProfilesRepository.countBy({ emailVerified: true, email: emailAddress, @@ -186,11 +181,11 @@ export class EmailService { reason?: string | null, } = { valid: true, reason: null }; - if (meta.enableActiveEmailValidation) { - if (meta.enableVerifymailApi && meta.verifymailAuthKey != null) { - validated = await this.verifyMail(emailAddress, meta.verifymailAuthKey); - } else if (meta.enableTruemailApi && meta.truemailInstance && meta.truemailAuthKey != null) { - validated = await this.trueMail(meta.truemailInstance, emailAddress, meta.truemailAuthKey); + if (this.meta.enableActiveEmailValidation) { + if (this.meta.enableVerifymailApi && this.meta.verifymailAuthKey != null) { + validated = await this.verifyMail(emailAddress, this.meta.verifymailAuthKey); + } else if (this.meta.enableTruemailApi && this.meta.truemailInstance && this.meta.truemailAuthKey != null) { + validated = await this.trueMail(this.meta.truemailInstance, emailAddress, this.meta.truemailAuthKey); } else { validated = await validateEmail({ email: emailAddress, @@ -220,7 +215,7 @@ export class EmailService { } const emailDomain: string = emailAddress.split('@')[1]; - const isBanned = this.utilityService.isBlockedHost(meta.bannedEmailDomains, emailDomain); + const isBanned = this.utilityService.isBlockedHost(this.meta.bannedEmailDomains, emailDomain); if (isBanned) { return { diff --git a/packages/backend/src/core/HashtagService.ts b/packages/backend/src/core/HashtagService.ts index eb192ee6da..793bbeecb1 100644 --- a/packages/backend/src/core/HashtagService.ts +++ b/packages/backend/src/core/HashtagService.ts @@ -10,16 +10,18 @@ import type { MiUser } from '@/models/User.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; import { IdService } from '@/core/IdService.js'; import type { MiHashtag } from '@/models/Hashtag.js'; -import type { HashtagsRepository } from '@/models/_.js'; +import type { HashtagsRepository, MiMeta } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { FeaturedService } from '@/core/FeaturedService.js'; -import { MetaService } from '@/core/MetaService.js'; import { UtilityService } from '@/core/UtilityService.js'; @Injectable() export class HashtagService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.redis) private redisClient: Redis.Redis, // TODO: 専用のRedisサーバーを設定できるようにする @@ -29,7 +31,6 @@ export class HashtagService { private userEntityService: UserEntityService, private featuredService: FeaturedService, private idService: IdService, - private metaService: MetaService, private utilityService: UtilityService, ) { } @@ -160,10 +161,9 @@ export class HashtagService { @bindThis public async updateHashtagsRanking(hashtag: string, userId: MiUser['id']): Promise { - const instance = await this.metaService.fetch(); - const hiddenTags = instance.hiddenTags.map(t => normalizeForSearch(t)); + const hiddenTags = this.meta.hiddenTags.map(t => normalizeForSearch(t)); if (hiddenTags.includes(hashtag)) return; - if (this.utilityService.isKeyWordIncluded(hashtag, instance.sensitiveWords)) return; + if (this.utilityService.isKeyWordIncluded(hashtag, this.meta.sensitiveWords)) return; // YYYYMMDDHHmm (10分間隔) const now = new Date(); diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 1d8d248322..18efc9d562 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -8,13 +8,12 @@ import * as mfm from 'mfm-js'; import { In, DataSource, IsNull, LessThan } from 'typeorm'; import * as Redis from 'ioredis'; import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; -import RE2 from 're2'; import { extractMentions } from '@/misc/extract-mentions.js'; import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js'; import { extractHashtags } from '@/misc/extract-hashtags.js'; import type { IMentionedRemoteUsers } from '@/models/Note.js'; import { MiNote } from '@/models/Note.js'; -import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { ChannelFollowingsRepository, ChannelsRepository, FollowingsRepository, InstancesRepository, MiFollowing, MiMeta, MutingsRepository, NotesRepository, NoteThreadMutingsRepository, UserListMembershipsRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiApp } from '@/models/App.js'; import { concat } from '@/misc/prelude/array.js'; @@ -23,11 +22,8 @@ import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { IPoll } from '@/models/Poll.js'; import { MiPoll } from '@/models/Poll.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import { checkWordMute } from '@/misc/check-word-mute.js'; import type { MiChannel } from '@/models/Channel.js'; import { normalizeForSearch } from '@/misc/normalize-for-search.js'; -import { MemorySingleCache } from '@/misc/cache.js'; -import type { MiUserProfile } from '@/models/UserProfile.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -51,7 +47,6 @@ import { RemoteUserResolveService } from '@/core/RemoteUserResolveService.js'; import { bindThis } from '@/decorators.js'; import { DB_MAX_NOTE_TEXT_LENGTH } from '@/const.js'; import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { FeaturedService } from '@/core/FeaturedService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; @@ -156,6 +151,9 @@ export class NoteCreateService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.db) private db: DataSource, @@ -210,7 +208,6 @@ export class NoteCreateService implements OnApplicationShutdown { private apDeliverManagerService: ApDeliverManagerService, private apRendererService: ApRendererService, private roleService: RoleService, - private metaService: MetaService, private searchService: SearchService, private notesChart: NotesChart, private perUserNotesChart: PerUserNotesChart, @@ -251,10 +248,8 @@ export class NoteCreateService implements OnApplicationShutdown { if (data.channel != null) data.visibleUsers = []; if (data.channel != null) data.localOnly = true; - const meta = await this.metaService.fetch(); - if (data.visibility === 'public' && data.channel == null) { - const sensitiveWords = meta.sensitiveWords; + const sensitiveWords = this.meta.sensitiveWords; if (this.utilityService.isKeyWordIncluded(data.cw ?? data.text ?? '', sensitiveWords)) { data.visibility = 'home'; } else if ((await this.roleService.getUserPolicies(user.id)).canPublicNote === false) { @@ -262,17 +257,17 @@ export class NoteCreateService implements OnApplicationShutdown { } } - const hasProhibitedWords = await this.checkProhibitedWordsContain({ + const hasProhibitedWords = this.checkProhibitedWordsContain({ cw: data.cw, text: data.text, pollChoices: data.poll?.choices, - }, meta.prohibitedWords); + }, this.meta.prohibitedWords); if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } - const inSilencedInstance = this.utilityService.isSilencedHost(meta.silencedHosts, user.host); + const inSilencedInstance = this.utilityService.isSilencedHost(this.meta.silencedHosts, user.host); if (data.visibility === 'public' && inSilencedInstance && user.host !== null) { data.visibility = 'home'; @@ -365,7 +360,7 @@ export class NoteCreateService implements OnApplicationShutdown { } // if the host is media-silenced, custom emojis are not allowed - if (this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, user.host)) emojis = []; + if (this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, user.host)) emojis = []; tags = tags.filter(tag => Array.from(tag).length <= 128).splice(0, 32); @@ -506,10 +501,8 @@ export class NoteCreateService implements OnApplicationShutdown { host: MiUser['host']; isBot: MiUser['isBot']; }, data: Option, silent: boolean, tags: string[], mentionedUsers: MinimumUser[]) { - const meta = await this.metaService.fetch(); - this.notesChart.update(note, true); - if (note.visibility !== 'specified' && (meta.enableChartsForRemoteUser || (user.host == null))) { + if (note.visibility !== 'specified' && (this.meta.enableChartsForRemoteUser || (user.host == null))) { this.perUserNotesChart.update(user, note, true); } @@ -517,7 +510,7 @@ export class NoteCreateService implements OnApplicationShutdown { if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'notesCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, true); } }); @@ -853,15 +846,14 @@ export class NoteCreateService implements OnApplicationShutdown { @bindThis private async pushToTl(note: MiNote, user: { id: MiUser['id']; host: MiUser['host']; }) { - const meta = await this.metaService.fetch(); - if (!meta.enableFanoutTimeline) return; + if (!this.meta.enableFanoutTimeline) return; const r = this.redisForTimelines.pipeline(); if (note.channelId) { this.fanoutTimelineService.push(`channelTimeline:${note.channelId}`, note.id, this.config.perChannelMaxNoteCacheCount, r); - this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithChannel:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); const channelFollowings = await this.channelFollowingsRepository.find({ where: { @@ -871,9 +863,9 @@ export class NoteCreateService implements OnApplicationShutdown { }); for (const channelFollowing of channelFollowings) { - this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${channelFollowing.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } else { @@ -911,9 +903,9 @@ export class NoteCreateService implements OnApplicationShutdown { if (!following.withReplies) continue; } - this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${following.followerId}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } @@ -930,25 +922,25 @@ export class NoteCreateService implements OnApplicationShutdown { if (!userListMembership.withReplies) continue; } - this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax, r); + this.fanoutTimelineService.push(`userListTimeline:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, meta.perUserListTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userListTimelineWithFiles:${userListMembership.userListId}`, note.id, this.meta.perUserListTimelineCacheMax / 2, r); } } // 自分自身のHTL if (note.userHost == null) { if (note.visibility !== 'specified' || !note.visibleUserIds.some(v => v === user.id)) { - this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax, r); + this.fanoutTimelineService.push(`homeTimeline:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, meta.perUserHomeTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`homeTimelineWithFiles:${user.id}`, note.id, this.meta.perUserHomeTimelineCacheMax / 2, r); } } } // 自分自身以外への返信 if (isReply(note)) { - this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimelineWithReplies:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.visibility === 'public' && note.userHost == null) { this.fanoutTimelineService.push('localTimelineWithReplies', note.id, 300, r); @@ -957,9 +949,9 @@ export class NoteCreateService implements OnApplicationShutdown { } } } else { - this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax : meta.perRemoteUserUserTimelineCacheMax, r); + this.fanoutTimelineService.push(`userTimeline:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax : this.meta.perRemoteUserUserTimelineCacheMax, r); if (note.fileIds.length > 0) { - this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? meta.perLocalUserUserTimelineCacheMax / 2 : meta.perRemoteUserUserTimelineCacheMax / 2, r); + this.fanoutTimelineService.push(`userTimelineWithFiles:${user.id}`, note.id, note.userHost == null ? this.meta.perLocalUserUserTimelineCacheMax / 2 : this.meta.perRemoteUserUserTimelineCacheMax / 2, r); } if (note.visibility === 'public' && note.userHost == null) { @@ -1018,9 +1010,9 @@ export class NoteCreateService implements OnApplicationShutdown { } } - public async checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { + public checkProhibitedWordsContain(content: Parameters[0], prohibitedWords?: string[]) { if (prohibitedWords == null) { - prohibitedWords = (await this.metaService.fetch()).prohibitedWords; + prohibitedWords = this.meta.prohibitedWords; } if ( diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts index b7c01c64c8..f9f8ace386 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -7,7 +7,7 @@ import { Brackets, In } from 'typeorm'; import { Injectable, Inject } from '@nestjs/common'; import type { MiUser, MiLocalUser, MiRemoteUser } from '@/models/User.js'; import type { MiNote, IMentionedRemoteUsers } from '@/models/Note.js'; -import type { InstancesRepository, NotesRepository, UsersRepository } from '@/models/_.js'; +import type { InstancesRepository, MiMeta, NotesRepository, UsersRepository } from '@/models/_.js'; import { RelayService } from '@/core/RelayService.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { DI } from '@/di-symbols.js'; @@ -19,9 +19,7 @@ import { GlobalEventService } from '@/core/GlobalEventService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; -import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { SearchService } from '@/core/SearchService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js'; import { isQuote, isRenote } from '@/misc/is-renote.js'; @@ -32,6 +30,9 @@ export class NoteDeleteService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -42,13 +43,11 @@ export class NoteDeleteService { private instancesRepository: InstancesRepository, private userEntityService: UserEntityService, - private noteEntityService: NoteEntityService, private globalEventService: GlobalEventService, private relayService: RelayService, private federatedInstanceService: FederatedInstanceService, private apRendererService: ApRendererService, private apDeliverManagerService: ApDeliverManagerService, - private metaService: MetaService, private searchService: SearchService, private moderationLogService: ModerationLogService, private notesChart: NotesChart, @@ -102,17 +101,15 @@ export class NoteDeleteService { } //#endregion - const meta = await this.metaService.fetch(); - this.notesChart.update(note, false); - if (meta.enableChartsForRemoteUser || (user.host == null)) { + if (this.meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserNotesChart.update(user, note, false); } if (this.userEntityService.isRemoteUser(user)) { this.federatedInstanceService.fetch(user.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateNote(i.host, note, false); } }); diff --git a/packages/backend/src/core/ProxyAccountService.ts b/packages/backend/src/core/ProxyAccountService.ts index 71d663bf90..c3ff2a68d3 100644 --- a/packages/backend/src/core/ProxyAccountService.ts +++ b/packages/backend/src/core/ProxyAccountService.ts @@ -4,26 +4,25 @@ */ import { Inject, Injectable } from '@nestjs/common'; -import type { UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsersRepository } from '@/models/_.js'; import type { MiLocalUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; @Injectable() export class ProxyAccountService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, - - private metaService: MetaService, ) { } @bindThis public async fetch(): Promise { - const meta = await this.metaService.fetch(); - if (meta.proxyAccountId == null) return null; - return await this.usersRepository.findOneByOrFail({ id: meta.proxyAccountId }) as MiLocalUser; + if (this.meta.proxyAccountId == null) return null; + return await this.usersRepository.findOneByOrFail({ id: this.meta.proxyAccountId }) as MiLocalUser; } } diff --git a/packages/backend/src/core/PushNotificationService.ts b/packages/backend/src/core/PushNotificationService.ts index 6a845b951d..1479bb00d9 100644 --- a/packages/backend/src/core/PushNotificationService.ts +++ b/packages/backend/src/core/PushNotificationService.ts @@ -10,8 +10,7 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import type { Packed } from '@/misc/json-schema.js'; import { getNoteSummary } from '@/misc/get-note-summary.js'; -import type { MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; -import { MetaService } from '@/core/MetaService.js'; +import type { MiMeta, MiSwSubscription, SwSubscriptionsRepository } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { RedisKVCache } from '@/misc/cache.js'; @@ -54,13 +53,14 @@ export class PushNotificationService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.redis) private redisClient: Redis.Redis, @Inject(DI.swSubscriptionsRepository) private swSubscriptionsRepository: SwSubscriptionsRepository, - - private metaService: MetaService, ) { this.subscriptionsCache = new RedisKVCache(this.redisClient, 'userSwSubscriptions', { lifetime: 1000 * 60 * 60 * 1, // 1h @@ -73,14 +73,12 @@ export class PushNotificationService implements OnApplicationShutdown { @bindThis public async pushNotification(userId: string, type: T, body: PushNotificationsTypes[T]) { - const meta = await this.metaService.fetch(); - - if (!meta.enableServiceWorker || meta.swPublicKey == null || meta.swPrivateKey == null) return; + if (!this.meta.enableServiceWorker || this.meta.swPublicKey == null || this.meta.swPrivateKey == null) return; // アプリケーションの連絡先と、サーバーサイドの鍵ペアの情報を登録 push.setVapidDetails(this.config.url, - meta.swPublicKey, - meta.swPrivateKey); + this.meta.swPublicKey, + this.meta.swPrivateKey); const subscriptions = await this.subscriptionsCache.fetch(userId); diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts index db8fe1a838..f0a2876450 100644 --- a/packages/backend/src/core/ReactionService.ts +++ b/packages/backend/src/core/ReactionService.ts @@ -5,7 +5,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js'; +import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository, MiMeta } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { MiRemoteUser, MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; @@ -20,7 +20,6 @@ import { ApDeliverManagerService } from '@/core/activitypub/ApDeliverManagerServ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; @@ -71,6 +70,9 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/; @Injectable() export class ReactionService { constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -84,7 +86,6 @@ export class ReactionService { private emojisRepository: EmojisRepository, private utilityService: UtilityService, - private metaService: MetaService, private customEmojiService: CustomEmojiService, private roleService: RoleService, private userEntityService: UserEntityService, @@ -103,8 +104,6 @@ export class ReactionService { @bindThis public async create(user: { id: MiUser['id']; host: MiUser['host']; isBot: MiUser['isBot'] }, note: MiNote, _reaction?: string | null) { - const meta = await this.metaService.fetch(); - // Check blocking if (note.userId !== user.id) { const blocked = await this.userBlockingService.checkBlocked(note.userId, user.id); @@ -150,7 +149,7 @@ export class ReactionService { } // for media silenced host, custom emoji reactions are not allowed - if (reacterHost != null && this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, reacterHost)) { + if (reacterHost != null && this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, reacterHost)) { reaction = FALLBACK; } } else { @@ -195,7 +194,7 @@ export class ReactionService { } // Increment reactions count - if (meta.enableReactionsBuffering) { + if (this.meta.enableReactionsBuffering) { await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache); } else { const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`; @@ -228,7 +227,7 @@ export class ReactionService { } } - if (meta.enableChartsForRemoteUser || (user.host == null)) { + if (this.meta.enableChartsForRemoteUser || (user.host == null)) { this.perUserReactionsChart.update(user, note); } @@ -305,10 +304,8 @@ export class ReactionService { throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted'); } - const meta = await this.metaService.fetch(); - // Decrement reactions count - if (meta.enableReactionsBuffering) { + if (this.meta.enableReactionsBuffering) { await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction); } else { const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 24752edcf6..583eea1a34 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -8,6 +8,7 @@ import * as Redis from 'ioredis'; import { In } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import type { + MiMeta, MiRole, MiRoleAssignment, RoleAssignmentsRepository, @@ -18,7 +19,6 @@ import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import type { MiUser } from '@/models/User.js'; import { DI } from '@/di-symbols.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { RoleCondFormulaValue } from '@/models/Role.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; @@ -111,8 +111,8 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { constructor( private moduleRef: ModuleRef, - @Inject(DI.redis) - private redisClient: Redis.Redis, + @Inject(DI.meta) + private meta: MiMeta, @Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis, @@ -129,7 +129,6 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @Inject(DI.roleAssignmentsRepository) private roleAssignmentsRepository: RoleAssignmentsRepository, - private metaService: MetaService, private cacheService: CacheService, private userEntityService: UserEntityService, private globalEventService: GlobalEventService, @@ -349,8 +348,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { @bindThis public async getUserPolicies(userId: MiUser['id'] | null): Promise { - const meta = await this.metaService.fetch(); - const basePolicies = { ...DEFAULT_POLICIES, ...meta.policies }; + const basePolicies = { ...DEFAULT_POLICIES, ...this.meta.policies }; if (userId == null) return basePolicies; diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts index de45898328..cc8a3d6461 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -8,7 +8,7 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { DataSource, IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; +import type { MiMeta, UsedUsernamesRepository, UsersRepository } from '@/models/_.js'; import { MiUser } from '@/models/User.js'; import { MiUserProfile } from '@/models/UserProfile.js'; import { IdService } from '@/core/IdService.js'; @@ -20,7 +20,6 @@ import { InstanceActorService } from '@/core/InstanceActorService.js'; import { bindThis } from '@/decorators.js'; import UsersChart from '@/core/chart/charts/users.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { UserService } from '@/core/UserService.js'; @Injectable() @@ -29,6 +28,9 @@ export class SignupService { @Inject(DI.db) private db: DataSource, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -39,7 +41,6 @@ export class SignupService { private userService: UserService, private userEntityService: UserEntityService, private idService: IdService, - private metaService: MetaService, private instanceActorService: InstanceActorService, private usersChart: UsersChart, ) { @@ -88,8 +89,7 @@ export class SignupService { const isTheFirstUser = !await this.instanceActorService.realLocalUsersPresent(); if (!opts.ignorePreservedUsernames && !isTheFirstUser) { - const instance = await this.metaService.fetch(true); - 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 Error('USED_USERNAME'); } diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts index 6aab8fde70..3f1c6b7125 100644 --- a/packages/backend/src/core/UserFollowingService.ts +++ b/packages/backend/src/core/UserFollowingService.ts @@ -13,23 +13,20 @@ import PerUserFollowingChart from '@/core/chart/charts/per-user-following.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { IdService } from '@/core/IdService.js'; import { isDuplicateKeyValueError } from '@/misc/is-duplicate-key-value-error.js'; -import type { Packed } from '@/misc/json-schema.js'; import InstanceChart from '@/core/chart/charts/instance.js'; import { FederatedInstanceService } from '@/core/FederatedInstanceService.js'; import { UserWebhookService } from '@/core/UserWebhookService.js'; import { NotificationService } from '@/core/NotificationService.js'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, FollowRequestsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { ApRendererService } from '@/core/activitypub/ApRendererService.js'; import { bindThis } from '@/decorators.js'; import { UserBlockingService } from '@/core/UserBlockingService.js'; -import { MetaService } from '@/core/MetaService.js'; import { CacheService } from '@/core/CacheService.js'; import type { Config } from '@/config.js'; import { AccountMoveService } from '@/core/AccountMoveService.js'; import { UtilityService } from '@/core/UtilityService.js'; -import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import type { ThinUser } from '@/queue/types.js'; import Logger from '../logger.js'; @@ -58,6 +55,9 @@ export class UserFollowingService implements OnModuleInit { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -79,13 +79,11 @@ export class UserFollowingService implements OnModuleInit { private idService: IdService, private queueService: QueueService, private globalEventService: GlobalEventService, - private metaService: MetaService, private notificationService: NotificationService, private federatedInstanceService: FederatedInstanceService, private webhookService: UserWebhookService, private apRendererService: ApRendererService, private accountMoveService: AccountMoveService, - private fanoutTimelineService: FanoutTimelineService, private perUserFollowingChart: PerUserFollowingChart, private instanceChart: InstanceChart, ) { @@ -172,7 +170,7 @@ export class UserFollowingService implements OnModuleInit { followee.isLocked || (followeeProfile.carefulBot && follower.isBot) || (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee) && process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING !== 'true') || - (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost((await this.metaService.fetch()).silencedHosts, follower.host)) + (this.userEntityService.isLocalUser(followee) && this.userEntityService.isRemoteUser(follower) && this.utilityService.isSilencedHost(this.meta.silencedHosts, follower.host)) ) { let autoAccept = false; @@ -307,14 +305,14 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowing(i.host, true); } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.increment({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, true); } }); @@ -439,14 +437,14 @@ export class UserFollowingService implements OnModuleInit { if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) { this.federatedInstanceService.fetch(follower.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowing(i.host, false); } }); } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) { this.federatedInstanceService.fetch(followee.host).then(async i => { this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.updateFollowers(i.host, false); } }); diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts index ec9f4484a4..a40c6ff1c9 100644 --- a/packages/backend/src/core/WebAuthnService.ts +++ b/packages/backend/src/core/WebAuthnService.ts @@ -12,10 +12,9 @@ import { } from '@simplewebauthn/server'; import { AttestationFormat, isoCBOR, isoUint8Array } from '@simplewebauthn/server/helpers'; import { DI } from '@/di-symbols.js'; -import type { UserSecurityKeysRepository } from '@/models/_.js'; +import type { MiMeta, UserSecurityKeysRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiUser } from '@/models/_.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import type { @@ -23,7 +22,6 @@ import type { AuthenticatorTransportFuture, CredentialDeviceType, PublicKeyCredentialCreationOptionsJSON, - PublicKeyCredentialDescriptorFuture, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, } from '@simplewebauthn/types'; @@ -31,33 +29,33 @@ import type { @Injectable() export class WebAuthnService { constructor( - @Inject(DI.redis) - private redisClient: Redis.Redis, - @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + + @Inject(DI.redis) + private redisClient: Redis.Redis, + @Inject(DI.userSecurityKeysRepository) private userSecurityKeysRepository: UserSecurityKeysRepository, - - private metaService: MetaService, ) { } @bindThis - public async getRelyingParty(): Promise<{ origin: string; rpId: string; rpName: string; rpIcon?: string; }> { - const instance = await this.metaService.fetch(); + public getRelyingParty(): { origin: string; rpId: string; rpName: string; rpIcon?: string; } { return { origin: this.config.url, rpId: this.config.hostname, - rpName: instance.name ?? this.config.host, - rpIcon: instance.iconUrl ?? undefined, + rpName: this.meta.name ?? this.config.host, + rpIcon: this.meta.iconUrl ?? undefined, }; } @bindThis public async initiateRegistration(userId: MiUser['id'], userName: string, userDisplayName?: string): Promise { - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -104,7 +102,7 @@ export class WebAuthnService { await this.redisClient.del(`webauthn:challenge:${userId}`); - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); let verification; try { @@ -143,7 +141,7 @@ export class WebAuthnService { @bindThis public async initiateAuthentication(userId: MiUser['id']): Promise { - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); const keys = await this.userSecurityKeysRepository.findBy({ userId: userId, }); @@ -209,7 +207,7 @@ export class WebAuthnService { } } - const relyingParty = await this.getRelyingParty(); + const relyingParty = this.getRelyingParty(); let verification; try { diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index e2164fec1d..90da032895 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -17,14 +17,13 @@ import { NoteCreateService } from '@/core/NoteCreateService.js'; import { concat, toArray, toSingle, unique } from '@/misc/prelude/array.js'; import { AppLockService } from '@/core/AppLockService.js'; import type Logger from '@/logger.js'; -import { MetaService } from '@/core/MetaService.js'; import { IdService } from '@/core/IdService.js'; import { StatusError } from '@/misc/status-error.js'; import { UtilityService } from '@/core/UtilityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { QueueService } from '@/core/QueueService.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import type { MiRemoteUser } from '@/models/User.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; @@ -48,6 +47,9 @@ export class ApInboxService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -64,7 +66,6 @@ export class ApInboxService { private noteEntityService: NoteEntityService, private utilityService: UtilityService, private idService: IdService, - private metaService: MetaService, private abuseReportService: AbuseReportService, private userFollowingService: UserFollowingService, private apAudienceService: ApAudienceService, @@ -290,8 +291,7 @@ export class ApInboxService { } // アナウンス先をブロックしてたら中断 - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; const unlock = await this.appLockService.getApLock(uri); diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index bb3c40f093..fdef7a8ffd 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; -import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository } from '@/models/_.js'; +import type { NotesRepository, PollsRepository, NoteReactionsRepository, UsersRepository, FollowRequestsRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { DI } from '@/di-symbols.js'; import { UtilityService } from '@/core/UtilityService.js'; @@ -29,6 +28,7 @@ export class Resolver { constructor( private config: Config, + private meta: MiMeta, private usersRepository: UsersRepository, private notesRepository: NotesRepository, private pollsRepository: PollsRepository, @@ -36,7 +36,6 @@ export class Resolver { private followRequestsRepository: FollowRequestsRepository, private utilityService: UtilityService, private instanceActorService: InstanceActorService, - private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, @@ -94,8 +93,7 @@ export class Resolver { return await this.resolveLocal(value); } - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, host)) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, host)) { throw new Error('Instance is blocked'); } @@ -178,6 +176,9 @@ export class ApResolverService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -195,7 +196,6 @@ export class ApResolverService { private utilityService: UtilityService, private instanceActorService: InstanceActorService, - private metaService: MetaService, private apRequestService: ApRequestService, private httpRequestService: HttpRequestService, private apRendererService: ApRendererService, @@ -208,6 +208,7 @@ export class ApResolverService { public createResolver(): Resolver { return new Resolver( this.config, + this.meta, this.usersRepository, this.notesRepository, this.pollsRepository, @@ -215,7 +216,6 @@ export class ApResolverService { this.followRequestsRepository, this.utilityService, this.instanceActorService, - this.metaService, this.apRequestService, this.httpRequestService, this.apRendererService, diff --git a/packages/backend/src/core/activitypub/models/ApImageService.ts b/packages/backend/src/core/activitypub/models/ApImageService.ts index 3691967270..e7ece87b01 100644 --- a/packages/backend/src/core/activitypub/models/ApImageService.ts +++ b/packages/backend/src/core/activitypub/models/ApImageService.ts @@ -5,10 +5,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; -import type { DriveFilesRepository } from '@/models/_.js'; +import type { DriveFilesRepository, MiMeta } from '@/models/_.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; -import { MetaService } from '@/core/MetaService.js'; import { truncate } from '@/misc/truncate.js'; import { DB_MAX_IMAGE_COMMENT_LENGTH } from '@/const.js'; import { DriveService } from '@/core/DriveService.js'; @@ -24,10 +23,12 @@ export class ApImageService { private logger: Logger; constructor( + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.driveFilesRepository) private driveFilesRepository: DriveFilesRepository, - private metaService: MetaService, private apResolverService: ApResolverService, private driveService: DriveService, private apLoggerService: ApLoggerService, @@ -63,12 +64,10 @@ export class ApImageService { this.logger.info(`Creating the Image: ${image.url}`); - const instance = await this.metaService.fetch(); - // Cache if remote file cache is on AND either // 1. remote sensitive file is also on // 2. or the image is not sensitive - const shouldBeCached = instance.cacheRemoteFiles && (instance.cacheRemoteSensitiveFiles || !image.sensitive); + const shouldBeCached = this.meta.cacheRemoteFiles && (this.meta.cacheRemoteSensitiveFiles || !image.sensitive); const file = await this.driveService.uploadFromUrl({ url: image.url, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 5b75da22a0..00acb19a0f 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -6,13 +6,12 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { In } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { PollsRepository, EmojisRepository } from '@/models/_.js'; +import type { PollsRepository, EmojisRepository, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiRemoteUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; import { toArray, toSingle, unique } from '@/misc/prelude/array.js'; import type { MiEmoji } from '@/models/Emoji.js'; -import { MetaService } from '@/core/MetaService.js'; import { AppLockService } from '@/core/AppLockService.js'; import type { MiDriveFile } from '@/models/DriveFile.js'; import { NoteCreateService } from '@/core/NoteCreateService.js'; @@ -46,6 +45,9 @@ export class ApNoteService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.pollsRepository) private pollsRepository: PollsRepository, @@ -65,7 +67,6 @@ export class ApNoteService { private apMentionService: ApMentionService, private apImageService: ApImageService, private apQuestionService: ApQuestionService, - private metaService: MetaService, private appLockService: AppLockService, private pollService: PollService, private noteCreateService: NoteCreateService, @@ -182,7 +183,7 @@ export class ApNoteService { /** * 禁止ワードチェック */ - const hasProhibitedWords = await this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); + const hasProhibitedWords = this.noteCreateService.checkProhibitedWordsContain({ cw, text, pollChoices: poll?.choices }); if (hasProhibitedWords) { throw new IdentifiableError('689ee33f-f97c-479a-ac49-1b9f8140af99', 'Note contains prohibited words'); } @@ -336,8 +337,7 @@ export class ApNoteService { const uri = getApId(value); // ブロックしていたら中断 - const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, this.utilityService.extractDbHost(uri))) { throw new StatusError('blocked host', 451); } diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts index f3ddf3952c..39c18e5e15 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -8,7 +8,7 @@ import promiseLimit from 'promise-limit'; import { DataSource } from 'typeorm'; import { ModuleRef } from '@nestjs/core'; import { DI } from '@/di-symbols.js'; -import type { FollowingsRepository, InstancesRepository, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository, MiMeta, UserProfilesRepository, UserPublickeysRepository, UsersRepository } from '@/models/_.js'; import type { Config } from '@/config.js'; import type { MiLocalUser, MiRemoteUser } from '@/models/User.js'; import { MiUser } from '@/models/User.js'; @@ -35,7 +35,6 @@ import type { UtilityService } from '@/core/UtilityService.js'; import type { UserEntityService } from '@/core/entities/UserEntityService.js'; import { bindThis } from '@/decorators.js'; import { RoleService } from '@/core/RoleService.js'; -import { MetaService } from '@/core/MetaService.js'; import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js'; import type { AccountMoveService } from '@/core/AccountMoveService.js'; import { checkHttps } from '@/misc/check-https.js'; @@ -62,7 +61,6 @@ export class ApPersonService implements OnModuleInit { private driveFileEntityService: DriveFileEntityService; private idService: IdService; private globalEventService: GlobalEventService; - private metaService: MetaService; private federatedInstanceService: FederatedInstanceService; private fetchInstanceMetadataService: FetchInstanceMetadataService; private cacheService: CacheService; @@ -84,6 +82,9 @@ export class ApPersonService implements OnModuleInit { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.db) private db: DataSource, @@ -112,7 +113,6 @@ export class ApPersonService implements OnModuleInit { this.driveFileEntityService = this.moduleRef.get('DriveFileEntityService'); this.idService = this.moduleRef.get('IdService'); this.globalEventService = this.moduleRef.get('GlobalEventService'); - this.metaService = this.moduleRef.get('MetaService'); this.federatedInstanceService = this.moduleRef.get('FederatedInstanceService'); this.fetchInstanceMetadataService = this.moduleRef.get('FetchInstanceMetadataService'); this.cacheService = this.moduleRef.get('CacheService'); @@ -407,10 +407,10 @@ export class ApPersonService implements OnModuleInit { this.cacheService.uriPersonCache.set(user.uri, user); // Register host - this.federatedInstanceService.fetch(host).then(async i => { + this.federatedInstanceService.fetch(host).then(i => { this.instancesRepository.increment({ id: i.id }, 'usersCount', 1); this.fetchInstanceMetadataService.fetchInstanceMetadata(i); - if ((await this.metaService.fetch()).enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.newUser(i.host); } }); diff --git a/packages/backend/src/core/chart/charts/federation.ts b/packages/backend/src/core/chart/charts/federation.ts index f40a26495d..c9b43cc66d 100644 --- a/packages/backend/src/core/chart/charts/federation.ts +++ b/packages/backend/src/core/chart/charts/federation.ts @@ -5,10 +5,9 @@ import { Injectable, Inject } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import type { FollowingsRepository, InstancesRepository } from '@/models/_.js'; +import type { FollowingsRepository, InstancesRepository, MiMeta } from '@/models/_.js'; import { AppLockService } from '@/core/AppLockService.js'; import { DI } from '@/di-symbols.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import Chart from '../core.js'; import { ChartLoggerService } from '../ChartLoggerService.js'; @@ -24,13 +23,15 @@ export default class FederationChart extends Chart { // eslint-di @Inject(DI.db) private db: DataSource, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.followingsRepository) private followingsRepository: FollowingsRepository, @Inject(DI.instancesRepository) private instancesRepository: InstancesRepository, - private metaService: MetaService, private appLockService: AppLockService, private chartLoggerService: ChartLoggerService, ) { @@ -43,8 +44,6 @@ export default class FederationChart extends Chart { // eslint-di } protected async tickMinor(): Promise>> { - const meta = await this.metaService.fetch(); - const suspendedInstancesQuery = this.instancesRepository.createQueryBuilder('instance') .select('instance.host') .where('instance.suspensionState != \'none\''); @@ -65,21 +64,21 @@ export default class FederationChart extends Chart { // eslint-di this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followerHost)') .where('following.followerHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followerHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followerHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .getRawOne() .then(x => parseInt(x.count, 10)), this.followingsRepository.createQueryBuilder('following') .select('COUNT(DISTINCT following.followeeHost)') .where('following.followeeHost IS NOT NULL') - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'following.followeeHost NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere(`following.followeeHost NOT IN (${ suspendedInstancesQuery.getQuery() })`) .andWhere(`following.followeeHost IN (${ pubsubSubQuery.getQuery() })`) .setParameters(pubsubSubQuery.getParameters()) @@ -88,7 +87,7 @@ export default class FederationChart extends Chart { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ subInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() @@ -96,7 +95,7 @@ export default class FederationChart extends Chart { // eslint-di this.instancesRepository.createQueryBuilder('instance') .select('COUNT(instance.id)') .where(`instance.host IN (${ pubInstancesQuery.getQuery() })`) - .andWhere(meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) + .andWhere(this.meta.blockedHosts.length === 0 ? '1=1' : 'instance.host NOT ILIKE ALL(ARRAY[:...blocked])', { blocked: this.meta.blockedHosts.flatMap(x => [x, `%.${x}`]) }) .andWhere('instance.suspensionState = \'none\'') .andWhere('instance.isNotResponding = false') .getRawOne() diff --git a/packages/backend/src/core/entities/InstanceEntityService.ts b/packages/backend/src/core/entities/InstanceEntityService.ts index 4956bc22ce..284537b986 100644 --- a/packages/backend/src/core/entities/InstanceEntityService.ts +++ b/packages/backend/src/core/entities/InstanceEntityService.ts @@ -3,19 +3,22 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import type { Packed } from '@/misc/json-schema.js'; import type { MiInstance } from '@/models/Instance.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UtilityService } from '@/core/UtilityService.js'; import { RoleService } from '@/core/RoleService.js'; import { MiUser } from '@/models/User.js'; +import { DI } from '@/di-symbols.js'; +import { MiMeta } from '@/models/_.js'; @Injectable() export class InstanceEntityService { constructor( - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, + private roleService: RoleService, private utilityService: UtilityService, @@ -27,7 +30,6 @@ export class InstanceEntityService { instance: MiInstance, me?: { id: MiUser['id']; } | null | undefined, ): Promise> { - const meta = await this.metaService.fetch(); const iAmModerator = me ? await this.roleService.isModerator(me as MiUser) : false; return { @@ -41,7 +43,7 @@ export class InstanceEntityService { isNotResponding: instance.isNotResponding, isSuspended: instance.suspensionState !== 'none', suspensionState: instance.suspensionState, - isBlocked: this.utilityService.isBlockedHost(meta.blockedHosts, instance.host), + isBlocked: this.utilityService.isBlockedHost(this.meta.blockedHosts, instance.host), softwareName: instance.softwareName, softwareVersion: instance.softwareVersion, openRegistrations: instance.openRegistrations, @@ -49,8 +51,8 @@ export class InstanceEntityService { description: instance.description, maintainerName: instance.maintainerName, maintainerEmail: instance.maintainerEmail, - isSilenced: this.utilityService.isSilencedHost(meta.silencedHosts, instance.host), - isMediaSilenced: this.utilityService.isMediaSilencedHost(meta.mediaSilencedHosts, instance.host), + isSilenced: this.utilityService.isSilencedHost(this.meta.silencedHosts, instance.host), + isMediaSilenced: this.utilityService.isMediaSilencedHost(this.meta.mediaSilencedHosts, instance.host), iconUrl: instance.iconUrl, faviconUrl: instance.faviconUrl, themeColor: instance.themeColor, diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index f4b1e302d0..fbd982eb34 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -10,7 +10,6 @@ import type { Packed } from '@/misc/json-schema.js'; import type { MiMeta } from '@/models/Meta.js'; import type { AdsRepository } from '@/models/_.js'; import { MAX_NOTE_TEXT_LENGTH } from '@/const.js'; -import { MetaService } from '@/core/MetaService.js'; import { bindThis } from '@/decorators.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { InstanceActorService } from '@/core/InstanceActorService.js'; @@ -24,11 +23,13 @@ export class MetaEntityService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.adsRepository) private adsRepository: AdsRepository, private userEntityService: UserEntityService, - private metaService: MetaService, private instanceActorService: InstanceActorService, ) { } @@ -37,7 +38,7 @@ export class MetaEntityService { let instance = meta; if (!instance) { - instance = await this.metaService.fetch(); + instance = this.meta; } const ads = await this.adsRepository.createQueryBuilder('ads') @@ -140,7 +141,7 @@ export class MetaEntityService { let instance = meta; if (!instance) { - instance = await this.metaService.fetch(); + instance = this.meta; } const packed = await this.pack(instance); diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts index 0d0b80765a..65a47d7591 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -11,7 +11,7 @@ import type { Packed } from '@/misc/json-schema.js'; import { awaitAll } from '@/misc/prelude/await-all.js'; import type { MiUser } from '@/models/User.js'; import type { MiNote } from '@/models/Note.js'; -import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository } from '@/models/_.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, PollsRepository, PollVotesRepository, NoteReactionsRepository, ChannelsRepository, MiMeta } from '@/models/_.js'; import { bindThis } from '@/decorators.js'; import { DebounceLoader } from '@/misc/loader.js'; import { IdService } from '@/core/IdService.js'; @@ -43,12 +43,14 @@ export class NoteEntityService implements OnModuleInit { private reactionService: ReactionService; private reactionsBufferingService: ReactionsBufferingService; private idService: IdService; - private metaService: MetaService; private noteLoader = new DebounceLoader(this.findNoteOrFail); constructor( private moduleRef: ModuleRef, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -76,7 +78,6 @@ export class NoteEntityService implements OnModuleInit { //private reactionService: ReactionService, //private reactionsBufferingService: ReactionsBufferingService, //private idService: IdService, - //private metaService: MetaService, ) { } @@ -87,7 +88,6 @@ export class NoteEntityService implements OnModuleInit { this.reactionService = this.moduleRef.get('ReactionService'); this.reactionsBufferingService = this.moduleRef.get('ReactionsBufferingService'); this.idService = this.moduleRef.get('IdService'); - this.metaService = this.moduleRef.get('MetaService'); } @bindThis @@ -324,11 +324,9 @@ export class NoteEntityService implements OnModuleInit { const note = typeof src === 'object' ? src : await this.noteLoader.load(src); const host = note.userHost; - const meta = await this.metaService.fetch(); - const bufferdReactions = opts._hint_?.bufferdReactions != null ? (opts._hint_.bufferdReactions.get(note.id) ?? { deltas: {}, pairs: [] }) - : meta.enableReactionsBuffering + : this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.get(note.id) : { deltas: {}, pairs: [] }; const reactions = mergeReactions(note.reactions, bufferdReactions.deltas ?? {}); @@ -441,9 +439,7 @@ export class NoteEntityService implements OnModuleInit { ) { if (notes.length === 0) return []; - const meta = await this.metaService.fetch(); - - const bufferdReactions = meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null; + const bufferdReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null; const meId = me ? me.id : null; const myReactionsMap = new Map(); diff --git a/packages/backend/src/daemons/ServerStatsService.ts b/packages/backend/src/daemons/ServerStatsService.ts index 2c70344c94..d229efb123 100644 --- a/packages/backend/src/daemons/ServerStatsService.ts +++ b/packages/backend/src/daemons/ServerStatsService.ts @@ -3,13 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import si from 'systeminformation'; import Xev from 'xev'; import * as osUtils from 'os-utils'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import type { OnApplicationShutdown } from '@nestjs/common'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; const ev = new Xev(); @@ -23,7 +24,8 @@ export class ServerStatsService implements OnApplicationShutdown { private intervalId: NodeJS.Timeout | null = null; constructor( - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, ) { } @@ -32,7 +34,7 @@ export class ServerStatsService implements OnApplicationShutdown { */ @bindThis public async start(): Promise { - if (!(await this.metaService.fetch(true)).enableServerMachineStats) return; + if (!this.meta.enableServerMachineStats) return; const log = [] as any[]; diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index b6f003c2e6..e599fc7b37 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -6,6 +6,7 @@ export const DI = { config: Symbol('config'), db: Symbol('db'), + meta: Symbol('meta'), meilisearch: Symbol('meilisearch'), redis: Symbol('redis'), redisForPub: Symbol('redisForPub'), diff --git a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts index cd56ba9837..d49c99f694 100644 --- a/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts +++ b/packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts @@ -7,17 +7,20 @@ import { Inject, Injectable } from '@nestjs/common'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js'; -import { MetaService } from '@/core/MetaService.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 metaService: MetaService, private queueLoggerService: QueueLoggerService, ) { this.logger = this.queueLoggerService.logger.createSubLogger('bake-buffered-reactions'); @@ -25,8 +28,7 @@ export class BakeBufferedReactionsProcessorService { @bindThis public async process(): Promise { - const meta = await this.metaService.fetch(); - if (!meta.enableReactionsBuffering) { + if (!this.meta.enableReactionsBuffering) { this.logger.info('Reactions buffering is disabled. Skipping...'); return; } diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts index 4076e9da90..fc9078251f 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, @@ -53,8 +54,7 @@ export class DeliverProcessorService { 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.isBlockedHost(this.meta.blockedHosts, this.utilityService.toPuny(host))) { return 'skip (blocked)'; } @@ -88,7 +88,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 +120,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/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index fa7009f8f5..2df37bedf4 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 } 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'; @@ -28,14 +27,18 @@ import { bindThis } from '@/decorators.js'; import { IdentifiableError } from '@/misc/identifiable-error.js'; import { QueueLoggerService } from '../QueueLoggerService.js'; import type { InboxJobData } from '../types.js'; +import { MiMeta } from '@/models/Meta.js'; +import { DI } from '@/di-symbols.js'; @Injectable() export class InboxProcessorService { private logger: Logger; constructor( + @Inject(DI.meta) + private meta: MiMeta, + private utilityService: UtilityService, - private metaService: MetaService, private apInboxService: ApInboxService, private federatedInstanceService: FederatedInstanceService, private fetchInstanceMetadataService: FetchInstanceMetadataService, @@ -64,8 +67,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.isBlockedHost(this.meta.blockedHosts, host)) { return `Blocked request: ${host}`; } @@ -166,7 +168,7 @@ export class InboxProcessorService { // ブロックしてたら中断 const ldHost = this.utilityService.extractDbHost(authUser.user.uri); - if (this.utilityService.isBlockedHost(meta.blockedHosts, ldHost)) { + if (this.utilityService.isBlockedHost(this.meta.blockedHosts, ldHost)) { throw new Bull.UnrecoverableError(`Blocked request: ${ldHost}`); } } else { @@ -197,7 +199,7 @@ export class InboxProcessorService { this.apRequestChart.inbox(); this.federationChart.inbox(i.host); - if (meta.enableChartsForFederatedInstances) { + if (this.meta.enableChartsForFederatedInstances) { this.instanceChart.requestReceived(i.host); } }); diff --git a/packages/backend/src/server/ServerService.ts b/packages/backend/src/server/ServerService.ts index 2d5535df0c..fd2bd3267d 100644 --- a/packages/backend/src/server/ServerService.ts +++ b/packages/backend/src/server/ServerService.ts @@ -13,7 +13,7 @@ import fastifyRawBody from 'fastify-raw-body'; import { IsNull } from 'typeorm'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import type { Config } from '@/config.js'; -import type { EmojisRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; +import type { EmojisRepository, MiMeta, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import type Logger from '@/logger.js'; import * as Acct from '@/misc/acct.js'; @@ -21,7 +21,6 @@ import { genIdenticon } from '@/misc/gen-identicon.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { LoggerService } from '@/core/LoggerService.js'; import { bindThis } from '@/decorators.js'; -import { MetaService } from '@/core/MetaService.js'; import { ActivityPubServerService } from './ActivityPubServerService.js'; import { NodeinfoServerService } from './NodeinfoServerService.js'; import { ApiServerService } from './api/ApiServerService.js'; @@ -44,6 +43,9 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -53,7 +55,6 @@ export class ServerService implements OnApplicationShutdown { @Inject(DI.emojisRepository) private emojisRepository: EmojisRepository, - private metaService: MetaService, private userEntityService: UserEntityService, private apiServerService: ApiServerService, private openApiServerService: OpenApiServerService, @@ -193,7 +194,7 @@ export class ServerService implements OnApplicationShutdown { reply.header('Content-Type', 'image/png'); reply.header('Cache-Control', 'public, max-age=86400'); - if ((await this.metaService.fetch()).enableIdenticonGeneration) { + if (this.meta.enableIdenticonGeneration) { return await genIdenticon(request.params.x); } else { return reply.redirect('/static-assets/avatar.png'); diff --git a/packages/backend/src/server/api/ApiCallService.ts b/packages/backend/src/server/api/ApiCallService.ts index e8d56ee50a..aad833f126 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, @@ -265,9 +266,8 @@ export class ApiCallService implements OnApplicationShutdown { } @bindThis - private async logIp(request: FastifyRequest, user: MiLocalUser) { - const meta = await this.metaService.fetch(); - if (!meta.enableIpLogging) return; + private logIp(request: FastifyRequest, user: MiLocalUser) { + if (!this.meta.enableIpLogging) return; const ip = request.ip; const ips = this.userIpHistories.get(user.id); if (ips == null || !ips.has(ip)) { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index 632b0c62bc..c499638018 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -7,9 +7,8 @@ import { Inject, Injectable } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { IsNull } from 'typeorm'; import { DI } from '@/di-symbols.js'; -import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket } from '@/models/_.js'; +import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository, MiRegistrationTicket, MiMeta } from '@/models/_.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { CaptchaService } from '@/core/CaptchaService.js'; import { IdService } from '@/core/IdService.js'; import { SignupService } from '@/core/SignupService.js'; @@ -28,6 +27,9 @@ export class SignupApiService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -45,7 +47,6 @@ export class SignupApiService { private userEntityService: UserEntityService, private idService: IdService, - private metaService: MetaService, private captchaService: CaptchaService, private signupService: SignupService, private signinService: SigninService, @@ -72,31 +73,29 @@ export class SignupApiService { ) { const body = request.body; - const instance = await this.metaService.fetch(true); - // Verify *Captcha // ただしテスト時はこの機構は障害となるため無効にする if (process.env.NODE_ENV !== 'test') { - if (instance.enableHcaptcha && instance.hcaptchaSecretKey) { - await this.captchaService.verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { + if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) { + await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableMcaptcha && instance.mcaptchaSecretKey && instance.mcaptchaSitekey && instance.mcaptchaInstanceUrl) { - await this.captchaService.verifyMcaptcha(instance.mcaptchaSecretKey, instance.mcaptchaSitekey, instance.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { + if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) { + await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableRecaptcha && instance.recaptchaSecretKey) { - await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { + if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) { + await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => { throw new FastifyReplyError(400, err); }); } - if (instance.enableTurnstile && instance.turnstileSecretKey) { - await this.captchaService.verifyTurnstile(instance.turnstileSecretKey, body['turnstile-response']).catch(err => { + if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) { + await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => { throw new FastifyReplyError(400, err); }); } @@ -108,7 +107,7 @@ export class SignupApiService { const invitationCode = body['invitationCode']; const emailAddress = body['emailAddress']; - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { if (emailAddress == null || typeof emailAddress !== 'string') { reply.code(400); return; @@ -123,7 +122,7 @@ export class SignupApiService { let ticket: MiRegistrationTicket | null = null; - if (instance.disableRegistration) { + if (this.meta.disableRegistration) { if (invitationCode == null || typeof invitationCode !== 'string') { reply.code(400); return; @@ -144,7 +143,7 @@ export class SignupApiService { } // メアド認証が有効の場合 - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { // メアド認証済みならエラー if (ticket.usedBy) { reply.code(400); @@ -162,7 +161,7 @@ export class SignupApiService { } } - if (instance.emailRequiredForSignup) { + if (this.meta.emailRequiredForSignup) { if (await this.usersRepository.exists({ where: { usernameLower: username.toLowerCase(), host: IsNull() } })) { throw new FastifyReplyError(400, 'DUPLICATED_USERNAME'); } @@ -172,7 +171,7 @@ export class SignupApiService { throw new FastifyReplyError(400, 'USED_USERNAME'); } - const isPreserved = instance.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); + const isPreserved = this.meta.preservedUsernames.map(x => x.toLowerCase()).includes(username.toLowerCase()); if (isPreserved) { throw new FastifyReplyError(400, 'DENIED_USERNAME'); } diff --git a/packages/backend/src/server/api/endpoints/ap/show.ts b/packages/backend/src/server/api/endpoints/ap/show.ts index d3c40dba59..577ca0b24c 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'; @@ -20,6 +19,8 @@ import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import { ApiError } from '../../error.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['federation'], @@ -88,10 +89,12 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private utilityService: UtilityService, private userEntityService: UserEntityService, private noteEntityService: NoteEntityService, - private metaService: MetaService, private apResolverService: ApResolverService, private apDbResolverService: ApDbResolverService, private apPersonService: ApPersonService, @@ -112,9 +115,8 @@ export default class extends Endpoint { // eslint- */ @bindThis private async fetchAny(uri: string, me: MiLocalUser | null | undefined): Promise | null> { - // ブロックしてたら中断 - const fetchedMeta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(fetchedMeta.blockedHosts, this.utilityService.extractDbHost(uri))) return null; + // ブロックしてたら中断 + if (this.utilityService.isBlockedHost(this.serverSettings.blockedHosts, this.utilityService.extractDbHost(uri))) return null; let local = await this.mergePack(me, ...await Promise.all([ this.apDbResolverService.getUserFromApId(uri), diff --git a/packages/backend/src/server/api/endpoints/channels/timeline.ts b/packages/backend/src/server/api/endpoints/channels/timeline.ts index 8c55673590..d4fd75e049 100644 --- a/packages/backend/src/server/api/endpoints/channels/timeline.ts +++ b/packages/backend/src/server/api/endpoints/channels/timeline.ts @@ -5,14 +5,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { ChannelsRepository, NotesRepository } from '@/models/_.js'; +import type { ChannelsRepository, MiMeta, NotesRepository } from '@/models/_.js'; import { QueryService } from '@/core/QueryService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { MiLocalUser } from '@/models/User.js'; import { ApiError } from '../../error.js'; @@ -58,6 +56,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -68,16 +69,12 @@ export default class extends Endpoint { // eslint- private noteEntityService: NoteEntityService, private queryService: QueryService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, - private cacheService: CacheService, private activeUsersChart: ActiveUsersChart, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const serverSettings = await this.metaService.fetch(); - const channel = await this.channelsRepository.findOneBy({ id: ps.channelId, }); @@ -88,7 +85,7 @@ export default class extends Endpoint { // eslint- if (me) this.activeUsersChart.read(me); - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { return await this.noteEntityService.packMany(await this.getFromDb({ untilId, sinceId, limit: ps.limit, channelId: channel.id }, me), me); } diff --git a/packages/backend/src/server/api/endpoints/drive.ts b/packages/backend/src/server/api/endpoints/drive.ts index 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/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/update-email.ts b/packages/backend/src/server/api/endpoints/i/update-email.ts index eea657ebbd..da1faee30d 100644 --- a/packages/backend/src/server/api/endpoints/i/update-email.ts +++ b/packages/backend/src/server/api/endpoints/i/update-email.ts @@ -7,7 +7,7 @@ import { Inject, Injectable } from '@nestjs/common'; import ms from 'ms'; import bcrypt from 'bcryptjs'; import { Endpoint } from '@/server/api/endpoint-base.js'; -import type { UserProfilesRepository } from '@/models/_.js'; +import type { MiMeta, UserProfilesRepository } from '@/models/_.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { EmailService } from '@/core/EmailService.js'; import type { Config } from '@/config.js'; @@ -15,7 +15,6 @@ import { DI } from '@/di-symbols.js'; import { GlobalEventService } from '@/core/GlobalEventService.js'; import { L_CHARS, secureRndstr } from '@/misc/secure-rndstr.js'; import { UserAuthService } from '@/core/UserAuthService.js'; -import { MetaService } from '@/core/MetaService.js'; import { ApiError } from '../../error.js'; export const meta = { @@ -70,10 +69,12 @@ export default class extends Endpoint { // eslint- @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.userProfilesRepository) private userProfilesRepository: UserProfilesRepository, - private metaService: MetaService, private userEntityService: UserEntityService, private emailService: EmailService, private userAuthService: UserAuthService, @@ -105,7 +106,7 @@ export default class extends Endpoint { // eslint- if (!res.available) { throw new ApiError(meta.errors.unavailable); } - } else if ((await this.metaService.fetch()).emailRequiredForSignup) { + } else if (this.serverSettings.emailRequiredForSignup) { throw new ApiError(meta.errors.emailRequired); } diff --git a/packages/backend/src/server/api/endpoints/notes/create.ts b/packages/backend/src/server/api/endpoints/notes/create.ts index beb77ca7ab..253a360815 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 2a2c659942..aed9065bf9 100644 --- a/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/hybrid-timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; @@ -16,7 +16,6 @@ import { CacheService } from '@/core/CacheService.js'; import { FanoutTimelineName } from '@/core/FanoutTimelineService.js'; import { QueryService } from '@/core/QueryService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -74,6 +73,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -87,7 +89,6 @@ export default class extends Endpoint { // eslint- private cacheService: CacheService, private queryService: QueryService, private userFollowingService: UserFollowingService, - private metaService: MetaService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, ) { super(meta, paramDef, async (ps, me) => { @@ -101,9 +102,7 @@ export default class extends Endpoint { // eslint- if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -156,7 +155,7 @@ export default class extends Endpoint { // eslint- allowPartial: ps.allowPartial, me, redisTimelines: timelineConfig, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, noteFilter: note => { diff --git a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts index be82b5a8a7..0b48f2c78b 100644 --- a/packages/backend/src/server/api/endpoints/notes/local-timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/local-timeline.ts @@ -5,16 +5,14 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository } from '@/models/_.js'; +import type { MiMeta, NotesRepository } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; import { DI } from '@/di-symbols.js'; import { RoleService } from '@/core/RoleService.js'; import { IdService } from '@/core/IdService.js'; -import { CacheService } from '@/core/CacheService.js'; import { QueryService } from '@/core/QueryService.js'; -import { MetaService } from '@/core/MetaService.js'; import { MiLocalUser } from '@/models/User.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; import { ApiError } from '../../error.js'; @@ -66,6 +64,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -73,10 +74,8 @@ export default class extends Endpoint { // eslint- private roleService: RoleService, private activeUsersChart: ActiveUsersChart, private idService: IdService, - private cacheService: CacheService, private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); @@ -89,9 +88,7 @@ export default class extends Endpoint { // eslint- if (ps.withReplies && ps.withFiles) throw new ApiError(meta.errors.bothWithRepliesAndWithFiles); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -115,7 +112,7 @@ export default class extends Endpoint { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? ['localTimelineWithFiles'] : ps.withReplies ? ['localTimeline', 'localTimelineWithReplies'] diff --git a/packages/backend/src/server/api/endpoints/notes/timeline.ts b/packages/backend/src/server/api/endpoints/notes/timeline.ts index c9b43b5359..7cb11cc1eb 100644 --- a/packages/backend/src/server/api/endpoints/notes/timeline.ts +++ b/packages/backend/src/server/api/endpoints/notes/timeline.ts @@ -5,7 +5,7 @@ import { Brackets } from 'typeorm'; import { Inject, Injectable } from '@nestjs/common'; -import type { NotesRepository, ChannelFollowingsRepository } from '@/models/_.js'; +import type { NotesRepository, ChannelFollowingsRepository, MiMeta } from '@/models/_.js'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { QueryService } from '@/core/QueryService.js'; import ActiveUsersChart from '@/core/chart/charts/active-users.js'; @@ -15,7 +15,6 @@ import { IdService } from '@/core/IdService.js'; import { CacheService } from '@/core/CacheService.js'; import { UserFollowingService } from '@/core/UserFollowingService.js'; import { MiLocalUser } from '@/models/User.js'; -import { MetaService } from '@/core/MetaService.js'; import { FanoutTimelineEndpointService } from '@/core/FanoutTimelineEndpointService.js'; export const meta = { @@ -56,6 +55,9 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + @Inject(DI.notesRepository) private notesRepository: NotesRepository, @@ -69,15 +71,12 @@ export default class extends Endpoint { // eslint- private fanoutTimelineEndpointService: FanoutTimelineEndpointService, private userFollowingService: UserFollowingService, private queryService: QueryService, - private metaService: MetaService, ) { super(meta, paramDef, async (ps, me) => { const untilId = ps.untilId ?? (ps.untilDate ? this.idService.gen(ps.untilDate!) : null); const sinceId = ps.sinceId ?? (ps.sinceDate ? this.idService.gen(ps.sinceDate!) : null); - const serverSettings = await this.metaService.fetch(); - - if (!serverSettings.enableFanoutTimeline) { + if (!this.serverSettings.enableFanoutTimeline) { const timeline = await this.getFromDb({ untilId, sinceId, @@ -108,7 +107,7 @@ export default class extends Endpoint { // eslint- limit: ps.limit, allowPartial: ps.allowPartial, me, - useDbFallback: serverSettings.enableFanoutTimelineDbFallback, + useDbFallback: this.serverSettings.enableFanoutTimelineDbFallback, redisTimelines: ps.withFiles ? [`homeTimelineWithFiles:${me.id}`] : [`homeTimeline:${me.id}`], alwaysIncludeMyNotes: true, excludePureRenotes: !ps.withRenotes, diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 38a9660aa2..e9a6a36b02 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -4,14 +4,15 @@ */ import { URLSearchParams } from 'node:url'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Endpoint } from '@/server/api/endpoint-base.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { GetterService } from '@/server/api/GetterService.js'; import { RoleService } from '@/core/RoleService.js'; import { ApiError } from '../../error.js'; +import { MiMeta } from '@/models/_.js'; +import { DI } from '@/di-symbols.js'; export const meta = { tags: ['notes'], @@ -59,9 +60,11 @@ export const paramDef = { @Injectable() export default class extends Endpoint { // eslint-disable-line import/no-default-export constructor( + @Inject(DI.meta) + private serverSettings: MiMeta, + private noteEntityService: NoteEntityService, private getterService: GetterService, - private metaService: MetaService, private httpRequestService: HttpRequestService, private roleService: RoleService, ) { @@ -84,9 +87,7 @@ export default class extends Endpoint { // eslint- return; } - const instance = await this.metaService.fetch(); - - if (instance.deeplAuthKey == null) { + if (this.serverSettings.deeplAuthKey == null) { throw new ApiError(meta.errors.unavailable); } @@ -94,11 +95,11 @@ export default class extends Endpoint { // eslint- if (targetLang.includes('-')) targetLang = targetLang.split('-')[0]; const params = new URLSearchParams(); - params.append('auth_key', instance.deeplAuthKey); + params.append('auth_key', this.serverSettings.deeplAuthKey); params.append('text', note.text); params.append('target_lang', targetLang); - const endpoint = instance.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; + const endpoint = this.serverSettings.deeplIsPro ? 'https://api.deepl.com/v2/translate' : 'https://api-free.deepl.com/v2/translate'; const res = await this.httpRequestService.send(endpoint, { method: 'POST', diff --git a/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts b/packages/backend/src/server/api/endpoints/notes/user-list-timeline.ts index 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 1dc53a9a70..063141273a 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -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, @@ -73,6 +72,9 @@ export class ClientServerService { @Inject(DI.config) private config: Config, + @Inject(DI.meta) + private meta: MiMeta, + @Inject(DI.usersRepository) private usersRepository: UsersRepository, @@ -109,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, @@ -129,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', @@ -178,7 +177,7 @@ export class ClientServerService { manifest = { ...manifest, - ...JSON.parse(instance.manifestJsonOverride === '' ? '{}' : instance.manifestJsonOverride), + ...JSON.parse(this.meta.manifestJsonOverride === '' ? '{}' : this.meta.manifestJsonOverride), }; reply.header('Cache-Control', 'max-age=300'); @@ -453,9 +452,7 @@ export class ClientServerService { // OpenSearch XML fastify.get('/opensearch.xml', async (request, reply) => { - const meta = await this.metaService.fetch(); - - const name = meta.name ?? 'Misskey'; + const name = this.meta.name ?? 'Misskey'; let content = ''; content += ''; content += `${name}`; @@ -472,14 +469,13 @@ export class ClientServerService { //#endregion const renderBase = async (reply: FastifyReply, data: { [key: string]: any } = {}) => { - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=30'); return await reply.view('base', { - img: meta.bannerUrl, + img: this.meta.bannerUrl, url: this.config.url, - title: meta.name ?? 'Misskey', - desc: meta.description, - ...await this.generateCommonPugData(meta), + title: this.meta.name ?? 'Misskey', + desc: this.meta.description, + ...await this.generateCommonPugData(this.meta), ...data, }); }; @@ -557,7 +553,6 @@ export class ClientServerService { if (user != null) { const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id }); - const meta = await this.metaService.fetch(); const me = profile.fields ? profile.fields .filter(filed => filed.value != null && filed.value.match(/^https?:/)) @@ -573,7 +568,7 @@ export class ClientServerService { user, profile, me, avatarUrl: user.avatarUrl ?? this.userEntityService.getIdenticonUrl(user), sub: request.params.sub, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { // リモートユーザーなので @@ -611,7 +606,6 @@ export class ClientServerService { if (note) { const _note = await this.noteEntityService.pack(note); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: note.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -623,7 +617,7 @@ export class ClientServerService { avatarUrl: _note.user.avatarUrl, // TODO: Let locale changeable by instance setting summary: getNoteSummary(_note), - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -648,7 +642,6 @@ export class ClientServerService { if (page) { const _page = await this.pageEntityService.pack(page); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: page.userId }); - const meta = await this.metaService.fetch(); if (['public'].includes(page.visibility)) { reply.header('Cache-Control', 'public, max-age=15'); } else { @@ -662,7 +655,7 @@ export class ClientServerService { page: _page, profile, avatarUrl: _page.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -678,7 +671,6 @@ export class ClientServerService { if (flash) { const _flash = await this.flashEntityService.pack(flash); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: flash.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -688,7 +680,7 @@ export class ClientServerService { flash: _flash, profile, avatarUrl: _flash.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -704,7 +696,6 @@ export class ClientServerService { if (clip && clip.isPublic) { const _clip = await this.clipEntityService.pack(clip); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: clip.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -714,7 +705,7 @@ export class ClientServerService { clip: _clip, profile, avatarUrl: _clip.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -728,7 +719,6 @@ export class ClientServerService { if (post) { const _post = await this.galleryPostEntityService.pack(post); const profile = await this.userProfilesRepository.findOneByOrFail({ userId: post.userId }); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); if (profile.preventAiLearning) { reply.header('X-Robots-Tag', 'noimageai'); @@ -738,7 +728,7 @@ export class ClientServerService { post: _post, profile, avatarUrl: _post.user.avatarUrl, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -753,11 +743,10 @@ export class ClientServerService { if (channel) { const _channel = await this.channelEntityService.pack(channel); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=15'); return await reply.view('channel', { channel: _channel, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -772,11 +761,10 @@ export class ClientServerService { if (game) { const _game = await this.reversiGameEntityService.packDetail(game); - const meta = await this.metaService.fetch(); reply.header('Cache-Control', 'public, max-age=3600'); return await reply.view('reversi-game', { game: _game, - ...await this.generateCommonPugData(meta), + ...await this.generateCommonPugData(this.meta), }); } else { return await renderBase(reply); @@ -798,26 +786,22 @@ export class ClientServerService { //#region embed pages fastify.get('/embed/*', async (request, reply) => { - const meta = await this.metaService.fetch(); - reply.removeHeader('X-Frame-Options'); reply.header('Cache-Control', 'public, max-age=3600'); return await reply.view('base-embed', { - title: meta.name ?? 'Misskey', - ...await this.generateCommonPugData(meta), + 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() }), }); diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts index 8f8f08a305..5d493c2c46 100644 --- a/packages/backend/src/server/web/UrlPreviewService.ts +++ b/packages/backend/src/server/web/UrlPreviewService.ts @@ -8,7 +8,6 @@ import { summaly } from '@misskey-dev/summaly'; import { SummalyResult } from '@misskey-dev/summaly/built/summary.js'; import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; -import { MetaService } from '@/core/MetaService.js'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import type Logger from '@/logger.js'; import { query } from '@/misc/prelude/url.js'; @@ -26,7 +25,9 @@ export class UrlPreviewService { @Inject(DI.config) private config: Config, - private metaService: MetaService, + @Inject(DI.meta) + private meta: MiMeta, + private httpRequestService: HttpRequestService, private loggerService: LoggerService, ) { @@ -62,9 +63,7 @@ export class UrlPreviewService { return; } - const meta = await this.metaService.fetch(); - - if (!meta.urlPreviewEnabled) { + if (!this.meta.urlPreviewEnabled) { reply.code(403); return { error: new ApiError({ @@ -75,14 +74,14 @@ export class UrlPreviewService { }; } - this.logger.info(meta.urlPreviewSummaryProxyUrl + this.logger.info(this.meta.urlPreviewSummaryProxyUrl ? `(Proxy) Getting preview of ${url}@${lang} ...` : `Getting preview of ${url}@${lang} ...`); try { - const summary = meta.urlPreviewSummaryProxyUrl - ? await this.fetchSummaryFromProxy(url, meta, lang) - : await this.fetchSummary(url, meta, lang); + const summary = this.meta.urlPreviewSummaryProxyUrl + ? await this.fetchSummaryFromProxy(url, this.meta, lang) + : await this.fetchSummary(url, this.meta, lang); this.logger.succ(`Got preview of ${url}: ${summary.title}`); diff --git a/packages/backend/test-server/entry.ts b/packages/backend/test-server/entry.ts index 866a7e1f5b..04bf62d209 100644 --- a/packages/backend/test-server/entry.ts +++ b/packages/backend/test-server/entry.ts @@ -6,12 +6,16 @@ import { MainModule } from '@/MainModule.js'; import { ServerService } from '@/server/ServerService.js'; import { loadConfig } from '@/config.js'; import { NestLogger } from '@/NestLogger.js'; +import { INestApplicationContext } from '@nestjs/common'; const config = loadConfig(); const originEnv = JSON.stringify(process.env); process.env.NODE_ENV = 'test'; +let app: INestApplicationContext; +let serverService: ServerService; + /** * テスト用のサーバインスタンスを起動する */ @@ -20,10 +24,10 @@ async function launch() { console.log('starting application...'); - const app = await NestFactory.createApplicationContext(MainModule, { + app = await NestFactory.createApplicationContext(MainModule, { logger: new NestLogger(), }); - const serverService = app.get(ServerService); + serverService = app.get(ServerService); await serverService.launch(); await startControllerEndpoints(); @@ -71,6 +75,20 @@ async function startControllerEndpoints(port = config.port + 1000) { fastify.post<{ Body: { key?: string, value?: string } }>('/env-reset', async (req, res) => { process.env = JSON.parse(originEnv); + + await serverService.dispose(); + await app.close(); + + await killTestServer(); + + console.log('starting application...'); + + app = await NestFactory.createApplicationContext(MainModule, { + logger: new NestLogger(), + }); + serverService = app.get(ServerService); + await serverService.launch(); + res.code(200).send({ success: true }); }); diff --git a/packages/backend/test/jest.setup.ts b/packages/backend/test/jest.setup.ts index 861bc6db66..7c6dd6a55f 100644 --- a/packages/backend/test/jest.setup.ts +++ b/packages/backend/test/jest.setup.ts @@ -6,8 +6,6 @@ import { initTestDb, sendEnvResetRequest } from './utils.js'; beforeAll(async () => { - await Promise.all([ - initTestDb(false), - sendEnvResetRequest(), - ]); + await initTestDb(false); + await sendEnvResetRequest(); }); diff --git a/packages/backend/test/misc/mock-resolver.ts b/packages/backend/test/misc/mock-resolver.ts index 3c7e796700..c8f3db8aac 100644 --- a/packages/backend/test/misc/mock-resolver.ts +++ b/packages/backend/test/misc/mock-resolver.ts @@ -17,6 +17,7 @@ import type { UtilityService } from '@/core/UtilityService.js'; import { bindThis } from '@/decorators.js'; import type { FollowRequestsRepository, + MiMeta, NoteReactionsRepository, NotesRepository, PollsRepository, @@ -35,6 +36,7 @@ export class MockResolver extends Resolver { constructor(loggerService: LoggerService) { super( {} as Config, + {} as MiMeta, {} as UsersRepository, {} as NotesRepository, {} as PollsRepository, @@ -42,7 +44,6 @@ export class MockResolver extends Resolver { {} as FollowRequestsRepository, {} as UtilityService, {} as InstanceActorService, - {} as MetaService, {} as ApRequestService, {} as HttpRequestService, {} as ApRendererService, diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts index b6cbe4c520..ef80d25f81 100644 --- a/packages/backend/test/unit/RoleService.ts +++ b/packages/backend/test/unit/RoleService.ts @@ -13,6 +13,7 @@ import * as lolex from '@sinonjs/fake-timers'; import { GlobalModule } from '@/GlobalModule.js'; import { RoleService } from '@/core/RoleService.js'; import { + MiMeta, MiRole, MiRoleAssignment, MiUser, @@ -41,7 +42,7 @@ describe('RoleService', () => { let usersRepository: UsersRepository; let rolesRepository: RolesRepository; let roleAssignmentsRepository: RoleAssignmentsRepository; - let metaService: jest.Mocked; + let meta: jest.Mocked; let notificationService: jest.Mocked; let clock: lolex.InstalledClock; @@ -142,7 +143,7 @@ describe('RoleService', () => { rolesRepository = app.get(DI.rolesRepository); roleAssignmentsRepository = app.get(DI.roleAssignmentsRepository); - metaService = app.get(MetaService) as jest.Mocked; + meta = app.get(DI.meta) as jest.Mocked; notificationService = app.get(NotificationService) as jest.Mocked; await roleService.onModuleInit(); @@ -164,11 +165,9 @@ describe('RoleService', () => { describe('getUserPolicies', () => { test('instance default policies', async () => { const user = await createUser(); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); @@ -177,11 +176,9 @@ describe('RoleService', () => { test('instance default policies 2', async () => { const user = await createUser(); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: true, - }, - } as any); + meta.policies = { + canManageCustomEmojis: true, + }; const result = await roleService.getUserPolicies(user.id); @@ -201,11 +198,9 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); @@ -236,11 +231,9 @@ describe('RoleService', () => { }); await roleService.assign(user.id, role1.id); await roleService.assign(user.id, role2.id); - metaService.fetch.mockResolvedValue({ - policies: { - driveCapacityMb: 50, - }, - } as any); + meta.policies = { + driveCapacityMb: 50, + }; const result = await roleService.getUserPolicies(user.id); @@ -260,11 +253,9 @@ describe('RoleService', () => { }, }); await roleService.assign(user.id, role.id, new Date(Date.now() + (1000 * 60 * 60 * 24))); - metaService.fetch.mockResolvedValue({ - policies: { - canManageCustomEmojis: false, - }, - } as any); + meta.policies = { + canManageCustomEmojis: false, + }; const result = await roleService.getUserPolicies(user.id); expect(result.canManageCustomEmojis).toBe(true); diff --git a/packages/backend/test/unit/activitypub.ts b/packages/backend/test/unit/activitypub.ts index 763ce2b336..2fc08aec91 100644 --- a/packages/backend/test/unit/activitypub.ts +++ b/packages/backend/test/unit/activitypub.ts @@ -24,7 +24,6 @@ import { MiMeta, MiNote, UserProfilesRepository } from '@/models/_.js'; import { DI } from '@/di-symbols.js'; import { secureRndstr } from '@/misc/secure-rndstr.js'; import { DownloadService } from '@/core/DownloadService.js'; -import { MetaService } from '@/core/MetaService.js'; import type { MiRemoteUser } from '@/models/User.js'; import { genAidx } from '@/misc/id/aidx.js'; import { MockResolver } from '../misc/mock-resolver.js'; @@ -107,7 +106,14 @@ describe('ActivityPub', () => { sensitiveWords: [] as string[], prohibitedWords: [] as string[], } as MiMeta; - let meta = metaInitial; + const meta = { ...metaInitial }; + + function updateMeta(newMeta: Partial): void { + for (const key in meta) { + delete (meta as any)[key]; + } + Object.assign(meta, newMeta); + } beforeAll(async () => { const app = await Test.createTestingModule({ @@ -120,11 +126,8 @@ describe('ActivityPub', () => { }; }, }) - .overrideProvider(MetaService).useValue({ - async fetch(): Promise { - return meta; - }, - }).compile(); + .overrideProvider(DI.meta).useFactory({ factory: () => meta }) + .compile(); await app.init(); app.enableShutdownHooks(); @@ -367,7 +370,7 @@ describe('ActivityPub', () => { }); test('cacheRemoteFiles=false disables caching', async () => { - meta = { ...metaInitial, cacheRemoteFiles: false }; + updateMeta({ ...metaInitial, cacheRemoteFiles: false }); const imageObject: IApDocument = { type: 'Document', @@ -396,7 +399,7 @@ describe('ActivityPub', () => { }); test('cacheRemoteSensitiveFiles=false only affects sensitive files', async () => { - meta = { ...metaInitial, cacheRemoteSensitiveFiles: false }; + updateMeta({ ...metaInitial, cacheRemoteSensitiveFiles: false }); const imageObject: IApDocument = { type: 'Document', From 891bbcf47597fb621917721f9b7e59c7fac9c9ab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 22 Sep 2024 03:56:51 +0000 Subject: [PATCH 350/635] Bump version to 2024.9.0-alpha.3 --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 68bb2fc1f2..c8285c0d43 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.9.0-alpha.2", + "version": "2024.9.0-alpha.3", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 13bb114fbb..be5f9a4906 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.9.0-alpha.2", + "version": "2024.9.0-alpha.3", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 3df1bb2d71bfed3cc49f6576d5809ecfdbdb6fe1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:01:13 +0900 Subject: [PATCH 351/635] enhance(frontend): tweak control panel --- packages/frontend/src/pages/admin/index.vue | 5 - .../src/pages/admin/instance-block.vue | 84 -------- .../frontend/src/pages/admin/moderation.vue | 202 +++++++++++++----- .../frontend/src/pages/admin/settings.vue | 25 +++ packages/frontend/src/router/definition.ts | 4 - 5 files changed, 175 insertions(+), 145 deletions(-) delete mode 100644 packages/frontend/src/pages/admin/instance-block.vue diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index 40dec55deb..cd1dd2ca9d 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -199,11 +199,6 @@ const menuDef = computed(() => [{ text: i18n.ts.relays, to: '/admin/relays', active: currentPage.value?.route.name === 'relays', - }, { - icon: 'ti ti-ban', - text: i18n.ts.instanceBlocking, - to: '/admin/instance-block', - active: currentPage.value?.route.name === 'instance-block', }, { icon: 'ti ti-ghost', text: i18n.ts.proxyAccount, diff --git a/packages/frontend/src/pages/admin/instance-block.vue b/packages/frontend/src/pages/admin/instance-block.vue deleted file mode 100644 index e090616b26..0000000000 --- a/packages/frontend/src/pages/admin/instance-block.vue +++ /dev/null @@ -1,84 +0,0 @@ - - - - - diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index a75799696d..82e053c2da 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -10,61 +10,102 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + {{ i18n.ts.serverRules }} - - - - - - - - - - - - - - - - - + + - - - +
+ + + + {{ i18n.ts.save }} +
+ + + + - -
- +
+ + + + {{ i18n.ts.save }} +
+ + + + - -
- +
+ + + + {{ i18n.ts.save }} +
+ + + + - -
+ +
+ + + + {{ i18n.ts.save }} +
+ + + + + + +
+ + + + {{ i18n.ts.save }} +
+
+ + + + + +
+ + + + {{ i18n.ts.save }} +
+
+ + + + + +
+ + + + {{ i18n.ts.save }} +
+
-
@@ -83,6 +124,7 @@ import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import MkButton from '@/components/MkButton.vue'; import FormLink from '@/components/form/link.vue'; +import MkFolder from '@/components/MkFolder.vue'; const enableRegistration = ref(false); const emailRequiredForSignup = ref(false); @@ -90,9 +132,9 @@ const sensitiveWords = ref(''); const prohibitedWords = ref(''); const hiddenTags = ref(''); const preservedUsernames = ref(''); -const tosUrl = ref(null); -const privacyPolicyUrl = ref(null); -const inquiryUrl = ref(null); +const blockedHosts = ref(''); +const silencedHosts = ref(''); +const mediaSilencedHosts = ref(''); async function init() { const meta = await misskeyApi('admin/meta'); @@ -102,27 +144,83 @@ async function init() { prohibitedWords.value = meta.prohibitedWords.join('\n'); hiddenTags.value = meta.hiddenTags.join('\n'); preservedUsernames.value = meta.preservedUsernames.join('\n'); - tosUrl.value = meta.tosUrl; - privacyPolicyUrl.value = meta.privacyPolicyUrl; - inquiryUrl.value = meta.inquiryUrl; + blockedHosts.value = meta.blockedHosts.join('\n'); + silencedHosts.value = meta.silencedHosts.join('\n'); + mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); } -function save() { +function onChange_enableRegistration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + disableRegistration: !value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_emailRequiredForSignup(value: boolean) { + os.apiWithDialog('admin/update-meta', { + emailRequiredForSignup: value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_preservedUsernames() { os.apiWithDialog('admin/update-meta', { - disableRegistration: !enableRegistration.value, - emailRequiredForSignup: emailRequiredForSignup.value, - tosUrl: tosUrl.value, - privacyPolicyUrl: privacyPolicyUrl.value, - inquiryUrl: inquiryUrl.value, - sensitiveWords: sensitiveWords.value.split('\n'), - prohibitedWords: prohibitedWords.value.split('\n'), - hiddenTags: hiddenTags.value.split('\n'), preservedUsernames: preservedUsernames.value.split('\n'), }).then(() => { fetchInstance(true); }); } +function save_sensitiveWords() { + os.apiWithDialog('admin/update-meta', { + sensitiveWords: sensitiveWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_prohibitedWords() { + os.apiWithDialog('admin/update-meta', { + prohibitedWords: prohibitedWords.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_hiddenTags() { + os.apiWithDialog('admin/update-meta', { + hiddenTags: hiddenTags.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function save_blockedHosts() { + os.apiWithDialog('admin/update-meta', { + blockedHosts: blockedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_silencedHosts() { + os.apiWithDialog('admin/update-meta', { + silencedHosts: silencedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + +function save_mediaSilencedHosts() { + os.apiWithDialog('admin/update-meta', { + mediaSilencedHosts: mediaSilencedHosts.value.split('\n') || [], + }).then(() => { + fetchInstance(true); + }); +} + const headerTabs = computed(() => []); definePageMetadata(() => ({ diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index ffff57b454..6eaafed6df 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -34,6 +34,22 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + + + + + + + + + @@ -196,6 +212,9 @@ const shortName = ref(null); const description = ref(null); const maintainerName = ref(null); const maintainerEmail = ref(null); +const tosUrl = ref(null); +const privacyPolicyUrl = ref(null); +const inquiryUrl = ref(null); const repositoryUrl = ref(null); const impressumUrl = ref(null); const pinnedUsers = ref(''); @@ -219,6 +238,9 @@ async function init(): Promise { description.value = meta.description; maintainerName.value = meta.maintainerName; maintainerEmail.value = meta.maintainerEmail; + tosUrl.value = meta.tosUrl; + privacyPolicyUrl.value = meta.privacyPolicyUrl; + inquiryUrl.value = meta.inquiryUrl; repositoryUrl.value = meta.repositoryUrl; impressumUrl.value = meta.impressumUrl; pinnedUsers.value = meta.pinnedUsers.join('\n'); @@ -243,6 +265,9 @@ async function save() { description: description.value, maintainerName: maintainerName.value, maintainerEmail: maintainerEmail.value, + tosUrl: tosUrl.value, + privacyPolicyUrl: privacyPolicyUrl.value, + inquiryUrl: inquiryUrl.value, repositoryUrl: repositoryUrl.value, impressumUrl: impressumUrl.value, pinnedUsers: pinnedUsers.value.split('\n'), diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index 8a29fd677e..bcd1d9a159 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -462,10 +462,6 @@ const routes: RouteDef[] = [{ path: '/relays', name: 'relays', component: page(() => import('@/pages/admin/relays.vue')), - }, { - path: '/instance-block', - name: 'instance-block', - component: page(() => import('@/pages/admin/instance-block.vue')), }, { path: '/proxy-account', name: 'proxy-account', From 8ad9f7209b8b1a4584428118dda98339d575a0d6 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:16:50 +0900 Subject: [PATCH 352/635] enhance(frontend): tweak control panel --- locales/index.d.ts | 4 + locales/ja-JP.yml | 1 + packages/frontend/src/pages/admin/index.vue | 8 +- .../{other-settings.vue => performance.vue} | 125 +++++++++++++----- packages/frontend/src/router/definition.ts | 6 +- 5 files changed, 107 insertions(+), 37 deletions(-) rename packages/frontend/src/pages/admin/{other-settings.vue => performance.vue} (66%) diff --git a/locales/index.d.ts b/locales/index.d.ts index f234262195..55e76e2e43 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -5092,6 +5092,10 @@ export interface Locale extends ILocale { * これ以上このクリップにノートを追加できません。 */ "clipNoteLimitExceeded": string; + /** + * パフォーマンス + */ + "performance": string; "_delivery": { /** * 配信状態 diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 8e48508e78..995bf8bc7c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1269,6 +1269,7 @@ fromX: "{x}から" genEmbedCode: "埋め込みコードを生成" noteOfThisUser: "このユーザーのノート一覧" clipNoteLimitExceeded: "これ以上このクリップにノートを追加できません。" +performance: "パフォーマンス" _delivery: status: "配信状態" diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue index cd1dd2ca9d..b9f72e6fb6 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -215,10 +215,10 @@ const menuDef = computed(() => [{ to: '/admin/system-webhook', active: currentPage.value?.route.name === 'system-webhook', }, { - icon: 'ti ti-adjustments', - text: i18n.ts.other, - to: '/admin/other-settings', - active: currentPage.value?.route.name === 'other-settings', + icon: 'ti ti-bolt', + text: i18n.ts.performance, + to: '/admin/performance', + active: currentPage.value?.route.name === 'performance', }], }, { title: i18n.ts.info, diff --git a/packages/frontend/src/pages/admin/other-settings.vue b/packages/frontend/src/pages/admin/performance.vue similarity index 66% rename from packages/frontend/src/pages/admin/other-settings.vue rename to packages/frontend/src/pages/admin/performance.vue index cad111997f..721a11e60e 100644 --- a/packages/frontend/src/pages/admin/other-settings.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -10,28 +10,28 @@ SPDX-License-Identifier: AGPL-3.0-only
- +
- +
- +
- + @@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + - + - + - + - +
@@ -77,12 +77,12 @@ SPDX-License-Identifier: AGPL-3.0-only - +
- + @@ -135,30 +135,95 @@ async function init() { enableReactionsBuffering.value = meta.enableReactionsBuffering; } -function save() { +function onChange_enableServerMachineStats(value: boolean) { os.apiWithDialog('admin/update-meta', { - enableServerMachineStats: enableServerMachineStats.value, - enableIdenticonGeneration: enableIdenticonGeneration.value, - enableChartsForRemoteUser: enableChartsForRemoteUser.value, - enableChartsForFederatedInstances: enableChartsForFederatedInstances.value, - enableFanoutTimeline: enableFanoutTimeline.value, - enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value, - perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value, - perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value, - perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value, - perUserListTimelineCacheMax: perUserListTimelineCacheMax.value, - enableReactionsBuffering: enableReactionsBuffering.value, + enableServerMachineStats: value, }).then(() => { fetchInstance(true); }); } -const headerActions = computed(() => [{ - asFullButton: true, - icon: 'ti ti-check', - text: i18n.ts.save, - handler: save, -}]); +function onChange_enableIdenticonGeneration(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableIdenticonGeneration: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForRemoteUser(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForRemoteUser: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableChartsForFederatedInstances(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableChartsForFederatedInstances: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableFanoutTimeline(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableFanoutTimeline: value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableFanoutTimelineDbFallback(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableFanoutTimelineDbFallback: value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perLocalUserUserTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perRemoteUserUserTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perUserHomeTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_perUserListTimelineCacheMax() { + os.apiWithDialog('admin/update-meta', { + perUserListTimelineCacheMax: perUserListTimelineCacheMax.value, + }).then(() => { + fetchInstance(true); + }); +} + +function onChange_enableReactionsBuffering(value: boolean) { + os.apiWithDialog('admin/update-meta', { + enableReactionsBuffering: value, + }).then(() => { + fetchInstance(true); + }); +} + +const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts index bcd1d9a159..fa19e6cd9e 100644 --- a/packages/frontend/src/router/definition.ts +++ b/packages/frontend/src/router/definition.ts @@ -471,9 +471,9 @@ const routes: RouteDef[] = [{ name: 'external-services', component: page(() => import('@/pages/admin/external-services.vue')), }, { - path: '/other-settings', - name: 'other-settings', - component: page(() => import('@/pages/admin/other-settings.vue')), + path: '/performance', + name: 'performance', + component: page(() => import('@/pages/admin/performance.vue')), }, { path: '/server-rules', name: 'server-rules', From 0e92cbf9052898d17c6e5dec8027203c62dde687 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:24:53 +0900 Subject: [PATCH 353/635] enhance(frontend): tweak control panel --- .../src/pages/admin/external-services.vue | 23 ++++--------------- .../frontend/src/pages/admin/moderation.vue | 7 ------ 2 files changed, 5 insertions(+), 25 deletions(-) diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue index e0b82eb02e..91f41166e9 100644 --- a/packages/frontend/src/pages/admin/external-services.vue +++ b/packages/frontend/src/pages/admin/external-services.vue @@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only - +
@@ -19,17 +19,11 @@ SPDX-License-Identifier: AGPL-3.0-only + Save
-
+
- @@ -40,12 +34,12 @@ import MkInput from '@/components/MkInput.vue'; import MkButton from '@/components/MkButton.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; -import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import MkFolder from '@/components/MkFolder.vue'; const deeplAuthKey = ref(''); const deeplIsPro = ref(false); @@ -56,7 +50,7 @@ async function init() { deeplIsPro.value = meta.deeplIsPro; } -function save() { +function save_deepl() { os.apiWithDialog('admin/update-meta', { deeplAuthKey: deeplAuthKey.value, deeplIsPro: deeplIsPro.value, @@ -74,10 +68,3 @@ definePageMetadata(() => ({ icon: 'ti ti-link', })); - - diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue index 82e053c2da..54eb95cd51 100644 --- a/packages/frontend/src/pages/admin/moderation.vue +++ b/packages/frontend/src/pages/admin/moderation.vue @@ -228,10 +228,3 @@ definePageMetadata(() => ({ icon: 'ti ti-shield', })); - - From 01ec7080205fadb80e658e2e2bb87f3aa14c796b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sun, 22 Sep 2024 17:50:54 +0900 Subject: [PATCH 354/635] ffix(frontend): lint fixes for tweak control panel (#14607) --- packages/frontend/src/pages/admin/performance.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 721a11e60e..1e61358117 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -57,19 +57,19 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + - + - +
From d435d04eaf992f994ff4e690a658207757c8bdf3 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:26:21 +0900 Subject: [PATCH 355/635] enhance(frontend): tweak control panel --- packages/frontend/src/components/MkFolder.vue | 18 ++ .../frontend/src/pages/admin/settings.vue | 210 +++++++++++------- 2 files changed, 151 insertions(+), 77 deletions(-) diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue index f805be7b57..79676e8354 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -41,6 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only +
+ +
@@ -56,9 +59,11 @@ import { defaultStore } from '@/store.js'; const props = withDefaults(defineProps<{ defaultOpen?: boolean; maxHeight?: number | null; + withFooter?: boolean; }>(), { defaultOpen: false, maxHeight: null, + withFooter: false }); const getBgColor = (el: HTMLElement) => { @@ -224,4 +229,17 @@ onMounted(() => { background: var(--bg); } } + +.footer { + position: sticky !important; + z-index: 1; + bottom: var(--stickyBottom, 0px); + left: 0; + padding: 9px 12px; + border-top: solid 0.5px var(--divider); + background: var(--acrylicBg); + -webkit-backdrop-filter: var(--blur, blur(15px)); + backdrop-filter: var(--blur, blur(15px)); + border-radius: 0 0 6px 6px; +} diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 6eaafed6df..1e9682775a 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -10,71 +10,93 @@ SPDX-License-Identifier: AGPL-3.0-only
- - - + + + + - - - - +
+ + + - - - + + + + - - - - + + + - - - - - + + + + - - - - + + + + + - - - - + + + + - - - - - + + + + - - - - - + + + + + - - {{ i18n.ts.repositoryUrlOrTarballRequired }} - + + + + + - - - - - + + {{ i18n.ts.repositoryUrlOrTarballRequired }} + - + + + + + +
+
+ + + - - + - + + + + + + + + + -
+
@@ -87,12 +109,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -110,12 +136,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -126,12 +156,16 @@ SPDX-License-Identifier: AGPL-3.0-only
- + - + + + -
+
@@ -173,17 +207,10 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
-
@@ -195,7 +222,6 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkInfo from '@/components/MkInfo.vue'; -import FormSection from '@/components/form/section.vue'; import FormSplit from '@/components/form/split.vue'; import FormSuspense from '@/components/form/suspense.vue'; import * as os from '@/os.js'; @@ -258,8 +284,8 @@ async function init(): Promise { urlPreviewSummaryProxyUrl.value = meta.urlPreviewSummaryProxyUrl; } -async function save() { - await os.apiWithDialog('admin/update-meta', { +function saveInfo() { + os.apiWithDialog('admin/update-meta', { name: name.value, shortName: shortName.value === '' ? null : shortName.value, description: description.value, @@ -270,22 +296,57 @@ async function save() { inquiryUrl: inquiryUrl.value, repositoryUrl: repositoryUrl.value, impressumUrl: impressumUrl.value, + }).then(() => { + fetchInstance(true); + }); +} + +function save_pinnedUsers() { + os.apiWithDialog('admin/update-meta', { pinnedUsers: pinnedUsers.value.split('\n'), + }).then(() => { + fetchInstance(true); + }); +} + +function saveFiles() { + os.apiWithDialog('admin/update-meta', { cacheRemoteFiles: cacheRemoteFiles.value, cacheRemoteSensitiveFiles: cacheRemoteSensitiveFiles.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveServiceWorker() { + os.apiWithDialog('admin/update-meta', { enableServiceWorker: enableServiceWorker.value, swPublicKey: swPublicKey.value, swPrivateKey: swPrivateKey.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveAd() { + os.apiWithDialog('admin/update-meta', { notesPerOneAd: notesPerOneAd.value, + }).then(() => { + fetchInstance(true); + }); +} + +function saveUrlPreview() { + os.apiWithDialog('admin/update-meta', { urlPreviewEnabled: urlPreviewEnabled.value, urlPreviewTimeout: urlPreviewTimeout.value, urlPreviewMaximumContentLength: urlPreviewMaximumContentLength.value, urlPreviewRequireContentLength: urlPreviewRequireContentLength.value, urlPreviewUserAgent: urlPreviewUserAgent.value, urlPreviewSummaryProxyUrl: urlPreviewSummaryProxyUrl.value, + }).then(() => { + fetchInstance(true); }); - - fetchInstance(true); } const headerTabs = computed(() => []); @@ -297,11 +358,6 @@ definePageMetadata(() => ({ diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue new file mode 100644 index 0000000000..1e88d59d8e --- /dev/null +++ b/packages/frontend/src/components/MkFormFooter.vue @@ -0,0 +1,49 @@ + + + + + + + diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 0f4d94aa4e..57f68a2a26 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -7,103 +7,100 @@ SPDX-License-Identifier: AGPL-3.0-only - -
-
- - - - -
- -
- - - - -
- -
- - - - -
- -
- - - - -
- - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - -
- - - - -
-
+
+
+ + + +
- + +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + + + +
+ + + + +
+
+
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue index 9bccee89a5..975a4a1265 100644 --- a/packages/frontend/src/pages/admin/security.vue +++ b/packages/frontend/src/pages/admin/security.vue @@ -7,119 +7,115 @@ SPDX-License-Identifier: AGPL-3.0-only - -
- - - - - - - - +
+ - - + + + + + + + + - - - - - - - +
+ {{ i18n.ts._sensitiveMediaDetection.description }} -
- {{ i18n.ts._sensitiveMediaDetection.description }} + + + + + + - - - - - - + + + + - - - - + + + + - - - - + + + + - - - - + +
+ - + + + + + - {{ i18n.ts.save }} -
-
+
+ {{ i18n.ts.activeEmailValidationDescription }} + + + + + + + + + + + + + + + + + + + + + +
+
- - - - + + + -
- {{ i18n.ts.activeEmailValidationDescription }} - - - - - - - - - - - - - - - - - - - - - - {{ i18n.ts.save }} -
-
+
+ + + +
+
- - + + + + + -
- - - - {{ i18n.ts.save }} -
-
- - - - - - -
- - - -
-
-
- +
+ + + +
+
+
@@ -131,83 +127,80 @@ import XHeader from './_header_.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkRadios from '@/components/MkRadios.vue'; import MkSwitch from '@/components/MkSwitch.vue'; -import FormSuspense from '@/components/form/suspense.vue'; import MkRange from '@/components/MkRange.vue'; import MkInput from '@/components/MkInput.vue'; -import MkButton from '@/components/MkButton.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { fetchInstance } from '@/instance.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; +import { useForm } from '@/scripts/use-form.js'; +import MkFormFooter from '@/components/MkFormFooter.vue'; -const enableHcaptcha = ref(false); -const enableMcaptcha = ref(false); -const enableRecaptcha = ref(false); -const enableTurnstile = ref(false); -const sensitiveMediaDetection = ref('none'); -const sensitiveMediaDetectionSensitivity = ref(0); -const setSensitiveFlagAutomatically = ref(false); -const enableSensitiveMediaDetectionForVideos = ref(false); -const enableIpLogging = ref(false); -const enableActiveEmailValidation = ref(false); -const enableVerifymailApi = ref(false); -const verifymailAuthKey = ref(null); -const enableTruemailApi = ref(false); -const truemailInstance = ref(null); -const truemailAuthKey = ref(null); -const bannedEmailDomains = ref(''); +const meta = await misskeyApi('admin/meta'); -async function init() { - const meta = await misskeyApi('admin/meta'); - enableHcaptcha.value = meta.enableHcaptcha; - enableMcaptcha.value = meta.enableMcaptcha; - enableRecaptcha.value = meta.enableRecaptcha; - enableTurnstile.value = meta.enableTurnstile; - sensitiveMediaDetection.value = meta.sensitiveMediaDetection; - sensitiveMediaDetectionSensitivity.value = - meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : - meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 : - meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 : - meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 : - meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0; - setSensitiveFlagAutomatically.value = meta.setSensitiveFlagAutomatically; - enableSensitiveMediaDetectionForVideos.value = meta.enableSensitiveMediaDetectionForVideos; - enableIpLogging.value = meta.enableIpLogging; - enableActiveEmailValidation.value = meta.enableActiveEmailValidation; - enableVerifymailApi.value = meta.enableVerifymailApi; - verifymailAuthKey.value = meta.verifymailAuthKey; - enableTruemailApi.value = meta.enableTruemailApi; - truemailInstance.value = meta.truemailInstance; - truemailAuthKey.value = meta.truemailAuthKey; - bannedEmailDomains.value = meta.bannedEmailDomains?.join('\n') || ''; -} - -function save() { - os.apiWithDialog('admin/update-meta', { - sensitiveMediaDetection: sensitiveMediaDetection.value, +const sensitiveMediaDetectionForm = useForm({ + sensitiveMediaDetection: meta.sensitiveMediaDetection, + sensitiveMediaDetectionSensitivity: meta.sensitiveMediaDetectionSensitivity === 'veryLow' ? 0 : + meta.sensitiveMediaDetectionSensitivity === 'low' ? 1 : + meta.sensitiveMediaDetectionSensitivity === 'medium' ? 2 : + meta.sensitiveMediaDetectionSensitivity === 'high' ? 3 : + meta.sensitiveMediaDetectionSensitivity === 'veryHigh' ? 4 : 0, + setSensitiveFlagAutomatically: meta.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: meta.enableSensitiveMediaDetectionForVideos, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + sensitiveMediaDetection: state.sensitiveMediaDetection, sensitiveMediaDetectionSensitivity: - sensitiveMediaDetectionSensitivity.value === 0 ? 'veryLow' : - sensitiveMediaDetectionSensitivity.value === 1 ? 'low' : - sensitiveMediaDetectionSensitivity.value === 2 ? 'medium' : - sensitiveMediaDetectionSensitivity.value === 3 ? 'high' : - sensitiveMediaDetectionSensitivity.value === 4 ? 'veryHigh' : + state.sensitiveMediaDetectionSensitivity === 0 ? 'veryLow' : + state.sensitiveMediaDetectionSensitivity === 1 ? 'low' : + state.sensitiveMediaDetectionSensitivity === 2 ? 'medium' : + state.sensitiveMediaDetectionSensitivity === 3 ? 'high' : + state.sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' : 0, - setSensitiveFlagAutomatically: setSensitiveFlagAutomatically.value, - enableSensitiveMediaDetectionForVideos: enableSensitiveMediaDetectionForVideos.value, - enableIpLogging: enableIpLogging.value, - enableActiveEmailValidation: enableActiveEmailValidation.value, - enableVerifymailApi: enableVerifymailApi.value, - verifymailAuthKey: verifymailAuthKey.value, - enableTruemailApi: enableTruemailApi.value, - truemailInstance: truemailInstance.value, - truemailAuthKey: truemailAuthKey.value, - bannedEmailDomains: bannedEmailDomains.value.split('\n'), - }).then(() => { - fetchInstance(true); + setSensitiveFlagAutomatically: state.setSensitiveFlagAutomatically, + enableSensitiveMediaDetectionForVideos: state.enableSensitiveMediaDetectionForVideos, }); -} + fetchInstance(true); +}); + +const ipLoggingForm = useForm({ + enableIpLogging: meta.enableIpLogging, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableIpLogging: state.enableIpLogging, + }); + fetchInstance(true); +}); + +const emailValidationForm = useForm({ + enableActiveEmailValidation: meta.enableActiveEmailValidation, + enableVerifymailApi: meta.enableVerifymailApi, + verifymailAuthKey: meta.verifymailAuthKey, + enableTruemailApi: meta.enableTruemailApi, + truemailInstance: meta.truemailInstance, + truemailAuthKey: meta.truemailAuthKey, +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + enableActiveEmailValidation: state.enableActiveEmailValidation, + enableVerifymailApi: state.enableVerifymailApi, + verifymailAuthKey: state.verifymailAuthKey, + enableTruemailApi: state.enableTruemailApi, + truemailInstance: state.truemailInstance, + truemailAuthKey: state.truemailAuthKey, + }); + fetchInstance(true); +}); + +const bannedEmailDomainsForm = useForm({ + bannedEmailDomains: meta.bannedEmailDomains?.join('\n') || '', +}, async (state) => { + await os.apiWithDialog('admin/update-meta', { + bannedEmailDomains: state.bannedEmailDomains.split('\n'), + }); + fetchInstance(true); +}); const headerActions = computed(() => []); From 3f0aaaa41efe42776d70490ea213e3c8b194c152 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Mon, 23 Sep 2024 19:49:52 +0900 Subject: [PATCH 372/635] perf(embed): improve embed performance (#14613) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * wip * refactor * refactor --------- Co-authored-by: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> --- .../src/server/web/ClientServerService.ts | 66 +++++++++++++++++++ .../src/server/web/views/base-embed.pug | 3 + packages/frontend-embed/src/boot.ts | 10 ++- .../src/components/EmNoteDetailed.vue | 4 +- .../frontend-embed/src/components/EmNotes.vue | 6 +- packages/frontend-embed/src/di.ts | 2 + packages/frontend-embed/src/pages/clip.vue | 44 +++++++------ packages/frontend-embed/src/pages/note.vue | 35 +++++----- packages/frontend-embed/src/pages/tag.vue | 7 +- .../src/pages/user-timeline.vue | 49 ++++++++------ packages/frontend-embed/src/server-context.ts | 21 ++++++ packages/frontend-embed/src/ui.vue | 16 +++-- 12 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 packages/frontend-embed/src/server-context.ts diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index 063141273a..5de1f87667 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -785,6 +785,72 @@ export class ClientServerService { //#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'); diff --git a/packages/backend/src/server/web/views/base-embed.pug b/packages/backend/src/server/web/views/base-embed.pug index d773f2676a..2bab20a36c 100644 --- a/packages/backend/src/server/web/views/base-embed.pug +++ b/packages/backend/src/server/web/views/base-embed.pug @@ -43,6 +43,9 @@ html(class='embed') script(type='application/json' id='misskey_meta' data-generated-at=now) != metaJson + script(type='application/json' id='misskey_embedCtx' data-generated-at=now) + != embedCtx + script include ../boot.embed.js diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts index fcea7d32ea..00c7944eb3 100644 --- a/packages/frontend-embed/src/boot.ts +++ b/packages/frontend-embed/src/boot.ts @@ -20,16 +20,19 @@ import { serverMetadata } from '@/server-metadata.js'; import { url } from '@@/js/config.js'; import { parseEmbedParams } from '@@/js/embed-page.js'; import { postMessageToParentWindow, setIframeId } from '@/post-message.js'; +import { serverContext } from '@/server-context.js'; import type { Theme } from '@/theme.js'; console.log('Misskey Embed'); +//#region Embedパラメータの取得・パース const params = new URLSearchParams(location.search); const embedParams = parseEmbedParams(params); - if (_DEV_) console.log(embedParams); +//#endregion +//#region テーマ function parseThemeOrNull(theme: string | null): Theme | null { if (theme == null) return null; try { @@ -65,6 +68,7 @@ if (embedParams.colorMode === 'dark') { } }); } +//#endregion // サイズの制限 document.documentElement.style.maxWidth = '500px'; @@ -89,6 +93,10 @@ const app = createApp( app.provide(DI.mediaProxy, new MediaProxy(serverMetadata, url)); +app.provide(DI.serverMetadata, serverMetadata); + +app.provide(DI.serverContext, serverContext); + app.provide(DI.embedParams, embedParams); // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue index 8169f500a9..a233011af7 100644 --- a/packages/frontend-embed/src/components/EmNoteDetailed.vue +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -142,8 +142,8 @@ import EmAcct from '@/components/EmAcct.vue'; import { userPage } from '@/utils.js'; import { notePage } from '@/utils.js'; import { i18n } from '@/i18n.js'; +import { DI } from '@/di.js'; import { shouldCollapsed } from '@@/js/collapsed.js'; -import { serverMetadata } from '@/server-metadata.js'; import { url } from '@@/js/config.js'; import EmMfm from '@/components/EmMfm.js'; @@ -151,6 +151,8 @@ const props = defineProps<{ note: Misskey.entities.Note; }>(); +const serverMetadata = inject(DI.serverMetadata)!; + const inChannel = inject('inChannel', null); const note = ref(props.note); diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue index 6370f4aeae..3418d97f77 100644 --- a/packages/frontend-embed/src/components/EmNotes.vue +++ b/packages/frontend-embed/src/components/EmNotes.vue @@ -20,12 +20,12 @@ SPDX-License-Identifier: AGPL-3.0-only From 1b2b95e199938d30be546c1afa1088eecdc1097c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 24 Sep 2024 01:22:57 +0000 Subject: [PATCH 385/635] Bump version to 2024.9.0-alpha.8 --- package.json | 2 +- packages/misskey-js/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 3bfa95a206..63c22cdc5b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.9.0-alpha.7", + "version": "2024.9.0-alpha.8", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index 6c07ce87da..0f797e8259 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.9.0-alpha.7", + "version": "2024.9.0-alpha.8", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", From 4be307f22363ab594984c240a292509bfb6895fa Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 24 Sep 2024 13:55:35 +0900 Subject: [PATCH 386/635] refactor --- packages/frontend/src/ui/_common_/navbar-for-mobile.vue | 2 +- packages/frontend/src/ui/_common_/navbar.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue index e80d5fd399..5115d21d56 100644 --- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue +++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue @@ -82,7 +82,7 @@ function more() { diff --git a/idea/README.md b/idea/README.md new file mode 100644 index 0000000000..f64d16800a --- /dev/null +++ b/idea/README.md @@ -0,0 +1 @@ +使われなくなったけど消すのは勿体ない(将来使えるかもしれない)コードを入れておくとこ diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue index 57f68a2a26..7e0a932f82 100644 --- a/packages/frontend/src/pages/admin/performance.vue +++ b/packages/frontend/src/pages/admin/performance.vue @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
- - - - +
@@ -110,7 +112,6 @@ import MkSwitch from '@/components/MkSwitch.vue'; import MkFolder from '@/components/MkFolder.vue'; import MkInput from '@/components/MkInput.vue'; import MkLink from '@/components/MkLink.vue'; -import MkButton from '@/components/MkButton.vue'; import { useForm } from '@/scripts/use-form.js'; import MkFormFooter from '@/components/MkFormFooter.vue'; diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue index 537c86cb14..5207f0e38e 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -169,42 +169,44 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - +
@@ -230,7 +232,6 @@ SPDX-License-Identifier: AGPL-3.0-only From 8c3be57ab362b8b2a24dad9b42ac0c3762bcb34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Wed, 25 Sep 2024 16:12:34 +0900 Subject: [PATCH 405/635] =?UTF-8?q?fix(frontend-embed):=20URL=E3=82=A8?= =?UTF-8?q?=E3=83=B3=E3=82=B3=E3=83=BC=E3=83=89=E3=81=95=E3=82=8C=E3=81=9F?= =?UTF-8?q?=E6=96=87=E5=AD=97=E5=88=97=E3=81=8C=E6=AD=A3=E5=B8=B8=E3=81=AB?= =?UTF-8?q?=E8=AA=AD=E3=81=BF=E8=BE=BC=E3=82=81=E3=81=AA=E3=81=84=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#14630)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend-embed): URLエンコードされた文字列が正常に読み込めない問題を修正 * fix(frontend-embed): bring back missing bits --- packages/frontend-embed/src/pages/user-timeline.vue | 13 ++++++++++++- packages/frontend-embed/src/ui.vue | 10 +++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue index 2d5dbb687b..85e6f52d50 100644 --- a/packages/frontend-embed/src/pages/user-timeline.vue +++ b/packages/frontend-embed/src/pages/user-timeline.vue @@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only -
+
+