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 +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +39 -0
- data/LICENSE.md +21 -0
- data/README.md +175 -0
- data/lib/custom_addr.rb +59 -0
- data/lib/nostr_ruby/version.rb +3 -0
- data/lib/nostr_ruby.rb +280 -0
- data/nostr_ruby.gemspec +22 -0
- metadata +135 -0
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
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
|
+
```
|
data/lib/custom_addr.rb
ADDED
@@ -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
|
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
|
data/nostr_ruby.gemspec
ADDED
@@ -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: []
|