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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +23 -0
- data/CHANGELOG.md +16 -0
- data/README.md +232 -13
- data/Steepfile +14 -0
- data/lib/nostr/client.rb +176 -0
- data/lib/nostr/client_message_type.rb +15 -0
- data/lib/nostr/event.rb +93 -0
- data/lib/nostr/event_fragment.rb +111 -0
- data/lib/nostr/event_kind.rb +35 -0
- data/lib/nostr/filter.rb +172 -0
- data/lib/nostr/key_pair.rb +46 -0
- data/lib/nostr/keygen.rb +78 -0
- data/lib/nostr/relay.rb +43 -0
- data/lib/nostr/subscription.rb +65 -0
- data/lib/nostr/user.rb +92 -0
- data/lib/nostr/version.rb +2 -1
- data/lib/nostr.rb +11 -2
- data/nostr.gemspec +15 -2
- data/sig/nostr/client.rbs +20 -0
- data/sig/nostr/client_message_type.rbs +7 -0
- data/sig/nostr/event.rbs +24 -0
- data/sig/nostr/event_fragment.rbs +12 -0
- data/sig/nostr/event_kind.rbs +8 -0
- data/sig/nostr/filter.rbs +25 -0
- data/sig/nostr/key_pair.rbs +9 -0
- data/sig/nostr/keygen.rbs +13 -0
- data/sig/nostr/relay.rbs +9 -0
- data/sig/nostr/subscription.rbs +9 -0
- data/sig/nostr/user.rbs +24 -0
- data/sig/vendor/ecsda/group/secp256k1.rbs +6 -0
- data/sig/vendor/event_emitter.rbs +9 -0
- data/sig/vendor/event_machine/channel.rbs +18 -0
- data/sig/vendor/event_machine.rbs +69 -0
- data/sig/vendor/schnorr.rbs +4 -0
- metadata +205 -8
@@ -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
|
data/lib/nostr/filter.rb
ADDED
@@ -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
|
data/lib/nostr/keygen.rb
ADDED
@@ -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
|
data/lib/nostr/relay.rb
ADDED
@@ -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
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
|