fetch-kinds.js raw
1 #!/usr/bin/env node
2 /**
3 * Fetches kinds.json from the nostr library and generates eventKinds.js
4 * Run: node scripts/fetch-kinds.js
5 */
6
7 import { fileURLToPath } from 'url';
8 import { dirname, join } from 'path';
9 import { writeFileSync, existsSync } from 'fs';
10
11 const __filename = fileURLToPath(import.meta.url);
12 const __dirname = dirname(__filename);
13
14 const KINDS_URL = 'https://git.mleku.dev/mleku/nostr/raw/branch/main/encoders/kind/kinds.json';
15 const OUTPUT_PATH = join(__dirname, '..', 'src', 'eventKinds.js');
16
17 async function fetchKinds() {
18 console.log(`Fetching kinds from ${KINDS_URL}...`);
19
20 try {
21 const response = await fetch(KINDS_URL, { timeout: 10000 });
22 if (!response.ok) {
23 throw new Error(`HTTP ${response.status} ${response.statusText}`);
24 }
25
26 const data = await response.json();
27 console.log(`Fetched ${Object.keys(data.kinds).length} kinds (version: ${data.version})`);
28 return data;
29 } catch (error) {
30 // Check if we have an existing eventKinds.js we can use
31 if (existsSync(OUTPUT_PATH)) {
32 console.warn(`Warning: Could not fetch kinds.json (${error.message})`);
33 console.log(`Using existing ${OUTPUT_PATH}`);
34 return null; // Signal to skip generation
35 }
36 throw new Error(`Failed to fetch kinds.json and no existing file: ${error.message}`);
37 }
38 }
39
40 function generateEventKinds(data) {
41 const kinds = [];
42
43 for (const [kindNum, info] of Object.entries(data.kinds)) {
44 const k = parseInt(kindNum, 10);
45
46 // Determine classification
47 let isReplaceable = false;
48 let isAddressable = false;
49 let isEphemeral = false;
50
51 if (info.classification === 'replaceable' || k === 0 || k === 3 ||
52 (k >= data.ranges.replaceable.start && k < data.ranges.replaceable.end)) {
53 isReplaceable = true;
54 } else if (info.classification === 'parameterized' ||
55 (k >= data.ranges.parameterized.start && k <= data.ranges.parameterized.end)) {
56 isAddressable = true;
57 } else if (info.classification === 'ephemeral' ||
58 (k >= data.ranges.ephemeral.start && k < data.ranges.ephemeral.end)) {
59 isEphemeral = true;
60 }
61
62 const entry = {
63 kind: k,
64 name: info.name,
65 description: info.description,
66 nip: info.nip || null,
67 };
68
69 if (isReplaceable) entry.isReplaceable = true;
70 if (isAddressable) entry.isAddressable = true;
71 if (isEphemeral) entry.isEphemeral = true;
72 if (info.deprecated) entry.deprecated = true;
73 if (info.spec) entry.spec = info.spec;
74
75 // Add basic template
76 entry.template = {
77 kind: k,
78 content: "",
79 tags: []
80 };
81
82 // Add d tag for addressable events
83 if (isAddressable) {
84 entry.template.tags = [["d", "identifier"]];
85 }
86
87 kinds.push(entry);
88 }
89
90 // Sort by kind number
91 kinds.sort((a, b) => a.kind - b.kind);
92
93 return kinds;
94 }
95
96 function generateJS(kinds, data) {
97 return `/**
98 * Nostr Event Kinds Database
99 * Auto-generated from ${KINDS_URL}
100 * Version: ${data.version}
101 * Source: ${data.source}
102 *
103 * DO NOT EDIT - This file is auto-generated by scripts/fetch-kinds.js
104 */
105
106 export const eventKinds = ${JSON.stringify(kinds, null, 2)};
107
108 // Kind ranges for classification
109 export const kindRanges = ${JSON.stringify(data.ranges, null, 2)};
110
111 // Privileged kinds (require auth)
112 export const privilegedKinds = ${JSON.stringify(data.privileged)};
113
114 // Directory kinds (public discovery)
115 export const directoryKinds = ${JSON.stringify(data.directory)};
116
117 // Kind aliases
118 export const kindAliases = ${JSON.stringify(data.aliases, null, 2)};
119
120 // Helper function to get event kind by number
121 export function getEventKind(kindNumber) {
122 return eventKinds.find(k => k.kind === kindNumber);
123 }
124
125 // Alias for compatibility
126 export function getKindInfo(kind) {
127 return getEventKind(kind);
128 }
129
130 export function getKindName(kind) {
131 const info = getEventKind(kind);
132 return info ? info.name : \`Kind \${kind}\`;
133 }
134
135 // Helper function to search event kinds by name or description
136 export function searchEventKinds(query) {
137 const lowerQuery = query.toLowerCase();
138 return eventKinds.filter(k =>
139 k.name.toLowerCase().includes(lowerQuery) ||
140 k.description.toLowerCase().includes(lowerQuery) ||
141 k.kind.toString().includes(query)
142 );
143 }
144
145 // Helper function to get all event kinds grouped by category
146 export function getEventKindsByCategory() {
147 return {
148 regular: eventKinds.filter(k => k.kind < 10000 && !k.isReplaceable),
149 replaceable: eventKinds.filter(k => k.isReplaceable),
150 ephemeral: eventKinds.filter(k => k.isEphemeral),
151 addressable: eventKinds.filter(k => k.isAddressable)
152 };
153 }
154
155 // Helper function to create a template event with current timestamp
156 export function createTemplateEvent(kindNumber, userPubkey = null) {
157 const kindInfo = getEventKind(kindNumber);
158 if (!kindInfo) {
159 return {
160 kind: kindNumber,
161 content: "",
162 tags: [],
163 created_at: Math.floor(Date.now() / 1000),
164 pubkey: userPubkey || "<your_pubkey_here>"
165 };
166 }
167
168 return {
169 ...kindInfo.template,
170 created_at: Math.floor(Date.now() / 1000),
171 pubkey: userPubkey || "<your_pubkey_here>"
172 };
173 }
174
175 export function isReplaceable(kind) {
176 if (kind === 0 || kind === 3) return true;
177 return kind >= ${data.ranges.replaceable.start} && kind < ${data.ranges.replaceable.end};
178 }
179
180 export function isEphemeral(kind) {
181 return kind >= ${data.ranges.ephemeral.start} && kind < ${data.ranges.ephemeral.end};
182 }
183
184 export function isAddressable(kind) {
185 return kind >= ${data.ranges.parameterized.start} && kind <= ${data.ranges.parameterized.end};
186 }
187
188 export function isPrivileged(kind) {
189 return privilegedKinds.includes(kind);
190 }
191
192 // Export kind categories for filtering in UI
193 export const kindCategories = [
194 { id: "all", name: "All Kinds", filter: () => true },
195 { id: "regular", name: "Regular Events (0-9999)", filter: k => k.kind < 10000 && !k.isReplaceable },
196 { id: "replaceable", name: "Replaceable (10000-19999)", filter: k => k.isReplaceable },
197 { id: "ephemeral", name: "Ephemeral (20000-29999)", filter: k => k.isEphemeral },
198 { id: "addressable", name: "Addressable (30000-39999)", filter: k => k.isAddressable },
199 { id: "social", name: "Social", filter: k => [0, 1, 3, 6, 7].includes(k.kind) },
200 { id: "messaging", name: "Messaging", filter: k => [4, 9, 10, 11, 12, 14, 15, 40, 41, 42].includes(k.kind) },
201 { id: "lists", name: "Lists", filter: k => k.name.toLowerCase().includes("list") || k.name.toLowerCase().includes("set") },
202 { id: "marketplace", name: "Marketplace", filter: k => [30017, 30018, 30019, 30020, 1021, 1022, 30402, 30403].includes(k.kind) },
203 { id: "lightning", name: "Lightning/Zaps", filter: k => [9734, 9735, 9041, 9321, 7374, 7375, 7376].includes(k.kind) },
204 { id: "media", name: "Media", filter: k => [20, 21, 22, 1063, 1222, 1244].includes(k.kind) },
205 { id: "git", name: "Git/Code", filter: k => [818, 1337, 1617, 1618, 1619, 1621, 1622, 30617, 30618].includes(k.kind) },
206 { id: "calendar", name: "Calendar", filter: k => [31922, 31923, 31924, 31925].includes(k.kind) },
207 { id: "groups", name: "Groups", filter: k => (k.kind >= 9000 && k.kind <= 9030) || (k.kind >= 39000 && k.kind <= 39009) },
208 ];
209 `;
210 }
211
212 async function main() {
213 try {
214 const data = await fetchKinds();
215
216 // If fetchKinds returned null, we're using the existing file
217 if (data === null) {
218 console.log('Skipping generation, using existing eventKinds.js');
219 return;
220 }
221
222 const kinds = generateEventKinds(data);
223 const js = generateJS(kinds, data);
224
225 writeFileSync(OUTPUT_PATH, js);
226 console.log(`Generated ${OUTPUT_PATH} with ${kinds.length} kinds`);
227 } catch (error) {
228 console.error('Error:', error.message);
229 process.exit(1);
230 }
231 }
232
233 main();
234