merge: Restore report forwarding to Pleroma (resolves #641) (!690)

View MR for information: https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/690

Closes #641

Approved-by: dakkar <dakkar@thenautilus.net>
Approved-by: Marie <github@yuugi.dev>
This commit is contained in:
Hazelnoot 2024-10-15 21:24:57 +00:00
commit 16847ba491
7 changed files with 65 additions and 28 deletions

View file

@ -59,7 +59,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
} }
@bindThis @bindThis
public parseUri(value: string | IObject): UriParseResult { public parseUri(value: string | IObject | [string | IObject]): UriParseResult {
const separator = '/'; const separator = '/';
const uri = new URL(getApId(value)); const uri = new URL(getApId(value));
@ -78,7 +78,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
* AP Note => Misskey Note in DB * AP Note => Misskey Note in DB
*/ */
@bindThis @bindThis
public async getNoteFromApId(value: string | IObject): Promise<MiNote | null> { public async getNoteFromApId(value: string | IObject | [string | IObject]): Promise<MiNote | null> {
const parsed = this.parseUri(value); const parsed = this.parseUri(value);
if (parsed.local) { if (parsed.local) {
@ -98,7 +98,7 @@ export class ApDbResolverService implements OnApplicationShutdown {
* AP Person => Misskey User in DB * AP Person => Misskey User in DB
*/ */
@bindThis @bindThis
public async getUserFromApId(value: string | IObject): Promise<MiLocalUser | MiRemoteUser | null> { public async getUserFromApId(value: string | IObject | [string | IObject]): Promise<MiLocalUser | MiRemoteUser | null> {
const parsed = this.parseUri(value); const parsed = this.parseUri(value);
if (parsed.local) { if (parsed.local) {

View file

@ -41,6 +41,7 @@ import { ApPersonService } from './models/ApPersonService.js';
import { ApQuestionService } from './models/ApQuestionService.js'; import { ApQuestionService } from './models/ApQuestionService.js';
import type { Resolver } from './ApResolverService.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 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() @Injectable()
export class ApInboxService { export class ApInboxService {
@ -253,7 +254,8 @@ export class ApInboxService {
} }
if (activity.target === actor.featured) { 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'; if (note == null) return 'note not found';
await this.notePiningService.addPinned(actor, note.id); await this.notePiningService.addPinned(actor, note.id);
return; return;
@ -270,11 +272,12 @@ export class ApInboxService {
const resolver = this.apResolverService.createResolver(); const resolver = this.apResolverService.createResolver();
if (!activity.object) return 'skip: activity has no object property'; const activityObject = fromTuple(activity.object);
const targetUri = getApId(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.'; 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}`); this.logger.error(`Resolution failed: ${e}`);
return e; return e;
}); });
@ -370,29 +373,30 @@ export class ApInboxService {
this.logger.info(`Create: ${uri}`); this.logger.info(`Create: ${uri}`);
if (!activity.object) return 'skip: activity has no object property'; const activityObject = fromTuple(activity.object);
const targetUri = getApId(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.'; if (targetUri.startsWith('bear:')) return 'skip: bearcaps url not supported.';
// copy audiences between activity <=> object. // copy audiences between activity <=> object.
if (typeof activity.object === 'object') { if (typeof activityObject === 'object') {
const to = unique(concat([toArray(activity.to), toArray(activity.object.to)])); const to = unique(concat([toArray(activity.to), toArray(activityObject.to)]));
const cc = unique(concat([toArray(activity.cc), toArray(activity.object.cc)])); const cc = unique(concat([toArray(activity.cc), toArray(activityObject.cc)]));
activity.to = to; activity.to = to;
activity.cc = cc; activity.cc = cc;
activity.object.to = to; activityObject.to = to;
activity.object.cc = cc; activityObject.cc = cc;
} }
// If there is no attributedTo, use Activity actor. // If there is no attributedTo, use Activity actor.
if (typeof activity.object === 'object' && !activity.object.attributedTo) { if (typeof activityObject === 'object' && !activityObject.attributedTo) {
activity.object.attributedTo = activity.actor; activityObject.attributedTo = activity.actor;
} }
const resolver = this.apResolverService.createResolver(); 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}`); this.logger.error(`Resolution failed: ${e}`);
throw e; throw e;
}); });
@ -448,15 +452,15 @@ export class ApInboxService {
// 削除対象objectのtype // 削除対象objectのtype
let formerType: string | undefined; let formerType: string | undefined;
if (typeof activity.object === 'string') { const activityObject = fromTuple(activity.object);
if (typeof activityObject === 'string') {
// typeが不明だけど、どうせ消えてるのでremote resolveしない // typeが不明だけど、どうせ消えてるのでremote resolveしない
formerType = undefined; formerType = undefined;
} else { } else {
const object = activity.object; if (isTombstone(activityObject)) {
if (isTombstone(object)) { formerType = toSingle(activityObject.formerType);
formerType = toSingle(object.formerType);
} else { } else {
formerType = toSingle(object.type); formerType = toSingle(activityObject.type);
} }
} }
@ -616,7 +620,8 @@ export class ApInboxService {
} }
if (activity.target === actor.featured) { 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'; if (note == null) return 'note not found';
await this.notePiningService.removePinned(actor, note.id); await this.notePiningService.removePinned(actor, note.id);
return; return;

View file

@ -199,7 +199,8 @@ export class ApRendererService {
type: 'Flag', type: 'Flag',
actor: this.userEntityService.genLocalUserUri(user.id), actor: this.userEntityService.genLocalUserUri(user.id),
content, content,
object, // This MUST be an array for Pleroma compatibility: https://activitypub.software/TransFem-org/Sharkey/-/issues/641#note_7301
object: [object],
}; };
} }

View file

@ -21,6 +21,7 @@ import { ApDbResolverService } from './ApDbResolverService.js';
import { ApRendererService } from './ApRendererService.js'; import { ApRendererService } from './ApRendererService.js';
import { ApRequestService } from './ApRequestService.js'; import { ApRequestService } from './ApRequestService.js';
import type { IObject, ICollection, IOrderedCollection } from './type.js'; import type { IObject, ICollection, IOrderedCollection } from './type.js';
import { fromTuple } from '@/misc/from-tuple.js';
export class Resolver { export class Resolver {
private history: Set<string>; private history: Set<string>;
@ -67,7 +68,10 @@ export class Resolver {
} }
@bindThis @bindThis
public async resolve(value: string | IObject): Promise<IObject> { public async resolve(value: string | IObject | [string | IObject]): Promise<IObject> {
// eslint-disable-next-line no-param-reassign
value = fromTuple(value);
if (typeof value !== 'string') { if (typeof value !== 'string') {
return value; return value;
} }

View file

@ -3,6 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { fromTuple } from '@/misc/from-tuple.js';
export type Obj = { [x: string]: any }; export type Obj = { [x: string]: any };
export type ApObject = IObject | string | (IObject | string)[]; export type ApObject = IObject | string | (IObject | string)[];
@ -52,10 +54,13 @@ export function getOneApId(value: ApObject): string {
/** /**
* Get ActivityStreams Object id * 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 === 'string') return value;
if (typeof value.id === 'string') return value.id; 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 { export interface IActivity extends IObject {
//type: 'Activity'; //type: 'Activity';
actor: IObject | string; 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; target?: IObject | string;
/** LD-Signature */ /** LD-Signature */
signature?: { signature?: {

View file

@ -0,0 +1,7 @@
export function fromTuple<T>(value: T | [T]): T {
if (Array.isArray(value)) {
return value[0];
}
return value;
}

View file

@ -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');
});
});