import { Inject, Injectable } from '@nestjs/common'; import escapeRegexp from 'escape-regexp'; import { DI } from '@/di-symbols.js'; import type { MessagingMessagesRepository, NotesRepository, UserPublickeysRepository, UsersRepository } from '@/models/index.js'; import type { Config } from '@/config.js'; import type { CacheableRemoteUser, CacheableUser } from '@/models/entities/User.js'; import { Cache } from '@/misc/cache.js'; import type { UserPublickey } from '@/models/entities/UserPublickey.js'; import { UserCacheService } from '@/core/UserCacheService.js'; import type { Note } from '@/models/entities/Note.js'; import type { MessagingMessage } from '@/models/entities/MessagingMessage.js'; import { bindThis } from '@/decorators.js'; import { getApId } from './type.js'; import { ApPersonService } from './models/ApPersonService.js'; import type { IObject } from './type.js'; export type UriParseResult = { /** wether the URI was generated by us */ local: true; /** id in DB */ id: string; /** hint of type, e.g. "notes", "users" */ type: string; /** any remaining text after type and id, not including the slash after id. undefined if empty */ rest?: string; } | { /** wether the URI was generated by us */ local: false; /** uri in DB */ uri: string; }; @Injectable() export class ApDbResolverService { private publicKeyCache: Cache; private publicKeyByUserIdCache: Cache; constructor( @Inject(DI.config) private config: Config, @Inject(DI.usersRepository) private usersRepository: UsersRepository, @Inject(DI.messagingMessagesRepository) private messagingMessagesRepository: MessagingMessagesRepository, @Inject(DI.notesRepository) private notesRepository: NotesRepository, @Inject(DI.userPublickeysRepository) private userPublickeysRepository: UserPublickeysRepository, private userCacheService: UserCacheService, private apPersonService: ApPersonService, ) { this.publicKeyCache = new Cache(Infinity); this.publicKeyByUserIdCache = new Cache(Infinity); } @bindThis public parseUri(value: string | IObject): UriParseResult { const uri = getApId(value); // the host part of a URL is case insensitive, so use the 'i' flag. const localRegex = new RegExp('^' + escapeRegexp(this.config.url) + '/(\\w+)/(\\w+)(?:\/(.+))?', 'i'); const matchLocal = uri.match(localRegex); if (matchLocal) { return { local: true, type: matchLocal[1], id: matchLocal[2], rest: matchLocal[3], }; } else { return { local: false, uri, }; } } /** * AP Note => Misskey Note in DB */ @bindThis public async getNoteFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { if (parsed.type !== 'notes') return null; return await this.notesRepository.findOneBy({ id: parsed.id, }); } else { return await this.notesRepository.findOneBy({ uri: parsed.uri, }); } } @bindThis public async getMessageFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { if (parsed.type !== 'notes') return null; return await this.messagingMessagesRepository.findOneBy({ id: parsed.id, }); } else { return await this.messagingMessagesRepository.findOneBy({ uri: parsed.uri, }); } } /** * AP Person => Misskey User in DB */ @bindThis public async getUserFromApId(value: string | IObject): Promise { const parsed = this.parseUri(value); if (parsed.local) { if (parsed.type !== 'users') return null; return await this.userCacheService.userByIdCache.fetchMaybe(parsed.id, () => this.usersRepository.findOneBy({ id: parsed.id, }).then(x => x ?? undefined)) ?? null; } else { return await this.userCacheService.uriPersonCache.fetch(parsed.uri, () => this.usersRepository.findOneBy({ uri: parsed.uri, })); } } /** * AP KeyId => Misskey User and Key */ @bindThis public async getAuthUserFromKeyId(keyId: string): Promise<{ user: CacheableRemoteUser; key: UserPublickey; } | 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; return { user: await this.userCacheService.findById(key.userId) as CacheableRemoteUser, key, }; } /** * AP Actor id => Misskey User and Key */ @bindThis public async getAuthUserFromApId(uri: string): Promise<{ user: CacheableRemoteUser; key: UserPublickey | null; } | null> { const user = await this.apPersonService.resolvePerson(uri) as CacheableRemoteUser; if (user == null) return null; const key = await this.publicKeyByUserIdCache.fetch(user.id, () => this.userPublickeysRepository.findOneBy({ userId: user.id }), v => v != null); return { user, key, }; } }