Merge branch 'strip-schema' into 'main'

Remove zod schemas that we can get from NSchema

See merge request soapbox-pub/ditto!208
This commit is contained in:
Alex Gleason 2024-05-02 00:59:06 +00:00
commit a7cbd452e7
18 changed files with 66 additions and 205 deletions

View File

@ -1,4 +1,4 @@
import { NostrFilter } from '@nostrify/nostrify'; import { NostrFilter, NSchema as n } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
@ -6,7 +6,6 @@ import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { getAuthor, getFollowedPubkeys } from '@/queries.ts'; import { getAuthor, getFollowedPubkeys } from '@/queries.ts';
import { booleanParamSchema, fileSchema } from '@/schema.ts'; import { booleanParamSchema, fileSchema } from '@/schema.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts'; import { addTag, deleteTag, findReplyTag, getTagSet } from '@/tags.ts';
import { uploadFile } from '@/upload.ts'; import { uploadFile } from '@/upload.ts';
@ -198,7 +197,7 @@ const updateCredentialsController: AppController = async (c) => {
} }
const author = await getAuthor(pubkey); const author = await getAuthor(pubkey);
const meta = author ? jsonMetaContentSchema.parse(author.content) : {}; const meta = author ? n.json().pipe(n.metadata()).catch({}).parse(author.content) : {};
const { const {
avatar: avatarFile, avatar: avatarFile,

View File

@ -1,6 +1,8 @@
import { NSchema as n } from '@nostrify/nostrify';
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; import { serverMetaSchema } from '@/schemas/nostr.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
const instanceController: AppController = async (c) => { const instanceController: AppController = async (c) => {
@ -8,7 +10,7 @@ const instanceController: AppController = async (c) => {
const { signal } = c.req.raw; const { signal } = c.req.raw;
const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal });
const meta = jsonServerMetaSchema.parse(event?.content); const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content);
/** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */ /** Protocol to use for WebSocket URLs, depending on the protocol of the `LOCAL_DOMAIN`. */
const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:'; const wsProtocol = protocol === 'http:' ? 'ws:' : 'wss:';

View File

@ -1,3 +1,4 @@
import { NSchema as n } from '@nostrify/nostrify';
import { z } from 'zod'; import { z } from 'zod';
import { type AppController } from '@/app.ts'; import { type AppController } from '@/app.ts';
@ -6,7 +7,6 @@ import { configSchema, elixirTupleSchema, type PleromaConfig } from '@/schemas/p
import { AdminSigner } from '@/signers/AdminSigner.ts'; import { AdminSigner } from '@/signers/AdminSigner.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { createAdminEvent } from '@/utils/api.ts'; import { createAdminEvent } from '@/utils/api.ts';
import { jsonSchema } from '@/schema.ts';
const frontendConfigController: AppController = async (c) => { const frontendConfigController: AppController = async (c) => {
const configs = await getConfigs(c.req.raw.signal); const configs = await getConfigs(c.req.raw.signal);
@ -75,7 +75,7 @@ async function getConfigs(signal: AbortSignal): Promise<PleromaConfig[]> {
try { try {
const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content); const decrypted = await new AdminSigner().nip44.decrypt(Conf.pubkey, event.content);
return jsonSchema.pipe(configSchema.array()).catch([]).parse(decrypted); return n.json().pipe(configSchema.array()).catch([]).parse(decrypted);
} catch (_e) { } catch (_e) {
return []; return [];
} }

View File

@ -1,10 +1,9 @@
import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { z } from 'zod'; 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 { nostrIdSchema } from '@/schemas/nostr.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { dedupeEvents } from '@/utils.ts'; import { dedupeEvents } from '@/utils.ts';
import { nip05Cache } from '@/utils/nip05.ts'; import { nip05Cache } from '@/utils/nip05.ts';
@ -20,7 +19,7 @@ const searchQuerySchema = z.object({
type: z.enum(['accounts', 'statuses', 'hashtags']).optional(), type: z.enum(['accounts', 'statuses', 'hashtags']).optional(),
resolve: booleanParamSchema.optional().transform(Boolean), resolve: booleanParamSchema.optional().transform(Boolean),
following: z.boolean().default(false), following: z.boolean().default(false),
account_id: nostrIdSchema.optional(), account_id: n.id().optional(),
limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)), limit: z.coerce.number().catch(20).transform((value) => Math.min(Math.max(value, 0), 40)),
}); });

