Skip to content

Commit

Permalink
Added service for delivering activities
Browse files Browse the repository at this point in the history
ref https://linear.app/tryghost/issue/MOM-120

This will allow us to deliver Follow activities to other sites
  • Loading branch information
allouis committed May 15, 2024
1 parent 4d24bdb commit 3a56b79
Show file tree
Hide file tree
Showing 2 changed files with 112 additions and 0 deletions.
51 changes: 51 additions & 0 deletions ghost/ghost/src/core/activitypub/tell-the-world.service.test.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
});
61 changes: 61 additions & 0 deletions ghost/ghost/src/core/activitypub/tell-the-world.service.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 3a56b79

Please sign in to comment.