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:
commit
a7cbd452e7
|
@ -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,
|
||||||
|
|
|
@ -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:';
|
||||||
|
|
|
@ -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 [];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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 ?? ''}`;
|
||||||
|
|
|
@ -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. */
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
49
src/utils.ts
49
src/utils.ts
|
@ -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,
|
||||||
|
|
|
@ -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). */
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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];
|
||||||
|
|
||||||
|
|
|
@ -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(
|
||||||
|
|
Loading…
Reference in New Issue