0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-03-11 02:12:21 -05:00

Added very basic Outbox for Actors

ref https://linear.app/tryghost/issue/MOM-28
ref https://linear.app/tryghost/issue/MOM-29
ref https://linear.app/tryghost/issue/MOM-30

Basic wire up of Create Activities, Articles for Posts & Actor's Outbox!
I'd definitely like to rethink the whole storage layer and how we split things
out - I think separating the Outbox from the Actor would make sense, otherwise
the size of thsi is gonna grow, or we're gonna have to deal with sub-pagination.
This commit is contained in:
Fabien O'Carroll 2024-04-23 15:38:30 +07:00 committed by Fabien 'egg' O'Carroll
parent d592b1e9c9
commit 299f7c408e
6 changed files with 144 additions and 21 deletions

View file

@ -0,0 +1,12 @@
import {BaseEvent} from '../../common/event.base';
import {Activity} from './activity.object';
type ActivityEventData = {
activity: Activity
}
export class ActivityEvent extends BaseEvent<ActivityEventData> {
static create(activity: Activity) {
return new ActivityEvent({activity});
}
}

View file

@ -0,0 +1,27 @@
import {Actor} from './actor.entity';
import {Article} from './article.object';
import {ActivityPub} from './types';
type ActivityData = {
type: ActivityPub.ActivityType;
actor: Actor;
object: Article
}
export class Activity {
constructor(private readonly attr: ActivityData) {}
getJSONLD(url: URL): ActivityPub.Activity {
const actor = this.attr.actor.getJSONLD(url);
const object = this.attr.object.getJSONLD(url);
return {
'@context': 'https://www.w3.org/ns/activitystreams',
id: null,
type: 'Create',
summary: `${actor.name} created an ${object.type.toLowerCase()}.`,
actor: actor.id,
object: object.id
};
}
}

View file

@ -1,11 +1,15 @@
import ObjectID from 'bson-objectid';
import {Entity} from '../../common/entity.base';
import {ActivityPub} from './types';
import {Activity} from './activity.object';
import {Article} from './article.object';
import {ActivityEvent} from './activity.event';
type ActorData = {
username: string;
publicKey: string;
privateKey: string;
outbox: Activity[];
};
type CreateActorData = ActorData & {
@ -17,6 +21,29 @@ export class Actor extends Entity<ActorData> {
return this.attr.username;
}
get outbox() {
return this.attr.outbox;
}
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({
type: 'Create',
actor: this,
object: article
});
this.attr.outbox.push(activity);
this.activities.push(activity);
this.addEvent(ActivityEvent.create(activity));
}
getJSONLD(url: URL): ActivityPub.Actor & ActivityPub.RootObject {
if (!url.href.endsWith('/')) {
url.href += '/';
@ -95,7 +122,8 @@ export class Actor extends Entity<ActorData> {
id: data.id instanceof ObjectID ? data.id : undefined,
username: data.username,
publicKey: data.publicKey,
privateKey: data.privateKey
privateKey: data.privateKey,
outbox: data.outbox
});
}
}

View file

@ -0,0 +1,49 @@
import ObjectID from 'bson-objectid';
import {ActivityPub} from './types';
type ArticleData = {
id: ObjectID
name: string
content: string
url: URL
};
type Post = {
id: ObjectID;
title: string;
slug: string;
html: string;
lexical: string;
status: 'draft' | 'published' | 'scheduled' | 'sent';
};
export class Article {
constructor(private readonly attr: ArticleData) {}
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.href,
attributedTo: url.href
};
}
static fromPost(post: Post) {
return new Article({
id: post.id,
name: post.title,
content: post.html,
url: new URL(`/posts/${post.slug}`, 'https://example.com')
});
}
}

View file

@ -24,23 +24,8 @@ export class JSONLDService {
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: '<p>Hello, world!</p>',
attributedTo: json.id,
to: [
'https://www.w3.org/ns/activitystreams#Public'
]
}
}]
totalItems: actor.outbox.length,
orderedItems: actor.outbox.map(activity => activity.getJSONLD(this.url))
};
}
}

View file

@ -3,19 +3,31 @@ 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';
import {Activity} from '../../core/activitypub/activity.object';
interface DomainEvents {
dispatch(event: unknown): void
}
export class ActorRepositoryInMemory implements ActorRepository {
actors: Actor[];
constructor(@Inject('SettingsCache') settingsCache: SettingsCache) {
private readonly domainEvents: DomainEvents;
constructor(
@Inject('SettingsCache') settingsCache: SettingsCache,
@Inject('DomainEvents') domainEvents: DomainEvents
) {
this.actors = [
Actor.create({
id: ObjectID.createFromHexString('deadbeefdeadbeefdeadbeef'),
username: 'index',
publicKey: settingsCache.get('ghost_public_key'),
privateKey: settingsCache.get('ghost_private_key')
privateKey: settingsCache.get('ghost_private_key'),
outbox: []
})
];
this.domainEvents = domainEvents;
}
private getOneByUsername(username: string) {
@ -36,6 +48,16 @@ export class ActorRepositoryInMemory implements ActorRepository {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async save(actor: Actor) {
throw new Error('Not Implemented');
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);
}
});
}
}