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