From 5acdafc0e8549a2417fadb4926babab993d963bf Mon Sep 17 00:00:00 2001 From: Fabien 'egg' O'Carroll Date: Wed, 19 Jun 2024 15:36:36 +0700 Subject: [PATCH] Removed ActivityPub backend from Ghost (#20410) We are no longer going to run any ActivityPub logic inside of Ghost, instead we're moving to a separate service, so all of this code is now redundant. --- ghost/core/core/boot.js | 5 - ghost/core/core/frontend/web/site.js | 33 --- .../server/web/api/endpoints/admin/app.js | 2 +- .../core/activitypub/activity.entity.test.ts | 89 ------ .../src/core/activitypub/activity.entity.ts | 127 --------- .../src/core/activitypub/activity.event.ts | 14 - .../core/activitypub/activity.repository.ts | 6 - .../core/activitypub/activity.service.test.ts | 145 ---------- .../src/core/activitypub/activity.service.ts | 36 --- .../activitypub/activitypub.service.test.ts | 61 ---- .../core/activitypub/activitypub.service.ts | 29 -- .../src/core/activitypub/actor.entity.test.ts | 229 --------------- .../src/core/activitypub/actor.entity.ts | 262 ------------------ .../src/core/activitypub/actor.repository.ts | 8 - .../src/core/activitypub/article.object.ts | 63 ----- .../http-signature.service.test.ts | 100 ------- .../activitypub/http-signature.service.ts | 209 -------------- .../core/activitypub/inbox.service.test.ts | 83 ------ .../src/core/activitypub/inbox.service.ts | 25 -- .../core/activitypub/jsonld.service.test.ts | 122 -------- .../src/core/activitypub/jsonld.service.ts | 91 ------ .../src/core/activitypub/post.repository.ts | 19 -- .../tell-the-world.service.test.ts | 116 -------- .../activitypub/tell-the-world.service.ts | 86 ------ ghost/ghost/src/core/activitypub/types.ts | 79 ------ .../ghost/src/core/activitypub/uri.object.ts | 12 - .../activitypub/webfinger.service.test.ts | 79 ------ .../src/core/activitypub/webfinger.service.ts | 81 ------ .../activity.repository.in-memory.ts | 18 -- .../in-memory/actor.repository.in-memory.ts | 63 ----- .../ghost/src/db/knex/post.repository.knex.ts | 58 ---- .../activitypub.controller.test.ts | 20 -- .../controllers/activitypub.controller.ts | 28 -- .../activitypub.controller.test.ts | 142 ---------- .../controllers/activitypub.controller.ts | 108 -------- .../controllers/webfinger.controller.test.ts | 38 --- .../controllers/webfinger.controller.ts | 18 -- .../src/listeners/activity.listener.test.ts | 51 ---- .../ghost/src/listeners/activity.listener.ts | 15 - .../src/nestjs/modules/activitypub.module.ts | 47 ---- .../src/nestjs/modules/admin-api.module.ts | 12 +- ghost/ghost/src/nestjs/modules/app.module.ts | 7 +- ghost/posts-service/lib/PostsService.js | 9 - 43 files changed, 3 insertions(+), 2842 deletions(-) delete mode 100644 ghost/ghost/src/core/activitypub/activity.entity.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/activity.entity.ts delete mode 100644 ghost/ghost/src/core/activitypub/activity.event.ts delete mode 100644 ghost/ghost/src/core/activitypub/activity.repository.ts delete mode 100644 ghost/ghost/src/core/activitypub/activity.service.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/activity.service.ts delete mode 100644 ghost/ghost/src/core/activitypub/activitypub.service.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/activitypub.service.ts delete mode 100644 ghost/ghost/src/core/activitypub/actor.entity.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/actor.entity.ts delete mode 100644 ghost/ghost/src/core/activitypub/actor.repository.ts delete mode 100644 ghost/ghost/src/core/activitypub/article.object.ts delete mode 100644 ghost/ghost/src/core/activitypub/http-signature.service.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/http-signature.service.ts delete mode 100644 ghost/ghost/src/core/activitypub/inbox.service.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/inbox.service.ts delete mode 100644 ghost/ghost/src/core/activitypub/jsonld.service.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/jsonld.service.ts delete mode 100644 ghost/ghost/src/core/activitypub/post.repository.ts delete mode 100644 ghost/ghost/src/core/activitypub/tell-the-world.service.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/tell-the-world.service.ts delete mode 100644 ghost/ghost/src/core/activitypub/types.ts delete mode 100644 ghost/ghost/src/core/activitypub/uri.object.ts delete mode 100644 ghost/ghost/src/core/activitypub/webfinger.service.test.ts delete mode 100644 ghost/ghost/src/core/activitypub/webfinger.service.ts delete mode 100644 ghost/ghost/src/db/in-memory/activity.repository.in-memory.ts delete mode 100644 ghost/ghost/src/db/in-memory/actor.repository.in-memory.ts delete mode 100644 ghost/ghost/src/db/knex/post.repository.knex.ts delete mode 100644 ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts delete mode 100644 ghost/ghost/src/http/admin/controllers/activitypub.controller.ts delete mode 100644 ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts delete mode 100644 ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts delete mode 100644 ghost/ghost/src/http/frontend/controllers/webfinger.controller.test.ts delete mode 100644 ghost/ghost/src/http/frontend/controllers/webfinger.controller.ts delete mode 100644 ghost/ghost/src/listeners/activity.listener.test.ts delete mode 100644 ghost/ghost/src/listeners/activity.listener.ts delete mode 100644 ghost/ghost/src/nestjs/modules/activitypub.module.ts diff --git a/ghost/core/core/boot.js b/ghost/core/core/boot.js index 575b31cbc2..280983f018 100644 --- a/ghost/core/core/boot.js +++ b/ghost/core/core/boot.js @@ -388,8 +388,6 @@ async function initNestDependencies() { debug('Begin: initNestDependencies'); const GhostNestApp = require('@tryghost/ghost'); const providers = []; - const urlUtils = require('./shared/url-utils'); - const activityPubBaseUrl = new URL('activitypub', urlUtils.urlFor('home', true)); providers.push({ provide: 'logger', useValue: require('@tryghost/logging') @@ -402,9 +400,6 @@ async function initNestDependencies() { }, { provide: 'DomainEvents', useValue: require('@tryghost/domain-events') - }, { - provide: 'ActivityPubBaseURL', - useValue: activityPubBaseUrl }, { provide: 'SettingsCache', useValue: require('./shared/settings-cache') diff --git a/ghost/core/core/frontend/web/site.js b/ghost/core/core/frontend/web/site.js index b5ef8ff43b..8ffafeb3aa 100644 --- a/ghost/core/core/frontend/web/site.js +++ b/ghost/core/core/frontend/web/site.js @@ -3,7 +3,6 @@ const path = require('path'); const express = require('../../shared/express'); const DomainEvents = require('@tryghost/domain-events'); const {MemberPageViewEvent} = require('@tryghost/member-events'); -const GhostNestApp = require('@tryghost/ghost'); // App requires const config = require('../../shared/config'); @@ -21,8 +20,6 @@ const siteRoutes = require('./routes'); const shared = require('../../server/web/shared'); const errorHandler = require('@tryghost/mw-error-handler'); const mw = require('./middleware'); -const labs = require('../../shared/labs'); -const bodyParser = require('body-parser'); const STATIC_IMAGE_URL_PREFIX = `/${urlUtils.STATIC_IMAGE_URL_PREFIX}`; const STATIC_MEDIA_URL_PREFIX = `/${constants.STATIC_MEDIA_URL_PREFIX}`; @@ -51,36 +48,6 @@ module.exports = function setupSiteApp(routerConfig) { // enable CORS headers (allows admin client to hit front-end when configured on separate URLs) siteApp.use(mw.cors); - const jsonParser = bodyParser.json({ - type: ['application/activity+json', 'application/ld+json', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'], - // TODO: The @RawBody decorator in nest isn't working without this atm... - verify: function (req, res, buf) { - req.rawBody = buf; - } - }); - siteApp.use(async function nestBodyParser(req, res, next) { - if (labs.isSet('NestPlayground') || labs.isSet('ActivityPub')) { - jsonParser(req, res, next); - return; - } - return next(); - }); - - siteApp.use(async function nestApp(req, res, next) { - if (labs.isSet('NestPlayground') || labs.isSet('ActivityPub')) { - const originalExpressApp = req.app; - const app = await GhostNestApp.getApp(); - - const instance = app.getHttpAdapter().getInstance(); - instance(req, res, function (err) { - req.app = originalExpressApp; - next(err); - }); - return; - } - return next(); - }); - siteApp.use(offersService.middleware); siteApp.use(linkRedirects.service.handleRequest); diff --git a/ghost/core/core/server/web/api/endpoints/admin/app.js b/ghost/core/core/server/web/api/endpoints/admin/app.js index f8e340528a..5c31f6e44d 100644 --- a/ghost/core/core/server/web/api/endpoints/admin/app.js +++ b/ghost/core/core/server/web/api/endpoints/admin/app.js @@ -39,7 +39,7 @@ module.exports = function setupApiApp() { apiApp.use(routes()); apiApp.use(async function nestApp(req, res, next) { - if (labs.isSet('NestPlayground') || labs.isSet('ActivityPub')) { + if (labs.isSet('NestPlayground')) { const originalExpressApp = req.app; const app = await GhostNestApp.getApp(); diff --git a/ghost/ghost/src/core/activitypub/activity.entity.test.ts b/ghost/ghost/src/core/activitypub/activity.entity.test.ts deleted file mode 100644 index 3e4f17a55f..0000000000 --- a/ghost/ghost/src/core/activitypub/activity.entity.test.ts +++ /dev/null @@ -1,89 +0,0 @@ -import assert from 'assert'; -import {Activity} from './activity.entity'; -import {URI} from './uri.object'; -import {Article} from './article.object'; -import ObjectID from 'bson-objectid'; -import {Actor} from './actor.entity'; - -describe('Activity', function () { - describe('fromJSONLD', function () { - it('Can construct an entity from JSONLD with various id types', async function () { - const input = { - id: new URI('https://site.com/activity'), - type: 'Follow', - actor: { - id: 'https://site.com/actor' - }, - object: 'https://site.com/object' - }; - - const activity = Activity.fromJSONLD(input); - assert(activity); - }); - - it('Will throw for unknown types', async function () { - const input = { - id: new URI('https://site.com/activity'), - type: 'Unknown', - actor: { - id: 'https://site.com/actor' - }, - object: 'https://site.com/object' - }; - - assert.throws(() => { - Activity.fromJSONLD(input); - }); - }); - - it('Will throw for missing actor,object or type', async function () { - const input = { - id: new URI('https://site.com/activity'), - type: 'Unknown', - actor: { - id: 'https://site.com/actor' - }, - object: 'https://site.com/object' - }; - - for (const prop of ['actor', 'object', 'type']) { - const modifiedInput = Object.create(input); - delete modifiedInput[prop]; - assert.throws(() => { - Activity.fromJSONLD(modifiedInput); - }); - } - }); - - it('Can correctly reconstruct', function () { - const actor = Actor.create({username: 'testing'}); - const article = Article.fromPost({ - id: new ObjectID(), - title: 'My Title', - slug: 'my-title', - html: '

big boi contents

', - excerpt: 'lil contents', - authors: ['Jeremy Paxman'], - url: new URI('blah'), - publishedAt: new Date(), - featuredImage: null, - visibility: 'public' - }); - const activity = new Activity({ - type: 'Create', - activity: null, - actor: actor, - object: article, - to: new URI('bloo') - }); - - const baseUrl = new URL('https://ghost.org'); - - const input = activity.getJSONLD(baseUrl); - - const created = Activity.fromJSONLD(input); - - assert.deepEqual(created.getJSONLD(baseUrl), input); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/activity.entity.ts b/ghost/ghost/src/core/activitypub/activity.entity.ts deleted file mode 100644 index ebdcf7edaa..0000000000 --- a/ghost/ghost/src/core/activitypub/activity.entity.ts +++ /dev/null @@ -1,127 +0,0 @@ -import {Entity} from '../../common/entity.base'; -import {Actor} from './actor.entity'; -import {Article} from './article.object'; -import {ActivityPub} from './types'; -import {URI} from './uri.object'; - -type ActivityData = { - activity: URI | null; - type: ActivityPub.ActivityType; - actor: { - id: URI; - type: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any; - } | Actor; - object: { - id: URI; - type: string; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any; - } | Article; - to: URI | null; -} - -function getURI(input: unknown) { - if (input instanceof URI) { - return input; - } - if (typeof input === 'string') { - return new URI(input); - } - if (typeof input !== 'object' || input === null) { - throw new Error(`Could not create URI from ${JSON.stringify(input)}`); - } - if ('id' in input && typeof input.id === 'string') { - return new URI(input.id); - } - throw new Error(`Could not create URI from ${JSON.stringify(input)}`); -} - -function checkKeys(keys: T[], obj: object): Record { - for (const key of keys) { - if (!(key in obj)) { - throw new Error(`Missing key ${key}`); - } - } - return obj as Record; -} - -export class Activity extends Entity { - get type() { - return this.attr.type; - } - - getObject(url: URL) { - if (this.attr.object instanceof Article) { - return this.attr.object.getJSONLD(url); - } - return this.attr.object; - } - - getActor(url: URL) { - if (this.attr.actor instanceof Actor) { - return this.attr.actor.getJSONLD(url); - } - return this.attr.actor; - } - - get actorId() { - if (this.attr.actor instanceof Actor) { - return this.attr.actor.actorId; - } - return this.attr.actor.id; - } - - get objectId() { - if (this.attr.object instanceof Article) { - return this.attr.object.objectId; - } - return this.attr.object.id; - } - - get activityId() { - return this.attr.activity; - } - - getJSONLD(url: URL): ActivityPub.Activity { - const object = this.getObject(url); - const actor = this.getActor(url); - return { - '@context': 'https://www.w3.org/ns/activitystreams', - id: this.activityId?.getValue(url) || null, - type: this.attr.type, - actor: { - ...actor, - id: this.actorId.getValue(url) - }, - object: { - ...object, - id: this.objectId.getValue(url) - }, - to: this.attr.to?.getValue(url) || null - }; - } - - static fromJSONLD(json: object) { - const parsed = checkKeys(['type', 'actor', 'object'], json); - if (typeof parsed.type !== 'string' || !['Create', 'Follow', 'Accept'].includes(parsed.type)) { - throw new Error(`Unknown type ${parsed.type}`); - } - return new Activity({ - activity: 'id' in json && typeof json.id === 'string' ? getURI(json.id) : null, - type: parsed.type as ActivityPub.ActivityType, - actor: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(parsed.actor as any), - id: getURI(parsed.actor) - }, - object: { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(parsed.object as any), - id: getURI(parsed.object) - }, - to: 'to' in json ? getURI(json.to) : null - }); - } -} diff --git a/ghost/ghost/src/core/activitypub/activity.event.ts b/ghost/ghost/src/core/activitypub/activity.event.ts deleted file mode 100644 index 47a370efaf..0000000000 --- a/ghost/ghost/src/core/activitypub/activity.event.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {BaseEvent} from '../../common/event.base'; -import {Activity} from './activity.entity'; -import {Actor} from './actor.entity'; - -type ActivityEventData = { - activity: Activity, - actor: Actor -} - -export class ActivityEvent extends BaseEvent { - static create(activity: Activity, actor: Actor) { - return new ActivityEvent({activity, actor}); - } -} diff --git a/ghost/ghost/src/core/activitypub/activity.repository.ts b/ghost/ghost/src/core/activitypub/activity.repository.ts deleted file mode 100644 index cc83545f80..0000000000 --- a/ghost/ghost/src/core/activitypub/activity.repository.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Activity} from './activity.entity'; - -export interface ActivityRepository { - getOne(id: URL): Promise - save(activity: Activity): Promise -} diff --git a/ghost/ghost/src/core/activitypub/activity.service.test.ts b/ghost/ghost/src/core/activitypub/activity.service.test.ts deleted file mode 100644 index 2e4d4a5213..0000000000 --- a/ghost/ghost/src/core/activitypub/activity.service.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import ObjectID from 'bson-objectid'; -import {ActivityService} from './activity.service'; -import {Actor} from './actor.entity'; -import assert from 'assert'; -import {URI} from './uri.object'; - -describe('ActivityService', function () { - describe('#createArticleForPost', function () { - it('Adds a Create activity for an Article Object to the default actors Outbox', async function () { - const actor = Actor.create({username: 'testing'}); - const mockActorRepository = { - async getOne() { - return actor; - }, - async save() {} - }; - const mockPostRepository = { - async getOne(id: ObjectID) { - return { - id: id, - title: 'Testing', - slug: 'testing', - html: '

Testing stuff..

', - visibility: 'public', - authors: ['Mr Bean'], - publishedAt: new Date(), - featuredImage: null, - excerpt: 'Small text', - url: new URI('blah') - }; - } - }; - const service = new ActivityService( - mockActorRepository, - mockPostRepository - ); - - const postId = new ObjectID(); - - await service.createArticleForPost(postId); - - const found = actor.outbox.find(activity => activity.type === 'Create'); - - assert.ok(found); - }); - - it('Does not add a Create activity for non public posts', async function () { - const actor = Actor.create({username: 'testing'}); - const mockActorRepository = { - async getOne() { - return actor; - }, - async save() {} - }; - const mockPostRepository = { - async getOne(id: ObjectID) { - return { - id: id, - title: 'Testing', - slug: 'testing', - html: '

Testing stuff..

', - visibility: 'private', - authors: ['Mr Bean'], - publishedAt: new Date(), - featuredImage: null, - excerpt: 'Small text', - url: new URI('blah') - }; - } - }; - const service = new ActivityService( - mockActorRepository, - mockPostRepository - ); - - const postId = new ObjectID(); - - await service.createArticleForPost(postId); - - const found = actor.outbox.find(activity => activity.type === 'Create'); - - assert.ok(!found); - }); - - it('Throws when post is not found', async function () { - const actor = Actor.create({username: 'testing'}); - const mockActorRepository = { - async getOne() { - return actor; - }, - async save() {} - }; - const mockPostRepository = { - async getOne() { - return null; - } - }; - const service = new ActivityService( - mockActorRepository, - mockPostRepository - ); - - const postId = new ObjectID(); - - await assert.rejects(async () => { - await service.createArticleForPost(postId); - }, /Post not found/); - }); - - it('Throws when actor is not found', async function () { - const mockActorRepository = { - async getOne() { - return null; - }, - async save() {} - }; - const mockPostRepository = { - async getOne(id: ObjectID) { - return { - id: id, - title: 'Testing', - slug: 'testing', - html: '

Testing stuff..

', - visibility: 'private', - authors: ['Mr Bean'], - publishedAt: new Date(), - featuredImage: null, - excerpt: 'Small text', - url: new URI('blah') - }; - } - }; - const service = new ActivityService( - mockActorRepository, - mockPostRepository - ); - - const postId = new ObjectID(); - - await assert.rejects(async () => { - await service.createArticleForPost(postId); - }, /Actor not found/); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/activity.service.ts b/ghost/ghost/src/core/activitypub/activity.service.ts deleted file mode 100644 index 098ff71679..0000000000 --- a/ghost/ghost/src/core/activitypub/activity.service.ts +++ /dev/null @@ -1,36 +0,0 @@ -import ObjectID from 'bson-objectid'; -import {ActorRepository} from './actor.repository'; -import {Article} from './article.object'; -import {PostRepository} from './post.repository'; -import {Inject} from '@nestjs/common'; - -export class ActivityService { - constructor( - @Inject('ActorRepository') private readonly actorRepository: ActorRepository, - @Inject('PostRepository') private readonly postRepository: PostRepository - ) {} - - async createArticleForPost(postId: ObjectID) { - const actor = await this.actorRepository.getOne('index'); - - if (!actor) { - throw new Error('Actor not found'); - } - - const post = await this.postRepository.getOne(postId); - - if (!post) { - throw new Error('Post not found'); - } - - if (post.visibility !== 'public') { - return; - } - - const article = Article.fromPost(post); - - actor.createArticle(article); - - await this.actorRepository.save(actor); - } -} diff --git a/ghost/ghost/src/core/activitypub/activitypub.service.test.ts b/ghost/ghost/src/core/activitypub/activitypub.service.test.ts deleted file mode 100644 index 9a74ab5683..0000000000 --- a/ghost/ghost/src/core/activitypub/activitypub.service.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import assert from 'assert'; -import {ActivityPubService} from './activitypub.service'; -import {ActorRepository} from './actor.repository'; -import {WebFingerService} from './webfinger.service'; -import {Actor} from './actor.entity'; -import Sinon from 'sinon'; -import {URI} from './uri.object'; - -describe('ActivityPubService', function () { - describe('#follow', function () { - it('Throws if it cannot find the default actor', async function () { - const mockWebFingerService: WebFingerService = { - async finger() { - return {}; - } - } as unknown as WebFingerService; - const mockActorRepository = { - async getOne() { - return null; - } - } as unknown as ActorRepository; - - const service = new ActivityPubService( - mockWebFingerService, - mockActorRepository - ); - - await assert.rejects(async () => { - await service.follow('@egg@ghost.org'); - }, /Could not find default actor/); - }); - - it('Follows the actor and saves', 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'}); - const mockActorRepository = { - getOne: Sinon.stub().resolves(actor), - save: Sinon.stub().resolves() - }; - - const service = new ActivityPubService( - mockWebFingerService, - mockActorRepository - ); - - const followStub = Sinon.stub(actor, 'follow'); - await service.follow('@egg@ghost.org'); - - assert(followStub.calledWithMatch({ - id: new URI('https://example.com/user-to-follow'), - username: '@egg@ghost.org' - })); - - assert(mockActorRepository.save.calledWith(actor)); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/activitypub.service.ts b/ghost/ghost/src/core/activitypub/activitypub.service.ts deleted file mode 100644 index 37e594ab3e..0000000000 --- a/ghost/ghost/src/core/activitypub/activitypub.service.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {Inject} from '@nestjs/common'; -import {ActorRepository} from './actor.repository'; -import {WebFingerService} from './webfinger.service'; -import {URI} from './uri.object'; - -export class ActivityPubService { - constructor( - private readonly webfinger: WebFingerService, - @Inject('ActorRepository') private readonly actors: ActorRepository - ) {} - - async follow(username: string): Promise { - const json = await this.webfinger.finger(username); - const actor = await this.actors.getOne('index'); - - if (!actor) { - throw new Error('Could not find default actor'); - } - - const actorToFollow = { - id: new URI(json.id), - username - }; - - actor.follow(actorToFollow); - - await this.actors.save(actor); - } -} diff --git a/ghost/ghost/src/core/activitypub/actor.entity.test.ts b/ghost/ghost/src/core/activitypub/actor.entity.test.ts deleted file mode 100644 index 2b26a10146..0000000000 --- a/ghost/ghost/src/core/activitypub/actor.entity.test.ts +++ /dev/null @@ -1,229 +0,0 @@ -import crypto from 'node:crypto'; -import {Actor} from './actor.entity'; -import {HTTPSignature} from './http-signature.service'; -import assert from 'node:assert'; -import {URI} from './uri.object'; -import {ActivityEvent} from './activity.event'; -import {Activity} from './activity.entity'; -import {Article} from './article.object'; -import ObjectID from 'bson-objectid'; - -describe('Actor', function () { - describe('getters', function () { - describe('displayName', function () { - it('Uses displayName, but falls back to username', function () { - const hasDisplayName = Actor.create({ - username: 'username', - displayName: 'displayName' - }); - - const doesnaeHaveDisplayName = Actor.create({ - username: 'username' - }); - - assert.equal(hasDisplayName.displayName, 'displayName'); - assert.equal(doesnaeHaveDisplayName.displayName, 'username'); - }); - }); - - describe('actorId', function () { - it('Correctly returns the actor url', function () { - const actor = Actor.create({username: 'testing'}); - const idString = actor.id.toHexString(); - const actorId = actor.actorId; - - const baseUrl = new URL('https://domain.tld/base'); - - assert.equal( - actorId.getValue(baseUrl), - `https://domain.tld/base/actor/${idString}` - ); - }); - }); - }); - - describe('#createArticle', function () { - it('Adds an activity to the outbox', function () { - const actor = Actor.create({username: 'username'}); - - const article = Article.fromPost({ - id: new ObjectID(), - title: 'Post Title', - slug: 'post-slug', - html: '

Hello world

', - visibility: 'public', - url: new URI(''), - authors: ['Mr Burns'], - featuredImage: null, - publishedAt: null, - excerpt: 'Hey' - }); - - actor.createArticle(article); - - const found = actor.outbox.find((value) => { - return value.type === 'Create'; - }); - - assert.ok(found); - }); - }); - - describe('#sign', function () { - it('returns a request with a valid Signature header', async function () { - const baseUrl = new URL('https://example.com/ap'); - const actor = Actor.create({ - username: 'Testing' - }); - - const url = new URL('https://some-server.com/users/username/inbox'); - const date = new Date(); - const request = new Request(url, { - headers: { - Host: url.host, - Date: date.toISOString(), - Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' - } - }); - - const signedRequest = await actor.sign(request, baseUrl); - - const publicKey = actor.getJSONLD(baseUrl).publicKey; - - class MockHTTPSignature extends HTTPSignature { - protected static async getPublicKey() { - return crypto.createPublicKey(publicKey.publicKeyPem); - } - } - - const signedRequestURL = new URL(signedRequest.url); - - const actual = await MockHTTPSignature.validate( - signedRequest.method, - signedRequestURL.pathname, - signedRequest.headers - ); - - const expected = true; - - assert.equal(actual, expected, 'The signature should have been valid'); - }); - }); - - describe('#follow', function () { - it('Creates a Follow activity', async function () { - const actor = Actor.create({username: 'TestingFollow'}); - - const actorToFollow = { - id: new URI('https://activitypub.server/actor'), - username: '@user@domain' - }; - - actor.follow(actorToFollow); - - Actor.getActivitiesToSave(actor, function (activities) { - const followActivity = activities.find(activity => activity.type === 'Follow'); - - assert.equal(followActivity?.objectId.href, actorToFollow.id.href); - }); - - Actor.getEventsToDispatch(actor, function (events) { - const followActivityEvent: ActivityEvent = (events.find(event => (event as ActivityEvent).data.activity?.type === 'Follow') as ActivityEvent); - - assert.equal(followActivityEvent.data.activity.objectId.href, actorToFollow.id.href); - }); - }); - }); - - describe('#postToInbox', function () { - it('Handles Follow activities', async function () { - const actor = Actor.create({username: 'TestingPostToInbox'}); - - const newFollower = new URI('https://activitypub.server/actor'); - - const followActivity = new Activity({ - activity: new URI(`https://activitypub.server/activity`), - type: 'Follow', - actor: { - id: newFollower, - type: 'Person' - }, - object: { - id: actor.actorId, - type: 'Person' - }, - to: actor.actorId - }); - - await actor.postToInbox(followActivity); - - assert(actor.followers.find(follower => follower.id.href === newFollower.href)); - }); - - it('Throws if the Follow activity is anonymous', async function () { - const actor = Actor.create({username: 'TestingPostToInbox'}); - - const newFollower = new URI('https://activitypub.server/actor'); - - const followActivity = new Activity({ - activity: null, - type: 'Follow', - actor: { - id: newFollower, - type: 'Person' - }, - object: { - id: actor.actorId, - type: 'Person' - }, - to: actor.actorId - }); - - let error: unknown = null; - try { - await actor.postToInbox(followActivity); - } catch (err) { - error = err; - } - - assert.ok(error); - }); - - it('Handles Accept activities', async function () { - const actor = Actor.create({username: 'TestingPostToInbox'}); - - const newFollower = new URI('https://activitypub.server/actor'); - - const activity = new Activity({ - activity: null, - type: 'Accept', - actor: { - id: newFollower, - type: 'Person' - }, - object: { - id: newFollower, - type: 'Person' - }, - to: actor.actorId - }); - - await actor.postToInbox(activity); - - assert(actor.following.find(follower => follower.id.href === newFollower.href)); - assert(actor.following.find(follower => follower.username === '@index@activitypub.server')); - }); - }); - - describe('#toJSONLD', function () { - it('Returns valid JSONLD', async function () { - const actor = Actor.create({username: 'TestingJSONLD'}); - - const baseUrl = new URL('https://example.com'); - - const jsonld = actor.getJSONLD(baseUrl); - - assert.ok(jsonld); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/actor.entity.ts b/ghost/ghost/src/core/activitypub/actor.entity.ts deleted file mode 100644 index f339e62c20..0000000000 --- a/ghost/ghost/src/core/activitypub/actor.entity.ts +++ /dev/null @@ -1,262 +0,0 @@ -import crypto from 'crypto'; -import ObjectID from 'bson-objectid'; -import {Entity} from '../../common/entity.base'; -import {ActivityPub} from './types'; -import {Activity} from './activity.entity'; -import {Article} from './article.object'; -import {ActivityEvent} from './activity.event'; -import {HTTPSignature} from './http-signature.service'; -import {URI} from './uri.object'; - -type ActorData = { - username: string; - displayName?: string; - publicKey: string; - privateKey: string; - outbox: Activity[]; - inbox: Activity[]; - following: {id: URI, username?: string;}[]; - followers: {id: URI;}[]; -}; - -type CreateActorData = ActorData & { - id? : ObjectID -}; - -export class Actor extends Entity { - private getURI(input: string): URI { - const id = this.id.toHexString(); - return new URI(input.replace(':id', id)); - } - - get actorId() { - return this.getURI('actor/:id'); - } - - get publicKeyId() { - return this.getURI('actor/:id#main-key'); - } - - get inboxId() { - return this.getURI('inbox/:id'); - } - - get outboxId() { - return this.getURI('outbox/:id'); - } - - get followingCollectionId() { - return this.getURI('following/:id'); - } - - get followersCollectionId() { - return this.getURI('followers/:id'); - } - - get featuredCollectionId() { - return this.getURI('featured/:id'); - } - - get username() { - return this.attr.username; - } - - get displayName() { - if (this.attr.displayName) { - return this.attr.displayName; - } - return this.username; - } - - get inbox() { - return this.attr.inbox; - } - - get outbox() { - return this.attr.outbox; - } - - get following() { - return this.attr.following; - } - - get followers() { - return this.attr.followers; - } - - async sign(request: Request, baseUrl: URL): Promise { - const keyId = new URL(this.getJSONLD(baseUrl).publicKey.id); - const key = crypto.createPrivateKey(this.attr.privateKey); - return HTTPSignature.sign(request, keyId, key); - } - - public readonly publicAccount = true; - - async postToInbox(activity: Activity) { - this.attr.inbox.unshift(activity); - if (activity.type === 'Follow') { - if (this.publicAccount) { - await this.acceptFollow(activity); - return; - } - } - if (activity.type === 'Accept') { - // TODO: Check that the Accept is for a real Follow activity - this.attr.following.push({ - id: activity.actorId, - username: `@index@${activity.actorId.hostname}` - }); - } - } - - async follow(actor: {id: URI, username: string;}) { - const activity = new Activity({ - activity: new URI(`activity/${(new ObjectID).toHexString()}`), - type: 'Follow', - actor: this, - object: { - ...actor, - type: 'Person' - }, - to: actor.id - }); - this.doActivity(activity); - } - - async acceptFollow(activity: Activity) { - if (!activity.activityId) { - throw new Error('Cannot accept Follow of anonymous activity'); - } - this.attr.followers.push({id: activity.actorId}); - const accept = new Activity({ - activity: new URI(`activity/${(new ObjectID).toHexString()}`), - type: 'Accept', - to: activity.actorId, - actor: this, - object: { - id: activity.activityId, - type: 'Follow' - } - }); - this.doActivity(accept); - } - - private doActivity(activity: Activity) { - this.attr.outbox.push(activity); - this.activities.push(activity); - this.addEvent(ActivityEvent.create(activity, this)); - } - - private activities: Activity[] = []; - - static getActivitiesToSave(actor: Actor, fn: (activities: Activity[]) => void) { - const activities = actor.activities; - actor.activities = []; - fn(activities); - } - - createArticle(article: Article) { - const activity = new Activity({ - activity: new URI(`activity/${new ObjectID().toHexString()}`), - to: this.followersCollectionId, - type: 'Create', - actor: this, - object: article - }); - this.doActivity(activity); - } - - getJSONLD(url: URL): ActivityPub.Actor & ActivityPub.RootObject { - if (!url.href.endsWith('/')) { - url.href += '/'; - } - - return { - '@context': [ - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - { - featured: { - '@id': 'http://joinmastodon.org/ns#featured', - '@type': '@id' - } - }, - { - discoverable: { - '@id': 'http://joinmastodon.org/ns#discoverable', - '@type': '@id' - } - }, - { - manuallyApprovesFollowers: { - '@id': 'http://joinmastodon.org/ns#manuallyApprovesFollowers', - '@type': '@id' - } - }, - { - schema: 'http://schema.org#', - PropertyValue: 'schema:PropertyValue', - value: 'schema:value' - } - ], - type: 'Person', - id: this.actorId.getValue(url), - name: this.displayName, // Full name - preferredUsername: this.username, // Username - summary: 'The bio for the actor', // Bio - url: this.actorId.getValue(url), // Profile URL - icon: '', // Avatar - image: '', // Header image - published: '1970-01-01T00:00:00Z', // When profile was created - manuallyApprovesFollowers: false, // Locked account - discoverable: true, // Shown in the profile directory - attachment: [{ - type: 'PropertyValue', - name: 'Website', - value: `${url.hostname}` - }], - - // Collections - following: this.followingCollectionId.getValue(url), - followers: this.followersCollectionId.getValue(url), - inbox: this.inboxId.getValue(url), - outbox: this.outboxId.getValue(url), - featured: this.featuredCollectionId.getValue(url), - - publicKey: { - id: this.publicKeyId.getValue(url), - owner: this.actorId.getValue(url), - publicKeyPem: this.attr.publicKey - } - }; - } - - static create(data: Partial & {username: string;}) { - let publicKey = data.publicKey; - let privateKey = data.privateKey; - - if (!publicKey || !privateKey) { - const keypair = crypto.generateKeyPairSync('rsa', { - modulusLength: 512 - }); - publicKey = keypair.publicKey - .export({type: 'pkcs1', format: 'pem'}) - .toString(); - privateKey = keypair.privateKey - .export({type: 'pkcs1', format: 'pem'}) - .toString(); - } - - return new Actor({ - id: data.id instanceof ObjectID ? data.id : undefined, - username: data.username, - displayName: data.displayName, - publicKey: publicKey, - privateKey: privateKey, - outbox: data.outbox || [], - inbox: data.inbox || [], - followers: data.followers || [], - following: data.following || [] - }); - } -} diff --git a/ghost/ghost/src/core/activitypub/actor.repository.ts b/ghost/ghost/src/core/activitypub/actor.repository.ts deleted file mode 100644 index aadc232268..0000000000 --- a/ghost/ghost/src/core/activitypub/actor.repository.ts +++ /dev/null @@ -1,8 +0,0 @@ -import ObjectID from 'bson-objectid'; -import {Actor} from './actor.entity'; - -export interface ActorRepository { - getOne(username: string): Promise - getOne(id: ObjectID): Promise - save(actor: Actor): Promise -} diff --git a/ghost/ghost/src/core/activitypub/article.object.ts b/ghost/ghost/src/core/activitypub/article.object.ts deleted file mode 100644 index 950388ea7b..0000000000 --- a/ghost/ghost/src/core/activitypub/article.object.ts +++ /dev/null @@ -1,63 +0,0 @@ -import ObjectID from 'bson-objectid'; -import {ActivityPub} from './types'; -import {Post} from './post.repository'; -import {URI} from './uri.object'; - -type ArticleData = { - id: ObjectID - name: string - content: string - url: URI - image: URI | null - published: Date | null - attributedTo: {type: string, name: string}[] - preview: {type: string, content: string} -}; - -export class Article { - constructor(private readonly attr: ArticleData) {} - - get objectId() { - return new URI(`article/${this.attr.id.toHexString()}`); - } - - getJSONLD(url: URL): ActivityPub.Article & ActivityPub.RootObject { - if (!url.href.endsWith('/')) { - url.href += '/'; - } - - const id = new URL(`article/${this.attr.id.toHexString()}`, url.href); - - return { - '@context': 'https://www.w3.org/ns/activitystreams', - type: 'Article', - id: id.href, - name: this.attr.name, - content: this.attr.content, - url: this.attr.url.getValue(url), - image: this.attr.image?.getValue(url), - published: this.attr.published?.toISOString(), - attributedTo: this.attr.attributedTo, - preview: this.attr.preview - }; - } - - static fromPost(post: Post) { - return new Article({ - id: post.id, - name: post.title, - content: post.html, - url: post.url, - image: post.featuredImage, - published: post.publishedAt, - attributedTo: post.authors.map(name => ({ - type: 'Person', - name - })), - preview: { - type: 'Note', - content: post.excerpt - } - }); - } -} diff --git a/ghost/ghost/src/core/activitypub/http-signature.service.test.ts b/ghost/ghost/src/core/activitypub/http-signature.service.test.ts deleted file mode 100644 index 0b969cb0cc..0000000000 --- a/ghost/ghost/src/core/activitypub/http-signature.service.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -import assert from 'assert'; -import crypto from 'crypto'; -import {HTTPSignature} from './http-signature.service'; - -describe('HTTPSignature', function () { - describe('#validate', function () { - it('returns true when the signature is valid', async function () { - const requestMethod = 'POST'; - const requestUrl = '/activitypub/inbox/deadbeefdeadbeefdeadbeef'; - const requestHeaders = new Headers({ - host: 'a424-171-97-56-187.ngrok-free.app', - 'user-agent': 'http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-04-30; +https://mastodon.social/)', - 'content-length': '286', - 'accept-encoding': 'gzip', - 'content-type': 'application/activity+json', - date: 'Thu, 02 May 2024 09:51:57 GMT', - digest: 'SHA-256=tbr1NMXoLisaWc4LplxkUO19vrpGSjslPpHN5qGMEaU=', - signature: 'keyId="https://mastodon.social/users/testingshtuff#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="rbkHYjeJ6WpO5Pa6Ui3Z/9GzOeB4c/3IMKlXH+ZrBwtAy7DGannGzHXBe+sYWlLOS9U18IQvOcHvsnWkKMs6f63Fbk9kIylxoSOwZqlkWekI5/dfAhEnlz6azW0X3psiW6I/nAqTdAmWYTqszfQVRD19TwgsQXNsPVD/lEfbsopANCGALePY7mPhmf/ukGluy7Ck4sskwDn6eCqoSHSXi7Mav6ZEp5OABX9C626CyvRG5U/IWE2AVjc8hwGghp7NUgxSLiMKk/Tt3xKFd39dDMDJwj8NinCZQTBmvcZurdzChH2ShDsETxZDvPTFrj30jeH2g29kxZhq5rqHP7a6Gw=="', - 'x-forwarded-for': '49.13.137.65', - 'x-forwarded-host': 'a424-171-97-56-187.ngrok-free.app', - 'x-forwarded-proto': 'https' - }); - const requestBody = Buffer.from('eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy9ucy9hY3Rpdml0eXN0cmVhbXMiLCJpZCI6Imh0dHBzOi8vbWFzdG9kb24uc29jaWFsLzgzMWNlOWMyLWNkYWYtNGJhMC05NmUyLWE3MzY5NDk3MmI5OSIsInR5cGUiOiJGb2xsb3ciLCJhY3RvciI6Imh0dHBzOi8vbWFzdG9kb24uc29jaWFsL3VzZXJzL3Rlc3RpbmdzaHR1ZmYiLCJvYmplY3QiOiJodHRwczovL2E0MjQtMTcxLTk3LTU2LTE4Ny5uZ3Jvay1mcmVlLmFwcC9hY3Rpdml0eXB1Yi9hY3Rvci9kZWFkYmVlZmRlYWRiZWVmZGVhZGJlZWYifQ==', 'base64'); - - const actual = await HTTPSignature.validate(requestMethod, requestUrl, requestHeaders, requestBody); - const expected = true; - - assert.equal(actual, expected, 'The signature should have been validated'); - }); - it('also returns true when the signature is valid', async function () { - const requestMethod = 'POST'; - const requestUrl = '/activitypub/inbox/deadbeefdeadbeefdeadbeef'; - const requestHeaders = new Headers({ - host: 'a424-171-97-56-187.ngrok-free.app', - 'user-agent': 'http.rb/5.2.0 (Mastodon/4.3.0-nightly.2024-04-30; +https://mastodon.social/)', - 'content-length': '438', - 'accept-encoding': 'gzip', - 'content-type': 'application/activity+json', - date: 'Thu, 02 May 2024 09:51:30 GMT', - digest: 'SHA-256=Bru67GlP+0N3ySTtv/D8/QfhCaBc2P9vC1AjCxl5gmA=', - signature: 'keyId="https://mastodon.social/users/testingshtuff#main-key",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="qx5uo2gRN447a1B+yzjFyc5zy/lYCZqC8tJnIe2Tn6Q+vvVLRZL5hUoZQhFzwlxMPpcpibz2EoFdGlNBf/OFuNBoKa+dsjRA9JyCyc0fd/W2adoA+cp/y1smgSpLFjZUrIViG/SfnVBa3JTw+YeeqX4yY27WYiDMw1hSiQYGWbb64kwayChP6povH5MyoqkjyS1QZWYxOmbn27hlcGuqHgqhEEQhDeqwVEOPzq+JrkuosfIxCPTw/oLX0SWITGUwIffXFquOIV8oB1pWkqfbIXjstrMfFq5n48Ee/5vadsj3rR/dDFLMbUUAwO7uKTsvfurcWmzM4fJKoLyAOxzAgQ=="', - 'x-forwarded-for': '78.47.65.118', - 'x-forwarded-host': 'a424-171-97-56-187.ngrok-free.app', - 'x-forwarded-proto': 'https' - }); - const requestBody = Buffer.from('eyJAY29udGV4dCI6Imh0dHBzOi8vd3d3LnczLm9yZy9ucy9hY3Rpdml0eXN0cmVhbXMiLCJpZCI6Imh0dHBzOi8vbWFzdG9kb24uc29jaWFsL3VzZXJzL3Rlc3RpbmdzaHR1ZmYjZm9sbG93cy80MjQ3NDc2Ny91bmRvIiwidHlwZSI6IlVuZG8iLCJhY3RvciI6Imh0dHBzOi8vbWFzdG9kb24uc29jaWFsL3VzZXJzL3Rlc3RpbmdzaHR1ZmYiLCJvYmplY3QiOnsiaWQiOiJodHRwczovL21hc3RvZG9uLnNvY2lhbC8yNmY5M2Q2Yy03NmU3LTRiNzAtOWE4Yy03MzMzMTBhMjU4MjQiLCJ0eXBlIjoiRm9sbG93IiwiYWN0b3IiOiJodHRwczovL21hc3RvZG9uLnNvY2lhbC91c2Vycy90ZXN0aW5nc2h0dWZmIiwib2JqZWN0IjoiaHR0cHM6Ly9hNDI0LTE3MS05Ny01Ni0xODcubmdyb2stZnJlZS5hcHAvYWN0aXZpdHlwdWIvYWN0b3IvZGVhZGJlZWZkZWFkYmVlZmRlYWRiZWVmIn19', 'base64'); - - const actual = await HTTPSignature.validate(requestMethod, requestUrl, requestHeaders, requestBody); - const expected = true; - - assert.equal(actual, expected, 'The signature should have been validated'); - }); - }); - - describe('#sign', function () { - it('Can sign a request that does not have explicit Date or Host headers', async function () { - const keypair = crypto.generateKeyPairSync('rsa', { - modulusLength: 512 - }); - const request = new Request('https://example.com:2368/blah'); - const signed = await HTTPSignature.sign(request, new URL('https://keyid.com'), keypair.privateKey); - - assert.ok(signed); - }); - - it('Can sign a post request which is valid', async function () { - const keypair = crypto.generateKeyPairSync('rsa', { - modulusLength: 512 - }); - class MockHTTPSignature extends HTTPSignature { - protected static async getPublicKey() { - return keypair.publicKey; - } - } - - const request = new Request('https://example.com:2368/blah', { - method: 'POST', - headers: { - 'Content-Type': 'application/ld+json' - }, - body: JSON.stringify({ - id: 'https://mastodon.social/users/testingshtuff', - type: 'Accept', - actor: 'https://mastodon.social/users/testingshtuff', - object: 'https://mastodon.social/79f89120-fd13-43e8-aa6d-3bd03652cfad' - }) - }); - - const signed = await MockHTTPSignature.sign(request, new URL('https://keyid.com'), keypair.privateKey); - - assert.ok(signed); - - const url = new URL(signed.url); - const body = Buffer.from(await signed.text()); - const result = await MockHTTPSignature.validate(signed.method, url.pathname, signed.headers, body); - - assert.equal(result, true); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/http-signature.service.ts b/ghost/ghost/src/core/activitypub/http-signature.service.ts deleted file mode 100644 index 48920e2918..0000000000 --- a/ghost/ghost/src/core/activitypub/http-signature.service.ts +++ /dev/null @@ -1,209 +0,0 @@ -import crypto from 'node:crypto'; - -type Signature = { - signature: Buffer - headers: string[] - keyId: URL - algorithm: string -}; - -export class HTTPSignature { - private static generateSignatureString( - signature: Signature, - headers: Headers, - requestMethod: string, - requestUrl: string - ): string { - const data = signature.headers - .map((header) => { - return `${header}: ${this.getHeader( - header, - headers, - requestMethod, - requestUrl - )}`; - }) - .join('\n'); - - return data; - } - - private static parseSignatureHeader(signature: string): Signature { - const signatureData: Record = signature - .split(',') - .reduce((data, str) => { - try { - const [key, value] = str.split('='); - return { - // values are wrapped in quotes like key="the value" - [key]: value.replace(/"/g, ''), - ...data - }; - } catch (err) { - return data; - } - }, {}); - - if ( - !signatureData.signature || - !signatureData.headers || - !signatureData.keyId || - !signatureData.algorithm - ) { - throw new Error('Could not parse signature'); - } - - return { - keyId: new URL(signatureData.keyId), - headers: signatureData.headers.split(/\s/), - signature: Buffer.from(signatureData.signature, 'base64url'), - algorithm: signatureData.algorithm - }; - } - - private static getHeader( - header: string, - headers: Headers, - requestMethod: string, - requestUrl: string - ) { - if (header === '(request-target)') { - return `${requestMethod.toLowerCase()} ${requestUrl}`; - } - if (!headers.has(header)) { - throw new Error(`Missing Header ${header}`); - } - return headers.get(header); - } - - protected static async getPublicKey(keyId: URL): Promise { - try { - const keyRes = await fetch(keyId, { - headers: { - Accept: 'application/ld+json' - } - }); - - // This whole thing is wrapped in try/catch so we can just cast as we want and not worry about errors - const json = (await keyRes.json()) as { - publicKey: { publicKeyPem: string }; - }; - - const key = crypto.createPublicKey(json.publicKey.publicKeyPem); - return key; - } catch (err) { - throw new Error(`Could not find public key ${keyId.href}: ${err}`); - } - } - - private static validateDigest( - signatureData: Signature, - requestBody: Buffer, - requestHeaders: Headers - ) { - const digest = crypto - .createHash(signatureData.algorithm) - .update(requestBody) - .digest('base64'); - - const parts = requestHeaders.get('digest')?.split('='); - parts?.shift(); - const remoteDigest = parts?.join('='); - - return digest === remoteDigest; - } - - static async validate( - requestMethod: string, - requestUrl: string, - requestHeaders: Headers, - requestBody: Buffer = Buffer.alloc(0, 0) - ) { - const signatureHeader = requestHeaders.get('signature'); - if (typeof signatureHeader !== 'string') { - throw new Error('Invalid Signature header'); - } - const signatureData = this.parseSignatureHeader(signatureHeader); - - if (requestMethod.toLowerCase() === 'post') { - const digestIsValid = this.validateDigest( - signatureData, - requestBody, - requestHeaders - ); - if (!digestIsValid) { - return false; - } - } - - const publicKey = await this.getPublicKey(signatureData.keyId); - const signatureString = this.generateSignatureString( - signatureData, - requestHeaders, - requestMethod, - requestUrl - ); - - const verified = crypto - .createVerify(signatureData.algorithm) - .update(signatureString) - .verify(publicKey, signatureData.signature); - - return verified; - } - - static async sign( - request: Request, - keyId: URL, - privateKey: crypto.KeyObject - ): Promise { - let headers; - if (request.method.toLowerCase() === 'post') { - headers = ['(request-target)', 'host', 'date', 'digest']; - } else { - headers = ['(request-target)', 'host', 'date']; - } - const signatureData: Signature = { - signature: Buffer.alloc(0, 0), - headers, - keyId, - algorithm: 'rsa-sha256' - }; - const url = new URL(request.url); - const requestHeaders = new Headers(request.headers); - if (!requestHeaders.has('host')) { - requestHeaders.set('host', url.host); - } - if (!requestHeaders.has('date')) { - requestHeaders.set('date', (new Date()).toUTCString()); - } - if (request.method.toLowerCase() === 'post') { - const digest = crypto - .createHash(signatureData.algorithm) - .update(Buffer.from(await request.clone().text(), 'utf8')) - .digest('base64'); - - requestHeaders.set('digest', `${signatureData.algorithm}=${digest}`); - } - const signatureString = this.generateSignatureString( - signatureData, - requestHeaders, - request.method, - url.pathname - ); - const signature = crypto - .createSign(signatureData.algorithm) - .update(signatureString) - .sign(privateKey) - .toString('base64'); - - requestHeaders.set( - 'Signature', - `keyId="${keyId}",headers="${headers.join(' ')}",signature="${signature}",algorithm="${signatureData.algorithm}"` - ); - - return new Request(request, { - headers: requestHeaders - }); - } -} diff --git a/ghost/ghost/src/core/activitypub/inbox.service.test.ts b/ghost/ghost/src/core/activitypub/inbox.service.test.ts deleted file mode 100644 index 33fec95549..0000000000 --- a/ghost/ghost/src/core/activitypub/inbox.service.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import Sinon from 'sinon'; -import {InboxService} from './inbox.service'; -import assert from 'assert'; -import ObjectID from 'bson-objectid'; -import {Activity} from './activity.entity'; -import {URI} from './uri.object'; -import {Actor} from './actor.entity'; - -describe('InboxService', function () { - describe('#post', function () { - it('Throws if it cannot find the actor', async function () { - const mockActorRepository = { - getOne: Sinon.stub().resolves(null), - save: Sinon.stub().rejects() - }; - const mockActivityRepository = { - getOne: Sinon.stub().resolves(null), - save: Sinon.stub().rejects() - }; - const service = new InboxService( - mockActorRepository, - mockActivityRepository - ); - - const owner = new ObjectID(); - const activity = new Activity({ - type: 'Follow', - activity: null, - object: { - type: 'Application', - id: new URI('https://whatever.com') - }, - actor: { - type: 'Person', - id: new URI('https://blak.com') - }, - to: new URI('https://whatever.com') - }); - - await assert.rejects(async () => { - await service.post(owner, activity); - }, /Not Found/); - }); - it('Posts the activity to the actors inbox, saves the actor & the activity', async function () { - const actor = Actor.create({username: 'username'}); - const mockActorRepository = { - getOne: Sinon.stub().resolves(actor), - save: Sinon.stub().resolves() - }; - const mockActivityRepository = { - getOne: Sinon.stub().resolves(null), - save: Sinon.stub().resolves() - }; - const service = new InboxService( - mockActorRepository, - mockActivityRepository - ); - - const postToInboxStub = Sinon.stub(actor, 'postToInbox'); - - const owner = new ObjectID(); - const activity = new Activity({ - type: 'Follow', - activity: null, - object: { - type: 'Person', - id: new URI('https://whatever.com') - }, - actor: { - type: 'Person', - id: new URI('https://blak.com') - }, - to: new URI('https://whatever.com') - }); - - await service.post(owner, activity); - - assert(postToInboxStub.calledWith(activity)); - assert(mockActorRepository.save.calledWith(actor)); - assert(mockActivityRepository.save.calledWith(activity)); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/inbox.service.ts b/ghost/ghost/src/core/activitypub/inbox.service.ts deleted file mode 100644 index c248780cd7..0000000000 --- a/ghost/ghost/src/core/activitypub/inbox.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import {Inject} from '@nestjs/common'; -import {Activity} from './activity.entity'; -import {ActorRepository} from './actor.repository'; -import {ActivityRepository} from './activity.repository'; -import ObjectID from 'bson-objectid'; - -export class InboxService { - constructor( - @Inject('ActorRepository') private readonly actors: ActorRepository, - @Inject('ActivityRepository') private readonly activities: ActivityRepository - ) {} - - async post(owner: ObjectID, activity: Activity) { - const actor = await this.actors.getOne(owner); - - if (!actor) { - throw new Error('Not Found'); - } - - await actor.postToInbox(activity); - - await this.actors.save(actor); - await this.activities.save(activity); - } -} diff --git a/ghost/ghost/src/core/activitypub/jsonld.service.test.ts b/ghost/ghost/src/core/activitypub/jsonld.service.test.ts deleted file mode 100644 index bdc77c43a0..0000000000 --- a/ghost/ghost/src/core/activitypub/jsonld.service.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import Sinon from 'sinon'; -import {Actor} from './actor.entity'; -import {JSONLDService} from './jsonld.service'; -import assert from 'assert'; -import ObjectID from 'bson-objectid'; - -describe('JSONLDService', function () { - describe('#getActor', function () { - it('Returns JSONLD representation of Actor', async function () { - const actor = Actor.create({username: 'freddy'}); - const mockActorRepository = { - getOne: Sinon.stub().resolves(actor), - save: Sinon.stub().rejects() - }; - const mockPostRepository = { - getOne: Sinon.stub().resolves(null) - }; - const url = new URL('https://example.com'); - - const service = new JSONLDService( - mockActorRepository, - mockPostRepository, - url - ); - - const result = await service.getActor(actor.id); - - assert(result); - assert.equal(result.type, 'Person'); - }); - }); - describe('#getOutbox', function () { - it('returns JSONLD representation of Outbox', async function () { - const actor = Actor.create({username: 'freddy'}); - const mockActorRepository = { - getOne: Sinon.stub().resolves(actor), - save: Sinon.stub().rejects() - }; - const mockPostRepository = { - getOne: Sinon.stub().resolves(null) - }; - const url = new URL('https://example.com'); - - const service = new JSONLDService( - mockActorRepository, - mockPostRepository, - url - ); - - const result = await service.getOutbox(actor.id); - - assert(result); - assert.equal(result.type, 'OrderedCollection'); - }); - it('returns null if actor not found', async function () { - const actor = Actor.create({username: 'freddy'}); - const mockActorRepository = { - getOne: Sinon.stub().resolves(null), - save: Sinon.stub().rejects() - }; - const mockPostRepository = { - getOne: Sinon.stub().resolves(null) - }; - const url = new URL('https://example.com'); - - const service = new JSONLDService( - mockActorRepository, - mockPostRepository, - url - ); - - const result = await service.getOutbox(actor.id); - - assert.equal(result, null); - }); - }); - describe('#getArticle', function () { - it('Throws if no post found', async function () { - const mockActorRepository = { - getOne: Sinon.stub().resolves(null), - save: Sinon.stub().rejects() - }; - const mockPostRepository = { - getOne: Sinon.stub().resolves(null) - }; - const url = new URL('https://example.com'); - - const service = new JSONLDService( - mockActorRepository, - mockPostRepository, - url - ); - - await assert.rejects(async () => { - await service.getArticle(new ObjectID()); - }); - }); - - it('Throws if post not public', async function () { - const mockActorRepository = { - getOne: Sinon.stub().resolves(null), - save: Sinon.stub().rejects() - }; - const mockPostRepository = { - getOne: Sinon.stub().resolves({ - visibility: 'private' - }) - }; - const url = new URL('https://example.com'); - - const service = new JSONLDService( - mockActorRepository, - mockPostRepository, - url - ); - - await assert.rejects(async () => { - await service.getArticle(new ObjectID()); - }); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/jsonld.service.ts b/ghost/ghost/src/core/activitypub/jsonld.service.ts deleted file mode 100644 index 944397a4d0..0000000000 --- a/ghost/ghost/src/core/activitypub/jsonld.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {Inject} from '@nestjs/common'; -import {ActorRepository} from './actor.repository'; -import ObjectID from 'bson-objectid'; -import {PostRepository} from './post.repository'; -import {Article} from './article.object'; - -export class JSONLDService { - constructor( - @Inject('ActorRepository') private repository: ActorRepository, - @Inject('PostRepository') private postRepository: PostRepository, - @Inject('ActivityPubBaseURL') private url: URL - ) {} - - async getActor(id: ObjectID) { - const actor = await this.repository.getOne(id); - 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 getInbox(owner: ObjectID) { - const actor = await this.repository.getOne(owner); - if (!actor) { - return null; - } - const json = actor.getJSONLD(this.url); - return { - '@context': 'https://www.w3.org/ns/activitystreams', - id: json.inbox, - summary: `Inbox for ${actor.username}`, - type: 'OrderedCollection', - totalItems: actor.inbox.length, - orderedItems: actor.inbox.map(activity => activity.getJSONLD(this.url)) - }; - } - - async getOutbox(owner: ObjectID) { - const actor = await this.repository.getOne(owner); - if (!actor) { - return null; - } - const json = actor.getJSONLD(this.url); - return { - '@context': 'https://www.w3.org/ns/activitystreams', - id: json.outbox, - summary: `Outbox for ${actor.username}`, - type: 'OrderedCollection', - totalItems: actor.outbox.length, - orderedItems: actor.outbox.map(activity => activity.getJSONLD(this.url)) - }; - } - - async getArticle(id: ObjectID) { - const post = await this.postRepository.getOne(id); - if (!post) { - throw new Error('Not found'); - } - if (post.visibility !== 'public') { - throw new Error('Cannot view'); - } - return Article.fromPost(post).getJSONLD(this.url); - } -} diff --git a/ghost/ghost/src/core/activitypub/post.repository.ts b/ghost/ghost/src/core/activitypub/post.repository.ts deleted file mode 100644 index 0a18559c8c..0000000000 --- a/ghost/ghost/src/core/activitypub/post.repository.ts +++ /dev/null @@ -1,19 +0,0 @@ -import ObjectID from 'bson-objectid'; -import {URI} from './uri.object'; - -export type Post = { - id: ObjectID; - title: string; - slug: string; - html: string; - visibility: string; - featuredImage: URI | null; - url: URI; - publishedAt: Date | null; - authors: string[]; - excerpt: string; -}; - -export interface PostRepository { - getOne(id: ObjectID): Promise -} diff --git a/ghost/ghost/src/core/activitypub/tell-the-world.service.test.ts b/ghost/ghost/src/core/activitypub/tell-the-world.service.test.ts deleted file mode 100644 index 6dc524e24f..0000000000 --- a/ghost/ghost/src/core/activitypub/tell-the-world.service.test.ts +++ /dev/null @@ -1,116 +0,0 @@ -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'), console); - - 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, - object: { - type: 'Person', - 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'); - }); - - it('Can deliver the activity to the inboxes of a collection of actors', async function () { - const service = new TheWorld(new URL('https://base.com'), console); - - const actor = Actor.create({ - username: 'Testing' - }); - - const followers = new URI('https://main.ghost.org/activitypub/followers/deadbeefdeadbeefdeadbeef'); - - const activity = new Activity({ - type: 'Create', - activity: null, - actor: actor, - object: { - id: new URI('https://main.ghost.org/hello-world'), - type: 'Note', - content: '

Hello, world.

' - }, - to: followers - }); - - nock('https://main.ghost.org') - .get('/activitypub/followers/deadbeefdeadbeefdeadbeef') - .reply(200, { - '@context': '', - type: 'Collection', - totalItems: 3, - items: [ - 'https://main.ghost.org/activitypub/actor/deadbeefdeadbeefdeadbeef', - { - id: 'https://main.ghost.org/activitypub/actor/beefdeadbeefdeadbeefdead' - }, - { - invalid: true - } - ] - }); - - nock('https://main.ghost.org') - .get('/activitypub/actor/deadbeefdeadbeefdeadbeef') - .reply(200, { - inbox: 'https://main.ghost.org/activitypub/inbox/deadbeefdeadbeefdeadbeef' - }); - - nock('https://main.ghost.org') - .get('/activitypub/actor/beefdeadbeefdeadbeefdead') - .reply(200, { - inbox: 'https://main.ghost.org/activitypub/inbox/beefdeadbeefdeadbeefdead' - }); - - const firstActivityDelivery = nock('https://main.ghost.org') - .post('/activitypub/inbox/deadbeefdeadbeefdeadbeef') - .reply(201, {}); - - const secondActivityDelivery = nock('https://main.ghost.org') - .post('/activitypub/inbox/beefdeadbeefdeadbeefdead') - .reply(201, {}); - - await service.deliverActivity(activity, actor); - - assert(firstActivityDelivery.isDone(), 'Expected activity to be delivered'); - assert(secondActivityDelivery.isDone(), 'Expected activity to be delivered'); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/tell-the-world.service.ts b/ghost/ghost/src/core/activitypub/tell-the-world.service.ts deleted file mode 100644 index 584a83c230..0000000000 --- a/ghost/ghost/src/core/activitypub/tell-the-world.service.ts +++ /dev/null @@ -1,86 +0,0 @@ -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, - @Inject('logger') private readonly logger: Console - ) {} - - async deliverActivity(activity: Activity, actor: Actor): Promise { - 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); - } - - if ('type' in data && data.type === 'Collection') { - if ('items' in data && Array.isArray(data.items)) { - for (const item of data.items) { - let url; - if (typeof item === 'string') { - url = new URL(item); - } else if ('id' in item && typeof item.id === 'string') { - url = new URL(item.id); - } - if (url) { - const fetchedActor = await this.fetchForActor(url.href, actor); - if ('inbox' in fetchedActor && typeof fetchedActor.inbox === 'string') { - const inbox = new URL(fetchedActor.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); - try { - await fetch(signedRequest); - } catch (err) { - this.logger.error(err); - } - } - - private async getRecipients(activity: Activity): Promise{ - 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; - } -} diff --git a/ghost/ghost/src/core/activitypub/types.ts b/ghost/ghost/src/core/activitypub/types.ts deleted file mode 100644 index a8328c1c98..0000000000 --- a/ghost/ghost/src/core/activitypub/types.ts +++ /dev/null @@ -1,79 +0,0 @@ -// eslint-disable-next-line @typescript-eslint/no-namespace -export namespace ActivityPub { - export type AnonymousObject = { - '@context': string | (string | object)[]; - id: null; - type: string | string[]; - }; - - export type RootObject = { - '@context': string | (string | object)[]; - id: string; - type: string | string[]; - }; - - export type SubObject = { - id: string; - type: string | string[]; - }; - - export type Object = RootObject | AnonymousObject | SubObject; - - export type Actor = ActivityPub.Object & { - inbox: string; - outbox: string; - name: string; - preferredUsername: string; - summary: string; - url: string; - icon: string; - image: string; - published: string; - manuallyApprovesFollowers: boolean; - discoverable: boolean; - attachment: object[]; - following: string; - followers: string; - featured: string; - - publicKey: { - id: string, - owner: string, - publicKeyPem: string - }, - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [x: string]: any - }; - - export type Article = ActivityPub.Object & { - type: 'Article'; - name: string; - content: string; - url?: string; - attributedTo?: string | object[]; - image?: string; - published?: string; - preview?: {type: string, content: string}; - }; - - export type Link = string | { - type: 'Link' - href: string - id?: string - hreflang?: string - mediaType?: string - rel?: string - height?: number - width?: number - }; - - export type ActivityType = 'Create' | 'Update' | 'Delete' | 'Follow' | 'Accept' | 'Reject' | 'Undo'; - - export type Activity = ActivityPub.Object & { - type: ActivityType; - actor: Link | Actor | ActivityPub.Object; - object: Link | ActivityPub.Object; - to: Link | Actor | null; - } -} diff --git a/ghost/ghost/src/core/activitypub/uri.object.ts b/ghost/ghost/src/core/activitypub/uri.object.ts deleted file mode 100644 index a25316e505..0000000000 --- a/ghost/ghost/src/core/activitypub/uri.object.ts +++ /dev/null @@ -1,12 +0,0 @@ -export class URI extends URL { - static readonly BASE_URL = new URL('https://example.com'); - - constructor(url: string | URI, base?: string | URI) { - super(url, base || URI.BASE_URL); - } - - getValue(url: URL) { - const replaceValue = url.href.endsWith('/') ? url.href : url.href + '/'; - return this.href.replace(URI.BASE_URL.href, replaceValue); - } -} diff --git a/ghost/ghost/src/core/activitypub/webfinger.service.test.ts b/ghost/ghost/src/core/activitypub/webfinger.service.test.ts deleted file mode 100644 index eee90a0a45..0000000000 --- a/ghost/ghost/src/core/activitypub/webfinger.service.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import assert from 'assert'; -import {ActorRepository} from './actor.repository'; -import {WebFingerService} from './webfinger.service'; -import {Actor} from './actor.entity'; - -describe('WebFingerService', function () { - describe('getResource', function () { - it('Throws with invalid resource', async function () { - const repository: ActorRepository = {} as ActorRepository; - const service = new WebFingerService(repository, new URL('https://activitypub.server')); - - await service.getResource('invalid').then( - () => { - throw new Error('Should have thrown'); - }, - (err) => { - assert.ok(err); - } - ); - }); - - it('Throws with missing actor', async function () { - const repository: ActorRepository = { - async getOne() { - return null; - } - } as unknown as ActorRepository; - const service = new WebFingerService(repository, new URL('https://activitypub.server')); - - await service.getResource('invalid').then( - () => { - throw new Error('Should have thrown'); - }, - (err) => { - assert.ok(err); - } - ); - }); - - it('Responds with webfinger for found actors', async function () { - const actor = Actor.create({username: 'c00ld00d'}); - const repository: ActorRepository = { - async getOne() { - return actor; - } - } as unknown as ActorRepository; - const url = new URL('https://activitypub.server'); - const service = new WebFingerService(repository, url); - - const subject = 'acct:c00ld00d@activitypub.server'; - const result = await service.getResource(subject); - - assert.deepEqual(result, { - subject, - links: [{ - rel: 'self', - type: 'application/activity+json', - href: actor.getJSONLD(url).id - }] - }); - }); - }); - - describe('#finger', function () { - it('Throws with invalid usernames', async function () { - const repository: ActorRepository = {} as ActorRepository; - const service = new WebFingerService(repository, new URL('https://activitypub.server')); - - await service.finger('invalid').then( - () => { - throw new Error('Should have thrown'); - }, - (err) => { - assert.ok(err); - } - ); - }); - }); -}); diff --git a/ghost/ghost/src/core/activitypub/webfinger.service.ts b/ghost/ghost/src/core/activitypub/webfinger.service.ts deleted file mode 100644 index 9257923c72..0000000000 --- a/ghost/ghost/src/core/activitypub/webfinger.service.ts +++ /dev/null @@ -1,81 +0,0 @@ -import {Inject} from '@nestjs/common'; -import {ActorRepository} from './actor.repository'; - -const accountResourceMatcher = /acct:(\w+)@(\w+)/; -const usernameMatcher = /@?(.+)@(.+)/; -export class WebFingerService { - constructor( - @Inject('ActorRepository') private repository: ActorRepository, - @Inject('ActivityPubBaseURL') private url: URL - ) {} - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async getResource(resource: string, rel?: string[]) { - const match = resource.match(accountResourceMatcher); - if (!match) { - throw new Error('Invalid Resource'); - } - - const username = match[1]; - - const actor = await this.repository.getOne(username); - - if (!actor) { - throw new Error('not found'); - } - - const result = { - subject: resource, - links: [ - { - rel: 'self', - type: 'application/activity+json', - href: actor.getJSONLD(this.url).id - } - ] - }; - - return result; - } - - async finger(handle: string) { - const match = handle.match(usernameMatcher); - - if (!match) { - throw new Error('Invalid username'); - } - - const username = match[1]; - const host = match[2]; - - let protocol = 'https'; - - // TODO Never in prod - if (host.startsWith('localhost') || host.startsWith('127.0.0.1')) { - protocol = 'http'; - } - - const res = await fetch(`${protocol}://${host}/.well-known/webfinger?resource=acct:${username}@${host}`); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const json: any = await res.json(); - - if (json.subject !== `acct:${username}@${host}`) { - throw new Error('Subject does not match - not jumping thru hoops'); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const self = json.links.find((link: any) => link.rel === 'self'); - - const selfRes = await fetch(self.href, { - headers: { - accept: self.type - } - }); - - const data = await selfRes.json(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return data as any; - } -} diff --git a/ghost/ghost/src/db/in-memory/activity.repository.in-memory.ts b/ghost/ghost/src/db/in-memory/activity.repository.in-memory.ts deleted file mode 100644 index dcc9929d8d..0000000000 --- a/ghost/ghost/src/db/in-memory/activity.repository.in-memory.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Activity} from '../../core/activitypub/activity.entity'; -import {ActivityRepository} from '../../core/activitypub/activity.repository'; - -export class ActivityRepositoryInMemory implements ActivityRepository { - private activities: Activity[] = []; - - async getOne(id: URL) { - const found = this.activities.find(entity => entity.activityId?.href === id.href); - if (!found) { - return null; - } - return found; - } - - async save(activity: Activity) { - this.activities.push(activity); - } -} diff --git a/ghost/ghost/src/db/in-memory/actor.repository.in-memory.ts b/ghost/ghost/src/db/in-memory/actor.repository.in-memory.ts deleted file mode 100644 index 5ef17baf21..0000000000 --- a/ghost/ghost/src/db/in-memory/actor.repository.in-memory.ts +++ /dev/null @@ -1,63 +0,0 @@ -import {Actor} from '../../core/activitypub/actor.entity'; -import {ActorRepository} from '../../core/activitypub/actor.repository'; -import ObjectID from 'bson-objectid'; -import {Inject} from '@nestjs/common'; -import {SettingsCache} from '../../common/types/settings-cache.type'; - -interface DomainEvents { - dispatch(event: unknown): void -} - -export class ActorRepositoryInMemory implements ActorRepository { - actors: Actor[]; - - private readonly domainEvents: DomainEvents; - - constructor( - @Inject('SettingsCache') settingsCache: SettingsCache, - @Inject('DomainEvents') domainEvents: DomainEvents - ) { - this.actors = [ - Actor.create({ - id: ObjectID.createFromHexString('deadbeefdeadbeefdeadbeef'), - username: 'index', - displayName: settingsCache.get('title'), - publicKey: settingsCache.get('ghost_public_key'), - privateKey: settingsCache.get('ghost_private_key'), - following: [] - }) - ]; - this.domainEvents = domainEvents; - } - - private getOneByUsername(username: string) { - return this.actors.find(actor => actor.username === username) || null; - } - - private getOneById(id: ObjectID) { - return this.actors.find(actor => actor.id.equals(id)) || null; - } - - async getOne(identifier: string | ObjectID) { - if (identifier instanceof ObjectID) { - return this.getOneById(identifier); - } else { - return this.getOneByUsername(identifier); - } - } - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - async save(actor: Actor) { - if (!this.actors.includes(actor)) { - this.actors.push(actor); - } - Actor.getActivitiesToSave(actor, (/* activities */) => { - // Persist activities - }); - Actor.getEventsToDispatch(actor, (events) => { - for (const event of events) { - this.domainEvents.dispatch(event); - } - }); - } -} diff --git a/ghost/ghost/src/db/knex/post.repository.knex.ts b/ghost/ghost/src/db/knex/post.repository.knex.ts deleted file mode 100644 index 2c3562d25c..0000000000 --- a/ghost/ghost/src/db/knex/post.repository.knex.ts +++ /dev/null @@ -1,58 +0,0 @@ -import {Inject} from '@nestjs/common'; -import ObjectID from 'bson-objectid'; -import {PostRepository} from '../../core/activitypub/post.repository'; -import {URI} from '../../core/activitypub/uri.object'; -import htmlToPlaintext from '@tryghost/html-to-plaintext'; - -type UrlUtils = { - transformReadyToAbsolute(html: string): string -} - -export class KnexPostRepository implements PostRepository { - constructor( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - @Inject('knex') private readonly knex: any, - @Inject('UrlUtils') private readonly urlUtils: UrlUtils - ) {} - async getOne(identifier: ObjectID) { - return this.getOneById(identifier); - } - - async getOneById(id: ObjectID) { - const row = await this.knex('posts').where('id', id.toHexString()).first(); - const authorRows = await this.knex('users') - .leftJoin('posts_authors', 'users.id', 'posts_authors.author_id') - .where('posts_authors.post_id', id.toHexString()) - .select('users.name'); - - if (!row) { - return null; - } - - let excerpt = row.custom_excerpt; - - if (!excerpt) { - const metaRow = await this.knex('posts_meta').where('post_id', id.toHexString()).select('meta_description').first(); - if (metaRow?.meta_description) { - excerpt = metaRow.meta_description; - } - } - - if (!excerpt) { - excerpt = htmlToPlaintext.excerpt(row.html); - } - - return { - id, - title: row.title, - html: this.urlUtils.transformReadyToAbsolute(row.html), - slug: row.slug, - visibility: row.visibility, - featuredImage: row.feature_image ? new URI(row.feature_image) : null, - publishedAt: row.published_at ? new Date(row.published_at) : null, - authors: authorRows.map((authorRow: {name: string}) => authorRow.name), - excerpt, - url: new URI('') // TODO: Get URL for Post - }; - } -}; diff --git a/ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts b/ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts deleted file mode 100644 index 5396706c38..0000000000 --- a/ghost/ghost/src/http/admin/controllers/activitypub.controller.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import Sinon from 'sinon'; -import {ActivityPubController} from './activitypub.controller'; -import assert from 'assert'; -import {ActivityPubService} from '../../../core/activitypub/activitypub.service'; - -describe('ActivityPubController', function () { - describe('#follow', function () { - it('Calls follow on the ActivityPubService and returns an empty object', async function () { - const mockActivityPubService = { - follow: Sinon.stub().resolves(), - getFollowing: Sinon.stub().resolves([]) - } as unknown as ActivityPubService; - const controller = new ActivityPubController(mockActivityPubService); - - await controller.follow('@egg@ghost.org'); - - assert((mockActivityPubService.follow as Sinon.SinonStub).calledWith('@egg@ghost.org')); - }); - }); -}); diff --git a/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts b/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts deleted file mode 100644 index ec22171d16..0000000000 --- a/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { - Controller, - Param, - Post, - UseGuards, - UseInterceptors -} from '@nestjs/common'; -import {Roles} from '../../../common/decorators/permissions.decorator'; -import {LocationHeaderInterceptor} from '../../../nestjs/interceptors/location-header.interceptor'; -import {AdminAPIAuthentication} from '../../../nestjs/guards/admin-api-authentication.guard'; -import {PermissionsGuard} from '../../../nestjs/guards/permissions.guard'; -import {ActivityPubService} from '../../../core/activitypub/activitypub.service'; - -@UseInterceptors(LocationHeaderInterceptor) -@UseGuards(AdminAPIAuthentication, PermissionsGuard) -@Controller('activitypub') -export class ActivityPubController { - constructor(private readonly activitypub: ActivityPubService) {} - - @Roles([ - 'Owner' - ]) - @Post('follow/:username') - async follow(@Param('username') username: string): Promise { - await this.activitypub.follow(username); - return {}; - } -} diff --git a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts deleted file mode 100644 index f96828c076..0000000000 --- a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import request from 'supertest'; -import {Test} from '@nestjs/testing'; -import {WebFingerService} from '../../../core/activitypub/webfinger.service'; -import {ActivityRepositoryInMemory} from '../../../db/in-memory/activity.repository.in-memory'; -import {ActivityPubController} from './activitypub.controller'; -import {JSONLDService} from '../../../core/activitypub/jsonld.service'; -import {InboxService} from '../../../core/activitypub/inbox.service'; -import {ActivityPubService} from '../../../core/activitypub/activitypub.service'; -import {ActorRepositoryInMemory} from '../../../db/in-memory/actor.repository.in-memory'; -import {ActivityService} from '../../../core/activitypub/activity.service'; -import {HTTPSignature} from '../../../core/activitypub/http-signature.service'; -import {ActivityListener} from '../../../listeners/activity.listener'; -import {TheWorld} from '../../../core/activitypub/tell-the-world.service'; -import DomainEvents from '@tryghost/domain-events'; -import {NestApplication} from '@nestjs/core'; -import ObjectID from 'bson-objectid'; -import {URI} from '../../../core/activitypub/uri.object'; - -describe('ActivityPubController', function () { - let app: NestApplication; - before(async function () { - const moduleRef = await Test.createTestingModule({ - controllers: [ActivityPubController], - providers: [ - { - provide: 'logger', - useValue: console - }, - { - provide: 'ActivityPubBaseURL', - useValue: new URL('https://example.com') - }, - { - provide: 'SettingsCache', - useValue: { - get(_key: string) { - return 'value'; - } - } - }, - { - provide: 'DomainEvents', - useValue: DomainEvents - }, - { - provide: 'ActorRepository', - useClass: ActorRepositoryInMemory - }, - { - provide: 'ActivityService', - useClass: ActivityService - }, - { - provide: 'PostRepository', - useValue: { - async getOne(id: ObjectID) { - return { - id, - title: 'Testing', - slug: 'testing', - html: '

testing

', - visibility: 'public', - authors: ['Mr Roach'], - url: new URI('roachie'), - publishedAt: new Date(), - featuredImage: null, - excerpt: 'testing...' - }; - } - } - }, - { - provide: 'ActivityRepository', - useClass: ActivityRepositoryInMemory - }, - WebFingerService, - JSONLDService, - HTTPSignature, - ActivityService, - InboxService, - ActivityListener, - ActivityPubService, - TheWorld - ] - }).compile(); - - app = moduleRef.createNestApplication(); - await app.init(); - }); - after(async function () { - await app.close(); - }); - - it('Can handle requests to get the actor', async function () { - await request(app.getHttpServer()) - .get('/activitypub/actor/deadbeefdeadbeefdeadbeef') - .expect(200); - }); - - it('Can handle requests to get the outbox', async function () { - await request(app.getHttpServer()) - .get('/activitypub/outbox/deadbeefdeadbeefdeadbeef') - .expect(200); - }); - - it('Can handle requests to get the inbox', async function () { - await request(app.getHttpServer()) - .get('/activitypub/inbox/deadbeefdeadbeefdeadbeef') - .expect(200); - }); - - it('Can handle requests to get the following', async function () { - await request(app.getHttpServer()) - .get('/activitypub/following/deadbeefdeadbeefdeadbeef') - .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') - .expect(200); - }); - - describe('/inbox/:id', function () { - it('Errors with invalid requests', async function () { - await request(app.getHttpServer()) - .post('/activitypub/inbox/deadbeefdeadbeefdeadbeef') - .send({ - type: 'Follow', - actor: 'https://site.com/actor', - object: 'https://site.com/object' - }) - .expect(500); - }); - }); -}); diff --git a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts deleted file mode 100644 index bbf245d42a..0000000000 --- a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.ts +++ /dev/null @@ -1,108 +0,0 @@ -import {Controller, Get, Header, Param, Post, RawBody, Headers as NestHeaders, Req, Body} from '@nestjs/common'; -import {Roles} from '../../../common/decorators/permissions.decorator'; -import ObjectID from 'bson-objectid'; -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'; - -@Controller('activitypub') -export class ActivityPubController { - constructor( - private readonly service: JSONLDService, - private readonly inboxService: InboxService - ) {} - - @Header('Cache-Control', 'no-store') - @Header('Content-Type', 'application/activity+json') - @Roles(['Anon']) - @Get('actor/:id') - async getActor(@Param('id') id: unknown) { - if (typeof id !== 'string') { - throw new Error('Bad Request'); - } - return this.service.getActor(ObjectID.createFromHexString(id)); - } - - @Header('Cache-Control', 'no-store') - @Header('Content-Type', 'application/activity+json') - @Roles(['Anon']) - @Get('inbox/:owner') - async getInbox(@Param('owner') owner: unknown) { - if (typeof owner !== 'string') { - throw new Error('Bad Request'); - } - return this.service.getInbox(ObjectID.createFromHexString(owner)); - } - - @Header('Cache-Control', 'no-store') - @Header('Content-Type', 'application/activity+json') - @Roles(['Anon']) - @Post('inbox/:owner') - async handleActivity( - @Param('owner') owner: unknown, - @Body() body: unknown, - @RawBody() rawbody: Buffer, - @NestHeaders() headers: Record, - @Req() req: Request - ) { - if (typeof owner !== 'string') { - throw new Error('Bad Request'); - } - if (typeof body !== 'object' || body === null) { - throw new Error('Bad Request'); - } - if (!('id' in body) || !('type' in body) || !('actor' in body) || !('object' in body)) { - throw new Error('Bad Request'); - } - const verified = await HTTPSignature.validate(req.method, req.url, new Headers(headers), rawbody); - if (!verified) { - throw new Error('Not Authorized'); - } - this.inboxService.post(ObjectID.createFromHexString(owner), Activity.fromJSONLD(body)); - } - - @Header('Cache-Control', 'no-store') - @Header('Content-Type', 'application/activity+json') - @Roles(['Anon']) - @Get('outbox/:owner') - async getOutbox(@Param('owner') owner: unknown) { - if (typeof owner !== 'string') { - throw new Error('Bad Request'); - } - return this.service.getOutbox(ObjectID.createFromHexString(owner)); - } - - @Header('Cache-Control', 'no-store') - @Header('Content-Type', 'application/activity+json') - @Roles(['Anon']) - @Get('following/:owner') - async getFollowing(@Param('owner') owner: unknown) { - if (typeof owner !== 'string') { - throw new Error('Bad Request'); - } - 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') - @Header('Content-Type', 'application/activity+json') - @Roles(['Anon']) - @Get('article/:id') - async getArticle(@Param('id') id: unknown) { - if (typeof id !== 'string') { - throw new Error('Bad Request'); - } - return this.service.getArticle(ObjectID.createFromHexString(id)); - } -} diff --git a/ghost/ghost/src/http/frontend/controllers/webfinger.controller.test.ts b/ghost/ghost/src/http/frontend/controllers/webfinger.controller.test.ts deleted file mode 100644 index 76ee7017d6..0000000000 --- a/ghost/ghost/src/http/frontend/controllers/webfinger.controller.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -import request from 'supertest'; -import {Test} from '@nestjs/testing'; -import {WebFingerService} from '../../../core/activitypub/webfinger.service'; -import {WebFingerController} from './webfinger.controller'; -import {ActivityRepositoryInMemory} from '../../../db/in-memory/activity.repository.in-memory'; - -describe('WebFingerController', function () { - it('Responds to HTTP requests for .well-known/webfinger correctly', async function () { - const moduleRef = await Test.createTestingModule({ - controllers: [WebFingerController], - providers: [ - WebFingerService, - { - provide: 'ActivityPubBaseURL', - useValue: new URL('https://example.com') - }, - { - provide: 'ActorRepository', - useClass: ActivityRepositoryInMemory - } - ] - }) - .overrideProvider(WebFingerService) - .useValue({ - async getResource() {} - }) - .compile(); - - const app = moduleRef.createNestApplication(); - await app.init(); - - request(app.getHttpServer()) - .get('/.well-known/webfinger?resource=acct:egg@ghost.org') - .expect(200); - - await app.close(); - }); -}); diff --git a/ghost/ghost/src/http/frontend/controllers/webfinger.controller.ts b/ghost/ghost/src/http/frontend/controllers/webfinger.controller.ts deleted file mode 100644 index 317ba48e11..0000000000 --- a/ghost/ghost/src/http/frontend/controllers/webfinger.controller.ts +++ /dev/null @@ -1,18 +0,0 @@ -import {Controller, Get, Header, Query} from '@nestjs/common'; -import {WebFingerService} from '../../../core/activitypub/webfinger.service'; - -@Controller('.well-known/webfinger') -export class WebFingerController { - constructor( - private readonly service: WebFingerService - ) {} - - @Header('Cache-Control', 'no-store') - @Get('') - async getResource(@Query('resource') resource: unknown) { - if (typeof resource !== 'string') { - throw new Error('Bad Request'); - } - return await this.service.getResource(resource); - } -} diff --git a/ghost/ghost/src/listeners/activity.listener.test.ts b/ghost/ghost/src/listeners/activity.listener.test.ts deleted file mode 100644 index c28d7be216..0000000000 --- a/ghost/ghost/src/listeners/activity.listener.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import assert from 'assert'; -import {Activity} from '../core/activitypub/activity.entity'; -import {ActivityEvent} from '../core/activitypub/activity.event'; -import {Actor} from '../core/activitypub/actor.entity'; -import {TheWorld} from '../core/activitypub/tell-the-world.service'; -import {URI} from '../core/activitypub/uri.object'; -import {ActivityListener} from './activity.listener'; - -describe('ActivityListener', function () { - describe('#dispatchActivity', function () { - it('uses the service to deliver the activity', function () { - let called = false; - const calledWith: [unknown, unknown][] = []; - class MockTheWorld extends TheWorld { - async deliverActivity(activity: Activity, actor: Actor): Promise { - called = true; - calledWith.push([activity, actor]); - } - } - const listener = new ActivityListener(new MockTheWorld(new URL('https://example.com'), console)); - - const actor = Actor.create({ - username: 'Testing' - }); - - const toFollow = new URI('https://example.com/user'); - - const activity = new Activity({ - type: 'Follow', - activity: null, - actor: actor, - object: { - type: 'Person', - id: toFollow - }, - to: toFollow - }); - - const event = new ActivityEvent({ - activity, - actor - }); - - listener.dispatchActivity(event); - - assert.equal(called, true); - assert.equal(calledWith[0][0], activity); - assert.equal(calledWith[0][1], actor); - }); - }); -}); diff --git a/ghost/ghost/src/listeners/activity.listener.ts b/ghost/ghost/src/listeners/activity.listener.ts deleted file mode 100644 index 7564989afe..0000000000 --- a/ghost/ghost/src/listeners/activity.listener.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Inject} from '@nestjs/common'; -import {OnEvent} from '../common/decorators/handle-event.decorator'; -import {ActivityEvent} from '../core/activitypub/activity.event'; -import {TheWorld} from '../core/activitypub/tell-the-world.service'; - -export class ActivityListener { - constructor( - @Inject(TheWorld) private readonly service: TheWorld - ) {} - - @OnEvent(ActivityEvent) - async dispatchActivity(event: ActivityEvent) { - await this.service.deliverActivity(event.data.activity, event.data.actor); - } -} diff --git a/ghost/ghost/src/nestjs/modules/activitypub.module.ts b/ghost/ghost/src/nestjs/modules/activitypub.module.ts deleted file mode 100644 index 86e54d5be0..0000000000 --- a/ghost/ghost/src/nestjs/modules/activitypub.module.ts +++ /dev/null @@ -1,47 +0,0 @@ -import {Global, Module} from '@nestjs/common'; -import {ActorRepositoryInMemory} from '../../db/in-memory/actor.repository.in-memory'; -import {ActivityPubController} from '../../http/frontend/controllers/activitypub.controller'; -import {WebFingerService} from '../../core/activitypub/webfinger.service'; -import {JSONLDService} from '../../core/activitypub/jsonld.service'; -import {WebFingerController} from '../../http/frontend/controllers/webfinger.controller'; -import {ActivityService} from '../../core/activitypub/activity.service'; -import {KnexPostRepository} from '../../db/knex/post.repository.knex'; -import {HTTPSignature} from '../../core/activitypub/http-signature.service'; -import {InboxService} from '../../core/activitypub/inbox.service'; -import {ActivityRepositoryInMemory} from '../../db/in-memory/activity.repository.in-memory'; -import {ActivityListener} from '../../listeners/activity.listener'; -import {TheWorld} from '../../core/activitypub/tell-the-world.service'; -import {ActivityPubService} from '../../core/activitypub/activitypub.service'; - -@Global() -@Module({ - controllers: [ActivityPubController, WebFingerController], - exports: [], - providers: [ - { - provide: 'ActorRepository', - useClass: ActorRepositoryInMemory - }, - { - provide: 'ActivityService', - useClass: ActivityService - }, - { - provide: 'PostRepository', - useClass: KnexPostRepository - }, - { - provide: 'ActivityRepository', - useClass: ActivityRepositoryInMemory - }, - WebFingerService, - JSONLDService, - HTTPSignature, - ActivityService, - InboxService, - ActivityListener, - ActivityPubService, - TheWorld - ] -}) -export class ActivityPubModule {} diff --git a/ghost/ghost/src/nestjs/modules/admin-api.module.ts b/ghost/ghost/src/nestjs/modules/admin-api.module.ts index 638b84ea05..ce04e7517f 100644 --- a/ghost/ghost/src/nestjs/modules/admin-api.module.ts +++ b/ghost/ghost/src/nestjs/modules/admin-api.module.ts @@ -2,25 +2,15 @@ import {Module} from '@nestjs/common'; import {ExampleController} from '../../http/admin/controllers/example.controller'; import {ExampleService} from '../../core/example/example.service'; import {ExampleRepositoryInMemory} from '../../db/in-memory/example.repository.in-memory'; -import {ActivityPubController} from '../../http/admin/controllers/activitypub.controller'; -import {ActivityPubService} from '../../core/activitypub/activitypub.service'; -import {WebFingerService} from '../../core/activitypub/webfinger.service'; -import {ActorRepositoryInMemory} from '../../db/in-memory/actor.repository.in-memory'; @Module({ - controllers: [ExampleController, ActivityPubController], + controllers: [ExampleController], exports: [ExampleService], providers: [ ExampleService, - ActivityPubService, - WebFingerService, { provide: 'ExampleRepository', useClass: ExampleRepositoryInMemory - }, - { - provide: 'ActorRepository', - useClass: ActorRepositoryInMemory } ] }) diff --git a/ghost/ghost/src/nestjs/modules/app.module.ts b/ghost/ghost/src/nestjs/modules/app.module.ts index 40fad9d9aa..25a4ba74dc 100644 --- a/ghost/ghost/src/nestjs/modules/app.module.ts +++ b/ghost/ghost/src/nestjs/modules/app.module.ts @@ -4,7 +4,6 @@ import {AdminAPIModule} from './admin-api.module'; import {NotFoundFallthroughExceptionFilter} from '../filters/not-found-fallthrough.filter'; import {ExampleListener} from '../../listeners/example.listener'; import {GlobalExceptionFilter} from '../filters/global-exception.filter'; -import {ActivityPubModule} from './activitypub.module'; class AppModuleClass {} @@ -16,13 +15,9 @@ export const AppModule: DynamicModule = { { path: 'ghost/api/admin', module: AdminAPIModule - }, { - path: '', - module: ActivityPubModule } ]), - AdminAPIModule, - ActivityPubModule + AdminAPIModule ], exports: [], controllers: [], diff --git a/ghost/posts-service/lib/PostsService.js b/ghost/posts-service/lib/PostsService.js index b225a1151b..1299161eec 100644 --- a/ghost/posts-service/lib/PostsService.js +++ b/ghost/posts-service/lib/PostsService.js @@ -12,8 +12,6 @@ const { PostsBulkUnfeaturedEvent, PostsBulkAddTagsEvent } = require('@tryghost/post-events'); -const GhostNestApp = require('@tryghost/ghost'); -const {default: ObjectID} = require('bson-objectid'); const messages = { invalidVisibilityFilter: 'Invalid visibility filter.', @@ -209,13 +207,6 @@ class PostsService { } } - if (this.isSet('ActivityPub')) { - if (model.previous('status') !== model.get('status') && model.get('status') === 'published') { - const activityService = await GhostNestApp.resolve('ActivityService'); - await activityService.createArticleForPost(ObjectID.createFromHexString(model.id)); - } - } - if (typeof options?.eventHandler === 'function') { await options.eventHandler(this.getChanges(model), dto); }