nostr_ruby 0.1.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 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: []