diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index fd9c7c2ba..f1283cc5e 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -38,7 +38,7 @@ import { ApAudienceService } from './ApAudienceService.js'; import { ApPersonService } from './models/ApPersonService.js'; import { ApQuestionService } from './models/ApQuestionService.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'; +import type { IAccept, IAdd, IAnnounce, IBlock, ICreate, IDelete, IFlag, IFollow, ILike, IObject, IReject, IRemove, IUndo, IUpdate, IMove, IPost } from './type.js'; @Injectable() export class ApInboxService { @@ -90,13 +90,15 @@ export class ApInboxService { } @bindThis - public async performActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise { + public async performActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise { + let result = 'error'; if (isCollectionOrOrderedCollection(activity)) { + const results = [] as [string, string | void][]; const resolver = this.apResolverService.createResolver(); for (const item of toArray(isCollection(activity) ? activity.items : activity.orderedItems)) { const act = await resolver.resolve(item); try { - await this.performOneActivity(actor, act, additionalCc); + results.push([getApId(item), await this.performOneActivity(actor, act, additionalCc)]); } catch (err) { if (err instanceof Error || typeof err === 'string') { this.logger.error(err); @@ -105,8 +107,13 @@ export class ApInboxService { } } } + + const hasReason = results.some(([, reason]) => (reason != null && !reason.startsWith('ok'))); + if (hasReason) { + result = results.map(([id, reason]) => `${id}: ${reason}`).join('\n'); + } } else { - await this.performOneActivity(actor, activity, additionalCc); + result = await this.performOneActivity(actor, activity, additionalCc); } // ついでにリモートユーザーの情報が古かったら更新しておく @@ -117,42 +124,44 @@ export class ApInboxService { }); } } + + return result; } @bindThis - public async performOneActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise { - if (actor.isSuspended) return; + public async performOneActivity(actor: MiRemoteUser, activity: IObject, additionalCc?: MiLocalUser['id']): Promise { + if (actor.isSuspended) return 'skip: actor is suspended'; if (isCreate(activity)) { - await this.create(actor, activity, additionalCc); + return await this.create(actor, activity, additionalCc); } else if (isDelete(activity)) { - await this.delete(actor, activity); + return await this.delete(actor, activity); } else if (isUpdate(activity)) { - await this.update(actor, activity, additionalCc); + return await this.update(actor, activity, additionalCc); } else if (isFollow(activity)) { - await this.follow(actor, activity); + return await this.follow(actor, activity); } else if (isAccept(activity)) { - await this.accept(actor, activity); + return await this.accept(actor, activity); } else if (isReject(activity)) { - await this.reject(actor, activity); + return await this.reject(actor, activity); } else if (isAdd(activity)) { - await this.add(actor, activity).catch(err => this.logger.error(err)); + return await this.add(actor, activity).catch(err => { this.logger.error(err); return `error: ${err.message}`; }); } else if (isRemove(activity)) { - await this.remove(actor, activity).catch(err => this.logger.error(err)); + return await this.remove(actor, activity).catch(err => { this.logger.error(err); return `error: ${err.message}`; }); } else if (isAnnounce(activity)) { - await this.announce(actor, activity); + return await this.announce(actor, activity); } else if (isLike(activity)) { - await this.like(actor, activity); + return await this.like(actor, activity); } else if (isUndo(activity)) { - await this.undo(actor, activity); + return await this.undo(actor, activity); } else if (isBlock(activity)) { - await this.block(actor, activity); + return await this.block(actor, activity); } else if (isFlag(activity)) { - await this.flag(actor, activity); + return await this.flag(actor, activity); } else if (isMove(activity)) { - await this.move(actor, activity); + return await this.move(actor, activity); } else { - this.logger.warn(`unrecognized activity type: ${activity.type}`); + return `skip: unknown activity type ${activity.type}`; } } @@ -234,47 +243,56 @@ export class ApInboxService { } @bindThis - private async add(actor: MiRemoteUser, activity: IAdd): Promise { + private async add(actor: MiRemoteUser, activity: IAdd): Promise { if (actor.uri !== activity.actor) { - throw new Error('invalid actor'); + return 'skip: invalid actor'; } if (activity.target == null) { - throw new Error('target is null'); + return 'skip: target is null'; } if (activity.target === actor.featured) { const note = await this.apNoteService.resolveNote(activity.object); - if (note == null) throw new Error('note not found'); + if (note == null) return 'skip: note not found'; await this.notePiningService.addPinned(actor, note.id); - return; + return 'ok'; } - throw new Error(`unknown target: ${activity.target}`); + return `skip: unknown target ${activity.target}`; } @bindThis - private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise { + private async announce(actor: MiRemoteUser, activity: IAnnounce): Promise { const uri = getApId(activity); this.logger.info(`Announce: ${uri}`); - const targetUri = getApId(activity.object); + const resolver = this.apResolverService.createResolver(); - await this.announceNote(actor, activity, targetUri); + if (!activity.object) return 'skip: activity has no object property'; + + const target = await resolver.resolve(activity.object).catch(err => { + this.logger.error(`Resolution failed: ${err}`, { error: err }); + return err; + }); + + if (isPost(target)) await this.announceNote(actor, activity, target); + + return `skip: unknown object type ${getApType(target)}`; } @bindThis - private async announceNote(actor: MiRemoteUser, activity: IAnnounce, targetUri: string): Promise { + private async announceNote(actor: MiRemoteUser, activity: IAnnounce, target: IPost): Promise { const uri = getApId(activity); if (actor.isSuspended) { - return; + return 'skip: actor is suspended'; } // アナウンス先をブロックしてたら中断 const meta = await this.metaService.fetch(); - if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return; + if (this.utilityService.isBlockedHost(meta.blockedHosts, this.utilityService.extractDbHost(uri))) return 'skip: blocked host'; const unlock = await this.appLockService.getApLock(uri); @@ -282,30 +300,28 @@ export class ApInboxService { // 既に同じURIを持つものが登録されていないかチェック const exist = await this.apNoteService.fetchNote(uri); if (exist) { - return; + return 'skip: note exists'; } // Announce対象をresolve let renote; try { - renote = await this.apNoteService.resolveNote(targetUri); - if (renote == null) throw new Error('announce target is null'); + renote = await this.apNoteService.resolveNote(target); + if (renote == null) return 'skip: target note not found'; } catch (err) { // 対象が4xxならスキップ if (err instanceof StatusError) { if (!err.isRetryable) { - this.logger.warn(`Ignored announce target ${targetUri} - ${err.statusCode}`); - return; + return `skip: Ignored announce target ${target} - ${err.statusCode}`; } - this.logger.warn(`Error in announce target ${targetUri} - ${err.statusCode}`); + this.logger.warn(`Error in announce target ${target} - ${err.statusCode}`); } throw err; } if (!await this.noteEntityService.isVisibleForMe(renote, actor.id)) { - this.logger.warn('skip: invalid actor for this activity'); - return; + return 'skip: invalid actor for this activity'; } this.logger.info(`Creating the (Re)Note: ${uri}`); @@ -314,8 +330,7 @@ export class ApInboxService { const createdAt = activity.published ? new Date(activity.published) : null; if (createdAt && createdAt < this.idService.parse(renote.id).date) { - this.logger.warn('skip: malformed createdAt'); - return; + return 'skip: malformed createdAt'; } await this.noteCreateService.create(actor, { @@ -328,6 +343,8 @@ export class ApInboxService { } finally { unlock(); } + + return 'ok'; } @bindThis @@ -349,11 +366,13 @@ export class ApInboxService { } @bindThis - private async create(actor: MiRemoteUser, activity: ICreate, additionalCc?: MiLocalUser['id']): Promise { + private async create(actor: MiRemoteUser, activity: ICreate, additionalCc?: MiLocalUser['id']): Promise { const uri = getApId(activity); this.logger.info(`Create: ${uri}`); + if (!activity.object) return 'skip: activity has no object property'; + // copy audiences between activity <=> object. if (typeof activity.object === 'object') { const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); @@ -378,9 +397,9 @@ export class ApInboxService { }); if (isPost(object)) { - await this.createNote(resolver, actor, object, false, activity, additionalCc); + return await this.createNote(resolver, actor, object, false, activity, additionalCc); } else { - this.logger.warn(`Unknown type: ${getApType(object)}`); + return `skip: Unknown type ${getApType(object)}`; } } @@ -433,7 +452,7 @@ export class ApInboxService { @bindThis private async delete(actor: MiRemoteUser, activity: IDelete): Promise { if (actor.uri !== activity.actor) { - throw new Error('invalid actor'); + return 'skip: invalid actor'; } // 削除対象objectのtype @@ -595,29 +614,29 @@ export class ApInboxService { } @bindThis - private async remove(actor: MiRemoteUser, activity: IRemove): Promise { + private async remove(actor: MiRemoteUser, activity: IRemove): Promise { if (actor.uri !== activity.actor) { - throw new Error('invalid actor'); + return 'skip: invalid actor'; } if (activity.target == null) { - throw new Error('target is null'); + return 'skip: target is null'; } if (activity.target === actor.featured) { const note = await this.apNoteService.resolveNote(activity.object); - if (note == null) throw new Error('note not found'); + if (note == null) return 'skip: note not found'; await this.notePiningService.removePinned(actor, note.id); - return; + return 'ok'; } - throw new Error(`unknown target: ${activity.target}`); + return `skip: unknown target ${activity.target}`; } @bindThis private async undo(actor: MiRemoteUser, activity: IUndo): Promise { if (actor.uri !== activity.actor) { - throw new Error('invalid actor'); + return 'skip: invalid actor'; } const uri = activity.id ?? activity; diff --git a/packages/backend/src/core/activitypub/models/ApNoteService.ts b/packages/backend/src/core/activitypub/models/ApNoteService.ts index 3d72c502e..fefd53f72 100644 --- a/packages/backend/src/core/activitypub/models/ApNoteService.ts +++ b/packages/backend/src/core/activitypub/models/ApNoteService.ts @@ -84,20 +84,20 @@ export class ApNoteService { const expectHost = this.utilityService.extractDbHost(uri); if (!validPost.includes(getApType(object))) { - return new Error(`invalid Note: invalid object type ${getApType(object)}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `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)}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: id has different host. expected: ${expectHost}, actual: ${this.utilityService.extractDbHost(object.id)}`); } const actualHost = object.attributedTo && this.utilityService.extractDbHost(getOneApId(object.attributedTo)); if (object.attributedTo && actualHost !== expectHost) { - return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', `invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${actualHost}`); } if (object.published && !this.idService.isSafeT(new Date(object.published).valueOf())) { - return new Error('invalid Note: published timestamp is malformed'); + return new IdentifiableError('d450b8a9-48e4-4dab-ae36-f4db763fda7c', 'invalid Note: published timestamp is malformed'); } return null; diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 09322888d..5b6c6c8ca 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -328,3 +328,4 @@ 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 const isNote = (object: IObject): object is IPost => getApType(object) === 'Note'; diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts index 259fd06f6..0bb30d27f 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -198,7 +198,11 @@ export class InboxProcessorService implements OnApplicationShutdown { // アクティビティを処理 try { - await this.apInboxService.performActivity(authUser.user, activity, job.data.user?.id); + const result = await this.apInboxService.performActivity(authUser.user, activity, job.data.user?.id); + if (result && !result.startsWith('ok')) { + this.logger.warn(`inbox activity ignored (maybe): id=${activity.id} reason=${result}`); + return result; + } } catch (e) { if (e instanceof IdentifiableError) { if ([ @@ -206,6 +210,7 @@ export class InboxProcessorService implements OnApplicationShutdown { '689ee33f-f97c-479a-ac49-1b9f8140af99', '9f466dab-c856-48cd-9e65-ff90ff750580', '85ab9bd7-3a41-4530-959d-f07073900109', + 'd450b8a9-48e4-4dab-ae36-f4db763fda7c', ].includes(e.id)) return e.message; } throw e;