mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
Initial wire up of Posts -> Outbox flow
ref https://linear.app/tryghost/issue/MOM-29 This is very rough, and all still behind a flag. The idea is that any public post which is published gets added to the Outbox of the site Actor. We also dispatch an event, which will be used to deliver the Activity to any relevant inboxes, but that is outside the scope of this commit.
This commit is contained in:
parent
e01c9cb546
commit
af02ca7044
@ -408,6 +408,12 @@ async function initNestDependencies() {
|
|||||||
}, {
|
}, {
|
||||||
provide: 'SettingsCache',
|
provide: 'SettingsCache',
|
||||||
useValue: require('./shared/settings-cache')
|
useValue: require('./shared/settings-cache')
|
||||||
|
}, {
|
||||||
|
provide: 'knex',
|
||||||
|
useValue: require('./server/data/db').knex
|
||||||
|
}, {
|
||||||
|
provide: 'UrlUtils',
|
||||||
|
useValue: require('./shared/url-utils')
|
||||||
});
|
});
|
||||||
for (const provider of providers) {
|
for (const provider of providers) {
|
||||||
GhostNestApp.addProvider(provider);
|
GhostNestApp.addProvider(provider);
|
||||||
|
36
ghost/ghost/src/core/activitypub/activity.service.ts
Normal file
36
ghost/ghost/src/core/activitypub/activity.service.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
import {ActivityPub} from './types';
|
import {ActivityPub} from './types';
|
||||||
|
import {Post} from './post.repository';
|
||||||
|
|
||||||
type ArticleData = {
|
type ArticleData = {
|
||||||
id: ObjectID
|
id: ObjectID
|
||||||
@ -8,15 +9,6 @@ type ArticleData = {
|
|||||||
url: URL
|
url: URL
|
||||||
};
|
};
|
||||||
|
|
||||||
type Post = {
|
|
||||||
id: ObjectID;
|
|
||||||
title: string;
|
|
||||||
slug: string;
|
|
||||||
html: string;
|
|
||||||
lexical: string;
|
|
||||||
status: 'draft' | 'published' | 'scheduled' | 'sent';
|
|
||||||
};
|
|
||||||
|
|
||||||
export class Article {
|
export class Article {
|
||||||
constructor(private readonly attr: ArticleData) {}
|
constructor(private readonly attr: ArticleData) {}
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
import {Inject} from '@nestjs/common';
|
import {Inject} from '@nestjs/common';
|
||||||
import {ActorRepository} from './actor.repository';
|
import {ActorRepository} from './actor.repository';
|
||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
|
import {PostRepository} from './post.repository';
|
||||||
|
import {Article} from './article.object';
|
||||||
|
|
||||||
export class JSONLDService {
|
export class JSONLDService {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject('ActorRepository') private repository: ActorRepository,
|
@Inject('ActorRepository') private repository: ActorRepository,
|
||||||
|
@Inject('PostRepository') private postRepository: PostRepository,
|
||||||
@Inject('ActivityPubBaseURL') private url: URL
|
@Inject('ActivityPubBaseURL') private url: URL
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -28,4 +31,15 @@ export class JSONLDService {
|
|||||||
orderedItems: actor.outbox.map(activity => activity.getJSONLD(this.url))
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
13
ghost/ghost/src/core/activitypub/post.repository.ts
Normal file
13
ghost/ghost/src/core/activitypub/post.repository.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import ObjectID from 'bson-objectid';
|
||||||
|
|
||||||
|
export type Post = {
|
||||||
|
id: ObjectID;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
html: string;
|
||||||
|
visibility: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface PostRepository {
|
||||||
|
getOne(id: ObjectID): Promise<Post | null>
|
||||||
|
}
|
@ -3,7 +3,6 @@ import {ActorRepository} from '../../core/activitypub/actor.repository';
|
|||||||
import ObjectID from 'bson-objectid';
|
import ObjectID from 'bson-objectid';
|
||||||
import {Inject} from '@nestjs/common';
|
import {Inject} from '@nestjs/common';
|
||||||
import {SettingsCache} from '../../common/types/settings-cache.type';
|
import {SettingsCache} from '../../common/types/settings-cache.type';
|
||||||
import {Activity} from '../../core/activitypub/activity.object';
|
|
||||||
|
|
||||||
interface DomainEvents {
|
interface DomainEvents {
|
||||||
dispatch(event: unknown): void
|
dispatch(event: unknown): void
|
||||||
|
32
ghost/ghost/src/db/knex/post.repository.knex.ts
Normal file
32
ghost/ghost/src/db/knex/post.repository.knex.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import {Inject} from '@nestjs/common';
|
||||||
|
import ObjectID from 'bson-objectid';
|
||||||
|
import {PostRepository} from '../../core/activitypub/post.repository';
|
||||||
|
|
||||||
|
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();
|
||||||
|
if (!row) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title: row.title,
|
||||||
|
html: this.urlUtils.transformReadyToAbsolute(row.html),
|
||||||
|
slug: row.slug,
|
||||||
|
visibility: row.visibility
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
@ -30,4 +30,15 @@ export class ActivityPubController {
|
|||||||
}
|
}
|
||||||
return this.service.getOutbox(ObjectID.createFromHexString(owner));
|
return this.service.getOutbox(ObjectID.createFromHexString(owner));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Header('Cache-Control', 'no-store')
|
||||||
|
@Header('Content-Type', 'application/activity+json')
|
||||||
|
@Roles(['Anon'])
|
||||||
|
@Get('article/:id')
|
||||||
|
async getArticle(@Param('id') id: unknown) {
|
||||||
|
if (typeof id !== 'string') {
|
||||||
|
throw new Error('Bad Request');
|
||||||
|
}
|
||||||
|
return this.service.getArticle(ObjectID.createFromHexString(id));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import {ActivityPubController} from '../../http/frontend/controllers/activitypub
|
|||||||
import {WebFingerService} from '../../core/activitypub/webfinger.service';
|
import {WebFingerService} from '../../core/activitypub/webfinger.service';
|
||||||
import {JSONLDService} from '../../core/activitypub/jsonld.service';
|
import {JSONLDService} from '../../core/activitypub/jsonld.service';
|
||||||
import {WebFingerController} from '../../http/frontend/controllers/webfinger.controller';
|
import {WebFingerController} from '../../http/frontend/controllers/webfinger.controller';
|
||||||
|
import {ActivityService} from '../../core/activitypub/activity.service';
|
||||||
|
import {KnexPostRepository} from '../../db/knex/post.repository.knex';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
controllers: [ActivityPubController, WebFingerController],
|
controllers: [ActivityPubController, WebFingerController],
|
||||||
@ -13,6 +15,14 @@ import {WebFingerController} from '../../http/frontend/controllers/webfinger.con
|
|||||||
provide: 'ActorRepository',
|
provide: 'ActorRepository',
|
||||||
useClass: ActorRepositoryInMemory
|
useClass: ActorRepositoryInMemory
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: 'ActivityService',
|
||||||
|
useClass: ActivityService
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: 'PostRepository',
|
||||||
|
useClass: KnexPostRepository
|
||||||
|
},
|
||||||
WebFingerService,
|
WebFingerService,
|
||||||
JSONLDService
|
JSONLDService
|
||||||
]
|
]
|
||||||
|
@ -12,6 +12,8 @@ const {
|
|||||||
PostsBulkUnfeaturedEvent,
|
PostsBulkUnfeaturedEvent,
|
||||||
PostsBulkAddTagsEvent
|
PostsBulkAddTagsEvent
|
||||||
} = require('@tryghost/post-events');
|
} = require('@tryghost/post-events');
|
||||||
|
const GhostNestApp = require('@tryghost/ghost');
|
||||||
|
const {default: ObjectID} = require('bson-objectid');
|
||||||
|
|
||||||
const messages = {
|
const messages = {
|
||||||
invalidVisibilityFilter: 'Invalid visibility filter.',
|
invalidVisibilityFilter: 'Invalid visibility filter.',
|
||||||
@ -207,6 +209,13 @@ class PostsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.isSet('ActivityPub')) {
|
||||||
|
if (model.previous('status') !== model.get('status') && model.get('status') === 'published') {
|
||||||
|
const activityService = await GhostNestApp.resolve('ActivityService');
|
||||||
|
await activityService.createArticleForPost(ObjectID.createFromHexString(model.id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (typeof options?.eventHandler === 'function') {
|
if (typeof options?.eventHandler === 'function') {
|
||||||
await options.eventHandler(this.getChanges(model), dto);
|
await options.eventHandler(this.getChanges(model), dto);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user