Added service for delivering activities

ref https://linear.app/tryghost/issue/MOM-120

This will allow us to deliver Follow activities to other sites
This commit is contained in:
Fabien O'Carroll 2024-05-14 12:08:43 +07:00 committed by Fabien 'egg' O'Carroll
parent 4d24bdbccb
commit 3a56b79a8c
2 changed files with 112 additions and 0 deletions

View File

@ -0,0 +1,51 @@
import assert from 'assert';
import {Activity} from './activity.entity';
import {Actor} from './actor.entity';
import {TheWorld} from './tell-the-world.service';
import {URI} from './uri.object';
import nock from 'nock';
describe('TheWorld', function () {
describe('deliverActivity', function () {
beforeEach(function () {
nock.disableNetConnect();
});
afterEach(function () {
nock.enableNetConnect();
});
it('Can deliver the activity to the inbox of the desired recipient', async function () {
const service = new TheWorld(new URL('https://base.com'));
const actor = Actor.create({
username: 'Testing'
});
const toFollow = new URI('https://main.ghost.org/activitypub/actor/deadbeefdeadbeefdeadbeef');
const activity = new Activity({
type: 'Follow',
activity: null,
actor: actor.actorId,
object: {
id: toFollow
},
to: toFollow
});
const actorFetch = nock('https://main.ghost.org')
.get('/activitypub/actor/deadbeefdeadbeefdeadbeef')
.reply(200, {
inbox: 'https://main.ghost.org/activitypub/inbox/deadbeefdeadbeefdeadbeef'
});
const activityDelivery = nock('https://main.ghost.org')
.post('/activitypub/inbox/deadbeefdeadbeefdeadbeef')
.reply(201, {});
await service.deliverActivity(activity, actor);
assert(actorFetch.isDone(), 'Expected actor to be fetched');
assert(activityDelivery.isDone(), 'Expected activity to be delivered');
});
});
});

View File

@ -0,0 +1,61 @@
import {Inject} from '@nestjs/common';
import {Activity} from './activity.entity';
import {Actor} from './actor.entity';
export class TheWorld {
constructor(
@Inject('ActivityPubBaseURL') private readonly url: URL
) {}
async deliverActivity(activity: Activity, actor: Actor): Promise<void> {
const recipients = await this.getRecipients(activity);
for (const recipient of recipients) {
const data = await this.fetchForActor(recipient.href, actor);
if ('inbox' in data && typeof data.inbox === 'string') {
const inbox = new URL(data.inbox);
await this.sendActivity(inbox, activity, actor);
}
}
}
private async sendActivity(to: URL, activity: Activity, from: Actor) {
const request = new Request(to.href, {
method: 'POST',
headers: {
'Content-Type': 'application/ld+json'
},
body: JSON.stringify(activity.getJSONLD(this.url))
});
const signedRequest = await from.sign(request, this.url);
await fetch(signedRequest);
}
private async getRecipients(activity: Activity): Promise<URL[]>{
const json = activity.getJSONLD(this.url);
const recipients = [];
if (json.to) {
recipients.push(new URL(json.to));
}
return recipients;
}
private async fetchForActor(uri: string, actor: Actor) {
const request = new Request(uri, {
headers: {
Accept: 'application/ld+json'
}
});
const signedRequest = await actor.sign(request, this.url);
const result = await fetch(signedRequest);
const json = await result.json();
if (typeof json !== 'object' || json === null) {
throw new Error('Could not read data');
}
return json;
}
}