Add high-level documentation

https://nostr-ruby.com
This commit is contained in:
Wilson Silva 2023-11-13 17:42:07 +07:00
parent b206f6504e
commit d49fac49b6
No known key found for this signature in database
GPG Key ID: 65135F94E23F82C8
23 changed files with 1014 additions and 256 deletions

View File

@ -6,6 +6,6 @@ max_line_length = 120
trim_trailing_whitespace = true trim_trailing_whitespace = true
# 2 space indentation # 2 space indentation
[*.rb] [{*.rb, *.mjs}]
indent_style = space indent_style = space
indent_size = 2 indent_size = 2

320
README.md
View File

@ -4,31 +4,30 @@
[![Maintainability](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/maintainability)](https://codeclimate.com/github/wilsonsilva/nostr/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/maintainability)](https://codeclimate.com/github/wilsonsilva/nostr/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/test_coverage)](https://codeclimate.com/github/wilsonsilva/nostr/test_coverage) [![Test Coverage](https://api.codeclimate.com/v1/badges/c7633eb2c89eb95ee7f2/test_coverage)](https://codeclimate.com/github/wilsonsilva/nostr/test_coverage)
Asynchronous Nostr client. Please note that the API is likely to change as the gem is still in development and Asynchronous Nostr client for Rubyists.
has not yet reached a stable release. Use with caution.
## Table of contents ## Table of contents
- [Installation](#installation) - [Key features](#-key-features)
- [Usage](#usage) - [Installation](#-installation)
* [Requiring the gem](#requiring-the-gem) - [Quickstart](#-quickstart)
* [Generating a keypair](#generating-a-keypair) - [Documentation](#-documentation)
* [Generating a private key and a public key](#generating-a-private-key-and-a-public-key) - [Implemented NIPs](#-implemented-nips)
* [Connecting to a Relay](#connecting-to-a-relay) - [Development](#-development)
* [WebSocket events](#websocket-events)
* [Requesting for events / creating a subscription](#requesting-for-events--creating-a-subscription)
* [Stop previous subscriptions](#stop-previous-subscriptions)
* [Publishing an event](#publishing-an-event)
* [Creating/updating your contact list](#creatingupdating-your-contact-list)
* [Sending an encrypted direct message](#sending-an-encrypted-direct-message)
- [Implemented NIPs](#implemented-nips)
- [Development](#development)
* [Type checking](#type-checking) * [Type checking](#type-checking)
- [Contributing](#contributing) - [Contributing](#-contributing)
- [License](#license) - [License](#-license)
- [Code of Conduct](#code-of-conduct) - [Code of Conduct](#-code-of-conduct)
## Installation ## 🔑 Key features
- Asynchronous
- Easy to use
- Fully documented
- Fully tested
- Fully typed
## 📦 Installation
Install the gem and add to the application's Gemfile by executing: Install the gem and add to the application's Gemfile by executing:
@ -38,248 +37,96 @@ If bundler is not being used to manage dependencies, install the gem by executin
$ gem install nostr $ gem install nostr
## Usage ## ⚡️ Quickstart
### Requiring the gem Here is a quick example of how to use the gem. For more detailed documentation, please check the
[documentation website](https://nostr-omega.vercel.app).
All examples below assume that the gem has been required.
```ruby ```ruby
# Require the gem
require 'nostr' require 'nostr'
```
### Generating a keypair # Instantiate a client
```ruby
keygen = Nostr::Keygen.new
keypair = keygen.generate_key_pair
keypair.private_key
keypair.public_key
```
### Generating a private key and a public key
```ruby
keygen = Nostr::Keygen.new
private_key = keygen.generate_private_key
public_key = keygen.extract_public_key(private_key)
```
### Connecting to a Relay
Clients can connect to multiple Relays. In this version, a Client can only connect to a single Relay at a time.
You may instantiate multiple Clients and multiple Relays.
```ruby
client = Nostr::Client.new client = Nostr::Client.new
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
# a) Use an existing keypair
keypair = Nostr::KeyPair.new(
private_key: 'add-your-private-key-here',
public_key: 'add-your-public-key-here',
)
# b) Or create a new keypair
keygen = Nostr::Keygen.new
keypair = keygen.generate_keypair
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
# Create a signed event
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
# Connect asynchronously to a relay
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
client.connect(relay) client.connect(relay)
```
### WebSocket events # Listen asynchronously for the connect event
All communication between clients and relays happen in WebSockets.
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
```ruby
client.on :connect do client.on :connect do
# all the code goes here # Send the event to the Relay
end client.publish(text_note_event)
```
The `:close` event is fired when a connection with a WebSocket has been closed because of an error. # Create a filter to receive the first 20 text notes
# and encrypted direct messages from the relay that
# were created in the previous hour
filter = Nostr::Filter.new(
kinds: [
Nostr::EventKind::TEXT_NOTE,
Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE
],
since: Time.now.to_i - 3600, # 1 hour ago
until: Time.now.to_i,
limit: 20,
)
```ruby # Subscribe to events matching conditions of a filter
client.on :error do |error_message| subscription = client.subscribe(filter)
puts error_message
# Unsubscribe from events matching the filter above
client.unsubscribe(subscription.id)
end end
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the server certificate for 'rsslay.fiatjaf.com' # Listen for incoming messages and print them
```
The `:message` event is fired when data is received through a WebSocket.
```ruby
client.on :message do |message| client.on :message do |message|
puts message puts message
end end
# [ # Listen for error messages
# "EVENT", client.on :error do |error_message|
# "d34107357089bfc9882146d3bfab0386", # Handle the error
# { end
# "content":"",
# "created_at":1676456512,
# "id":"18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
# "kind":3,
# "pubkey":"117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
# "sig":"d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
# "tags":[
# ["p","1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
# ]
# }
# ]
```
The `:close` event is fired when a connection with a WebSocket is closed. # Listen for the close event
```ruby
client.on :close do |code, reason| client.on :close do |code, reason|
# you may attempt to reconnect # You may attempt to reconnect to the relay here
client.connect(relay)
end end
``` ```
### Requesting for events / creating a subscription ## 📚 Documentation
A client can request events and subscribe to new updates after it has established a connection with the Relay. I made a detailed documentation for this gem and it's usage. The code is also fully documented using YARD.
You may use a `Nostr::Filter` instance with as many attributes as you wish: - [Guide documentation](https://nostr-omega.vercel.app)
- [YARD documentation](https://rubydoc.info/gems/nostr)
```ruby ## ✅ Implemented NIPs
client.on :connect do
filter = Nostr::Filter.new(
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
kinds: [Nostr::EventKind::TEXT_NOTE],
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
since: 1230981305,
until: 1292190341,
limit: 420,
)
subscription = client.subscribe('a_random_subscription_id', filter)
end
```
With just a few:
```ruby
client.on :connect do
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
subscription = client.subscribe('a_random_subscription_id', filter)
end
```
Or omit the filter:
```ruby
client.on :connect do
subscription = client.subscribe('a_random_subscription_id')
end
```
Or even omit the subscription id:
```ruby
client.on :connect do
subscription = client.subscribe('a_random_subscription_id')
end
```
### Stop previous subscriptions
You can stop receiving messages from a subscription by calling `#unsubscribe`:
```ruby
client.unsubscribe('your_subscription_id')
```
### Publishing an event
To publish an event you need a keypair.
```ruby
# Create a keypair
keypair = Nostr::KeyPair.new(
private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
)
# Add the keypair to the user facade
user = Nostr::User.new(keypair: keypair)
# Create a signed event
event = user.create_event(
created_at: 1667422587, # optional, defaults to the current time
kind: Nostr::EventKind::TEXT_NOTE,
tags: [], # optional, defaults to []
content: 'Your feedback is appreciated, now pay $8'
)
# Send it to the Relay
client.publish(event)
```
### Creating/updating your contact list
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
```ruby
# Creating a contact list event with 2 contacts
update_contacts_event = user.create_event(
kind: Nostr::EventKind::CONTACT_LIST,
tags: [
[
"p", # mandatory
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
"wss://alicerelay.com/", # can be an empty string or can be omitted
"alice" # can be an empty string or can be omitted
],
[
"p", # mandatory
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
"bob" # can be an empty string or can be omitted
],
],
)
# Send it to the Relay
client.publish(update_contacts_event)
```
### Sending an encrypted direct message
```ruby
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
sender_private_key: sender_private_key,
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
plain_text: 'Your feedback is appreciated, now pay $8',
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
)
encrypted_direct_message.sign(sender_private_key)
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
# @created_at=1676456512,
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
# @kind=4,
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
# Send it to the Relay
client.publish(encrypted_direct_message)
````
## Implemented NIPs
- [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md) - [x] [NIP-01 - Client](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [x] [NIP-02 - Client](https://github.com/nostr-protocol/nips/blob/master/02.md) - [x] [NIP-02 - Client](https://github.com/nostr-protocol/nips/blob/master/02.md)
- [x] [NIP-04 - Client](https://github.com/nostr-protocol/nips/blob/master/04.md) - [x] [NIP-04 - Client](https://github.com/nostr-protocol/nips/blob/master/04.md)
## Development ## 🔨 Development
After checking out the repo, run `bin/setup` to install dependencies. After checking out the repo, run `bin/setup` to install dependencies.
@ -323,17 +170,22 @@ used to provide type checking and autocompletion in your editor. Run `bundle exe
an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep) an RBS definition for the given Ruby file. And validate all definitions using [Steep](https://github.com/soutaro/steep)
with the command `bundle exec steep check`. with the command `bundle exec steep check`.
## Contributing ## 🐞 Issues & Bugs
If you find any issues or bugs, please report them [here](https://github.com/wilsonsilva/nostr/issues), I will be happy
to have a look at them and fix them as soon as possible.
## 🤝 Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr. Bug reports and pull requests are welcome on GitHub at https://github.com/wilsonsilva/nostr.
This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere
to the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md). to the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md).
## License ## 📜 License
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
## Code of Conduct ## 👔 Code of Conduct
Everyone interacting in the Nostr project's codebases, issue trackers, chat rooms and mailing lists is expected Everyone interacting in the Nostr project's codebases, issue trackers, chat rooms and mailing lists is expected
to follow the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md). to follow the [code of conduct](https://github.com/wilsonsilva/nostr/blob/main/CODE_OF_CONDUCT.md).

View File

@ -1,31 +1,106 @@
import { defineConfig } from 'vitepress' import { defineConfig } from 'vitepress'
import { withMermaid } from "vitepress-plugin-mermaid";
// https://vitepress.dev/reference/site-config // https://vitepress.dev/reference/site-config
export default defineConfig({ // https://www.npmjs.com/package/vitepress-plugin-mermaid
export default defineConfig(withMermaid({
title: "Nostr", title: "Nostr",
description: "Ruby gem documentation", description: "Documentation of the Nostr Ruby gem",
// https://vitepress.dev/reference/site-config#head
head: [
['link', { rel: 'icon', href: '/favicon.ico' }]
],
themeConfig: { themeConfig: {
// https://vitepress.dev/reference/default-theme-last-updated
lastUpdated: true,
// https://vitepress.dev/reference/default-theme-config // https://vitepress.dev/reference/default-theme-config
nav: [ nav: [
{ text: 'Home', link: '/' }, { text: 'Home', link: '/' },
{ text: 'Examples', link: '/markdown-examples' } { text: 'Guide', link: '/getting-started/overview' }
], ],
// https://vitepress.dev/reference/default-theme-search
search: {
provider: 'local'
},
// https://vitepress.dev/reference/default-theme-sidebar
sidebar: [ sidebar: [
{ {
text: 'Examples', text: 'Getting started',
link: '/getting-started',
collapsed: false,
items: [ items: [
{ text: 'Markdown Examples', link: '/markdown-examples' }, { text: 'Overview', link: '/getting-started/overview' },
{ text: 'Runtime API Examples', link: '/api-examples' } { text: 'Installation', link: '/getting-started/installation' },
] ]
} },
{
text: 'Core',
collapsed: false,
items: [
{ text: 'Client', link: '/core/client' },
{ text: 'Keys', link: '/core/keys' },
{ text: 'User', link: '/core/user' },
]
},
{
text: 'Relays',
items: [
{ text: 'Connecting to a relay', link: '/relays/connecting-to-a-relay' },
{ text: 'Publishing events', link: '/relays/publishing-events' },
{ text: 'Receiving events', link: '/relays/receiving-events' },
]
},
{
text: 'Subscriptions',
collapsed: false,
items: [
{ text: 'Creating a subscription', link: '/subscriptions/creating-a-subscription' },
{ text: 'Filtering subscription events', link: '/subscriptions/filtering-subscription-events' },
{ text: 'Updating a subscription', link: '/subscriptions/updating-a-subscription' },
{ text: 'Deleting a subscription', link: '/subscriptions/deleting-a-subscription' },
]
},
{
text: 'Events',
link: '/events',
collapsed: false,
items: [
{ text: 'Set Metadata', link: '/events/set-metadata' },
{ text: 'Text Note', link: '/events/text-note' },
{ text: 'Recommend Server', link: '/events/recommend-server' },
{ text: 'Contact List', link: '/events/contact-list' },
{ text: 'Encrypted Direct Message', link: '/events/encrypted-direct-message' },
]
},
{
text: 'Implemented NIPs',
link: '/implemented-nips',
},
], ],
// https://vitepress.dev/reference/default-theme-config#sociallinks
socialLinks: [ socialLinks: [
{ icon: 'github', link: 'https://github.com/vuejs/vitepress' } { icon: 'github', link: 'https://github.com/wilsonsilva/nostr' }
], ],
// https://vitepress.dev/reference/default-theme-edit-link
editLink: {
pattern: 'https://github.com/wilsonsilva/nostr/edit/main/docs/:path',
text: 'Edit this page on GitHub'
},
// https://vitepress.dev/reference/default-theme-footer
footer: {
message: 'Released under the <a href="https://github.com/wilsonsilva/nostr/blob/main/LICENSE.txt">MIT License</a>.',
copyright: 'Copyright © 2023-present <a href="https://github.com/wilsonsilva">Wilson Silva</a>'
}
}, },
// https://vitepress.dev/reference/site-config#ignoredeadlinks
ignoreDeadLinks: [ ignoreDeadLinks: [
/^https?:\/\/localhost/ /^https?:\/\/localhost/
], ],
}) }))

108
docs/core/client.md Normal file
View File

@ -0,0 +1,108 @@
# Client
Clients establish a WebSocket connection to [relays](../relays/connecting-to-a-relay). Through this connection, clients
communicate and subscribe to a range of [Nostr events](../events) based on specified subscription filters. These filters
define the Nostr events a client wishes to receive updates about.
::: info
Clients do not need to sign up or create an account to use Nostr. Upon connecting to a relay, a client provides
its subscription filters. The relay then streams events that match these filters to the client for the duration of the
connection.
:::
## WebSocket events
Communication between clients and relays happen via WebSockets. The client will emit WebSocket events when the
connection is __opened__, __closed__, when a __message__ is received or when there's an __error__.
::: info
WebSocket events are not [Nostr events](https://nostr.com/the-protocol/events). They are events emitted by the
WebSocket connection. The WebSocket `:message` event, however, contains a Nostr event in its payload.
:::
### connect
The `:connect` event is fired when a connection with a WebSocket is opened. You must call `Nostr::Client#connect` first.
```ruby
client = Nostr::Client.new
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
client.on :connect do
# When this block executes, you're connected to the relay
end
# Connect to a relay asynchronously
client.connect(relay)
```
Once the connection is open, you can send events to the relay, manage subscriptions, etc.
::: tip
Define the connection event handler before calling
[`Nostr::Client#connect`](https://www.rubydoc.info/gems/nostr/Nostr/Client#connect-instance_method). Otherwise,
you may miss the event.
:::
### error
The `:error` event is fired when a connection with a WebSocket has been closed because of an error.
```ruby
client.on :error do |error_message|
puts error_message
end
# > Network error: wss://rsslay.fiatjaf.com: Unable to verify the
# server certificate for 'rsslay.fiatjaf.com'
```
### message
The `:message` event is fired when data is received through a WebSocket.
```ruby
client.on :message do |message|
puts message
end
```
The message will be one of these 4 types, which must also be JSON arrays, according to the following patterns:
- `["EVENT", <subscription_id>, <event JSON>]`
- `["OK", <event_id>, <true|false>, <message>]`
- `["EOSE", <subscription_id>]`
- `["NOTICE", <message>]`
::: details Click me to see how a WebSocket message looks like
```ruby
[
"EVENT",
"d34107357089bfc9882146d3bfab0386",
{
"content": "",
"created_at": 1676456512,
"id": "18f63550da74454c5df7caa2a349edc5b2a6175ea4c5367fa4b4212781e5b310",
"kind": 3,
"pubkey": "117a121fa41dc2caa0b3d6c5b9f42f90d114f1301d39f9ee96b646ebfee75e36",
"sig": "d171420bd62cf981e8f86f2dd8f8f86737ea2bbe2d98da88db092991d125535860d982139a3c4be39886188613a9912ef380be017686a0a8b74231dc6e0b03cb",
"tags":[
["p", "1cc821cc2d47191b15fcfc0f73afed39a86ac6fb34fbfa7993ee3e0f0186ef7c"]
]
}
]
```
:::
### close
The `:close` event is fired when a connection with a WebSocket is closed.
```ruby
client.on :close do |code, reason|
puts "Error: #{code} - #{reason}"
end
```
::: tip
This handler is useful to attempt to reconnect to the relay.
:::

83
docs/core/keys.md Normal file
View File

@ -0,0 +1,83 @@
# Keys
To [sign events](#signing-an-event), you need a **private key**. To verify signatures, you need a **public key**. The combination of a
private and a public key is called a **keypair**.
There are a few ways to generate a keypair.
## a) Generating a keypair
If you don't have any keys, you can generate a keypair using the [`Nostr::Keygen`](https://www.rubydoc.info/gems/nostr/Nostr/Keygen) class:
```ruby
keygen = Nostr::Keygen.new
keypair = keygen.generate_key_pair
keypair.private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
keypair.public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
```
## b) Generating a private key and a public key
Alternatively, if you have already generated a private key, you can extract the corresponding public key by calling
`Keygen#extract_public_key`:
```ruby
keygen = Nostr::Keygen.new
private_key = keygen.generate_private_key
public_key = keygen.extract_public_key(private_key)
private_key # => '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900'
public_key # => '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558'
```
## c) Using existing keys
If you already have a private key and a public key, you can create a keypair using the `Nostr::KeyPair` class:
```ruby
keypair = Nostr::KeyPair.new(
private_key: '893c4cc8088924796b41dc788f7e2f746734497010b1a9f005c1faad7074b900',
public_key: '2d7661527d573cc8e84f665fa971dd969ba51e2526df00c149ff8e40a58f9558',
)
```
## Signing an event
KeyPairs are used to sign [events](../events). To create a signed event, you need to instantiate a
[`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) with a keypair:
```ruby{9,12-15}
# a) Use an existing keypair
keypair = Nostr::KeyPair.new(private_key: 'your-key', public_key: 'your-key')
# b) Or generate a new keypair
keygen = Nostr::Keygen.new
keypair = keygen.generate_key_pair
# Add the keypair to a user
user = Nostr::User.new(keypair: keypair)
# Create signed events
text_note = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
```
::: details Click me to view the text_note
```ruby
# text_note.to_h
{
id: '5feb10973dbcf5f210cfc1f0aa338fee62bed6a29696a67957713599b9baf0eb',
pubkey: 'b9b9821074d1b60b8fb4a3983632af3ef9669f55b20d515bf982cda5c439ad61', # from keypair
created_at: 1699847447,
kind: 1, # Nostr::EventKind::TEXT_NOTE,
tags: [],
content: 'Your feedback is appreciated, now pay $8',
sig: 'e30f2f08331f224e41a4099d16aefc780bf9f2d1191b71777e1e1789e6b51fdf7bb956f25d4ea9a152d1c66717a9d68c081ce6c89c298c3c5e794914013381ab'
}
```
:::

43
docs/core/user.md Normal file
View File

@ -0,0 +1,43 @@
# User
The class [`Nostr::User`](https://www.rubydoc.info/gems/nostr/Nostr/User) is an abstraction to facilitate the creation
of signed events. It is not required to use it to create events, but it is recommended.
Here's an example of how to create a signed event without the class `Nostr::User`:
```ruby
event = Nostr::Event.new(
pubkey: keypair.public_key,
kind: Nostr::EventKind::TEXT_NOTE,
tags: [],
content: 'Your feedback is appreciated, now pay $8',
)
event.sign(keypair.private_key)
```
::: details Click me to view the event
```ruby
# event.to_h
{
id: '5feb10973dbcf5f210cfc1f0aa338fee62bed6a29696a67957713599b9baf0eb',
pubkey: 'b9b9821074d1b60b8fb4a3983632af3ef9669f55b20d515bf982cda5c439ad61',
created_at: 1699847447,
kind: 1, # Nostr::EventKind::TEXT_NOTE,
tags: [],
content: 'Your feedback is appreciated, now pay $8',
sig: 'e30f2f08331f224e41a4099d16aefc780bf9f2d1191b71777e1e1789e6b51fdf7bb956f25d4ea9a152d1c66717a9d68c081ce6c89c298c3c5e794914013381ab'
}
```
:::
And here's how to create it with the class `Nostr::User`:
```ruby
user = Nostr::User.new(keypair: keypair)
event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
```

11
docs/events.md Normal file
View File

@ -0,0 +1,11 @@
# Events
## Event Kinds
| kind | description | NIP |
| ------------- |----------------------------------------------------------------| -------------------------------------------------------------- |
| `0` | [Metadata](./events/set-metadata) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
| `1` | [Short Text Note](./events/text-note) | [1](https://github.com/nostr-protocol/nips/blob/master/01.md) |
| `2` | [Recommend Relay](./events/recommend-server) | |
| `3` | [Contacts](./events/contact-list) | [2](https://github.com/nostr-protocol/nips/blob/master/02.md) |
| `4` | [Encrypted Direct Messages](./events/encrypted-direct-message) | [4](https://github.com/nostr-protocol/nips/blob/master/04.md) |

View File

@ -0,0 +1,29 @@
# Contact List
## Creating/updating your contact list
Every new contact list that gets published overwrites the past ones, so it should contain all entries.
```ruby
# Creating a contact list event with 2 contacts
update_contacts_event = user.create_event(
kind: Nostr::EventKind::CONTACT_LIST,
tags: [
[
"p", # mandatory
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", # public key of the user to add to the contacts
"wss://alicerelay.com/", # can be an empty string or can be omitted
"alice" # can be an empty string or can be omitted
],
[
"p", # mandatory
"3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681", # public key of the user to add to the contacts
"wss://bobrelay.com/nostr", # can be an empty string or can be omitted
"bob" # can be an empty string or can be omitted
],
],
)
# Send it to the Relay
client.publish(update_contacts_event)
```

View File

@ -0,0 +1,28 @@
# Encrypted Direct Message
## Sending an encrypted direct message
```ruby
sender_private_key = '3185a47e3802f956ca5a2b4ea606c1d51c7610f239617e8f0f218d55bdf2b757'
encrypted_direct_message = Nostr::Events::EncryptedDirectMessage.new(
sender_private_key: sender_private_key,
recipient_public_key: '6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0',
plain_text: 'Your feedback is appreciated, now pay $8',
previous_direct_message: 'ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460' # optional
)
encrypted_direct_message.sign(sender_private_key)
# #<Nostr::Events::EncryptedDirectMessage:0x0000000104c9fa68
# @content="mjIFNo1sSP3KROE6QqhWnPSGAZRCuK7Np9X+88HSVSwwtFyiZ35msmEVoFgRpKx4?iv=YckChfS2oWCGpMt1uQ4GbQ==",
# @created_at=1676456512,
# @id="daac98826d5eb29f7c013b6160986c4baf4fe6d4b995df67c1b480fab1839a9b",
# @kind=4,
# @pubkey="8a9d69c56e3c691bec8f9565e4dcbe38ae1d88fffeec3ce66b9f47558a3aa8ca",
# @sig="028bb5f5bab0396e2065000c84a4bcce99e68b1a79bb1b91a84311546f49c5b67570b48d4a328a1827e7a8419d74451347d4f55011a196e71edab31aa3d6bdac",
# @tags=[["p", "6c31422248998e300a1a457167565da7d15d0da96651296ee2791c29c11b6aa0"], ["e", "ccf9fdf3e1466d7c20969c71ec98defcf5f54aee088513e1b73ccb7bd770d460"]]>
# Send it to the Relay
client.publish(encrypted_direct_message)
```

View File

@ -0,0 +1,32 @@
# Recommend Server
The `Recommend Server` event, has a set of tags with the following structure `['e', <event-id>, <relay-url>, <marker>]`
Where:
- `<event-id>` is the id of the event being referenced.
- `<relay-url>` is the URL of a recommended relay associated with the reference. Clients SHOULD add a valid `<relay-URL>`
field, but may instead leave it as `''`.
- `<marker>` is optional and if present is one of `'reply'`, `'root'`, or `'mention'`.
Those marked with `'reply'` denote the id of the reply event being responded to. Those marked with `'root'` denote the
root id of the reply thread being responded to. For top level replies (those replying directly to the root event),
only the `'root'` marker should be used. Those marked with `'mention'` denote a quoted or reposted event id.
A direct reply to the root of a thread should have a single marked `'e'` tag of type `'root'`.
## Recommending a server
```ruby
recommend_server_event = user.create_event(
kind: Nostr::EventKind::RECOMMEND_SERVER,
tags: [
[
'e',
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
'wss://relay.damus.io'
],
]
)
client.publish(recommend_server_event)
```

View File

@ -0,0 +1,20 @@
# Set Metadata
In the `Metadata` event, the `content` is set to a stringified JSON object
`{name: <username>, about: <string>, picture: <url, string>}` describing the [user](../core/user) who created the event. A relay may
delete older events once it gets a new one for the same pubkey.
## Setting the user's metadata
```ruby
metadata_event = user.create_event(
kind: Nostr::EventKind::SET_METADATA,
content: {
name: 'Wilson Silva',
about: 'Used to make hydrochloric acid bombs in high school.',
picture: 'https://thispersondoesnotexist.com/'
}
)
client.publish(metadata_event)
```

15
docs/events/text-note.md Normal file
View File

@ -0,0 +1,15 @@
# Text Note
In the `Text Note` event, the `content` is set to the plaintext content of a note (anything the user wants to say).
Content that must be parsed, such as Markdown and HTML, should not be used.
## Sending a text note event
```ruby
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
client.publish(text_note_event)
```

View File

@ -0,0 +1,21 @@
# Installation
Install the gem and add to the application's Gemfile by executing:
```shell
bundle add nostr
```
If bundler is not being used to manage dependencies, install the gem by executing:
```shell
gem install nostr
```
## Requiring the gem
All examples in this guide assume that the gem has been required:
```ruby
require 'nostr'
```

View File

@ -0,0 +1,164 @@
---
editLink: true
---
# Getting started
This gem abstracts the complexity that you would face when trying to connect to relays web sockets, send and receive
events, handle events callbacks and much more.
## Visual overview
Begin your journey with an overview of the essential functions. A visual representation below maps out the key
components we'll delve into in this section.
```mermaid
classDiagram
class Client {
connect(relay)
publish(event)
subscribe(subscription_id, filter)
unsubscribe(subscription_id)
}
class Relay {
url
name
}
class Event {
pubkey
created_at
kind
tags
content
add_event_reference(event_id)
add_pubkey_reference(pubkey)
serialize()
to_h()
sign(private_key)
}
class Subscription {
id
filter
}
class Filter {
ids
authors
kinds
since
until
limit
to_h()
}
class EventKind {
<<Enumeration>>
SET_METADATA
TEXT_NOTE
RECOMMEND_SERVER
CONTACT_LIST
ENCRYPTED_DIRECT_MESSAGE
}
class KeyPair {
private_key
public_key
}
class Keygen {
generate_key_pair()
generate_private_key()
extract_public_key(private_key)
}
class User {
keypair
create_event(event_attributes)
}
Client --> Relay : connects via <br> WebSockets to
Client --> Event : uses WebSockets to <br> publish and receive
Client --> Subscription : receives Events via
Subscription --> Filter : uses
Event --> EventKind : is of kind
User --> KeyPair : has
User --> Event : creates and signs
User --> Keygen : uses to generate keys
Keygen --> KeyPair : generates
```
## Code overview
Explore the provided code snippet to learn about initializing the Nostr [client](../core/client.md), generating
a [keypair](../core/keys), [publishing](../relays/publishing-events) an event, and
efficiently [managing event subscriptions](../subscriptions/creating-a-subscription) (including event reception,
filtering, and WebSocket event handling).
```ruby
# Require the gem
require 'nostr'
# Instantiate a client
client = Nostr::Client.new
# a) Use an existing keypair
keypair = Nostr::KeyPair.new(
private_key: 'your-private-key',
public_key: 'your-public-key',
)
# b) Or create a new keypair
keygen = Nostr::Keygen.new
keypair = keygen.generate_keypair
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
# Create a signed event
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
# Connect asynchronously to a relay
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
client.connect(relay)
# Listen asynchronously for the connect event
client.on :connect do
# Send the event to the Relay
client.publish(text_note_event)
# Create a filter to receive the first 20 text notes
# and encrypted direct messages from the relay that
# were created in the previous hour
filter = Nostr::Filter.new(
kinds: [
Nostr::EventKind::TEXT_NOTE,
Nostr::EventKind::ENCRYPTED_DIRECT_MESSAGE
],
since: Time.now.to_i - 3600, # 1 hour ago
until: Time.now.to_i,
limit: 20,
)
# Subscribe to events matching conditions of a filter
subscription = client.subscribe(filter)
# Unsubscribe from events matching the filter above
client.unsubscribe(subscription.id)
end
# Listen for incoming messages and print them
client.on :message do |message|
puts message
end
# Listen for error messages
client.on :error do |error_message|
# Handle the error
end
# Listen for the close event
client.on :close do |code, reason|
# You may attempt to reconnect to the relay here
end
```
Beyond what's covered here, the Nostr protocol and this gem boast a wealth of additional functionalities. For an
in-depth exploration of these capabilities, proceed to the next page.

8
docs/implemented-nips.md Normal file
View File

@ -0,0 +1,8 @@
# Implemented NIPs
NIPs stand for Nostr Implementation Possibilities. They exist to document what may be implemented by Nostr-compatible
relay and client software.
- [NIP-01: Basic protocol flow description](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-02: Contact List and Petnames](https://github.com/nostr-protocol/nips/blob/master/02.md)
- [NIP-04: Encrypted Direct Message](https://github.com/nostr-protocol/nips/blob/master/04.md)

View File

@ -4,22 +4,41 @@ layout: home
hero: hero:
name: "Nostr" name: "Nostr"
text: "Ruby gem documentation" text: "Ruby"
tagline: My great project tagline tagline: "The Nostr protocol implemented in a Ruby gem."
actions: actions:
- theme: brand - theme: brand
text: Markdown Examples text: Getting Started
link: /markdown-examples link: /getting-started/overview
- theme: alt - theme: alt
text: API Examples text: View on Github
link: /api-examples link: https://github.com/wilsonsilva/nostr
- theme: alt
text: View on RubyDoc
link: https://www.rubydoc.info/gems/nostr
- theme: alt
text: View on RubyGems
link: https://rubygems.org/gems/nostr
features: features:
- title: Feature A - title: Asynchronous
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit details: Non-blocking I/O for maximum performance.
- title: Feature B icon: ⚡
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit - title: Lightweight
- title: Feature C details: Minimal runtime dependencies.
details: Lorem ipsum dolor sit amet, consectetur adipiscing elit icon: 🪶
- title: Intuitive
details: The API mirrors the Nostr protocol specification domain language.
icon: 💡
- title: Fully documented
details: All code is documented from both the maintainer's as well as the consumer's perspective.
icon: 📚
- title: Fully tested
details: All code is tested with 100% coverage.
icon: 🧪
- title: Fully typed
details: All code is typed with <a href="https://rubygems.org/gems/rbs" target="_blank">RBS</a> with the help of <a href="https://rubygems.org/gems/typeprof" target="_blank">TypeProf</a>. Type correctness is enforced by <a href="https://rubygems.org/gems/steep" target="_blank">Steep</a>.
icon: ✅
--- ---

View File

@ -0,0 +1,21 @@
# Connecting to a Relay
You must connect your nostr [Client](../core/client) to a relay in order to send and receive [Events](../events).
Instantiate a [`Nostr::Client`](https://www.rubydoc.info/gems/nostr/Nostr/Client) and a
[`Nostr::Relay`](https://www.rubydoc.info/gems/nostr/Nostr/Relay) giving it the `url` of your relay. The `name`
attribute is just descriptive.
Calling [`Client#connect`](https://www.rubydoc.info/gems/nostr/Nostr/Client#connect-instance_method) attempts to
establish a WebSocket connection between the Client and the Relay.
```ruby
client = Nostr::Client.new
relay = Nostr::Relay.new(url: 'wss://relay.damus.io', name: 'Damus')
# Listen for the connect event
client.on :connect do
# When this block executes, you're connected to the relay
end
# Connect to a relay asynchronously
client.connect(relay)
```

View File

@ -0,0 +1,29 @@
# Publishing events
Create a [signed event](../core/keys) and call the method
[`Nostr::Client#publish`](https://www.rubydoc.info/gems/nostr/Nostr/Client#publish-instance_method) to send the
event to the relay.
```ruby{4-8,17}
# Create a user with the keypair
user = Nostr::User.new(keypair: keypair)
# Create a signed event
text_note_event = user.create_event(
kind: Nostr::EventKind::TEXT_NOTE,
content: 'Your feedback is appreciated, now pay $8'
)
# Connect asynchronously to a relay
relay = Nostr::Relay.new(url: 'wss://nostr.wine', name: 'Wine')
client.connect(relay)
# Listen asynchronously for the connect event
client.on :connect do
# Send the event to the relay
client.publish(text_note_event)
end
```
The relay will verify the signature of the event with the public key. If the signature is valid, the relay should
broadcast the event to all subscribers.

View File

@ -0,0 +1,6 @@
# Receiving events
To receive events from Relays, you must create a subscription on the relay. A subscription is a filter that defines the
events you want to receive.
For more information, read the [Subscription](../subscriptions/creating-a-subscription.md) section.

View File

@ -0,0 +1,49 @@
# Creating a subscription
A client can request events and subscribe to new updates __after__ it has established a connection with the Relay.
You may use a [`Nostr::Filter`](https://www.rubydoc.info/gems/nostr/Nostr/Filter) instance with as many attributes as
you wish:
```ruby
client.on :connect do
filter = Nostr::Filter.new(
ids: ['8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8'],
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
kinds: [Nostr::EventKind::TEXT_NOTE],
e: ["f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52"],
p: ["f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8"],
since: 1230981305,
until: 1292190341,
limit: 420,
)
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
end
```
With just a few:
```ruby
client.on :connect do
filter = Nostr::Filter.new(kinds: [Nostr::EventKind::TEXT_NOTE])
subscription = client.subscribe(subscription_id: 'an-id', filter: filter)
end
```
Or omit the filter:
```ruby
client.on :connect do
subscription = client.subscribe(subscription_id: 'an-id')
end
```
Or even omit the subscription id:
```ruby
client.on :connect do
subscription = client.subscribe(filter: filter)
subscription.id # => "13736f08dee8d7b697222ba605c6fab2" (randomly generated)
end
```

View File

@ -0,0 +1,10 @@
# Stop previous subscriptions
You can stop receiving messages from a subscription by calling
[`Nostr::Client#unsubscribe`](https://www.rubydoc.info/gems/nostr/Nostr/Client#unsubscribe-instance_method) with the
ID of the subscription you want to stop receiving messages from:
```ruby
client.unsubscribe('your-subscription-id')
client.unsubscribe(subscription.id)
```

View File

@ -0,0 +1,131 @@
# Filtering events
## Filtering by id
You can filter events by their ids or prefixes:
```ruby
filter = Nostr::Filter.new(
ids: [
# matches events with these exact IDs
'8535d5e2d7b9dc07567f676fbe70428133c9884857e1915f5b1cc6514c2fdff8',
'461544014d87c9eaf3e76e021240007dff2c7afb356319f99c741b45749bf82f',
# and match events whose id start with '76fbe'
'76fbe',
# and match events whose id start with 'b'
'b',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by author
You can filter events by their author's pubkey or prefix:
```ruby
filter = Nostr::Filter.new(
authors: [
# matches events whose (authors) pubkey match these exact IDs
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
# and match events whose (authors) pubkey start with 'db508e957'
'db508e957',
# and match events whose (authors) pubkey start with 'f'
'f',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by kind
You can filter events by their kind:
```ruby
filter = Nostr::Filter.new(
kinds: [
# matches events whose kind is TEXT_NOTE
Nostr::EventKind::TEXT_NOTE,
# and matches events whose kind is CONTACT_LIST
Nostr::EventKind::CONTACT_LIST,
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by referenced event
You can filter events by the events they reference (in their `e` tag):
```ruby
filter = Nostr::Filter.new(
e: [
# matches events that reference other events whose ids match these exact IDs
'f111593a72cc52a7f0978de5ecf29b4653d0cf539f1fa50d2168fc1dc8280e52',
'f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8',
# and match events that reference other events whose id starts with '76fbe'
'76fbe',
# and match events that reference other events whose id starts with 'b'
'b',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by referenced pubkey
You can filter events by the pubkeys they reference (in their `p` tag):
```ruby
filter = Nostr::Filter.new(
p: [
# matches events that reference other pubkeys that match these exact IDs
'b698043170d580f8ae5bad4ac80b1fdb508e957f0bbffe97f2a8915fa8b34070',
'51f853ff4894b062950e46ebed8c1c7015160f8173994414a96dd286f65f0f49',
# and match events that reference other pubkeys that start with 'db508e957'
'db508e957',
# and match events that reference other pubkeys that start with 'f'
'f',
]
)
subscription = client.subscribe(filter: filter)
```
## Filtering by timestamp
You can filter events by their timestamp:
```ruby
filter = Nostr::Filter.new(
since: 1230981305, # matches events that are newer than this timestamp
until: 1292190341, # matches events that are older than this timestamp
)
subscription = client.subscribe(filter: filter)
```
## Limiting the number of events
You can limit the number of events received:
```ruby
filter = Nostr::Filter.new(
limit: 420, # matches at most 420 events
)
subscription = client.subscribe(filter: filter)
```
## Combining filters
You can combine filters. For example, to match `5` text note events that are newer than `1230981305` from the author
`ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577`:
```ruby
filter = Nostr::Filter.new(
authors: ['ae00f88a885ce76afad5cbb2459ef0dcf0df0907adc6e4dac16e1bfbd7074577'],
kinds: [Nostr::EventKind::TEXT_NOTE],
since: 1230981305,
limit: 5,
)
subscription = client.subscribe(filter: filter)
```

View File

@ -0,0 +1,4 @@
# Updating a subscription
Updating a subscription is done by creating a new subscription with the same id as the previous one. See
[creating a subscription](./creating-a-subscription.md) for more information.