diff --git a/src/app.ts b/src/app.ts index fadb5115..2cc24bd9 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,7 @@ import { } from './controllers/api/accounts.ts'; import { appCredentialsController, createAppController } from './controllers/api/apps.ts'; import { emptyArrayController, emptyObjectController } from './controllers/api/fallback.ts'; -import { homeController } from './controllers/api/timelines.ts'; +import { homeController, publicController } from './controllers/api/timelines.ts'; import instanceController from './controllers/api/instance.ts'; import { createTokenController, oauthAuthorizeController, oauthController } from './controllers/api/oauth.ts'; import { frontendConfigController } from './controllers/api/pleroma.ts'; @@ -75,6 +75,7 @@ app.post('/api/v1/statuses/:id{[0-9a-f]{64}}/favourite', favouriteController); app.post('/api/v1/statuses', requireAuth, createStatusController); app.get('/api/v1/timelines/home', requireAuth, homeController); +app.get('/api/v1/timelines/public', publicController); app.get('/api/v1/preferences', preferencesController); app.get('/api/v1/search', searchController); @@ -92,7 +93,6 @@ app.get('/api/v1/blocks', emptyArrayController); app.get('/api/v1/mutes', emptyArrayController); app.get('/api/v1/domain_blocks', emptyArrayController); app.get('/api/v1/markers', emptyObjectController); -app.get('/api/v1/timelines/public', emptyArrayController); app.get('/api/v1/conversations', emptyArrayController); app.get('/api/v1/favourites', emptyArrayController); app.get('/api/v1/lists', emptyArrayController); diff --git a/src/client.ts b/src/client.ts index b796b377..211e3c5d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -3,7 +3,7 @@ import { type Event, type SignedEvent } from '@/event.ts'; import { Conf } from './config.ts'; -import { eventDateComparator, nostrNow } from './utils.ts'; +import { eventDateComparator, type PaginationParams } from './utils.ts'; const db = await Deno.openKv(); @@ -99,9 +99,14 @@ const getEvent = async (id: string, kind?: K): Promis }; /** Get a Nostr `set_medatadata` event for a user's pubkey. */ -const getAuthor = async (pubkey: string): Promise | undefined> => { +const getAuthor = async (pubkey: string, timeout = 1000): Promise | undefined> => { const author = new Author(getPool(), Conf.poolRelays, pubkey); - const event: SignedEvent<0> | null = await new Promise((resolve) => author.metaData(resolve, 0)); + + const event: SignedEvent<0> | null = await new Promise((resolve) => { + setTimeout(resolve, timeout, null); + return author.metaData(resolve, 0); + }); + return event?.pubkey === pubkey ? event : undefined; }; @@ -119,16 +124,8 @@ const getFollows = async (pubkey: string): Promise | undefined> = } }; -interface PaginationParams { - since?: number; - until?: number; - limit?: number; -} - /** Get events from people the user follows. */ -async function getFeed(event3: Event<3>, params: PaginationParams = {}): Promise[]> { - const limit = Math.max(params.limit ?? 20, 40); - +async function getFeed(event3: Event<3>, params: PaginationParams): Promise[]> { const authors = event3.tags .filter((tag) => tag[0] === 'p') .map((tag) => tag[1]); @@ -138,15 +135,19 @@ async function getFeed(event3: Event<3>, params: PaginationParams = {}): Promise const filter: Filter = { authors, kinds: [1], - since: params.since, - until: params.until ?? nostrNow(), - limit, + ...params, }; const results = await getFilter(filter, { timeout: 5000 }) as SignedEvent<1>[]; return results.sort(eventDateComparator); } +/** Get a feed of all known text notes. */ +async function getPublicFeed(params: PaginationParams): Promise[]> { + const results = await getFilter({ kinds: [1], ...params }, { timeout: 5000 }); + return results.sort(eventDateComparator); +} + async function getAncestors(event: Event<1>, result = [] as Event<1>[]): Promise[]> { if (result.length < 100) { const replyTag = findReplyTag(event); @@ -179,4 +180,4 @@ function publish(event: SignedEvent, relays = Conf.publishRelays): void { } } -export { getAncestors, getAuthor, getDescendants, getEvent, getFeed, getFilter, getFollows, publish }; +export { getAncestors, getAuthor, getDescendants, getEvent, getFeed, getFilter, getFollows, getPublicFeed, publish }; diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index dfd3197a..4d4971c7 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -1,11 +1,11 @@ -import { getFeed, getFollows } from '@/client.ts'; +import { getFeed, getFollows, getPublicFeed } from '@/client.ts'; import { toStatus } from '@/transmute.ts'; import { buildLinkHeader, paginationSchema } from '@/utils.ts'; import type { AppController } from '@/app.ts'; const homeController: AppController = async (c) => { - const { since, until } = paginationSchema.parse(c.req.query()); + const params = paginationSchema.parse(c.req.query()); const pubkey = c.get('pubkey')!; const follows = await getFollows(pubkey); @@ -13,7 +13,7 @@ const homeController: AppController = async (c) => { return c.json([]); } - const events = await getFeed(follows, { since, until }); + const events = await getFeed(follows, params); if (!events.length) { return c.json([]); } @@ -24,4 +24,13 @@ const homeController: AppController = async (c) => { return c.json(statuses, 200, link ? { link } : undefined); }; -export { homeController }; +const publicController: AppController = async (c) => { + const params = paginationSchema.parse(c.req.query()); + const events = await getPublicFeed(params); + const statuses = (await Promise.all(events.map(toStatus))).filter(Boolean); + const link = buildLinkHeader(c.req.url, events); + + return c.json(statuses, 200, link ? { link } : undefined); +}; + +export { homeController, publicController }; diff --git a/src/utils.ts b/src/utils.ts index 0d2bf308..7ca64047 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -78,9 +78,12 @@ async function parseBody(req: Request): Promise { const paginationSchema = z.object({ since: z.coerce.number().optional().catch(undefined), - until: z.coerce.number().optional().catch(undefined), + until: z.coerce.number().catch(nostrNow()), + limit: z.coerce.number().min(0).max(40).catch(20), }); +type PaginationParams = z.infer; + function buildLinkHeader(url: string, events: Event[]): string | undefined { if (!events.length) return; const firstEvent = events[0]; @@ -103,6 +106,7 @@ export { lookupAccount, type Nip05, nostrNow, + type PaginationParams, paginationSchema, parseBody, parseNip05,