nostr 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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