mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-26 19:47:25 +02:00
Finish Event console
This commit is contained in:
@@ -2,4 +2,4 @@
|
|||||||
"nostrudel": minor
|
"nostrudel": minor
|
||||||
---
|
---
|
||||||
|
|
||||||
Added simple query events tool
|
Added Event Console tool
|
||||||
|
@@ -24,7 +24,9 @@
|
|||||||
"@chakra-ui/react": "^2.8.2",
|
"@chakra-ui/react": "^2.8.2",
|
||||||
"@chakra-ui/shared-utils": "^2.0.4",
|
"@chakra-ui/shared-utils": "^2.0.4",
|
||||||
"@chakra-ui/styled-system": "^2.9.2",
|
"@chakra-ui/styled-system": "^2.9.2",
|
||||||
|
"@codemirror/autocomplete": "^6.12.0",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
|
"@codemirror/language": "^6.10.1",
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.1",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@getalby/bitcoin-connect": "^3.2.1",
|
"@getalby/bitcoin-connect": "^3.2.1",
|
||||||
|
@@ -3,7 +3,7 @@ import { useAsync } from "react-use";
|
|||||||
|
|
||||||
import db from "../../services/db";
|
import db from "../../services/db";
|
||||||
|
|
||||||
export type UserDirectory = { pubkey: string; names: [] }[];
|
export type UserDirectory = { pubkey: string; names: string[] }[];
|
||||||
export type GetDirectoryFn = () => UserDirectory;
|
export type GetDirectoryFn = () => UserDirectory;
|
||||||
const UserSearchDirectoryContext = createContext<GetDirectoryFn>(() => []);
|
const UserSearchDirectoryContext = createContext<GetDirectoryFn>(() => []);
|
||||||
|
|
||||||
|
@@ -23,7 +23,7 @@ export default function ListMenu({ list, ...props }: { list: NostrEvent } & Omit
|
|||||||
{!isSpecial && <DeleteEventMenuItem event={list} label="Delete List" />}
|
{!isSpecial && <DeleteEventMenuItem event={list} label="Delete List" />}
|
||||||
{hasPeople && (
|
{hasPeople && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
icon={<Image w="4" h="4" src="https://www.makeprisms.com/favicon.ico" />}
|
icon={<Image w="4" h="4" src="https://framerusercontent.com/images/3S3Pyvkh2tEvvKyX47QrUq7XQLk.png" />}
|
||||||
onClick={() => window.open(`https://www.makeprisms.com/create/${naddr}`, "_blank")}
|
onClick={() => window.open(`https://www.makeprisms.com/create/${naddr}`, "_blank")}
|
||||||
>
|
>
|
||||||
Create $prism
|
Create $prism
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import { Code, Flex, Text, useDisclosure } from "@chakra-ui/react";
|
import { Box, Code, Flex, Heading, Text, useDisclosure } from "@chakra-ui/react";
|
||||||
import { NostrEvent } from "nostr-tools";
|
import { NostrEvent } from "nostr-tools";
|
||||||
import ExpandButton from "./expand-button";
|
import ExpandButton from "./expand-button";
|
||||||
import UserName from "../../../components/user-name";
|
import UserName from "../../../components/user-name";
|
||||||
|
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||||
|
import Timestamp from "../../../components/timestamp";
|
||||||
|
|
||||||
export default function EventRow({ event }: { event: NostrEvent }) {
|
export default function EventRow({ event }: { event: NostrEvent }) {
|
||||||
const expanded = useDisclosure();
|
const expanded = useDisclosure();
|
||||||
@@ -31,9 +33,38 @@ export default function EventRow({ event }: { event: NostrEvent }) {
|
|||||||
|
|
||||||
{expanded.isOpen && (
|
{expanded.isOpen && (
|
||||||
<Flex gap="2" direction="column" px="2" pb="2" alignItems="flex-start">
|
<Flex gap="2" direction="column" px="2" pb="2" alignItems="flex-start">
|
||||||
<Code whiteSpace="pre-wrap" noOfLines={4} w="full">
|
<Flex gap="2">
|
||||||
|
<Text>ID: </Text>
|
||||||
|
<Code>{event.id}</Code>
|
||||||
|
<CopyIconButton text={event.id} aria-label="Copy ID" title="Copy ID" size="xs" />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Text>Pubkey: </Text>
|
||||||
|
<Code>{event.pubkey}</Code>
|
||||||
|
<CopyIconButton text={event.pubkey} aria-label="Copy Pubkey" title="Copy Pubkey" size="xs" />
|
||||||
|
</Flex>
|
||||||
|
<Flex gap="2">
|
||||||
|
<Text>Created: </Text>
|
||||||
|
<Code>{event.created_at}</Code>
|
||||||
|
<Timestamp timestamp={event.created_at} />
|
||||||
|
</Flex>
|
||||||
|
<Code whiteSpace="pre-wrap" w="full">
|
||||||
{event.content}
|
{event.content}
|
||||||
</Code>
|
</Code>
|
||||||
|
<Box>
|
||||||
|
<Text>Tags:</Text>
|
||||||
|
<Flex gap="2" direction="column">
|
||||||
|
{event.tags.map((t, i) => (
|
||||||
|
<Flex key={t.join(",") + i} gap="1" wrap="wrap">
|
||||||
|
{t.map((v, ii) => (
|
||||||
|
<Code key={v} minW="1rem" fontFamily="monospace" fontWeight={ii === 0 ? "bold" : "normal"}>
|
||||||
|
{v}
|
||||||
|
</Code>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
51
src/views/tools/event-console/help-modal.tsx
Normal file
51
src/views/tools/event-console/help-modal.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
Code,
|
||||||
|
Flex,
|
||||||
|
Heading,
|
||||||
|
Modal,
|
||||||
|
ModalBody,
|
||||||
|
ModalCloseButton,
|
||||||
|
ModalContent,
|
||||||
|
ModalHeader,
|
||||||
|
ModalOverlay,
|
||||||
|
ModalProps,
|
||||||
|
Text,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
|
|
||||||
|
export default function HelpModal({ isOpen, onClose, ...props }: Omit<ModalProps, "children">) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={onClose} size="lg" {...props}>
|
||||||
|
<ModalOverlay />
|
||||||
|
<ModalContent>
|
||||||
|
<ModalHeader p="4">Help</ModalHeader>
|
||||||
|
<ModalCloseButton />
|
||||||
|
<ModalBody px="4" pt="0" pb="4">
|
||||||
|
<Heading size="sm">Keyboard shortcuts</Heading>
|
||||||
|
<Text>
|
||||||
|
<Code>Ctrl+Shift+Enter</Code>: Run Filter
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Heading size="sm" mt="4">
|
||||||
|
Pubkeys
|
||||||
|
</Heading>
|
||||||
|
<Text>
|
||||||
|
Typing <Code>@</Code> inside any string will autocomplete with a list of users the app has seen
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Heading size="sm" mt="4">
|
||||||
|
Dates
|
||||||
|
</Heading>
|
||||||
|
<Text>
|
||||||
|
<Code>since</Code> and <Code>until</Code> fields can both take relative times in the form of strings
|
||||||
|
</Text>
|
||||||
|
<Text>Examples:</Text>
|
||||||
|
<Flex gap="1">
|
||||||
|
{["now", "n-3h", "n-5", "n+4m", "n-7d", "n-30s", "n-4w"].map((t) => (
|
||||||
|
<Code key={t}>{t}</Code>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
</ModalBody>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
@@ -11,6 +11,7 @@ import {
|
|||||||
Flex,
|
Flex,
|
||||||
Heading,
|
Heading,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
useColorMode,
|
useColorMode,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
@@ -18,24 +19,65 @@ import {
|
|||||||
import ReactCodeMirror from "@uiw/react-codemirror";
|
import ReactCodeMirror from "@uiw/react-codemirror";
|
||||||
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
import { githubLight, githubDark } from "@uiw/codemirror-theme-github";
|
||||||
import { jsonSchema } from "codemirror-json-schema";
|
import { jsonSchema } from "codemirror-json-schema";
|
||||||
import { Filter, NostrEvent } from "nostr-tools";
|
import { NostrEvent, Relay, Subscription } from "nostr-tools";
|
||||||
import { keymap } from "@codemirror/view";
|
import { keymap } from "@codemirror/view";
|
||||||
import { useLocalStorage } from "react-use";
|
import { useInterval, useLocalStorage } from "react-use";
|
||||||
|
import { Subscription as IDBSubscription, CacheRelay } from "nostr-idb";
|
||||||
|
import _throttle from "lodash.throttle";
|
||||||
|
import { CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||||
|
import { jsonLanguage } from "@codemirror/lang-json";
|
||||||
|
import { syntaxTree } from "@codemirror/language";
|
||||||
|
|
||||||
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
import VerticalPageLayout from "../../../components/vertical-page-layout";
|
||||||
import BackButton from "../../../components/back-button";
|
import BackButton from "../../../components/back-button";
|
||||||
import { NostrFilterSchema } from "./schema";
|
import { NostrFilterSchema } from "./schema";
|
||||||
import { relayRequest } from "../../../helpers/relay";
|
|
||||||
import { localRelay } from "../../../services/local-relay";
|
import { localRelay } from "../../../services/local-relay";
|
||||||
import Play from "../../../components/icons/play";
|
import Play from "../../../components/icons/play";
|
||||||
import ClockRewind from "../../../components/icons/clock-rewind";
|
import ClockRewind from "../../../components/icons/clock-rewind";
|
||||||
import HistoryDrawer from "./history-drawer";
|
import HistoryDrawer from "./history-drawer";
|
||||||
import EventRow from "./event-row";
|
import EventRow from "./event-row";
|
||||||
import { processFilter } from "./process";
|
import { processFilter } from "./process";
|
||||||
|
import HelpModal from "./help-modal";
|
||||||
|
import HelpCircle from "../../../components/icons/help-circle";
|
||||||
|
import stringify from "json-stringify-deterministic";
|
||||||
|
import { DownloadIcon } from "../../../components/icons";
|
||||||
|
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||||
|
import { validateRelayURL } from "../../../helpers/relay";
|
||||||
|
import { UserDirectory, useUserSearchDirectoryContext } from "../../../providers/global/user-directory-provider";
|
||||||
|
|
||||||
|
let users: UserDirectory = [];
|
||||||
|
function userAutocomplete(context: CompletionContext): CompletionResult | null {
|
||||||
|
let nodeBefore = syntaxTree(context.state).resolveInner(context.pos, -1);
|
||||||
|
if (nodeBefore.name !== "String") return null;
|
||||||
|
|
||||||
|
let textBefore = context.state.sliceDoc(nodeBefore.from, context.pos);
|
||||||
|
let tagBefore = /@\w*$/.exec(textBefore);
|
||||||
|
if (!tagBefore && !context.explicit) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: tagBefore ? nodeBefore.from + tagBefore.index : context.pos,
|
||||||
|
validFor: /^(@\w*)?$/,
|
||||||
|
// options: tagOptions,
|
||||||
|
options: users
|
||||||
|
.filter((u) => !!u.names[0])
|
||||||
|
.map((user) => ({
|
||||||
|
label: "@" + user.names[0]!,
|
||||||
|
type: "keyword",
|
||||||
|
apply: user.pubkey,
|
||||||
|
detail: "pubkey",
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const FilterEditor = memo(
|
const FilterEditor = memo(
|
||||||
({ value, onChange, onRun }: { value: string; onChange: (v: string) => void; onRun: () => void }) => {
|
({ value, onChange, onRun }: { value: string; onChange: (v: string) => void; onRun: () => void }) => {
|
||||||
|
const getDirectory = useUserSearchDirectoryContext();
|
||||||
const { colorMode } = useColorMode();
|
const { colorMode } = useColorMode();
|
||||||
|
|
||||||
|
useInterval(() => {
|
||||||
|
users = getDirectory();
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
const extensions = useMemo(
|
const extensions = useMemo(
|
||||||
() => [
|
() => [
|
||||||
keymap.of([
|
keymap.of([
|
||||||
@@ -55,6 +97,9 @@ const FilterEditor = memo(
|
|||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
jsonSchema(NostrFilterSchema),
|
jsonSchema(NostrFilterSchema),
|
||||||
|
jsonLanguage.data.of({
|
||||||
|
autocomplete: userAutocomplete,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
[onRun],
|
[onRun],
|
||||||
);
|
);
|
||||||
@@ -84,33 +129,98 @@ const EventTimeline = memo(({ events }: { events: NostrEvent[] }) => {
|
|||||||
export default function EventConsoleView() {
|
export default function EventConsoleView() {
|
||||||
const historyDrawer = useDisclosure();
|
const historyDrawer = useDisclosure();
|
||||||
const [history, setHistory] = useLocalStorage<string[]>("console-history", []);
|
const [history, setHistory] = useLocalStorage<string[]>("console-history", []);
|
||||||
|
const helpModal = useDisclosure();
|
||||||
|
const queryRelay = useDisclosure();
|
||||||
|
const [relayURL, setRelayURL] = useState("");
|
||||||
|
const [relay, setRelay] = useState<Relay | null>(null);
|
||||||
|
|
||||||
|
const [sub, setSub] = useState<Subscription | IDBSubscription | null>(null);
|
||||||
|
|
||||||
const [query, setQuery] = useState(() => history?.[0] || JSON.stringify({ kinds: [1], limit: 20 }, null, 2));
|
const [query, setQuery] = useState(() => history?.[0] || JSON.stringify({ kinds: [1], limit: 20 }, null, 2));
|
||||||
const queryRef = useRef(query);
|
|
||||||
queryRef.current = query;
|
|
||||||
|
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [events, setEvents] = useState<NostrEvent[]>([]);
|
const [events, setEvents] = useState<NostrEvent[]>([]);
|
||||||
const loadEvents = useCallback(async () => {
|
const loadEvents = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const filter = await processFilter(JSON.parse(queryRef.current));
|
if (queryRelay.isOpen && !relayURL) throw new Error("Must set relay");
|
||||||
|
|
||||||
|
const filter = await processFilter(JSON.parse(query));
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setHistory((arr) => (arr || []).concat(queryRef.current));
|
setHistory((arr) => (arr ? (!arr.includes(query) ? [query, ...arr] : arr) : [query]));
|
||||||
const e = await relayRequest(localRelay, [filter]);
|
setEvents([]);
|
||||||
setEvents(e);
|
|
||||||
|
if (sub) sub.close();
|
||||||
|
|
||||||
|
let r: Relay | CacheRelay = localRelay;
|
||||||
|
if (queryRelay.isOpen) {
|
||||||
|
const url = validateRelayURL(relayURL);
|
||||||
|
if (!relay || relay.url !== url.toString()) {
|
||||||
|
if (relay) relay.close();
|
||||||
|
r = new Relay(url.toString());
|
||||||
|
await r.connect();
|
||||||
|
setRelay(r);
|
||||||
|
} else r = relay;
|
||||||
|
} else {
|
||||||
|
if (relay) {
|
||||||
|
relay.close();
|
||||||
|
setRelay(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise<void>((res) => {
|
||||||
|
let buffer: NostrEvent[] = [];
|
||||||
|
const flush = _throttle(() => setEvents([...buffer]), 1000 / 10, { trailing: true });
|
||||||
|
|
||||||
|
const s = r.subscribe([filter], {
|
||||||
|
onevent: (e) => {
|
||||||
|
buffer.push(e);
|
||||||
|
flush();
|
||||||
|
},
|
||||||
|
oneose: () => {
|
||||||
|
setEvents([...buffer]);
|
||||||
|
res();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setSub(s);
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) setError(e.message);
|
if (e instanceof Error) setError(e.message);
|
||||||
}
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, []);
|
}, [queryRelay.isOpen, query, relayURL, relay, sub]);
|
||||||
|
|
||||||
|
const submitRef = useRef(loadEvents);
|
||||||
|
submitRef.current = loadEvents;
|
||||||
|
|
||||||
|
const submitCode = useCallback(() => submitRef.current(), []);
|
||||||
|
|
||||||
|
const downloadEvents = () => {
|
||||||
|
const lines = events.map((e) => stringify(e)).join("\n");
|
||||||
|
const file = new File([lines], "events.json", { type: "application/jsonl" });
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
window.open(url, "_blank");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VerticalPageLayout>
|
<VerticalPageLayout>
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center" wrap="wrap">
|
||||||
<BackButton size="sm" />
|
<BackButton size="sm" />
|
||||||
<Heading size="md">Event Console</Heading>
|
<Heading size="md">Event Console</Heading>
|
||||||
|
<Switch size="sm" checked={queryRelay.isOpen} onChange={queryRelay.onToggle}>
|
||||||
|
Query Relay
|
||||||
|
</Switch>
|
||||||
|
{queryRelay.isOpen && (
|
||||||
|
<RelayUrlInput
|
||||||
|
size="sm"
|
||||||
|
borderRadius="md"
|
||||||
|
w="xs"
|
||||||
|
value={relayURL}
|
||||||
|
onChange={(e) => setRelayURL(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<ButtonGroup ml="auto">
|
<ButtonGroup ml="auto">
|
||||||
|
<IconButton icon={<HelpCircle />} aria-label="Help" title="Help" size="sm" onClick={helpModal.onOpen} />
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={<ClockRewind />}
|
icon={<ClockRewind />}
|
||||||
aria-label="History"
|
aria-label="History"
|
||||||
@@ -124,7 +234,7 @@ export default function EventConsoleView() {
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<FilterEditor value={query} onChange={setQuery} onRun={loadEvents} />
|
<FilterEditor value={query} onChange={setQuery} onRun={submitCode} />
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<Alert status="error">
|
<Alert status="error">
|
||||||
@@ -139,6 +249,20 @@ export default function EventConsoleView() {
|
|||||||
|
|
||||||
<Flex gap="2">
|
<Flex gap="2">
|
||||||
<Text>{events.length} events</Text>
|
<Text>{events.length} events</Text>
|
||||||
|
{sub && (
|
||||||
|
<Text color="green.500" ml="auto">
|
||||||
|
Subscribed
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{events.length > 0 && (
|
||||||
|
<IconButton
|
||||||
|
aria-label="Download Events"
|
||||||
|
title="Download Events"
|
||||||
|
icon={<DownloadIcon />}
|
||||||
|
onClick={downloadEvents}
|
||||||
|
size="xs"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box>
|
<Box>
|
||||||
<EventTimeline events={events} />
|
<EventTimeline events={events} />
|
||||||
@@ -154,6 +278,8 @@ export default function EventConsoleView() {
|
|||||||
historyDrawer.onClose();
|
historyDrawer.onClose();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<HelpModal isOpen={helpModal.isOpen} onClose={helpModal.onClose} />
|
||||||
</VerticalPageLayout>
|
</VerticalPageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,33 @@
|
|||||||
|
import dayjs from "dayjs";
|
||||||
import { Filter, nip19 } from "nostr-tools";
|
import { Filter, nip19 } from "nostr-tools";
|
||||||
|
|
||||||
|
function processDateString(date: string) {
|
||||||
|
if (date.startsWith("n")) {
|
||||||
|
const match = date.match(/n([+-])(\d+)([hwmsd])?/i);
|
||||||
|
if (match === null) throw new Error(`Cant parse relative date string ${date}`);
|
||||||
|
|
||||||
|
if (match[1] === "-") {
|
||||||
|
return (
|
||||||
|
dayjs()
|
||||||
|
// @ts-expect-error
|
||||||
|
.subtract(parseInt(match[2]), match[3] || "h")
|
||||||
|
.unix()
|
||||||
|
);
|
||||||
|
} else if (match[1] === "+") {
|
||||||
|
return (
|
||||||
|
dayjs()
|
||||||
|
// @ts-expect-error
|
||||||
|
.add(parseInt(match[2]), match[3] || "h")
|
||||||
|
.unix()
|
||||||
|
);
|
||||||
|
} else throw Error(`Unknown operation ${match[1]}`);
|
||||||
|
} else if (date.toLowerCase() === "now") {
|
||||||
|
return dayjs().unix();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unknown date string ${date}`);
|
||||||
|
}
|
||||||
|
|
||||||
export async function processFilter(f: Filter): Promise<Filter> {
|
export async function processFilter(f: Filter): Promise<Filter> {
|
||||||
const filter = JSON.parse(JSON.stringify(f)) as Filter;
|
const filter = JSON.parse(JSON.stringify(f)) as Filter;
|
||||||
|
|
||||||
@@ -9,5 +37,12 @@ export async function processFilter(f: Filter): Promise<Filter> {
|
|||||||
return p;
|
return p;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (typeof filter.since === "string") {
|
||||||
|
filter.since = processDateString(filter.since);
|
||||||
|
}
|
||||||
|
if (typeof filter.until === "string") {
|
||||||
|
filter.until = processDateString(filter.until);
|
||||||
|
}
|
||||||
|
|
||||||
return filter;
|
return filter;
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { type JSONSchema7 } from "json-schema";
|
import { type JSONSchema7 } from "json-schema";
|
||||||
|
import { kinds } from "nostr-tools";
|
||||||
|
|
||||||
|
const kindNumbers = Object.values(kinds).filter((t) => typeof t === "number") as number[];
|
||||||
|
|
||||||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||||
export const NostrFilterSchema: JSONSchema7 = {
|
export const NostrFilterSchema: JSONSchema7 = {
|
||||||
@@ -19,6 +22,8 @@ export const NostrFilterSchema: JSONSchema7 = {
|
|||||||
uniqueItems: true,
|
uniqueItems: true,
|
||||||
items: {
|
items: {
|
||||||
type: "integer",
|
type: "integer",
|
||||||
|
minimum: 0,
|
||||||
|
examples: kindNumbers,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authors: {
|
authors: {
|
||||||
@@ -34,14 +39,27 @@ export const NostrFilterSchema: JSONSchema7 = {
|
|||||||
type: "integer",
|
type: "integer",
|
||||||
description: "max number of events to return",
|
description: "max number of events to return",
|
||||||
default: 20,
|
default: 20,
|
||||||
|
minimum: 0,
|
||||||
},
|
},
|
||||||
until: {
|
until: {
|
||||||
type: "integer",
|
|
||||||
description: "Return events before or on this date",
|
description: "Return events before or on this date",
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: "integer",
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
|
{ type: "string" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
since: {
|
since: {
|
||||||
type: "integer",
|
|
||||||
description: "Return events after or on this date",
|
description: "Return events after or on this date",
|
||||||
|
oneOf: [
|
||||||
|
{
|
||||||
|
type: "integer",
|
||||||
|
minimum: 0,
|
||||||
|
},
|
||||||
|
{ type: "string" },
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -2026,7 +2026,7 @@
|
|||||||
human-id "^1.0.2"
|
human-id "^1.0.2"
|
||||||
prettier "^2.7.1"
|
prettier "^2.7.1"
|
||||||
|
|
||||||
"@codemirror/autocomplete@^6.0.0":
|
"@codemirror/autocomplete@^6.0.0", "@codemirror/autocomplete@^6.12.0":
|
||||||
version "6.12.0"
|
version "6.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz#3fa620a8a3f42ded7751749916e8375f6bbbb333"
|
resolved "https://registry.yarnpkg.com/@codemirror/autocomplete/-/autocomplete-6.12.0.tgz#3fa620a8a3f42ded7751749916e8375f6bbbb333"
|
||||||
integrity sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==
|
integrity sha512-r4IjdYFthwbCQyvqnSlx0WBHRHi8nBvU+WjJxFUij81qsBfhNudf/XKKmmC2j3m0LaOYUQTf3qiEK1J8lO1sdg==
|
||||||
@@ -2054,7 +2054,7 @@
|
|||||||
"@codemirror/language" "^6.0.0"
|
"@codemirror/language" "^6.0.0"
|
||||||
"@lezer/json" "^1.0.0"
|
"@lezer/json" "^1.0.0"
|
||||||
|
|
||||||
"@codemirror/language@^6.0.0":
|
"@codemirror/language@^6.0.0", "@codemirror/language@^6.10.1":
|
||||||
version "6.10.1"
|
version "6.10.1"
|
||||||
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.1.tgz#428c932a158cb75942387acfe513c1ece1090b05"
|
resolved "https://registry.yarnpkg.com/@codemirror/language/-/language-6.10.1.tgz#428c932a158cb75942387acfe513c1ece1090b05"
|
||||||
integrity sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==
|
integrity sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==
|
||||||
|
Reference in New Issue
Block a user