From c4091fc000a7dc5a4bc437b52b063216100c1f6f Mon Sep 17 00:00:00 2001 From: Fabien O'Carroll Date: Thu, 16 May 2024 15:28:32 +0700 Subject: [PATCH] Added more data to Articles ref https://linear.app/tryghost/issue/MOM-128 We want to render more than just the content, so we need to bulk out the Article objects with metadata like feature images etc... --- .../src/common/libraries.defintitions.ts | 1 + .../core/activitypub/activity.service.test.ts | 22 +++++++++++++-- .../src/core/activitypub/actor.entity.test.ts | 7 ++++- .../src/core/activitypub/article.object.ts | 25 ++++++++++++++--- .../src/core/activitypub/post.repository.ts | 6 ++++ ghost/ghost/src/core/activitypub/types.ts | 7 +++-- .../ghost/src/db/knex/post.repository.knex.ts | 28 ++++++++++++++++++- .../activitypub.controller.test.ts | 8 +++++- 8 files changed, 92 insertions(+), 12 deletions(-) diff --git a/ghost/ghost/src/common/libraries.defintitions.ts b/ghost/ghost/src/common/libraries.defintitions.ts index b0a9045a2a..409ccd5543 100644 --- a/ghost/ghost/src/common/libraries.defintitions.ts +++ b/ghost/ghost/src/common/libraries.defintitions.ts @@ -1,2 +1,3 @@ declare module '@tryghost/errors'; declare module '@tryghost/domain-events'; +declare module '@tryghost/html-to-plaintext'; diff --git a/ghost/ghost/src/core/activitypub/activity.service.test.ts b/ghost/ghost/src/core/activitypub/activity.service.test.ts index accc89da85..2e4d4a5213 100644 --- a/ghost/ghost/src/core/activitypub/activity.service.test.ts +++ b/ghost/ghost/src/core/activitypub/activity.service.test.ts @@ -2,6 +2,7 @@ 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 () { @@ -20,7 +21,12 @@ describe('ActivityService', function () { title: 'Testing', slug: 'testing', html: '

Testing stuff..

', - visibility: 'public' + visibility: 'public', + authors: ['Mr Bean'], + publishedAt: new Date(), + featuredImage: null, + excerpt: 'Small text', + url: new URI('blah') }; } }; @@ -53,7 +59,12 @@ describe('ActivityService', function () { title: 'Testing', slug: 'testing', html: '

Testing stuff..

', - visibility: 'private' + visibility: 'private', + authors: ['Mr Bean'], + publishedAt: new Date(), + featuredImage: null, + excerpt: 'Small text', + url: new URI('blah') }; } }; @@ -110,7 +121,12 @@ describe('ActivityService', function () { title: 'Testing', slug: 'testing', html: '

Testing stuff..

', - visibility: 'private' + visibility: 'private', + authors: ['Mr Bean'], + publishedAt: new Date(), + featuredImage: null, + excerpt: 'Small text', + url: new URI('blah') }; } }; diff --git a/ghost/ghost/src/core/activitypub/actor.entity.test.ts b/ghost/ghost/src/core/activitypub/actor.entity.test.ts index 30d725a8ca..d4c6650ddf 100644 --- a/ghost/ghost/src/core/activitypub/actor.entity.test.ts +++ b/ghost/ghost/src/core/activitypub/actor.entity.test.ts @@ -36,7 +36,12 @@ describe('Actor', function () { title: 'Post Title', slug: 'post-slug', html: '

Hello world

', - visibility: 'public' + visibility: 'public', + url: new URI(''), + authors: ['Mr Burns'], + featuredImage: null, + publishedAt: null, + excerpt: 'Hey' }); actor.createArticle(article); diff --git a/ghost/ghost/src/core/activitypub/article.object.ts b/ghost/ghost/src/core/activitypub/article.object.ts index 5b6667f2a2..950388ea7b 100644 --- a/ghost/ghost/src/core/activitypub/article.object.ts +++ b/ghost/ghost/src/core/activitypub/article.object.ts @@ -7,7 +7,11 @@ type ArticleData = { id: ObjectID name: string content: string - url: URL + url: URI + image: URI | null + published: Date | null + attributedTo: {type: string, name: string}[] + preview: {type: string, content: string} }; export class Article { @@ -30,8 +34,11 @@ export class Article { id: id.href, name: this.attr.name, content: this.attr.content, - url: this.attr.url.href, - attributedTo: url.href + 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 }; } @@ -40,7 +47,17 @@ export class Article { id: post.id, name: post.title, content: post.html, - url: new URL(`/posts/${post.slug}`, 'https://example.com') + 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/post.repository.ts b/ghost/ghost/src/core/activitypub/post.repository.ts index da66e99369..0a18559c8c 100644 --- a/ghost/ghost/src/core/activitypub/post.repository.ts +++ b/ghost/ghost/src/core/activitypub/post.repository.ts @@ -1,4 +1,5 @@ import ObjectID from 'bson-objectid'; +import {URI} from './uri.object'; export type Post = { id: ObjectID; @@ -6,6 +7,11 @@ export type Post = { slug: string; html: string; visibility: string; + featuredImage: URI | null; + url: URI; + publishedAt: Date | null; + authors: string[]; + excerpt: string; }; export interface PostRepository { diff --git a/ghost/ghost/src/core/activitypub/types.ts b/ghost/ghost/src/core/activitypub/types.ts index 8b6046d7c3..a8328c1c98 100644 --- a/ghost/ghost/src/core/activitypub/types.ts +++ b/ghost/ghost/src/core/activitypub/types.ts @@ -50,8 +50,11 @@ export namespace ActivityPub { type: 'Article'; name: string; content: string; - url: string; - attributedTo: string | object[]; + url?: string; + attributedTo?: string | object[]; + image?: string; + published?: string; + preview?: {type: string, content: string}; }; export type Link = string | { diff --git a/ghost/ghost/src/db/knex/post.repository.knex.ts b/ghost/ghost/src/db/knex/post.repository.knex.ts index c010247e6d..2c3562d25c 100644 --- a/ghost/ghost/src/db/knex/post.repository.knex.ts +++ b/ghost/ghost/src/db/knex/post.repository.knex.ts @@ -1,6 +1,8 @@ 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 @@ -18,15 +20,39 @@ export class KnexPostRepository implements PostRepository { 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 + 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/frontend/controllers/activitypub.controller.test.ts b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts index 6a686d6e7f..9ad14fa81c 100644 --- a/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts +++ b/ghost/ghost/src/http/frontend/controllers/activitypub.controller.test.ts @@ -14,6 +14,7 @@ import {TheWorld} from '../../../core/activitypub/tell-the-world.service'; import DomainEvents from '@tryghost/domain-events'; import {NestApplication} from '@nestjs/core'; import ObjectID from 'bson-objectid'; +import {URI} from '../../../core/activitypub/uri.object'; describe('ActivityPubController', function () { let app: NestApplication; @@ -58,7 +59,12 @@ describe('ActivityPubController', function () { title: 'Testing', slug: 'testing', html: '

testing

', - visibility: 'public' + visibility: 'public', + authors: ['Mr Roach'], + url: new URI('roachie'), + publishedAt: new Date(), + featuredImage: null, + excerpt: 'testing...' }; } }