333 lines
7.6 KiB
Markdown
333 lines
7.6 KiB
Markdown
# Go-Roots-WS - Nostr WebSocket Transport for Golang
|
|
|
|
Source: https://git.wisehodl.dev/jay/go-roots-ws
|
|
|
|
Mirror: https://github.com/wisehodl/go-roots-ws
|
|
|
|
## What this library does
|
|
|
|
`go-roots-ws` is a consensus-layer Nostr protocol websocket transport library for golang. It only provides primitives for working with Nostr protocol websocket connection states and messages:
|
|
|
|
- Websocket Connection States
|
|
- Envelope Structure
|
|
- Message Validation
|
|
- Protocol Message Creation
|
|
- Protocol Message Parsing
|
|
- Standard Label Handling
|
|
|
|
## What this library does not do
|
|
|
|
`go-roots-ws` serves as a foundation for other libraries and applications to implement higher level transport abstractions on top of it, including:
|
|
|
|
- Connection Management
|
|
- Event Loops
|
|
- Subscription Handling
|
|
- State Management
|
|
- Reconnection Logic
|
|
|
|
## Installation
|
|
|
|
1. Add `go-roots-ws` to your project:
|
|
|
|
```bash
|
|
go get git.wisehodl.dev/jay/go-roots-ws
|
|
```
|
|
|
|
If the primary repository is unavailable, use the `replace` directive in your go.mod file to get the package from the github mirror:
|
|
|
|
```
|
|
replace git.wisehodl.dev/jay/go-roots-ws => github.com/wisehodl/go-roots-ws latest
|
|
```
|
|
|
|
2. Import the packages:
|
|
|
|
```golang
|
|
import (
|
|
"encoding/json"
|
|
"git.wisehodl.dev/jay/go-roots/events"
|
|
"git.wisehodl.dev/jay/go-roots/filters"
|
|
"git.wisehodl.dev/jay/go-roots-ws/envelope"
|
|
"git.wisehodl.dev/jay/go-roots-ws/errors"
|
|
)
|
|
```
|
|
|
|
3. Access functions with appropriate namespaces.
|
|
|
|
## Usage Examples
|
|
|
|
### Envelope Creation
|
|
|
|
#### Create EVENT envelope
|
|
|
|
```go
|
|
// Create an event using go-roots
|
|
event := events.Event{
|
|
ID: "abc123",
|
|
PubKey: "def456",
|
|
Kind: 1,
|
|
Content: "Hello Nostr!",
|
|
CreatedAt: int(time.Now().Unix()),
|
|
}
|
|
|
|
// Convert to JSON
|
|
eventJSON, err := json.Marshal(event)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Create envelope
|
|
env := envelope.EncloseEvent(eventJSON)
|
|
// Result: ["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}]
|
|
```
|
|
|
|
#### Create subscription EVENT envelope
|
|
|
|
```go
|
|
// Create an event using go-roots
|
|
event := events.Event{
|
|
ID: "abc123",
|
|
PubKey: "def456",
|
|
Kind: 1,
|
|
Content: "Hello Nostr!",
|
|
CreatedAt: int(time.Now().Unix()),
|
|
}
|
|
|
|
// Convert to JSON
|
|
eventJSON, err := json.Marshal(event)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Create envelope with subscription ID
|
|
subID := "sub1"
|
|
env := envelope.EncloseSubscriptionEvent(subID, eventJSON)
|
|
// Result: ["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!","created_at":1636394097}]
|
|
```
|
|
|
|
#### Create REQ envelope
|
|
|
|
```go
|
|
// Create filters using go-roots
|
|
since := int(time.Now().Add(-24 * time.Hour).Unix())
|
|
limit := 50
|
|
|
|
filter1 := filters.Filter{
|
|
Kinds: []int{1},
|
|
Limit: &limit,
|
|
Since: &since,
|
|
}
|
|
|
|
filter2 := filters.Filter{
|
|
Authors: []string{"def456"},
|
|
}
|
|
|
|
// Marshal filters to JSON
|
|
filter1JSON, err := filters.MarshalJSON(filter1)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
filter2JSON, err := filters.MarshalJSON(filter2)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Create envelope
|
|
subID := "sub1"
|
|
filtersJSON := [][]byte{filter1JSON, filter2JSON}
|
|
env := envelope.EncloseReq(subID, filtersJSON)
|
|
// Result: ["REQ","sub1",{"kinds":[1],"limit":50,"since":1636307697},{"authors":["def456"]}]
|
|
```
|
|
|
|
#### Create other envelope types
|
|
|
|
```go
|
|
// Create CLOSE envelope
|
|
env := envelope.EncloseClose("sub1")
|
|
// Result: ["CLOSE","sub1"]
|
|
|
|
// Create EOSE envelope
|
|
env := envelope.EncloseEOSE("sub1")
|
|
// Result: ["EOSE","sub1"]
|
|
|
|
// Create NOTICE envelope
|
|
env := envelope.EncloseNotice("This is a notice")
|
|
// Result: ["NOTICE","This is a notice"]
|
|
|
|
// Create OK envelope
|
|
env := envelope.EncloseOK("abc123", true, "Event accepted")
|
|
// Result: ["OK","abc123",true,"Event accepted"]
|
|
|
|
// Create AUTH challenge
|
|
env := envelope.EncloseAuthChallenge("random-challenge-string")
|
|
// Result: ["AUTH","random-challenge-string"]
|
|
|
|
// Create AUTH response
|
|
// Create an event using go-roots
|
|
authEvent := events.Event{
|
|
ID: "abc123",
|
|
PubKey: "def456",
|
|
Kind: 22242,
|
|
Content: "",
|
|
CreatedAt: int(time.Now().Unix()),
|
|
}
|
|
|
|
// Convert to JSON
|
|
authEventJSON, err := json.Marshal(authEvent)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Create envelope
|
|
env := envelope.EncloseAuthResponse(authEventJSON)
|
|
// Result: ["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":"","created_at":1636394097}]
|
|
```
|
|
|
|
---
|
|
|
|
### Envelope Parsing
|
|
|
|
#### Extract label from envelope
|
|
|
|
```go
|
|
env := []byte(`["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`)
|
|
label, err := envelope.GetLabel(env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
// label: "EVENT"
|
|
|
|
// Check if label is standard
|
|
isStandard := envelope.IsStandardLabel(label)
|
|
// isStandard: true
|
|
```
|
|
|
|
#### Extract event from EVENT envelope
|
|
|
|
```go
|
|
env := []byte(`["EVENT",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`)
|
|
eventJSON, err := envelope.FindEvent(env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Parse into go-roots Event
|
|
var event events.Event
|
|
err = json.Unmarshal(eventJSON, &event)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Validate the event
|
|
if err := events.Validate(event); err != nil {
|
|
log.Printf("Invalid event: %v", err)
|
|
}
|
|
|
|
// Now you can access event properties
|
|
fmt.Println(event.ID, event.Kind, event.Content)
|
|
```
|
|
|
|
#### Extract subscription event
|
|
|
|
```go
|
|
env := []byte(`["EVENT","sub1",{"id":"abc123","pubkey":"def456","kind":1,"content":"Hello Nostr!"}]`)
|
|
subID, eventJSON, err := envelope.FindSubscriptionEvent(env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Parse into go-roots Event
|
|
var event events.Event
|
|
err = json.Unmarshal(eventJSON, &event)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
fmt.Printf("Subscription: %s, Event ID: %s\n", subID, event.ID)
|
|
```
|
|
|
|
#### Extract subscription request
|
|
|
|
```go
|
|
env := []byte(`["REQ","sub1",{"kinds":[1],"limit":50},{"authors":["def456"]}]`)
|
|
subID, filtersJSON, err := envelope.FindReq(env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Parse each filter
|
|
var parsedFilters []filters.Filter
|
|
for _, filterJSON := range filtersJSON {
|
|
var filter filters.Filter
|
|
err := filters.UnmarshalJSON(filterJSON, &filter)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
parsedFilters = append(parsedFilters, filter)
|
|
}
|
|
|
|
// Now you can use the filter objects
|
|
for i, filter := range parsedFilters {
|
|
fmt.Printf("Filter %d: %+v\n", i, filter)
|
|
}
|
|
```
|
|
|
|
#### Extract other envelope types
|
|
|
|
```go
|
|
// Extract OK response
|
|
env := []byte(`["OK","abc123",true,"Event accepted"]`)
|
|
eventID, status, message, err := envelope.FindOK(env)
|
|
// eventID: "abc123"
|
|
// status: true
|
|
// message: "Event accepted"
|
|
|
|
// Extract EOSE message
|
|
env := []byte(`["EOSE","sub1"]`)
|
|
subID, err := envelope.FindEOSE(env)
|
|
// subID: "sub1"
|
|
|
|
// Extract CLOSE message
|
|
env := []byte(`["CLOSE","sub1"]`)
|
|
subID, err := envelope.FindClose(env)
|
|
// subID: "sub1"
|
|
|
|
// Extract CLOSED message
|
|
env := []byte(`["CLOSED","sub1","Subscription complete"]`)
|
|
subID, message, err := envelope.FindClosed(env)
|
|
// subID: "sub1"
|
|
// message: "Subscription complete"
|
|
|
|
// Extract NOTICE message
|
|
env := []byte(`["NOTICE","This is a notice"]`)
|
|
message, err := envelope.FindNotice(env)
|
|
// message: "This is a notice"
|
|
|
|
// Extract AUTH challenge
|
|
env := []byte(`["AUTH","random-challenge-string"]`)
|
|
challenge, err := envelope.FindAuthChallenge(env)
|
|
// challenge: "random-challenge-string"
|
|
|
|
// Extract AUTH response
|
|
env := []byte(`["AUTH",{"id":"abc123","pubkey":"def456","kind":22242,"content":""}]`)
|
|
authEventJSON, err := envelope.FindAuthResponse(env)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// Parse into go-roots Event
|
|
var authEvent events.Event
|
|
err = json.Unmarshal(authEventJSON, &authEvent)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
```
|
|
|
|
## Testing
|
|
|
|
This library contains a comprehensive suite of unit tests. Run them with:
|
|
|
|
```bash
|
|
go test ./...
|
|
```
|