mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-12 08:27:27 +02:00
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.
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user