diff --git a/src/controllers/api/statuses.ts b/src/controllers/api/statuses.ts index e0956e74..486ea28b 100644 --- a/src/controllers/api/statuses.ts +++ b/src/controllers/api/statuses.ts @@ -1,6 +1,5 @@ import { HTTPException } from '@hono/hono/http-exception'; import { NostrEvent, NSchema as n } from '@nostrify/nostrify'; -import ISO6391 from 'iso-639-1'; import 'linkify-plugin-hashtag'; import linkify from 'linkifyjs'; import { nip19 } from 'nostr-tools'; @@ -14,6 +13,7 @@ import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts' import { addTag, deleteTag } from '@/utils/tags.ts'; import { asyncReplaceAll } from '@/utils/text.ts'; import { lookupPubkey } from '@/utils/lookup.ts'; +import { languageSchema } from '@/schema.ts'; import { Storages } from '@/storages.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { createEvent, paginated, paginatedList, parseBody, updateListEvent } from '@/utils/api.ts'; @@ -26,7 +26,7 @@ import { renderReblog, renderStatus } from '@/views/mastodon/statuses.ts'; const createStatusSchema = z.object({ in_reply_to_id: n.id().nullish(), - language: z.string().refine(ISO6391.validate).nullish(), + language: languageSchema.nullish(), media_ids: z.string().array().nullish(), poll: z.object({ options: z.string().array(), diff --git a/src/controllers/api/timelines.ts b/src/controllers/api/timelines.ts index 483676e1..8f783dd8 100644 --- a/src/controllers/api/timelines.ts +++ b/src/controllers/api/timelines.ts @@ -4,7 +4,7 @@ import { z } from 'zod'; import { type AppContext, type AppController } from '@/app.ts'; import { Conf } from '@/config.ts'; import { getFeedPubkeys } from '@/queries.ts'; -import { booleanParamSchema } from '@/schema.ts'; +import { booleanParamSchema, languageSchema } from '@/schema.ts'; import { hydrateEvents } from '@/storages/hydrate.ts'; import { paginated } from '@/utils/api.ts'; import { getTagSet } from '@/utils/tags.ts'; @@ -18,22 +18,37 @@ const homeTimelineController: AppController = async (c) => { }; const publicQuerySchema = z.object({ - local: booleanParamSchema.catch(false), - instance: z.string().optional().catch(undefined), + local: booleanParamSchema.default('false'), + instance: z.string().optional(), + language: languageSchema.optional(), }); const publicTimelineController: AppController = (c) => { const params = c.get('pagination'); - const { local, instance } = publicQuerySchema.parse(c.req.query()); + const result = publicQuerySchema.safeParse(c.req.query()); + + if (!result.success) { + return c.json({ error: 'Bad request', schema: result.error }, 400); + } + + const { local, instance, language } = result.data; const filter: NostrFilter = { kinds: [1], ...params }; + const search: `${string}:${string}`[] = []; + if (local) { - filter.search = `domain:${Conf.url.host}`; + search.push(`domain:${Conf.url.host}`); } else if (instance) { - filter.search = `domain:${instance}`; + search.push(`domain:${instance}`); } + if (language) { + search.push(`language:${language}`); + } + + filter.search = search.join(' '); + return renderStatuses(c, [filter]); }; diff --git a/src/schema.ts b/src/schema.ts index fc7efd01..5efa7769 100644 --- a/src/schema.ts +++ b/src/schema.ts @@ -1,3 +1,4 @@ +import ISO6391 from 'iso-639-1'; import { z } from 'zod'; /** Validates individual items in an array, dropping any that aren't valid. */ @@ -40,12 +41,24 @@ const fileSchema = z.custom((value) => value instanceof File); const percentageSchema = z.coerce.number().int().gte(1).lte(100); +const languageSchema = z.string().transform((val, ctx) => { + if (!ISO6391.validate(val)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Not a valid language in ISO-639-1 format', + }); + return z.NEVER; + } + return val; +}); + export { booleanParamSchema, decode64Schema, fileSchema, filteredArray, hashtagSchema, + languageSchema, percentageSchema, safeUrlSchema, };