diff --git a/packages/backend/src/core/activitypub/ApDbResolverService.ts b/packages/backend/src/core/activitypub/ApDbResolverService.ts index 062af39732..2cb558dbff 100644 --- a/packages/backend/src/core/activitypub/ApDbResolverService.ts +++ b/packages/backend/src/core/activitypub/ApDbResolverService.ts @@ -59,7 +59,7 @@ export class ApDbResolverService implements OnApplicationShutdown { } @bindThis - public parseUri(value: string | IObject): UriParseResult { + public parseUri(value: string | IObject | [string | IObject]): UriParseResult { const separator = '/'; const uri = new URL(getApId(value)); @@ -78,7 +78,7 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Note => Misskey Note in DB */ @bindThis - public async getNoteFromApId(value: string | IObject): Promise { + public async getNoteFromApId(value: string | IObject | [string | IObject]): Promise { const parsed = this.parseUri(value); if (parsed.local) { @@ -98,7 +98,7 @@ export class ApDbResolverService implements OnApplicationShutdown { * AP Person => Misskey User in DB */ @bindThis - public async getUserFromApId(value: string | IObject): Promise { + public async getUserFromApId(value: string | IObject | [string | IObject]): Promise { const parsed = this.parseUri(value); if (parsed.local) { diff --git a/packages/backend/src/core/activitypub/ApInboxService.ts b/packages/backend/src/core/activitypub/ApInboxService.ts index bce67a458f..fd4b3d8d6f 100644 --- a/packages/backend/src/core/activitypub/ApInboxService.ts +++ b/packages/backend/src/core/activitypub/ApInboxService.ts @@ -41,6 +41,7 @@ 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, IPost } from './type.js'; +import { fromTuple } from '@/misc/from-tuple.js'; @Injectable() export class ApInboxService { @@ -253,7 +254,8 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object); + const object = fromTuple(activity.object); + const note = await this.apNoteService.resolveNote(object); if (note == null) return 'note not found'; await this.notePiningService.addPinned(actor, note.id); return; @@ -270,11 +272,12 @@ export class ApInboxService { const resolver = this.apResolverService.createResolver(); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); + const activityObject = fromTuple(activity.object); + if (!activityObject) return 'skip: activity has no object property'; + const targetUri = getApId(activityObject); if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; - const target = await resolver.resolve(activity.object).catch(e => { + const target = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); return e; }); @@ -370,29 +373,30 @@ export class ApInboxService { this.logger.info(`Create: ${uri}`); - if (!activity.object) return 'skip: activity has no object property'; - const targetUri = getApId(activity.object); + const activityObject = fromTuple(activity.object); + if (!activityObject) return 'skip: activity has no object property'; + const targetUri = getApId(activityObject); if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.'; // copy audiences between activity <=> object. - if (typeof activity.object === 'object') { - const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); - const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)])); + if (typeof activityObject === 'object') { + const to = unique(concat([toArray(activity.to), toArray(activityObject.to)])); + const cc = unique(concat([toArray(activity.cc), toArray(activityObject.cc)])); activity.to = to; activity.cc = cc; - activity.object.to = to; - activity.object.cc = cc; + activityObject.to = to; + activityObject.cc = cc; } // If there is no attributedTo, use Activity actor. - if (typeof activity.object === 'object' && !activity.object.attributedTo) { - activity.object.attributedTo = activity.actor; + if (typeof activityObject === 'object' && !activityObject.attributedTo) { + activityObject.attributedTo = activity.actor; } const resolver = this.apResolverService.createResolver(); - const object = await resolver.resolve(activity.object).catch(e => { + const object = await resolver.resolve(activityObject).catch(e => { this.logger.error(`Resolution failed: ${e}`); throw e; }); @@ -448,15 +452,15 @@ export class ApInboxService { // 削除対象objectのtype let formerType: string | undefined; - if (typeof activity.object === 'string') { + const activityObject = fromTuple(activity.object); + if (typeof activityObject === 'string') { // typeが不明だけど、どうせ消えてるのでremote resolveしない formerType = undefined; } else { - const object = activity.object; - if (isTombstone(object)) { - formerType = toSingle(object.formerType); + if (isTombstone(activityObject)) { + formerType = toSingle(activityObject.formerType); } else { - formerType = toSingle(object.type); + formerType = toSingle(activityObject.type); } } @@ -616,7 +620,8 @@ export class ApInboxService { } if (activity.target === actor.featured) { - const note = await this.apNoteService.resolveNote(activity.object); + const activityObject = fromTuple(activity.object); + const note = await this.apNoteService.resolveNote(activityObject); if (note == null) return 'note not found'; await this.notePiningService.removePinned(actor, note.id); return; diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts index 499a163d6c..ff6f462b4c 100644 --- a/packages/backend/src/core/activitypub/ApRendererService.ts +++ b/packages/backend/src/core/activitypub/ApRendererService.ts @@ -199,7 +199,8 @@ export class ApRendererService { type: 'Flag', actor: this.userEntityService.genLocalUserUri(user.id), content, - object, + // This MUST be an array for Pleroma compatibility: https://activitypub.software/TransFem-org/Sharkey/-/issues/641#note_7301 + object: [object], }; } diff --git a/packages/backend/src/core/activitypub/ApResolverService.ts b/packages/backend/src/core/activitypub/ApResolverService.ts index b047a6c59b..fd69c7269b 100644 --- a/packages/backend/src/core/activitypub/ApResolverService.ts +++ b/packages/backend/src/core/activitypub/ApResolverService.ts @@ -21,6 +21,7 @@ import { ApDbResolverService } from './ApDbResolverService.js'; import { ApRendererService } from './ApRendererService.js'; import { ApRequestService } from './ApRequestService.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js'; +import { fromTuple } from '@/misc/from-tuple.js'; export class Resolver { private history: Set; @@ -67,7 +68,10 @@ export class Resolver { } @bindThis - public async resolve(value: string | IObject): Promise { + public async resolve(value: string | IObject | [string | IObject]): Promise { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + if (typeof value !== 'string') { return value; } diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts index 2f58825de1..144793c214 100644 --- a/packages/backend/src/core/activitypub/type.ts +++ b/packages/backend/src/core/activitypub/type.ts @@ -3,6 +3,8 @@ * SPDX-License-Identifier: AGPL-3.0-only */ +import { fromTuple } from '@/misc/from-tuple.js'; + export type Obj = { [x: string]: any }; export type ApObject = IObject | string | (IObject | string)[]; @@ -52,10 +54,13 @@ export function getOneApId(value: ApObject): string { /** * Get ActivityStreams Object id */ -export function getApId(value: string | IObject): string { +export function getApId(value: string | IObject | [string | IObject]): string { + // eslint-disable-next-line no-param-reassign + value = fromTuple(value); + 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'); } /** @@ -84,7 +89,9 @@ export function getApHrefNullable(value: string | IObject | undefined): string | export interface IActivity extends IObject { //type: 'Activity'; actor: IObject | string; - object: IObject | string; + // ActivityPub spec allows for arrays: https://www.w3.org/TR/activitystreams-vocabulary/#properties + // Misskey can only handle one value, so we use a tuple for that case. + object: IObject | string | [IObject | string] ; target?: IObject | string; /** LD-Signature */ signature?: { diff --git a/packages/backend/src/misc/from-tuple.ts b/packages/backend/src/misc/from-tuple.ts new file mode 100644 index 0000000000..366b1e310f --- /dev/null +++ b/packages/backend/src/misc/from-tuple.ts @@ -0,0 +1,7 @@ +export function fromTuple(value: T | [T]): T { + if (Array.isArray(value)) { + return value[0]; + } + + return value; +} diff --git a/packages/backend/test/unit/misc/from-tuple.ts b/packages/backend/test/unit/misc/from-tuple.ts new file mode 100644 index 0000000000..b523cb5782 --- /dev/null +++ b/packages/backend/test/unit/misc/from-tuple.ts @@ -0,0 +1,13 @@ +import { fromTuple } from '@/misc/from-tuple.js'; + +describe(fromTuple, () => { + it('should return value when value is not an array', () => { + const value = fromTuple('abc'); + expect(value).toBe('abc'); + }); + + it('should return first element when value is an array', () => { + const value = fromTuple(['abc']); + expect(value).toBe('abc'); + }); +});