-
-
Notifications
You must be signed in to change notification settings - Fork 8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implemented Ghost site changed webhook handler #5
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,10 +8,20 @@ import { | |
Note, | ||
} from '@fedify/fedify'; | ||
import { Context, Next } from 'hono'; | ||
import ky from 'ky'; | ||
import { v4 as uuidv4 } from 'uuid'; | ||
import { addToList } from './kv-helpers'; | ||
import { toURL } from './toURL'; | ||
import { ContextData, HonoContextVariables, fedify } from './app'; | ||
import type { PersonData } from './user'; | ||
|
||
type GhostSiteSettings = { | ||
site: { | ||
description: string; | ||
icon: string; | ||
title: string; | ||
} | ||
} | ||
|
||
async function postToArticle(ctx: RequestContext<ContextData>, post: any) { | ||
if (!post) { | ||
|
@@ -121,6 +131,43 @@ export async function postPublishedWebhook( | |
}); | ||
} | ||
|
||
export async function siteChangedWebhook( | ||
ctx: Context<{ Variables: HonoContextVariables }>, | ||
next: Next, | ||
) { | ||
try { | ||
// Retrieve site settings from Ghost | ||
const host = ctx.req.header('host'); | ||
|
||
const settings = await ky | ||
.get(`https://${host}/ghost/api/admin/site/`) | ||
.json<GhostSiteSettings>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is sus, but not sure what the best thing to do here is as we don't know anything about the requesting client (so am just assuming that the request came from the Ghost instance on that is accessible on the same host 🙃) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, agree this is sus! Maybe something like: async function getGhostSiteSettings(host: string): Promise<GhostSiteSettings> {
const url = new URL(`https://${host}/ghost/api/admin/site/`);
const settings = await ky
.get(`https://${host}/ghost/api/admin/site/`)
.json<unknown>();
// Some assertions here so TS doesn't complain about `unknown`
return {
site: {
description: settings?.site?.description || 'This is a summary',
title: settings?.site?.title || 'index',
icon: settings?.site?.icon || 'https://ghost.org/favicon.ico',
}
};
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The above couples site settings -> default actor data, which isn't great, but it's a step forward. Would be better to have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For me the sus part was using the But good to know you think the rest is sus 😂 Is there a need for the defaults? The retrieved settings are merged with the existing data - If any of the expected settings are not present or 🤔 I liked the typing of the response, but understand that that might not actually be what we get, was optimising for the happy path 😄 We could add a validation step? (i.e There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think typing the response is nice - but we don't actually know what we get, so I was thinking that the lil wrapper gives us the type AND verifies it RE: The Defaults - hmm, I think we should have some defaults - because we don't want empty profiles, I don't think the object spread operator will ignore There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah yeh ofc the |
||
|
||
// Update the database | ||
const handle = 'index'; | ||
const db = ctx.get('db'); | ||
|
||
const current = await db.get<PersonData>(['handle', handle]); | ||
|
||
await db.set(['handle', handle], { | ||
...current, | ||
icon: settings.site.icon, | ||
name: settings.site.title, | ||
summary: settings.site.description, | ||
}); | ||
} catch (err) { | ||
console.log(err); | ||
} | ||
|
||
// Return 200 OK | ||
return new Response(JSON.stringify({}), { | ||
headers: { | ||
'Content-Type': 'application/activity+json', | ||
}, | ||
status: 200, | ||
}); | ||
} | ||
|
||
export async function inboxHandler( | ||
ctx: Context<{ Variables: HonoContextVariables }>, | ||
next: Next, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import { | ||
Image, | ||
RequestContext, | ||
generateCryptoKeyPair, | ||
exportJwk, | ||
importJwk, | ||
} from '@fedify/fedify'; | ||
import { ContextData } from './app'; | ||
|
||
export type PersonData = { | ||
id: string; | ||
name: string; | ||
summary: string; | ||
preferredUsername: string; | ||
icon: string; | ||
inbox: string; | ||
outbox: string; | ||
following: string; | ||
followers: string; | ||
}; | ||
|
||
export async function getUserData(ctx: RequestContext<ContextData>, handle: string) { | ||
const existing = await ctx.data.db.get<PersonData>(['handle', handle]); | ||
|
||
if (existing) { | ||
let icon = null; | ||
try { | ||
icon = new Image({ url: new URL(existing.icon) }); | ||
} catch (err) { | ||
console.log('Could not create Image from Icon value', existing.icon); | ||
console.log(err); | ||
} | ||
return { | ||
id: new URL(existing.id), | ||
name: existing.name, | ||
summary: existing.summary, | ||
preferredUsername: existing.preferredUsername, | ||
icon, | ||
inbox: new URL(existing.inbox), | ||
outbox: new URL(existing.outbox), | ||
following: new URL(existing.following), | ||
followers: new URL(existing.followers), | ||
publicKeys: (await ctx.getActorKeyPairs(handle)).map( | ||
(key) => key.cryptographicKey, | ||
), | ||
}; | ||
} | ||
|
||
const data = { | ||
id: ctx.getActorUri(handle), | ||
name: `Local Ghost site`, | ||
summary: 'This is a summary', | ||
preferredUsername: handle, | ||
icon: new Image({ url: new URL('https://ghost.org/favicon.ico') }), | ||
inbox: ctx.getInboxUri(handle), | ||
outbox: ctx.getOutboxUri(handle), | ||
following: ctx.getFollowingUri(handle), | ||
followers: ctx.getFollowersUri(handle), | ||
publicKeys: (await ctx.getActorKeyPairs(handle)).map( | ||
(key) => key.cryptographicKey, | ||
), | ||
}; | ||
|
||
const dataToStore: PersonData = { | ||
id: data.id.href, | ||
name: data.name, | ||
summary: data.summary, | ||
preferredUsername: data.preferredUsername, | ||
icon: 'https://ghost.org/favicon.ico', | ||
inbox: data.inbox.href, | ||
outbox: data.outbox.href, | ||
following: data.following.href, | ||
followers: data.followers.href, | ||
}; | ||
|
||
await ctx.data.db.set(['handle', handle], data); | ||
|
||
return data; | ||
} | ||
|
||
export async function getUserKeypair(ctx: ContextData, handle: string) { | ||
const existing = await ctx.db.get<{ publicKey: any; privateKey: any }>([ | ||
'keypair', | ||
handle, | ||
]); | ||
|
||
if (existing) { | ||
return { | ||
publicKey: await importJwk(existing.publicKey, 'public'), | ||
privateKey: await importJwk(existing.privateKey, 'private'), | ||
}; | ||
} | ||
|
||
const keys = await generateCryptoKeyPair(); | ||
|
||
const data = { | ||
publicKey: keys.publicKey, | ||
privateKey: keys.privateKey, | ||
}; | ||
|
||
await ctx.db.set(['keypair', handle], { | ||
publicKey: await exportJwk(data.publicKey), | ||
privateKey: await exportJwk(data.privateKey), | ||
}); | ||
|
||
return data; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1412,6 +1412,11 @@ ky-universal@^0.11.0: | |
abort-controller "^3.0.0" | ||
node-fetch "^3.2.10" | ||
|
||
[email protected]: | ||
version "1.4.0" | ||
resolved "https://registry.yarnpkg.com/ky/-/ky-1.4.0.tgz#68b4a71eccfb4177199fe6ee2d5041b50bb41931" | ||
integrity sha512-tPhhoGUiEiU/WXR4rt8klIoLdnTtyu+9jVKHd/wauEjYud32jyn63mzKWQweaQrHWxBQtYoVtdcEnYX1LosnFQ== | ||
|
||
ky@^0.33.3: | ||
version "0.33.3" | ||
resolved "https://registry.yarnpkg.com/ky/-/ky-0.33.3.tgz#bf1ad322a3f2c3428c13cfa4b3af95e6c4a2f543" | ||
|
@@ -1907,16 +1912,7 @@ sqlstring@^2.3.2: | |
resolved "https://registry.yarnpkg.com/sqlstring/-/sqlstring-2.3.3.tgz#2ddc21f03bce2c387ed60680e739922c65751d0c" | ||
integrity sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg== | ||
|
||
"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== | ||
|
@@ -1934,14 +1930,7 @@ string-width@^5.0.1, string-width@^5.1.2: | |
emoji-regex "^9.2.2" | ||
strip-ansi "^7.0.1" | ||
|
||
"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" | ||
|
||
strip-ansi@^6.0.0, strip-ansi@^6.0.1: | ||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", 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== | ||
|
@@ -2102,16 +2091,7 @@ [email protected]: | |
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" | ||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== | ||
|
||
"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== | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needed to make use of this type in the webhook handler, but felt weird importing it from
dispatchers
so extracted "user" functionality to auser.ts
module. Not sure what convention we want to use for locating shared types etc.