nostr 0.3.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.editorconfig +1 -1
- data/.rubocop.yml +26 -0
- data/.tool-versions +2 -1
- data/CHANGELOG.md +65 -1
- data/README.md +96 -183
- data/Steepfile +2 -0
- data/docs/.gitignore +4 -0
- data/docs/.vitepress/config.mjs +112 -0
- data/docs/README.md +44 -0
- data/docs/api-examples.md +49 -0
- data/docs/bun.lockb +0 -0
- data/docs/common-use-cases/bech32-encoding-and-decoding-(NIP-19).md +190 -0
- data/docs/core/client.md +108 -0
- data/docs/core/keys.md +136 -0
- data/docs/core/user.md +43 -0
- data/docs/events/contact-list.md +29 -0
- data/docs/events/encrypted-direct-message.md +28 -0
- data/docs/events/recommend-server.md +32 -0
- data/docs/events/set-metadata.md +20 -0
- data/docs/events/text-note.md +15 -0
- data/docs/events.md +11 -0
- data/docs/getting-started/installation.md +21 -0
- data/docs/getting-started/overview.md +170 -0
- data/docs/implemented-nips.md +9 -0
- data/docs/index.md +44 -0
- data/docs/markdown-examples.md +85 -0
- data/docs/package.json +12 -0
- data/docs/relays/connecting-to-a-relay.md +21 -0
- data/docs/relays/publishing-events.md +29 -0
- data/docs/relays/receiving-events.md +6 -0
- data/docs/subscriptions/creating-a-subscription.md +49 -0
- data/docs/subscriptions/deleting-a-subscription.md +10 -0
- data/docs/subscriptions/filtering-subscription-events.md +115 -0
- data/docs/subscriptions/updating-a-subscription.md +4 -0
- data/lib/nostr/bech32.rb +203 -0
- data/lib/nostr/client.rb +2 -1
- data/lib/nostr/crypto.rb +147 -0
- data/lib/nostr/errors/error.rb +7 -0
- data/lib/nostr/errors/invalid_hrp_error.rb +21 -0
- data/lib/nostr/errors/invalid_key_format_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_length_error.rb +20 -0
- data/lib/nostr/errors/invalid_key_type_error.rb +18 -0
- data/lib/nostr/errors/key_validation_error.rb +6 -0
- data/lib/nostr/errors.rb +8 -0
- data/lib/nostr/event.rb +157 -12
- data/lib/nostr/event_kind.rb +8 -0
- data/lib/nostr/events/encrypted_direct_message.rb +54 -0
- data/lib/nostr/filter.rb +4 -4
- data/lib/nostr/key.rb +100 -0
- data/lib/nostr/key_pair.rb +30 -6
- data/lib/nostr/keygen.rb +43 -4
- data/lib/nostr/private_key.rb +36 -0
- data/lib/nostr/public_key.rb +36 -0
- data/lib/nostr/relay_message_type.rb +18 -0
- data/lib/nostr/subscription.rb +2 -2
- data/lib/nostr/user.rb +17 -36
- data/lib/nostr/version.rb +1 -1
- data/lib/nostr.rb +8 -1
- data/nostr.gemspec +9 -9
- data/sig/nostr/bech32.rbs +14 -0
- data/sig/nostr/client.rbs +5 -5
- data/sig/nostr/crypto.rbs +16 -0
- data/sig/nostr/errors/error.rbs +4 -0
- data/sig/nostr/errors/invalid_hrb_error.rbs +6 -0
- data/sig/nostr/errors/invalid_key_format_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_length_error.rbs +5 -0
- data/sig/nostr/errors/invalid_key_type_error.rbs +5 -0
- data/sig/nostr/errors/key_validation_error.rbs +4 -0
- data/sig/nostr/event.rbs +24 -9
- data/sig/nostr/event_kind.rbs +1 -0
- data/sig/nostr/events/encrypted_direct_message.rbs +12 -0
- data/sig/nostr/filter.rbs +3 -12
- data/sig/nostr/key.rbs +16 -0
- data/sig/nostr/key_pair.rbs +7 -3
- data/sig/nostr/keygen.rbs +5 -2
- data/sig/nostr/private_key.rbs +4 -0
- data/sig/nostr/public_key.rbs +4 -0
- data/sig/nostr/relay_message_type.rbs +8 -0
- data/sig/nostr/user.rbs +4 -10
- data/sig/vendor/bech32/nostr/entity.rbs +41 -0
- data/sig/vendor/bech32/nostr/nip19.rbs +20 -0
- data/sig/vendor/bech32/segwit_addr.rbs +21 -0
- data/sig/vendor/bech32.rbs +25 -0
- data/sig/vendor/event_emitter.rbs +10 -3
- data/sig/vendor/event_machine/channel.rbs +1 -1
- data/sig/vendor/faye/websocket/api.rbs +45 -0
- data/sig/vendor/faye/websocket/client.rbs +43 -0
- data/sig/vendor/faye/websocket.rbs +30 -0
- metadata +83 -23
- data/lib/nostr/event_fragment.rb +0 -111
- data/sig/nostr/event_fragment.rbs +0 -12
@@ -0,0 +1,49 @@
|
|
1
|
+
---
|
2
|
+
outline: deep
|
3
|
+
---
|
4
|
+
|
5
|
+
# Runtime API Examples
|
6
|
+
|
7
|
+
This page demonstrates usage of some of the runtime APIs provided by VitePress.
|
8
|
+
|
9
|
+
The main `useData()` API can be used to access site, theme, and page data for the current page. It works in both `.md` and `.vue` files:
|
10
|
+
|
11
|
+
```md
|
12
|
+
<script setup>
|
13
|
+
import { useData } from 'vitepress'
|
14
|
+
|
15
|
+
const { theme, page, frontmatter } = useData()
|
16
|
+
</script>
|
17
|
+
|
18
|
+
## Results
|
19
|
+
|
20
|
+
### Theme Data
|
21
|
+
<pre>{{ theme }}</pre>
|
22
|
+
|
23
|
+
### Page Data
|
24
|
+
<pre>{{ page }}</pre>
|
25
|
+
|
26
|
+
### Page Frontmatter
|
27
|
+
<pre>{{ frontmatter }}</pre>
|
28
|
+
```
|
29
|
+
|
30
|
+
<script setup>
|
31
|
+
import { useData } from 'vitepress'
|
32
|
+
|
33
|
+
const { site, theme, page, frontmatter } = useData()
|
34
|
+
</script>
|
35
|
+
|
36
|
+
## Results
|
37
|
+
|
38
|
+
### Theme Data
|
39
|
+
<pre>{{ theme }}</pre>
|
40
|
+
|
41
|
+
### Page Data
|
42
|
+
<pre>{{ page }}</pre>
|
43
|
+
|
44
|
+
### Page Frontmatter
|
45
|
+
<pre>{{ frontmatter }}</pre>
|
46
|
+
|
47
|
+
## More
|
48
|
+
|
49
|
+
Check out the documentation for the [full list of runtime APIs](https://vitepress.dev/reference/runtime-api#usedata).
|
data/docs/bun.lockb
ADDED
Binary file
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# Encoding/decoding bech-32 strings (NIP-19)
|
2
|
+
|
3
|
+
[NIP-19](https://github.com/nostr-protocol/nips/blob/master/19.md) standardizes bech32-formatted strings that can be
|
4
|
+
used to display keys, ids and other information in clients. These formats are not meant to be used anywhere in the core
|
5
|
+
protocol, they are only meant for displaying to users, copy-pasting, sharing, rendering QR codes and inputting data.
|
6
|
+
|
7
|
+
|
8
|
+
In order to guarantee the deterministic nature of the documentation, the examples below assume that there is a `keypair`
|
9
|
+
variable with the following values:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
keypair = Nostr::KeyPair.new(
|
13
|
+
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
14
|
+
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
15
|
+
)
|
16
|
+
|
17
|
+
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
18
|
+
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
19
|
+
```
|
20
|
+
|
21
|
+
## Public key (npub)
|
22
|
+
|
23
|
+
### Encoding
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
npub = Nostr::Bech32.npub_encode('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e')
|
27
|
+
npub # => 'npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'
|
28
|
+
```
|
29
|
+
|
30
|
+
### Decoding
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
type, public_key = Nostr::Bech32.decode('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg')
|
34
|
+
type # => 'npub'
|
35
|
+
public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
36
|
+
```
|
37
|
+
|
38
|
+
## Private key (nsec)
|
39
|
+
|
40
|
+
### Encoding
|
41
|
+
|
42
|
+
```ruby
|
43
|
+
nsec = Nostr::Bech32.nsec_encode('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
|
44
|
+
nsec # => 'nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'
|
45
|
+
```
|
46
|
+
|
47
|
+
### Decoding
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
type, private_key = Nostr::Bech32.decode('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5')
|
51
|
+
type # => 'npub'
|
52
|
+
private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
53
|
+
```
|
54
|
+
|
55
|
+
## Relay (nrelay)
|
56
|
+
|
57
|
+
### Encoding
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
nrelay = Nostr::Bech32.nrelay_encode('wss://relay.damus.io')
|
61
|
+
nrelay # => 'nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x'
|
62
|
+
```
|
63
|
+
|
64
|
+
### Decoding
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
type, data = Nostr::Bech32.decode('nrelay1qq28wumn8ghj7un9d3shjtnyv9kh2uewd9hsc5zt2x')
|
68
|
+
|
69
|
+
type # => 'nrelay'
|
70
|
+
data.entries.first.label # => 'relay'
|
71
|
+
data.entries.first.value # => 'wss://relay.damus.io'
|
72
|
+
```
|
73
|
+
|
74
|
+
## Event (nevent)
|
75
|
+
|
76
|
+
### Encoding
|
77
|
+
|
78
|
+
```ruby{8-12}
|
79
|
+
user = Nostr::User.new(keypair: keypair)
|
80
|
+
text_note_event = user.create_event(
|
81
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
82
|
+
created_at: 1700467997,
|
83
|
+
content: 'Your feedback is appreciated, now pay $8'
|
84
|
+
)
|
85
|
+
|
86
|
+
nevent = Nostr::Bech32.nevent_encode(
|
87
|
+
id: text_note_event.id,
|
88
|
+
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
89
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
90
|
+
)
|
91
|
+
|
92
|
+
nevent # => 'nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0praxwkjagcpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqs03k8v3'
|
93
|
+
```
|
94
|
+
|
95
|
+
### Decoding
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
type, event = Nostr::Bech32.decode('nevent1qgsqlkuslr3rf56qpmd0m5ndfyl39m7q6l0zcmuly8ue0praxwkjagcpz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqs03k8v3')
|
99
|
+
|
100
|
+
type # => 'nevent'
|
101
|
+
event.entries[0].label # => 'author'
|
102
|
+
event.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
103
|
+
event.entries[1].relay # => 'relay'
|
104
|
+
event.entries[1].value # => 'wss://relay.damus.io'
|
105
|
+
event.entries[2].label # => 'relay'
|
106
|
+
event.entries[2].value # => 'wss://nos.lol'
|
107
|
+
event.entries[3].label # => 'kind'
|
108
|
+
event.entries[3].value # => 1
|
109
|
+
```
|
110
|
+
|
111
|
+
## Address (naddr)
|
112
|
+
|
113
|
+
### Encoding
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
naddr = Nostr::Bech32.naddr_encode(
|
117
|
+
pubkey: keypair.public_key,
|
118
|
+
relays: ['wss://relay.damus.io', 'wss://nos.lol'],
|
119
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
120
|
+
identifier: 'damus',
|
121
|
+
)
|
122
|
+
|
123
|
+
naddr # => 'naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqsqptyv9kh2uc3qfs2p'
|
124
|
+
```
|
125
|
+
|
126
|
+
### Decoding
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
type, addr = Nostr::Bech32.decode('naddr1qgs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dspsgqqqqqqsqptyv9kh2uc3qfs2p')
|
130
|
+
|
131
|
+
type # => 'naddr'
|
132
|
+
addr.entries[0].label # => 'author'
|
133
|
+
addr.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
134
|
+
addr.entries[1].label # => 'relay'
|
135
|
+
addr.entries[1].value # => 'wss://relay.damus.io'
|
136
|
+
addr.entries[2].label # => 'relay'
|
137
|
+
addr.entries[2].value # => 'wss://nos.lol'
|
138
|
+
addr.entries[3].label # => 'kind'
|
139
|
+
addr.entries[3].value # => 1
|
140
|
+
addr.entries[4].label # => 'identifier'
|
141
|
+
addr.entries[4].value # => 'damus'
|
142
|
+
```
|
143
|
+
|
144
|
+
## Profile (nprofile)
|
145
|
+
|
146
|
+
### Encoding
|
147
|
+
```ruby
|
148
|
+
relay_urls = %w[wss://relay.damus.io wss://nos.lol]
|
149
|
+
nprofile = Nostr::Bech32.nprofile_encode(pubkey: keypair.public_key, relays: relay_urls)
|
150
|
+
|
151
|
+
nprofile # => nprofile1qqs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsxe58m5
|
152
|
+
```
|
153
|
+
|
154
|
+
### Decoding
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
type, profile = Nostr::Bech32.decode('nprofile1qqs8ul5ug253hlh3n75jne0a5xmjur4urfxpzst88cnegg6ds6ka7nspz3mhxue69uhhyetvv9ujuerpd46hxtnfduqs6amnwvaz7tmwdaejumr0dsxe58m5')
|
158
|
+
|
159
|
+
type # => 'nprofile'
|
160
|
+
profile.entries[0].label # => 'pubkey'
|
161
|
+
profile.entries[0].value # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
162
|
+
profile.entries[1].label # => 'relay'
|
163
|
+
profile.entries[1].value # => 'wss://relay.damus.io'
|
164
|
+
profile.entries[2].label # => 'relay'
|
165
|
+
profile.entries[2].value # => 'wss://nos.lol'
|
166
|
+
```
|
167
|
+
|
168
|
+
## Other simple types (note)
|
169
|
+
|
170
|
+
### Encoding
|
171
|
+
|
172
|
+
```ruby{8-9}
|
173
|
+
user = Nostr::User.new(keypair: keypair)
|
174
|
+
text_note_event = user.create_event(
|
175
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
176
|
+
created_at: 1700467997,
|
177
|
+
content: 'Your feedback is appreciated, now pay $8'
|
178
|
+
)
|
179
|
+
|
180
|
+
note = Nostr::Bech32.encode(hrp: 'note', data: text_note_event.id)
|
181
|
+
note # => 'note10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qnx3ujq'
|
182
|
+
```
|
183
|
+
|
184
|
+
### Decoding
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
type, note = Nostr::Bech32.decode('note1pldep78zxnf5qrk6lhfx6jflzthup47793he7g0ej7z86vad963s42v0rr')
|
188
|
+
type # => 'note'
|
189
|
+
note # => '0fdb90f8e234d3400edafdd26d493f12efc0d7de2c6f9f21f997847d33ad2ea3'
|
190
|
+
```
|
data/docs/core/client.md
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
# Client
|
2
|
+
|
3
|
+
Clients establish a WebSocket connection to [relays](../relays/connecting-to-a-relay). Through this connection, clients
|
4
|
+
communicate and subscribe to a range of [Nostr events](../events) based on specified subscription filters. These filters
|
5
|
+
define the Nostr events a client wishes to receive updates about.
|
6
|
+
|
7
|
+
::: info
|
8
|
+
Clients do not need to sign up or create an account to use Nostr. Upon connecting to a relay, a client provides
|
9
|
+
its subscription filters. The relay then streams events that match these filters to the client for the duration of the
|
10
|
+
connection.
|
11
|
+
:::
|
12
|
+
|
13
|
+
## WebSocket events
|
14
|
+
|
15
|
+
Communication between clients and relays happen via WebSockets. The client will emit WebSocket events when the
|
16
|
+
connection is __opened__, __closed__, when a __message__ is received or when there's an __error__.
|
17
|
+
|
18
|
+
::: info
|
19
|
+
WebSocket events are not [Nostr events](https://nostr.com/the-protocol/events). They are events emitted by the
|
20
|
+
WebSocket connection. The WebSocket `:message` event, however, contains a Nostr event in its payload.
|
21
|
+
:::
|
22
|
+
|
23
|
+
### connect
|
24
|
+
|
25
|
+
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
|
26
|
+
|
27
|
+
```ruby
|
28
|
+
client = Nostr::Client.new
|
29
|
+
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
|
30
|
+
|
31
|
+
client.on :connect do
|
32
|
+
# When this block executes, you're connected to the relay
|
33
|
+
end
|
34
|
+
|
35
|
+
# Connect to a relay asynchronously
|
36
|
+
client.connect(relay)
|
37
|
+
```
|
38
|
+
|
39
|
+
Once the connection is open, you can send events to the relay, manage subscriptions, etc.
|
40
|
+
|
41
|
+
::: tip
|
42
|
+
Define the connection event handler before calling
|
43
|
+
[`Nostr::Client#connect`](https://www.rubydoc.info/gems/nostr/Nostr/Client#connect-instance_method). Otherwise,
|
44
|
+
you may miss the event.
|
45
|
+
:::
|
46
|
+
|
47
|
+
### error
|
48
|
+
|
49
|
+
The `:error` event is fired when a connection with a WebSocket has been closed because of an error.
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
client.on :error do |error_message|
|
53
|
+
puts error_message
|
54
|
+
end
|
55
|
+
|
56
|
+
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the
|
57
|
+
# server certificate for 'rsslay.fiatjaf.com'
|
58
|
+
```
|
59
|
+
|
60
|
+
### message
|
61
|
+
|
62
|
+
The `:message` event is fired when data is received through a WebSocket.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
client.on :message do |message|
|
66
|
+
puts message
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
The message will be one of these 4 types, which must also be JSON arrays, according to the following patterns:
|
71
|
+
- `["EVENT", <subscription_id>, <event JSON>]`
|
72
|
+
- `["OK", <event_id>, <true|false>, <message>]`
|
73
|
+
- `["EOSE", <subscription_id>]`
|
74
|
+
- `["NOTICE", <message>]`
|
75
|
+
|
76
|
+
::: details Click me to see how a WebSocket message looks like
|
77
|
+
```ruby
|
78
|
+
[
|
79
|
+
"EVENT",
|
80
|
+
"d34107357089bfc9882146d3bfab0386",
|
81
|
+
{
|
82
|
+
"content": "",
|
83
|
+
"created_at": 1676456512,
|
84
|
+
"id": "18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
|
85
|
+
"kind": 3,
|
86
|
+
"pubkey": "117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
|
87
|
+
"sig": "d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
|
88
|
+
"tags":[
|
89
|
+
["p", "1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
|
90
|
+
]
|
91
|
+
}
|
92
|
+
]
|
93
|
+
```
|
94
|
+
:::
|
95
|
+
|
96
|
+
### close
|
97
|
+
|
98
|
+
The `:close` event is fired when a connection with a WebSocket is closed.
|
99
|
+
|
100
|
+
```ruby
|
101
|
+
client.on :close do |code, reason|
|
102
|
+
puts "Error: #{code} - #{reason}"
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
::: tip
|
107
|
+
This handler is useful to attempt to reconnect to the relay.
|
108
|
+
:::
|
data/docs/core/keys.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# Keys
|
2
|
+
|
3
|
+
To [sign events](#signing-an-event), you need a **private key**. To verify signatures, you need a **public key**. The combination of a
|
4
|
+
private and a public key is called a **keypair**.
|
5
|
+
|
6
|
+
Both public and private keys are 64-character hexadecimal strings. They can be represented in bech32 format,
|
7
|
+
which is a human-readable format that starts with `nsec` for private keys and `npub` for public keys.
|
8
|
+
|
9
|
+
There are a few ways to generate a keypair.
|
10
|
+
|
11
|
+
## a) Generating a keypair
|
12
|
+
|
13
|
+
If you don't have any keys, you can generate a keypair using the
|
14
|
+
[`Nostr::Keygen`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen) class:
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
keygen = Nostr::Keygen.new
|
18
|
+
keypair = keygen.generate_key_pair
|
19
|
+
|
20
|
+
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
21
|
+
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
22
|
+
```
|
23
|
+
|
24
|
+
## b) Generating a private key and a public key
|
25
|
+
|
26
|
+
Alternatively, if you have already generated a private key, you can extract the corresponding public key by calling
|
27
|
+
`Keygen#extract_public_key`:
|
28
|
+
|
29
|
+
```ruby
|
30
|
+
keygen = Nostr::Keygen.new
|
31
|
+
|
32
|
+
private_key = keygen.generate_private_key
|
33
|
+
public_key = keygen.extract_public_key(private_key)
|
34
|
+
|
35
|
+
private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
36
|
+
public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
37
|
+
```
|
38
|
+
|
39
|
+
## c) Using existing hexadecimal keys
|
40
|
+
|
41
|
+
If you already have a private key and a public key in hexadecimal format, you can create a keypair using the
|
42
|
+
`Nostr::KeyPair` class:
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
keypair = Nostr::KeyPair.new(
|
46
|
+
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
47
|
+
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
48
|
+
)
|
49
|
+
|
50
|
+
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
51
|
+
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
52
|
+
```
|
53
|
+
|
54
|
+
### d) Use existing bech32 keys
|
55
|
+
|
56
|
+
If you already have a private key and a public key in bech32 format, you can create a keypair using the
|
57
|
+
`Nostr::KeyPair` class:
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
keypair = Nostr::KeyPair.new(
|
61
|
+
private_key: Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5'),
|
62
|
+
public_key: Nostr::PublicKey.from_bech32('npub10elfcs4fr0l0r8af98jlmgdh9c8tcxjvz9qkw038js35mp4dma8qzvjptg'),
|
63
|
+
)
|
64
|
+
|
65
|
+
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
66
|
+
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
67
|
+
```
|
68
|
+
|
69
|
+
## e) Using an existing hexadecimal private key
|
70
|
+
|
71
|
+
If you already have a private key in hexadecimal format, you can create a keypair using the method
|
72
|
+
[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method):
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
private_key = Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa')
|
76
|
+
|
77
|
+
keygen= Nostr::Keygen.new
|
78
|
+
keypair = keygen.get_key_pair_from_private_key(private_key)
|
79
|
+
|
80
|
+
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
81
|
+
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
82
|
+
```
|
83
|
+
|
84
|
+
## f) Using an existing bech32 private key
|
85
|
+
|
86
|
+
If you already have a private key in bech32 format, you can create a keypair using the methods
|
87
|
+
[`Nostr::PrivateKey.from_bech32`](https://www.rubydoc.info/gems/nostr/Nostr/PrivateKey.from_bech32-class_method) and
|
88
|
+
[`Nostr::Keygen#get_key_pair_from_private_key`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen#get_key_pair_from_private_key-instance_method):
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
private_key = Nostr::PrivateKey.from_bech32('nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5')
|
92
|
+
|
93
|
+
keygen= Nostr::Keygen.new
|
94
|
+
keypair = keygen.get_key_pair_from_private_key(private_key)
|
95
|
+
|
96
|
+
keypair.private_key # => '67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'
|
97
|
+
keypair.public_key # => '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'
|
98
|
+
```
|
99
|
+
|
100
|
+
## Signing an event
|
101
|
+
|
102
|
+
KeyPairs are used to sign [events](../events). To create a signed event, you need to instantiate a
|
103
|
+
[`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) with a keypair:
|
104
|
+
|
105
|
+
```ruby{8,11-14}
|
106
|
+
# Use an existing keypair
|
107
|
+
keypair = Nostr::KeyPair.new(
|
108
|
+
private_key: Nostr::PrivateKey.new('67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa'),
|
109
|
+
public_key: Nostr::PublicKey.new('7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e'),
|
110
|
+
)
|
111
|
+
|
112
|
+
# Add the keypair to a user
|
113
|
+
user = Nostr::User.new(keypair: keypair)
|
114
|
+
|
115
|
+
# Create signed events
|
116
|
+
text_note = user.create_event(
|
117
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
118
|
+
content: 'Your feedback is appreciated, now pay $8'
|
119
|
+
)
|
120
|
+
```
|
121
|
+
|
122
|
+
::: details Click me to view the text_note
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
# text_note.to_h
|
126
|
+
{
|
127
|
+
id: '030fbc71151379e5b58e7428ed6e7f2884e5dfc9087fd64d1dc4cc677f5097c8',
|
128
|
+
pubkey: '7e7e9c42a91bfef19fa929e5fda1b72e0ebc1a4c1141673e2794234d86addf4e', # from the keypair
|
129
|
+
created_at: 1700119819,
|
130
|
+
kind: 1, # Nostr::EventKind::TEXT_NOTE,
|
131
|
+
tags: [],
|
132
|
+
content: 'Your feedback is appreciated, now pay $8',
|
133
|
+
sig: '586877896ef6f7d54fa4dd2ade04e3fdc4dfcd6166dd0df696b3c3c768868c0b690338f5baed6ab4fc717785333cb487363384de9fb0f740ac4775522cb4acb3' # signed with the private key from the keypair
|
134
|
+
}
|
135
|
+
```
|
136
|
+
:::
|
data/docs/core/user.md
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
# User
|
2
|
+
|
3
|
+
The class [`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) is an abstraction to facilitate the creation
|
4
|
+
of signed events. It is not required to use it to create events, but it is recommended.
|
5
|
+
|
6
|
+
Here's an example of how to create a signed event without the class `Nostr::User`:
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
event = Nostr::Event.new(
|
10
|
+
pubkey: keypair.public_key,
|
11
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
12
|
+
tags: [],
|
13
|
+
content: 'Your feedback is appreciated, now pay $8',
|
14
|
+
)
|
15
|
+
event.sign(keypair.private_key)
|
16
|
+
```
|
17
|
+
|
18
|
+
::: details Click me to view the event
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# event.to_h
|
22
|
+
{
|
23
|
+
id: '5feb10973dbcf5f210cfc1f0aa338fee62bed6a29696a67957713599b9baf0eb',
|
24
|
+
pubkey: 'b9b9821074d1b60b8fb4a3983632af3ef9669f55b20d515bf982cda5c439ad61',
|
25
|
+
created_at: 1699847447,
|
26
|
+
kind: 1, # Nostr::EventKind::TEXT_NOTE,
|
27
|
+
tags: [],
|
28
|
+
content: 'Your feedback is appreciated, now pay $8',
|
29
|
+
sig: 'e30f2f08331f224e41a4099d16aefc780bf9f2d1191b71777e1e1789e6b51fdf7bb956f25d4ea9a152d1c66717a9d68c081ce6c89c298c3c5e794914013381ab'
|
30
|
+
}
|
31
|
+
```
|
32
|
+
:::
|
33
|
+
|
34
|
+
And here's how to create it with the class `Nostr::User`:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
user = Nostr::User.new(keypair: keypair)
|
38
|
+
|
39
|
+
event = user.create_event(
|
40
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
41
|
+
content: 'Your feedback is appreciated, now pay $8'
|
42
|
+
)
|
43
|
+
```
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Contact List
|
2
|
+
|
3
|
+
## Creating/updating your contact list
|
4
|
+
|
5
|
+
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
# Creating a contact list event with 2 contacts
|
9
|
+
update_contacts_event = user.create_event(
|
10
|
+
kind: Nostr::EventKind::CONTACT_LIST,
|
11
|
+
tags: [
|
12
|
+
[
|
13
|
+
"p", # mandatory
|
14
|
+
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
|
15
|
+
"wss://alicerelay.com/", # can be an empty string or can be omitted
|
16
|
+
"alice" # can be an empty string or can be omitted
|
17
|
+
],
|
18
|
+
[
|
19
|
+
"p", # mandatory
|
20
|
+
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
|
21
|
+
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
|
22
|
+
"bob" # can be an empty string or can be omitted
|
23
|
+
],
|
24
|
+
],
|
25
|
+
)
|
26
|
+
|
27
|
+
# Send it to the Relay
|
28
|
+
client.publish(update_contacts_event)
|
29
|
+
```
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# Encrypted Direct Message
|
2
|
+
|
3
|
+
## Sending an encrypted direct message
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
|
7
|
+
|
8
|
+
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
|
9
|
+
sender_private_key: sender_private_key,
|
10
|
+
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
|
11
|
+
plain_text: 'Your feedback is appreciated, now pay $8',
|
12
|
+
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
|
13
|
+
)
|
14
|
+
|
15
|
+
encrypted_direct_message.sign(sender_private_key)
|
16
|
+
|
17
|
+
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
|
18
|
+
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
|
19
|
+
# @created_at=1676456512,
|
20
|
+
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
|
21
|
+
# @kind=4,
|
22
|
+
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
|
23
|
+
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
|
24
|
+
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
|
25
|
+
|
26
|
+
# Send it to the Relay
|
27
|
+
client.publish(encrypted_direct_message)
|
28
|
+
```
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# Recommend Server
|
2
|
+
|
3
|
+
The `Recommend Server` event, has a set of tags with the following structure `['e', <event-id>, <relay-url>, <marker>]`
|
4
|
+
|
5
|
+
Where:
|
6
|
+
|
7
|
+
- `<event-id>` is the id of the event being referenced.
|
8
|
+
- `<relay-url>` is the URL of a recommended relay associated with the reference. Clients SHOULD add a valid `<relay-URL>`
|
9
|
+
field, but may instead leave it as `''`.
|
10
|
+
- `<marker>` is optional and if present is one of `'reply'`, `'root'`, or `'mention'`.
|
11
|
+
Those marked with `'reply'` denote the id of the reply event being responded to. Those marked with `'root'` denote the
|
12
|
+
root id of the reply thread being responded to. For top level replies (those replying directly to the root event),
|
13
|
+
only the `'root'` marker should be used. Those marked with `'mention'` denote a quoted or reposted event id.
|
14
|
+
|
15
|
+
A direct reply to the root of a thread should have a single marked `'e'` tag of type `'root'`.
|
16
|
+
|
17
|
+
## Recommending a server
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
recommend_server_event = user.create_event(
|
21
|
+
kind: Nostr::EventKind::RECOMMEND_SERVER,
|
22
|
+
tags: [
|
23
|
+
[
|
24
|
+
'e',
|
25
|
+
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
|
26
|
+
'wss://relay.damus.io'
|
27
|
+
],
|
28
|
+
]
|
29
|
+
)
|
30
|
+
|
31
|
+
client.publish(recommend_server_event)
|
32
|
+
```
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# Set Metadata
|
2
|
+
|
3
|
+
In the `Metadata` event, the `content` is set to a stringified JSON object
|
4
|
+
`{name: <username>, about: <string>, picture: <url, string>}` describing the [user](../core/user) who created the event. A relay may
|
5
|
+
delete older events once it gets a new one for the same pubkey.
|
6
|
+
|
7
|
+
## Setting the user's metadata
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
metadata_event = user.create_event(
|
11
|
+
kind: Nostr::EventKind::SET_METADATA,
|
12
|
+
content: {
|
13
|
+
name: 'Wilson Silva',
|
14
|
+
about: 'Used to make hydrochloric acid bombs in high school.',
|
15
|
+
picture: 'https://thispersondoesnotexist.com/'
|
16
|
+
}
|
17
|
+
)
|
18
|
+
|
19
|
+
client.publish(metadata_event)
|
20
|
+
```
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Text Note
|
2
|
+
|
3
|
+
In the `Text Note` event, the `content` is set to the plaintext content of a note (anything the user wants to say).
|
4
|
+
Content that must be parsed, such as Markdown and HTML, should not be used.
|
5
|
+
|
6
|
+
## Sending a text note event
|
7
|
+
|
8
|
+
```ruby
|
9
|
+
text_note_event = user.create_event(
|
10
|
+
kind: Nostr::EventKind::TEXT_NOTE,
|
11
|
+
content: 'Your feedback is appreciated, now pay $8'
|
12
|
+
)
|
13
|
+
|
14
|
+
client.publish(text_note_event)
|
15
|
+
```
|
data/docs/events.md
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# Events
|
2
|
+
|
3
|
+
## Event Kinds
|
4
|
+
|
5
|
+
| kind | description | NIP |
|
6
|
+
| ------------- |----------------------------------------------------------------| -------------------------------------------------------------- |
|
7
|
+
| `0` | [Metadata](./events/set-metadata) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
|
8
|
+
| `1` | [Short Text Note](./events/text-note) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
|
9
|
+
| `2` | [Recommend Relay](./events/recommend-server) | |
|
10
|
+
| `3` | [Contacts](./events/contact-list) | [2](https://github.com/nostr-protocol/nips/blob/master/02.md) |
|
11
|
+
| `4` | [Encrypted Direct Messages](./events/encrypted-direct-message) | [4](https://github.com/nostr-protocol/nips/blob/master/04.md) |
|