use core::cmp::Ordering;
use core::fmt;
use core::hash::{Hash, Hasher};
use core::num::ParseIntError;
use core::ops::{Add, Range};
use core::str::FromStr;
use serde::de::{Deserialize, Deserializer};
use serde::ser::{Serialize, Serializer};
pub const NIP90_JOB_REQUEST_RANGE: Range<u16> = 5_000..6_000;
pub const NIP90_JOB_RESULT_RANGE: Range<u16> = 6_000..7_000;
pub const REGULAR_RANGE: Range<u16> = 1_000..10_000;
pub const REPLACEABLE_RANGE: Range<u16> = 10_000..20_000;
pub const EPHEMERAL_RANGE: Range<u16> = 20_000..30_000;
pub const ADDRESSABLE_RANGE: Range<u16> = 30_000..40_000;
macro_rules! kind_variants {
($($name:ident => $value:expr, $doc0:expr, $doc1:expr),* $(,)?) => {
#[derive(Debug, Clone, Copy)]
pub enum Kind {
$(
#[doc = concat!($doc0, " (kind: ", stringify!($value), ")")]
#[doc = ""]
#[doc = $doc1]
$name,
)*
Custom(u16),
}
impl Kind {
pub const fn from_u16(kind: u16) -> Self {
match kind {
$(
$value => Self::$name,
)*
x => Self::Custom(x),
}
}
#[inline]
pub const fn as_u16(&self) -> u16 {
match self {
$(
Self::$name => $value,
)*
Self::Custom(u) => *u,
}
}
}
};
}
kind_variants! {
Metadata => 0, "Metadata", "<https://github.com/nostr-protocol/nips/blob/master/01.md> and <https://github.com/nostr-protocol/nips/blob/master/05.md>",
TextNote => 1, "Short Text Note", "<https://github.com/nostr-protocol/nips/blob/master/32.md>",
RecommendRelay => 2, "Recommend Relay (deprecated)", "",
ContactList => 3, "Contacts", "<https://github.com/nostr-protocol/nips/blob/master/02.md>",
OpenTimestamps => 1040, "OpenTimestamps Attestations", "<https://github.com/nostr-protocol/nips/blob/master/03.md>",
EncryptedDirectMessage => 4, "Encrypted Direct Messages", "<https://github.com/nostr-protocol/nips/blob/master/04.md>",
EventDeletion => 5, "Event Deletion", "<https://github.com/nostr-protocol/nips/blob/master/09.md>",
Repost => 6, "Repos", "<https://github.com/nostr-protocol/nips/blob/master/18.md>",
GenericRepost => 16, "Generic Repos", "<https://github.com/nostr-protocol/nips/blob/master/18.md>",
Comment => 1111, "Comment", "<https://github.com/nostr-protocol/nips/blob/master/22.md>",
Reaction => 7, "Reaction", "<https://github.com/nostr-protocol/nips/blob/master/25.md>",
BadgeAward => 8, "Badge Award", "<https://github.com/nostr-protocol/nips/blob/master/58.md>",
ChannelCreation => 40, "Channel Creation", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
ChannelMetadata => 41, "Channel Metadata", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
ChannelMessage => 42, "Channel Message", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
ChannelHideMessage => 43, "Channel Hide Message", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
ChannelMuteUser => 44, "Channel Mute User", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
PublicChatReserved45 => 45, "Public Chat Reserved", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
PublicChatReserved46 => 46, "Public Chat Reserved", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
PublicChatReserved47 => 47, "Public Chat Reserved", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
PublicChatReserved48 => 48, "Public Chat Reserved", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
PublicChatReserved49 => 49, "Public Chat Reserved", "<https://github.com/nostr-protocol/nips/blob/master/28.md>",
MlsKeyPackage => 443, "MLS Key Package", "<https://github.com/nostr-protocol/nips/blob/master/104.md>",
MlsWelcome => 444, "MLS Welcome", "<https://github.com/nostr-protocol/nips/blob/master/104.md>",
MlsGroupMessage => 445, "MLS Group Message", "<https://github.com/nostr-protocol/nips/blob/master/104.md>",
RepoState => 30618, "Repository state announcements", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitPatch => 1617, "Git Patch", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitPullRequest => 1618, "Git Pull Request", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitPullRequestUpdate => 1619, "Git Pull Request Update", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitIssue => 1621, "Git Issue", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitReply => 1622, "Git Reply", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitStatusOpen => 1630, "Open Status of Git Patch or Issue", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitStatusApplied => 1631, "Applied / Merged Status of Git Patch or Resolved Status of Git Issue", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitStatusClosed => 1632, "Closed Status of Git Patch or Issue", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
GitStatusDraft => 1633, "Draft Status of Git Patch or Issue", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
WalletConnectInfo => 13194, "Wallet Service Info", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
Reporting => 1984, "Reporting", "<https://github.com/nostr-protocol/nips/blob/master/56.md>",
Label => 1985, "Label", "<https://github.com/nostr-protocol/nips/blob/master/32.md>",
ZapPrivateMessage => 9733, "Zap Private Message", "<https://github.com/nostr-protocol/nips/blob/master/57.md>",
ZapRequest => 9734, "Zap Request", "<https://github.com/nostr-protocol/nips/blob/master/57.md>",
ZapReceipt => 9735, "Zap Receipt", "<https://github.com/nostr-protocol/nips/blob/master/57.md>",
Highlight => 9802, "Highlights", "<https://github.com/nostr-protocol/nips/blob/master/84.md>",
MuteList => 10000, "Mute List", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
PinList => 10001, "Pin List", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
RelayList => 10002, "Relay List Metadata", "<https://github.com/nostr-protocol/nips/blob/master/65.md>",
Bookmarks => 10003, "Bookmarks", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
Communities => 10004, "Communities", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
PublicChats => 10005, "Public Chats", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
BlockedRelays => 10006, "Blocked Relays", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
SearchRelays => 10007, "Search Relays", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
SimpleGroups => 10009, "Simple Groups", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
Interests => 10015, "Interests", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
Emojis => 10030, "Emojis", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
InboxRelays => 10050, "Inbox Relays", "<https://github.com/nostr-protocol/nips/blob/master/17.md>",
MlsKeyPackageRelays => 10051, "MLS Key Package Relays", "<https://github.com/nostr-protocol/nips/blob/master/104.md>",
BlossomServerList => 10063, "Blossom Server List", "<https://github.com/nostr-protocol/nips/blob/master/B7.md>",
Authentication => 22242, "Client Authentication", "<https://github.com/nostr-protocol/nips/blob/master/42.md>",
WalletConnectRequest => 23194, "Wallet Connect Request", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
WalletConnectResponse => 23195, "Wallet Connect Response", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
WalletConnectNotification => 23196, "Wallet Connect Notification", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
NostrConnect => 24133, "Nostr Connect", "<https://github.com/nostr-protocol/nips/blob/master/47.md>",
LiveEvent => 30311, "Live Event", "<https://github.com/nostr-protocol/nips/blob/master/53.md>",
LiveEventMessage => 1311, "Live Event Message", "<https://github.com/nostr-protocol/nips/blob/master/53.md>",
ProfileBadges => 10008, "Profile Badges", "<https://github.com/nostr-protocol/nips/blob/master/58.md>",
BadgeSet => 30008, "Badge Set", "<https://github.com/nostr-protocol/nips/blob/master/58.md>",
BadgeDefinition => 30009, "Badge Definition", "<https://github.com/nostr-protocol/nips/blob/master/58.md>",
Seal => 13, "Seal", "<https://github.com/nostr-protocol/nips/blob/master/59.md>",
GiftWrap => 1059, "Gift Wrap", "<https://github.com/nostr-protocol/nips/blob/master/59.md>",
PrivateDirectMessage => 14, "Private Direct message", "<https://github.com/nostr-protocol/nips/blob/master/17.md>",
SetStall => 30017, "Set stall", "<https://github.com/nostr-protocol/nips/blob/master/15.md>",
SetProduct => 30018, "Set product", "<https://github.com/nostr-protocol/nips/blob/master/15.md>",
JobFeedback => 7000, "Job Feedback", "<https://github.com/nostr-protocol/nips/blob/master/90.md>",
FollowSet => 30000, "Follow Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
RelaySet => 30002, "Relay Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
BookmarkSet => 30003, "Bookmark Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
ArticlesCurationSet => 30004, "Articles Curation Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
VideosCurationSet => 30005, "Videos Curation Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
InterestSet => 30015, "Interest Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
EmojiSet => 30030, "Emoji Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
ReleaseArtifactSet => 30063, "Release Artifact Set", "<https://github.com/nostr-protocol/nips/blob/master/51.md>",
LongFormTextNote => 30023, "Long-form Text Note", "<https://github.com/nostr-protocol/nips/blob/master/23.md>",
GitRepoAnnouncement => 30617, "Git Repository Announcement", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
FileMetadata => 1063, "File Metadata", "<https://github.com/nostr-protocol/nips/blob/master/94.md>",
BlossomAuth => 24242, "Blossom Authorization", "<https://github.com/hzrd149/blossom/blob/master/buds/01.md>",
HttpAuth => 27235, "HTTP Auth", "<https://github.com/nostr-protocol/nips/blob/master/98.md>",
ApplicationSpecificData => 30078, "Application-specific Data", "<https://github.com/nostr-protocol/nips/blob/master/78.md>",
Torrent => 2003, "Torrent", "<https://github.com/nostr-protocol/nips/blob/master/35.md>",
TorrentComment => 2004, "Torrent Comment", "<https://github.com/nostr-protocol/nips/blob/master/35.md>",
PeerToPeerOrder => 38383, "Peer-to-peer Order events", "<https://github.com/nostr-protocol/nips/blob/master/69.md>",
RequestToVanish => 62, "Request to Vanish", "<https://github.com/nostr-protocol/nips/blob/master/62.md>",
UserStatus => 30315, "User Status", "<https://github.com/nostr-protocol/nips/blob/master/38.md>",
VoiceMessage => 1222, "Voice Message", "<https://github.com/nostr-protocol/nips/blob/master/A0.md>",
VoiceMessageReply => 1244, "Voice Message Reply", "<https://github.com/nostr-protocol/nips/blob/master/A0.md>",
CashuWallet => 17375, "Cashu Wallet", "<https://github.com/nostr-protocol/nips/blob/master/60.md>",
CashuWalletUnspentProof => 7375, "Cashu Wallet Unspent Proof", "<https://github.com/nostr-protocol/nips/blob/master/60.md>",
CashuWalletSpendingHistory => 7376, "Cashu Wallet Spending History", "<https://github.com/nostr-protocol/nips/blob/master/60.md>",
CashuWalletQuote => 7374, "Cashu Wallet Redeeming a quote", "<https://github.com/nostr-protocol/nips/blob/master/60.md>",
CashuNutZapInfo => 10019, "Cashu Nut Zap informational event", "<https://github.com/nostr-protocol/nips/blob/master/61.md>",
GitUserGraspList => 10317, "User Grasp List", "<https://github.com/nostr-protocol/nips/blob/master/34.md>",
CashuNutZap => 9321, "Cashu Nut Zap", "<https://github.com/nostr-protocol/nips/blob/master/61.md>",
CodeSnippet => 1337, "Code Snippets", "<https://github.com/nostr-protocol/nips/blob/master/C0.md>",
Poll => 1068, "Poll", "<https://github.com/nostr-protocol/nips/blob/master/88.md>",
PollResponse => 1018, "Poll response", "<https://github.com/nostr-protocol/nips/blob/master/88.md>",
ChatMessage => 9, "Chat Message", "<https://github.com/nostr-protocol/nips/blob/master/C7.md>",
Thread => 11, "Thread", "<https://github.com/nostr-protocol/nips/blob/master/7D.md>",
WebBookmark => 39701, "Web Bookmark", "<https://github.com/nostr-protocol/nips/blob/master/B0.md>",
RelayMonitor => 10166, "Relay Monitor", "<https://github.com/nostr-protocol/nips/blob/master/66.md>",
RelayDiscovery => 30166, "Relay Discovery", "<https://github.com/nostr-protocol/nips/blob/master/66.md>",
}
impl PartialEq for Kind {
fn eq(&self, other: &Kind) -> bool {
self.as_u16() == other.as_u16()
}
}
impl Eq for Kind {}
impl PartialOrd for Kind {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Kind {
fn cmp(&self, other: &Self) -> Ordering {
self.as_u16().cmp(&other.as_u16())
}
}
impl Hash for Kind {
fn hash<H>(&self, state: &mut H)
where
H: Hasher,
{
self.as_u16().hash(state);
}
}
impl Kind {
pub fn is_regular(&self) -> bool {
let kind: u16 = self.as_u16();
if kind > 10_000 {
return false;
}
REGULAR_RANGE.contains(&kind) || !self.is_replaceable()
}
#[inline]
pub fn is_replaceable(&self) -> bool {
matches!(self, Kind::Metadata)
|| matches!(self, Kind::ContactList)
|| matches!(self, Kind::ChannelMetadata)
|| REPLACEABLE_RANGE.contains(&self.as_u16())
}
#[inline]
pub fn is_ephemeral(&self) -> bool {
EPHEMERAL_RANGE.contains(&self.as_u16())
}
#[inline]
pub fn is_addressable(&self) -> bool {
ADDRESSABLE_RANGE.contains(&self.as_u16())
}
#[inline]
pub fn is_job_request(&self) -> bool {
NIP90_JOB_REQUEST_RANGE.contains(&self.as_u16())
}
#[inline]
pub fn is_job_result(&self) -> bool {
NIP90_JOB_RESULT_RANGE.contains(&self.as_u16())
}
}
impl fmt::Display for Kind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_u16())
}
}
impl FromStr for Kind {
type Err = ParseIntError;
fn from_str(kind: &str) -> Result<Self, Self::Err> {
let kind: u16 = kind.parse()?;
Ok(Self::from_u16(kind))
}
}
impl From<u16> for Kind {
fn from(kind: u16) -> Self {
Self::from_u16(kind)
}
}
impl From<Kind> for u16 {
fn from(kind: Kind) -> u16 {
kind.as_u16()
}
}
impl Add<u16> for Kind {
type Output = Self;
fn add(self, rhs: u16) -> Self::Output {
let kind: u16 = self.as_u16();
let sum: u16 = kind.saturating_add(rhs);
Kind::from_u16(sum)
}
}
impl Serialize for Kind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u16(self.as_u16())
}
}
impl<'de> Deserialize<'de> for Kind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let kind: u16 = Deserialize::deserialize(deserializer)?;
Ok(Self::from_u16(kind))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Serialize, Deserialize)]
struct TestKind {
kind: Kind,
}
#[test]
fn test_equal_kind() {
assert_eq!(Kind::Custom(20100), Kind::from_u16(20100));
assert_eq!(Kind::TextNote, Kind::Custom(1));
assert_eq!(Kind::Custom(30017), Kind::SetStall);
assert_eq!(Kind::Custom(30018), Kind::SetProduct);
}
#[test]
fn test_not_equal_kind() {
assert_ne!(Kind::Custom(20100), Kind::Custom(2000));
assert_ne!(Kind::Authentication, Kind::EncryptedDirectMessage);
assert_ne!(Kind::TextNote, Kind::Custom(2));
}
#[test]
fn test_kind_is_addressable() {
assert!(Kind::Custom(32122).is_addressable());
assert!(!Kind::TextNote.is_addressable());
}
#[test]
fn test_kind_from_str() {
assert_eq!(Kind::from_str("0").unwrap(), Kind::Metadata);
assert_eq!(Kind::from_str("1").unwrap(), Kind::TextNote);
assert_eq!(Kind::from_str("1621").unwrap(), Kind::Custom(1621));
assert_eq!(Kind::from_str("20100").unwrap(), Kind::Custom(20100));
}
#[test]
fn test_kind_deserialize() {
let json: &str = r#"{"kind": 1621}"#;
let TestKind { kind } = serde_json::from_str(json).unwrap();
assert_eq!(kind, Kind::Custom(1621));
}
#[test]
fn test_kind_serialize() {
let kind = TestKind {
kind: Kind::Metadata,
};
let json = serde_json::to_string(&kind).unwrap();
let expected_json: &str = r#"{"kind":0}"#;
assert_eq!(json, expected_json);
}
#[test]
fn test_kind_serialize_deserialize_round_trip() {
let kind = Kind::Custom(20100);
let json = serde_json::to_string(&kind).unwrap();
let deserialized_kind: Kind = serde_json::from_str(&json).unwrap();
assert_eq!(kind, deserialized_kind);
}
#[test]
fn test_kind_add() {
let kind = Kind::TextNote + 100;
assert_eq!(kind, Kind::Custom(101));
let kind = Kind::Custom(20100) + 100;
assert_eq!(kind, Kind::Custom(20200));
}
#[test]
fn test_kind_add_overflow() {
let kind = Kind::Custom(u16::MAX - 1) + 10;
assert_eq!(kind, Kind::Custom(u16::MAX));
}
}
#[cfg(bench)]
mod benches {
use test::{Bencher, black_box};
use super::*;
#[bench]
pub fn parse_ephemeral_kind(bh: &mut Bencher) {
bh.iter(|| {
black_box(Kind::from(29_999));
});
}
#[bench]
pub fn parse_kind(bh: &mut Bencher) {
bh.iter(|| {
black_box(Kind::from(0));
});
}
}