diff --git a/src/api/statuses.ts b/src/api/statuses.ts index 38c35b5..7689b5e 100644 --- a/src/api/statuses.ts +++ b/src/api/statuses.ts @@ -1,7 +1,7 @@ import { type AppContext, AppController } from '@/app.ts'; import { validator, z } from '@/deps.ts'; import { type Event } from '@/event.ts'; -import { getEvent } from '../client.ts'; +import { getAncestors, getDescendants, getEvent } from '../client.ts'; import publish from '../publisher.ts'; import { toStatus } from '../transmute.ts'; @@ -13,9 +13,8 @@ const createStatusSchema = z.object({ const statusController: AppController = async (c) => { const id = c.req.param('id'); - const event = await getEvent(id); - - if (event && event.kind === 1) { + const event = await getEvent(id, 1); + if (event) { return c.json(await toStatus(event as Event<1>)); } @@ -46,4 +45,22 @@ const createStatusController = validator('json', async (value, c: AppContext) => } }); -export { createStatusController, statusController }; +const contextController: AppController = async (c) => { + const id = c.req.param('id'); + + const event = await getEvent(id, 1); + + if (event) { + const ancestorEvents = await getAncestors(event); + const descendantEvents = await getDescendants(event.id); + + return c.json({ + ancestors: (await Promise.all((ancestorEvents).map(toStatus))).filter(Boolean), + descendants: (await Promise.all((descendantEvents).map(toStatus))).filter(Boolean), + }); + } + + return c.json({ error: 'Event not found.' }, 404); +}; + +export { contextController, createStatusController, statusController }; diff --git a/src/app.ts b/src/app.ts index 2e28594..7d08d6c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -12,7 +12,7 @@ import { emptyArrayController, emptyObjectController } from './api/fallback.ts'; import homeController from './api/home.ts'; import instanceController from './api/instance.ts'; import { createTokenController } from './api/oauth.ts'; -import { createStatusController, statusController } from './api/statuses.ts'; +import { contextController, createStatusController, statusController } from './api/statuses.ts'; import { requireAuth, setAuth } from './middleware/auth.ts'; interface AppEnv extends HonoEnv { @@ -44,6 +44,7 @@ app.get('/api/v1/accounts/lookup', accountLookupController); app.get('/api/v1/accounts/relationships', relationshipsController); app.get('/api/v1/accounts/:pubkey{[0-9a-f]{64}}', accountController); +app.get('/api/v1/statuses/:id{[0-9a-f]{64}}/context', contextController); app.get('/api/v1/statuses/:id{[0-9a-f]{64}}', statusController); app.post('/api/v1/statuses', requireAuth, createStatusController); diff --git a/src/client.ts b/src/client.ts index bf30a49..66ff669 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,4 +1,4 @@ -import { Author, RelayPool } from '@/deps.ts'; +import { Author, type Filter, matchFilter, RelayPool } from '@/deps.ts'; import { type Event, type SignedEvent } from '@/event.ts'; import { poolRelays } from './config.ts'; @@ -7,10 +7,33 @@ import { eventDateComparator, nostrNow } from './utils.ts'; const pool = new RelayPool(poolRelays); +/** Get events from a NIP-01 filter. */ +function getFilter(filter: Filter): Promise { + return new Promise((resolve) => { + const results: SignedEvent[] = []; + pool.subscribe( + [filter], + poolRelays, + (event: SignedEvent | null) => { + if (event && matchFilter(filter, event)) { + results.push(event); + } + }, + undefined, + () => resolve(results), + { unsubscribeOnEose: true }, + ); + }); +} + /** Get a Nostr event by its ID. */ -const getEvent = async (id: string): Promise => { +const getEvent = async (id: string, kind?: K): Promise | undefined> => { const event = await (pool.getEventById(id, poolRelays, 0) as Promise); - return event?.id === id ? event : undefined; + if (event) { + if (event.id !== id) return undefined; + if (kind && event.kind !== kind) return undefined; + return event as SignedEvent; + } }; /** Get a Nostr `set_medatadata` event for a user's pubkey. */ @@ -74,4 +97,28 @@ function getFeed(event3: Event<3>, params: PaginationParams = {}): Promise, result = [] as Event<1>[]): Promise[]> { + if (result.length < 100) { + const replyTag = event.tags + .find((t) => t[0] === 'e' && (!t[2] || t[2] === 'reply' || t[2] === 'root')); + + const inReplyTo = replyTag ? replyTag[1] : undefined; + + if (inReplyTo) { + const parentEvent = await getEvent(inReplyTo, 1); + + if (parentEvent) { + result.push(parentEvent); + return getAncestors(parentEvent, result); + } + } + } + + return result.reverse(); +} + +function getDescendants(eventId: string): Promise[]> { + return getFilter({ kinds: [1], '#e': [eventId] }) as Promise[]>; +} + +export { getAncestors, getAuthor, getDescendants, getEvent, getFeed, getFollows, pool }; diff --git a/src/deps.ts b/src/deps.ts index 08817db..f52b576 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -14,6 +14,7 @@ export { type Filter, getEventHash, getPublicKey, + matchFilter, nip05, nip19, nip21,