mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
search: support pagination via Link header
This commit is contained in:
parent
eb94da6cca
commit
c379c11b25
3 changed files with 17 additions and 23 deletions
12
deno.lock
generated
12
deno.lock
generated
|
|
@ -31,7 +31,6 @@
|
||||||
"jsr:@hono/hono@^4.4.6": "4.6.15",
|
"jsr:@hono/hono@^4.4.6": "4.6.15",
|
||||||
"jsr:@negrel/http-ece@0.6.0": "0.6.0",
|
"jsr:@negrel/http-ece@0.6.0": "0.6.0",
|
||||||
"jsr:@negrel/webpush@0.3": "0.3.0",
|
"jsr:@negrel/webpush@0.3": "0.3.0",
|
||||||
"jsr:@nostrify/db@0.38": "0.38.0",
|
|
||||||
"jsr:@nostrify/nostrify@0.31": "0.31.0",
|
"jsr:@nostrify/nostrify@0.31": "0.31.0",
|
||||||
"jsr:@nostrify/nostrify@0.32": "0.32.0",
|
"jsr:@nostrify/nostrify@0.32": "0.32.0",
|
||||||
"jsr:@nostrify/nostrify@0.36": "0.36.2",
|
"jsr:@nostrify/nostrify@0.36": "0.36.2",
|
||||||
|
|
@ -357,15 +356,6 @@
|
||||||
"jsr:@std/path@0.224.0"
|
"jsr:@std/path@0.224.0"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@nostrify/db@0.38.0": {
|
|
||||||
"integrity": "44118756b95f747779839f0e578a5e1dbca164ec44edb8885bd1c99840775e8a",
|
|
||||||
"dependencies": [
|
|
||||||
"jsr:@nostrify/nostrify@~0.38.1",
|
|
||||||
"jsr:@nostrify/types@0.36",
|
|
||||||
"npm:kysely@~0.27.3",
|
|
||||||
"npm:nostr-tools@^2.10.4"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@nostrify/nostrify@0.22.4": {
|
"@nostrify/nostrify@0.22.4": {
|
||||||
"integrity": "1c8a7847e5773213044b491e85fd7cafae2ad194ce59da4d957d2b27c776b42d",
|
"integrity": "1c8a7847e5773213044b491e85fd7cafae2ad194ce59da4d957d2b27c776b42d",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
|
|
@ -2372,7 +2362,7 @@
|
||||||
"jsr:@gfx/canvas-wasm@~0.4.2",
|
"jsr:@gfx/canvas-wasm@~0.4.2",
|
||||||
"jsr:@hono/hono@^4.4.6",
|
"jsr:@hono/hono@^4.4.6",
|
||||||
"jsr:@negrel/webpush@0.3",
|
"jsr:@negrel/webpush@0.3",
|
||||||
"jsr:@nostrify/db@0.38",
|
"jsr:@nostrify/db@0.39",
|
||||||
"jsr:@nostrify/nostrify@~0.38.1",
|
"jsr:@nostrify/nostrify@~0.38.1",
|
||||||
"jsr:@nostrify/policies@~0.36.1",
|
"jsr:@nostrify/policies@~0.36.1",
|
||||||
"jsr:@nostrify/types@0.36",
|
"jsr:@nostrify/types@0.36",
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
import { renderStatus } from '@/views/mastodon/statuses.ts';
|
||||||
import { getFollowedPubkeys } from '@/queries.ts';
|
import { getFollowedPubkeys } from '@/queries.ts';
|
||||||
import { getPubkeysBySearch } from '@/utils/search.ts';
|
import { getPubkeysBySearch } from '@/utils/search.ts';
|
||||||
|
import { paginated } from '@/utils/api.ts';
|
||||||
|
|
||||||
const searchQuerySchema = z.object({
|
const searchQuerySchema = z.object({
|
||||||
q: z.string().transform(decodeURIComponent),
|
q: z.string().transform(decodeURIComponent),
|
||||||
|
|
@ -19,14 +20,14 @@ const searchQuerySchema = z.object({
|
||||||
resolve: booleanParamSchema.optional().transform(Boolean),
|
resolve: booleanParamSchema.optional().transform(Boolean),
|
||||||
following: z.boolean().default(false),
|
following: z.boolean().default(false),
|
||||||
account_id: n.id().optional(),
|
account_id: n.id().optional(),
|
||||||
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
|
|
||||||
offset: z.coerce.number().nonnegative().catch(0),
|
offset: z.coerce.number().nonnegative().catch(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
type SearchQuery = z.infer<typeof searchQuerySchema>;
|
type SearchQuery = z.infer<typeof searchQuerySchema> & { since?: number; until?: number; limit: number };
|
||||||
|
|
||||||
const searchController: AppController = async (c) => {
|
const searchController: AppController = async (c) => {
|
||||||
const result = searchQuerySchema.safeParse(c.req.query());
|
const result = searchQuerySchema.safeParse(c.req.query());
|
||||||
|
const params = c.get('pagination');
|
||||||
const { signal } = c.req.raw;
|
const { signal } = c.req.raw;
|
||||||
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
const viewerPubkey = await c.get('signer')?.getPublicKey();
|
||||||
|
|
||||||
|
|
@ -34,14 +35,14 @@ 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 = await lookupEvent(result.data, signal);
|
const event = await lookupEvent({ ...result.data, ...params }, signal);
|
||||||
const lookup = extractIdentifier(result.data.q);
|
const lookup = extractIdentifier(result.data.q);
|
||||||
|
|
||||||
// Render account from pubkey.
|
// Render account from pubkey.
|
||||||
if (!event && lookup) {
|
if (!event && lookup) {
|
||||||
const pubkey = await lookupPubkey(lookup);
|
const pubkey = await lookupPubkey(lookup);
|
||||||
return c.json({
|
return c.json({
|
||||||
accounts: pubkey ? [await accountFromPubkey(pubkey)] : [],
|
accounts: pubkey ? [accountFromPubkey(pubkey)] : [],
|
||||||
statuses: [],
|
statuses: [],
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
});
|
});
|
||||||
|
|
@ -52,7 +53,8 @@ const searchController: AppController = async (c) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
events = [event];
|
events = [event];
|
||||||
}
|
}
|
||||||
events.push(...(await searchEvents({ ...result.data, viewerPubkey }, signal)));
|
|
||||||
|
events.push(...(await searchEvents({ ...result.data, ...params, viewerPubkey }, signal)));
|
||||||
|
|
||||||
const [accounts, statuses] = await Promise.all([
|
const [accounts, statuses] = await Promise.all([
|
||||||
Promise.all(
|
Promise.all(
|
||||||
|
|
@ -69,16 +71,18 @@ const searchController: AppController = async (c) => {
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return c.json({
|
const body = {
|
||||||
accounts,
|
accounts,
|
||||||
statuses,
|
statuses,
|
||||||
hashtags: [],
|
hashtags: [],
|
||||||
});
|
};
|
||||||
|
|
||||||
|
return paginated(c, events, body);
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Get events for the search params. */
|
/** Get events for the search params. */
|
||||||
async function searchEvents(
|
async function searchEvents(
|
||||||
{ q, type, limit, offset, account_id, viewerPubkey }: SearchQuery & { viewerPubkey?: string },
|
{ q, type, since, until, limit, offset, account_id, viewerPubkey }: SearchQuery & { viewerPubkey?: string },
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
): Promise<NostrEvent[]> {
|
): Promise<NostrEvent[]> {
|
||||||
// Hashtag search is not supported.
|
// Hashtag search is not supported.
|
||||||
|
|
@ -91,6 +95,8 @@ async function searchEvents(
|
||||||
const filter: NostrFilter = {
|
const filter: NostrFilter = {
|
||||||
kinds: typeToKinds(type),
|
kinds: typeToKinds(type),
|
||||||
search: q,
|
search: q,
|
||||||
|
since,
|
||||||
|
until,
|
||||||
limit,
|
limit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,12 +207,10 @@ function buildLinkHeader(url: string, events: NostrEvent[]): string | undefined
|
||||||
return `<${next}>; rel="next", <${prev}>; rel="prev"`;
|
return `<${next}>; rel="next", <${prev}>; rel="prev"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// deno-lint-ignore ban-types
|
|
||||||
type Entity = {};
|
|
||||||
type HeaderRecord = Record<string, string | string[]>;
|
type HeaderRecord = Record<string, string | string[]>;
|
||||||
|
|
||||||
/** Return results with pagination headers. Assumes chronological sorting of events. */
|
/** Return results with pagination headers. Assumes chronological sorting of events. */
|
||||||
function paginated(c: AppContext, events: NostrEvent[], entities: (Entity | undefined)[], headers: HeaderRecord = {}) {
|
function paginated(c: AppContext, events: NostrEvent[], body: object | unknown[], headers: HeaderRecord = {}) {
|
||||||
const link = buildLinkHeader(c.req.url, events);
|
const link = buildLinkHeader(c.req.url, events);
|
||||||
|
|
||||||
if (link) {
|
if (link) {
|
||||||
|
|
@ -220,7 +218,7 @@ function paginated(c: AppContext, events: NostrEvent[], entities: (Entity | unde
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out undefined entities.
|
// Filter out undefined entities.
|
||||||
const results = entities.filter((entity): entity is Entity => Boolean(entity));
|
const results = Array.isArray(body) ? body.filter(Boolean) : body;
|
||||||
return c.json(results, 200, headers);
|
return c.json(results, 200, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue