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:
parent
d592b1e9c9
commit
299f7c408e
6 changed files with 144 additions and 21 deletions
12
ghost/ghost/src/core/activitypub/activity.event.ts
Normal file
12
ghost/ghost/src/core/activitypub/activity.event.ts
Normal 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});
|
||||
}
|
||||
}
|
27
ghost/ghost/src/core/activitypub/activity.object.ts
Normal file
27
ghost/ghost/src/core/activitypub/activity.object.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
49
ghost/ghost/src/core/activitypub/article.object.ts
Normal file
49
ghost/ghost/src/core/activitypub/article.object.ts
Normal 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')
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue