From 603891645df49b3e6902d55dfacb066e90a26054 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 15 May 2024 16:41:22 +0700 Subject: [PATCH] Used proper ActivityPub Collection for Followers/Following ref https://linear.app/tryghost/issue/MOM-126 We want to return proper ActivityPub JSONLD rather than a plain array! That was just a stop-gap to get us moving. --- .../activitypub/activitypub.service.test.ts | 54 ------------------- .../core/activitypub/activitypub.service.ts | 15 ------ .../src/core/activitypub/jsonld.service.ts | 29 ++++++++++ .../activitypub.controller.test.ts | 16 ------ .../controllers/activitypub.controller.ts | 11 ---- .../activitypub.controller.test.ts | 6 +++ .../controllers/activitypub.controller.ts | 17 ++++-- 7 files changed, 48 insertions(+), 100 deletions(-) diff --git a/ghost/ghost/src/core/activitypub/activitypub.service.test.ts b/ghost/ghost/src/core/activitypub/activitypub.service.test.ts index e7e715d4c6..9a74ab5683 100644 --- a/ghost/ghost/src/core/activitypub/activitypub.service.test.ts +++ b/ghost/ghost/src/core/activitypub/activitypub.service.test.ts @@ -58,58 +58,4 @@ describe('ActivityPubService', function () { assert(mockActorRepository.save.calledWith(actor)); }); }); - - describe('#getFollowing', function () { - it('Throws if the default actor is not found', async function () { - const mockWebFingerService: WebFingerService = { - finger: Sinon.stub().resolves({ - id: 'https://example.com/user-to-follow' - }) - } as unknown as WebFingerService; - const mockActorRepository = { - getOne: Sinon.stub().resolves(null), - save: Sinon.stub().resolves() - }; - - const service = new ActivityPubService( - mockWebFingerService, - mockActorRepository - ); - - await assert.rejects(async () => { - await service.getFollowing(); - }, /Could not find default actor/); - }); - - it('Returns a list of the default actors following', async function () { - const mockWebFingerService: WebFingerService = { - finger: Sinon.stub().resolves({ - id: 'https://example.com/user-to-follow' - }) - } as unknown as WebFingerService; - const actor = Actor.create({ - username: 'testing', - following: [{ - id: new URI('https://site.com/user'), - username: '@person@site.com' - }] - }); - const mockActorRepository = { - getOne: Sinon.stub().resolves(actor), - save: Sinon.stub().resolves() - }; - - const service = new ActivityPubService( - mockWebFingerService, - mockActorRepository - ); - - const result = await service.getFollowing(); - - assert.deepEqual(result, [{ - id: 'https://site.com/user', - username: '@person@site.com' - }]); - }); - }); }); diff --git a/ghost/ghost/src/core/activitypub/activitypub.service.ts b/ghost/ghost/src/core/activitypub/activitypub.service.ts index 3b28d37cec..37e594ab3e 100644 --- a/ghost/ghost/src/core/activitypub/activitypub.service.ts +++ b/ghost/ghost/src/core/activitypub/activitypub.service.ts @@ -26,19 +26,4 @@ export class ActivityPubService { await this.actors.save(actor); } - - async getFollowing(): Promise<{id: string, username?: string}[]> { - const actor = await this.actors.getOne('index'); - - if (!actor) { - throw new Error('Could not find default actor'); - } - - return actor.following.map((x) => { - return { - id: x.id.href, - username: x.username - }; - }); - } } diff --git a/ghost/ghost/src/core/activitypub/jsonld.service.ts b/ghost/ghost/src/core/activitypub/jsonld.service.ts index 7380cd8eee..3800390fc4 100644 --- a/ghost/ghost/src/core/activitypub/jsonld.service.ts +++ b/ghost/ghost/src/core/activitypub/jsonld.service.ts @@ -16,6 +16,35 @@ export class JSONLDService { return actor?.getJSONLD(this.url); } + async getFollowing(owner: ObjectID) { + const actor = await this.repository.getOne(owner); + if (!actor) { + return null; + } + return { + '@context': 'https://www.w3.org/ns/activitystreams', + id: actor.followingCollectionId.getValue(this.url), + summary: `Following collection for ${actor.username}`, + type: 'Collection', + totalItems: actor.following.length, + items: actor.following.map(item => ({id: item.id.getValue(this.url), username: item.username})) + }; + } + + async getFollowers(owner: ObjectID) { + const actor = await this.repository.getOne(owner); + if (!actor) { + return null; + } + return { + '@context': 'https://www.w3.org/ns/activitystreams', + id: actor.followersCollectionId.getValue(this.url), + summary: `Followers collection for ${actor.username}`, + type: 'Collection', + totalItems: actor.followers.length, + items: actor.followers.map(item => item.id.getValue(this.url)) + }; + } async getOutbox(owner: ObjectID) { const actor = await this.repository.getOne(owner); if (!actor) { diff --git a/ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts b/ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts index 05394b2975..5396706c38 100644 --- a/ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts +++ b/ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts @@ -17,20 +17,4 @@ describe('ActivityPubController', function () { assert((mockActivityPubService.follow as Sinon.SinonStub).calledWith('@egg@ghost.org')); }); }); - - describe('#getFollowing', function () { - it('Calls getFollowing on the ActivityPubService and returns the result', async function () { - const mockActivityPubService = { - follow: Sinon.stub().resolves(), - getFollowing: Sinon.stub().resolves([]) - } as unknown as ActivityPubService; - const controller = new ActivityPubController(mockActivityPubService); - - const result = await controller.getFollowing(); - - assert((mockActivityPubService.getFollowing as Sinon.SinonStub).called); - const returnValue = await (mockActivityPubService.getFollowing as Sinon.SinonStub).returnValues[0]; - assert.equal(result, returnValue); - }); - }); }); diff --git a/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts b/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts index 911a3c70e8..ec22171d16 100644 --- a/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts +++ b/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts @@ -1,6 +1,5 @@ import { Controller, - Get, Param, Post, UseGuards, @@ -26,14 +25,4 @@ export class ActivityPubController { await this.activitypub.follow(username); return {}; } - - @Roles([ - 'Owner' - ]) - @Get('following') - async getFollowing(): Promise<{id: string, username?: string;}[]> { - const followers = await this.activitypub.getFollowing(); - - return followers; - } } diff --git a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts index ae67afb403..70d1f1933d 100644 --- a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts +++ b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts @@ -99,6 +99,12 @@ describe('ActivityPubController', function () { .expect(200); }); + it('Can handle requests to get the followers', async function () { + await request(app.getHttpServer()) + .get('/activitypub/followers/deadbeefdeadbeefdeadbeef') + .expect(200); + }); + it('Can handle requests to get an article', async function () { await request(app.getHttpServer()) .get('/activitypub/article/deadbeefdeadbeefdeadbeef') diff --git a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts index 9ea89aa684..a662cdee86 100644 --- a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts +++ b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts @@ -5,14 +5,12 @@ import {JSONLDService} from '../../../core/activitypub/jsonld.service'; import {HTTPSignature} from '../../../core/activitypub/http-signature.service'; import {InboxService} from '../../../core/activitypub/inbox.service'; import {Activity} from '../../../core/activitypub/activity.entity'; -import {ActivityPubService} from '../../../core/activitypub/activitypub.service'; @Controller('activitypub') export class ActivityPubController { constructor( private readonly service: JSONLDService, - private readonly inboxService: InboxService, - private readonly activitypub: ActivityPubService + private readonly inboxService: InboxService ) {} @Header('Cache-Control', 'no-store') @@ -72,7 +70,18 @@ export class ActivityPubController { if (typeof owner !== 'string') { throw new Error('Bad Request'); } - return this.activitypub.getFollowing(); + return this.service.getFollowing(ObjectID.createFromHexString(owner)); + } + + @Header('Cache-Control', 'no-store') + @Header('Content-Type', 'application/activity+json') + @Roles(['Anon']) + @Get('followers/:owner') + async getFollowers(@Param('owner') owner: unknown) { + if (typeof owner !== 'string') { + throw new Error('Bad Request'); + } + return this.service.getFollowers(ObjectID.createFromHexString(owner)); } @Header('Cache-Control', 'no-store')