From 0a3e639fee735a321d9536368f37e40802400eec Mon Sep 17 00:00:00 2001 From: mleku Date: Tue, 16 Dec 2025 10:39:02 +0100 Subject: [PATCH] Add event template generator with 140+ Nostr event kinds (v0.36.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add comprehensive eventKinds.js database with all NIPs event kinds including templates, descriptions, NIP references, and type flags - Create EventTemplateSelector.svelte modal with search functionality and category filtering (Social, Messaging, Lists, Marketplace, etc.) - Update ComposeView with "Generate Template" button and error banner for displaying permission-aware publish error messages - Enhance publishEvent() in App.svelte with detailed error handling that explains policy restrictions, permission issues, and provides actionable guidance for users - Add permission pre-check to prevent read-only users from attempting to publish events - Update CLAUDE.md with Web UI event templates documentation - Create docs/WEB_UI_EVENT_TEMPLATES.md with comprehensive user guide Files modified: - app/web/src/eventKinds.js (new) - app/web/src/EventTemplateSelector.svelte (new) - app/web/src/ComposeView.svelte - app/web/src/App.svelte - docs/WEB_UI_EVENT_TEMPLATES.md (new) - CLAUDE.md - pkg/version/version 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CLAUDE.md | 25 +- app/web/src/App.svelte | 95 +- app/web/src/ComposeView.svelte | 116 +- app/web/src/EventTemplateSelector.svelte | 404 ++++ app/web/src/eventKinds.js | 2644 ++++++++++++++++++++++ docs/WEB_UI_EVENT_TEMPLATES.md | 163 ++ pkg/version/version | 2 +- 7 files changed, 3433 insertions(+), 16 deletions(-) create mode 100644 app/web/src/EventTemplateSelector.svelte create mode 100644 app/web/src/eventKinds.js create mode 100644 docs/WEB_UI_EVENT_TEMPLATES.md diff --git a/CLAUDE.md b/CLAUDE.md index 553f163..ef6f65e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -334,6 +334,11 @@ export ORLY_AUTH_TO_WRITE=false # Require auth only for writes - Features: event browser with advanced filtering, sprocket management, policy management, user admin, settings - **Event Browser:** Enhanced filter system with kind, author, tag, and time range filters (replaced simple search) - **Policy Management Tab:** JSON editor with validation, save publishes kind 12345 event +- **Compose Tab with Event Templates:** Generate pre-filled event templates for all 140+ Nostr event kinds + - `eventKinds.js` - Comprehensive database of event kinds from NIPs with templates + - `EventTemplateSelector.svelte` - Scrolling modal with search and category filtering + - Category filters: All, Regular, Replaceable, Ephemeral, Addressable, Social, Messaging, Lists, Marketplace, Lightning, Media, Git, Calendar, Groups + - Permission-aware error messages explaining policy/role restrictions when publishing fails **Command-line Tools (`cmd/`):** - `relay-tester/` - Nostr protocol compliance testing @@ -816,7 +821,7 @@ Files modified: 3. GitHub Actions workflow builds binaries for multiple platforms 4. Release created automatically with binaries and checksums -## Recent Features (v0.34.x) +## Recent Features (v0.34.x - v0.36.x) ### Directory Spider The directory spider (`pkg/spider/directory.go`) automatically discovers and syncs metadata from other relays: @@ -860,6 +865,22 @@ WebAssembly-compatible database backend (`pkg/wasmdb/`): - **Reference Documentation**: `docs/POLICY_CONFIGURATION_REFERENCE.md` provides authoritative read vs write applicability - See also: `pkg/policy/README.md` for quick reference +### Web UI Event Templates (v0.36.x) +The Compose tab now includes a comprehensive event template generator: +- **140+ Event Kinds**: Complete database of Nostr event kinds from the NIPs repository +- **Template Generator**: Click "Generate Template" to open searchable modal with all event types +- **Category Filtering**: Filter by Regular, Replaceable, Ephemeral, Addressable, or domain-specific categories (Social, Messaging, Lists, Marketplace, Lightning, Media, Git, Calendar, Groups) +- **Search**: Find events by name, description, kind number, or NIP reference +- **Pre-filled Templates**: Each kind includes proper tag structure and example content +- **Permission-Aware Errors**: When publishing fails, error messages explain: + - Policy restrictions (kind blocked, content limits) + - Permission issues (user role insufficient) + - Guidance on how to resolve (contact admin, policy config) +- **Key Files**: + - `app/web/src/eventKinds.js` - Event kinds database with templates + - `app/web/src/EventTemplateSelector.svelte` - Template selection modal + - `app/web/src/ComposeView.svelte` - Updated compose interface + ### Policy JSON Configuration Quick Reference ```json @@ -956,4 +977,6 @@ Invite-based access control system: | `pkg/neo4j/MODIFYING_SCHEMA.md` | How to modify Neo4j schema | | `pkg/neo4j/TESTING.md` | Neo4j testing guide | | `.claude/skills/cypher/SKILL.md` | Cypher query language skill for Neo4j | +| `app/web/src/eventKinds.js` | Comprehensive Nostr event kinds database (140+ kinds with templates) | +| `docs/WEB_UI_EVENT_TEMPLATES.md` | Web UI event template generator documentation | | `readme.adoc` | Project README with feature overview | diff --git a/app/web/src/App.svelte b/app/web/src/App.svelte index a72530e..0c5db92 100644 --- a/app/web/src/App.svelte +++ b/app/web/src/App.svelte @@ -117,6 +117,7 @@ // Compose tab state let composeEventJson = ""; + let composePublishError = ""; // Recovery tab state let recoverySelectedKind = null; @@ -2562,31 +2563,42 @@ } async function publishEvent() { + // Clear any previous errors + composePublishError = ""; + try { if (!composeEventJson.trim()) { - alert("Please enter an event to publish"); + composePublishError = "Please enter an event to publish"; return; } if (!isLoggedIn) { - alert("Please log in to publish events"); + composePublishError = "Please log in to publish events"; return; } if (!userSigner) { - alert( - "No signer available. Please log in with a valid authentication method.", - ); + composePublishError = "No signer available. Please log in with a valid authentication method."; return; } - const event = JSON.parse(composeEventJson); + let event; + try { + event = JSON.parse(composeEventJson); + } catch (parseError) { + composePublishError = `Invalid JSON: ${parseError.message}`; + return; + } // Validate that the event has required fields if (!event.id || !event.sig) { - alert( - 'Event must be signed before publishing. Please click "Sign" first.', - ); + composePublishError = 'Event must be signed before publishing. Please click "Sign" first.'; + return; + } + + // Pre-check: validate user has write permission + if (userRole === "read") { + composePublishError = `Permission denied: Your current role is "${userRole}" which does not allow publishing events. Contact a relay administrator to upgrade your permissions.`; return; } @@ -2602,20 +2614,72 @@ ); if (result.success) { + composePublishError = ""; alert("Event published successfully to ORLY relay!"); // Optionally clear the editor after successful publish // composeEventJson = ''; } else { - alert( - `Event publishing failed: ${result.reason || "Unknown error"}`, - ); + // Parse the error reason and provide helpful guidance + const reason = result.reason || "Unknown error"; + composePublishError = formatPublishError(reason, event.kind); } } catch (error) { console.error("Error publishing event:", error); - alert("Error publishing event: " + error.message); + const errorMsg = error.message || "Unknown error"; + composePublishError = formatPublishError(errorMsg, null); } } + // Helper function to format publish errors with helpful guidance + function formatPublishError(reason, eventKind) { + const lowerReason = reason.toLowerCase(); + + // Check for policy-related errors + if (lowerReason.includes("policy") || lowerReason.includes("blocked") || lowerReason.includes("denied")) { + let msg = `Policy Error: ${reason}`; + if (eventKind !== null) { + msg += `\n\nKind ${eventKind} may be restricted by the relay's policy configuration.`; + } + if (policyEnabled) { + msg += "\n\nThe relay has policy enforcement enabled. Contact a relay administrator to allow this event kind or adjust your permissions."; + } + return msg; + } + + // Check for permission/auth errors + if (lowerReason.includes("auth") || lowerReason.includes("permission") || lowerReason.includes("unauthorized")) { + return `Permission Error: ${reason}\n\nYour current permissions may not allow publishing this type of event. Current role: ${userRole || "unknown"}. Contact a relay administrator to upgrade your permissions.`; + } + + // Check for kind-specific restrictions + if (lowerReason.includes("kind") || lowerReason.includes("not allowed") || lowerReason.includes("restricted")) { + let msg = `Event Type Error: ${reason}`; + if (eventKind !== null) { + msg += `\n\nKind ${eventKind} is not currently allowed on this relay.`; + } + msg += "\n\nThe relay administrator may need to update the policy configuration to allow this event kind."; + return msg; + } + + // Check for rate limiting + if (lowerReason.includes("rate") || lowerReason.includes("limit") || lowerReason.includes("too many")) { + return `Rate Limit Error: ${reason}\n\nPlease wait a moment before trying again.`; + } + + // Check for size limits + if (lowerReason.includes("size") || lowerReason.includes("too large") || lowerReason.includes("content")) { + return `Size Limit Error: ${reason}\n\nThe event may exceed the relay's size limits. Try reducing the content length.`; + } + + // Default error message + return `Publishing failed: ${reason}`; + } + + // Clear the compose publish error + function clearComposeError() { + composePublishError = ""; + } + // Persist selected tab to local storage $: { localStorage.setItem("selectedTab", selectedTab); @@ -2720,9 +2784,14 @@ {:else if selectedTab === "compose"} {:else if selectedTab === "managed-acl"}
diff --git a/app/web/src/ComposeView.svelte b/app/web/src/ComposeView.svelte index 7b2c2c6..ff18754 100644 --- a/app/web/src/ComposeView.svelte +++ b/app/web/src/ComposeView.svelte @@ -1,9 +1,17 @@
+ @@ -27,16 +57,34 @@ >Publish
+ + {#if publishError} +
+
+ + {publishError} +
+ +
+ {/if} +
+ + diff --git a/app/web/src/EventTemplateSelector.svelte b/app/web/src/EventTemplateSelector.svelte new file mode 100644 index 0000000..71a0fa6 --- /dev/null +++ b/app/web/src/EventTemplateSelector.svelte @@ -0,0 +1,404 @@ + + + + +{#if isOpen} + + + +{/if} + + diff --git a/app/web/src/eventKinds.js b/app/web/src/eventKinds.js new file mode 100644 index 0000000..c86e391 --- /dev/null +++ b/app/web/src/eventKinds.js @@ -0,0 +1,2644 @@ +/** + * Comprehensive Nostr Event Kinds Database + * Based on NIPs (Nostr Implementation Possibilities) from https://github.com/nostr-protocol/nips + * + * Each entry contains: + * - kind: The numeric event kind + * - name: Human-readable name + * - description: Brief description of the event type + * - nip: Related NIP number(s) + * - template: Event template with placeholder fields + * - isReplaceable: Whether the event is replaceable (kinds 0, 3, 10000-19999) + * - isAddressable: Whether the event is addressable/parameterized replaceable (kinds 30000-39999) + * - isEphemeral: Whether the event is ephemeral (kinds 20000-29999) + */ + +export const eventKinds = [ + // Regular Events (0-9999) + { + kind: 0, + name: "User Metadata", + description: "Profile metadata (name, about, picture, etc.)", + nip: "01", + isReplaceable: true, + template: { + kind: 0, + content: JSON.stringify({ + name: "Your Name", + about: "A brief description about yourself", + picture: "https://example.com/avatar.jpg", + banner: "https://example.com/banner.jpg", + nip05: "you@example.com", + lud16: "you@walletofsatoshi.com", + website: "https://example.com" + }, null, 2), + tags: [] + } + }, + { + kind: 1, + name: "Short Text Note", + description: "Plain text note (like a tweet)", + nip: "01", + template: { + kind: 1, + content: "Your note content here", + tags: [] + } + }, + { + kind: 2, + name: "Recommend Relay", + description: "Recommend a relay to followers (deprecated)", + nip: "01", + template: { + kind: 2, + content: "wss://relay.example.com", + tags: [] + } + }, + { + kind: 3, + name: "Follows", + description: "Contact list / follow list", + nip: "02", + isReplaceable: true, + template: { + kind: 3, + content: "", + tags: [ + ["p", "", "wss://relay.example.com", "nickname"] + ] + } + }, + { + kind: 4, + name: "Encrypted Direct Messages", + description: "NIP-04 encrypted direct message (deprecated, use kind 14)", + nip: "04", + template: { + kind: 4, + content: "", + tags: [ + ["p", ""] + ] + } + }, + { + kind: 5, + name: "Event Deletion Request", + description: "Request to delete events", + nip: "09", + template: { + kind: 5, + content: "Reason for deletion (optional)", + tags: [ + ["e", ""], + ["k", ""] + ] + } + }, + { + kind: 6, + name: "Repost", + description: "Repost/boost a kind 1 note", + nip: "18", + template: { + kind: 6, + content: "", + tags: [ + ["e", "", "wss://relay.example.com"], + ["p", ""] + ] + } + }, + { + kind: 7, + name: "Reaction", + description: "Reaction to an event (like, emoji, etc.)", + nip: "25", + template: { + kind: 7, + content: "+", + tags: [ + ["e", ""], + ["p", ""] + ] + } + }, + { + kind: 8, + name: "Badge Award", + description: "Award a badge to a user", + nip: "58", + template: { + kind: 8, + content: "", + tags: [ + ["a", "30009::"], + ["p", ""] + ] + } + }, + { + kind: 9, + name: "Chat Message", + description: "Group chat message", + nip: "29", + template: { + kind: 9, + content: "Your chat message", + tags: [ + ["h", ""] + ] + } + }, + { + kind: 10, + name: "Group Chat Threaded Reply", + description: "Threaded reply in group chat", + nip: "29", + template: { + kind: 10, + content: "Your reply", + tags: [ + ["h", ""], + ["e", "", "", "root"], + ["e", "", "", "reply"] + ] + } + }, + { + kind: 11, + name: "Thread", + description: "Thread starter", + nip: "29", + template: { + kind: 11, + content: "Thread content", + tags: [ + ["h", ""] + ] + } + }, + { + kind: 12, + name: "Group Thread Reply", + description: "Reply in a group thread", + nip: "29", + template: { + kind: 12, + content: "Reply content", + tags: [ + ["h", ""], + ["e", "", "", "root"] + ] + } + }, + { + kind: 13, + name: "Seal", + description: "Encrypted wrapper for gift wraps", + nip: "59", + template: { + kind: 13, + content: "", + tags: [] + } + }, + { + kind: 14, + name: "Direct Message", + description: "NIP-17 private direct message (recommended)", + nip: "17", + template: { + kind: 14, + content: "Your private message content", + tags: [ + ["p", ""] + ] + } + }, + { + kind: 15, + name: "File Message", + description: "File attachment in DM", + nip: "17", + template: { + kind: 15, + content: "Optional caption", + tags: [ + ["p", ""], + ["url", "https://example.com/file.pdf"], + ["m", "application/pdf"], + ["size", "12345"] + ] + } + }, + { + kind: 16, + name: "Generic Repost", + description: "Repost any event kind", + nip: "18", + template: { + kind: 16, + content: "", + tags: [ + ["e", "", "wss://relay.example.com"], + ["p", ""], + ["k", ""] + ] + } + }, + { + kind: 17, + name: "Reaction to Website", + description: "React to a web page URL", + nip: "25", + template: { + kind: 17, + content: "+", + tags: [ + ["r", "https://example.com/page"] + ] + } + }, + { + kind: 20, + name: "Picture", + description: "Picture post", + nip: "68", + template: { + kind: 20, + content: "Photo description", + tags: [ + ["url", "https://example.com/photo.jpg"], + ["m", "image/jpeg"], + ["dim", "1920x1080"], + ["blurhash", ""] + ] + } + }, + { + kind: 21, + name: "Video Event", + description: "Video post (horizontal)", + nip: "71", + template: { + kind: 21, + content: "Video description", + tags: [ + ["url", "https://example.com/video.mp4"], + ["m", "video/mp4"], + ["dim", "1920x1080"], + ["duration", "120"], + ["thumb", "https://example.com/thumbnail.jpg"] + ] + } + }, + { + kind: 22, + name: "Short-form Portrait Video", + description: "Short vertical video (like TikTok/Reels)", + nip: "71", + template: { + kind: 22, + content: "Short video description", + tags: [ + ["url", "https://example.com/short.mp4"], + ["m", "video/mp4"], + ["dim", "1080x1920"], + ["duration", "30"] + ] + } + }, + { + kind: 40, + name: "Channel Creation", + description: "Create a public chat channel", + nip: "28", + template: { + kind: 40, + content: JSON.stringify({ + name: "Channel Name", + about: "Channel description", + picture: "https://example.com/channel-pic.jpg" + }, null, 2), + tags: [] + } + }, + { + kind: 41, + name: "Channel Metadata", + description: "Update channel metadata", + nip: "28", + template: { + kind: 41, + content: JSON.stringify({ + name: "Updated Channel Name", + about: "Updated description", + picture: "https://example.com/new-pic.jpg" + }, null, 2), + tags: [ + ["e", ""] + ] + } + }, + { + kind: 42, + name: "Channel Message", + description: "Message in a public channel", + nip: "28", + template: { + kind: 42, + content: "Your message in the channel", + tags: [ + ["e", "", "wss://relay.example.com", "root"] + ] + } + }, + { + kind: 43, + name: "Channel Hide Message", + description: "Hide a message in a channel (moderator action)", + nip: "28", + template: { + kind: 43, + content: JSON.stringify({ reason: "Spam" }, null, 2), + tags: [ + ["e", ""] + ] + } + }, + { + kind: 44, + name: "Channel Mute User", + description: "Mute a user in a channel (moderator action)", + nip: "28", + template: { + kind: 44, + content: JSON.stringify({ reason: "Harassment" }, null, 2), + tags: [ + ["p", ""] + ] + } + }, + { + kind: 62, + name: "Request to Vanish", + description: "Request relays to delete all your events", + nip: "62", + template: { + kind: 62, + content: "", + tags: [ + ["relay", "wss://relay1.example.com"], + ["relay", "wss://relay2.example.com"] + ] + } + }, + { + kind: 64, + name: "Chess (PGN)", + description: "Chess game in PGN format", + nip: "64", + template: { + kind: 64, + content: "1. e4 e5 2. Nf3 Nc6 3. Bb5", + tags: [ + ["p", ""], + ["p", ""] + ] + } + }, + { + kind: 443, + name: "KeyPackage", + description: "MLS KeyPackage for group messaging", + nip: "104", + template: { + kind: 443, + content: "", + tags: [] + } + }, + { + kind: 444, + name: "Welcome Message", + description: "MLS Welcome message for group join", + nip: "104", + template: { + kind: 444, + content: "", + tags: [ + ["p", ""] + ] + } + }, + { + kind: 445, + name: "Group Event", + description: "MLS group event (commit, proposal)", + nip: "104", + template: { + kind: 445, + content: "", + tags: [ + ["h", ""] + ] + } + }, + { + kind: 818, + name: "Merge Requests", + description: "Git merge request", + nip: "34", + template: { + kind: 818, + content: "Merge request description", + tags: [ + ["a", "30617::"], + ["p", ""], + ["branch", "feature-branch"], + ["base", "main"] + ] + } + }, + { + kind: 1018, + name: "Poll Response", + description: "Response to a poll", + nip: "88", + template: { + kind: 1018, + content: "", + tags: [ + ["e", ""], + ["response", "0"] + ] + } + }, + { + kind: 1021, + name: "Bid", + description: "Bid on an auction", + nip: "15", + template: { + kind: 1021, + content: "", + tags: [ + ["a", "30020::"], + ["amount", "10000", "sat"] + ] + } + }, + { + kind: 1022, + name: "Bid Confirmation", + description: "Confirm a winning bid", + nip: "15", + template: { + kind: 1022, + content: "", + tags: [ + ["e", ""], + ["p", ""] + ] + } + }, + { + kind: 1040, + name: "OpenTimestamps", + description: "OpenTimestamps attestation", + nip: "03", + template: { + kind: 1040, + content: "", + tags: [ + ["e", ""] + ] + } + }, + { + kind: 1059, + name: "Gift Wrap", + description: "Wrapper for private events", + nip: "59", + template: { + kind: 1059, + content: "", + tags: [ + ["p", ""] + ] + } + }, + { + kind: 1063, + name: "File Metadata", + description: "Metadata for a file", + nip: "94", + template: { + kind: 1063, + content: "File description", + tags: [ + ["url", "https://example.com/file.pdf"], + ["m", "application/pdf"], + ["x", ""], + ["size", "1048576"], + ["dim", "800x600"], + ["blurhash", ""] + ] + } + }, + { + kind: 1068, + name: "Poll", + description: "Create a poll", + nip: "88", + template: { + kind: 1068, + content: "What is your favorite color?", + tags: [ + ["option", "0", "Red"], + ["option", "1", "Blue"], + ["option", "2", "Green"], + ["poll_type", "single"], + ["closed_at", "1704067200"] + ] + } + }, + { + kind: 1111, + name: "Comment", + description: "Comment on any addressable event or URL", + nip: "22", + template: { + kind: 1111, + content: "Your comment here", + tags: [ + ["K", ""], + ["E", "", "wss://relay.example.com", ""], + ["A", "::", "wss://relay.example.com"], + ["k", ""], + ["e", "", "wss://relay.example.com", ""] + ] + } + }, + { + kind: 1222, + name: "Voice Message", + description: "Audio voice message", + nip: "88", + template: { + kind: 1222, + content: "Transcription (optional)", + tags: [ + ["url", "https://example.com/voice.ogg"], + ["m", "audio/ogg"], + ["duration", "30"] + ] + } + }, + { + kind: 1244, + name: "Voice Message Comment", + description: "Voice message reply to content", + nip: "88", + template: { + kind: 1244, + content: "", + tags: [ + ["url", "https://example.com/reply.ogg"], + ["e", ""] + ] + } + }, + { + kind: 1311, + name: "Live Chat Message", + description: "Chat message during a live stream", + nip: "53", + template: { + kind: 1311, + content: "Live chat message", + tags: [ + ["a", "30311::", "wss://relay.example.com"] + ] + } + }, + { + kind: 1337, + name: "Code Snippet", + description: "Share a code snippet", + nip: "XX", + template: { + kind: 1337, + content: "console.log('Hello, World!');", + tags: [ + ["language", "javascript"], + ["filename", "hello.js"] + ] + } + }, + { + kind: 1617, + name: "Patches", + description: "Git patches", + nip: "34", + template: { + kind: 1617, + content: "diff --git a/file.txt b/file.txt\n...", + tags: [ + ["a", "30617::"], + ["commit", ""] + ] + } + }, + { + kind: 1618, + name: "Pull Requests", + description: "Git pull request issues", + nip: "34", + template: { + kind: 1618, + content: "Pull request description", + tags: [ + ["a", "30617::"], + ["branch", "feature-branch"], + ["base", "main"] + ] + } + }, + { + kind: 1619, + name: "Pull Request Updates", + description: "Updates to pull requests", + nip: "34", + template: { + kind: 1619, + content: "Update description", + tags: [ + ["e", ""] + ] + } + }, + { + kind: 1621, + name: "Issues", + description: "Git repository issues", + nip: "34", + template: { + kind: 1621, + content: "Issue description", + tags: [ + ["a", "30617::"], + ["title", "Issue title"], + ["t", "bug"] + ] + } + }, + { + kind: 1622, + name: "Git Replies", + description: "Replies to git issues/PRs", + nip: "34", + template: { + kind: 1622, + content: "Reply content", + tags: [ + ["e", "", "", "root"], + ["p", ""] + ] + } + }, + { + kind: 1630, + name: "Status (General)", + description: "General status update", + nip: "38", + template: { + kind: 1630, + content: "Currently working on something cool", + tags: [ + ["d", "general"] + ] + } + }, + { + kind: 1971, + name: "Problem Tracker", + description: "Track problems/bugs", + nip: "XX", + template: { + kind: 1971, + content: "Problem description", + tags: [ + ["title", "Problem title"], + ["status", "open"] + ] + } + }, + { + kind: 1984, + name: "Reporting", + description: "Report user or content", + nip: "56", + template: { + kind: 1984, + content: "Reason for report", + tags: [ + ["p", "", "spam"], + ["e", "", "spam"] + ] + } + }, + { + kind: 1985, + name: "Label", + description: "Label/tag content", + nip: "32", + template: { + kind: 1985, + content: "", + tags: [ + ["L", "ugc"], + ["l", "nsfw", "ugc"], + ["e", ""] + ] + } + }, + { + kind: 1986, + name: "Relay Reviews", + description: "Review a relay", + nip: "XX", + template: { + kind: 1986, + content: "Great relay! Fast and reliable.", + tags: [ + ["r", "wss://relay.example.com"], + ["rating", "5"] + ] + } + }, + { + kind: 1987, + name: "AI Embeddings / Vector Lists", + description: "AI vector embeddings for content", + nip: "XX", + template: { + kind: 1987, + content: "", + tags: [ + ["e", ""], + ["embedding", "[0.1, 0.2, 0.3, ...]"] + ] + } + }, + { + kind: 2003, + name: "Torrent", + description: "Torrent metadata", + nip: "35", + template: { + kind: 2003, + content: "Torrent description", + tags: [ + ["title", "Torrent Title"], + ["x", ""], + ["file", "filename.ext", "1048576"] + ] + } + }, + { + kind: 2004, + name: "Torrent Comment", + description: "Comment on a torrent", + nip: "35", + template: { + kind: 2004, + content: "Comment on the torrent", + tags: [ + ["e", ""] + ] + } + }, + { + kind: 2022, + name: "Coinjoin Pool", + description: "Coinjoin coordination", + nip: "XX", + template: { + kind: 2022, + content: "", + tags: [ + ["amount", "100000"], + ["expiry", "1704067200"] + ] + } + }, + { + kind: 4550, + name: "Community Post Approval", + description: "Approve a post for a community", + nip: "72", + template: { + kind: 4550, + content: "", + tags: [ + ["a", "34550::"], + ["e", ""], + ["p", ""] + ] + } + }, + // Job Request Range (5000-5999) + { + kind: 5000, + name: "Job Request (Text Generation)", + description: "Request AI text generation", + nip: "90", + template: { + kind: 5000, + content: "Generate a poem about nostr", + tags: [ + ["i", "prompt text", "text"], + ["output", "text/plain"] + ] + } + }, + { + kind: 5100, + name: "Job Request (Text-to-Image)", + description: "Request AI image generation", + nip: "90", + template: { + kind: 5100, + content: "", + tags: [ + ["i", "A sunset over mountains", "text"], + ["output", "image/png"] + ] + } + }, + // Job Result Range (6000-6999) + { + kind: 6000, + name: "Job Result (Text)", + description: "Result of text generation job", + nip: "90", + template: { + kind: 6000, + content: "Generated text result", + tags: [ + ["e", ""], + ["p", ""], + ["status", "success"] + ] + } + }, + { + kind: 6100, + name: "Job Result (Image)", + description: "Result of image generation job", + nip: "90", + template: { + kind: 6100, + content: "", + tags: [ + ["e", ""], + ["p", ""], + ["url", "https://example.com/generated.png"], + ["status", "success"] + ] + } + }, + { + kind: 7000, + name: "Job Feedback", + description: "Feedback on a job result", + nip: "90", + template: { + kind: 7000, + content: "Great work!", + tags: [ + ["e", ""], + ["p", ""], + ["status", "success"], + ["amount", "1000", "sat"] + ] + } + }, + { + kind: 7374, + name: "Reserved Cashu Wallet Tokens", + description: "Reserved Cashu tokens", + nip: "60", + template: { + kind: 7374, + content: "", + tags: [] + } + }, + { + kind: 7375, + name: "Cashu Wallet Tokens", + description: "Cashu wallet token storage", + nip: "60", + template: { + kind: 7375, + content: "", + tags: [ + ["mint", "https://mint.example.com"] + ] + } + }, + { + kind: 7376, + name: "Cashu Wallet History", + description: "Cashu wallet transaction history", + nip: "60", + template: { + kind: 7376, + content: "", + tags: [] + } + }, + { + kind: 7516, + name: "Geocache Log", + description: "Log a geocache find", + nip: "XX", + template: { + kind: 7516, + content: "Found it! Great hide.", + tags: [ + ["a", "37516::"] + ] + } + }, + { + kind: 7517, + name: "Geocache Proof of Find", + description: "Proof of geocache discovery", + nip: "XX", + template: { + kind: 7517, + content: "", + tags: [ + ["a", "37516::"], + ["proof", ""] + ] + } + }, + { + kind: 8000, + name: "Add User (NIP-43)", + description: "Add a user to relay access", + nip: "43", + template: { + kind: 8000, + content: "", + tags: [ + ["p", ""], + ["role", "member"] + ] + } + }, + { + kind: 8001, + name: "Remove User (NIP-43)", + description: "Remove a user from relay access", + nip: "43", + template: { + kind: 8001, + content: "", + tags: [ + ["p", ""] + ] + } + }, + // Group Control Events (9000-9030) + { + kind: 9000, + name: "Group Add User", + description: "Add user to a group", + nip: "29", + template: { + kind: 9000, + content: "", + tags: [ + ["h", ""], + ["p", ""] + ] + } + }, + { + kind: 9001, + name: "Group Remove User", + description: "Remove user from a group", + nip: "29", + template: { + kind: 9001, + content: "", + tags: [ + ["h", ""], + ["p", ""] + ] + } + }, + { + kind: 9041, + name: "Zap Goal", + description: "Fundraising goal", + nip: "75", + template: { + kind: 9041, + content: "Help me reach my goal!", + tags: [ + ["amount", "1000000"], + ["relays", "wss://relay.example.com"], + ["closed_at", "1704067200"] + ] + } + }, + { + kind: 9321, + name: "Nutzap", + description: "Cashu-based zap", + nip: "61", + template: { + kind: 9321, + content: "", + tags: [ + ["p", ""], + ["e", ""], + ["mint", "https://mint.example.com"] + ] + } + }, + { + kind: 9467, + name: "Tidal Login", + description: "Tidal music service authentication", + nip: "XX", + template: { + kind: 9467, + content: "", + tags: [] + } + }, + { + kind: 9734, + name: "Zap Request", + description: "Request a Lightning zap", + nip: "57", + template: { + kind: 9734, + content: "Zap comment (optional)", + tags: [ + ["p", ""], + ["e", ""], + ["amount", "1000"], + ["relays", "wss://relay.example.com"] + ] + } + }, + { + kind: 9735, + name: "Zap", + description: "Lightning zap receipt", + nip: "57", + template: { + kind: 9735, + content: "", + tags: [ + ["p", ""], + ["e", ""], + ["bolt11", ""], + ["description", ""], + ["preimage", ""] + ] + } + }, + { + kind: 9802, + name: "Highlights", + description: "Highlight text from content", + nip: "84", + template: { + kind: 9802, + content: "The highlighted text portion", + tags: [ + ["a", "30023::"], + ["context", "surrounding text for context"] + ] + } + }, + // Replaceable Events (10000-19999) + { + kind: 10000, + name: "Mute List", + description: "List of muted users/events/words", + nip: "51", + isReplaceable: true, + template: { + kind: 10000, + content: "", + tags: [ + ["p", ""], + ["e", ""], + ["word", "spamword"], + ["t", "hashtag_to_mute"] + ] + } + }, + { + kind: 10001, + name: "Pin List", + description: "List of pinned events", + nip: "51", + isReplaceable: true, + template: { + kind: 10001, + content: "", + tags: [ + ["e", ""] + ] + } + }, + { + kind: 10002, + name: "Relay List Metadata", + description: "User's preferred relays", + nip: "65", + isReplaceable: true, + template: { + kind: 10002, + content: "", + tags: [ + ["r", "wss://relay1.example.com", "read"], + ["r", "wss://relay2.example.com", "write"], + ["r", "wss://relay3.example.com"] + ] + } + }, + { + kind: 10003, + name: "Bookmark List", + description: "Bookmarked events", + nip: "51", + isReplaceable: true, + template: { + kind: 10003, + content: "", + tags: [ + ["e", ""], + ["a", "30023::"] + ] + } + }, + { + kind: 10004, + name: "Communities List", + description: "Communities the user belongs to", + nip: "51", + isReplaceable: true, + template: { + kind: 10004, + content: "", + tags: [ + ["a", "34550::"] + ] + } + }, + { + kind: 10005, + name: "Public Chats List", + description: "Public chat channels user follows", + nip: "51", + isReplaceable: true, + template: { + kind: 10005, + content: "", + tags: [ + ["e", ""] + ] + } + }, + { + kind: 10006, + name: "Blocked Relays List", + description: "Relays the user has blocked", + nip: "51", + isReplaceable: true, + template: { + kind: 10006, + content: "", + tags: [ + ["r", "wss://blocked-relay.example.com"] + ] + } + }, + { + kind: 10007, + name: "Search Relays List", + description: "Preferred relays for search", + nip: "51", + isReplaceable: true, + template: { + kind: 10007, + content: "", + tags: [ + ["r", "wss://search-relay.example.com"] + ] + } + }, + { + kind: 10009, + name: "User Groups", + description: "Groups the user is part of", + nip: "51", + isReplaceable: true, + template: { + kind: 10009, + content: "", + tags: [ + ["h", "", "wss://group-relay.example.com"] + ] + } + }, + { + kind: 10012, + name: "Favorite Relays List", + description: "User's favorite relays", + nip: "51", + isReplaceable: true, + template: { + kind: 10012, + content: "", + tags: [ + ["r", "wss://favorite-relay.example.com"] + ] + } + }, + { + kind: 10013, + name: "Private Event Relay List", + description: "Relays for private events", + nip: "51", + isReplaceable: true, + template: { + kind: 10013, + content: "", + tags: [ + ["r", "wss://private-relay.example.com"] + ] + } + }, + { + kind: 10015, + name: "Interests List", + description: "User's interests/topics", + nip: "51", + isReplaceable: true, + template: { + kind: 10015, + content: "", + tags: [ + ["t", "bitcoin"], + ["t", "nostr"], + ["t", "programming"] + ] + } + }, + { + kind: 10019, + name: "Nutzap Mint Recommendation", + description: "Recommended mints for Nutzaps", + nip: "61", + isReplaceable: true, + template: { + kind: 10019, + content: "", + tags: [ + ["mint", "https://mint.example.com"] + ] + } + }, + { + kind: 10020, + name: "Media Follows", + description: "Media creators the user follows", + nip: "51", + isReplaceable: true, + template: { + kind: 10020, + content: "", + tags: [ + ["p", ""] + ] + } + }, + { + kind: 10030, + name: "User Emoji List", + description: "Custom emoji shortcuts", + nip: "30", + isReplaceable: true, + template: { + kind: 10030, + content: "", + tags: [ + ["emoji", "sats", "https://example.com/sats.png"], + ["emoji", "nostr", "https://example.com/nostr.gif"] + ] + } + }, + { + kind: 10050, + name: "Relay List to Receive DMs", + description: "Relays where user receives DMs", + nip: "17", + isReplaceable: true, + template: { + kind: 10050, + content: "", + tags: [ + ["r", "wss://dm-relay.example.com"] + ] + } + }, + { + kind: 10051, + name: "KeyPackage Relays List", + description: "Relays for MLS KeyPackages", + nip: "104", + isReplaceable: true, + template: { + kind: 10051, + content: "", + tags: [ + ["r", "wss://mls-relay.example.com"] + ] + } + }, + { + kind: 10063, + name: "User Server List", + description: "User's file servers", + nip: "96", + isReplaceable: true, + template: { + kind: 10063, + content: "", + tags: [ + ["server", "https://fileserver.example.com"] + ] + } + }, + { + kind: 10096, + name: "File Storage Server List", + description: "Preferred file storage servers", + nip: "96", + isReplaceable: true, + template: { + kind: 10096, + content: "", + tags: [ + ["server", "https://storage.example.com"] + ] + } + }, + { + kind: 10166, + name: "Relay Monitor Announcement", + description: "Announce relay monitoring service", + nip: "66", + isReplaceable: true, + template: { + kind: 10166, + content: "", + tags: [ + ["frequency", "3600"], + ["t", ""] + ] + } + }, + { + kind: 10312, + name: "Room Presence", + description: "User presence in a room/space", + nip: "XX", + isReplaceable: true, + template: { + kind: 10312, + content: "", + tags: [ + ["d", ""], + ["status", "online"] + ] + } + }, + { + kind: 10377, + name: "Proxy Announcement", + description: "Announce proxy service", + nip: "XX", + isReplaceable: true, + template: { + kind: 10377, + content: "", + tags: [] + } + }, + { + kind: 11111, + name: "Transport Method Announcement", + description: "Announce transport method availability", + nip: "XX", + isReplaceable: true, + template: { + kind: 11111, + content: "", + tags: [] + } + }, + { + kind: 13194, + name: "Wallet Info", + description: "NWC wallet service info", + nip: "47", + isReplaceable: true, + template: { + kind: 13194, + content: "", + tags: [ + ["capabilities", "pay_invoice", "get_balance"], + ["name", "My Wallet"] + ] + } + }, + { + kind: 13534, + name: "Membership Lists", + description: "List of relay members", + nip: "43", + isReplaceable: true, + template: { + kind: 13534, + content: "", + tags: [ + ["p", "", "member"], + ["p", "", "admin"] + ] + } + }, + { + kind: 17375, + name: "Cashu Wallet Event", + description: "Cashu wallet state event", + nip: "60", + isReplaceable: true, + template: { + kind: 17375, + content: "", + tags: [] + } + }, + { + kind: 21000, + name: "Lightning Pub RPC", + description: "Lightning.pub RPC call", + nip: "XX", + template: { + kind: 21000, + content: "", + tags: [] + } + }, + { + kind: 22242, + name: "Client Authentication", + description: "NIP-42 client auth event", + nip: "42", + template: { + kind: 22242, + content: "", + tags: [ + ["relay", "wss://relay.example.com"], + ["challenge", ""] + ] + } + }, + { + kind: 23194, + name: "Wallet Request", + description: "NWC wallet request", + nip: "47", + template: { + kind: 23194, + content: "", + tags: [ + ["p", ""] + ] + } + }, + { + kind: 23195, + name: "Wallet Response", + description: "NWC wallet response", + nip: "47", + template: { + kind: 23195, + content: "", + tags: [ + ["p", ""], + ["e", ""] + ] + } + }, + { + kind: 24133, + name: "Nostr Connect", + description: "NIP-46 remote signing request/response", + nip: "46", + template: { + kind: 24133, + content: "", + tags: [ + ["p", ""] + ] + } + }, + { + kind: 24242, + name: "Blobs Stored on Mediaservers", + description: "Reference to blob storage", + nip: "XX", + template: { + kind: 24242, + content: "", + tags: [ + ["x", ""], + ["url", "https://cdn.example.com/blob"], + ["m", "application/octet-stream"] + ] + } + }, + { + kind: 27235, + name: "HTTP Auth", + description: "NIP-98 HTTP authentication", + nip: "98", + template: { + kind: 27235, + content: "", + tags: [ + ["u", "https://api.example.com/endpoint"], + ["method", "POST"] + ] + } + }, + { + kind: 28934, + name: "Join Request", + description: "Request to join a group/community", + nip: "XX", + template: { + kind: 28934, + content: "Please let me join!", + tags: [ + ["h", ""] + ] + } + }, + { + kind: 28935, + name: "Invite Request", + description: "Invite someone to group/community", + nip: "XX", + template: { + kind: 28935, + content: "", + tags: [ + ["h", ""], + ["p", ""] + ] + } + }, + { + kind: 28936, + name: "Leave Request", + description: "Request to leave a group", + nip: "XX", + template: { + kind: 28936, + content: "", + tags: [ + ["h", ""] + ] + } + }, + // Addressable Events (30000-39999) + { + kind: 30000, + name: "Follow Sets", + description: "Named list of followed pubkeys", + nip: "51", + isAddressable: true, + template: { + kind: 30000, + content: "", + tags: [ + ["d", "my-follow-list"], + ["p", ""] + ] + } + }, + { + kind: 30001, + name: "Generic Lists", + description: "Generic named list", + nip: "51", + isAddressable: true, + template: { + kind: 30001, + content: "", + tags: [ + ["d", "my-list"], + ["e", ""], + ["p", ""], + ["t", "tag"] + ] + } + }, + { + kind: 30002, + name: "Relay Sets", + description: "Named list of relays", + nip: "51", + isAddressable: true, + template: { + kind: 30002, + content: "", + tags: [ + ["d", "my-relays"], + ["r", "wss://relay.example.com"] + ] + } + }, + { + kind: 30003, + name: "Bookmark Sets", + description: "Named bookmark collection", + nip: "51", + isAddressable: true, + template: { + kind: 30003, + content: "", + tags: [ + ["d", "favorites"], + ["e", ""] + ] + } + }, + { + kind: 30004, + name: "Curation Sets", + description: "Curated content collection", + nip: "51", + isAddressable: true, + template: { + kind: 30004, + content: "", + tags: [ + ["d", "best-of-2024"], + ["a", "30023::"] + ] + } + }, + { + kind: 30005, + name: "Video Sets", + description: "Video playlist", + nip: "51", + isAddressable: true, + template: { + kind: 30005, + content: "", + tags: [ + ["d", "my-playlist"], + ["a", "34235::"] + ] + } + }, + { + kind: 30007, + name: "Kind Mute Sets", + description: "Muted event kinds", + nip: "51", + isAddressable: true, + template: { + kind: 30007, + content: "", + tags: [ + ["d", "muted-kinds"], + ["k", "1"], + ["k", "7"] + ] + } + }, + { + kind: 30008, + name: "Profile Badges", + description: "Badges displayed on profile", + nip: "58", + isAddressable: true, + template: { + kind: 30008, + content: "", + tags: [ + ["d", "profile_badges"], + ["a", "30009::"], + ["e", ""] + ] + } + }, + { + kind: 30009, + name: "Badge Definition", + description: "Define a badge", + nip: "58", + isAddressable: true, + template: { + kind: 30009, + content: "", + tags: [ + ["d", "my-badge"], + ["name", "Badge Name"], + ["description", "Badge description"], + ["image", "https://example.com/badge.png"], + ["thumb", "https://example.com/badge-thumb.png"] + ] + } + }, + { + kind: 30015, + name: "Interest Sets", + description: "Named interest collection", + nip: "51", + isAddressable: true, + template: { + kind: 30015, + content: "", + tags: [ + ["d", "tech-interests"], + ["t", "bitcoin"], + ["t", "lightning"] + ] + } + }, + { + kind: 30017, + name: "Create or Update a Stall", + description: "Marketplace stall definition", + nip: "15", + isAddressable: true, + template: { + kind: 30017, + content: JSON.stringify({ + id: "stall-id", + name: "My Stall", + description: "Selling cool stuff", + currency: "sat", + shipping: [ + { id: "shipping-1", name: "Standard", cost: 1000, regions: ["worldwide"] } + ] + }, null, 2), + tags: [ + ["d", "my-stall"] + ] + } + }, + { + kind: 30018, + name: "Create or Update a Product", + description: "Marketplace product listing", + nip: "15", + isAddressable: true, + template: { + kind: 30018, + content: JSON.stringify({ + id: "product-id", + stall_id: "stall-id", + name: "Product Name", + description: "Product description", + images: ["https://example.com/product.jpg"], + currency: "sat", + price: 10000, + quantity: 10 + }, null, 2), + tags: [ + ["d", "my-product"], + ["t", "electronics"] + ] + } + }, + { + kind: 30019, + name: "Marketplace UI/UX", + description: "Marketplace display preferences", + nip: "15", + isAddressable: true, + template: { + kind: 30019, + content: JSON.stringify({ + theme: "dark", + layout: "grid" + }, null, 2), + tags: [ + ["d", "marketplace-settings"] + ] + } + }, + { + kind: 30020, + name: "Product Sold as Auction", + description: "Auction listing", + nip: "15", + isAddressable: true, + template: { + kind: 30020, + content: JSON.stringify({ + id: "auction-id", + stall_id: "stall-id", + name: "Rare Item", + description: "Auction description", + images: ["https://example.com/item.jpg"], + starting_bid: 1000, + currency: "sat" + }, null, 2), + tags: [ + ["d", "my-auction"], + ["start", "1704067200"], + ["end", "1704153600"] + ] + } + }, + { + kind: 30023, + name: "Long-form Content", + description: "Blog post / article", + nip: "23", + isAddressable: true, + template: { + kind: 30023, + content: "# Article Title\n\nYour long-form content in Markdown format...\n\n## Section 1\n\nContent here...", + tags: [ + ["d", "article-identifier"], + ["title", "Article Title"], + ["summary", "A brief summary of the article"], + ["image", "https://example.com/cover.jpg"], + ["published_at", "1704067200"], + ["t", "nostr"], + ["t", "bitcoin"] + ] + } + }, + { + kind: 30024, + name: "Draft Long-form Content", + description: "Draft blog post / article", + nip: "23", + isAddressable: true, + template: { + kind: 30024, + content: "# Draft Article\n\nWork in progress...", + tags: [ + ["d", "draft-identifier"], + ["title", "Draft Title"] + ] + } + }, + { + kind: 30030, + name: "Emoji Sets", + description: "Named emoji collection", + nip: "30", + isAddressable: true, + template: { + kind: 30030, + content: "", + tags: [ + ["d", "my-emojis"], + ["emoji", "pepe", "https://example.com/pepe.png"], + ["emoji", "wojak", "https://example.com/wojak.gif"] + ] + } + }, + { + kind: 30040, + name: "Curated Publication Index", + description: "Index for curated publication", + nip: "XX", + isAddressable: true, + template: { + kind: 30040, + content: "", + tags: [ + ["d", "publication-index"], + ["title", "My Publication"], + ["e", ""] + ] + } + }, + { + kind: 30041, + name: "Curated Publication Content", + description: "Content for curated publication", + nip: "XX", + isAddressable: true, + template: { + kind: 30041, + content: "Publication content", + tags: [ + ["d", "publication-content"], + ["a", "30040::"] + ] + } + }, + { + kind: 30063, + name: "Release Artifact Sets", + description: "Software release artifacts", + nip: "XX", + isAddressable: true, + template: { + kind: 30063, + content: "", + tags: [ + ["d", "v1.0.0"], + ["name", "Release v1.0.0"], + ["url", "https://github.com/user/repo/releases/v1.0.0"], + ["x", ""] + ] + } + }, + { + kind: 30078, + name: "Application-specific Data", + description: "App-specific user data", + nip: "78", + isAddressable: true, + template: { + kind: 30078, + content: JSON.stringify({ setting1: "value1", setting2: true }, null, 2), + tags: [ + ["d", "my-app-settings"] + ] + } + }, + { + kind: 30166, + name: "Relay Discovery", + description: "Relay discovery information", + nip: "66", + isAddressable: true, + template: { + kind: 30166, + content: "", + tags: [ + ["d", ""], + ["n", "clearnet"], + ["N", "50"], + ["R", "read"], + ["R", "write"] + ] + } + }, + { + kind: 30267, + name: "App Curation Sets", + description: "Curated app collections", + nip: "XX", + isAddressable: true, + template: { + kind: 30267, + content: "", + tags: [ + ["d", "nostr-apps"], + ["a", "32267::"] + ] + } + }, + { + kind: 30311, + name: "Live Event", + description: "Live streaming event", + nip: "53", + isAddressable: true, + template: { + kind: 30311, + content: "", + tags: [ + ["d", "stream-id"], + ["title", "My Live Stream"], + ["summary", "Stream description"], + ["image", "https://example.com/thumbnail.jpg"], + ["streaming", "https://stream.example.com/live.m3u8"], + ["status", "live"], + ["starts", "1704067200"], + ["p", "", "host"] + ] + } + }, + { + kind: 30312, + name: "Interactive Room", + description: "Interactive audio/video room", + nip: "XX", + isAddressable: true, + template: { + kind: 30312, + content: "", + tags: [ + ["d", "room-id"], + ["name", "Discussion Room"], + ["about", "Room description"] + ] + } + }, + { + kind: 30313, + name: "Conference Event", + description: "Conference/meetup event", + nip: "XX", + isAddressable: true, + template: { + kind: 30313, + content: "", + tags: [ + ["d", "conference-id"], + ["name", "Bitcoin Conference 2024"], + ["about", "Annual Bitcoin conference"], + ["location", "Miami, FL"], + ["start", "1704067200"], + ["end", "1704326400"] + ] + } + }, + { + kind: 30315, + name: "User Statuses", + description: "User status updates", + nip: "38", + isAddressable: true, + template: { + kind: 30315, + content: "Building on Nostr", + tags: [ + ["d", "general"], + ["expiration", "1704153600"] + ] + } + }, + { + kind: 30388, + name: "Slide Set", + description: "Presentation slides", + nip: "XX", + isAddressable: true, + template: { + kind: 30388, + content: "", + tags: [ + ["d", "presentation-id"], + ["title", "My Presentation"], + ["slide", "1", "# Slide 1"], + ["slide", "2", "## Slide 2"] + ] + } + }, + { + kind: 30402, + name: "Classified Listing", + description: "Classified ad", + nip: "99", + isAddressable: true, + template: { + kind: 30402, + content: "Full description of the item or service", + tags: [ + ["d", "listing-id"], + ["title", "Item for Sale"], + ["price", "100000", "sat"], + ["location", "New York, NY"], + ["image", "https://example.com/item.jpg"], + ["t", "electronics"] + ] + } + }, + { + kind: 30403, + name: "Draft Classified Listing", + description: "Draft classified ad", + nip: "99", + isAddressable: true, + template: { + kind: 30403, + content: "Draft description", + tags: [ + ["d", "draft-listing-id"], + ["title", "Draft Listing"] + ] + } + }, + { + kind: 30617, + name: "Repository Announcements", + description: "Git repository announcement", + nip: "34", + isAddressable: true, + template: { + kind: 30617, + content: "Repository description", + tags: [ + ["d", "repo-identifier"], + ["name", "my-project"], + ["description", "Project description"], + ["clone", "https://github.com/user/repo.git"], + ["web", "https://github.com/user/repo"], + ["relays", "wss://relay.example.com"] + ] + } + }, + { + kind: 30618, + name: "Repository State Announcements", + description: "Git repository state/refs", + nip: "34", + isAddressable: true, + template: { + kind: 30618, + content: "", + tags: [ + ["d", "repo-identifier"], + ["r", "refs/heads/main", ""], + ["r", "refs/tags/v1.0.0", ""] + ] + } + }, + { + kind: 30818, + name: "Wiki Article", + description: "Wiki/knowledge base article", + nip: "54", + isAddressable: true, + template: { + kind: 30818, + content: "# Wiki Article\n\nArticle content in markdown...", + tags: [ + ["d", "article-slug"], + ["title", "Article Title"], + ["summary", "Brief summary"] + ] + } + }, + { + kind: 30819, + name: "Redirects", + description: "Wiki redirect", + nip: "54", + isAddressable: true, + template: { + kind: 30819, + content: "", + tags: [ + ["d", "old-slug"], + ["a", "30818::"] + ] + } + }, + { + kind: 31234, + name: "Draft Event", + description: "Generic draft event", + nip: "XX", + isAddressable: true, + template: { + kind: 31234, + content: "Draft content", + tags: [ + ["d", "draft-id"], + ["k", ""] + ] + } + }, + { + kind: 31388, + name: "Link Set", + description: "Collection of links", + nip: "XX", + isAddressable: true, + template: { + kind: 31388, + content: "", + tags: [ + ["d", "my-links"], + ["r", "https://example.com", "Example Site"], + ["r", "https://nostr.com", "Nostr"] + ] + } + }, + { + kind: 31890, + name: "Feed", + description: "Custom feed definition", + nip: "XX", + isAddressable: true, + template: { + kind: 31890, + content: JSON.stringify({ + name: "My Custom Feed", + description: "A feed of my favorite content" + }, null, 2), + tags: [ + ["d", "my-feed"], + ["p", ""], + ["t", "bitcoin"] + ] + } + }, + { + kind: 31922, + name: "Date-Based Calendar Event", + description: "All-day calendar event", + nip: "52", + isAddressable: true, + template: { + kind: 31922, + content: "Event description", + tags: [ + ["d", "event-id"], + ["title", "Event Title"], + ["start", "2024-01-01"], + ["end", "2024-01-02"], + ["location", "Conference Center"] + ] + } + }, + { + kind: 31923, + name: "Time-Based Calendar Event", + description: "Timed calendar event", + nip: "52", + isAddressable: true, + template: { + kind: 31923, + content: "Meeting agenda", + tags: [ + ["d", "meeting-id"], + ["title", "Team Meeting"], + ["start", "1704067200"], + ["end", "1704070800"], + ["start_tzid", "America/New_York"], + ["location", "Zoom"], + ["p", ""] + ] + } + }, + { + kind: 31924, + name: "Calendar", + description: "Calendar definition", + nip: "52", + isAddressable: true, + template: { + kind: 31924, + content: "", + tags: [ + ["d", "my-calendar"], + ["title", "Personal Calendar"], + ["a", "31923::"] + ] + } + }, + { + kind: 31925, + name: "Calendar Event RSVP", + description: "RSVP to a calendar event", + nip: "52", + isAddressable: true, + template: { + kind: 31925, + content: "Looking forward to it!", + tags: [ + ["d", ""], + ["a", "31923::"], + ["status", "accepted"] + ] + } + }, + { + kind: 31989, + name: "Handler Recommendation", + description: "Recommend an app for event kind", + nip: "89", + isAddressable: true, + template: { + kind: 31989, + content: "", + tags: [ + ["d", "1"], + ["a", "31990::", "web", "https://app.example.com"] + ] + } + }, + { + kind: 31990, + name: "Handler Information", + description: "App handler definition", + nip: "89", + isAddressable: true, + template: { + kind: 31990, + content: "", + tags: [ + ["d", "my-app"], + ["name", "My Nostr App"], + ["about", "App description"], + ["picture", "https://example.com/app-icon.png"], + ["web", "https://app.example.com"], + ["k", "1"], + ["k", "30023"] + ] + } + }, + { + kind: 32267, + name: "Software Application", + description: "Software application definition", + nip: "XX", + isAddressable: true, + template: { + kind: 32267, + content: "Application description", + tags: [ + ["d", "app-identifier"], + ["name", "App Name"], + ["version", "1.0.0"], + ["url", "https://app.example.com"], + ["icon", "https://example.com/icon.png"] + ] + } + }, + { + kind: 34550, + name: "Community Definition", + description: "Define a community", + nip: "72", + isAddressable: true, + template: { + kind: 34550, + content: "", + tags: [ + ["d", "community-name"], + ["name", "My Community"], + ["description", "Community description"], + ["image", "https://example.com/community.jpg"], + ["p", "", "moderator"], + ["relay", "wss://community-relay.example.com"] + ] + } + }, + { + kind: 37516, + name: "Geocache Listing", + description: "Geocache location listing", + nip: "XX", + isAddressable: true, + template: { + kind: 37516, + content: "Geocache description and hints", + tags: [ + ["d", "cache-id"], + ["name", "Secret Spot"], + ["g", "u4pruydqqvj"], + ["difficulty", "3"], + ["terrain", "2"] + ] + } + }, + { + kind: 38172, + name: "Cashu Mint Announcement", + description: "Announce a Cashu mint", + nip: "XX", + isAddressable: true, + template: { + kind: 38172, + content: "", + tags: [ + ["d", ""], + ["mint", "https://mint.example.com"], + ["name", "My Cashu Mint"] + ] + } + }, + { + kind: 38173, + name: "Fedimint Announcement", + description: "Announce a Fedimint federation", + nip: "XX", + isAddressable: true, + template: { + kind: 38173, + content: "", + tags: [ + ["d", ""], + ["name", "My Federation"], + ["invite", ""] + ] + } + }, + { + kind: 38383, + name: "Peer-to-peer Order Events", + description: "P2P trading order", + nip: "XX", + isAddressable: true, + template: { + kind: 38383, + content: "", + tags: [ + ["d", "order-id"], + ["type", "buy"], + ["amount", "100000"], + ["currency", "USD"], + ["price", "50000"] + ] + } + }, + // Group Metadata (39000-39009) + { + kind: 39000, + name: "Group Metadata", + description: "Group/channel metadata", + nip: "29", + isAddressable: true, + template: { + kind: 39000, + content: "", + tags: [ + ["d", ""], + ["name", "Group Name"], + ["about", "Group description"], + ["picture", "https://example.com/group.jpg"] + ] + } + }, + { + kind: 39001, + name: "Group Admins", + description: "List of group admins", + nip: "29", + isAddressable: true, + template: { + kind: 39001, + content: "", + tags: [ + ["d", ""], + ["p", "", "admin"] + ] + } + }, + { + kind: 39002, + name: "Group Members", + description: "List of group members", + nip: "29", + isAddressable: true, + template: { + kind: 39002, + content: "", + tags: [ + ["d", ""], + ["p", ""] + ] + } + }, + { + kind: 39089, + name: "Starter Packs", + description: "Starter pack of accounts to follow", + nip: "XX", + isAddressable: true, + template: { + kind: 39089, + content: "", + tags: [ + ["d", "pack-id"], + ["name", "Bitcoin Developers"], + ["description", "Must-follow Bitcoin developers"], + ["p", ""] + ] + } + }, + { + kind: 39092, + name: "Media Starter Packs", + description: "Starter pack of media creators", + nip: "XX", + isAddressable: true, + template: { + kind: 39092, + content: "", + tags: [ + ["d", "media-pack-id"], + ["name", "Photography Creators"], + ["p", ""] + ] + } + }, + { + kind: 39701, + name: "Web Bookmarks", + description: "Bookmarked web pages", + nip: "XX", + isAddressable: true, + template: { + kind: 39701, + content: "", + tags: [ + ["d", "bookmarks"], + ["r", "https://example.com", "Example Site"], + ["t", "reference"] + ] + } + } +]; + +// Helper function to get event kind by number +export function getEventKind(kindNumber) { + return eventKinds.find(k => k.kind === kindNumber); +} + +// Helper function to search event kinds by name or description +export function searchEventKinds(query) { + const lowerQuery = query.toLowerCase(); + return eventKinds.filter(k => + k.name.toLowerCase().includes(lowerQuery) || + k.description.toLowerCase().includes(lowerQuery) || + k.kind.toString().includes(query) + ); +} + +// Helper function to get all event kinds grouped by category +export function getEventKindsByCategory() { + return { + regular: eventKinds.filter(k => k.kind < 10000 && !k.isReplaceable), + replaceable: eventKinds.filter(k => k.isReplaceable), + ephemeral: eventKinds.filter(k => k.kind >= 20000 && k.kind < 30000), + addressable: eventKinds.filter(k => k.isAddressable) + }; +} + +// Helper function to create a template event with current timestamp +export function createTemplateEvent(kindNumber, userPubkey = null) { + const kindInfo = getEventKind(kindNumber); + if (!kindInfo) { + return { + kind: kindNumber, + content: "", + tags: [], + created_at: Math.floor(Date.now() / 1000), + pubkey: userPubkey || "" + }; + } + + return { + ...kindInfo.template, + created_at: Math.floor(Date.now() / 1000), + pubkey: userPubkey || "" + }; +} + +// Export kind categories for filtering in UI +export const kindCategories = [ + { id: "all", name: "All Kinds", filter: () => true }, + { id: "regular", name: "Regular Events (0-9999)", filter: k => k.kind < 10000 && !k.isReplaceable }, + { id: "replaceable", name: "Replaceable (10000-19999)", filter: k => k.isReplaceable }, + { id: "ephemeral", name: "Ephemeral (20000-29999)", filter: k => k.kind >= 20000 && k.kind < 30000 }, + { id: "addressable", name: "Addressable (30000-39999)", filter: k => k.isAddressable }, + { id: "social", name: "Social", filter: k => [0, 1, 3, 6, 7].includes(k.kind) }, + { id: "messaging", name: "Messaging", filter: k => [4, 9, 10, 11, 12, 14, 15, 40, 41, 42].includes(k.kind) }, + { id: "lists", name: "Lists", filter: k => k.name.toLowerCase().includes("list") || k.name.toLowerCase().includes("set") }, + { id: "marketplace", name: "Marketplace", filter: k => [30017, 30018, 30019, 30020, 1021, 1022, 30402, 30403].includes(k.kind) }, + { id: "lightning", name: "Lightning/Zaps", filter: k => [9734, 9735, 9041, 9321, 7374, 7375, 7376].includes(k.kind) }, + { id: "media", name: "Media", filter: k => [20, 21, 22, 1063, 1222, 1244].includes(k.kind) }, + { id: "git", name: "Git/Code", filter: k => [818, 1337, 1617, 1618, 1619, 1621, 1622, 30617, 30618].includes(k.kind) }, + { id: "calendar", name: "Calendar", filter: k => [31922, 31923, 31924, 31925].includes(k.kind) }, + { id: "groups", name: "Groups", filter: k => k.kind >= 9000 && k.kind <= 9030 || k.kind >= 39000 && k.kind <= 39009 }, +]; diff --git a/docs/WEB_UI_EVENT_TEMPLATES.md b/docs/WEB_UI_EVENT_TEMPLATES.md new file mode 100644 index 0000000..44a130a --- /dev/null +++ b/docs/WEB_UI_EVENT_TEMPLATES.md @@ -0,0 +1,163 @@ +# Web UI Event Templates + +The ORLY Web UI includes a comprehensive event template generator that helps users create properly-structured Nostr events for any of the 140+ defined event kinds. + +## Overview + +The Compose tab provides a "Generate Template" button that opens a searchable, categorized modal dialog. Users can browse or search for any Nostr event kind and instantly load a pre-filled template with the correct structure, tags, and example content. + +## Features + +### Event Kind Database (`app/web/src/eventKinds.js`) + +A comprehensive JavaScript database containing: + +- **140+ event kinds** from the NIPs (Nostr Implementation Possibilities) repository +- Each entry includes: + - Kind number + - Human-readable name + - Description + - NIP reference (where applicable) + - Event type flags (replaceable, addressable, ephemeral) + - Pre-built template with proper tag structure + +### Template Selector Modal (`app/web/src/EventTemplateSelector.svelte`) + +A user-friendly modal interface featuring: + +- **Search functionality**: Find events by name, description, kind number, or NIP reference +- **Category filters**: Quick-filter buttons for event types: + - All Kinds + - Regular Events (0-9999) + - Replaceable (10000-19999) + - Ephemeral (20000-29999) + - Addressable (30000-39999) + - Domain-specific: Social, Messaging, Lists, Marketplace, Lightning, Media, Git, Calendar, Groups +- **Visual badges**: Color-coded indicators showing event type +- **NIP references**: Quick reference to the defining NIP +- **Keyboard navigation**: Escape key closes the modal + +### Permission-Aware Error Handling + +When publishing fails, the system provides detailed, actionable error messages: + +| Error Type | Description | User Guidance | +|------------|-------------|---------------| +| Policy Error | Event kind blocked by relay policy | Contact relay administrator to allow the kind | +| Permission Error | User role insufficient | Shows current role, suggests permission upgrade | +| Kind Restriction | Event type not allowed | Policy configuration may need updating | +| Rate Limit | Too many requests | Wait before retrying | +| Size Limit | Event too large | Reduce content length | + +## Usage + +### Generating a Template + +1. Navigate to the **Compose** tab in the Web UI +2. Click the **Generate Template** button (purple button) +3. In the modal: + - Use the search box to find specific event types + - Or click category tabs to filter by event type + - Click on any event kind to select it +4. The template is loaded into the editor with: + - Correct `kind` value + - Proper tag structure with placeholder values + - Example content (where applicable) + - Current timestamp + - Your pubkey (if logged in) + +### Editing and Publishing + +1. Replace placeholder values (marked with ``) with actual data +2. Click **Reformat** to clean up JSON formatting +3. Click **Sign** to sign the event with your key +4. Click **Publish** to send to the relay + +### Understanding Templates + +Templates use placeholder values in angle brackets that must be replaced: + +```json +{ + "kind": 1, + "content": "Your note content here", + "tags": [ + ["p", ""], + ["e", ""] + ], + "created_at": 1702857600, + "pubkey": "" +} +``` + +## Event Categories + +### Regular Events (0-9999) +Standard events that are stored indefinitely. Examples: +- Kind 0: User Metadata +- Kind 1: Short Text Note +- Kind 7: Reaction +- Kind 1984: Reporting + +### Replaceable Events (10000-19999) +Events where only the latest version is kept. Examples: +- Kind 10000: Mute List +- Kind 10002: Relay List Metadata +- Kind 13194: Wallet Info + +### Ephemeral Events (20000-29999) +Events not intended for permanent storage. Examples: +- Kind 22242: Client Authentication +- Kind 24133: Nostr Connect + +### Addressable Events (30000-39999) +Parameterized replaceable events identified by kind + pubkey + d-tag. Examples: +- Kind 30023: Long-form Content +- Kind 30311: Live Event +- Kind 34550: Community Definition + +## API Reference + +### Helper Functions in `eventKinds.js` + +```javascript +import { + eventKinds, // Array of all event kinds + kindCategories, // Array of category filter definitions + getEventKind, // Get kind info by number + searchEventKinds, // Search by query string + createTemplateEvent // Generate template with current timestamp +} from './eventKinds.js'; + +// Get information about a specific kind +const kind1 = getEventKind(1); +// Returns: { kind: 1, name: "Short Text Note", description: "...", template: {...} } + +// Search for kinds +const results = searchEventKinds("zap"); +// Returns: Array of matching kinds + +// Create a template event +const template = createTemplateEvent(1, "abc123..."); +// Returns: Event object with current timestamp and provided pubkey +``` + +## Troubleshooting + +### "Permission denied" error +Your user role does not allow publishing events. Check your role in the header badge and contact a relay administrator. + +### "Policy Error: kind blocked" +The relay's policy configuration does not allow this event kind. If you're an administrator, check `ORLY_POLICY_PATH` or the Policy tab. + +### "Event must be signed before publishing" +Click the **Sign** button before **Publish**. Events must be cryptographically signed before the relay will accept them. + +### Template not loading +Ensure JavaScript is enabled and the page has fully loaded. Try refreshing the page. + +## Related Documentation + +- [POLICY_USAGE_GUIDE.md](./POLICY_USAGE_GUIDE.md) - Policy configuration for event restrictions +- [POLICY_CONFIGURATION_REFERENCE.md](./POLICY_CONFIGURATION_REFERENCE.md) - Policy rule reference +- [NIPs Repository](https://github.com/nostr-protocol/nips) - Official Nostr protocol specifications diff --git a/pkg/version/version b/pkg/version/version index 303cd19..5203324 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.36.1 +v0.36.2