diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 6cd52bda..00000000 --- a/src/main.ts +++ /dev/null @@ -1,517 +0,0 @@ -import { serve } from "@hono/node-server"; -import { Article, Accept, Federation, Follow, KvKey, KvStore, Person, RequestContext, MemoryKvStore, Context, lookupObject, isActor, generateCryptoKeyPair, exportJwk, importJwk, Create, Note, Activity } from "@fedify/fedify"; -import { federation } from "@fedify/fedify/x/hono"; -import { Hono } from "hono"; -import { initializeApp, applicationDefault, cert } from 'firebase-admin/app'; -import { getFirestore, Timestamp, FieldValue, Filter } from 'firebase-admin/firestore'; - -initializeApp({ - credential: applicationDefault() -}); - -const store = getFirestore(); - -store.collection('testing').add({ - name: 'testing shit' -}) - -import { configure, getConsoleSink } from "@logtape/logtape"; - -await configure({ - sinks: { console: getConsoleSink() }, - filters: {}, - loggers: [ - { category: "fedify", sinks: ["console"], level: "debug" }, - ], -}); - -type ContextData = { - db: KvStore; -} - -const fedifyKv = new MemoryKvStore(); - -const fedify = new Federation({ - kv: fedifyKv, - treatHttps: true -}); - - -const db = new MemoryKvStore(); - -function scopeKvKey(scope: KvKey, key: KvKey): KvKey { - const [first, ...rest] = scope; - return [first, ...rest, ...key]; -} - -function scopeKvStore(db: KvStore, scope: KvKey): KvStore { - return { - get(key: KvKey) { - return db.get(scopeKvKey(scope, key)); - }, - set(key: KvKey, value: any) { - return db.set(scopeKvKey(scope, key), value); - }, - delete(key: KvKey) { - return db.delete(scopeKvKey(scope, key)); - }, - }; -} - -async function addToList(db: KvStore, key: KvKey, item: unknown) { - const list = await db.get(key); - if (!list || !Array.isArray(list)) { - await db.set(key, []); - return addToList(db, key, item); - } - console.log(`addToList(${key}, ${item})`) - await db.set(key, list.concat(item)); -} - -async function removeFromList(db: KvStore, key: KvKey, item: unknown) { - const list = await db.get(key); - if (!list || !Array.isArray(list)) { - await db.set(key, []); - return; - } - await db.set(key, list.filter(listItem => listItem !== item)); -} - -async function getUserData(ctx: RequestContext, handle: string) { - const existing = await ctx.data.db.get(['handle', handle]); - - if (existing) { - return existing - } - - const data = { - id: ctx.getActorUri(handle), - name: `Local Ghost site`, - summary: 'This is a summary', - preferredUsername: handle, - icon: 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) - }; - - await ctx.data.db.set(['handle', handle], data); - - return data; -} - -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; -} - -fedify.setActorDispatcher("/activitypub/users/{handle}", async (ctx, handle) => { - if (handle !== "index") return null; - - const data = await getUserData(ctx, handle); - - const person = new Person(data); - - return person; -}).setKeyPairsDispatcher(async (ctx, handle) => { - if (handle !== "index") return null; - - const data = await getUserKeypair(ctx, handle); - - if (!data) return []; - - return [data]; -}); - -fedify.setInboxListeners("/activitypub/inbox/{handle}", "/activitypub/inbox") - .on(Follow, async (ctx, follow) => { - console.log('Handling Follow'); - if (!follow.id) { - return; - } - const parsed = (ctx as any).parseUri(follow.objectId); - if (parsed?.type !== "actor") { - // TODO Log - return; - } - const sender = await follow.getActor(ctx); - if (sender === null || sender.id === null) { - return; - } - const senderJson = await sender.toJsonLd(); - const followJson = await follow.toJsonLd(); - db.set([follow.id.href], followJson); - db.set([sender.id.href], senderJson); - await addToList(ctx.data.db, ['inbox'], follow.id.href); - await addToList(ctx.data.db, ['followers'], sender.id.href); - - const acceptId = ctx.getObjectUri(Accept, {id: '123'}); - const accept = new Accept({ id: acceptId, actor: follow.objectId, object: follow }); - const acceptJson = await accept.toJsonLd(); - - await db.set([accept.id!.href], acceptJson) - await addToList(ctx.data.db, ['outbox'], accept.id!.href); - await ctx.sendActivity( - { handle: parsed.handle }, - sender, - accept, - ); - }).onError(async (ctx, error) => { - console.error('Error handling Follow'); - console.error(error); - }) - .on(Accept, async (ctx, accept) => { - console.log('Handling Accept'); - const parsed = (ctx as any).parseUri(accept.objectId); - console.log(parsed); - if (false && parsed?.type !== "follow") { - console.log('Not accepting a follow - exit') - return; - } - if (!accept.id) { - console.log('Accept missing id - exit') - return; - } - - const sender = await accept.getActor(ctx); - console.log('Accept sender'); - console.log(sender); - if (sender === null || sender.id === null) { - console.log('Sender missing, exit early'); - return; - } - - const senderJson = await sender.toJsonLd(); - const acceptJson = await accept.toJsonLd(); - db.set([accept.id.href], acceptJson); - db.set([sender.id.href], senderJson); - await addToList(ctx.data.db, ['inbox'], accept.id.href); - await addToList(ctx.data.db, ['following'], sender.id.href); - }).onError(async (ctx, error) => { - console.error('Error handling Accept'); - console.error(error); - }).on(Create, async (ctx, create) => { - console.log('Handling Create'); - const parsed = (ctx as any).parseUri(create.objectId); - console.log(parsed); - if (false && parsed?.type !== "article") { - console.log('Not accepting a follow - exit') - return; - } - if (!create.id) { - console.log('Accept missing id - exit') - return; - } - - const sender = await create.getActor(ctx); - if (sender === null || sender.id === null) { - console.log('Sender missing, exit early'); - return; - } - - // TODO Check Sender is in our following - - const createJson = await create.toJsonLd(); - db.set([create.id.href], createJson); - await addToList(ctx.data.db, ['inbox'], create.id.href); - }).onError(async (ctx, error) => { - console.error('Error handling Accept'); - console.error(error); - }); - -fedify - .setFollowersDispatcher('/activitypub/followers/{handle}', async (ctx, handle) => { - console.log('Followers Dispatcher'); - const results = await ctx.data.db.get(['followers']) || []; - console.log(results); - let items: Person[] = []; - for (const result of results) { - try { - const thing = await lookupObject(result); - if (thing instanceof Person) { - items.push(thing); - } - } catch (err) { - console.log(err); - } - } - return { - items, - }; - }) - .setCounter(async (ctx, handle) => { - const results = await ctx.data.db.get(['followers']) || []; - return results?.length; - }); - -fedify - .setFollowingDispatcher('/activitypub/following/{handle}', async (ctx, handle) => { - console.log('Following Dispatcher'); - const results = await ctx.data.db.get(['following']) || []; - console.log(results); - let items: Person[] = []; - for (const result of results) { - try { - const thing = await lookupObject(result); - if (thing instanceof Person) { - items.push(thing); - } - } catch (err) { - console.log(err); - } - } - return { - items, - }; - }) - .setCounter(async (ctx, handle) => { - const results = await ctx.data.db.get(['following']) || []; - return results?.length; - }); - -fedify - .setOutboxDispatcher('/activitypub/outbox/{handle}', async (ctx, handle) => { - console.log('Outbox Dispatcher'); - const results = await ctx.data.db.get(['outbox']) || []; - console.log(results); - let items: Activity[] = []; - for (const result of results) { - try { - const thing = await db.get([result]); - const activity = await Activity.fromJsonLd(thing); - items.push(activity); - } catch (err) { - console.log(err); - } - } - return { - items - }; - }) - .setCounter(async (ctx, handle) => { - const results = await ctx.data.db.get(['outbox']) || []; - return results?.length; - }); - -type HonoContextVariables = { - db: KvStore -}; - -const app = new Hono<{Variables: HonoContextVariables}>(); - -app.use(async (ctx, next) => { - const id = crypto.randomUUID(); - const start = Date.now(); - console.log(`${ctx.req.method.toUpperCase()} ${ctx.req.url} ${id}`) - await next(); - const end = Date.now(); - console.log(`${ctx.req.method.toUpperCase()} ${ctx.req.url} ${ctx.res.status} ${end - start}ms ${id}`) -}); - -app.use(async (ctx, next) => { - const request = ctx.req; - const host = request.header('host'); - if (!host) { - // TODO handle - throw new Error('No Host header') - } - - const scopedDb = scopeKvStore(db, ['sites', host]); - - ctx.set('db', scopedDb) - - await next(); -}); - -app.get('/activitypub/inbox/:handle', async (ctx, next) => { - console.log('Reading from inbox'); - const results = await ctx.get('db').get(['inbox']) || []; - let items: unknown[] = []; - for (const result of results) { - console.log('inbox item', result); - try { - const thing = await db.get([result]); - items.push(thing); - } catch (err) { - console.log(err); - } - } - return new Response(JSON.stringify({ - "@context": "https://www.w3.org/ns/activitystreams", - type: "OrderedCollection", - totalItems: results?.length, - items - }), { - headers: { - 'Content-Type': 'application/activity+json' - }, - status: 200 - }); -}); - -app.use(federation(fedify, (ctx) => { - return { - db: ctx.get('db') - }; -})); - -function addObjectDispatchers(dispatchers) { - for (const dispatcher of dispatchers) { - console.log('Adding dispatcher', `/activitypub/${dispatcher.name.toLowerCase()}/{id}`) - fedify.setObjectDispatcher(dispatcher, `/activitypub/${dispatcher.name.toLowerCase()}/{id}`, async function (ctx, data) { - const id = ctx.getObjectUri(dispatcher, data); - const exists = await db.get([id.href]); - if (!exists) { - return null; - } - return dispatcher.fromJsonLd(exists); - }); - } -} - -addObjectDispatchers([Follow, Accept, Article, Create, Note]); - -function toURL(x: unknown) { - if (x instanceof URL) { - return x; - } - if (typeof x !== 'string') { - return undefined; - } - try { - return new URL(x); - } catch (err) { - return undefined; - } -} - -async function postToArticle(ctx: RequestContext, post: any) { - 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: post.published_at, - preview: preview, - url: toURL(post.url) - }); - - return { - article, - preview - } -} - -app.post('/activitypub/webhooks/post/published', async (ctx, next) => { - console.log('Got webhook'); - // TODO: Validate webhook with secret - const data = await ctx.req.json(); - console.log(JSON.stringify(data, null, 4)); - const apCtx = fedify.createContext(ctx.req.raw as Request, { - db: ctx.get('db') - }); - const {article, preview} = await postToArticle(apCtx, data?.post?.current); - if (article) { - const actor = await apCtx.getActor('index'); - const create = new Create({ - actor, - object: article, - id: apCtx.getObjectUri(Create, {id: '123'}), - to: apCtx.getFollowersUri('index') - }); - try { - await article.toJsonLd(); - await db.set([preview.id!.href], await preview.toJsonLd()); - await db.set([create.id!.href], await create.toJsonLd()); - await db.set([article.id!.href], await article.toJsonLd()); - console.log(await article.toJsonLd()); - await addToList(ctx.get('db'), ['outbox'], create.id!.href); - await apCtx.sendActivity( - { handle: 'index' }, - 'followers', - create, - ) - } catch (err) { - console.log(err); - } - } - return new Response(JSON.stringify({}), { - headers: { - 'Content-Type': 'application/activity+json' - }, - status: 200 - }); -}); - - -app.post('/activitypub/actions/follow/:handle', async (ctx) => { - console.log('follow action'); - const handle = ctx.req.param('handle'); - const actorToFollow = await lookupObject(handle); - if (!isActor(actorToFollow)) { - // Not Found? - return; - } - const apCtx = fedify.createContext(ctx.req.raw as Request, { - db: ctx.get('db') - }); - const actor = await apCtx.getActor('index'); // TODO This should be the actor making the request - const followId = apCtx.getObjectUri(Follow, { - id: '123' - }); - const follow = new Follow({ - id: followId, - actor: actor, - object: actorToFollow - }); - const followJson = await follow.toJsonLd(); - db.set([follow.id!.href], followJson); - - apCtx.sendActivity( - {handle: 'index'}, - actorToFollow, - follow - ); - return new Response(); -}); - -serve({ - fetch: app.fetch, - port: parseInt(process.env.PORT || '42069'), -}, function (info) { - console.log(`listening on ${info.address}:${info.port}`); -}); - -process.on('SIGINT', () => process.exit(0));