nostr 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Part of an +Event+. A complete +Event+ must have an +id+ and a +sig+.
5
+ class EventFragment
6
+ # 32-bytes hex-encoded public key of the event creator
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # event.pubkey # => '48df4af6e240ac5f7c5de89bf5941b249880be0e87d03685b178ccb1a315f52e'
12
+ #
13
+ # @return [String]
14
+ #
15
+ attr_reader :pubkey
16
+
17
+ # Date of the creation of the vent. A UNIX timestamp, in seconds
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # event.created_at # => 1230981305
23
+ #
24
+ # @return [Integer]
25
+ #
26
+ attr_reader :created_at
27
+
28
+ # The kind of the event. An integer from 0 to 3
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # event.kind # => 1
34
+ #
35
+ # @return [Integer]
36
+ #
37
+ attr_reader :kind
38
+
39
+ # An array of tags. Each tag is an array of strings
40
+ #
41
+ # @api public
42
+ #
43
+ # @example Tags referencing an event
44
+ # event.tags #=> [["e", "event_id", "relay URL"]]
45
+ #
46
+ # @example Tags referencing a key
47
+ # event.tags #=> [["p", "event_id", "relay URL"]]
48
+ #
49
+ # @return [Array<Array>]
50
+ #
51
+ attr_reader :tags
52
+
53
+ # An arbitrary string
54
+ #
55
+ # @api public
56
+ #
57
+ # @example
58
+ # event.content # => 'Your feedback is appreciated, now pay $8'
59
+ #
60
+ # @return [String]
61
+ #
62
+ attr_reader :content
63
+
64
+ # Instantiates a new EventFragment
65
+ #
66
+ # @api public
67
+ #
68
+ # @example
69
+ # Nostr::EventFragment.new(
70
+ # pubkey: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460',
71
+ # created_at: 1230981305,
72
+ # kind: 1,
73
+ # tags: [['e', '189df012cfff8a075785b884bd702025f4a7a37710f581c4ac9d33e24b585408']],
74
+ # content: 'Your feedback is appreciated, now pay $8'
75
+ # )
76
+ #
77
+ # @param pubkey [String] 32-bytes hex-encoded public key of the event creator.
78
+ # @param created_at [Integer] Date of the creation of the vent. A UNIX timestamp, in seconds.
79
+ # @param kind [Integer] The kind of the event. An integer from 0 to 3.
80
+ # @param tags [Array<Array>] An array of tags. Each tag is an array of strings.
81
+ # @param content [String] Arbitrary string.
82
+ #
83
+ def initialize(pubkey:, kind:, content:, created_at: Time.now.to_i, tags: [])
84
+ @pubkey = pubkey
85
+ @created_at = created_at
86
+ @kind = kind
87
+ @tags = tags
88
+ @content = content
89
+ end
90
+
91
+ # Serializes the event fragment, to obtain a SHA256 hash of it
92
+ #
93
+ # @api public
94
+ #
95
+ # @example Converting the event to a hash
96
+ # event_fragment.serialize
97
+ #
98
+ # @return [Array] The event fragment as an array.
99
+ #
100
+ def serialize
101
+ [
102
+ 0,
103
+ pubkey,
104
+ created_at,
105
+ kind,
106
+ tags,
107
+ content
108
+ ]
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Defines the event kinds that can be emitted by clients.
5
+ module EventKind
6
+ # The content is set to a stringified JSON object +{name: <username>, about: <string>,
7
+ # picture: <url, string>}+ describing the user who created the event. A relay may delete past set_metadata
8
+ # events once it gets a new one for the same pubkey.
9
+ #
10
+ # @return [Integer]
11
+ #
12
+ SET_METADATA = 0
13
+
14
+ # The content is set to the text content of a note (anything the user wants to say).
15
+ # Non-plaintext notes should instead use kind 1000-10000 as described in NIP-16.
16
+ #
17
+ # @return [Integer]
18
+ #
19
+ TEXT_NOTE = 1
20
+
21
+ # The content is set to the URL (e.g., wss://somerelay.com) of a relay the event creator wants to
22
+ # recommend to its followers.
23
+ #
24
+ # @return [Integer]
25
+ #
26
+ RECOMMEND_SERVER = 2
27
+
28
+ # A special event with kind 3, meaning "contact list" is defined as having a list of p tags, one for each of
29
+ # the followed/known profiles one is following.
30
+ #
31
+ # @return [Integer]
32
+ #
33
+ CONTACT_LIST = 3
34
+ end
35
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # A filter determines what events will be sent in a subscription.
5
+ class Filter
6
+ # A list of event ids or prefixes
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # filter.ids # => ['c24881c305c5cfb7c1168be7e9b0e150', '35deb2612efdb9e13e8b0ca4fc162341']
12
+ #
13
+ # @return [Array<String>, nil]
14
+ #
15
+ attr_reader :ids
16
+
17
+ # A list of pubkeys or prefixes, the pubkey of an event must be one of these
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # filter.authors # => ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7']
23
+ #
24
+ # @return [Array<String>, nil]
25
+ #
26
+ attr_reader :authors
27
+
28
+ # A list of a kind numbers
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # filter.kinds # => [0, 1, 2]
34
+ #
35
+ # @return [Array<Integer>, nil]
36
+ #
37
+ attr_reader :kinds
38
+
39
+ # A list of event ids that are referenced in an "e" tag
40
+ #
41
+ # @api public
42
+ #
43
+ # @example
44
+ # filter.e # => ['7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2']
45
+ #
46
+ # @return [Array<String>, nil]
47
+ #
48
+ attr_reader :e
49
+
50
+ # A list of pubkeys that are referenced in a "p" tag
51
+ #
52
+ # @api public
53
+ #
54
+ # @example
55
+ # filter.p # => ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7']
56
+ #
57
+ # @return [Array<String>, nil]
58
+ #
59
+ attr_reader :p
60
+
61
+ # A timestamp, events must be newer than this to pass
62
+ #
63
+ # @api public
64
+ #
65
+ # @example
66
+ # filter.since # => 1230981305
67
+ #
68
+ # @return [Integer, nil]
69
+ #
70
+ attr_reader :since
71
+
72
+ # A timestamp, events must be older than this to pass
73
+ #
74
+ # @api public
75
+ #
76
+ # @example
77
+ # filter.until # => 1292190341
78
+ #
79
+ # @return [Integer, nil]
80
+ #
81
+ attr_reader :until
82
+
83
+ # Maximum number of events to be returned in the initial query
84
+ #
85
+ # @api public
86
+ #
87
+ # @example
88
+ # filter.limit # => 420
89
+ #
90
+ # @return [Integer, nil]
91
+ #
92
+ attr_reader :limit
93
+
94
+ # Instantiates a new Filter
95
+ #
96
+ # @api public
97
+ #
98
+ # @example
99
+ # Nostr::Filter.new(
100
+ # ids: ['c24881c305c5cfb7c1168be7e9b0e150'],
101
+ # authors: ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7'],
102
+ # kinds: [0, 1, 2],
103
+ # since: 1230981305,
104
+ # until: 1292190341,
105
+ # e: ['7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2'],
106
+ # p: ['000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7']
107
+ # )
108
+ #
109
+ # @param kwargs [Hash]
110
+ # @option kwargs [Array<String>, nil] ids A list of event ids or prefixes
111
+ # @option kwargs [Array<String>, nil] authors A list of pubkeys or prefixes, the pubkey of an event must be one
112
+ # of these
113
+ # @option kwargs [Array<Integer>, nil] kinds A list of a kind numbers
114
+ # @option kwargs [Array<String>, nil] e A list of event ids that are referenced in an "e" tag
115
+ # @option kwargs [Array<String, nil>] p A list of pubkeys that are referenced in a "p" tag
116
+ # @option kwargs [Integer, nil] since A timestamp, events must be newer than this to pass
117
+ # @option kwargs [Integer, nil] until A timestamp, events must be older than this to pass
118
+ # @option kwargs [Integer, nil] limit Maximum number of events to be returned in the initial query
119
+ #
120
+ def initialize(**kwargs)
121
+ @ids = kwargs[:ids]
122
+ @authors = kwargs[:authors]
123
+ @kinds = kwargs[:kinds]
124
+ @e = kwargs[:e]
125
+ @p = kwargs[:p]
126
+ @since = kwargs[:since]
127
+ @until = kwargs[:until]
128
+ @limit = kwargs[:limit]
129
+ end
130
+
131
+ # Converts the filter to a hash, removing all empty attributes
132
+ #
133
+ # @api public
134
+ #
135
+ # @example
136
+ # filter.to_h # => {:ids=>["c24881c305c5cfb7c1168be7e9b0e150"],
137
+ # :authors=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
138
+ # :kinds=>[0, 1, 2],
139
+ # :"#e"=>["7bdb422f254194ae4bb86d354c0bd5a888fce233ffc77dceb3e844ceec1fcfb2"],
140
+ # :"#p"=>["000000001c5c45196786e79f83d21fe801549fdc98e2c26f96dcef068a5dbcd7"],
141
+ # :since=>1230981305,
142
+ # :until=>1292190341}
143
+ #
144
+ # @return [Hash] The filter as a hash.
145
+ #
146
+ def to_h
147
+ {
148
+ ids:,
149
+ authors:,
150
+ kinds:,
151
+ '#e': e,
152
+ '#p': p,
153
+ since:,
154
+ until: self.until,
155
+ limit:
156
+ }.compact
157
+ end
158
+
159
+ # Compares two filters. Returns true if all attributes are equal and false otherwise
160
+ #
161
+ # @api public
162
+ #
163
+ # @example
164
+ # filter == filter # => true
165
+ #
166
+ # @return [Boolean] True if all attributes are equal and false otherwise
167
+ #
168
+ def ==(other)
169
+ to_h == other.to_h
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # A pair of public and private keys
5
+ class KeyPair
6
+ # 32-bytes hex-encoded private key
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
12
+ #
13
+ # @return [String]
14
+ #
15
+ attr_reader :private_key
16
+
17
+ # 32-bytes hex-encoded public key
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
23
+ #
24
+ # @return [String]
25
+ #
26
+ attr_reader :public_key
27
+
28
+ # Instantiates a key pair
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # keypair = Nostr::KeyPair.new(
34
+ # private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
35
+ # public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
36
+ # )
37
+ #
38
+ # @param private_key [String] 32-bytes hex-encoded private key.
39
+ # @param public_key [String] 32-bytes hex-encoded public key.
40
+ #
41
+ def initialize(private_key:, public_key:)
42
+ @private_key = private_key
43
+ @public_key = public_key
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ecdsa'
4
+ require 'securerandom'
5
+
6
+ module Nostr
7
+ # Generates private keys, public keys and key pairs.
8
+ class Keygen
9
+ # Instantiates a new keygen
10
+ #
11
+ # @api public
12
+ #
13
+ # @example
14
+ # keygen = Nostr::Keygen.new
15
+ #
16
+ def initialize
17
+ @group = ECDSA::Group::Secp256k1
18
+ end
19
+
20
+ # Generates a pair of private and public keys
21
+ #
22
+ # @api public
23
+ #
24
+ # @example
25
+ # keypair = keygen.generate_keypair
26
+ # keypair # #<Nostr::KeyPair:0x0000000107bd3550
27
+ # @private_key="893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900",
28
+ # @public_key="2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558">
29
+ #
30
+ # @return [KeyPair] An object containing a private key and a public key.
31
+ #
32
+ def generate_key_pair
33
+ private_key = generate_private_key
34
+ public_key = extract_public_key(private_key)
35
+
36
+ KeyPair.new(private_key:, public_key:)
37
+ end
38
+
39
+ # Generates a private key
40
+ #
41
+ # @api public
42
+ #
43
+ # @example
44
+ # private_key = keygen.generate_private_key
45
+ # private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
46
+ #
47
+ # @return [String] A 32-bytes hex-encoded private key.
48
+ #
49
+ def generate_private_key
50
+ (SecureRandom.random_number(group.order - 1) + 1).to_s(16)
51
+ end
52
+
53
+ # Extracts a public key from a private key
54
+ #
55
+ # @api public
56
+ #
57
+ # @example
58
+ # private_key = keygen.generate_private_key
59
+ # public_key = keygen.extract_public_key(private_key)
60
+ # public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
61
+ #
62
+ # @return [String] A 32-bytes hex-encoded public key.
63
+ #
64
+ def extract_public_key(private_key)
65
+ group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16)
66
+ end
67
+
68
+ private
69
+
70
+ # The elliptic curve group. Used to generate public and private keys
71
+ #
72
+ # @api private
73
+ #
74
+ # @return [ECDSA::Group]
75
+ #
76
+ attr_reader :group
77
+ end
78
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nostr
4
+ # Relays expose a websocket endpoint to which clients can connect.
5
+ class Relay
6
+ # The websocket URL of the relay
7
+ #
8
+ # @api public
9
+ #
10
+ # @example
11
+ # relay.url # => 'wss://relay.damus.io'
12
+ #
13
+ # @return [String]
14
+ #
15
+ attr_reader :url
16
+
17
+ # The name of the relay
18
+ #
19
+ # @api public
20
+ #
21
+ # @example
22
+ # relay.name # => 'Damus'
23
+ #
24
+ # @return [String]
25
+ #
26
+ attr_reader :name
27
+
28
+ # Instantiates a new Relay
29
+ #
30
+ # @api public
31
+ #
32
+ # @example
33
+ # relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
34
+ #
35
+ # @return [String] The websocket URL of the relay
36
+ # @return [String] The name of the relay
37
+ #
38
+ def initialize(url:, name:)
39
+ @url = url
40
+ @name = name
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Nostr
6
+ # A subscription the result of a request to receive events from a relay
7
+ class Subscription
8
+ # A random string that should be used to represent a subscription
9
+ #
10
+ # @api public
11
+ #
12
+ # @example
13
+ # subscription.id # => 'c24881c305c5cfb7c1168be7e9b0e150'
14
+ #
15
+ # @return [String]
16
+ #
17
+ attr_reader :id
18
+
19
+ # An object that determines what events will be sent in the subscription
20
+ #
21
+ # @api public
22
+ #
23
+ # @example
24
+ # subscription.filter # => #<Nostr::Subscription:0x0000000110eea460 @filter=#<Nostr::Filter:0x0000000110c24de8>,
25
+ # @id="0dd7f3fa06cd5f797438dd0b7477f3c7">
26
+ #
27
+ # @return [Filter]
28
+ #
29
+ attr_reader :filter
30
+
31
+ # Initializes a subscription
32
+ #
33
+ # @api public
34
+ #
35
+ # @example Creating a subscription with no id and no filters
36
+ # subscription = Nostr::Subscription.new
37
+ #
38
+ # @example Creating a subscription with an ID
39
+ # subscription = Nostr::Subscription.new(id: 'c24881c305c5cfb7c1168be7e9b0e150')
40
+ #
41
+ # @example Subscribing to all events created after a certain time
42
+ # subscription = Nostr::Subscription.new(filter: Nostr::Filter.new(since: 1230981305))
43
+ #
44
+ # @param id [String] A random string that should be used to represent a subscription
45
+ # @param filter [Filter] An object that determines what events will be sent in that subscription
46
+ #
47
+ def initialize(filter:, id: SecureRandom.hex)
48
+ @id = id
49
+ @filter = filter
50
+ end
51
+
52
+ # Compares two subscriptions. Returns true if all attributes are equal and false otherwise
53
+ #
54
+ # @api public
55
+ #
56
+ # @example
57
+ # subscription1 == subscription1 # => true
58
+ #
59
+ # @return [Boolean] True if all attributes are equal and false otherwise
60
+ #
61
+ def ==(other)
62
+ id == other.id && filter == other.filter
63
+ end
64
+ end
65
+ end
data/lib/nostr/user.rb ADDED
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'schnorr'
4
+ require 'json'
5
+
6
+ module Nostr
7
+ # Each user has a keypair. Signatures, public key, and encodings are done according to the
8
+ # Schnorr signatures standard for the curve secp256k1.
9
+ class User
10
+ # A pair of private and public keys
11
+ #
12
+ # @api public
13
+ #
14
+ # @example
15
+ # user.keypair # #<Nostr::KeyPair:0x0000000107bd3550
16
+ # @private_key="893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900",
17
+ # @public_key="2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558">
18
+ #
19
+ # @return [KeyPair]
20
+ #
21
+ attr_reader :keypair
22
+
23
+ # Instantiates a user
24
+ #
25
+ # @api public
26
+ #
27
+ # @example Creating a user with no keypair
28
+ # user = Nostr::User.new
29
+ #
30
+ # @example Creating a user with a keypair
31
+ # user = Nostr::User.new(keypair: keypair)
32
+ #
33
+ # @param keypair [Keypair] A pair of private and public keys
34
+ # @param keygen [Keygen] A private key and public key generator
35
+ #
36
+ def initialize(keypair: nil, keygen: Keygen.new)
37
+ @keypair = keypair || keygen.generate_key_pair
38
+ end
39
+
40
+ # Builds an Event
41
+ #
42
+ # @api public
43
+ #
44
+ # @example Creating a note event
45
+ # event = user.create_event(
46
+ # kind: Nostr::EventKind::TEXT_NOTE,
47
+ # content: 'Your feedback is appreciated, now pay $8'
48
+ # )
49
+ #
50
+ # @param event_attributes [Hash]
51
+ # @option event_attributes [String] :pubkey 32-bytes hex-encoded public key of the event creator.
52
+ # @option event_attributes [Integer] :created_at Date of the creation of the vent. A UNIX timestamp, in seconds.
53
+ # @option event_attributes [Integer] :kind The kind of the event. An integer from 0 to 3.
54
+ # @option event_attributes [Array<Array>] :tags An array of tags. Each tag is an array of strings.
55
+ # @option event_attributes [String] :content Arbitrary string.
56
+ #
57
+ # @return [Event]
58
+ #
59
+ def create_event(event_attributes)
60
+ event_fragment = EventFragment.new(**event_attributes.merge(pubkey: keypair.public_key))
61
+ event_sha256 = Digest::SHA256.hexdigest(JSON.dump(event_fragment.serialize))
62
+
63
+ signature = sign(event_sha256)
64
+
65
+ Event.new(
66
+ id: event_sha256,
67
+ pubkey: event_fragment.pubkey,
68
+ created_at: event_fragment.created_at,
69
+ kind: event_fragment.kind,
70
+ tags: event_fragment.tags,
71
+ content: event_fragment.content,
72
+ sig: signature
73
+ )
74
+ end
75
+
76
+ private
77
+
78
+ # Signs an event with the user's private key
79
+ #
80
+ # @api private
81
+ #
82
+ # @param event_sha256 [String] The SHA256 hash of the event.
83
+ #
84
+ # @return [String] The signature of the event.
85
+ #
86
+ def sign(event_sha256)
87
+ hex_private_key = Array(keypair.private_key).pack('H*')
88
+ hex_message = Array(event_sha256).pack('H*')
89
+ Schnorr.sign(hex_message, hex_private_key).encode.unpack1('H*')
90
+ end
91
+ end
92
+ end
data/lib/nostr/version.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Nostr
4
- VERSION = '0.1.0'
4
+ # The version of the gem
5
+ VERSION = '0.3.0'
5
6
  end
data/lib/nostr.rb CHANGED
@@ -1,9 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'nostr/version'
4
+ require_relative 'nostr/keygen'
5
+ require_relative 'nostr/client_message_type'
6
+ require_relative 'nostr/filter'
7
+ require_relative 'nostr/subscription'
8
+ require_relative 'nostr/relay'
9
+ require_relative 'nostr/key_pair'
10
+ require_relative 'nostr/event_kind'
11
+ require_relative 'nostr/event_fragment'
12
+ require_relative 'nostr/event'
13
+ require_relative 'nostr/client'
14
+ require_relative 'nostr/user'
4
15
 
5
16
  # Encapsulates all the gem's logic
6
17
  module Nostr
7
- class Error < StandardError; end
8
- # Your code goes here...
9
18
  end