Strictly follow Mastodon API's way of only returning one result of a lookup succeeds

This commit is contained in:
Alex Gleason 2024-08-07 14:41:16 -05:00
parent cdee2604a1
commit 385127761d
No known key found for this signature in database
GPG key ID: 7211D1F99744FBB7
3 changed files with 38 additions and 55 deletions

View file

@ -8,7 +8,7 @@ import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { uploadFile } from '@/utils/upload.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 { createEvent, paginated, parseBody, updateListEvent } from '@/utils/api.ts';
import { lookupAccount } from '@/utils/lookup.ts'; import { lookupAccount } from '@/utils/lookup.ts';
import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts'; import { renderAccounts, renderEventAccounts, renderStatuses } from '@/views.ts';
@ -110,48 +110,36 @@ const accountSearchQuerySchema = z.object({
q: z.string().transform(decodeURIComponent), q: z.string().transform(decodeURIComponent),
resolve: booleanParamSchema.optional().transform(Boolean), resolve: booleanParamSchema.optional().transform(Boolean),
following: z.boolean().default(false), 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 accountSearchController: AppController = async (c) => {
const result = accountSearchQuerySchema.safeParse(c.req.query());
const { signal } = c.req.raw; const { signal } = c.req.raw;
const { limit } = c.get('pagination');
const result = accountSearchQuerySchema.safeParse(c.req.query());
if (!result.success) { if (!result.success) {
return c.json({ error: 'Bad request', schema: result.error }, 422); return c.json({ error: 'Bad request', schema: result.error }, 422);
} }
const { q, limit } = result.data; const query = decodeURIComponent(result.data.q);
const query = decodeURIComponent(q);
const store = await Storages.search(); const store = await Storages.search();
const bech32 = extractBech32(query); const bech32 = extractBech32(query);
const event = await lookupAccount(bech32 ?? query);
const [event, events] = await Promise.all([ if (!event && bech32) {
lookupAccount(bech32 ?? query), const pubkey = bech32ToPubkey(bech32);
store.query([{ kinds: [0], search: query, limit }], { signal }), return c.json(pubkey ? [await accountFromPubkey(pubkey)] : []);
]);
if (event) {
events.unshift(event);
} }
const results = await hydrateEvents({ const events = await store.query([{ kinds: [0], search: query, limit }], { signal })
events: dedupeEvents(events), .then((events) => hydrateEvents({ events, store, signal }));
store,
signal,
});
const accounts = await Promise.all( 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); return c.json(accounts);
}; };

View file

@ -5,11 +5,11 @@ import { z } from 'zod';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { booleanParamSchema } from '@/schema.ts'; import { booleanParamSchema } from '@/schema.ts';
import { Storages } from '@/storages.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 { nip05Cache } from '@/utils/nip05.ts';
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { renderStatus } from '@/views/mastodon/statuses.ts'; import { renderStatus } from '@/views/mastodon/statuses.ts';
import { hydrateEvents } from '@/storages/hydrate.ts';
/** Matches NIP-05 names with or without an @ in front. */ /** Matches NIP-05 names with or without an @ in front. */
const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/; const ACCT_REGEX = /^@?(?:([\w.+-]+)@)?([\w.-]+)$/;
@ -33,39 +33,44 @@ const searchController: AppController = async (c) => {
return c.json({ error: 'Bad request', schema: result.error }, 422); return c.json({ error: 'Bad request', schema: result.error }, 422);
} }
const [event, events] = await Promise.all([ const event = await lookupEvent(result.data, signal);
lookupEvent(result.data, signal), const bech32 = extractBech32(result.data.q);
searchEvents(result.data, signal),
]);
if (event) { // Render account from pubkey.
events.unshift(event); 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 viewerPubkey = await c.get('signer')?.getPublicKey();
const [accounts, statuses] = await Promise.all([ const [accounts, statuses] = await Promise.all([
Promise.all( Promise.all(
results events
.filter((event) => event.kind === 0) .filter((event) => event.kind === 0)
.map((event) => renderAccount(event)) .map((event) => renderAccount(event))
.filter(Boolean), .filter(Boolean),
), ),
Promise.all( Promise.all(
results events
.filter((event) => event.kind === 1) .filter((event) => event.kind === 1)
.map((event) => renderStatus(event, { viewerPubkey })) .map((event) => renderStatus(event, { viewerPubkey }))
.filter(Boolean), .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({ return c.json({
accounts, accounts,
statuses, statuses,
@ -139,14 +144,10 @@ async function getLookupFilters({ q, type, resolve }: SearchQuery, signal: Abort
if (accounts) filters.push({ kinds: [0], authors: [result.data.pubkey] }); if (accounts) filters.push({ kinds: [0], authors: [result.data.pubkey] });
break; break;
case 'note': case 'note':
if (statuses) { if (statuses) filters.push({ kinds: [1], ids: [result.data] });
filters.push({ kinds: [1], ids: [result.data] });
}
break; break;
case 'nevent': case 'nevent':
if (statuses) { if (statuses) filters.push({ kinds: [1], ids: [result.data.id] });
filters.push({ kinds: [1], ids: [result.data.id] });
}
break; break;
} }
} catch { } catch {

View file

@ -19,7 +19,7 @@ function bech32ToPubkey(bech32: string): string | undefined {
case 'npub': case 'npub':
return decoded.data; return decoded.data;
} }
} catch (_) { } catch {
// //
} }
} }
@ -104,11 +104,6 @@ async function sha256(message: string): Promise<string> {
return hashHex; 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. */ /** Test whether the value is a Nostr ID. */
function isNostrId(value: unknown): boolean { function isNostrId(value: unknown): boolean {
return n.id().safeParse(value).success; return n.id().safeParse(value).success;
@ -121,7 +116,6 @@ function isURL(value: unknown): boolean {
export { export {
bech32ToPubkey, bech32ToPubkey,
dedupeEvents,
eventAge, eventAge,
extractBech32, extractBech32,
findTag, findTag,