View File

@ -1,4 +1,4 @@
import { NIP05, NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { NIP05, NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
import ISO6391 from 'iso-639-1'; import ISO6391 from 'iso-639-1';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
@ -7,7 +7,6 @@ import { type AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { getUnattachedMediaByIds } from '@/db/unattached-media.ts'; import { getUnattachedMediaByIds } from '@/db/unattached-media.ts';
import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts'; import { getAncestors, getAuthor, getDescendants, getEvent } from '@/queries.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { addTag, deleteTag } from '@/tags.ts'; import { addTag, deleteTag } from '@/tags.ts';
import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts'; import { createEvent, paginationSchema, parseBody, updateListEvent } from '@/utils/api.ts';
import { renderEventAccounts } from '@/views.ts'; import { renderEventAccounts } from '@/views.ts';
@ -406,7 +405,7 @@ const zapController: AppController = async (c) => {
const target = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'], signal }); const target = await getEvent(id, { kind: 1, relations: ['author', 'event_stats', 'author_stats'], signal });
const author = target?.author; const author = target?.author;
const meta = jsonMetaContentSchema.parse(author?.content); const meta = n.json().pipe(n.metadata()).catch({}).parse(author?.content);
const lnurl = getLnurl(meta); const lnurl = getLnurl(meta);
if (target && lnurl) { if (target && lnurl) {

View File

@ -1,12 +1,14 @@
import { NSchema as n } from '@nostrify/nostrify';
import { AppController } from '@/app.ts'; import { AppController } from '@/app.ts';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { jsonServerMetaSchema } from '@/schemas/nostr.ts'; import { serverMetaSchema } from '@/schemas/nostr.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
const relayInfoController: AppController = async (c) => { const relayInfoController: AppController = async (c) => {
const { signal } = c.req.raw; const { signal } = c.req.raw;
const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal }); const [event] = await Storages.db.query([{ kinds: [0], authors: [Conf.pubkey], limit: 1 }], { signal });
const meta = jsonServerMetaSchema.parse(event?.content); const meta = n.json().pipe(serverMetaSchema).catch({}).parse(event?.content);
return c.json({ return c.json({
name: meta.name ?? 'Ditto', name: meta.name ?? 'Ditto',

View File

@ -1,14 +1,15 @@
import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify'; import {
NostrClientCLOSE,
NostrClientCOUNT,
NostrClientEVENT,
NostrClientMsg,
NostrClientREQ,
NostrEvent,
NostrFilter,
NSchema as n,
} from '@nostrify/nostrify';
import { relayInfoController } from '@/controllers/nostr/relay-info.ts'; import { relayInfoController } from '@/controllers/nostr/relay-info.ts';
import * as pipeline from '@/pipeline.ts'; import * as pipeline from '@/pipeline.ts';
import {
type ClientCLOSE,
type ClientCOUNT,
type ClientEVENT,
type ClientMsg,
clientMsgSchema,
type ClientREQ,
} from '@/schemas/nostr.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import type { AppController } from '@/app.ts'; import type { AppController } from '@/app.ts';
@ -30,7 +31,7 @@ function connectStream(socket: WebSocket) {
const controllers = new Map<string, AbortController>(); const controllers = new Map<string, AbortController>();
socket.onmessage = (e) => { socket.onmessage = (e) => {
const result = n.json().pipe(clientMsgSchema).safeParse(e.data); const result = n.json().pipe(n.clientMsg()).safeParse(e.data);
if (result.success) { if (result.success) {
handleMsg(result.data); handleMsg(result.data);
} else { } else {
@ -45,7 +46,7 @@ function connectStream(socket: WebSocket) {
}; };
/** Handle client message. */ /** Handle client message. */
function handleMsg(msg: ClientMsg) { function handleMsg(msg: NostrClientMsg) {
switch (msg[0]) { switch (msg[0]) {
case 'REQ': case 'REQ':
handleReq(msg); handleReq(msg);
@ -63,7 +64,7 @@ function connectStream(socket: WebSocket) {
} }
/** Handle REQ. Start a subscription. */ /** Handle REQ. Start a subscription. */
async function handleReq([_, subId, ...rest]: ClientREQ): Promise<void> { async function handleReq([_, subId, ...rest]: NostrClientREQ): Promise<void> {
const filters = prepareFilters(rest); const filters = prepareFilters(rest);
const controller = new AbortController(); const controller = new AbortController();
@ -88,7 +89,7 @@ function connectStream(socket: WebSocket) {
} }
/** Handle EVENT. Store the event. */ /** Handle EVENT. Store the event. */
async function handleEvent([_, event]: ClientEVENT): Promise<void> { async function handleEvent([_, event]: NostrClientEVENT): Promise<void> {
try { try {
// This will store it (if eligible) and run other side-effects. // This will store it (if eligible) and run other side-effects.
await pipeline.handleEvent(event, AbortSignal.timeout(1000)); await pipeline.handleEvent(event, AbortSignal.timeout(1000));
@ -104,7 +105,7 @@ function connectStream(socket: WebSocket) {
} }
/** Handle CLOSE. Close the subscription. */ /** Handle CLOSE. Close the subscription. */
function handleClose([_, subId]: ClientCLOSE): void { function handleClose([_, subId]: NostrClientCLOSE): void {
const controller = controllers.get(subId); const controller = controllers.get(subId);
if (controller) { if (controller) {
controller.abort(); controller.abort();
@ -113,7 +114,7 @@ function connectStream(socket: WebSocket) {
} }
/** Handle COUNT. Return the number of events matching the filters. */ /** Handle COUNT. Return the number of events matching the filters. */
async function handleCount([_, subId, ...rest]: ClientCOUNT): Promise<void> { async function handleCount([_, subId, ...rest]: NostrClientCOUNT): Promise<void> {
const { count } = await Storages.db.count(prepareFilters(rest)); const { count } = await Storages.db.count(prepareFilters(rest));
send(['COUNT', subId, { count, approximate: false }]); send(['COUNT', subId, { count, approximate: false }]);
} }
@ -127,7 +128,7 @@ function connectStream(socket: WebSocket) {
} }
/** Enforce the filters with certain criteria. */ /** Enforce the filters with certain criteria. */
function prepareFilters(filters: ClientREQ[2][]): NostrFilter[] { function prepareFilters(filters: NostrClientREQ[2][]): NostrFilter[] {
return filters.map((filter) => { return filters.map((filter) => {
const narrow = Boolean(filter.ids?.length || filter.authors?.length); const narrow = Boolean(filter.ids?.length || filter.authors?.length);
const search = narrow ? filter.search : `domain:${Conf.url.host} ${filter.search ?? ''}`; const search = narrow ? filter.search : `domain:${Conf.url.host} ${filter.search ?? ''}`;

View File

@ -1,9 +1,8 @@
import { NostrEvent, NostrFilter } from '@nostrify/nostrify'; import { NostrEvent, NostrFilter, NSchema as n } from '@nostrify/nostrify';
import stringifyStable from 'fast-stable-stringify'; import stringifyStable from 'fast-stable-stringify';
import { z } from 'zod'; import { z } from 'zod';
import { isReplaceableKind } from '@/kinds.ts'; import { isReplaceableKind } from '@/kinds.ts';
import { nostrIdSchema } from '@/schemas/nostr.ts';
/** Microfilter to get one specific event by ID. */ /** Microfilter to get one specific event by ID. */
type IdMicrofilter = { ids: [NostrEvent['id']] }; type IdMicrofilter = { ids: [NostrEvent['id']] };
@ -42,8 +41,8 @@ function getMicroFilters(event: NostrEvent): MicroFilter[] {
/** Microfilter schema. */ /** Microfilter schema. */
const microFilterSchema = z.union([ const microFilterSchema = z.union([
z.object({ ids: z.tuple([nostrIdSchema]) }).strict(), z.object({ ids: z.tuple([n.id()]) }).strict(),
z.object({ kinds: z.tuple([z.literal(0)]), authors: z.tuple([nostrIdSchema]) }).strict(), z.object({ kinds: z.tuple([z.literal(0)]), authors: z.tuple([n.id()]) }).strict(),
]); ]);
/** Checks whether the filter is a microfilter. */ /** Checks whether the filter is a microfilter. */

View File

@ -111,7 +111,7 @@ async function parseMetadata(event: NostrEvent, signal: AbortSignal): Promise<vo
if (event.kind !== 0) return; if (event.kind !== 0) return;
// Parse metadata. // Parse metadata.
const metadata = n.json().pipe(n.metadata()).safeParse(event.content); const metadata = n.json().pipe(n.metadata()).catch({}).safeParse(event.content);
if (!metadata.success) return; if (!metadata.success) return;
// Get nip05. // Get nip05.

View File

@ -11,16 +11,6 @@ function filteredArray<T extends z.ZodTypeAny>(schema: T) {
)); ));
} }
/** Parses a JSON string into its native type. */
const jsonSchema = z.string().transform((value, ctx) => {
try {
return JSON.parse(value) as unknown;
} catch (_e) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid JSON' });
return z.NEVER;
}
});
/** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */ /** https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem */
const decode64Schema = z.string().transform((value, ctx) => { const decode64Schema = z.string().transform((value, ctx) => {
try { try {
@ -48,4 +38,4 @@ const booleanParamSchema = z.enum(['true', 'false']).transform((value) => value
/** Schema for `File` objects. */ /** Schema for `File` objects. */
const fileSchema = z.custom<File>((value) => value instanceof File); const fileSchema = z.custom<File>((value) => value instanceof File);
export { booleanParamSchema, decode64Schema, fileSchema, filteredArray, hashtagSchema, jsonSchema, safeUrlSchema }; export { booleanParamSchema, decode64Schema, fileSchema, filteredArray, hashtagSchema, safeUrlSchema };

View File

@ -1,80 +1,14 @@
import { NSchema as n } from '@nostrify/nostrify';
import { getEventHash, verifyEvent } from 'nostr-tools'; import { getEventHash, verifyEvent } from 'nostr-tools';
import { z } from 'zod'; import { z } from 'zod';
import { jsonSchema, safeUrlSchema } from '@/schema.ts'; import { safeUrlSchema } from '@/schema.ts';
/** Schema to validate Nostr hex IDs such as event IDs and pubkeys. */
const nostrIdSchema = z.string().regex(/^[0-9a-f]{64}$/);
/** Nostr kinds are positive integers. */
const kindSchema = z.number().int().nonnegative();
/** Nostr event schema. */
const eventSchema = z.object({
id: nostrIdSchema,
kind: kindSchema,
tags: z.array(z.array(z.string())),
content: z.string(),
created_at: z.number(),
pubkey: nostrIdSchema,
sig: z.string(),
});
/** Nostr event schema that also verifies the event's signature. */ /** Nostr event schema that also verifies the event's signature. */
const signedEventSchema = eventSchema const signedEventSchema = n.event()
.refine((event) => event.id === getEventHash(event), 'Event ID does not match hash') .refine((event) => event.id === getEventHash(event), 'Event ID does not match hash')
.refine(verifyEvent, 'Event signature is invalid'); .refine(verifyEvent, 'Event signature is invalid');
/** Nostr relay filter schema. */
const filterSchema = z.object({
kinds: kindSchema.array().optional(),
ids: nostrIdSchema.array().optional(),
authors: nostrIdSchema.array().optional(),
since: z.number().int().nonnegative().optional(),
until: z.number().int().nonnegative().optional(),
limit: z.number().int().nonnegative().optional(),
search: z.string().optional(),
}).passthrough().and(
z.record(
z.custom<`#${string}`>((val) => typeof val === 'string' && val.startsWith('#')),
z.string().array(),
).catch({}),
);
const clientReqSchema = z.tuple([z.literal('REQ'), z.string().min(1)]).rest(filterSchema);
const clientEventSchema = z.tuple([z.literal('EVENT'), signedEventSchema]);
const clientCloseSchema = z.tuple([z.literal('CLOSE'), z.string().min(1)]);
const clientCountSchema = z.tuple([z.literal('COUNT'), z.string().min(1)]).rest(filterSchema);
/** Client message to a Nostr relay. */
const clientMsgSchema = z.union([
clientReqSchema,
clientEventSchema,
clientCloseSchema,
clientCountSchema,
]);
/** REQ message from client to relay. */
type ClientREQ = z.infer<typeof clientReqSchema>;
/** EVENT message from client to relay. */
type ClientEVENT = z.infer<typeof clientEventSchema>;
/** CLOSE message from client to relay. */
type ClientCLOSE = z.infer<typeof clientCloseSchema>;
/** COUNT message from client to relay. */
type ClientCOUNT = z.infer<typeof clientCountSchema>;
/** Client message to a Nostr relay. */
type ClientMsg = z.infer<typeof clientMsgSchema>;
/** Kind 0 content schema. */
const metaContentSchema = z.object({
name: z.string().optional().catch(undefined),
about: z.string().optional().catch(undefined),
picture: z.string().optional().catch(undefined),
banner: z.string().optional().catch(undefined),
nip05: z.string().optional().catch(undefined),
lud06: z.string().optional().catch(undefined),
lud16: z.string().optional().catch(undefined),
}).partial().passthrough();
/** Media data schema from `"media"` tags. */ /** Media data schema from `"media"` tags. */
const mediaDataSchema = z.object({ const mediaDataSchema = z.object({
blurhash: z.string().optional().catch(undefined), blurhash: z.string().optional().catch(undefined),
@ -88,40 +22,25 @@ const mediaDataSchema = z.object({
}); });
/** Kind 0 content schema for the Ditto server admin user. */ /** Kind 0 content schema for the Ditto server admin user. */
const serverMetaSchema = metaContentSchema.extend({ const serverMetaSchema = n.metadata().and(z.object({
tagline: z.string().optional().catch(undefined), tagline: z.string().optional().catch(undefined),
email: z.string().optional().catch(undefined), email: z.string().optional().catch(undefined),
}); }));
/** Media data from `"media"` tags. */ /** Media data from `"media"` tags. */
type MediaData = z.infer<typeof mediaDataSchema>; type MediaData = z.infer<typeof mediaDataSchema>;
/** Parses kind 0 content from a JSON string. */
const jsonMetaContentSchema = jsonSchema.pipe(metaContentSchema).catch({});
/** Parses media data from a JSON string. */
const jsonMediaDataSchema = jsonSchema.pipe(mediaDataSchema).catch({});
/** Parses server admin meta from a JSON string. */
const jsonServerMetaSchema = jsonSchema.pipe(serverMetaSchema).catch({});
/** NIP-11 Relay Information Document. */ /** NIP-11 Relay Information Document. */
const relayInfoDocSchema = z.object({ const relayInfoDocSchema = z.object({
name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined), name: z.string().transform((val) => val.slice(0, 30)).optional().catch(undefined),
description: z.string().transform((val) => val.slice(0, 3000)).optional().catch(undefined), description: z.string().transform((val) => val.slice(0, 3000)).optional().catch(undefined),
pubkey: nostrIdSchema.optional().catch(undefined), pubkey: n.id().optional().catch(undefined),
contact: safeUrlSchema.optional().catch(undefined), contact: safeUrlSchema.optional().catch(undefined),
supported_nips: z.number().int().nonnegative().array().optional().catch(undefined), supported_nips: z.number().int().nonnegative().array().optional().catch(undefined),
software: safeUrlSchema.optional().catch(undefined), software: safeUrlSchema.optional().catch(undefined),
icon: safeUrlSchema.optional().catch(undefined), icon: safeUrlSchema.optional().catch(undefined),
}); });
/** NIP-46 signer response. */
const connectResponseSchema = z.object({
id: z.string(),
result: signedEventSchema,
});
/** Parses a Nostr emoji tag. */ /** Parses a Nostr emoji tag. */
const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()]); const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()]);
@ -129,23 +48,11 @@ const emojiTagSchema = z.tuple([z.literal('emoji'), z.string(), z.string().url()
type EmojiTag = z.infer<typeof emojiTagSchema>; type EmojiTag = z.infer<typeof emojiTagSchema>;
export { export {
type ClientCLOSE,
type ClientCOUNT,
type ClientEVENT,
type ClientMsg,
clientMsgSchema,
type ClientREQ,
connectResponseSchema,
type EmojiTag, type EmojiTag,
emojiTagSchema, emojiTagSchema,
filterSchema,
jsonMediaDataSchema,
jsonMetaContentSchema,
jsonServerMetaSchema,
type MediaData, type MediaData,
mediaDataSchema, mediaDataSchema,
metaContentSchema,
nostrIdSchema,
relayInfoDocSchema, relayInfoDocSchema,
serverMetaSchema,
signedEventSchema, signedEventSchema,
}; };

View File

@ -1,4 +1,4 @@
import { NIP50, NostrEvent, NostrFilter, NStore } from '@nostrify/nostrify'; import { NIP50, NostrEvent, NostrFilter, NSchema as n, NStore } from '@nostrify/nostrify';
import Debug from '@soapbox/stickynotes/debug'; import Debug from '@soapbox/stickynotes/debug';
import { Kysely, type SelectQueryBuilder } from 'kysely'; import { Kysely, type SelectQueryBuilder } from 'kysely';
@ -7,7 +7,6 @@ import { DittoTables } from '@/db/DittoTables.ts';
import { normalizeFilters } from '@/filter.ts'; import { normalizeFilters } from '@/filter.ts';
import { DittoEvent } from '@/interfaces/DittoEvent.ts'; import { DittoEvent } from '@/interfaces/DittoEvent.ts';
import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts'; import { isDittoInternalKind, isParameterizedReplaceableKind, isReplaceableKind } from '@/kinds.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { purifyEvent } from '@/storages/hydrate.ts'; import { purifyEvent } from '@/storages/hydrate.ts';
import { isNostrId, isURL } from '@/utils.ts'; import { isNostrId, isURL } from '@/utils.ts';
import { abortError } from '@/utils/abort.ts'; import { abortError } from '@/utils/abort.ts';
@ -412,7 +411,7 @@ function buildSearchContent(event: NostrEvent): string {
/** Build search content for a user. */ /** Build search content for a user. */
function buildUserSearchContent(event: NostrEvent): string { function buildUserSearchContent(event: NostrEvent): string {
const { name, nip05, about } = jsonMetaContentSchema.parse(event.content); const { name, nip05, about } = n.json().pipe(n.metadata()).catch({}).parse(event.content);
return [name, nip05, about].filter(Boolean).join('\n'); return [name, nip05, about].filter(Boolean).join('\n');
} }

View File

@ -1,16 +1,13 @@
import { NostrEvent } from '@nostrify/nostrify'; import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
import { EventTemplate, getEventHash, nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { z } from 'zod';
import { nostrIdSchema } from '@/schemas/nostr.ts';
/** Get the current time in Nostr format. */ /** Get the current time in Nostr format. */
const nostrNow = (): number => Math.floor(Date.now() / 1000); const nostrNow = (): number => Math.floor(Date.now() / 1000);
/** Convenience function to convert Nostr dates into native Date objects. */ /** Convenience function to convert Nostr dates into native Date objects. */
const nostrDate = (seconds: number): Date => new Date(seconds * 1000); const nostrDate = (seconds: number): Date => new Date(seconds * 1000);
/** Pass to sort() to sort events by date. */
const eventDateComparator = (a: NostrEvent, b: NostrEvent): number => b.created_at - a.created_at;
/** Get pubkey from bech32 string, if applicable. */ /** Get pubkey from bech32 string, if applicable. */
function bech32ToPubkey(bech32: string): string | undefined { function bech32ToPubkey(bech32: string): string | undefined {
try { try {
@ -86,54 +83,20 @@ function dedupeEvents(events: NostrEvent[]): NostrEvent[] {
return [...new Map(events.map((event) => [event.id, event])).values()]; return [...new Map(events.map((event) => [event.id, event])).values()];
} }
/** Return a copy of the event with the given tags removed. */
function stripTags<E extends EventTemplate>(event: E, tags: string[] = []): E {
if (!tags.length) return event;
return {
...event,
tags: event.tags.filter(([name]) => !tags.includes(name)),
};
}
/** Ensure the template and event match on their shared keys. */
function eventMatchesTemplate(event: NostrEvent, template: EventTemplate): boolean {
const whitelist = ['nonce'];
event = stripTags(event, whitelist);
template = stripTags(template, whitelist);
if (template.created_at > event.created_at) {
return false;
}
return getEventHash(event) === getEventHash({
pubkey: event.pubkey,
...template,
created_at: event.created_at,
});
}
/** 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 nostrIdSchema.safeParse(value).success; return n.id().safeParse(value).success;
} }
/** Test whether the value is a URL. */ /** Test whether the value is a URL. */
function isURL(value: unknown): boolean { function isURL(value: unknown): boolean {
try { return z.string().url().safeParse(value).success;
new URL(value as string);
return true;
} catch (_) {
return false;
}
} }
export { export {
bech32ToPubkey, bech32ToPubkey,
dedupeEvents, dedupeEvents,
eventAge, eventAge,
eventDateComparator,
eventMatchesTemplate,
findTag, findTag,
isNostrId, isNostrId,
isURL, isURL,

View File

@ -1,13 +1,13 @@
import { NostrEvent } from '@nostrify/nostrify'; import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
import { EventTemplate, nip13 } from 'nostr-tools'; import { EventTemplate, nip13 } from 'nostr-tools';
import { decode64Schema, jsonSchema } from '@/schema.ts'; import { decode64Schema } from '@/schema.ts';
import { signedEventSchema } from '@/schemas/nostr.ts'; import { signedEventSchema } from '@/schemas/nostr.ts';
import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts'; import { eventAge, findTag, nostrNow, sha256 } from '@/utils.ts';
import { Time } from '@/utils/time.ts'; import { Time } from '@/utils/time.ts';
/** Decode a Nostr event from a base64 encoded string. */ /** Decode a Nostr event from a base64 encoded string. */
const decode64EventSchema = decode64Schema.pipe(jsonSchema).pipe(signedEventSchema); const decode64EventSchema = decode64Schema.pipe(n.json()).pipe(signedEventSchema);
interface ParseAuthRequestOpts { interface ParseAuthRequestOpts {
/** Max event age (in ms). */ /** Max event age (in ms). */

View File

@ -1,5 +1,6 @@
import { NSchema as n } from '@nostrify/nostrify';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { getPublicKeyPem } from '@/utils/rsa.ts'; import { getPublicKeyPem } from '@/utils/rsa.ts';
import type { NostrEvent } from '@nostrify/nostrify'; import type { NostrEvent } from '@nostrify/nostrify';
@ -7,7 +8,7 @@ import type { Actor } from '@/schemas/activitypub.ts';
/** Nostr metadata event to ActivityPub actor. */ /** Nostr metadata event to ActivityPub actor. */
async function renderActor(event: NostrEvent, username: string): Promise<Actor | undefined> { async function renderActor(event: NostrEvent, username: string): Promise<Actor | undefined> {
const content = jsonMetaContentSchema.parse(event.content); const content = n.json().pipe(n.metadata()).catch({}).parse(event.content);
return { return {
type: 'Person', type: 'Person',

View File

@ -1,9 +1,9 @@
import { NSchema as n } from '@nostrify/nostrify';
import { nip19, UnsignedEvent } from 'nostr-tools'; import { nip19, UnsignedEvent } from 'nostr-tools';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { lodash } from '@/deps.ts'; import { lodash } from '@/deps.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { jsonMetaContentSchema } from '@/schemas/nostr.ts';
import { getLnurl } from '@/utils/lnurl.ts'; import { getLnurl } from '@/utils/lnurl.ts';
import { nip05Cache } from '@/utils/nip05.ts'; import { nip05Cache } from '@/utils/nip05.ts';
import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts'; import { Nip05, nostrDate, nostrNow, parseNip05 } from '@/utils.ts';
@ -28,7 +28,7 @@ async function renderAccount(
about, about,
lud06, lud06,
lud16, lud16,
} = jsonMetaContentSchema.parse(event.content); } = n.json().pipe(n.metadata()).catch({}).parse(event.content);
const npub = nip19.npubEncode(pubkey); const npub = nip19.npubEncode(pubkey);
const parsed05 = await parseAndVerifyNip05(nip05, pubkey); const parsed05 = await parseAndVerifyNip05(nip05, pubkey);

View File

@ -1,11 +1,10 @@
import { NostrEvent } from '@nostrify/nostrify'; import { NostrEvent, NSchema as n } from '@nostrify/nostrify';
import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts'; import { isCWTag } from 'https://gitlab.com/soapbox-pub/mostr/-/raw/c67064aee5ade5e01597c6d23e22e53c628ef0e2/src/nostr/tags.ts';
import { nip19 } from 'nostr-tools'; import { nip19 } from 'nostr-tools';
import { Conf } from '@/config.ts'; import { Conf } from '@/config.ts';
import { type DittoEvent } from '@/interfaces/DittoEvent.ts'; import { type DittoEvent } from '@/interfaces/DittoEvent.ts';
import { getMediaLinks, parseNoteContent } from '@/note.ts'; import { getMediaLinks, parseNoteContent } from '@/note.ts';
import { jsonMediaDataSchema } from '@/schemas/nostr.ts';
import { Storages } from '@/storages.ts'; import { Storages } from '@/storages.ts';
import { findReplyTag } from '@/tags.ts'; import { findReplyTag } from '@/tags.ts';
import { nostrDate } from '@/utils.ts'; import { nostrDate } from '@/utils.ts';
@ -13,6 +12,7 @@ import { unfurlCardCached } from '@/utils/unfurl.ts';
import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts'; import { accountFromPubkey, renderAccount } from '@/views/mastodon/accounts.ts';
import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts'; import { DittoAttachment, renderAttachment } from '@/views/mastodon/attachments.ts';
import { renderEmojis } from '@/views/mastodon/emojis.ts'; import { renderEmojis } from '@/views/mastodon/emojis.ts';
import { mediaDataSchema } from '@/schemas/nostr.ts';
interface statusOpts { interface statusOpts {
viewerPubkey?: string; viewerPubkey?: string;
@ -78,7 +78,7 @@ async function renderStatus(event: DittoEvent, opts: statusOpts): Promise<any> {
const mediaTags: DittoAttachment[] = event.tags const mediaTags: DittoAttachment[] = event.tags
.filter((tag) => tag[0] === 'media') .filter((tag) => tag[0] === 'media')
.map(([_, url, json]) => ({ url, data: jsonMediaDataSchema.parse(json) })); .map(([_, url, json]) => ({ url, data: n.json().pipe(mediaDataSchema).parse(json) }));
const media = [...mediaLinks, ...mediaTags]; const media = [...mediaLinks, ...mediaTags];

View File

@ -1,8 +1,8 @@
import { NSchema } from '@nostrify/nostrify';
import * as Comlink from 'comlink'; import * as Comlink from 'comlink';
import { Sqlite } from '@/deps.ts'; import { Sqlite } from '@/deps.ts';
import { hashtagSchema } from '@/schema.ts'; import { hashtagSchema } from '@/schema.ts';
import { nostrIdSchema } from '@/schemas/nostr.ts';
import { generateDateRange, Time } from '@/utils/time.ts'; import { generateDateRange, Time } from '@/utils/time.ts';
interface GetTrendingTagsOpts { interface GetTrendingTagsOpts {
@ -102,7 +102,7 @@ export const TrendsWorker = {
}, },
addTagUsages(pubkey: string, hashtags: string[], date = new Date()): void { addTagUsages(pubkey: string, hashtags: string[], date = new Date()): void {
const pubkey8 = nostrIdSchema.parse(pubkey).substring(0, 8); const pubkey8 = NSchema.id().parse(pubkey).substring(0, 8);
const tags = hashtagSchema.array().min(1).parse(hashtags); const tags = hashtagSchema.array().min(1).parse(hashtags);
db.query( db.query(