diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index bad03e84ed..c6e93a0f67 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -54,7 +54,7 @@ export class ApNoteService { // 循環参照のため / for circular dependency @Inject(forwardRef(() => ApPersonService)) private apPersonService: ApPersonService, - + private utilityService: UtilityService, private apAudienceService: ApAudienceService, private apMentionService: ApMentionService, @@ -73,15 +73,15 @@ export class ApNoteService { @bindThis public validateNote(object: IObject, uri: string) { const expectHost = this.utilityService.extractDbHost(uri); - + if (object == null) { return new Error('invalid Note: object is null'); } - + if (!validPost.includes(getApType(object))) { return new Error(`invalid Note: invalid object type ${getApType(object)}`); } - + if (object.id && this.utilityService.extractDbHost(object.id) !== expectHost) { return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); } @@ -90,10 +90,10 @@ export class ApNoteService { if (object.attributedTo && actualHost !== expectHost) { return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } - + return null; } - + /** * Noteをフェッチします。 * @@ -103,16 +103,16 @@ export class ApNoteService { public async fetchNote(object: string | IObject): Promise { return await this.apDbResolverService.getNoteFromApId(object); } - + /** * Noteを作成します。 */ @bindThis public async createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise { if (resolver == null) resolver = this.apResolverService.createResolver(); - + const object = await resolver.resolve(value); - + const entryUri = getApId(value); const err = this.validateNote(object, entryUri); if (err) { @@ -125,9 +125,9 @@ export class ApNoteService { }); throw new Error('invalid note'); } - + const note = object as IPost; - + this.logger.debug(`Note fetched: ${JSON.stringify(note, null, 2)}`); if (note.id && !checkHttps(note.id)) { @@ -139,21 +139,21 @@ export class ApNoteService { if (url && !checkHttps(url)) { throw new Error('unexpected shcema of note url: ' + url); } - + this.logger.info(`Creating the Note: ${note.id}`); - + // 投稿者をフェッチ const actor = await this.apPersonService.resolvePerson(getOneApId(note.attributedTo!), resolver) as RemoteUser; - + // 投稿者が凍結されていたらスキップ if (actor.isSuspended) { throw new Error('actor has been suspended'); } - + const noteAudience = await this.apAudienceService.parseAudience(actor, note.to, note.cc, resolver); let visibility = noteAudience.visibility; const visibleUsers = noteAudience.visibleUsers; - + // Audience (to, cc) が指定されてなかった場合 if (visibility === 'specified' && visibleUsers.length === 0) { if (typeof value === 'string') { // 入力がstringならばresolverでGETが発生している @@ -161,23 +161,23 @@ export class ApNoteService { visibility = 'public'; } } - + const apMentions = await this.apMentionService.extractApMentions(note.tag, resolver); const apHashtags = await extractApHashtags(note.tag); - + // 添付ファイル // TODO: attachmentは必ずしもImageではない // TODO: attachmentは必ずしも配列ではない // Noteがsensitiveなら添付もsensitiveにする const limit = promiseLimit(2); - + note.attachment = Array.isArray(note.attachment) ? note.attachment : note.attachment ? [note.attachment] : []; const files = note.attachment .map(attach => attach.sensitive = note.sensitive) ? (await Promise.all(note.attachment.map(x => limit(() => this.apImageService.resolveImage(actor, x)) as Promise))) .filter(image => image != null) : []; - + // リプライ const reply: Note | null = note.inReplyTo ? await this.resolveNote(note.inReplyTo, resolver).then(x => { @@ -192,10 +192,10 @@ export class ApNoteService { throw err; }) : null; - + // 引用 let quote: Note | undefined | null; - + if (note._misskey_quote || note.quoteUrl) { const tryResolveNote = async (uri: string): Promise<{ status: 'ok'; @@ -222,10 +222,10 @@ export class ApNoteService { }; } }; - + const uris = unique([note._misskey_quote, note.quoteUrl].filter((x): x is string => typeof x === 'string')); const results = await Promise.all(uris.map(uri => tryResolveNote(uri))); - + quote = results.filter((x): x is { status: 'ok', res: Note | null } => x.status === 'ok').map(x => x.res).find(x => x); if (!quote) { if (results.some(x => x.status === 'temperror')) { @@ -233,9 +233,9 @@ export class ApNoteService { } } } - + const cw = note.summary === '' ? null : note.summary; - + // テキストのパース let text: string | null = null; if (note.source?.mediaType === 'text/x.misskeymarkdown' && typeof note.source.content === 'string') { @@ -245,38 +245,38 @@ export class ApNoteService { } else if (typeof note.content === 'string') { text = this.apMfmService.htmlToMfm(note.content, note.tag); } - + // vote if (reply && reply.hasPoll) { const poll = await this.pollsRepository.findOneByOrFail({ noteId: reply.id }); - + const tryCreateVote = async (name: string, index: number): Promise => { if (poll.expiresAt && Date.now() > new Date(poll.expiresAt).getTime()) { this.logger.warn(`vote to expired poll from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); } else if (index >= 0) { this.logger.info(`vote from AP: actor=${actor.username}@${actor.host}, note=${note.id}, choice=${name}`); await this.pollService.vote(actor, reply, index); - + // リモートフォロワーにUpdate配信 this.pollService.deliverQuestionUpdate(reply.id); } return null; }; - + if (note.name) { return await tryCreateVote(note.name, poll.choices.findIndex(x => x === note.name)); } } - + const emojis = await this.extractEmojis(note.tag ?? [], actor.host).catch(e => { this.logger.info(`extractEmojis: ${e}`); return [] as Emoji[]; }); - + const apEmojis = emojis.map(emoji => emoji.name); - + const poll = await this.apQuestionService.extractPollFromQuestion(note, resolver).catch(() => undefined); - + return await this.noteCreateService.create(actor, { createdAt: note.published ? new Date(note.published) : null, files, @@ -296,7 +296,7 @@ export class ApNoteService { url: url, }, silent); } - + /** * Noteを解決します。 * @@ -307,26 +307,26 @@ export class ApNoteService { public async resolveNote(value: string | IObject, resolver?: Resolver): Promise { const uri = typeof value === 'string' ? value : value.id; if (uri == null) throw new Error('missing uri'); - + // ブロックしてたら中断 const meta = await this.metaService.fetch(); if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) throw new StatusError('blocked host', 451); - + const unlock = await this.appLockService.getApLock(uri); - + try { //#region このサーバーに既に登録されていたらそれを返す const exist = await this.fetchNote(uri); - + if (exist) { return exist; } //#endregion - + if (uri.startsWith(this.config.url)) { throw new StatusError('cannot resolve local note', 400, 'cannot resolve local note'); } - + // リモートサーバーからフェッチしてきて登録 // ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが // 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。 @@ -335,26 +335,26 @@ export class ApNoteService { unlock(); } } - + @bindThis public async extractEmojis(tags: IObject | IObject[], host: string): Promise { host = this.utilityService.toPuny(host); - + if (!tags) return []; - + const eomjiTags = toArray(tags).filter(isEmoji); const existingEmojis = await this.emojisRepository.findBy({ host, name: In(eomjiTags.map(tag => tag.name!.replaceAll(':', ''))), }); - + return await Promise.all(eomjiTags.map(async tag => { const name = tag.name!.replaceAll(':', ''); tag.icon = toSingle(tag.icon); - + const exists = existingEmojis.find(x => x.name === name); - + if (exists) { if ((tag.updated != null && exists.updatedAt == null) || (tag.id != null && exists.uri == null) @@ -370,18 +370,18 @@ export class ApNoteService { publicUrl: tag.icon!.url, updatedAt: new Date(), }); - + return await this.emojisRepository.findOneBy({ host, name, }) as Emoji; } - + return exists; } - + this.logger.info(`register emoji host=${host}, name=${name}`); - + return await this.emojisRepository.insert({ id: this.idService.genId(), host,