From 3449f5e66fa3f9b5ff52f10131823fbd8cd98729 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 21 Jan 2026 15:10:45 +0000 Subject: [PATCH] docs: add comprehensive blueprint documentation to applesauce skills Added detailed documentation for: - NoteBlueprint (automatic hashtag/mention/quote extraction) - NoteReplyBlueprint (NIP-10 threading) - ReactionBlueprint (kind 7 reactions) - GroupMessageBlueprint (NIP-29 groups) - DeleteBlueprint (NIP-09 deletion) - EventFactory usage patterns - Before/after examples showing code reduction - Best practices for using blueprints This documents the refactoring work done throughout the codebase. --- .claude/skills/applesauce-common/SKILL.md | 349 +++++++++++++++++++++- .claude/skills/applesauce-core/SKILL.md | 131 ++++++++ 2 files changed, 472 insertions(+), 8 deletions(-) diff --git a/.claude/skills/applesauce-common/SKILL.md b/.claude/skills/applesauce-common/SKILL.md index 6ab2124..22af114 100644 --- a/.claude/skills/applesauce-common/SKILL.md +++ b/.claude/skills/applesauce-common/SKILL.md @@ -196,23 +196,356 @@ function NoteComponent({ event }) { ## Blueprints -Blueprints provide templates for creating events. +Blueprints are factory functions that create Nostr events with automatic tag extraction and proper NIP compliance. They eliminate manual tag building and reduce boilerplate code significantly. + +### Key Benefits + +1. **Automatic Tag Extraction**: Blueprints automatically extract hashtags (#word), mentions (nostr:npub), and event quotes (nostr:note/nevent) from text content +2. **NIP Compliance**: Each blueprint follows the correct NIP specifications for its event type +3. **Less Code**: Replace 50-100 lines of manual tag building with 5-10 lines +4. **Type Safety**: Full TypeScript support with proper types for all options +5. **Maintainable**: Centralized event building logic that's easier to update + +### Using Blueprints ```typescript import { EventFactory } from 'applesauce-core/event-factory'; import { NoteBlueprint } from 'applesauce-common/blueprints'; -const factory = new EventFactory({ signer }); - -// Create a note using blueprint -const draft = await factory.build(NoteBlueprint({ - content: 'Hello Nostr!', - tags: [['t', 'nostr']] -})); +const factory = new EventFactory(); +factory.setSigner(signer); +// Create event from blueprint +const draft = await factory.create(NoteBlueprint, content, options); const event = await factory.sign(draft); ``` +### NoteBlueprint (Kind 1) + +Creates short text notes with automatic hashtag, mention, and quote extraction. + +**What it handles automatically:** +- Extracts `#hashtags` from content → `t` tags +- Extracts `nostr:npub...` mentions → `p` tags +- Extracts `nostr:note...` and `nostr:nevent...` quotes → `q` tags (NIP-18) +- Adds custom emoji tags (NIP-30) + +```typescript +import { NoteBlueprint } from 'applesauce-common/blueprints'; + +// Simple note +const draft = await factory.create( + NoteBlueprint, + 'Hello #nostr! Check out nostr:npub1abc...', + {} +); + +// With custom emojis +const draft = await factory.create( + NoteBlueprint, + 'Hello :rocket:!', + { + emojis: [{ shortcode: 'rocket', url: 'https://example.com/rocket.png' }] + } +); + +// The blueprint automatically adds: +// - ["t", "nostr"] for #nostr +// - ["p", "decoded-pubkey"] for the npub mention +// - ["emoji", "rocket", "https://example.com/rocket.png"] for custom emoji +``` + +**Options:** +- `emojis?: Array<{ shortcode: string; url: string }>` - Custom emojis (NIP-30) +- `contentWarning?: boolean | string` - Content warning tag + +**Before/After Example:** +```typescript +// ❌ BEFORE: Manual tag building (~70 lines) +const hashtags = content.match(/#(\w+)/g)?.map(tag => tag.slice(1)) || []; +const mentionRegex = /nostr:(npub1[a-z0-9]+)/g; +const mentions = []; +let match; +while ((match = mentionRegex.exec(content)) !== null) { + try { + const { data } = nip19.decode(match[1]); + mentions.push(data); + } catch (e) { /* ignore */ } +} +// ... more extraction logic ... +draft.tags = [ + ...hashtags.map(t => ['t', t]), + ...mentions.map(p => ['p', p]), + // ... more tags ... +]; + +// ✅ AFTER: Blueprint handles everything +const draft = await factory.create(NoteBlueprint, content, { emojis }); +``` + +### NoteReplyBlueprint (Kind 1 Reply) + +Creates threaded note replies following NIP-10 conventions. + +**What it handles automatically:** +- Extracts root event from parent's tags (NIP-10) +- Adds proper `e` tags with markers (root, reply) +- Copies `p` tags from parent for notifications +- Extracts hashtags, mentions, and quotes from content +- Uses `q` tags for quotes instead of `e` tags (correct semantic) + +```typescript +import { NoteReplyBlueprint } from 'applesauce-common/blueprints'; + +// Reply to a note +const parentEvent = await eventStore.event(parentId).toPromise(); + +const draft = await factory.create( + NoteReplyBlueprint, + parentEvent, + 'Great point! #bitcoin', + { + emojis: [{ shortcode: 'fire', url: 'https://example.com/fire.png' }] + } +); + +// The blueprint automatically: +// 1. Finds root from parent's tags (if parent is also a reply) +// 2. Adds ["e", rootId, relay, "root"] +// 3. Adds ["e", parentId, relay, "reply"] +// 4. Copies all ["p", ...] tags from parent +// 5. Extracts #bitcoin → ["t", "bitcoin"] +// 6. Adds emoji tag +``` + +**Options:** +- `emojis?: Array<{ shortcode: string; url: string }>` - Custom emojis +- `contentWarning?: boolean | string` - Content warning + +**Before/After Example:** +```typescript +// ❌ BEFORE: Manual NIP-10 threading (~95 lines) +const parentRefs = getNip10References(parentEvent); +const rootId = parentRefs.root?.e || parentEvent.id; +const rootRelay = parentRefs.root?.relay || ''; + +draft.tags = [ + ['e', rootId, rootRelay, 'root'], + ['e', parentEvent.id, '', 'reply'], +]; + +// Copy p-tags from parent +const parentPTags = parentEvent.tags.filter(t => t[0] === 'p'); +draft.tags.push(...parentPTags); +if (!parentPTags.some(t => t[1] === parentEvent.pubkey)) { + draft.tags.push(['p', parentEvent.pubkey]); +} +// ... hashtag extraction ... +// ... mention extraction ... + +// ✅ AFTER: Blueprint handles NIP-10 threading +const draft = await factory.create( + NoteReplyBlueprint, + parentEvent, + content, + { emojis } +); +``` + +### ReactionBlueprint (Kind 7) + +Creates reactions to events (likes, custom emoji reactions). + +**What it handles automatically:** +- Adds `e` tag pointing to reacted event +- Adds `k` tag for event kind +- Adds `p` tag for event author +- Handles custom emoji reactions (`:shortcode:` format) +- Supports both string emoji and Emoji objects + +```typescript +import { ReactionBlueprint } from 'applesauce-common/blueprints'; + +// Simple like (+ emoji) +const draft = await factory.create(ReactionBlueprint, messageEvent, '+'); + +// Custom emoji reaction +const draft = await factory.create( + ReactionBlueprint, + messageEvent, + { + shortcode: 'rocket', + url: 'https://example.com/rocket.png' + } +); + +// String emoji +const draft = await factory.create(ReactionBlueprint, messageEvent, '🚀'); + +// The blueprint automatically adds: +// - ["e", messageEvent.id] +// - ["k", messageEvent.kind.toString()] +// - ["p", messageEvent.pubkey] +// For custom emoji: ["emoji", "rocket", "url"] +``` + +**Options:** +- Second parameter: `emoji?: string | { shortcode: string; url: string }` + +**Before/After Example:** +```typescript +// ❌ BEFORE: Manual reaction building (~15 lines per adapter) +draft.kind = 7; +draft.content = typeof emoji === 'string' ? emoji : `:${emoji.shortcode}:`; +draft.tags = [ + ['e', messageEvent.id], + ['k', messageEvent.kind.toString()], + ['p', messageEvent.pubkey], +]; +if (typeof emoji === 'object') { + draft.tags.push(['emoji', emoji.shortcode, emoji.url]); +} + +// ✅ AFTER: Blueprint handles reactions +const draft = await factory.create(ReactionBlueprint, messageEvent, emoji); +``` + +### GroupMessageBlueprint (Kind 9 - NIP-29) + +Creates NIP-29 group chat messages. + +**What it handles automatically:** +- Adds `h` tag with group ID +- Extracts hashtags, mentions, and quotes from content +- Adds custom emoji tags +- Handles message threading with `previous` field + +```typescript +import { GroupMessageBlueprint } from 'applesauce-common/blueprints'; + +// Send message to NIP-29 group +const draft = await factory.create( + GroupMessageBlueprint, + { id: groupId, relay: relayUrl }, + 'Hello group! #welcome', + { + previous: [], // Array of previous message events for threading + emojis: [{ shortcode: 'wave', url: 'https://example.com/wave.png' }] + } +); + +// The blueprint automatically adds: +// - ["h", groupId] +// - ["t", "welcome"] for #welcome hashtag +// - ["emoji", "wave", "url"] for custom emoji +``` + +**Options:** +- `previous?: NostrEvent[]` - Previous messages for threading (required, use `[]` if no threading) +- `emojis?: Array<{ shortcode: string; url: string }>` - Custom emojis + +**Note:** The `previous` field is required by the type, but can be an empty array if you don't need threading. + +### DeleteBlueprint (Kind 5 - NIP-09) + +Creates event deletion requests. + +**What it handles automatically:** +- Adds `e` tags for each event to delete +- Sets proper kind and content format +- Adds optional reason in content + +```typescript +import { DeleteBlueprint } from 'applesauce-common/blueprints'; + +// Delete single event +const draft = await factory.create( + DeleteBlueprint, + [eventToDelete], + 'Accidental post' +); + +// Delete multiple events +const draft = await factory.create( + DeleteBlueprint, + [event1, event2, event3], + 'Cleaning up old posts' +); + +// Without reason +const draft = await factory.create(DeleteBlueprint, [event], ''); + +// The blueprint automatically: +// - Sets kind to 5 +// - Adds ["e", eventId] for each event +// - Sets content to reason (or empty) +``` + +**Parameters:** +- `events: (string | NostrEvent)[]` - Events to delete (IDs or full events) +- `reason?: string` - Optional deletion reason + +### Adding Custom Tags + +Blueprints handle common tags automatically, but you can add custom tags afterward: + +```typescript +// Create with blueprint +const draft = await factory.create(NoteBlueprint, content, { emojis }); + +// Add custom tags not handled by blueprint +draft.tags.push(['client', 'grimoire', '31990:...']); +draft.tags.push(['a', `${kind}:${pubkey}:${identifier}`]); + +// Add NIP-92 imeta tags for blob attachments +for (const blob of blobAttachments) { + draft.tags.push(['imeta', `url ${blob.url}`, `x ${blob.sha256}`, ...]); +} + +// Sign the modified draft +const event = await factory.sign(draft); +``` + +### Protocol-Specific Tag Additions + +Some protocols require additional tags beyond what blueprints provide: + +```typescript +// NIP-29: Add q-tag for replies (not in blueprint yet) +const draft = await factory.create(GroupMessageBlueprint, group, content, options); +if (replyToId) { + draft.tags.push(['q', replyToId]); +} + +// NIP-53: Add a-tag for live activity context +const draft = await factory.create(ReactionBlueprint, messageEvent, emoji); +draft.tags.push(['a', liveActivityATag, relay]); +``` + +### Available Blueprints + +All blueprints from `applesauce-common/blueprints`: + +- **NoteBlueprint** - Kind 1 short text notes +- **NoteReplyBlueprint** - Kind 1 threaded replies (NIP-10) +- **ReactionBlueprint** - Kind 7 reactions (NIP-25) +- **GroupMessageBlueprint** - Kind 9 group messages (NIP-29) +- **DeleteBlueprint** - Kind 5 deletion requests (NIP-09) +- **MetadataBlueprint** - Kind 0 profile metadata +- **ContactsBlueprint** - Kind 3 contact lists +- **ArticleBlueprint** - Kind 30023 long-form articles (NIP-23) +- **HighlightBlueprint** - Kind 9802 highlights (NIP-84) +- **ZapRequestBlueprint** - Kind 9734 zap requests (NIP-57) +- And more - check `node_modules/applesauce-common/dist/blueprints/` + +### Best Practices + +1. **Always use blueprints** when creating standard event types - they handle NIPs correctly +2. **Add custom tags after** blueprint creation for app-specific metadata +3. **Don't extract tags manually** - let blueprints handle hashtags, mentions, quotes +4. **Use proper emoji format** - blueprints expect `{ shortcode, url }` objects +5. **Check blueprint source** - when in doubt, read the blueprint code for exact behavior + ## Operations Operations modify existing events. diff --git a/.claude/skills/applesauce-core/SKILL.md b/.claude/skills/applesauce-core/SKILL.md index 5ce32fe..d599dda 100644 --- a/.claude/skills/applesauce-core/SKILL.md +++ b/.claude/skills/applesauce-core/SKILL.md @@ -619,6 +619,137 @@ const options = useMemo( ); ``` +## EventFactory + +### Overview + +The `EventFactory` class (moved to `applesauce-core/event-factory` in v5) provides a unified API for creating and signing Nostr events using blueprints. + +```typescript +import { EventFactory } from 'applesauce-core/event-factory'; + +// Create factory +const factory = new EventFactory(); + +// Set signer (required for signing) +factory.setSigner(signer); + +// Optional context +factory.setClient({ name: 'grimoire', address: { pubkey, identifier } }); +``` + +### Creating Events with Blueprints + +Blueprints are templates for event creation. See the **applesauce-common** skill for detailed blueprint documentation. + +```typescript +import { NoteBlueprint, NoteReplyBlueprint } from 'applesauce-common/blueprints'; + +// Create a note +const draft = await factory.create( + NoteBlueprint, + 'Hello #nostr!', + { emojis: [{ shortcode: 'wave', url: 'https://...' }] } +); + +// Sign the draft +const event = await factory.sign(draft); + +// Or combine: create and sign +const event = await factory.sign( + await factory.create(NoteBlueprint, content, options) +); +``` + +### Factory Methods + +```typescript +// Create from blueprint +const draft = await factory.create(Blueprint, ...args); + +// Sign event template +const event = await factory.sign(draft); + +// Stamp (add pubkey without signing) +const unsigned = await factory.stamp(draft); + +// Build with operations +const draft = await factory.build(template, ...operations); + +// Modify existing event +const modified = await factory.modify(event, ...operations); +``` + +### Using with Actions + +EventFactory integrates seamlessly with the action system: + +```typescript +import accountManager from '@/services/accounts'; +import { EventFactory } from 'applesauce-core/event-factory'; +import { NoteBlueprint } from 'applesauce-common/blueprints'; + +const account = accountManager.active; +const signer = account.signer; + +const factory = new EventFactory(); +factory.setSigner(signer); + +// Create and sign +const draft = await factory.create(NoteBlueprint, content, options); +const event = await factory.sign(draft); + +// Publish +await pool.publish(relays, event); +``` + +### Common Patterns + +**Creating with custom tags:** +```typescript +// Let blueprint handle automatic extraction +const draft = await factory.create(NoteBlueprint, content, { emojis }); + +// Add custom tags afterward +draft.tags.push(['client', 'grimoire', '31990:...']); +draft.tags.push(['imeta', `url ${blob.url}`, `x ${blob.sha256}`]); + +// Sign +const event = await factory.sign(draft); +``` + +**Error handling:** +```typescript +try { + const draft = await factory.create(NoteBlueprint, content, options); + const event = await factory.sign(draft); + await pool.publish(relays, event); +} catch (error) { + if (error.message.includes('User rejected')) { + // Handle rejection + } else { + // Handle other errors + } +} +``` + +**Optimistic updates:** +```typescript +// Create and sign +const event = await factory.sign( + await factory.create(NoteBlueprint, content, options) +); + +// Add to local store immediately +eventStore.add(event); + +// Publish in background +pool.publish(relays, event).catch(err => { + // Remove from store on failure + eventStore.remove(event.id); +}); +``` + ## NIP Helpers ### NIP-05 Verification