From c51a434f642b207deec5cf07c50bb4eced6dabb1 Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Wed, 17 Apr 2024 20:17:30 +0700 Subject: [PATCH] Added initial support for Outbox to Actors ref https://linear.app/tryghost/issue/MOM-32 This adds the basic building blocks for an Outbox for an Actor, currently it's hardcoded - which'll let us at lest test integration with other platforms. JSONLDService is an awful name, but it's late and this is a prototype. --- .../src/core/activitypub/jsonld.service.ts | 51 +++++++++++++++++++ .../controllers/activitypub.controller.ts | 38 ++++++++++++++ .../src/nestjs/modules/admin-api.module.ts | 7 ++- 3 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 ghost/ghost/src/core/activitypub/jsonld.service.ts create mode 100644 ghost/ghost/src/http/admin/controllers/activitypub.controller.ts diff --git a/ghost/ghost/src/core/activitypub/jsonld.service.ts b/ghost/ghost/src/core/activitypub/jsonld.service.ts new file mode 100644 index 0000000000..7e858c7305 --- /dev/null +++ b/ghost/ghost/src/core/activitypub/jsonld.service.ts @@ -0,0 +1,51 @@ +import {Inject} from '@nestjs/common'; +import {ActorRepository} from './actor.repository'; +import ObjectID from 'bson-objectid'; + +export class JSONLDService { + constructor( + @Inject('ActorRepository') private repository: ActorRepository, + @Inject('ActivityPubBaseURL') private url: URL + ) {} + + async getActor(id: ObjectID) { + const actor = await this.repository.getOne(id); + return actor?.getJSONLD(this.url); + } + + async getPublicKey(owner: ObjectID) { + const actor = await this.repository.getOne(owner); + return actor?.getJSONLD(this.url).publicKey; + } + + 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: 1, + orderedItems: [{ + type: 'Create', + actor: json.id, + to: [ + 'https://www.w3.org/ns/activitystreams#Public' + ], + object: { + type: 'Note', + name: 'My First Note', + content: '

Hello, world!

', + attributedTo: json.id, + to: [ + 'https://www.w3.org/ns/activitystreams#Public' + ] + } + }] + }; + } +} diff --git a/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts b/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts new file mode 100644 index 0000000000..e618aa08e6 --- /dev/null +++ b/ghost/ghost/src/http/admin/controllers/activitypub.controller.ts @@ -0,0 +1,38 @@ +import {Controller, Get, Query} from '@nestjs/common'; +import {Roles} from '../../../common/decorators/permissions.decorator'; +import ObjectID from 'bson-objectid'; +import {JSONLDService} from '../../../core/activitypub/jsonld.service'; + +@Controller('activitypub') +export class ActivityPubController { + constructor( + private readonly service: JSONLDService + ) {} + + @Roles(['Anon']) + @Get('actor') + async getActor(@Query('id') id: unknown) { + if (typeof id !== 'string') { + throw new Error('Bad Request'); + } + return this.service.getActor(ObjectID.createFromHexString(id)); + } + + @Roles(['Anon']) + @Get('key') + async getKey(@Query('owner') owner: unknown) { + if (typeof owner !== 'string') { + throw new Error('Bad Request'); + } + return this.service.getPublicKey(ObjectID.createFromHexString(owner)); + } + + @Roles(['Anon']) + @Get('outbox') + async getOutbox(@Query('owner') owner: unknown) { + if (typeof owner !== 'string') { + throw new Error('Bad Request'); + } + return this.service.getOutbox(ObjectID.createFromHexString(owner)); + } +} diff --git a/ghost/ghost/src/nestjs/modules/admin-api.module.ts b/ghost/ghost/src/nestjs/modules/admin-api.module.ts index 10fd032368..2b70bf05b8 100644 --- a/ghost/ghost/src/nestjs/modules/admin-api.module.ts +++ b/ghost/ghost/src/nestjs/modules/admin-api.module.ts @@ -3,13 +3,15 @@ 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 {ActorRepositoryInMemory} from '../../db/in-memory/actor.repository.in-memory'; +import {ActivityPubController} from '../../http/admin/controllers/activitypub.controller'; import {WebFingerService} from '../../core/activitypub/webfinger.service'; +import {JSONLDService} from '../../core/activitypub/jsonld.service'; class AdminAPIModuleClass {} export const AdminAPIModule: DynamicModule = { module: AdminAPIModuleClass, - controllers: [ExampleController], + controllers: [ExampleController, ActivityPubController], exports: [ExampleService, 'WebFingerService'], providers: [ ExampleService, @@ -22,6 +24,7 @@ export const AdminAPIModule: DynamicModule = { }, { provide: 'WebFingerService', useClass: WebFingerService - } + }, + JSONLDService ] };