diff --git a/src/app.ts b/src/app.ts index af956564..dadfebfc 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,6 +2,7 @@ import { type Context, cors, type Handler, Hono, type HonoEnv, logger, type Midd import { type Event } from '@/event.ts'; import '@/loopback.ts'; +import { actorController } from './controllers/activitypub/actor.ts'; import { accountController, accountLookupController, @@ -67,6 +68,8 @@ app.get('/.well-known/host-meta', hostMetaController); app.get('/.well-known/nodeinfo', nodeInfoController); app.get('/.well-known/nostr.json', nostrController); +app.get('/users/:username', actorController); + app.get('/nodeinfo/:version', nodeInfoSchemaController); app.get('/api/v1/instance', instanceController); diff --git a/src/controllers/activitypub/actor.ts b/src/controllers/activitypub/actor.ts new file mode 100644 index 00000000..92313447 --- /dev/null +++ b/src/controllers/activitypub/actor.ts @@ -0,0 +1,23 @@ +import { getAuthor } from '@/client.ts'; +import { db } from '@/db.ts'; +import { toActor } from '@/transformers/nostr-to-activitypub.ts'; +import { activityJson } from '@/utils.ts'; + +import type { AppController } from '@/app.ts'; + +const actorController: AppController = async (c) => { + const notFound = c.json({ error: 'Not found' }, 404); + + const username = c.req.param('username'); + const user = await db.users.findFirst({ where: { username } }); + + const event = await getAuthor(user.pubkey); + if (!event) return notFound; + + const actor = await toActor(event); + if (!actor) return notFound; + + return activityJson(c, actor); +}; + +export { actorController }; diff --git a/src/transformers/nostr-to-activitypub.ts b/src/transformers/nostr-to-activitypub.ts index 40a374c7..82f328d5 100644 --- a/src/transformers/nostr-to-activitypub.ts +++ b/src/transformers/nostr-to-activitypub.ts @@ -6,9 +6,13 @@ import type { Event } from '@/event.ts'; import type { Actor } from '@/schemas/activitypub.ts'; /** Nostr metadata event to ActivityPub actor. */ -async function toActor(event: Event<0>, username: string): Promise { +async function toActor(event: Event<0>): Promise { const content = parseMetaContent(event); + if (!content.nip05) return; + const [username, hostname] = content.nip05.split('@'); + if (hostname !== Conf.url.hostname) return; + return { type: 'Person', id: Conf.local(`/users/${username}`), diff --git a/src/utils.ts b/src/utils.ts index a81c0fd6..ed0a640f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import { getAuthor } from '@/client.ts'; import { Conf } from '@/config.ts'; -import { nip19, parseFormData, z } from '@/deps.ts'; +import { type Context, nip19, parseFormData, z } from '@/deps.ts'; import { type Event } from '@/event.ts'; import { lookupNip05Cached } from '@/nip05.ts'; @@ -124,7 +124,26 @@ async function sha256(message: string): Promise { return hashHex; } +/** JSON-LD context. */ +type LDContext = (string | Record>)[]; + +/** Add a basic JSON-LD context to ActivityStreams object, if it doesn't already exist. */ +function maybeAddContext(object: T): T & { '@context': LDContext } { + return { + '@context': ['https://www.w3.org/ns/activitystreams'], + ...object, + }; +} + +/** Like hono's `c.json()` except returns JSON-LD. */ +function activityJson(c: Context, object: T) { + const response = c.json(maybeAddContext(object)); + response.headers.set('content-type', 'application/activity+json; charset=UTF-8'); + return response; +} + export { + activityJson, bech32ToPubkey, buildLinkHeader, eventAge,