From 5638259defba16169d84e867e4f62e1f2a1e6451 Mon Sep 17 00:00:00 2001 From: Michael Barrett Date: Thu, 9 Jan 2025 15:44:41 +0000 Subject: [PATCH] Moved webhook actions to API no refs Moved webhook actions to API for easier maintenance and consistency with other API actions --- src/api/action/webhook/post-published.ts | 133 ++++++++++++++++++++ src/api/action/webhook/site-changed.ts | 35 ++++++ src/api/index.ts | 2 + src/app.ts | 8 +- src/handlers.ts | 149 +---------------------- 5 files changed, 176 insertions(+), 151 deletions(-) create mode 100644 src/api/action/webhook/post-published.ts create mode 100644 src/api/action/webhook/site-changed.ts diff --git a/src/api/action/webhook/post-published.ts b/src/api/action/webhook/post-published.ts new file mode 100644 index 00000000..8a5fc4eb --- /dev/null +++ b/src/api/action/webhook/post-published.ts @@ -0,0 +1,133 @@ +import { + type Actor, + Article, + Create, + Note, + PUBLIC_COLLECTION, + type RequestContext, +} from '@fedify/fedify'; +import { Temporal } from '@js-temporal/polyfill'; +import type { Context, Next } from 'hono'; +import { v4 as uuidv4 } from 'uuid'; +import { z } from 'zod'; + +import { + type ContextData, + type HonoContextVariables, + fedify, +} from '../../../app'; +import { ACTOR_DEFAULT_HANDLE } from '../../../constants'; +import { toURL } from '../../../helpers/uri'; +import { addToList } from '../../../kv-helpers'; + +const PostSchema = z.object({ + uuid: z.string().uuid(), + title: z.string(), + html: z.string().nullable(), + excerpt: z.string().nullable(), + feature_image: z.string().url().nullable(), + published_at: z.string().datetime(), + url: z.string().url(), +}); + +type Post = z.infer; + +async function postToArticle( + ctx: RequestContext, + post: Post, + author: Actor | null, +) { + if (!post) { + return { + article: null, + preview: null, + }; + } + const preview = new Note({ + id: ctx.getObjectUri(Note, { id: post.uuid }), + content: post.excerpt, + }); + const article = new Article({ + id: ctx.getObjectUri(Article, { id: post.uuid }), + attribution: author, + name: post.title, + content: post.html, + image: toURL(post.feature_image), + published: Temporal.Instant.from(post.published_at), + preview: preview, + url: toURL(post.url), + to: PUBLIC_COLLECTION, + cc: ctx.getFollowersUri(ACTOR_DEFAULT_HANDLE), + }); + + return { + article, + preview, + }; +} + +const PostPublishedWebhookSchema = z.object({ + post: z.object({ + current: PostSchema, + }), +}); + +export async function webookPostPublishedAction( + ctx: Context<{ Variables: HonoContextVariables }>, + next: Next, +) { + const data = PostPublishedWebhookSchema.parse( + (await ctx.req.json()) as unknown, + ); + const apCtx = fedify.createContext(ctx.req.raw as Request, { + db: ctx.get('db'), + globaldb: ctx.get('globaldb'), + logger: ctx.get('logger'), + }); + const actor = await apCtx.getActor(ACTOR_DEFAULT_HANDLE); + const { article, preview } = await postToArticle( + apCtx, + data.post.current, + actor, + ); + if (article) { + const create = new Create({ + actor, + object: article, + id: apCtx.getObjectUri(Create, { id: uuidv4() }), + to: PUBLIC_COLLECTION, + cc: apCtx.getFollowersUri('index'), + }); + try { + await article.toJsonLd(); + await ctx + .get('globaldb') + .set([preview.id!.href], await preview.toJsonLd()); + await ctx + .get('globaldb') + .set([create.id!.href], await create.toJsonLd()); + await ctx + .get('globaldb') + .set([article.id!.href], await article.toJsonLd()); + await addToList(ctx.get('db'), ['outbox'], create.id!.href); + await apCtx.sendActivity( + { handle: ACTOR_DEFAULT_HANDLE }, + 'followers', + create, + { + preferSharedInbox: true, + }, + ); + } catch (err) { + ctx.get('logger').error('Post published webhook failed: {error}', { + error: err, + }); + } + } + return new Response(JSON.stringify({}), { + headers: { + 'Content-Type': 'application/activity+json', + }, + status: 200, + }); +} diff --git a/src/api/action/webhook/site-changed.ts b/src/api/action/webhook/site-changed.ts new file mode 100644 index 00000000..374fc5b5 --- /dev/null +++ b/src/api/action/webhook/site-changed.ts @@ -0,0 +1,35 @@ +import type { Context } from 'hono'; + +import { type HonoContextVariables, fedify } from '../../../app'; +import { updateSiteActor } from '../../../helpers/activitypub/actor'; +import { getSiteSettings } from '../../../helpers/ghost'; + +export async function webhookSiteChangedAction( + ctx: Context<{ Variables: HonoContextVariables }>, +) { + try { + const host = ctx.req.header('host') || ''; + const db = ctx.get('db'); + const globaldb = ctx.get('globaldb'); + const logger = ctx.get('logger'); + + const apCtx = fedify.createContext(ctx.req.raw as Request, { + db, + globaldb, + logger, + }); + + await updateSiteActor(apCtx, getSiteSettings); + } catch (err) { + ctx.get('logger').error('Site changed webhook failed: {error}', { + error: err, + }); + } + + return new Response(JSON.stringify({}), { + headers: { + 'Content-Type': 'application/activity+json', + }, + status: 200, + }); +} diff --git a/src/api/index.ts b/src/api/index.ts index 860988a8..ff870f1d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -5,3 +5,5 @@ export { profileGetFollowersAction } from './action/profile/get-followers'; export { profileGetFollowingAction } from './action/profile/get-following'; export { profileGetPostsAction } from './action/profile/get-posts'; export { searchAction } from './action/search'; +export { webookPostPublishedAction } from './action/webhook/post-published'; +export { webhookSiteChangedAction } from './action/webhook/site-changed'; diff --git a/src/app.ts b/src/app.ts index decd58c3..f2912d26 100644 --- a/src/app.ts +++ b/src/app.ts @@ -43,6 +43,8 @@ import { profileGetFollowingAction, profileGetPostsAction, searchAction, + webhookSiteChangedAction, + webookPostPublishedAction, } from './api'; import { client, getSite } from './db'; import { @@ -82,9 +84,7 @@ import { inboxHandler, likeAction, noteAction, - postPublishedWebhook, replyAction, - siteChangedWebhook, unlikeAction, } from './handlers'; import { getTraceContext } from './helpers/context-header'; @@ -667,12 +667,12 @@ function validateWebhook() { app.post( '/.ghost/activitypub/webhooks/post/published', validateWebhook(), - spanWrapper(postPublishedWebhook), + spanWrapper(webookPostPublishedAction), ); app.post( '/.ghost/activitypub/webhooks/site/changed', validateWebhook(), - spanWrapper(siteChangedWebhook), + spanWrapper(webhookSiteChangedAction), ); function requireRole(role: GhostRole) { diff --git a/src/handlers.ts b/src/handlers.ts index 0e5687de..d98ec7ac 100644 --- a/src/handlers.ts +++ b/src/handlers.ts @@ -1,28 +1,25 @@ import { createHash } from 'node:crypto'; import { type Actor, - Article, Create, Follow, Like, Mention, Note, PUBLIC_COLLECTION, - type RequestContext, Undo, isActor, } from '@fedify/fedify'; import { Temporal } from '@js-temporal/polyfill'; -import type { Context, Next } from 'hono'; +import type { Context } from 'hono'; import { v4 as uuidv4 } from 'uuid'; -import { type ContextData, type HonoContextVariables, fedify } from './app'; +import { type HonoContextVariables, fedify } from './app'; import { ACTOR_DEFAULT_HANDLE } from './constants'; import { buildActivity, prepareNoteContent, } from './helpers/activitypub/activity'; import { escapeHtml } from './helpers/html'; -import { toURL } from './helpers/uri'; import { getUserData } from './helpers/user'; import { addToList, removeFromList } from './kv-helpers'; import { lookupActor, lookupObject } from './lookup-helpers'; @@ -32,52 +29,6 @@ import { getSite } from './db'; import { updateSiteActor } from './helpers/activitypub/actor'; import { getSiteSettings } from './helpers/ghost'; -const PostSchema = z.object({ - uuid: z.string().uuid(), - title: z.string(), - html: z.string().nullable(), - excerpt: z.string().nullable(), - feature_image: z.string().url().nullable(), - published_at: z.string().datetime(), - url: z.string().url(), -}); - -type Post = z.infer; - -async function postToArticle( - ctx: RequestContext, - post: Post, - author: Actor | null, -) { - if (!post) { - return { - article: null, - preview: null, - }; - } - const preview = new Note({ - id: ctx.getObjectUri(Note, { id: post.uuid }), - content: post.excerpt, - }); - const article = new Article({ - id: ctx.getObjectUri(Article, { id: post.uuid }), - attribution: author, - name: post.title, - content: post.html, - image: toURL(post.feature_image), - published: Temporal.Instant.from(post.published_at), - preview: preview, - url: toURL(post.url), - to: PUBLIC_COLLECTION, - cc: ctx.getFollowersUri(ACTOR_DEFAULT_HANDLE), - }); - - return { - article, - preview, - }; -} - export async function unlikeAction( ctx: Context<{ Variables: HonoContextVariables }>, ) { @@ -491,72 +442,6 @@ export async function followAction( }); } -const PostPublishedWebhookSchema = z.object({ - post: z.object({ - current: PostSchema, - }), -}); - -export async function postPublishedWebhook( - ctx: Context<{ Variables: HonoContextVariables }>, - next: Next, -) { - const data = PostPublishedWebhookSchema.parse( - (await ctx.req.json()) as unknown, - ); - const apCtx = fedify.createContext(ctx.req.raw as Request, { - db: ctx.get('db'), - globaldb: ctx.get('globaldb'), - logger: ctx.get('logger'), - }); - const actor = await apCtx.getActor(ACTOR_DEFAULT_HANDLE); - const { article, preview } = await postToArticle( - apCtx, - data.post.current, - actor, - ); - if (article) { - const create = new Create({ - actor, - object: article, - id: apCtx.getObjectUri(Create, { id: uuidv4() }), - to: PUBLIC_COLLECTION, - cc: apCtx.getFollowersUri('index'), - }); - try { - await article.toJsonLd(); - await ctx - .get('globaldb') - .set([preview.id!.href], await preview.toJsonLd()); - await ctx - .get('globaldb') - .set([create.id!.href], await create.toJsonLd()); - await ctx - .get('globaldb') - .set([article.id!.href], await article.toJsonLd()); - await addToList(ctx.get('db'), ['outbox'], create.id!.href); - await apCtx.sendActivity( - { handle: ACTOR_DEFAULT_HANDLE }, - 'followers', - create, - { - preferSharedInbox: true, - }, - ); - } catch (err) { - ctx.get('logger').error('Post published webhook failed: {error}', { - error: err, - }); - } - } - return new Response(JSON.stringify({}), { - headers: { - 'Content-Type': 'application/activity+json', - }, - status: 200, - }); -} - export async function getSiteDataHandler( ctx: Context<{ Variables: HonoContextVariables }>, ) { @@ -591,36 +476,6 @@ export async function getSiteDataHandler( }); } -export async function siteChangedWebhook( - ctx: Context<{ Variables: HonoContextVariables }>, -) { - try { - const host = ctx.req.header('host') || ''; - const db = ctx.get('db'); - const globaldb = ctx.get('globaldb'); - const logger = ctx.get('logger'); - - const apCtx = fedify.createContext(ctx.req.raw as Request, { - db, - globaldb, - logger, - }); - - await updateSiteActor(apCtx, getSiteSettings); - } catch (err) { - ctx.get('logger').error('Site changed webhook failed: {error}', { - error: err, - }); - } - - return new Response(JSON.stringify({}), { - headers: { - 'Content-Type': 'application/activity+json', - }, - status: 200, - }); -} - export async function inboxHandler( ctx: Context<{ Variables: HonoContextVariables }>, ) {