Skip to content

Commit

Permalink
ensure only public posts are published to fediverse
Browse files Browse the repository at this point in the history
  • Loading branch information
mike182uk committed Jan 6, 2025
1 parent 46912ed commit 5d6f459
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 30 deletions.
8 changes: 8 additions & 0 deletions features/create-article-from-post.feature
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ Feature: Deliver Create(Article) activities when a post.published webhook is rec
Given a valid "post.published" webhook
When it is sent to the webhook endpoint without a signature
Then the request is rejected with a 401

Scenario: We recieve a webhook for the post.published event with a non-public post
Given a valid "post.published" webhook:
| property | value |
| post.current.visibility | paid |
When it is sent to the webhook endpoint
Then the request is accepted
Then a "Create(Article)" activity is not in the Outbox
50 changes: 48 additions & 2 deletions features/step_definitions/stepdefs.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Then,
When,
} from '@cucumber/cucumber';
import { merge } from 'es-toolkit';
import jwt from 'jsonwebtoken';
import Knex from 'knex';
import jose from 'node-jose';
Expand Down Expand Up @@ -1000,7 +1001,7 @@ const webhooks = {
title: 'This is a title.',
html: '<p> This is some content. </p>',
feature_image: null,
visibility: 'paid',
visibility: 'public',
published_at: '1970-01-01T00:00:00.000Z',
url: 'http://fake-external-activitypub/post/',
excerpt: 'This is some content.',
Expand All @@ -1018,9 +1019,28 @@ Given('a valid {string} webhook', function (string) {
this.payloadType = string;
});

Given('a valid {string} webhook:', function (string, properties) {
this.payloadType = string;
this.payloadData = {};

for (const { property, value } of properties.hashes()) {
property.split('.').reduce((acc, key, index, array) => {
if (index === array.length - 1) {
acc[key] = value;
} else {
acc[key] = acc[key] || {};
}
return acc[key];
}, this.payloadData);
}
});

When('it is sent to the webhook endpoint', async function () {
const endpoint = endpoints[this.payloadType];
const payload = webhooks[this.payloadType];
let payload = webhooks[this.payloadType];
if (this.payloadData) {
payload = merge(payload, this.payloadData);
}
const body = JSON.stringify(payload);
const timestamp = Date.now();
const hmac = createHmac('sha256', webhookSecret)
Expand Down Expand Up @@ -1120,6 +1140,32 @@ Then('a {string} activity is in the Outbox', async function (string) {
assert.ok(found);
});

Then('a {string} activity is not in the Outbox', async (string) => {
const [match, activity, object] = string.match(/(\w+)\((\w+)\)/) || [null];
if (!match) {
throw new Error(`Could not match ${string} to an activity`);
}
const initialResponse = await fetchActivityPub(
'http://fake-ghost-activitypub/.ghost/activitypub/outbox/index',
{
headers: {
Accept: 'application/ld+json',
},
},
);
const initialResponseJson = await initialResponse.json();
const firstPageReponse = await fetchActivityPub(initialResponseJson.first, {
headers: {
Accept: 'application/ld+json',
},
});
const outbox = await firstPageReponse.json();
const found = (outbox.orderedItems || []).find((item) => {
return item.type === activity && item.object?.type === object;
});
assert.ok(!found);
});

Then('the found {string} as {string}', function (foundName, name) {
const found = this.found[foundName];

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@opentelemetry/resources": "1.29.0",
"@opentelemetry/sdk-trace-base": "1.29.0",
"@sentry/node": "8.42.0",
"es-toolkit": "1.31.0",
"hono": "4.6.10",
"jsonwebtoken": "9.0.2",
"knex": "3.1.0",
Expand Down
21 changes: 21 additions & 0 deletions src/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ import { getSite } from './db';
import { updateSiteActor } from './helpers/activitypub/actor';
import { getSiteSettings } from './helpers/ghost';

enum PostVisibility {
Public = 'public',
Members = 'members',
Paid = 'paid',
Tiers = 'tiers',
}

const PostSchema = z.object({
uuid: z.string().uuid(),
title: z.string(),
Expand All @@ -40,6 +47,7 @@ const PostSchema = z.object({
feature_image: z.string().url().nullable(),
published_at: z.string().datetime(),
url: z.string().url(),
visibility: z.nativeEnum(PostVisibility),
});

type Post = z.infer<typeof PostSchema>;
Expand Down Expand Up @@ -501,9 +509,22 @@ export async function postPublishedWebhook(
ctx: Context<{ Variables: HonoContextVariables }>,
next: Next,
) {
const logger = ctx.get('logger');
const data = PostPublishedWebhookSchema.parse(
(await ctx.req.json()) as unknown,
);

if (data.post.current.visibility !== PostVisibility.Public) {
logger.info('Post is not public, skipping');

return new Response(JSON.stringify({}), {
headers: {
'Content-Type': 'application/activity+json',
},
status: 200,
});
}

const apCtx = fedify.createContext(ctx.req.raw as Request, {
db: ctx.get('db'),
globaldb: ctx.get('globaldb'),
Expand Down
36 changes: 8 additions & 28 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1995,6 +1995,11 @@ es-module-lexer@^1.5.4:
resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78"
integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==

[email protected]:
version "1.31.0"
resolved "https://registry.yarnpkg.com/es-toolkit/-/es-toolkit-1.31.0.tgz#f4fc1382aea09cb239afa38f3c724a5658ff3163"
integrity sha512-vwS0lv/tzjM2/t4aZZRAgN9I9TP0MSkWuvt6By+hEXfG/uLs8yg2S1/ayRXH/x3pinbLgVJYT+eppueg3cM6tg==

es6-promise@^4.2.8:
version "4.2.8"
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a"
Expand Down Expand Up @@ -3528,16 +3533,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da"
integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==

"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"

string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
Expand All @@ -3562,14 +3558,7 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"

"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"

[email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", [email protected], strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
Expand Down Expand Up @@ -3912,16 +3901,7 @@ [email protected]:
dependencies:
axios "1.7.7"

"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"

wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
Expand Down

0 comments on commit 5d6f459

Please sign in to comment.