Skip to content

Commit

Permalink
Refactoring handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
allouis committed Aug 4, 2024
1 parent b0d6c62 commit e24a356
Show file tree
Hide file tree
Showing 3 changed files with 158 additions and 101 deletions.
61 changes: 59 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import {
Update,
Announce,
Context,
Actor,
RequestContext,
} from '@fedify/fedify';
import { federation } from '@fedify/fedify/x/hono';
import { Hono, Context as HonoContext } from 'hono';
Expand Down Expand Up @@ -49,7 +51,8 @@ import {
handleAnnounce,
} from './dispatchers';

import { followAction, inboxHandler, postPublishedWebhook, siteChangedWebhook } from './handlers';
import { followAction, inboxHandler, siteChangedWebhook } from './handlers';
import { PostPublishedHandler } from 'http/webhook-handlers/post.published.handler';

if (process.env.SENTRY_DSN) {
Sentry.init({ dsn: process.env.SENTRY_DSN });
Expand Down Expand Up @@ -249,8 +252,62 @@ app.get('/ping', (ctx) => {
});
});

type MethodType = 'post' | 'get';

interface HandlerConstructor<T> {
new(
context: RequestContext<unknown>,
globaldb: KvStore,
localdb: KvStore,
actor: Actor,
): Handler<T>;
method: MethodType;
url: string;
}

interface Handler<T> {
parse(body: unknown): Promise<T>;
execute(data: T): Promise<Response>;
}

async function mount<T>(
app: Hono<{Variables: HonoContextVariables}>,
handler: HandlerConstructor<T>
) {
app[handler.method](handler.url, async function (ctx, next) {
try {
const context = fedify.createContext(ctx.req.raw, {
globaldb: ctx.get('globaldb'),
db: ctx.get('db'),
});
const actor = await context.getActor('index');
if (!actor) {
throw new Error('Could not find actor');
}
const handler = new PostPublishedHandler(
context,
ctx.get('globaldb'),
ctx.get('db'),
actor,
);
const json = await ctx.req.json();
const data = await handler.parse(json);
return await handler.execute(data);
} catch (error: any) {
return new Response(JSON.stringify({
error: error,
}), {
headers: {
'Content-Type': 'application/json'
},
status: 500
});
}
});
}

app.get('/.ghost/activitypub/inbox/:handle', inboxHandler);
app.post('/.ghost/activitypub/webhooks/post/published', postPublishedWebhook);
mount(app, PostPublishedHandler);
app.post('/.ghost/activitypub/webhooks/site/changed', siteChangedWebhook);
app.post('/.ghost/activitypub/actions/follow/:handle', followAction);

Expand Down
99 changes: 0 additions & 99 deletions src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,47 +26,6 @@ type StoredThing = {
}
}

import z from 'zod';

const PostSchema = z.object({
uuid: z.string().uuid(),
title: z.string(),
html: z.string(),
excerpt: z.string(),
feature_image: z.string().url().nullable(),
published_at: z.string().datetime(),
url: z.string().url()
});

type Post = z.infer<typeof PostSchema>

async function postToArticle(ctx: RequestContext<ContextData>, post: Post) {
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 }),
name: post.title,
content: post.html,
image: toURL(post.feature_image),
published: Temporal.Instant.from(post.published_at),
preview: preview,
url: toURL(post.url),
});

return {
article,
preview,
};
}

export async function followAction(
ctx: Context<{ Variables: HonoContextVariables }>,
) {
Expand Down Expand Up @@ -101,64 +60,6 @@ export async function followAction(
});
}

const PostPublishedWebhookSchema = z.object({
post: z.object({
current: PostSchema
})
});

export async function postPublishedWebhook(
ctx: Context<{ Variables: HonoContextVariables }>,
next: Next,
) {
// TODO: Validate webhook with secret
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'),
});
const { article, preview } = await postToArticle(
apCtx,
data.post.current,
);
if (article) {
const actor = await apCtx.getActor(ACTOR_DEFAULT_HANDLE);
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) {
console.log(err);
}
}
return new Response(JSON.stringify({}), {
headers: {
'Content-Type': 'application/activity+json',
},
status: 200,
});
}

export async function siteChangedWebhook(
ctx: Context<{ Variables: HonoContextVariables }>,
next: Next,
Expand Down
99 changes: 99 additions & 0 deletions src/http/webhook-handlers/post.published.handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Actor, Article, RequestContext, Create, KvStore, Note, PUBLIC_COLLECTION } from '@fedify/fedify';
import z from 'zod';
import { Temporal } from '@js-temporal/polyfill';
import { toURL } from '../../toURL';
import { v4 as uuidv4 } from 'uuid';
import { addToList } from 'kv-helpers';

export class PostPublishedHandler {
static method: 'post'
static url: '/.ghost/activitypub/webhooks/post/published'

constructor(
private readonly context: RequestContext<unknown>,
private readonly globaldb: KvStore,
private readonly localdb: KvStore,
private readonly actor: Actor,
) {}

async parse(body: unknown): Promise<PostPublishedWebhook> {
return PostPublishedWebhook.parse(body);
}

async execute(body: PostPublishedWebhook): Promise<Response> {
const [article, preview] = this.postToArticle(body.post.current);

const create = new Create({
actor: this.actor,
object: article,
id: this.context.getObjectUri(Create, { id: uuidv4() }),
to: PUBLIC_COLLECTION,
cc: this.context.getFollowersUri('index'),
});

await this.globaldb.set([preview.id!.href], await preview.toJsonLd());
await this.globaldb.set([create.id!.href], await create.toJsonLd());
await this.globaldb.set([article.id!.href], await article.toJsonLd());

await addToList(this.localdb, ['outbox'], create.id!.href);

await this.context.sendActivity(
{
handle: 'index'
},
'followers',
create,
{
preferSharedInbox: true
}
);

return new Response('OK', {
headers: {
'Content-Type': 'text/plain',
},
status: 200,
});
}

postToArticle(post: Post): [Article, Note] {
const preview = new Note({
id: this.context.getObjectUri(Note, { id: post.uuid }),
content: post.excerpt,
});
const article = new Article({
id: this.context.getObjectUri(Article, { id: post.uuid }),
name: post.title,
content: post.html,
image: toURL(post.feature_image),
published: Temporal.Instant.from(post.published_at),
preview: preview,
url: toURL(post.url),
});

return [
article,
preview,
];
}
}

const Post = z.object({
uuid: z.string().uuid(),
title: z.string(),
html: z.string(),
excerpt: z.string(),
feature_image: z.string().url().nullable(),
published_at: z.string().datetime(),
url: z.string().url()
});

const PostPublishedWebhook = z.object({
post: z.object({
current: Post
})
});

export type Post = z.infer<typeof Post>

export type PostPublishedWebhook = z.infer<typeof PostPublishedWebhook>

0 comments on commit e24a356

Please sign in to comment.