mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
Let paginationMiddleware be configurable, add pagination to reactions handler
This commit is contained in:
parent
36ffd4283a
commit
88ef8087a5
8 changed files with 122 additions and 97 deletions
|
|
@ -633,7 +633,7 @@ const zappedByController: AppController = async (c) => {
|
||||||
const { db, relay } = c.var;
|
const { db, relay } = c.var;
|
||||||
|
|
||||||
const id = c.req.param('id');
|
const id = c.req.param('id');
|
||||||
const { offset, limit } = paginationSchema.parse(c.req.query());
|
const { offset, limit } = paginationSchema().parse(c.req.query());
|
||||||
|
|
||||||
const zaps = await db.kysely.selectFrom('event_zaps')
|
const zaps = await db.kysely.selectFrom('event_zaps')
|
||||||
.selectAll()
|
.selectAll()
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
|
||||||
|
|
||||||
export const suggestionsV1Controller: AppController = async (c) => {
|
export const suggestionsV1Controller: AppController = async (c) => {
|
||||||
const { signal } = c.var;
|
const { signal } = c.var;
|
||||||
const { offset, limit } = paginationSchema.parse(c.req.query());
|
const { offset, limit } = paginationSchema().parse(c.req.query());
|
||||||
const suggestions = await renderV2Suggestions(c, { offset, limit }, signal);
|
const suggestions = await renderV2Suggestions(c, { offset, limit }, signal);
|
||||||
const accounts = suggestions.map(({ account }) => account);
|
const accounts = suggestions.map(({ account }) => account);
|
||||||
return paginatedList(c, { offset, limit }, accounts);
|
return paginatedList(c, { offset, limit }, accounts);
|
||||||
|
|
@ -17,7 +17,7 @@ export const suggestionsV1Controller: AppController = async (c) => {
|
||||||
|
|
||||||
export const suggestionsV2Controller: AppController = async (c) => {
|
export const suggestionsV2Controller: AppController = async (c) => {
|
||||||
const { signal } = c.var;
|
const { signal } = c.var;
|
||||||
const { offset, limit } = paginationSchema.parse(c.req.query());
|
const { offset, limit } = paginationSchema().parse(c.req.query());
|
||||||
const suggestions = await renderV2Suggestions(c, { offset, limit }, signal);
|
const suggestions = await renderV2Suggestions(c, { offset, limit }, signal);
|
||||||
return paginatedList(c, { offset, limit }, suggestions);
|
return paginatedList(c, { offset, limit }, suggestions);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -124,7 +124,7 @@ async function getTrendingLinks(conf: DittoConf, relay: NStore): Promise<Trendin
|
||||||
|
|
||||||
const trendingStatusesController: AppController = async (c) => {
|
const trendingStatusesController: AppController = async (c) => {
|
||||||
const { conf, relay } = c.var;
|
const { conf, relay } = c.var;
|
||||||
const { limit, offset, until } = paginationSchema.parse(c.req.query());
|
const { limit, offset, until } = paginationSchema().parse(c.req.query());
|
||||||
|
|
||||||
const [label] = await relay.query([{
|
const [label] = await relay.query([{
|
||||||
kinds: [1985],
|
kinds: [1985],
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { userMiddleware } from '@ditto/mastoapi/middleware';
|
import { paginationMiddleware, userMiddleware } from '@ditto/mastoapi/middleware';
|
||||||
import { DittoRoute } from '@ditto/mastoapi/router';
|
import { DittoRoute } from '@ditto/mastoapi/router';
|
||||||
|
|
||||||
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
import { DittoEvent } from '@/interfaces/DittoEvent.ts';
|
||||||
|
|
@ -109,92 +109,97 @@ route.delete('/:id{[0-9a-f]{64}}/reactions/:emoji', userMiddleware(), async (c)
|
||||||
* Get an object of emoji to account mappings with accounts that reacted to the post.
|
* Get an object of emoji to account mappings with accounts that reacted to the post.
|
||||||
* https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactions
|
* https://docs.pleroma.social/backend/development/API/pleroma_api/#get-apiv1pleromastatusesidreactions
|
||||||
*/
|
*/
|
||||||
route.get('/:id{[0-9a-f]{64}}/reactions/:emoji?', userMiddleware({ required: false }), async (c) => {
|
route.get(
|
||||||
const { relay, user } = c.var;
|
'/:id{[0-9a-f]{64}}/reactions/:emoji?',
|
||||||
|
paginationMiddleware({ limit: 100 }),
|
||||||
|
userMiddleware({ required: false }),
|
||||||
|
async (c) => {
|
||||||
|
const { relay, user, pagination, paginate } = c.var;
|
||||||
|
|
||||||
const params = c.req.param();
|
const params = c.req.param();
|
||||||
const result = params.emoji ? parseEmojiParam(params.emoji) : undefined;
|
const result = params.emoji ? parseEmojiParam(params.emoji) : undefined;
|
||||||
const pubkey = await user?.signer.getPublicKey();
|
const pubkey = await user?.signer.getPublicKey();
|
||||||
|
|
||||||
const events = await relay.query([{ kinds: [7], '#e': [params.id], limit: 100 }])
|
const events = await relay.query([{ kinds: [7], '#e': [params.id], ...pagination }])
|
||||||
.then((events) =>
|
.then((events) =>
|
||||||
events.filter((event) => {
|
events.filter((event) => {
|
||||||
if (!result) return true;
|
if (!result) return true;
|
||||||
|
|
||||||
switch (result.type) {
|
switch (result.type) {
|
||||||
case 'native':
|
case 'native':
|
||||||
return event.content === result.native;
|
return event.content === result.native;
|
||||||
case 'custom':
|
case 'custom':
|
||||||
return event.content === `:${result.shortcode}:`;
|
return event.content === `:${result.shortcode}:`;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.then((events) => hydrateEvents({ ...c.var, events }));
|
.then((events) => hydrateEvents({ ...c.var, events }));
|
||||||
|
|
||||||
/** Events grouped by emoji key. */
|
/** Events grouped by emoji key. */
|
||||||
const byEmojiKey = events.reduce((acc, event) => {
|
const byEmojiKey = events.reduce((acc, event) => {
|
||||||
const result = parseEmojiInput(event.content);
|
const result = parseEmojiInput(event.content);
|
||||||
|
|
||||||
if (!result || result.type === 'basic') {
|
if (!result || result.type === 'basic') {
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
|
|
||||||
let url: URL | undefined;
|
|
||||||
|
|
||||||
if (result.type === 'custom') {
|
|
||||||
const tag = event.tags.find(([name, value]) => name === 'emoji' && value === result.shortcode);
|
|
||||||
try {
|
|
||||||
url = new URL(tag![2]);
|
|
||||||
} catch {
|
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let key: string;
|
let url: URL | undefined;
|
||||||
switch (result.type) {
|
|
||||||
case 'native':
|
|
||||||
key = result.native;
|
|
||||||
break;
|
|
||||||
case 'custom':
|
|
||||||
key = `${result.shortcode}:${url}`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
acc[key] = acc[key] || [];
|
if (result.type === 'custom') {
|
||||||
acc[key].push(event);
|
const tag = event.tags.find(([name, value]) => name === 'emoji' && value === result.shortcode);
|
||||||
|
try {
|
||||||
return acc;
|
url = new URL(tag![2]);
|
||||||
}, {} as Record<string, DittoEvent[]>);
|
} catch {
|
||||||
|
return acc;
|
||||||
const results = await Promise.all(
|
}
|
||||||
Object.entries(byEmojiKey).map(async ([key, events]) => {
|
|
||||||
let name: string = key;
|
|
||||||
let url: string | undefined;
|
|
||||||
|
|
||||||
// Custom emojis: `<shortcode>:<url>`
|
|
||||||
try {
|
|
||||||
const [shortcode, ...rest] = key.split(':');
|
|
||||||
|
|
||||||
url = new URL(rest.join(':')).toString();
|
|
||||||
name = shortcode;
|
|
||||||
} catch {
|
|
||||||
// fallthrough
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
let key: string;
|
||||||
name,
|
switch (result.type) {
|
||||||
count: events.length,
|
case 'native':
|
||||||
me: pubkey && events.some((event) => event.pubkey === pubkey),
|
key = result.native;
|
||||||
accounts: await Promise.all(
|
break;
|
||||||
events.map((event) => event.author ? renderAccount(event.author) : accountFromPubkey(event.pubkey)),
|
case 'custom':
|
||||||
),
|
key = `${result.shortcode}:${url}`;
|
||||||
url,
|
break;
|
||||||
};
|
}
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return c.json(results);
|
acc[key] = acc[key] || [];
|
||||||
});
|
acc[key].push(event);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, DittoEvent[]>);
|
||||||
|
|
||||||
|
const results = await Promise.all(
|
||||||
|
Object.entries(byEmojiKey).map(async ([key, events]) => {
|
||||||
|
let name: string = key;
|
||||||
|
let url: string | undefined;
|
||||||
|
|
||||||
|
// Custom emojis: `<shortcode>:<url>`
|
||||||
|
try {
|
||||||
|
const [shortcode, ...rest] = key.split(':');
|
||||||
|
|
||||||
|
url = new URL(rest.join(':')).toString();
|
||||||
|
name = shortcode;
|
||||||
|
} catch {
|
||||||
|
// fallthrough
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
count: events.length,
|
||||||
|
me: pubkey && events.some((event) => event.pubkey === pubkey),
|
||||||
|
accounts: await Promise.all(
|
||||||
|
events.map((event) => event.author ? renderAccount(event.author) : accountFromPubkey(event.pubkey)),
|
||||||
|
),
|
||||||
|
url,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return paginate(events, results);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
/** Determine if the input is a native or custom emoji, returning a structured object or throwing an error. */
|
/** Determine if the input is a native or custom emoji, returning a structured object or throwing an error. */
|
||||||
function parseEmojiParam(input: string):
|
function parseEmojiParam(input: string):
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ async function renderEventAccounts(c: AppContext, filters: NostrFilter[], opts?:
|
||||||
}
|
}
|
||||||
|
|
||||||
async function renderAccounts(c: AppContext, pubkeys: string[]) {
|
async function renderAccounts(c: AppContext, pubkeys: string[]) {
|
||||||
const { offset, limit } = paginationSchema.parse(c.req.query());
|
const { offset, limit } = paginationSchema().parse(c.req.query());
|
||||||
const authors = pubkeys.reverse().slice(offset, offset + limit);
|
const authors = pubkeys.reverse().slice(offset, offset + limit);
|
||||||
|
|
||||||
const { relay, signal } = c.var;
|
const { relay, signal } = c.var;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { paginated, paginatedList } from '../pagination/paginate.ts';
|
import { paginated, paginatedList } from '../pagination/paginate.ts';
|
||||||
import { paginationSchema } from '../pagination/schema.ts';
|
import { paginationSchema, type PaginationSchemaOpts } from '../pagination/schema.ts';
|
||||||
|
|
||||||
import type { DittoMiddleware } from '@ditto/mastoapi/router';
|
import type { DittoMiddleware } from '@ditto/mastoapi/router';
|
||||||
import type { NostrEvent } from '@nostrify/nostrify';
|
import type { NostrEvent } from '@nostrify/nostrify';
|
||||||
|
|
@ -19,19 +19,26 @@ type HeaderRecord = Record<string, string | string[]>;
|
||||||
type PaginateFn = (events: NostrEvent[], body: object | unknown[], headers?: HeaderRecord) => Response;
|
type PaginateFn = (events: NostrEvent[], body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||||
type ListPaginateFn = (params: ListPagination, body: object | unknown[], headers?: HeaderRecord) => Response;
|
type ListPaginateFn = (params: ListPagination, body: object | unknown[], headers?: HeaderRecord) => Response;
|
||||||
|
|
||||||
|
interface PaginationMiddlewareOpts extends PaginationSchemaOpts {
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
/** Fixes compatibility with Mastodon apps by that don't use `Link` headers. */
|
||||||
// @ts-ignore Types are right.
|
// @ts-ignore Types are right.
|
||||||
export function paginationMiddleware(): DittoMiddleware<{ pagination: Pagination; paginate: PaginateFn }>;
|
export function paginationMiddleware(): DittoMiddleware<{ pagination: Pagination; paginate: PaginateFn }>;
|
||||||
export function paginationMiddleware(
|
export function paginationMiddleware(
|
||||||
type: 'list',
|
opts: PaginationMiddlewareOpts & { type: 'list' },
|
||||||
): DittoMiddleware<{ pagination: ListPagination; paginate: ListPaginateFn }>;
|
): DittoMiddleware<{ pagination: ListPagination; paginate: ListPaginateFn }>;
|
||||||
export function paginationMiddleware(
|
export function paginationMiddleware(
|
||||||
type?: string,
|
opts?: PaginationMiddlewareOpts,
|
||||||
|
): DittoMiddleware<{ pagination: Pagination; paginate: PaginateFn }>;
|
||||||
|
export function paginationMiddleware(
|
||||||
|
opts: PaginationMiddlewareOpts = {},
|
||||||
): DittoMiddleware<{ pagination?: Pagination | ListPagination; paginate: PaginateFn | ListPaginateFn }> {
|
): DittoMiddleware<{ pagination?: Pagination | ListPagination; paginate: PaginateFn | ListPaginateFn }> {
|
||||||
return async (c, next) => {
|
return async (c, next) => {
|
||||||
const { relay } = c.var;
|
const { relay } = c.var;
|
||||||
|
|
||||||
const pagination = paginationSchema.parse(c.req.query());
|
const pagination = paginationSchema(opts).parse(c.req.query());
|
||||||
|
|
||||||
const {
|
const {
|
||||||
max_id: maxId,
|
max_id: maxId,
|
||||||
|
|
@ -59,7 +66,7 @@ export function paginationMiddleware(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'list') {
|
if (opts.type === 'list') {
|
||||||
c.set('pagination', {
|
c.set('pagination', {
|
||||||
limit: pagination.limit,
|
limit: pagination.limit,
|
||||||
offset: pagination.offset,
|
offset: pagination.offset,
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { assertEquals } from '@std/assert';
|
||||||
import { paginationSchema } from './schema.ts';
|
import { paginationSchema } from './schema.ts';
|
||||||
|
|
||||||
Deno.test('paginationSchema', () => {
|
Deno.test('paginationSchema', () => {
|
||||||
const pagination = paginationSchema.parse({
|
const pagination = paginationSchema().parse({
|
||||||
limit: '10',
|
limit: '10',
|
||||||
offset: '20',
|
offset: '20',
|
||||||
max_id: '1',
|
max_id: '1',
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,28 @@ export interface Pagination {
|
||||||
offset: number;
|
offset: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PaginationSchemaOpts {
|
||||||
|
limit?: number;
|
||||||
|
max?: number;
|
||||||
|
}
|
||||||
|
|
||||||
/** Schema to parse pagination query params. */
|
/** Schema to parse pagination query params. */
|
||||||
export const paginationSchema: z.ZodType<Pagination> = z.object({
|
export function paginationSchema(opts: PaginationSchemaOpts = {}): z.ZodType<Pagination> {
|
||||||
max_id: z.string().transform((val) => {
|
let { limit = 20, max = 40 } = opts;
|
||||||
if (!val.includes('-')) return val;
|
|
||||||
return val.split('-')[1];
|
if (limit > max) {
|
||||||
}).optional().catch(undefined),
|
max = limit;
|
||||||
min_id: z.string().optional().catch(undefined),
|
}
|
||||||
since: z.coerce.number().nonnegative().optional().catch(undefined),
|
|
||||||
until: z.coerce.number().nonnegative().optional().catch(undefined),
|
return z.object({
|
||||||
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
|
max_id: z.string().transform((val) => {
|
||||||
offset: z.coerce.number().nonnegative().catch(0),
|
if (!val.includes('-')) return val;
|
||||||
}) as z.ZodType<Pagination>;
|
return val.split('-')[1];
|
||||||
|
}).optional().catch(undefined),
|
||||||
|
min_id: z.string().optional().catch(undefined),
|
||||||
|
since: z.coerce.number().nonnegative().optional().catch(undefined),
|
||||||
|
until: z.coerce.number().nonnegative().optional().catch(undefined),
|
||||||
|
limit: z.coerce.number().catch(limit).transform((value) => Math.min(Math.max(value, 0), max)),
|
||||||
|
offset: z.coerce.number().nonnegative().catch(0),
|
||||||
|
}) as z.ZodType<Pagination>;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue