finish skills
This commit is contained in:
@@ -0,0 +1,293 @@
|
||||
# Event Hierarchy & Structure
|
||||
|
||||
## Core Hierarchy
|
||||
|
||||
```
|
||||
IEvent (empty interface)
|
||||
└── Event (@Immutable base class)
|
||||
├── BaseAddressableEvent (replaceable + addressable, has d-tag)
|
||||
│ ├── BaseReplaceableEvent (kinds 10000-20000, FIXED_D_TAG = "")
|
||||
│ └── [Specific addressable events - 30000-40000]
|
||||
└── [Specific event implementations - all other kinds]
|
||||
```
|
||||
|
||||
## Event Base Class
|
||||
|
||||
**Location**: `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip01Core/core/Event.kt`
|
||||
|
||||
```kotlin
|
||||
@Immutable
|
||||
open class Event(
|
||||
val id: HexKey, // SHA-256 hash of serialized event
|
||||
val pubKey: HexKey, // Author's public key (32 bytes hex)
|
||||
val createdAt: Long, // Unix timestamp
|
||||
val kind: Kind, // Event kind (Int typealias)
|
||||
val tags: TagArray, // Array of tag arrays
|
||||
val content: String, // Event content
|
||||
val sig: HexKey, // schnorr signature (64 bytes hex)
|
||||
) : IEvent, OptimizedSerializable
|
||||
```
|
||||
|
||||
## Kind Classification
|
||||
|
||||
```kotlin
|
||||
typealias Kind = Int
|
||||
|
||||
fun Kind.isEphemeral() = this in 20000..29999
|
||||
fun Kind.isReplaceable() = this == 0 || this == 3 || this in 10000..19999
|
||||
fun Kind.isAddressable() = this in 30000..39999
|
||||
fun Kind.isRegular() = this in 1000..9999
|
||||
```
|
||||
|
||||
## Common Event Types
|
||||
|
||||
### Text Note (kind 1)
|
||||
```kotlin
|
||||
class TextNoteEvent(...) : BaseThreadedEvent(...),
|
||||
EventHintProvider, AddressHintProvider, PubKeyHintProvider, SearchableEvent
|
||||
|
||||
// Threading support via markers: reply, root, mention
|
||||
fun replyTo(): List<Note> // Direct reply targets
|
||||
fun root(): Note? // Root of thread
|
||||
```
|
||||
|
||||
### Metadata (kind 0)
|
||||
```kotlin
|
||||
class MetadataEvent(...) : BaseAddressableEvent(...)
|
||||
|
||||
// Replaceable: newest version overwrites old
|
||||
// d-tag automatically set to "" for kind 0
|
||||
fun name(): String?
|
||||
fun displayName(): String?
|
||||
fun picture(): String?
|
||||
fun about(): String?
|
||||
fun lnAddress(): String?
|
||||
```
|
||||
|
||||
### Reaction (kind 7)
|
||||
```kotlin
|
||||
class ReactionEvent(...) : Event(...)
|
||||
|
||||
companion object {
|
||||
const val LIKE = "+"
|
||||
const val DISLIKE = "-"
|
||||
|
||||
fun like(reactedTo: EventHintBundle<Event>, ...)
|
||||
fun dislike(reactedTo: EventHintBundle<Event>, ...)
|
||||
}
|
||||
```
|
||||
|
||||
### Zap Request/Receipt (kinds 9734, 9735)
|
||||
```kotlin
|
||||
class LnZapRequestEvent(...) : Event(...)
|
||||
// Created by client, sent to Lightning Address
|
||||
|
||||
class LnZapEvent(...) : Event(...)
|
||||
// Receipt from LSP, contains bolt11 + embedded zap request
|
||||
val zapRequest: LnZapRequestEvent? by lazy { containedPost() }
|
||||
val amount: BigDecimal? by lazy { /* parse from bolt11 */ }
|
||||
```
|
||||
|
||||
### Long-Form Content (kind 30023)
|
||||
```kotlin
|
||||
class LongTextNoteEvent(...) : BaseAddressableEvent(...)
|
||||
// Blog posts, articles
|
||||
// Addressable via kind:pubkey:d-tag
|
||||
```
|
||||
|
||||
### Lists (kinds 10000-30004)
|
||||
```kotlin
|
||||
sealed class PeopleListEvent : BaseAddressableEvent {
|
||||
object MuteList : PeopleListEvent(10000)
|
||||
object PinList : PeopleListEvent(10001)
|
||||
object BookmarkList : PeopleListEvent(10003)
|
||||
// ... 18 list types total
|
||||
}
|
||||
```
|
||||
|
||||
## Event Interfaces
|
||||
|
||||
### Hint Providers
|
||||
Events can implement interfaces to optimize relay queries:
|
||||
|
||||
```kotlin
|
||||
interface EventHintProvider {
|
||||
fun taggedEventIds(): Set<HexKey>
|
||||
fun taggedEventRelays(): Map<HexKey, Set<NormalizedRelayUrl>>
|
||||
}
|
||||
|
||||
interface PubKeyHintProvider {
|
||||
fun taggedPubKeys(): Set<HexKey>
|
||||
fun taggedPubKeyRelays(): Map<HexKey, Set<NormalizedRelayUrl>>
|
||||
}
|
||||
|
||||
interface AddressHintProvider {
|
||||
fun taggedAddresses(): Set<Address>
|
||||
fun taggedAddressRelays(): Map<Address, Set<NormalizedRelayUrl>>
|
||||
}
|
||||
|
||||
interface SearchableEvent {
|
||||
fun subject(): String?
|
||||
fun isContentEncoded(): Boolean
|
||||
}
|
||||
```
|
||||
|
||||
## Event Building Pattern
|
||||
|
||||
### DSL Builder
|
||||
```kotlin
|
||||
TextNoteEvent.build(
|
||||
note = "Hello Nostr",
|
||||
replyingTo = eventBundle,
|
||||
createdAt = TimeUtils.now()
|
||||
) {
|
||||
pTag(pubKey, relayHint) // Tag person
|
||||
eTag(eventId, relayHint, "reply") // Tag event with marker
|
||||
hashtag("nostr") // Add hashtag
|
||||
alt("A short note") // Alt text
|
||||
}
|
||||
```
|
||||
|
||||
### Event Template (Low-level)
|
||||
```kotlin
|
||||
suspend fun eventTemplate(
|
||||
kind: Kind,
|
||||
content: String,
|
||||
createdAt: Long,
|
||||
initializer: TagArrayBuilder.() -> Unit
|
||||
): EventTemplate {
|
||||
val tags = TagArrayBuilder().apply(initializer).build()
|
||||
return EventTemplate(kind, tags, content, createdAt)
|
||||
}
|
||||
|
||||
// Sign with signer
|
||||
val template = eventTemplate(1, "Hello", now()) { pTag(pubkey) }
|
||||
val signedEvent = signer.sign(template)
|
||||
```
|
||||
|
||||
## Addressable vs Regular Events
|
||||
|
||||
| Feature | Regular Event | Addressable Event |
|
||||
|---------|---------------|-------------------|
|
||||
| **Identifier** | Event ID (SHA-256 hash) | Address (kind:pubkey:d-tag) |
|
||||
| **Replaceability** | Immutable | Newest replaces old |
|
||||
| **d-tag** | Optional | Required |
|
||||
| **Lookup** | By event ID | By address |
|
||||
| **Example** | Text note (kind 1) | Metadata (kind 0), Long-form (kind 30023) |
|
||||
|
||||
```kotlin
|
||||
// Regular event address
|
||||
note = LocalCache.getNoteIfExists(eventId)
|
||||
|
||||
// Addressable event address
|
||||
address = Address(kind = 30023, pubkey = authorHex, dTag = "my-article")
|
||||
note = LocalCache.getAddressableNoteIfExists(address)
|
||||
```
|
||||
|
||||
## Event Validation
|
||||
|
||||
```kotlin
|
||||
// Verify event ID matches computed hash
|
||||
fun Event.verifyId(): Boolean =
|
||||
EventHasher.hashIdCheck(id, pubKey, createdAt, kind, tags, content)
|
||||
|
||||
// Verify signature
|
||||
fun Event.verifySignature(): Boolean =
|
||||
Nip01.verify(Hex.decode(sig), Hex.decode(id), Hex.decode(pubKey))
|
||||
|
||||
// Complete verification
|
||||
fun Event.checkSignature() {
|
||||
if (!verifyId()) throw Exception("ID mismatch")
|
||||
if (!verifySignature()) throw Exception("Bad signature!")
|
||||
}
|
||||
```
|
||||
|
||||
## Event Serialization
|
||||
|
||||
```kotlin
|
||||
// To JSON (for transmission/signing)
|
||||
fun Event.toJson(): String = OptimizedJsonMapper.toJson(this)
|
||||
|
||||
// From JSON
|
||||
fun Event.fromJson(json: String): Event = OptimizedJsonMapper.fromJson(json)
|
||||
|
||||
// Event ID generation (SHA-256 of canonical JSON)
|
||||
fun EventHasher.hashId(
|
||||
pubKey: HexKey,
|
||||
createdAt: Long,
|
||||
kind: Kind,
|
||||
tags: TagArray,
|
||||
content: String
|
||||
): HexKey {
|
||||
val serialized = """[0,"$pubKey",$createdAt,$kind,${tags.toJson()},"$content"]"""
|
||||
return sha256(serialized.encodeToByteArray()).toHexKey()
|
||||
}
|
||||
```
|
||||
|
||||
## Event Lifecycle in LocalCache
|
||||
|
||||
```
|
||||
Event received from relay
|
||||
↓
|
||||
LocalCache.consume(event, relay, wasVerified)
|
||||
↓
|
||||
getOrCreateNote(event.id) or getOrCreateAddressableNote(address)
|
||||
↓
|
||||
justVerify(event) → checkSignature()
|
||||
↓
|
||||
note.loadEvent(event, author, replyTo)
|
||||
↓
|
||||
Update indices (replies, reactions, boosts)
|
||||
↓
|
||||
refreshNewNoteObservers(note) → emit to SharedFlow
|
||||
↓
|
||||
UI updates
|
||||
```
|
||||
|
||||
## Common Event Patterns
|
||||
|
||||
### Reply Threading
|
||||
```kotlin
|
||||
// Root event (top of thread)
|
||||
val rootEvent = TextNoteEvent.build("Thread root") { }
|
||||
|
||||
// Reply to root
|
||||
val reply1 = TextNoteEvent.build("First reply", replyingTo = rootEvent) {
|
||||
// Automatically adds:
|
||||
// ["e", <root_id>, <relay>, "root"]
|
||||
// ["e", <root_id>, <relay>, "reply"]
|
||||
}
|
||||
|
||||
// Reply to reply (nested)
|
||||
val reply2 = TextNoteEvent.build("Nested reply", replyingTo = reply1) {
|
||||
// Automatically adds:
|
||||
// ["e", <root_id>, <relay>, "root"]
|
||||
// ["e", <reply1_id>, <relay>, "reply"]
|
||||
}
|
||||
```
|
||||
|
||||
### Replaceable Events
|
||||
```kotlin
|
||||
// Metadata update (kind 0) - newest wins
|
||||
val metadata1 = MetadataEvent.createNew(name = "Alice", picture = "url1")
|
||||
Thread.sleep(1000)
|
||||
val metadata2 = MetadataEvent.createNew(name = "Alice Updated", picture = "url2")
|
||||
|
||||
// LocalCache keeps only metadata2 (higher createdAt)
|
||||
```
|
||||
|
||||
### Event Deletion
|
||||
```kotlin
|
||||
// Delete events
|
||||
val deletion = DeletionEvent.create(
|
||||
deleteEvents = listOf(eventId1, eventId2),
|
||||
reason = "Spam",
|
||||
signer = signer
|
||||
)
|
||||
|
||||
// LocalCache marks events as deleted, but doesn't remove (for verification)
|
||||
```
|
||||
|
||||
## 63+ Event Classes
|
||||
|
||||
Full list at `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip*/` - one class per event type across 60+ NIP implementations.
|
||||
@@ -0,0 +1,179 @@
|
||||
# NIP Catalog: 60 Standard + 8 Experimental NIPs in Quartz
|
||||
|
||||
## Standard NIPs by Category
|
||||
|
||||
### Core/Basic Protocol
|
||||
| NIP | Directory | Key Files | Description |
|
||||
|-----|-----------|-----------|-------------|
|
||||
| 01 | `nip01Core/` | Event.kt, Kind.kt, Tag.kt | Core protocol, event structure, kinds, tags |
|
||||
| 02 | `nip02FollowList/` | ContactListEvent.kt | Follow/contact lists (kind 3) |
|
||||
| 03 | `nip03Timestamp/` | OpenTimestampsAttestation.kt | Timestamps |
|
||||
| 04 | `nip04Dm/` | EncryptedDmEvent.kt | Legacy encrypted DMs (deprecated for NIP-17) |
|
||||
| 05 | `nip05DnsIdentifiers/` | Nip05Verifier.kt | DNS-based verification |
|
||||
| 06 | `nip06KeyDerivation/` | Mnemonic-related | BIP-39 key derivation |
|
||||
| 09 | `nip09Deletions/` | DeletionEvent.kt | Event deletion requests (kind 5) |
|
||||
| 11 | `nip11RelayInfo/` | RelayInformation.kt | Relay metadata |
|
||||
| 13 | `nip13Pow/` | ProofOfWork.kt | Proof of work |
|
||||
| 14 | `nip14Subject/` | Subject tags | Subject tags for text notes |
|
||||
| 17 | `nip17Dm/` | GiftWrapEvent.kt, SealedGossipEvent.kt | Private DMs (replacem
|
||||
|
||||
ent for NIP-04) |
|
||||
| 21 | `nip21UriScheme/` | URI scheme (`nostr:`) | URI scheme parsing |
|
||||
| 42 | `nip42RelayAuth/` | RelayAuthEvent.kt | Relay authentication (kind 22242) |
|
||||
| 44 | `nip44Encryption/` | Nip44.kt, Nip44v2.kt | Modern encryption (ChaCha20) |
|
||||
| 49 | `nip49PrivKeyEnc/` | NIP-49Ncryptsec.kt | Private key encryption format |
|
||||
|
||||
### Content Types
|
||||
| NIP | Directory | Key Files | Description |
|
||||
|-----|-----------|-----------|-------------|
|
||||
| 10 | `nip10Notes/` | TextNoteEvent.kt | Text notes with threading (kind 1) |
|
||||
| 18 | `nip18Reposts/` | RepostEvent.kt, GenericRepostEvent.kt | Reposts (kind 6, 16) |
|
||||
| 22 | `nip22Comments/` | CommentEvent.kt | Comments (kind 1111) |
|
||||
| 23 | `nip23LongContent/` | LongTextNoteEvent.kt | Long-form content (kind 30023) |
|
||||
| 25 | `nip25Reactions/` | ReactionEvent.kt | Reactions (kind 7) |
|
||||
| 31 | `nip31Alts/` | Alt tags | Alt description tags |
|
||||
| 36 | `nip36SensitiveContent/` | Content warnings | Content warning tags |
|
||||
| 37 | `nip37Drafts/` | DraftEvent.kt | Drafts (kind 31234) |
|
||||
| 50 | `nip50Search/` | Search filters | Full-text search |
|
||||
|
||||
### Encoding & Standards
|
||||
| NIP | Directory | Key Files | Description |
|
||||
|-----|-----------|-----------|-------------|
|
||||
| 19 | `nip19Bech32/` | Nip19.kt | Bech32 encoding (npub, nsec, note, nevent, nprofile, naddr) |
|
||||
| 40 | `nip40Expiration/` | Expiration tags | Event expiration |
|
||||
| 48 | `nip48ProxyTags/` | Proxy tags | Proxy tags for delegation |
|
||||
| 62 | `nip62RequestToVanish/` | RequestToVanishEvent.kt | Request to vanish (kind 12) |
|
||||
| 98 | `nip98HttpAuth/` | HTTP authorization | HTTP auth header |
|
||||
|
||||
### Lists & Management
|
||||
| NIP | Directory | Key Files | Description |
|
||||
|-----|-----------|-----------|-------------|
|
||||
| 51 | `nip51Lists/` | 18 list types | Named lists (mute, bookmarks, pins, communities, etc.) (kinds 10000-30004) |
|
||||
| 65 | `nip65RelayList/` | AdvertisedRelayListEvent.kt | Relay lists (kind 10002) |
|
||||
|
||||
### Social & Identity
|
||||
| NIP | Directory | Key Files | Description |
|
||||
|-----|-----------|-----------|-------------|
|
||||
| 39 | `nip39ExtIdentities/` | External identities | External identity claims |
|
||||
| 46 | `nip46RemoteSigner/` | NostrConnectEvent.kt | Remote signer protocol (bunker) |
|
||||
| 47 | `nip47WalletConnect/` | Nostr Wallet Connect | Wallet connection protocol |
|
||||
| 56 | `nip56Reports/` | ReportEvent.kt | Reports (kind 1984) |
|
||||
| 57 | `nip57Zaps/` | LnZapEvent.kt, LnZapRequestEvent.kt | Lightning zaps (kinds 9734, 9735) |
|
||||
| 58 | `nip58Badges/` | Badge events | Badge definitions & awards (kinds 30009, 8) |
|
||||
| 59 | `nip59Giftwrap/` | GiftWrapEvent.kt | Gift-wrapped events for privacy |
|
||||
| 75 | `nip75ZapGoals/` | ZapGoalEvent.kt | Zap goals (kind 9041) |
|
||||
|
||||
### Specialized Content
|
||||
| NIP | Directory | Key Files | Description |
|
||||
|-----|-----------|-----------|-------------|
|
||||
| 28 | `nip28PublicChat/` | ChannelCreateEvent.kt, ChannelMessageEvent.kt | Public chat channels (kinds 40-44) |
|
||||
| 30 | `nip30CustomEmoji/` | EmojiUrl.kt | Custom emoji |
|
||||
| 34 | `nip34Git/` | Git patch/issue events | Git repository tracking (kinds 30617, 30618, 1617, 1621, 1622, 1630, 1633) |
|
||||
| 35 | `nip35Torrents/` | Torrent events | Torrent tracking |
|
||||
| 52 | `nip52Calendar/` | Calendar events | Calendar time-based/date-based (kinds 31922-31925) |
|
||||
| 53 | `nip53LiveActivities/` | LiveActivitiesEvent.kt | Live events/streaming (kind 30311) |
|
||||
| 54 | `nip54Wiki/` | WikiNoteEvent.kt | Wiki pages (kind 30818) |
|
||||
| 68 | `nip68Picture/` | Picture metadata | Picture metadata |
|
||||
| 71 | `nip71Video/` | 7 video event types | Video events (kinds 34235, 35235, 1234, 1235) |
|
||||
| 72 | `nip72ModCommunities/` | Community events | Moderated communities (kinds 34550, 34551, 9041) |
|
||||
| 84 | `nip84Highlights/` | HighlightEvent.kt | Highlights (kind 9802) |
|
||||
| 89 | `nip89AppHandlers/` | AppDefinitionEvent.kt | App recommendations (kinds 31990, 31989) |
|
||||
| 90 | `nip90Dvms/` | DVM job events | Data Vending Machines (DVMs) (kinds 5000-7000) |
|
||||
| 92 | `nip92IMeta/` | IMeta tags | Image metadata tags |
|
||||
| 94 | `nip94FileMetadata/` | FileHeaderEvent.kt, FileStorageEvent.kt | File metadata (kind 1063) |
|
||||
| 96 | `nip96FileStorage/` | HTTP file storage | HTTP-based file storage |
|
||||
| 99 | `nip99Classifieds/` | ClassifiedsEvent.kt | Classifieds/marketplace (kind 30402) |
|
||||
| A0 | `nipA0VoiceMessages/` | Voice messages | Voice message events |
|
||||
| B7 | `nipB7Blossom/` | Blossom server URLs | Blossom file storage |
|
||||
|
||||
### Web/Storage/Other
|
||||
| NIP | Directory | Key Files | Description |
|
||||
|-----|-----------|-----------|-------------|
|
||||
| 38 | `nip38UserStatus/` | StatusEvent.kt | User status (kind 30315) |
|
||||
| 60 | `nip60Payment/` | Wallet events | Wallet info (kind 13194) |
|
||||
| 61 | `nip61PaymentRequest/` | Nut zaps | Cashu payment requests |
|
||||
| 64 | `nip64Chess/` | Chess moves | Chess move events |
|
||||
| 66 | `nip66Monitoring/` | Relay monitor events | Relay monitoring |
|
||||
| 67 | `nip67Invoices/` | Invoice tags | Lightning invoice tags |
|
||||
| 69 | `nip69Offers/` | BOLT-12 offers | BOLT-12 offer tags |
|
||||
| 70 | `nip70ProtectedEvts/` | Protected events | Protected event types |
|
||||
| 73 | `nip73ExternalIds/` | External content IDs | External content identifiers |
|
||||
| 78 | `nip78AppData/` | AppDataEvent.kt | Application data (kind 30078) |
|
||||
| 79 | `nip79Labels/` | Label events | Labeling (kinds 1985, 1986) |
|
||||
| 80-88 | Various | Various protocols | Relationship, preferences, polls, surveys, social graphs, etc. |
|
||||
| 91 | `nip91Feed/` | Feed display events | Feed definitions |
|
||||
| 93 | `nip93Gallery/` | Gallery events | Gallery collections |
|
||||
| 95 | `nip95Storage/` | Storage event tags | Storage events |
|
||||
| 97 | `nip97Nests/` | Audio rooms | Audio room events |
|
||||
|
||||
## Experimental NIPs (18 packages)
|
||||
|
||||
Located at `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/`:
|
||||
|
||||
| Package | Description |
|
||||
|---------|-------------|
|
||||
| `audio/` | Audio content, track events |
|
||||
| `bounties/` | Bounty/funding events |
|
||||
| `decoupling/` | Decoupling setup |
|
||||
| `edits/` | Event edit tracking |
|
||||
| `ephemChat/` | Ephemeral encrypted chat |
|
||||
| `forks/` | Fork tracking |
|
||||
| `inlineMetadata/` | Inline metadata |
|
||||
| `interactiveStories/` | Interactive story events |
|
||||
| `limits/` | Limit enforcement |
|
||||
| `medical/` | Medical data |
|
||||
| `nip95/` | File storage support |
|
||||
| `nipA3/` | A3 protocol extension |
|
||||
| `nns/` | Nostr Name System |
|
||||
| `profileGallery/` | Profile gallery lists |
|
||||
| `publicMessages/` | Public message lists |
|
||||
| `relationshipStatus/` | Relationship status events |
|
||||
| `trustedAssertions/` | Trust/assertion events |
|
||||
| `zapPolls/` | Zap-based polling |
|
||||
|
||||
## Quick Lookup by Kind
|
||||
|
||||
| Kind | Event Type | NIP |
|
||||
|------|------------|-----|
|
||||
| 0 | Metadata | 01 |
|
||||
| 1 | Text Note | 01, 10 |
|
||||
| 3 | Follow List | 02 |
|
||||
| 4 | Encrypted DM (legacy) | 04 |
|
||||
| 5 | Deletion | 09 |
|
||||
| 6 | Repost | 18 |
|
||||
| 7 | Reaction | 25 |
|
||||
| 8 | Badge Award | 58 |
|
||||
| 16 | Generic Repost | 18 |
|
||||
| 40-44 | Channel Events | 28 |
|
||||
| 1063 | File Metadata | 94 |
|
||||
| 1111 | Comment | 22 |
|
||||
| 1617, 1621, 1622, 1630, 1633 | Git | 34 |
|
||||
| 1984 | Report | 56 |
|
||||
| 1985, 1986 | Label | 79 |
|
||||
| 9734 | Zap Request | 57 |
|
||||
| 9735 | Zap Receipt | 57 |
|
||||
| 9802 | Highlight | 84 |
|
||||
| 10000-20000 | Replaceable Lists | 51 |
|
||||
| 10002 | Relay List | 65 |
|
||||
| 13194 | Wallet Info | 60 |
|
||||
| 22242 | Relay Auth | 42 |
|
||||
| 23194, 23195 | NWC Payment | 47 |
|
||||
| 30000-40000 | Addressable Events | Various |
|
||||
| 30009 | Badge Definition | 58 |
|
||||
| 30023 | Long-Form Content | 23 |
|
||||
| 30078 | App Data | 78 |
|
||||
| 30311 | Live Event | 53 |
|
||||
| 30315 | User Status | 38 |
|
||||
| 30402 | Classifieds | 99 |
|
||||
| 30818 | Wiki | 54 |
|
||||
| 31234 | Draft | 37 |
|
||||
| 31922-31925 | Calendar | 52 |
|
||||
| 31989, 31990 | App Handlers | 89 |
|
||||
| 34235, 34550-34551 | Video/Communities | 71, 72 |
|
||||
| 5000-7000 | DVM Jobs | 90 |
|
||||
|
||||
## File Location Pattern
|
||||
|
||||
All NIPs located at: `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip<NN><Name>/`
|
||||
|
||||
Example: NIP-57 → `/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/nip57Zaps/`
|
||||
@@ -0,0 +1,336 @@
|
||||
# Tag Patterns in Quartz
|
||||
|
||||
Tags are the primary way events reference other events, users, and metadata in Nostr.
|
||||
|
||||
## Tag Structure
|
||||
|
||||
```kotlin
|
||||
typealias Tag = Array<String> // ["tag_name", "value", "optional_param", ...]
|
||||
typealias TagArray = Array<Tag>
|
||||
```
|
||||
|
||||
**Pattern**: `[name, value, ...optionalParams]`
|
||||
|
||||
## TagArrayBuilder DSL
|
||||
|
||||
```kotlin
|
||||
fun tagArray(initializer: TagArrayBuilder<T>.() -> Unit): TagArray
|
||||
```
|
||||
|
||||
**Methods**:
|
||||
- `add(tag)` - Append tag
|
||||
- `addFirst(tag)` - Prepend tag
|
||||
- `addUnique(tag)` - Replace all tags with this name
|
||||
- `remove(tagName)` - Remove all tags with name
|
||||
- `removeIf(predicate, toCompare)` - Conditional removal
|
||||
|
||||
**Example**:
|
||||
```kotlin
|
||||
val tags = tagArray<TextNoteEvent> {
|
||||
add(arrayOf("e", eventId, relayHint, "reply"))
|
||||
add(arrayOf("p", pubkey))
|
||||
addUnique(arrayOf("subject", "Hello"))
|
||||
}
|
||||
```
|
||||
|
||||
## Core Tag Types (NIP-01)
|
||||
|
||||
### e-tag (Event Reference)
|
||||
```kotlin
|
||||
// ["e", <event-id>, <relay-hint>, <marker>]
|
||||
arrayOf("e", eventId, "wss://relay.damus.io", "reply")
|
||||
```
|
||||
|
||||
**Markers** (NIP-10):
|
||||
- `root` - Root of thread
|
||||
- `reply` - Direct reply target
|
||||
- `mention` - Mentioned event (not reply)
|
||||
|
||||
**Extensions**:
|
||||
```kotlin
|
||||
// nip01Core/tags/
|
||||
fun TagArrayBuilder.eTag(eventId: HexKey, relay: String? = null, marker: String? = null)
|
||||
```
|
||||
|
||||
### p-tag (Pubkey Reference)
|
||||
```kotlin
|
||||
// ["p", <pubkey>, <relay-hint>]
|
||||
arrayOf("p", pubkey, "wss://relay.damus.io")
|
||||
```
|
||||
|
||||
**Usage**: Tag users, indicate recipients
|
||||
|
||||
**Extensions**:
|
||||
```kotlin
|
||||
fun TagArrayBuilder.pTag(pubkey: HexKey, relay: String? = null)
|
||||
```
|
||||
|
||||
### a-tag (Addressable Event Reference)
|
||||
```kotlin
|
||||
// ["a", <kind>:<pubkey>:<d-tag>, <relay-hint>]
|
||||
arrayOf("a", "30023:${authorPubkey}:${dtag}", "wss://relay.damus.io")
|
||||
```
|
||||
|
||||
**Usage**: Reference replaceable/addressable events (kinds 10000-20000, 30000-40000)
|
||||
|
||||
**Extensions**:
|
||||
```kotlin
|
||||
fun TagArrayBuilder.aTag(kind: Int, pubkey: HexKey, dTag: String, relay: String? = null)
|
||||
```
|
||||
|
||||
### d-tag (Identifier)
|
||||
```kotlin
|
||||
// ["d", <identifier>]
|
||||
arrayOf("d", "my-article-slug")
|
||||
```
|
||||
|
||||
**Usage**: Unique identifier for addressable events
|
||||
|
||||
## Common Tag Extensions
|
||||
|
||||
### Subject (NIP-14)
|
||||
```kotlin
|
||||
// nip14Subject/
|
||||
fun Event.subject(): String?
|
||||
fun TagArrayBuilder.subject(text: String)
|
||||
```
|
||||
|
||||
### Content Warning (NIP-36)
|
||||
```kotlin
|
||||
// nip36SensitiveContent/
|
||||
fun Event.contentWarning(): String?
|
||||
fun TagArrayBuilder.contentWarning(reason: String = "")
|
||||
```
|
||||
|
||||
### Expiration (NIP-40)
|
||||
```kotlin
|
||||
// nip40Expiration/
|
||||
fun Event.expiration(): Long?
|
||||
fun TagArrayBuilder.expiration(unixTimestamp: Long)
|
||||
```
|
||||
|
||||
### Alt Description (NIP-31)
|
||||
```kotlin
|
||||
// nip31Alts/
|
||||
fun Event.alt(): String?
|
||||
fun TagArrayBuilder.alt(description: String)
|
||||
```
|
||||
|
||||
## Specialized Tags
|
||||
|
||||
### Zap Tags (NIP-57)
|
||||
```kotlin
|
||||
// nip57Zaps/tags/
|
||||
class BoltTag(val bolt11: String, val preimage: String?)
|
||||
class DescriptionTag(val zapRequestJson: String)
|
||||
```
|
||||
|
||||
### Imeta Tags (NIP-92)
|
||||
```kotlin
|
||||
// nip92IMeta/
|
||||
class IMetaTag(val url: String, val metadata: Map<String, String>)
|
||||
|
||||
// Usage: Image metadata
|
||||
IMetaTag("https://example.com/image.jpg", mapOf(
|
||||
"m" to "image/jpeg",
|
||||
"dim" to "1920x1080",
|
||||
"blurhash" to "..."
|
||||
))
|
||||
```
|
||||
|
||||
### Relay Tags (NIP-65)
|
||||
```kotlin
|
||||
// nip65RelayList/
|
||||
class RelayTag(val url: String, val type: RelayType)
|
||||
enum class RelayType { READ, WRITE, BOTH }
|
||||
```
|
||||
|
||||
## Tag Query Patterns
|
||||
|
||||
### Finding Tags
|
||||
```kotlin
|
||||
// Extension functions on TagArray
|
||||
fun TagArray.firstTag(name: String): Tag?
|
||||
fun TagArray.allTags(name: String): List<Tag>
|
||||
fun TagArray.tagValue(name: String): String?
|
||||
fun TagArray.tagValues(name: String): List<String>
|
||||
```
|
||||
|
||||
**Example**:
|
||||
```kotlin
|
||||
val event: TextNoteEvent = ...
|
||||
val subject = event.tags.tagValue("subject")
|
||||
val mentions = event.tags.allTags("p").mapNotNull { it.getOrNull(1) }
|
||||
```
|
||||
|
||||
### Parsing Tags
|
||||
```kotlin
|
||||
// Pattern: Companion object with parse methods
|
||||
object ETag {
|
||||
fun parse(tag: Tag): ETag? {
|
||||
if (tag.getOrNull(0) != "e") return null
|
||||
return ETag(
|
||||
eventId = tag.getOrNull(1) ?: return null,
|
||||
relay = tag.getOrNull(2),
|
||||
marker = tag.getOrNull(3)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
val eTags = event.tags.mapNotNull(ETag::parse)
|
||||
```
|
||||
|
||||
## Event Builder Pattern
|
||||
|
||||
Combining TagArrayBuilder with event creation:
|
||||
|
||||
```kotlin
|
||||
fun createTextNote(content: String, replyTo: Event?): EventTemplate {
|
||||
return eventTemplate(
|
||||
kind = 1,
|
||||
content = content,
|
||||
tags = tagArray {
|
||||
replyTo?.let {
|
||||
eTag(it.id, marker = "reply")
|
||||
pTag(it.pubKey)
|
||||
it.rootEvent()?.let { root ->
|
||||
eTag(root.id, marker = "root")
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Hint System
|
||||
|
||||
Tags can provide "hints" - optional relay URLs for fetching referenced content:
|
||||
|
||||
```kotlin
|
||||
// Event references
|
||||
["e", eventId, "wss://relay.example.com"] // relay hint
|
||||
|
||||
// Pubkey references
|
||||
["p", pubkey, "wss://relay.example.com"] // relay hint
|
||||
|
||||
// Addressable references
|
||||
["a", "30023:pubkey:dtag", "wss://relay.example.com"] // relay hint
|
||||
```
|
||||
|
||||
**Pattern**: Third parameter (index 2) is always the relay hint
|
||||
|
||||
## Tag Validation
|
||||
|
||||
```kotlin
|
||||
// Common validations
|
||||
fun validateETag(tag: Tag): Boolean {
|
||||
return tag.getOrNull(0) == "e" && tag.getOrNull(1)?.isValidHex() == true
|
||||
}
|
||||
|
||||
fun validatePTag(tag: Tag): Boolean {
|
||||
return tag.getOrNull(0) == "p" && tag.getOrNull(1)?.isValidHex() == true
|
||||
}
|
||||
```
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
### Tag Indexing
|
||||
```kotlin
|
||||
// TagArrayBuilder keeps an index by tag name
|
||||
private val tagList = mutableMapOf<String, MutableList<Tag>>()
|
||||
|
||||
// Fast lookup by name
|
||||
fun remove(tagName: String) {
|
||||
tagList.remove(tagName)
|
||||
}
|
||||
```
|
||||
|
||||
### Lazy Parsing
|
||||
```kotlin
|
||||
// Don't parse all tags upfront
|
||||
class TextNoteEvent(...) {
|
||||
private val _mentions by lazy {
|
||||
tags.mapNotNull(PTag::parse)
|
||||
}
|
||||
|
||||
fun mentions() = _mentions
|
||||
}
|
||||
```
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Creating a Reply
|
||||
```kotlin
|
||||
fun replyTo(original: TextNoteEvent, content: String): EventTemplate {
|
||||
return eventTemplate(
|
||||
kind = 1,
|
||||
content = content,
|
||||
tags = tagArray {
|
||||
// Reply to this event
|
||||
eTag(original.id, marker = "reply")
|
||||
|
||||
// Copy root marker if exists, or mark original as root
|
||||
original.rootEvent()?.let {
|
||||
eTag(it.id, marker = "root")
|
||||
} ?: eTag(original.id, marker = "root")
|
||||
|
||||
// Tag author
|
||||
pTag(original.pubKey)
|
||||
|
||||
// Tag all mentioned users
|
||||
original.mentions().forEach { pTag(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Creating a Reaction
|
||||
```kotlin
|
||||
fun createReaction(targetEvent: Event, emoji: String): EventTemplate {
|
||||
return eventTemplate(
|
||||
kind = 7,
|
||||
content = emoji,
|
||||
tags = tagArray {
|
||||
eTag(targetEvent.id)
|
||||
pTag(targetEvent.pubKey)
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### Creating an Addressable Event
|
||||
```kotlin
|
||||
fun createArticle(title: String, content: String, slug: String): EventTemplate {
|
||||
return eventTemplate(
|
||||
kind = 30023,
|
||||
content = content,
|
||||
tags = tagArray {
|
||||
addUnique(arrayOf("d", slug)) // Unique identifier
|
||||
add(arrayOf("title", title))
|
||||
add(arrayOf("published_at", "${TimeUtils.now()}"))
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Tag | NIP | Usage | Example |
|
||||
|-----|-----|-------|---------|
|
||||
| e | 01 | Event reference | `["e", eventId, relay, marker]` |
|
||||
| p | 01 | Pubkey reference | `["p", pubkey, relay]` |
|
||||
| a | 01 | Addressable event | `["a", "kind:pubkey:d"]` |
|
||||
| d | 01 | Identifier | `["d", "unique-id"]` |
|
||||
| subject | 14 | Subject line | `["subject", "Hello"]` |
|
||||
| content-warning | 36 | Content warning | `["content-warning", "nsfw"]` |
|
||||
| expiration | 40 | Expiration time | `["expiration", "1234567890"]` |
|
||||
| bolt11 | 57 | Lightning invoice | `["bolt11", "lnbc..."]` |
|
||||
| imeta | 92 | Media metadata | `["imeta", "url", "m", "image/jpeg"]` |
|
||||
| relay | 65 | User relays | `["relay", "wss://...", "read"]` |
|
||||
|
||||
## Resources
|
||||
|
||||
- Tag builders: `quartz/src/commonMain/.../nip01Core/tags/`
|
||||
- Tag extensions: Look for `TagArrayExt.kt`, `TagArrayBuilderExt.kt` in each NIP package
|
||||
- Event parsing: Each event class has tag parsing methods
|
||||
Reference in New Issue
Block a user