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.
This commit is contained in:
Fabien O'Carroll 2024-05-15 16:41:22 +07:00 committed by Fabien 'egg' O'Carroll
parent 27b8bad664
commit 603891645d
7 changed files with 48 additions and 100 deletions

View File

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

View File

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

View File

@ -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) {

View File

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

View File

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

View File

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

View File

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