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