From e35a370af072bbab967e7a654f133193936f3c76 Mon Sep 17 00:00:00 2001 From: okayurisotto Date: Thu, 13 Jul 2023 12:48:34 +0900 Subject: [PATCH] refactor(backend): `core/activitypub` (#11247) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eslint: `explicit-function-return-type` * eslint: `no-unnecessary-condition` * eslint: `eslint-disable-next-line` * eslint: `no-unused-vars` * eslint: `comma-dangle` * eslint: `import/order` * cleanup: unnecessary non-null assertion * cleanup: `IActivity`に`actor`は常に存在するようなので * cleanup: unnecessary `as` * cleanup: unnecessary `Promise.resolve` * cleanup * refactor: `String.prototype.match()`である必要がない部分をよりシンプルな書き方に変更 * refactor: よりよい型定義 * refactor: よりよい型定義 - `LdSignature`の`normalize`メソッドでの使われ方から、 - `data`引数の型定義を`any`から`JsonLdDocument`へ修正 - `getLoader`メソッドの返り値の型定義の一部を`any`から`RemoteDocument`へ修正 - `contextUrl`が不正な値(`null`)となっていたことが判明したため`undefined`へ修正 - `document`の型と合わせるために`CONTEXTS`の型定義の一部を`unknown`から`JsonLd`へ修正 - とりあえず`satisfies`を使用 - `document`の型と合わせるために`fetchDocument`メソッドの返り値の型定義の一部を`unknown`から`JsonLd`へ修正 - どうしようもなく`as`を使用 * refactor: 型ガードを使うことでnon-null assertionをやめた * refactor: non-null assertionをやめた `.filter()`で行っている型ガードなどの文脈から、より適しているだろうと思われる書き方に変更した。 * refactor: 型ガードを使うことで`as`をやめた * refactor: `as`をやめた * refactor: よりよい型定義 - `id`は`null`とのunionになっていたが、`null`を渡している場面はなかった - またおそらくこのメソッドは`IOrderedCollection`を返すため、そちらに合わせて`null`とのunionをやめた - `IOrderedCollection`とはまだ型に相違がある - `totalItems`をコメントや使われ方を元に`number`へ推論 * refactor: `for-of` -> `Array.prototype.map` * refactor: `delete`演算子を使わない形に --- .../src/core/activitypub/ApAudienceService.ts | 20 ++-- .../core/activitypub/ApDbResolverService.ts | 22 +++-- .../activitypub/ApDeliverManagerService.ts | 51 +++++----- .../src/core/activitypub/ApInboxService.ts | 31 +++--- .../src/core/activitypub/ApMfmService.ts | 9 +- .../src/core/activitypub/ApRendererService.ts | 94 ++++++++----------- .../src/core/activitypub/ApRequestService.ts | 4 +- .../src/core/activitypub/ApResolverService.ts | 8 +- .../core/activitypub/LdSignatureService.ts | 49 +++++----- .../src/core/activitypub/misc/contexts.ts | 10 +- .../core/activitypub/models/ApNoteService.ts | 2 +- packages/backend/src/core/activitypub/type.ts | 1 - .../src/server/ActivityPubServerService.ts | 21 ++++- 13 files changed, 163 insertions(+), 159 deletions(-) diff --git a/packages/backend/src/core/activitypub/ApAudienceService.ts b/packages/backend/src/core/activitypub/ApAudienceService.ts index 0eab7fa33..a4ab5eae2 100644 --- a/packages/backend/src/core/activitypub/ApAudienceService.ts +++ b/packages/backend/src/core/activitypub/ApAudienceService.ts @@ -16,6 +16,8 @@ type AudienceInfo = { visibleUsers: User[], }; +type GroupedAudience = Record<'public' | 'followers' | 'other', string[]>; + @Injectable() export class ApAudienceService { constructor( @@ -67,11 +69,11 @@ export class ApAudienceService { } @bindThis - private groupingAudience(ids: string[], actor: RemoteUser) { - const groups = { - public: [] as string[], - followers: [] as string[], - other: [] as string[], + private groupingAudience(ids: string[], actor: RemoteUser): GroupedAudience { + const groups: GroupedAudience = { + public: [], + followers: [], + other: [], }; for (const id of ids) { @@ -90,7 +92,7 @@ export class ApAudienceService { } @bindThis - private isPublic(id: string) { + private isPublic(id: string): boolean { return [ 'https://www.w3.org/ns/activitystreams#Public', 'as#Public', @@ -99,9 +101,7 @@ export class ApAudienceService { } @bindThis - private isFollowers(id: string, actor: RemoteUser) { - return ( - id === (actor.followersUri ?? `${actor.uri}/followers`) - ); + private isFollowers(id: string, actor: RemoteUser): boolean { + return id === (actor.followersUri ?? `${actor.uri}/followers`); } } diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 20283a163..d5a530c90 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -99,13 +99,15 @@ export class ApDbResolverService implements OnApplicationShutdown { if (parsed.local) { if (parsed.type !== 'users') return null; - return await this.cacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ - id: parsed.id, - }).then(x => x ?? undefined)) as LocalUser | undefined ?? null; + return await this.cacheService.userByIdCache.fetchMaybe( + parsed.id, + () => this.usersRepository.findOneBy({ id: parsed.id }).then(x => x ?? undefined), + ) as LocalUser | undefined ?? null; } else { - return await this.cacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ - uri: parsed.uri, - })) as RemoteUser | null; + return await this.cacheService.uriPersonCache.fetch( + parsed.uri, + () => this.usersRepository.findOneBy({ uri: parsed.uri }), + ) as RemoteUser | null; } } @@ -145,9 +147,11 @@ export class ApDbResolverService implements OnApplicationShutdown { } | null> { const user = await this.apPersonService.resolvePerson(uri) as RemoteUser; - if (user == null) return null; - - const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); + const key = await this.publicKeyByUserIdCache.fetch( + user.id, + () => this.userPublickeysRepository.findOneBy({ userId: user.id }), + v => v != null, + ); return { user, diff --git a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts index 82c2c9f71..ae1e42bf5 100644 --- a/packages/backend/src/core/activitypub/ApDeliverManagerService.ts +++ b/packages/backend/src/core/activitypub/ApDeliverManagerService.ts @@ -52,7 +52,7 @@ export class ApDeliverManagerService { * @param activity Activity */ @bindThis - public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity) { + public async deliverToFollowers(actor: { id: LocalUser['id']; host: null; }, activity: IActivity): Promise { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -71,7 +71,7 @@ export class ApDeliverManagerService { * @param to Target user */ @bindThis - public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser) { + public async deliverToUser(actor: { id: LocalUser['id']; host: null; }, activity: IActivity, to: RemoteUser): Promise { const manager = new DeliverManager( this.userEntityService, this.followingsRepository, @@ -84,7 +84,7 @@ export class ApDeliverManagerService { } @bindThis - public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null) { + public createDeliverManager(actor: { id: User['id']; host: null; }, activity: IActivity | null): DeliverManager { return new DeliverManager( this.userEntityService, this.followingsRepository, @@ -118,6 +118,7 @@ class DeliverManager { activity: IActivity | null, ) { // 型で弾いてはいるが一応ローカルユーザーかチェック + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (actor.host != null) throw new Error('actor.host must be null'); // パフォーマンス向上のためキューに突っ込むのはidのみに絞る @@ -131,10 +132,10 @@ class DeliverManager { * Add recipe for followers deliver */ @bindThis - public addFollowersRecipe() { - const deliver = { + public addFollowersRecipe(): void { + const deliver: IFollowersRecipe = { type: 'Followers', - } as IFollowersRecipe; + }; this.addRecipe(deliver); } @@ -144,11 +145,11 @@ class DeliverManager { * @param to To */ @bindThis - public addDirectRecipe(to: RemoteUser) { - const recipe = { + public addDirectRecipe(to: RemoteUser): void { + const recipe: IDirectRecipe = { type: 'Direct', to, - } as IDirectRecipe; + }; this.addRecipe(recipe); } @@ -158,7 +159,7 @@ class DeliverManager { * @param recipe Recipe */ @bindThis - public addRecipe(recipe: IRecipe) { + public addRecipe(recipe: IRecipe): void { this.recipes.push(recipe); } @@ -166,17 +167,13 @@ class DeliverManager { * Execute delivers */ @bindThis - public async execute() { + public async execute(): Promise { // The value flags whether it is shared or not. // key: inbox URL, value: whether it is sharedInbox const inboxes = new Map(); - /* - build inbox list - - Process follower recipes first to avoid duplication when processing - direct recipes later. - */ + // build inbox list + // Process follower recipes first to avoid duplication when processing direct recipes later. if (this.recipes.some(r => isFollowers(r))) { // followers deliver // TODO: SELECT DISTINCT ON ("followerSharedInbox") "followerSharedInbox" みたいな問い合わせにすればよりパフォーマンス向上できそう @@ -190,26 +187,24 @@ class DeliverManager { followerSharedInbox: true, followerInbox: true, }, - }) as { - followerSharedInbox: string | null; - followerInbox: string; - }[]; + }); for (const following of followers) { const inbox = following.followerSharedInbox ?? following.followerInbox; + if (inbox === null) throw new Error('inbox is null'); inboxes.set(inbox, following.followerSharedInbox != null); } } - this.recipes.filter((recipe): recipe is IDirectRecipe => - // followers recipes have already been processed - isDirect(recipe) + for (const recipe of this.recipes.filter(isDirect)) { // check that shared inbox has not been added yet - && !(recipe.to.sharedInbox && inboxes.has(recipe.to.sharedInbox)) + if (recipe.to.sharedInbox !== null && inboxes.has(recipe.to.sharedInbox)) continue; + // check that they actually have an inbox - && recipe.to.inbox != null, - ) - .forEach(recipe => inboxes.set(recipe.to.inbox!, false)); + if (recipe.to.inbox === null) continue; + + inboxes.set(recipe.to.inbox, false); + } // deliver this.queueService.deliverMany(this.actor, this.activity, inboxes); diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index b0428763c..8d5f4883e 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -21,10 +21,10 @@ import { CacheService } from '@/core/CacheService.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/index.js'; +import type { UsersRepository, NotesRepository, FollowingsRepository, AbuseUserReportsRepository, FollowRequestsRepository } from '@/models/index.js'; import { bindThis } from '@/decorators.js'; import type { RemoteUser } from '@/models/entities/User.js'; -import { getApHrefNullable, getApId, getApIds, getApType, getOneApHrefNullable, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; +import { getApHrefNullable, getApId, getApIds, getApType, isAccept, isActor, isAdd, isAnnounce, isBlock, isCollection, isCollectionOrOrderedCollection, isCreate, isDelete, isFlag, isFollow, isLike, isMove, isPost, isReject, isRemove, isTombstone, isUndo, isUpdate, validActor, validPost } from './type.js'; import { ApNoteService } from './models/ApNoteService.js'; import { ApLoggerService } from './ApLoggerService.js'; import { ApDbResolverService } from './ApDbResolverService.js'; @@ -86,7 +86,7 @@ export class ApInboxService { } @bindThis - public async performActivity(actor: RemoteUser, activity: IObject) { + public async performActivity(actor: RemoteUser, activity: IObject): Promise { if (isCollectionOrOrderedCollection(activity)) { const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { @@ -107,7 +107,7 @@ export class ApInboxService { if (actor.uri) { if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) { setImmediate(() => { - this.apPersonService.updatePerson(actor.uri!); + this.apPersonService.updatePerson(actor.uri); }); } } @@ -229,7 +229,7 @@ export class ApInboxService { @bindThis private async add(actor: RemoteUser, activity: IAdd): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -273,7 +273,7 @@ export class ApInboxService { const unlock = await this.appLockService.getApLock(uri); try { - // 既に同じURIを持つものが登録されていないかチェック + // 既に同じURIを持つものが登録されていないかチェック const exist = await this.apNoteService.fetchNote(uri); if (exist) { return; @@ -292,7 +292,7 @@ export class ApInboxService { return; } - this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode ?? err}`); + this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`); } throw err; } @@ -409,7 +409,7 @@ export class ApInboxService { @bindThis private async delete(actor: RemoteUser, activity: IDelete): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -420,7 +420,7 @@ export class ApInboxService { // typeが不明だけど、どうせ消えてるのでremote resolveしない formerType = undefined; } else { - const object = activity.object as IObject; + const object = activity.object; if (isTombstone(object)) { formerType = toSingle(object.formerType); } else { @@ -503,7 +503,10 @@ export class ApInboxService { // 対象ユーザーは一番最初のユーザー として あとはコメントとして格納する const uris = getApIds(activity.object); - const userIds = uris.filter(uri => uri.startsWith(this.config.url + '/users/')).map(uri => uri.split('/').pop()!); + const userIds = uris + .filter(uri => uri.startsWith(this.config.url + '/users/')) + .map(uri => uri.split('/').at(-1)) + .filter((userId): userId is string => userId !== undefined); const users = await this.usersRepository.findBy({ id: In(userIds), }); @@ -566,7 +569,7 @@ export class ApInboxService { @bindThis private async remove(actor: RemoteUser, activity: IRemove): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -586,7 +589,7 @@ export class ApInboxService { @bindThis private async undo(actor: RemoteUser, activity: IUndo): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + if (actor.uri !== activity.actor) { throw new Error('invalid actor'); } @@ -719,7 +722,7 @@ export class ApInboxService { @bindThis private async update(actor: RemoteUser, activity: IUpdate): Promise { - if ('actor' in activity && actor.uri !== activity.actor) { + if (actor.uri !== activity.actor) { return 'skip: invalid actor'; } @@ -733,7 +736,7 @@ export class ApInboxService { }); if (isActor(object)) { - await this.apPersonService.updatePerson(actor.uri!, resolver, object); + await this.apPersonService.updatePerson(actor.uri, resolver, object); return 'ok: Person updated'; } else if (getApType(object) === 'Question') { await this.apQuestionService.updateQuestion(object, resolver).catch(err => console.error(err)); diff --git a/packages/backend/src/core/activitypub/ApMfmService.ts b/packages/backend/src/core/activitypub/ApMfmService.ts index 411cf17d1..d7269eca9 100644 --- a/packages/backend/src/core/activitypub/ApMfmService.ts +++ b/packages/backend/src/core/activitypub/ApMfmService.ts @@ -4,9 +4,9 @@ import { DI } from '@/di-symbols.js'; import type { Config } from '@/config.js'; import { MfmService } from '@/core/MfmService.js'; import type { Note } from '@/models/entities/Note.js'; +import { bindThis } from '@/decorators.js'; import { extractApHashtagObjects } from './models/tag.js'; import type { IObject } from './type.js'; -import { bindThis } from '@/decorators.js'; @Injectable() export class ApMfmService { @@ -19,14 +19,13 @@ export class ApMfmService { } @bindThis - public htmlToMfm(html: string, tag?: IObject | IObject[]) { - const hashtagNames = extractApHashtagObjects(tag).map(x => x.name).filter((x): x is string => x != null); - + public htmlToMfm(html: string, tag?: IObject | IObject[]): string { + const hashtagNames = extractApHashtagObjects(tag).map(x => x.name); return this.mfmService.fromHtml(html, hashtagNames); } @bindThis - public getNoteHtml(note: Note) { + public getNoteHtml(note: Note): string | null { if (!note.text) return ''; return this.mfmService.toHtml(mfm.parse(note.text), JSON.parse(note.mentionedRemoteUsers)); } diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 96ac5c61f..95e3a936d 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -1,6 +1,6 @@ import { createPublicKey } from 'node:crypto'; import { Inject, Injectable } from '@nestjs/common'; -import { In, IsNull } from 'typeorm'; +import { In } from 'typeorm'; import { v4 as uuid } from 'uuid'; import * as mfm from 'mfm-js'; import { DI } from '@/di-symbols.js'; @@ -26,7 +26,6 @@ import { isNotNull } from '@/misc/is-not-null.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 { IIdentifier } from './models/identifier.js'; @Injectable() export class ApRendererService { @@ -63,7 +62,7 @@ export class ApRendererService { } @bindThis - public renderAccept(object: any, user: { id: User['id']; host: null }): IAccept { + public renderAccept(object: string | IObject, user: { id: User['id']; host: null }): IAccept { return { type: 'Accept', actor: this.userEntityService.genLocalUserUri(user.id), @@ -72,7 +71,7 @@ export class ApRendererService { } @bindThis - public renderAdd(user: LocalUser, target: any, object: any): IAdd { + public renderAdd(user: LocalUser, target: string | IObject | undefined, object: string | IObject): IAdd { return { type: 'Add', actor: this.userEntityService.genLocalUserUri(user.id), @@ -82,7 +81,7 @@ export class ApRendererService { } @bindThis - public renderAnnounce(object: any, note: Note): IAnnounce { + public renderAnnounce(object: string | IObject, note: Note): IAnnounce { const attributedTo = this.userEntityService.genLocalUserUri(note.userId); let to: string[] = []; @@ -133,13 +132,13 @@ export class ApRendererService { @bindThis public renderCreate(object: IObject, note: Note): ICreate { - const activity = { + const activity: ICreate = { id: `${this.config.url}/notes/${note.id}/activity`, actor: this.userEntityService.genLocalUserUri(note.userId), type: 'Create', published: note.createdAt.toISOString(), object, - } as ICreate; + }; if (object.to) activity.to = object.to; if (object.cc) activity.cc = object.cc; @@ -209,7 +208,7 @@ export class ApRendererService { * @param id Follower|Followee ID */ @bindThis - public async renderFollowUser(id: User['id']) { + public async renderFollowUser(id: User['id']): Promise { const user = await this.usersRepository.findOneByOrFail({ id: id }) as PartialLocalUser | PartialRemoteUser; return this.userEntityService.getUserUri(user); } @@ -223,8 +222,8 @@ export class ApRendererService { return { id: requestId ?? `${this.config.url}/follows/${follower.id}/${followee.id}`, type: 'Follow', - actor: this.userEntityService.getUserUri(follower)!, - object: this.userEntityService.getUserUri(followee)!, + actor: this.userEntityService.getUserUri(follower), + object: this.userEntityService.getUserUri(followee), }; } @@ -264,14 +263,14 @@ export class ApRendererService { public async renderLike(noteReaction: NoteReaction, note: { uri: string | null }): Promise { const reaction = noteReaction.reaction; - const object = { + const object: ILike = { type: 'Like', id: `${this.config.url}/likes/${noteReaction.id}`, actor: `${this.config.url}/users/${noteReaction.userId}`, object: note.uri ? note.uri : `${this.config.url}/notes/${noteReaction.noteId}`, content: reaction, _misskey_reaction: reaction, - } as ILike; + }; if (reaction.startsWith(':')) { const name = reaction.replaceAll(':', ''); @@ -287,7 +286,7 @@ export class ApRendererService { public renderMention(mention: PartialLocalUser | PartialRemoteUser): IApMention { return { type: 'Mention', - href: this.userEntityService.getUserUri(mention)!, + href: this.userEntityService.getUserUri(mention), name: this.userEntityService.isRemoteUser(mention) ? `@${mention.username}@${mention.host}` : `@${(mention as LocalUser).username}`, }; } @@ -297,8 +296,8 @@ export class ApRendererService { src: PartialLocalUser | PartialRemoteUser, dst: PartialLocalUser | PartialRemoteUser, ): IMove { - const actor = this.userEntityService.getUserUri(src)!; - const target = this.userEntityService.getUserUri(dst)!; + const actor = this.userEntityService.getUserUri(src); + const target = this.userEntityService.getUserUri(dst); return { id: `${this.config.url}/moves/${src.id}/${dst.id}`, actor, @@ -310,10 +309,10 @@ export class ApRendererService { @bindThis public async renderNote(note: Note, dive = true): Promise { - const getPromisedFiles = async (ids: string[]) => { - if (!ids || ids.length === 0) return []; + const getPromisedFiles = async (ids: string[]): Promise => { + if (ids.length === 0) return []; const items = await this.driveFilesRepository.findBy({ id: In(ids) }); - return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[]; + return ids.map(id => items.find(item => item.id === id)).filter((item): item is DriveFile => item != null); }; let inReplyTo; @@ -375,7 +374,7 @@ export class ApRendererService { id: In(note.mentions), }) : []; - const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag)); + const hashtagTags = note.tags.map(tag => this.renderHashtag(tag)); const mentionTags = mentionedUsers.map(u => this.renderMention(u as LocalUser | RemoteUser)); const files = await getPromisedFiles(note.fileIds); @@ -451,37 +450,26 @@ export class ApRendererService { @bindThis public async renderPerson(user: LocalUser) { const id = this.userEntityService.genLocalUserUri(user.id); - const isSystem = !!user.username.match(/\./); + const isSystem = user.username.includes('.'); const [avatar, banner, profile] = await Promise.all([ - user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : Promise.resolve(undefined), - user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : Promise.resolve(undefined), + user.avatarId ? this.driveFilesRepository.findOneBy({ id: user.avatarId }) : undefined, + user.bannerId ? this.driveFilesRepository.findOneBy({ id: user.bannerId }) : undefined, this.userProfilesRepository.findOneByOrFail({ userId: user.id }), ]); - const attachment: { + const attachment = profile.fields.map(field => ({ type: 'PropertyValue', - name: string, - value: string, - identifier?: IIdentifier, - }[] = []; - - if (profile.fields) { - for (const field of profile.fields) { - attachment.push({ - type: 'PropertyValue', - name: field.name, - value: (field.value != null && field.value.match(/^https?:/)) - ? `${new URL(field.value).href}` - : field.value, - }); - } - } + name: field.name, + value: /^https?:/.test(field.value) + ? `${new URL(field.value).href}` + : field.value, + })); const emojis = await this.getEmojis(user.emojis); const apemojis = emojis.filter(emoji => !emoji.localOnly).map(emoji => this.renderEmoji(emoji)); - const hashtagTags = (user.tags ?? []).map(tag => this.renderHashtag(tag)); + const hashtagTags = user.tags.map(tag => this.renderHashtag(tag)); const tag = [ ...apemojis, @@ -490,7 +478,7 @@ export class ApRendererService { const keypair = await this.userKeypairService.getUserKeypair(user.id); - const person = { + const person: any = { type: isSystem ? 'Application' : user.isBot ? 'Service' : 'Person', id, inbox: `${id}/inbox`, @@ -508,11 +496,11 @@ export class ApRendererService { image: banner ? this.renderImage(banner) : null, tag, manuallyApprovesFollowers: user.isLocked, - discoverable: !!user.isExplorable, + discoverable: user.isExplorable, publicKey: this.renderKey(user, keypair, '#main-key'), isCat: user.isCat, attachment: attachment.length ? attachment : undefined, - } as any; + }; if (user.movedToUri) { person.movedTo = user.movedToUri; @@ -552,7 +540,7 @@ export class ApRendererService { } @bindThis - public renderReject(object: any, user: { id: User['id'] }): IReject { + public renderReject(object: string | IObject, user: { id: User['id'] }): IReject { return { type: 'Reject', actor: this.userEntityService.genLocalUserUri(user.id), @@ -561,7 +549,7 @@ export class ApRendererService { } @bindThis - public renderRemove(user: { id: User['id'] }, target: any, object: any): IRemove { + public renderRemove(user: { id: User['id'] }, target: string | IObject | undefined, object: string | IObject): IRemove { return { type: 'Remove', actor: this.userEntityService.genLocalUserUri(user.id), @@ -579,8 +567,8 @@ export class ApRendererService { } @bindThis - public renderUndo(object: any, user: { id: User['id'] }): IUndo { - const id = typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; + public renderUndo(object: string | IObject, user: { id: User['id'] }): IUndo { + const id = typeof object !== 'string' && typeof object.id === 'string' && object.id.startsWith(this.config.url) ? `${object.id}/undo` : undefined; return { type: 'Undo', @@ -592,7 +580,7 @@ export class ApRendererService { } @bindThis - public renderUpdate(object: any, user: { id: User['id'] }): IUpdate { + public renderUpdate(object: string | IObject, user: { id: User['id'] }): IUpdate { return { id: `${this.config.url}/users/${user.id}#updates/${new Date().getTime()}`, actor: this.userEntityService.genLocalUserUri(user.id), @@ -658,7 +646,7 @@ export class ApRendererService { vcard: 'http://www.w3.org/2006/vcard/ns#', }, ], - }, x as T & { id: string; }); + }, x as T & { id: string }); } @bindThis @@ -683,13 +671,13 @@ export class ApRendererService { */ @bindThis public renderOrderedCollectionPage(id: string, totalItems: any, orderedItems: any, partOf: string, prev?: string, next?: string) { - const page = { + const page: any = { id, partOf, type: 'OrderedCollectionPage', totalItems, orderedItems, - } as any; + }; if (prev) page.prev = prev; if (next) page.next = next; @@ -706,7 +694,7 @@ export class ApRendererService { * @param orderedItems attached objects (optional) */ @bindThis - public renderOrderedCollection(id: string | null, totalItems: any, first?: string, last?: string, orderedItems?: IObject[]) { + public renderOrderedCollection(id: string, totalItems: number, first?: string, last?: string, orderedItems?: IObject[]) { const page: any = { id, type: 'OrderedCollection', @@ -722,7 +710,7 @@ export class ApRendererService { @bindThis private async getEmojis(names: string[]): Promise { - if (names == null || names.length === 0) return []; + if (names.length === 0) return []; const allEmojis = await this.customEmojiService.localEmojisCache.fetch(); const emojis = names.map(name => allEmojis.get(name)).filter(isNotNull); diff --git a/packages/backend/src/core/activitypub/ApRequestService.ts b/packages/backend/src/core/activitypub/ApRequestService.ts index 5005612ab..44676cac2 100644 --- a/packages/backend/src/core/activitypub/ApRequestService.ts +++ b/packages/backend/src/core/activitypub/ApRequestService.ts @@ -140,7 +140,7 @@ export class ApRequestService { } @bindThis - public async signedPost(user: { id: User['id'] }, url: string, object: any) { + public async signedPost(user: { id: User['id'] }, url: string, object: unknown): Promise { const body = JSON.stringify(object); const keypair = await this.userKeypairService.getUserKeypair(user.id); @@ -169,7 +169,7 @@ export class ApRequestService { * @param url URL to fetch */ @bindThis - public async signedGet(url: string, user: { id: User['id'] }) { + public async signedGet(url: string, user: { id: User['id'] }): Promise { const keypair = await this.userKeypairService.getUserKeypair(user.id); const req = ApRequestCreator.createSignedGet({ diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index d3e0345c9..aa4720c56 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -61,10 +61,6 @@ export class Resolver { @bindThis public async resolve(value: string | IObject): Promise { - if (value == null) { - throw new Error('resolvee is null (or undefined)'); - } - if (typeof value !== 'string') { return value; } @@ -104,11 +100,11 @@ export class Resolver { ? await this.apRequestService.signedGet(value, this.user) as IObject : await this.httpRequestService.getJson(value, 'application/activity+json, application/ld+json')) as IObject; - if (object == null || ( + if ( Array.isArray(object['@context']) ? !(object['@context'] as unknown[]).includes('https://www.w3.org/ns/activitystreams') : object['@context'] !== 'https://www.w3.org/ns/activitystreams' - )) { + ) { throw new Error('invalid response'); } diff --git a/packages/backend/src/core/activitypub/LdSignatureService.ts b/packages/backend/src/core/activitypub/LdSignatureService.ts index 20fe2a0a7..af7e24322 100644 --- a/packages/backend/src/core/activitypub/LdSignatureService.ts +++ b/packages/backend/src/core/activitypub/LdSignatureService.ts @@ -3,6 +3,8 @@ import { Injectable } from '@nestjs/common'; import { HttpRequestService } from '@/core/HttpRequestService.js'; import { bindThis } from '@/decorators.js'; import { CONTEXTS } from './misc/contexts.js'; +import type { JsonLdDocument } from 'jsonld'; +import type { JsonLd, RemoteDocument } from 'jsonld/jsonld-spec.js'; // RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017 @@ -18,22 +20,21 @@ class LdSignature { @bindThis public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise { - const options = { - type: 'RsaSignature2017', - creator, - domain, - nonce: crypto.randomBytes(16).toString('hex'), - created: (created ?? new Date()).toISOString(), - } as { + const options: { type: string; creator: string; domain?: string; nonce: string; created: string; + } = { + type: 'RsaSignature2017', + creator, + nonce: crypto.randomBytes(16).toString('hex'), + created: (created ?? new Date()).toISOString(), }; - if (!domain) { - delete options.domain; + if (domain) { + options.domain = domain; } const toBeSigned = await this.createVerifyData(data, options); @@ -62,7 +63,7 @@ class LdSignature { } @bindThis - public async createVerifyData(data: any, options: any) { + public async createVerifyData(data: any, options: any): Promise { const transformedOptions = { ...options, '@context': 'https://w3id.org/identity/v1', @@ -82,7 +83,7 @@ class LdSignature { } @bindThis - public async normalize(data: any) { + public async normalize(data: JsonLdDocument): Promise { const customLoader = this.getLoader(); // XXX: Importing jsonld dynamically since Jest frequently fails to import it statically // https://github.com/misskey-dev/misskey/pull/9894#discussion_r1103753595 @@ -93,14 +94,14 @@ class LdSignature { @bindThis private getLoader() { - return async (url: string): Promise => { - if (!url.match('^https?\:\/\/')) throw new Error(`Invalid URL ${url}`); + return async (url: string): Promise => { + if (!/^https?:\/\//.test(url)) throw new Error(`Invalid URL ${url}`); if (this.preLoad) { if (url in CONTEXTS) { if (this.debug) console.debug(`HIT: ${url}`); return { - contextUrl: null, + contextUrl: undefined, document: CONTEXTS[url], documentUrl: url, }; @@ -110,7 +111,7 @@ class LdSignature { if (this.debug) console.debug(`MISS: ${url}`); const document = await this.fetchDocument(url); return { - contextUrl: null, + contextUrl: undefined, document: document, documentUrl: url, }; @@ -118,13 +119,17 @@ class LdSignature { } @bindThis - private async fetchDocument(url: string) { - const json = await this.httpRequestService.send(url, { - headers: { - Accept: 'application/ld+json, application/json', + private async fetchDocument(url: string): Promise { + const json = await this.httpRequestService.send( + url, + { + headers: { + Accept: 'application/ld+json, application/json', + }, + timeout: this.loderTimeout, }, - timeout: this.loderTimeout, - }, { throwErrorWhenResponseNotOk: false }).then(res => { + { throwErrorWhenResponseNotOk: false }, + ).then(res => { if (!res.ok) { throw new Error(`${res.status} ${res.statusText}`); } else { @@ -132,7 +137,7 @@ class LdSignature { } }); - return json; + return json as JsonLd; } @bindThis diff --git a/packages/backend/src/core/activitypub/misc/contexts.ts b/packages/backend/src/core/activitypub/misc/contexts.ts index aee0d3629..2bcd3811b 100644 --- a/packages/backend/src/core/activitypub/misc/contexts.ts +++ b/packages/backend/src/core/activitypub/misc/contexts.ts @@ -1,3 +1,5 @@ +import type { JsonLd } from 'jsonld/jsonld-spec.js'; + /* eslint:disable:quotemark indent */ const id_v1 = { '@context': { @@ -86,7 +88,7 @@ const id_v1 = { 'accessControl': { '@id': 'perm:accessControl', '@type': '@id' }, 'writePermission': { '@id': 'perm:writePermission', '@type': '@id' }, }, -}; +} satisfies JsonLd; const security_v1 = { '@context': { @@ -137,7 +139,7 @@ const security_v1 = { 'signatureAlgorithm': 'sec:signingAlgorithm', 'signatureValue': 'sec:signatureValue', }, -}; +} satisfies JsonLd; const activitystreams = { '@context': { @@ -517,9 +519,9 @@ const activitystreams = { '@type': '@id', }, }, -}; +} satisfies JsonLd; -export const CONTEXTS: Record = { +export const CONTEXTS: Record = { 'https://w3id.org/identity/v1': id_v1, 'https://w3id.org/security/v1': security_v1, 'https://www.w3.org/ns/activitystreams': activitystreams, diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index d3359ef90..46ed976a6 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -200,7 +200,7 @@ export class ApNoteService { | { status: 'ok'; res: Note } | { status: 'permerror' | 'temperror' } > => { - if (!uri.match(/^https?:/)) return { status: 'permerror' }; + if (!/^https?:/.test(uri)) return { status: 'permerror' }; try { const res = await this.resolveNote(uri); if (res == null) return { status: 'permerror' }; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 625135da6..4bb0fa61e 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -194,7 +194,6 @@ export interface IApPropertyValue extends IObject { } export const isPropertyValue = (object: IObject): object is IApPropertyValue => - object && getApType(object) === 'PropertyValue' && typeof object.name === 'string' && 'value' in object && diff --git a/packages/backend/src/server/ActivityPubServerService.ts b/packages/backend/src/server/ActivityPubServerService.ts index f53351d99..f75170934 100644 --- a/packages/backend/src/server/ActivityPubServerService.ts +++ b/packages/backend/src/server/ActivityPubServerService.ts @@ -189,7 +189,11 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followersCount, `${partOf}?page=true`); + const rendered = this.apRendererService.renderOrderedCollection( + partOf, + user.followersCount, + `${partOf}?page=true`, + ); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); @@ -277,7 +281,11 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection(partOf, user.followingCount, `${partOf}?page=true`); + const rendered = this.apRendererService.renderOrderedCollection( + partOf, + user.followingCount, + `${partOf}?page=true`, + ); reply.header('Cache-Control', 'public, max-age=180'); this.setResponseType(request, reply); return (this.apRendererService.addContext(rendered)); @@ -310,7 +318,10 @@ export class ActivityPubServerService { const rendered = this.apRendererService.renderOrderedCollection( `${this.config.url}/users/${userId}/collections/featured`, - renderedNotes.length, undefined, undefined, renderedNotes, + renderedNotes.length, + undefined, + undefined, + renderedNotes, ); reply.header('Cache-Control', 'public, max-age=180'); @@ -395,7 +406,9 @@ export class ActivityPubServerService { return (this.apRendererService.addContext(rendered)); } else { // index page - const rendered = this.apRendererService.renderOrderedCollection(partOf, user.notesCount, + const rendered = this.apRendererService.renderOrderedCollection( + partOf, + user.notesCount, `${partOf}?page=true`, `${partOf}?page=true&since_id=000000000000000000000000`, );