From 385127761d55734413e80f5cbedb8ab70466b481 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Wed, 7 Aug 2024 14:41:16 -0500 Subject: [PATCH] Strictly follow Mastodon API's way of only returning one result of a lookup succeeds --- src/controllers/api/accounts.ts | 38 +++++++++----------------- src/controllers/api/search.ts | 47 +++++++++++++++++---------------- src/utils.ts | 8 +----- 3 files changed, 38 insertions(+), 55 deletions(-) diff --git a/src/controllers/api/accounts.ts b/src/controllers/api/accounts.ts index 87bef328..c4fd6721 100644 --- a/src/controllers/api/accounts.ts +++ b/src/controllers/api/accounts.ts @@ -8,7 +8,7 @@ import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; import { uploadFile } from '@/utils/upload.ts'; -import { dedupeEvents, extractBech32, nostrNow } from '@/utils.ts'; +import { extractBech32, nostrNow } from '@/utils.ts'; import { createEvent, paginated, parseBody, updateListEvent } from '@/utils/api.ts'; import { lookupAccount } from '@/utils/lookup.ts'; import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts'; @@ -110,48 +110,36 @@ const accountSearchQuerySchema = z.object({ q: z.string().transform(decodeURIComponent), resolve: booleanParamSchema.optional().transform(Boolean), following: z.boolean().default(false), - limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)), }); const accountSearchController: AppController = async (c) => { - const result = accountSearchQuerySchema.safeParse(c.req.query()); const { signal } = c.req.raw; + const { limit } = c.get('pagination'); + + const result = accountSearchQuerySchema.safeParse(c.req.query()); if (!result.success) { return c.json({ error: 'Bad request', schema: result.error }, 422); } - const { q, limit } = result.data; - - const query = decodeURIComponent(q); + const query = decodeURIComponent(result.data.q); const store = await Storages.search(); + const bech32 = extractBech32(query); + const event = await lookupAccount(bech32 ?? query); - const [event, events] = await Promise.all([ - lookupAccount(bech32 ?? query), - store.query([{ kinds: [0], search: query, limit }], { signal }), - ]); - - if (event) { - events.unshift(event); + if (!event && bech32) { + const pubkey = bech32ToPubkey(bech32); + return c.json(pubkey ? [await accountFromPubkey(pubkey)] : []); } - const results = await hydrateEvents({ - events: dedupeEvents(events), - store, - signal, - }); + const events = await store.query([{ kinds: [0], search: query, limit }], { signal }) + .then((events) => hydrateEvents({ events, store, signal })); const accounts = await Promise.all( - results.map((event) => renderAccount(event)), + events.map((event) => renderAccount(event)), ); - // Render account from pubkey. - const pubkey = bech32ToPubkey(result.data.q); - if (pubkey && !accounts.find((account) => account.id === pubkey)) { - accounts.unshift(await accountFromPubkey(pubkey)); - } - return c.json(accounts); }; diff --git a/src/controllers/api/search.ts b/src/controllers/api/search.ts index 3ca2aa3d..075843f3 100644 --- a/src/controllers/api/search.ts +++ b/src/controllers/api/search.ts @@ -5,11 +5,11 @@ import { z } from 'zod'; import { AppController } from '@/app.ts'; import { booleanParamSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; -import { bech32ToPubkey, dedupeEvents, extractBech32 } from '@/utils.ts'; +import { hydrateEvents } from '@/storages/hydrate.ts'; +import { bech32ToPubkey, extractBech32 } from '@/utils.ts'; import { nip05Cache } from '@/utils/nip05.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts'; -import { hydrateEvents } from '@/storages/hydrate.ts'; /** Matches NIP-05 names with or without an @ in front. */ const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/; @@ -33,39 +33,44 @@ const searchController: AppController = async (c) => { return c.json({ error: 'Bad request', schema: result.error }, 422); } - const [event, events] = await Promise.all([ - lookupEvent(result.data, signal), - searchEvents(result.data, signal), - ]); + const event = await lookupEvent(result.data, signal); + const bech32 = extractBech32(result.data.q); - if (event) { - events.unshift(event); + // Render account from pubkey. + if (!event && bech32) { + const pubkey = bech32ToPubkey(bech32); + return c.json({ + accounts: pubkey ? [await accountFromPubkey(pubkey)] : [], + statuses: [], + hashtags: [], + }); + } + + let events: NostrEvent[] = []; + + if (event) { + events = [event]; + } else { + events = await searchEvents(result.data, signal); } - const results = dedupeEvents(events); const viewerPubkey = await c.get('signer')?.getPublicKey(); const [accounts, statuses] = await Promise.all([ Promise.all( - results + events .filter((event) => event.kind === 0) .map((event) => renderAccount(event)) .filter(Boolean), ), Promise.all( - results + events .filter((event) => event.kind === 1) .map((event) => renderStatus(event, { viewerPubkey })) .filter(Boolean), ), ]); - // Render account from pubkey. - const pubkey = bech32ToPubkey(result.data.q); - if (pubkey && !accounts.find((account) => account.id === pubkey)) { - accounts.unshift(await accountFromPubkey(pubkey)); - } - return c.json({ accounts, statuses, @@ -139,14 +144,10 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort if (accounts) filters.push({ kinds: [0], authors: [result.data.pubkey] }); break; case 'note': - if (statuses) { - filters.push({ kinds: [1], ids: [result.data] }); - } + if (statuses) filters.push({ kinds: [1], ids: [result.data] }); break; case 'nevent': - if (statuses) { - filters.push({ kinds: [1], ids: [result.data.id] }); - } + if (statuses) filters.push({ kinds: [1], ids: [result.data.id] }); break; } } catch { diff --git a/src/utils.ts b/src/utils.ts index 5abc2360..eddf2623 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -19,7 +19,7 @@ function bech32ToPubkey(bech32: string): string | undefined { case 'npub': return decoded.data; } - } catch (_) { + } catch { // } } @@ -104,11 +104,6 @@ async function sha256(message: string): Promise { return hashHex; } -/** Deduplicate events by ID. */ -function dedupeEvents(events: NostrEvent[]): NostrEvent[] { - return [...new Map(events.map((event) => [event.id, event])).values()]; -} - /** Test whether the value is a Nostr ID. */ function isNostrId(value: unknown): boolean { return n.id().safeParse(value).success; @@ -121,7 +116,6 @@ function isURL(value: unknown): boolean { export { bech32ToPubkey, - dedupeEvents, eventAge, extractBech32, findTag,