Wrote roots-ws golang implementation.
This commit is contained in:
332
README.md
Normal file
332
README.md
Normal file
@@ -0,0 +1,332 @@
|
||||
# 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 ./...
|
||||
```
|
||||
Reference in New Issue
Block a user