Current section
Files
Jump to
Current section
Files
lib/nostr/event/parser.ex
defmodule Nostr.Event.Parser do
@moduledoc """
Internal module for parsing raw event data into `Nostr.Event` structs.
This module handles two types of parsing:
- `parse/1` - Converts raw JSON maps to generic `Nostr.Event` structs
- `parse_specific/1` - Converts `Nostr.Event` to type-specific structs based on event kind
## Event Kind Routing
Specific event kinds are routed to their dedicated modules:
- Kind 0: `Nostr.Event.Metadata` (NIP-01)
- Kind 1: `Nostr.Event.Note` (NIP-01)
- Kind 2: `Nostr.Event.RecommendRelay` (NIP-01)
- Kind 3: `Nostr.Event.Contacts` (NIP-02)
- Kind 4: `Nostr.Event.DirectMessage` (NIP-04)
- Kind 5: `Nostr.Event.Deletion` (NIP-09)
- Kind 6: `Nostr.Event.Repost` (NIP-18)
- Kind 7: `Nostr.Event.Reaction` (NIP-25)
- Kind 8: `Nostr.Event.BadgeAward` (NIP-58)
- Kind 13: `Nostr.Event.Seal` (NIP-59)
- Kind 14: `Nostr.Event.PrivateMessage` (NIP-17)
- Kind 15: `Nostr.Event.FileMessage` (NIP-17)
- Kind 17: `Nostr.Event.ExternalReaction` (NIP-25)
- Kinds 40-44: Channel operations (NIP-28)
- Kind 9734: `Nostr.Event.ZapRequest` (NIP-57)
- Kind 9735: `Nostr.Event.ZapReceipt` (NIP-57)
- Kind 1111: `Nostr.Event.Comment` (NIP-22)
- Kind 1040: `Nostr.Event.OpenTimestamps` (NIP-03)
- Kind 1059: `Nostr.Event.GiftWrap` (NIP-59)
- Kind 1063: `Nostr.Event.FileMetadata`
- Kind 1984: `Nostr.Event.Report`
- Kind 1985: `Nostr.Event.Label` (NIP-32)
- Kind 10000: `Nostr.Event.ListMute` (NIP-51)
- Kind 10001: `Nostr.Event.PinnedNotes` (NIP-51)
- Kind 10002: `Nostr.Event.RelayList` (NIP-51/NIP-65)
- Kind 10003: `Nostr.Event.Bookmarks` (NIP-51)
- Kind 10004: `Nostr.Event.Communities` (NIP-51)
- Kind 10005: `Nostr.Event.PublicChats` (NIP-51)
- Kind 10006: `Nostr.Event.BlockedRelays` (NIP-51)
- Kind 10007: `Nostr.Event.SearchRelays` (NIP-51)
- Kind 10009: `Nostr.Event.SimpleGroups` (NIP-51)
- Kind 10012: `Nostr.Event.RelayFeeds` (NIP-51)
- Kind 10013: `Nostr.Event.PrivateContentRelayList` (NIP-37)
- Kind 10015: `Nostr.Event.Interests` (NIP-51)
- Kind 10020: `Nostr.Event.MediaFollows` (NIP-51)
- Kind 10030: `Nostr.Event.EmojiList` (NIP-51)
- Kind 10050: `Nostr.Event.DMRelayList` (NIP-17)
- Kind 10101: `Nostr.Event.GoodWikiAuthors` (NIP-51)
- Kind 10102: `Nostr.Event.GoodWikiRelays` (NIP-51)
- Kind 22242: `Nostr.Event.ClientAuth` (NIP-42)
- Kind 30000: `Nostr.Event.FollowSets` (NIP-51)
- Kind 30002: `Nostr.Event.RelaySets` (NIP-51)
- Kind 30003: `Nostr.Event.BookmarkSets` (NIP-51)
- Kinds 30004-30006: `Nostr.Event.CurationSets` (NIP-51)
- Kind 30007: `Nostr.Event.KindMuteSets` (NIP-51)
- Kind 30015: `Nostr.Event.InterestSets` (NIP-51)
- Kind 30023: `Nostr.Event.Article` (NIP-23)
- Kind 30024: `Nostr.Event.Article` (NIP-23 draft)
- Kind 30030: `Nostr.Event.EmojiSets` (NIP-51)
- Kind 30063: `Nostr.Event.ReleaseArtifactSets` (NIP-51)
- Kind 30267: `Nostr.Event.AppCurationSets` (NIP-51)
- Kind 31234: `Nostr.Event.DraftWrap` (NIP-37)
- Kind 31924: `Nostr.Event.Calendar` (NIP-51)
- Kind 39089: `Nostr.Event.StarterPacks` (NIP-51)
- Kind 39092: `Nostr.Event.MediaStarterPacks` (NIP-51)
Event kind ranges (NIP-16):
- 1000-9999: `Nostr.Event.Regular`
- 10000-19999: `Nostr.Event.Replaceable`
- 20000-29999: `Nostr.Event.Ephemeral`
- 30000-39999: `Nostr.Event.ParameterizedReplaceable`
Unknown kinds fall back to `Nostr.Event.Unknown`.
"""
alias Nostr.Event
@doc """
Parses a JSON string or map into a `Nostr.Event` struct.
Converts Unix timestamps to `DateTime` and parses tags into `Nostr.Tag` structs.
"""
@spec parse(String.t() | map()) :: Nostr.Event.t() | {:error, String.t(), term()}
def parse(event) when is_binary(event) do
case JSON.decode!(event) do
{:ok, event} -> parse(event)
{:error, %JSON.DecodeError{}} -> {:error, "Cannot decode event", event}
end
end
def parse(event) when is_map(event) do
%Nostr.Event{
id: event["id"],
pubkey: event["pubkey"],
kind: event["kind"],
tags: Enum.map(event["tags"], &Nostr.Tag.parse/1),
created_at: DateTime.from_unix!(event["created_at"]),
content: event["content"],
sig: event["sig"]
}
end
@doc """
Converts a generic `Nostr.Event` to a type-specific struct based on event kind.
Returns a specialized struct (e.g., `Nostr.Event.Note`, `Nostr.Event.Metadata`)
that may contain parsed content and additional type-specific fields.
"""
@spec parse_specific(Nostr.Event.t()) :: struct()
# Basic event kinds (0-17)
def parse_specific(%Event{kind: 0} = event), do: Event.Metadata.parse(event)
def parse_specific(%Event{kind: 1} = event), do: Event.Note.parse(event)
def parse_specific(%Event{kind: 2} = event), do: Event.RecommendRelay.parse(event)
def parse_specific(%Event{kind: 3} = event), do: Event.Contacts.parse(event)
def parse_specific(%Event{kind: 4} = event), do: Event.DirectMessage.parse(event)
def parse_specific(%Event{kind: 5} = event), do: Event.Deletion.parse(event)
def parse_specific(%Event{kind: 6} = event), do: Event.Repost.parse(event)
def parse_specific(%Event{kind: 7} = event), do: Event.Reaction.parse(event)
def parse_specific(%Event{kind: 8} = event), do: Event.BadgeAward.parse(event)
def parse_specific(%Event{kind: 13} = event), do: Event.Seal.parse(event)
def parse_specific(%Event{kind: 14} = event), do: Event.PrivateMessage.parse(event)
def parse_specific(%Event{kind: 15} = event), do: Event.FileMessage.parse(event)
def parse_specific(%Event{kind: 17} = event), do: Event.ExternalReaction.parse(event)
# Channel operations (40-44, NIP-28)
def parse_specific(%Event{kind: 40} = event), do: Event.ChannelCreation.parse(event)
def parse_specific(%Event{kind: 41} = event), do: Event.ChannelMetadata.parse(event)
def parse_specific(%Event{kind: 42} = event), do: Event.ChannelMessage.parse(event)
def parse_specific(%Event{kind: 43} = event), do: Event.ChannelHideMessage.parse(event)
def parse_specific(%Event{kind: 44} = event), do: Event.ChannelMuteUser.parse(event)
# NIP-57 Zaps (9734-9735)
def parse_specific(%Event{kind: 9734} = event), do: Event.ZapRequest.parse(event)
def parse_specific(%Event{kind: 9735} = event), do: Event.ZapReceipt.parse(event)
# Regular events (1000-9999)
def parse_specific(%Event{kind: 1111} = event), do: Event.Comment.parse(event)
def parse_specific(%Event{kind: 1040} = event), do: Event.OpenTimestamps.parse(event)
def parse_specific(%Event{kind: 1059} = event), do: Event.GiftWrap.parse(event)
def parse_specific(%Event{kind: 1063} = event), do: Event.FileMetadata.parse(event)
def parse_specific(%Event{kind: 1984} = event), do: Event.Report.parse(event)
def parse_specific(%Event{kind: 1985} = event), do: Event.Label.parse(event)
# NIP-51 Standard Lists (10000-10102)
def parse_specific(%Event{kind: 10_000} = event), do: Event.ListMute.parse(event)
def parse_specific(%Event{kind: 10_001} = event), do: Event.PinnedNotes.parse(event)
def parse_specific(%Event{kind: 10_002} = event), do: Event.RelayList.parse(event)
def parse_specific(%Event{kind: 10_003} = event), do: Event.Bookmarks.parse(event)
def parse_specific(%Event{kind: 10_004} = event), do: Event.Communities.parse(event)
def parse_specific(%Event{kind: 10_005} = event), do: Event.PublicChats.parse(event)
def parse_specific(%Event{kind: 10_006} = event), do: Event.BlockedRelays.parse(event)
def parse_specific(%Event{kind: 10_007} = event), do: Event.SearchRelays.parse(event)
def parse_specific(%Event{kind: 10_009} = event), do: Event.SimpleGroups.parse(event)
def parse_specific(%Event{kind: 10_012} = event), do: Event.RelayFeeds.parse(event)
def parse_specific(%Event{kind: 10_015} = event), do: Event.Interests.parse(event)
def parse_specific(%Event{kind: 10_020} = event), do: Event.MediaFollows.parse(event)
def parse_specific(%Event{kind: 10_030} = event), do: Event.EmojiList.parse(event)
def parse_specific(%Event{kind: 10_013} = event), do: Event.PrivateContentRelayList.parse(event)
def parse_specific(%Event{kind: 10_050} = event), do: Event.DMRelayList.parse(event)
def parse_specific(%Event{kind: 10_101} = event), do: Event.GoodWikiAuthors.parse(event)
def parse_specific(%Event{kind: 10_102} = event), do: Event.GoodWikiRelays.parse(event)
# Other replaceable events
def parse_specific(%Event{kind: 22_242} = event), do: Event.ClientAuth.parse(event)
# NIP-51 Parameterized Sets (30000-39999)
def parse_specific(%Event{kind: 30_000} = event), do: Event.FollowSets.parse(event)
def parse_specific(%Event{kind: 30_002} = event), do: Event.RelaySets.parse(event)
def parse_specific(%Event{kind: 30_003} = event), do: Event.BookmarkSets.parse(event)
def parse_specific(%Event{kind: 30_004} = event), do: Event.CurationSets.parse(event)
def parse_specific(%Event{kind: 30_005} = event), do: Event.CurationSets.parse(event)
def parse_specific(%Event{kind: 30_006} = event), do: Event.CurationSets.parse(event)
def parse_specific(%Event{kind: 30_007} = event), do: Event.KindMuteSets.parse(event)
def parse_specific(%Event{kind: 30_015} = event), do: Event.InterestSets.parse(event)
def parse_specific(%Event{kind: 30_023} = event), do: Event.Article.parse(event)
def parse_specific(%Event{kind: 30_024} = event), do: Event.Article.parse(event)
def parse_specific(%Event{kind: 30_030} = event), do: Event.EmojiSets.parse(event)
def parse_specific(%Event{kind: 30_063} = event), do: Event.ReleaseArtifactSets.parse(event)
def parse_specific(%Event{kind: 30_267} = event), do: Event.AppCurationSets.parse(event)
def parse_specific(%Event{kind: 30_315} = event), do: Event.UserStatus.parse(event)
def parse_specific(%Event{kind: 31_234} = event), do: Event.DraftWrap.parse(event)
def parse_specific(%Event{kind: 31_924} = event), do: Event.Calendar.parse(event)
def parse_specific(%Event{kind: 39_089} = event), do: Event.StarterPacks.parse(event)
def parse_specific(%Event{kind: 39_092} = event), do: Event.MediaStarterPacks.parse(event)
# Fallback ranges (NIP-16)
def parse_specific(%Event{kind: kind} = event) when kind >= 1000 and kind < 10_000 do
Event.Regular.parse(event)
end
def parse_specific(%Event{kind: kind} = event) when kind >= 10_000 and kind < 20_000 do
Event.Replaceable.parse(event)
end
def parse_specific(%Event{kind: kind} = event) when kind >= 20_000 and kind < 30_000 do
Event.Ephemeral.parse(event)
end
def parse_specific(%Event{kind: kind} = event) when kind >= 30_000 and kind < 40_000 do
Event.ParameterizedReplaceable.parse(event)
end
def parse_specific(%Nostr.Event{} = event) do
%Event.Unknown{event: event}
end
end