nostr_ruby 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e00f86ed7be72e972550e69600e39cb66aabb68c544c49d78fa2187c659f758f
4
+ data.tar.gz: 3ee6d5158fdf2875b1a250da54034c693b811a8f9de1385f23e4f0b6d1391140
5
+ SHA512:
6
+ metadata.gz: '03848ad6d793f19ea6a36a6b7d829129929a15c3adb873029f2bb66a65c5cbdef0400f407292cacd0d49ed94ef77c04eab361c2b219b5a5130060a85e03e333c'
7
+ data.tar.gz: 8786d82bcb14658491ab3c2a659f0d78b1cedc25f9d9048bece634331da488b5d276e3b0e844ea8900102809b70ab356d0bb3028abd2b4ca4f18e7e0224dcb84
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,39 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ nostr_ruby (0.1.0)
5
+ base64 (~> 0.1.1)
6
+ bech32 (~> 1.3.0)
7
+ bip-schnorr (~> 0.4.0)
8
+ json (~> 2.6.2)
9
+ unicode-emoji (~> 3.3.1)
10
+ websocket-client-simple (~> 0.6.0)
11
+
12
+ GEM
13
+ remote: https://rubygems.org/
14
+ specs:
15
+ base64 (0.1.1)
16
+ bech32 (1.3.0)
17
+ thor (>= 1.1.0)
18
+ bip-schnorr (0.4.0)
19
+ ecdsa (~> 1.2.0)
20
+ ecdsa (1.2.0)
21
+ event_emitter (0.2.6)
22
+ json (2.6.3)
23
+ thor (1.2.1)
24
+ unicode-emoji (3.3.1)
25
+ unicode-version (~> 1.0)
26
+ unicode-version (1.3.0)
27
+ websocket (1.2.9)
28
+ websocket-client-simple (0.6.0)
29
+ event_emitter
30
+ websocket
31
+
32
+ PLATFORMS
33
+ arm64-darwin-21
34
+
35
+ DEPENDENCIES
36
+ nostr_ruby!
37
+
38
+ BUNDLED WITH
39
+ 2.3.14
data/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT LICENSE
2
+
3
+ Copyright (c) 2023 Daniele Tonon <tonon@vitamino.it>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # Nostr Ruby
2
+
3
+ A ruby library to interact with the [Nostr Protocol](https://github.com/nostr-protocol/nostr).
4
+
5
+ ---
6
+
7
+ **Note**: this is a first proof of concept version, the API will probably change in the near future.
8
+
9
+ ---
10
+
11
+ ## Installation
12
+
13
+ ```
14
+ gem install nostr_ruby
15
+ ```
16
+ ## Usage
17
+ ### Manage the keys
18
+ ```ruby
19
+ require "nostr_ruby"
20
+
21
+ n = Nostr.new({private_key: "964b29795d621cdacf05fd94fb23206c88742db1fa50b34d7545f3a2221d8124"})
22
+ # <Nostr:0x00000001063ffa28 @private_key="964b29795d621cdacf05fd94fb23206c88742db1fa50b34d7545f3a2221d8124" @public_key="da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81">
23
+
24
+ n.bech32_keys
25
+ # => {:public_key=>"npub1mg2nzunrsk9df94zr3uudhzltnu6lzq2muax09xmhu5gxxrvnkqsvpjg3p", :private_key=>"nsec1je9jj72avgwd4nc9lk20kgeqdjy8gtd3lfgtxnt4ghe6ygsasyjq7kh6c4"}
26
+
27
+ n = Nostr.new({private_key: "nsec1je9jj72avgwd4nc9lk20kgeqdjy8gtd3lfgtxnt4ghe6ygsasyjq7kh6c4"})
28
+ # => #<Nostr:0x00000001060952c0 @private_key="964b29795d621cdacf05fd94fb23206c88742db1fa50b34d7545f3a2221d8124", @public_key="da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81">
29
+
30
+ n.keys
31
+ # => {:public_key=>"da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81", :private_key=>"964b29795d621cdacf05fd94fb23206c88742db1fa50b34d7545f3a2221d8124"}
32
+ ```
33
+
34
+ ### Set the user metadata
35
+ ```ruby
36
+ metadata = n.build_metadata_event("Mr Robot", "I walk around the city", "https://upload.wikimedia.org/wikipedia/commons/3/35/Mr_robot_photo.jpg", "mrrobot@mrrobot.com")
37
+ #["EVENT",
38
+ # {:pubkey=>"9be59510fa12b77340bb57e555bac716455fedf46d1a354185d4e72bd0340b6f",
39
+ # :created_at=>1671546067,
40
+ # :kind=>0,
41
+ # :tags=>[],
42
+ # :content=>"{\"name\":\"Mr Robot\",\"about\":\"I walk around the city\",\"picture\":\"https://upload.wikimedia.org/wikipedia/commons/3/35/Mr_robot_photo.jpg\",\"nip05\":\"mrrobot@mrrobot.com\"}",
43
+ # "id"=>"3bd77596ea999dde26689c24370dc4adfa66c33abf1b4c23bf863a516106cda2",
44
+ # "sig"=>"2ff752e9f3ed824e7677c41c73728315f0532f3437857774d7a50a577563f391785afd1f84bef3e3574939b14cf096380d4790375953c793504ffcf2f0467d69"}]
45
+ ```
46
+
47
+ ### Create a post
48
+ ```ruby
49
+ note = n.build_note_event("Hello Nostr!")
50
+ # =>
51
+ # ["EVENT",
52
+ # {:pubkey=>"da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81",
53
+ # :created_at=>1671406583,
54
+ # :kind=>1,
55
+ # :tags=>[],
56
+ # :content=>"Hello Nostr!",
57
+ # "id"=>"23411895658d374ec922adf774a70172290b2c738ae67815bd8945e5d8fff3bb",
58
+ # "sig"=>"871177b77840bdf092dabacf98c47690647fd6ceb3cc79dd7af7e98c6aded0b808abd5566e2864bd438364cea2f17bd6f9d55091b3c5136839cf160beca42b63"}]
59
+ ```
60
+
61
+ ### Create a channel post
62
+ ```ruby
63
+ channel_note = n.build_note_event("Welcome on my channel :)", "136b0b99eff742e0939799417d04d8b48049672beb6d8110ce6b0fc978cd67a1")
64
+ # =>
65
+ # ["EVENT",
66
+ # {:pubkey=>"da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81",
67
+ # :created_at=>1671406522,
68
+ # :kind=>42,
69
+ # :tags=>[["e", "136b0b99eff742e0939799417d04d8b48049672beb6d8110ce6b0fc978cd67a1"]],
70
+ # :content=>"Welcome on my channel :)",
71
+ # "id"=>"96ac317516e9cc3bae8238cf11a95a2f12d1bd2f6553c0867d47f3165ca3483b",
72
+ # "sig"=>"ccb6cbfa5c3cfac7b7f48dd9cda25d6a2493cbf8df91fa8f9fee2559a20c92613326a319f5b76aff9fef85278e04ce0ee78e636afb4ef2bb000ee8a6fdf418d2"}]
73
+ ```
74
+
75
+ ### Recommend a relay
76
+ ```ruby
77
+ recommendation = n.build_recommended_relay_event("wss://relay.damus.io")
78
+ # =>
79
+ # ["EVENT",
80
+ # {:pubkey=>"d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef",
81
+ # :created_at=>1672079256,
82
+ # :kind=>2,
83
+ # :tags=>[],
84
+ # :content=>"wss://relay.damus.io",
85
+ # "id"=>"1842c9feb3bf2ad7095c8a51238f598fa028116d4fd919af22ad2c63ba3b7d69",
86
+ # "sig"=>"9c2f158f379b2d234fd0d363b46a7f90c25392f9296111b6cc04224df8aec69817fa62d7225c12b90fdc31eb89c7afaa427b18147cc8ad6cd411b47dda1331b6"}]
87
+ ```
88
+
89
+ ### Share a contact list
90
+ ```ruby
91
+ contact_list = n.build_contact_list_event(
92
+ [["54399b6d8200813bfc53177ad4f13d6ab712b6b23f91aefbf5da45aeb5c96b08", "wss://alicerelay.com/", "alice"],
93
+ ["850708b7099215bf9a1356d242c2354939e9a844c1359d3b5209592a0b420452", "wss://bobrelay.com/nostr", "bob"],
94
+ ["f7f4b0072368460a09138bf3966fb1c59d0bdadfc3aff4e59e6896194594a82a", "ws://carolrelay.com/ws", "carol"]]
95
+ )
96
+ # =>
97
+ # ["EVENT",
98
+ # {:pubkey=>"d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef",
99
+ # :created_at=>1672079733,
100
+ # :kind=>3,
101
+ # :tags=>
102
+ # [["p", "54399b6d8200813bfc53177ad4f13d6ab712b6b23f91aefbf5da45aeb5c96b08", "wss://alicerelay.com/", "alice"],
103
+ # ["p", "850708b7099215bf9a1356d242c2354939e9a844c1359d3b5209592a0b420452", "wss://bobrelay.com/nostr", "bob"],
104
+ # ["p", "f7f4b0072368460a09138bf3966fb1c59d0bdadfc3aff4e59e6896194594a82a", "ws://carolrelay.com/ws", "carol"]],
105
+ # :content=>"",
106
+ # "id"=>"3cdc1b5fa9d29aaa6b068cfb66cfd95f79784792beaec6cbb2645187b1c632e9",
107
+ # "sig"=>"e560e0d1a42261900c8ec32bf2d2016b95c3291adb45c7bf82ef94061beb44a45d6a768d9be773ec48ba9f54d05b4505bda0c1f21805e2be681c7436b3d39791"}]
108
+ ```
109
+
110
+ ### Create a private message
111
+ ```ruby
112
+ private_message = n.build_dm_event("Hello!", "da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81") # To myself
113
+ # =>
114
+ # ["EVENT",
115
+ # {:pubkey=>"da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81",
116
+ # :created_at=>1671406025,
117
+ # :kind=>4,
118
+ # :tags=>[["p", "da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81"]],
119
+ # :content=>"AIZ7vomEJEFgB934gWzlNA==?iv=mwKLb6lZSG5X1y1BNYv6dg==",
120
+ # "id"=>"6a3efcf47a31bb05aeca0b13bf1f9b9e91b126e0a67e783253fb3bae20f0dc63",
121
+ # "sig"=>"0e390bb3c783157b3e32c3f6641fb40df9f62e326ac8a3448a70c94103b909e80292a2c4530562298f2c3935899111843a43548185abf09abf583bc3e6e3ddde"}]
122
+ ```
123
+
124
+ ### Decrypt a private message
125
+ ```ruby
126
+ # Get the reply from the relay
127
+ reply
128
+ # => ["EVENT","0.27631406274260906",{"tags":[["p","da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81"]],"content":"vWATHf2l/KI7RSyVlbxpvufZc+Lui/0oDysTyfEG5vs=?iv=G4SG7ArMGkglX0UJbJBDUA==","sig":"fe816b86579f5d13ab23c88410364442a9b4393ac0d74f4642cd51a1887f04908ff57ef60409d529a6939c50d77b048320417005460b0353c8f990d1b35c3661","id":"5a0274f33cdb064136c1423ac4b096d7f1e3fb36f60404ef2449ee44331de03f","kind":4,"pubkey":"da15317263858ad496a21c79c6dc5f5cf9af880adf3a6794dbbf2883186c9d81","created_at":1671407520}]
129
+
130
+ n.decrypt_dm(reply)
131
+ # => "Nice to meet you!"
132
+ ```
133
+
134
+ ### Delete an event
135
+ ```ruby
136
+ deletion = n.build_deletion_event(["23411895658d374ec922adf774a70172290b2c738ae67815bd8945e5d8fff3bb"], "Duplicate")
137
+ # =>
138
+ # ["EVENT",
139
+ # {:pubkey=>"d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef",
140
+ # :created_at=>1672080450,
141
+ # :kind=>5,
142
+ # :tags=>[["e", "23411895658d374ec922adf774a70172290b2c738ae67815bd8945e5d8fff3bb"]],
143
+ # :content=>"Duplicate",
144
+ # "id"=>"e4a8556da9dc35da54dff747593073a90ac1de55131ca0deef6a5fd3b402d5fd",
145
+ # "sig"=>"95ccb5e965c1a6ba36b919a00cd7d3b65286435f93f49a2ebb846dc791a61179e55d544ebf00d4f8eeb53b0a75a97c072287c7458dfbaccb70b4aef6b0acf766"
146
+ ```
147
+
148
+ ### React to an event
149
+ ```ruby
150
+ reaction = n.build_reaction_event("🔥", "23411895658d374ec922adf774a70172290b2c738ae67815bd8945e5d8fff3bb", "d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef")
151
+ # =>
152
+ # ["EVENT",
153
+ # {:pubkey=>"d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef",
154
+ # :created_at=>1672080671,
155
+ # :kind=>7,
156
+ # :tags=>[["e", "23411895658d374ec922adf774a70172290b2c738ae67815bd8945e5d8fff3bb"], ["p", "d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef"]],
157
+ # :content=>"🔥",
158
+ # "id"=>"f267c8ee24989b633b261efaa3892b07cdc90af80cedfd007b24a5c6232fc631",
159
+ # "sig"=>"84f2fc213337c6d2c26a4638b1db4e39f788811acd5bce5b9141b7ef56a9aa80768fbbd1109a7783a0d3033732675e231de286c6c745e62436865f4f15b838b6"}]
160
+ ```
161
+
162
+ ### Create events with a PoW difficulty
163
+ ```ruby
164
+ n.set_pow_difficulty(16)
165
+ note = n.build_note_event("Hello Nostr!")
166
+ # =>
167
+ # ["EVENT",
168
+ # {:pubkey=>"d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef",
169
+ # :created_at=>1672095162,
170
+ # :kind=>42,
171
+ # :tags=>[["e", "d0fbe5e40469bba810ecb9e1b0b6c13370592df161655e81497c2eb69d0d5bef"], ["nonce", "232735", "16"]],
172
+ # :content=>"Hello Nostr!",
173
+ # "id"=>"0000fb0c4563274e742e56d7d6de08684a2a25dfb52b79cccdb49c649dccbf45",
174
+ # "sig"=>"838a1457c75084319e4723fbd9cbcf4c3311c466daf3908ffa114682094140e3b188996a73ae9fd3d3c6dbf08beecf9081b8d2bf0e60163b07cdf36a50dea1c0"}]
175
+ ```
@@ -0,0 +1,59 @@
1
+ class CustomAddr
2
+
3
+ attr_accessor :hrp # human-readable part
4
+ attr_accessor :prog # witness program
5
+
6
+ def initialize(addr = nil)
7
+ @hrp, @prog = parse_addr(addr) if addr
8
+ end
9
+
10
+ def to_scriptpubkey
11
+ prog.map{|p|[p].pack("C")}.join.unpack('H*').first
12
+ end
13
+
14
+ def scriptpubkey=(script)
15
+ values = [script].pack('H*').unpack("C*")
16
+ @prog = values
17
+ end
18
+
19
+ def addr
20
+ spec = Bech32::Encoding::BECH32
21
+ Bech32.encode(hrp, convert_bits(prog, 8, 5), spec)
22
+ end
23
+
24
+ private
25
+
26
+ def parse_addr(addr)
27
+ hrp, data, spec = Bech32.decode(addr)
28
+ raise 'Invalid address.' if hrp.nil? || data[0].nil?
29
+ # raise 'Invalid witness version' if ver > 16
30
+ prog = convert_bits(data, 5, 8, false)
31
+ # raise 'Invalid witness program' if prog.nil? || prog.length < 2 || prog.length > 40
32
+ # raise 'Invalid witness program with version 0' if ver == 0 && (prog.length != 20 && prog.length != 32)
33
+ [hrp, prog]
34
+ end
35
+
36
+ def convert_bits(data, from, to, padding=true)
37
+ acc = 0
38
+ bits = 0
39
+ ret = []
40
+ maxv = (1 << to) - 1
41
+ max_acc = (1 << (from + to - 1)) - 1
42
+ data.each do |v|
43
+ return nil if v < 0 || (v >> from) != 0
44
+ acc = ((acc << from) | v) & max_acc
45
+ bits += from
46
+ while bits >= to
47
+ bits -= to
48
+ ret << ((acc >> bits) & maxv)
49
+ end
50
+ end
51
+ if padding
52
+ ret << ((acc << (to - bits)) & maxv) unless bits == 0
53
+ elsif bits >= from || ((acc << (to - bits)) & maxv) != 0
54
+ return nil
55
+ end
56
+ ret
57
+ end
58
+
59
+ end
@@ -0,0 +1,3 @@
1
+ module NostrRuby
2
+ VERSION = '0.1.0'
3
+ end
data/lib/nostr_ruby.rb ADDED
@@ -0,0 +1,280 @@
1
+ require 'custom_addr'
2
+ require 'ecdsa'
3
+ require 'schnorr'
4
+ require 'json'
5
+ require 'base64'
6
+ require 'bech32'
7
+ require 'unicode/emoji'
8
+ require 'websocket-client-simple'
9
+
10
+ # * Ruby library to interact with the Nostr protocol
11
+
12
+ class Nostr
13
+ attr_reader :private_key, :public_key, :pow_difficulty_target
14
+
15
+ def initialize(key)
16
+ hex_private_key = if key[:private_key]&.include?('nsec')
17
+ Nostr.to_hex(key[:private_key])
18
+ else
19
+ key[:private_key]
20
+ end
21
+
22
+ hex_public_key = if key[:public_key]&.include?('npub')
23
+ Nostr.to_hex(key[:public_key])
24
+ else
25
+ key[:public_key]
26
+ end
27
+
28
+ if hex_private_key
29
+ @private_key = hex_private_key
30
+ group = ECDSA::Group::Secp256k1
31
+ @public_key = group.generator.multiply_by_scalar(private_key.to_i(16)).x.to_s(16)
32
+ elsif hex_public_key
33
+ @public_key = hex_public_key
34
+ else
35
+ raise 'Missing private or public key'
36
+ end
37
+ end
38
+
39
+ def keys
40
+ keys = { public_key: @public_key }
41
+ keys[:private_key] = @private_key if @private_key
42
+ keys
43
+ end
44
+
45
+ def bech32_keys
46
+ bech32_keys = { public_key: Nostr.to_bech32(@public_key, 'npub') }
47
+ bech32_keys[:private_key] = Nostr.to_bech32(@private_key, 'nsec') if @private_key
48
+ bech32_keys
49
+ end
50
+
51
+ def self.to_hex(bech32_key)
52
+ public_addr = CustomAddr.new(bech32_key)
53
+ public_addr.to_scriptpubkey
54
+ end
55
+
56
+ def self.to_bech32(hex_key, hrp)
57
+ custom_addr = CustomAddr.new
58
+ custom_addr.scriptpubkey = hex_key
59
+ custom_addr.hrp = hrp
60
+ custom_addr.addr
61
+ end
62
+
63
+ def calculate_shared_key(other_public_key)
64
+ ec = OpenSSL::PKey::EC.new('secp256k1')
65
+ ec.private_key = OpenSSL::BN.new(@private_key, 16)
66
+ recipient_key_hex = "02#{other_public_key}"
67
+ recipient_pub_bn = OpenSSL::BN.new(recipient_key_hex, 16)
68
+ secret_point = OpenSSL::PKey::EC::Point.new(ec.group, recipient_pub_bn)
69
+ ec.dh_compute_key(secret_point)
70
+ end
71
+
72
+ def sign_event(event)
73
+ raise 'Invalid pubkey' unless event[:pubkey].is_a?(String) && event[:pubkey].size == 64
74
+ raise 'Invalid created_at' unless event[:created_at].is_a?(Integer)
75
+ raise 'Invalid kind' unless (0..29_999).include?(event[:kind])
76
+ raise 'Invalid tags' unless event[:tags].is_a?(Array)
77
+ raise 'Invalid content' unless event[:content].is_a?(String)
78
+
79
+ serialized_event = [
80
+ 0,
81
+ event[:pubkey],
82
+ event[:created_at],
83
+ event[:kind],
84
+ event[:tags],
85
+ event[:content]
86
+ ]
87
+
88
+ serialized_event_sha256 = nil
89
+ if @pow_difficulty_target
90
+ nonce = 1
91
+ loop do
92
+ nonce_tag = ['nonce', nonce.to_s, @pow_difficulty_target.to_s]
93
+ nonced_serialized_event = serialized_event.clone
94
+ nonced_serialized_event[4] = nonced_serialized_event[4] + [nonce_tag]
95
+ serialized_event_sha256 = Digest::SHA256.hexdigest(JSON.dump(nonced_serialized_event))
96
+ if match_pow_difficulty?(serialized_event_sha256)
97
+ event[:tags] << nonce_tag
98
+ break
99
+ end
100
+ nonce += 1
101
+ end
102
+ else
103
+ serialized_event_sha256 = Digest::SHA256.hexdigest(JSON.dump(serialized_event))
104
+ end
105
+
106
+ private_key = Array(@private_key).pack('H*')
107
+ message = Array(serialized_event_sha256).pack('H*')
108
+ event_signature = Schnorr.sign(message, private_key).encode.unpack('H*')[0]
109
+
110
+ event['id'] = serialized_event_sha256
111
+ event['sig'] = event_signature
112
+ event
113
+ end
114
+
115
+ def build_event(payload)
116
+ event = sign_event(payload)
117
+ ['EVENT', event]
118
+ end
119
+
120
+ def build_metadata_event(name, about, picture, nip05)
121
+ data = {}
122
+ data[:name] = name if name
123
+ data[:about] = about if about
124
+ data[:picture] = picture if picture
125
+ data[:nip05] = nip05 if nip05
126
+ event = {
127
+ "pubkey": @public_key,
128
+ "created_at": Time.now.utc.to_i,
129
+ "kind": 0,
130
+ "tags": [],
131
+ "content": data.to_json
132
+ }
133
+
134
+ event = sign_event(event)
135
+ ['EVENT', event]
136
+ end
137
+
138
+ def build_note_event(text, channel_key = nil)
139
+ event = {
140
+ "pubkey": @public_key,
141
+ "created_at": Time.now.utc.to_i,
142
+ "kind": channel_key ? 42 : 1,
143
+ "tags": channel_key ? [['e', channel_key]] : [],
144
+ "content": text
145
+ }
146
+
147
+ event = sign_event(event)
148
+ ['EVENT', event]
149
+ end
150
+
151
+ def build_recommended_relay_event(relay)
152
+ raise 'Invalid relay' unless relay.start_with?('wss://') || relay.start_with?('ws://')
153
+
154
+ event = {
155
+ "pubkey": @public_key,
156
+ "created_at": Time.now.utc.to_i,
157
+ "kind": 2,
158
+ "tags": [],
159
+ "content": relay
160
+ }
161
+
162
+ event = sign_event(event)
163
+ ['EVENT', event]
164
+ end
165
+
166
+ def build_contact_list_event(contacts)
167
+ event = {
168
+ "pubkey": @public_key,
169
+ "created_at": Time.now.utc.to_i,
170
+ "kind": 3,
171
+ "tags": contacts.map { |c| ['p'] + c },
172
+ "content": ''
173
+ }
174
+
175
+ event = sign_event(event)
176
+ ['EVENT', event]
177
+ end
178
+
179
+ def build_dm_event(text, recipient_public_key)
180
+ cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
181
+ cipher.encrypt
182
+ cipher.iv = iv = cipher.random_iv
183
+ cipher.key = calculate_shared_key(recipient_public_key)
184
+ encrypted_text = cipher.update(text)
185
+ encrypted_text << cipher.final
186
+ encrypted_text = "#{Base64.encode64(encrypted_text)}?iv=#{Base64.encode64(iv)}"
187
+ encrypted_text = encrypted_text.gsub("\n", '')
188
+
189
+ event = {
190
+ "pubkey": @public_key,
191
+ "created_at": Time.now.utc.to_i,
192
+ "kind": 4,
193
+ "tags": [['p', recipient_public_key]],
194
+ "content": encrypted_text
195
+ }
196
+
197
+ event = sign_event(event)
198
+ ['EVENT', event]
199
+ end
200
+
201
+ def build_deletion_event(events, reason = '')
202
+ event = {
203
+ "pubkey": @public_key,
204
+ "created_at": Time.now.utc.to_i,
205
+ "kind": 5,
206
+ "tags": events.map{ |e| ['e', e] },
207
+ "content": reason
208
+ }
209
+
210
+ event = sign_event(event)
211
+ ['EVENT', event]
212
+ end
213
+
214
+ def build_reaction_event(reaction, event, author)
215
+ raise 'Invalid reaction' unless ['+', '-'].include?(reaction) || reaction.match?(Unicode::Emoji::REGEX)
216
+ raise 'Invalid author' unless event.is_a?(String) && event.size == 64
217
+ raise 'Invalid event' unless author.is_a?(String) && author.size == 64
218
+
219
+ event = {
220
+ "pubkey": @public_key,
221
+ "created_at": Time.now.utc.to_i,
222
+ "kind": 7,
223
+ "tags": [['e', event], ['p', author]],
224
+ "content": reaction
225
+ }
226
+
227
+ event = sign_event(event)
228
+ ['EVENT', event]
229
+ end
230
+
231
+ def decrypt_dm(event)
232
+ data = event[2]
233
+ sender_public_key = dat['pubkey']
234
+ encrypted = data['content'].split('?iv=')[0]
235
+ iv = data['content'].split('?iv=')[1]
236
+ cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc')
237
+ cipher.decrypt
238
+ cipher.iv = Base64.decode64(iv)
239
+ cipher.key = calculate_shared_key(sender_public_key)
240
+ (cipher.update(Base64.decode64(encrypted)) + cipher.final).force_encoding('UTF-8')
241
+ end
242
+
243
+ def build_req_event(filters)
244
+ ['REQ', SecureRandom.random_number.to_s, filters]
245
+ end
246
+
247
+ def build_close_event(subscription_id)
248
+ ['CLOSE', subscription_id]
249
+ end
250
+
251
+ def build_notice_event(message)
252
+ ['NOTICE', message]
253
+ end
254
+
255
+ def match_pow_difficulty?(event_id)
256
+ @pow_difficulty_target.nil? || @pow_difficulty_target == [event_id].pack("H*").unpack("B*")[0].index('1')
257
+ end
258
+
259
+ def set_pow_difficulty_target(n)
260
+ @pow_difficulty_target = n
261
+ end
262
+
263
+ def test_post_event(event, relay)
264
+ response = nil
265
+ ws = WebSocket::Client::Simple.connect relay
266
+ ws.on :message do |msg|
267
+ puts msg
268
+ response = JSON.parse(msg.data)
269
+ ws.close
270
+ end
271
+ ws.on :open do
272
+ ws.send event.to_json
273
+ end
274
+ while response.nil? do
275
+ sleep 0.1
276
+ end
277
+ response[0] == 'OK'
278
+ end
279
+
280
+ end
@@ -0,0 +1,22 @@
1
+ $:.unshift File.expand_path('../lib', __FILE__)
2
+ require 'nostr_ruby/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'nostr_ruby'
6
+ s.version = NostrRuby::VERSION
7
+ s.summary = 'A Ruby library to interact with the Nostr protocol'
8
+ s.description = 'This gem provides a simple class, MyGem, that can be used in other Ruby projects.'
9
+ s.authors = ['Daniele Tonon']
10
+ s.homepage = 'https://github.com/dtonon/nostr-ruby'
11
+ s.licenses = ['MIT']
12
+ s.files = Dir.glob('{bin/*,lib/**/*,[A-Z]*}')
13
+ s.platform = Gem::Platform::RUBY
14
+ s.require_paths = ['lib']
15
+
16
+ s.add_dependency 'base64', '~> 0.1.1'
17
+ s.add_dependency 'bech32', '~> 1.3.0'
18
+ s.add_dependency 'bip-schnorr', '~> 0.4.0'
19
+ s.add_dependency 'json', '~> 2.6.2'
20
+ s.add_dependency 'unicode-emoji', '~> 3.3.1'
21
+ s.add_dependency 'websocket-client-simple', '~> 0.6.0'
22
+ end
metadata ADDED
@@ -0,0 +1,135 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nostr_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Daniele Tonon
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-01-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.1.1
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.1.1
27
+ - !ruby/object:Gem::Dependency
28
+ name: bech32
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 1.3.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 1.3.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: bip-schnorr
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.4.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.4.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: json
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 2.6.2
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 2.6.2
69
+ - !ruby/object:Gem::Dependency
70
+ name: unicode-emoji
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 3.3.1
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 3.3.1
83
+ - !ruby/object:Gem::Dependency
84
+ name: websocket-client-simple
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.6.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.6.0
97
+ description: This gem provides a simple class, MyGem, that can be used in other Ruby
98
+ projects.
99
+ email:
100
+ executables: []
101
+ extensions: []
102
+ extra_rdoc_files: []
103
+ files:
104
+ - Gemfile
105
+ - Gemfile.lock
106
+ - LICENSE.md
107
+ - README.md
108
+ - lib/custom_addr.rb
109
+ - lib/nostr_ruby.rb
110
+ - lib/nostr_ruby/version.rb
111
+ - nostr_ruby.gemspec
112
+ homepage: https://github.com/dtonon/nostr-ruby
113
+ licenses:
114
+ - MIT
115
+ metadata: {}
116
+ post_install_message:
117
+ rdoc_options: []
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ required_rubygems_version: !ruby/object:Gem::Requirement
126
+ requirements:
127
+ - - ">="
128
+ - !ruby/object:Gem::Version
129
+ version: '0'
130
+ requirements: []
131
+ rubygems_version: 3.3.7
132
+ signing_key:
133
+ specification_version: 4
134
+ summary: A Ruby library to interact with the Nostr protocol
135
+ test_files: []