mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Strictly follow Mastodon API's way of only returning one result of a lookup succeeds
This commit is contained in:
parent
cdee2604a1
commit
385127761d
3 changed files with 38 additions and 55 deletions
|
|
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue