Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
cad366795a
|
|||
|
e14b89bc8b
|
|||
|
5b4dd9ea60
|
|||
|
bae1d09f8d
|
|||
|
f1f3236196
|
@@ -35,11 +35,22 @@ func (l *Listener) HandleEvent(msg []byte) (err error) {
|
||||
|
||||
// Check if sprocket is enabled and process event through it
|
||||
if l.sprocketManager != nil && l.sprocketManager.IsEnabled() {
|
||||
if !l.sprocketManager.IsRunning() {
|
||||
// Sprocket is enabled but not running - drop all messages
|
||||
log.W.F("sprocket is enabled but not running, dropping event %0x", env.E.ID)
|
||||
if l.sprocketManager.IsDisabled() {
|
||||
// Sprocket is disabled due to failure - reject all events
|
||||
log.W.F("sprocket is disabled, rejecting event %0x", env.E.ID)
|
||||
if err = Ok.Error(
|
||||
l, env, "sprocket policy not available",
|
||||
l, env, "sprocket disabled - events rejected until sprocket is restored",
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if !l.sprocketManager.IsRunning() {
|
||||
// Sprocket is enabled but not running - reject all events
|
||||
log.W.F("sprocket is enabled but not running, rejecting event %0x", env.E.ID)
|
||||
if err = Ok.Error(
|
||||
l, env, "sprocket not running - events rejected until sprocket starts",
|
||||
); chk.E(err) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -19,11 +19,9 @@ func Run(
|
||||
) (quit chan struct{}) {
|
||||
// shutdown handler
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.I.F("shutting down")
|
||||
close(quit)
|
||||
}
|
||||
<-ctx.Done()
|
||||
log.I.F("shutting down")
|
||||
close(quit)
|
||||
}()
|
||||
// get the admins
|
||||
var err error
|
||||
|
||||
@@ -37,6 +37,7 @@ type SprocketManager struct {
|
||||
mutex sync.RWMutex
|
||||
isRunning bool
|
||||
enabled bool
|
||||
disabled bool // true when sprocket is disabled due to failure
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
stderr io.ReadCloser
|
||||
@@ -56,21 +57,105 @@ func NewSprocketManager(ctx context.Context, appName string, enabled bool) *Spro
|
||||
configDir: configDir,
|
||||
scriptPath: scriptPath,
|
||||
enabled: enabled,
|
||||
disabled: false,
|
||||
responseChan: make(chan SprocketResponse, 100), // Buffered channel for responses
|
||||
}
|
||||
|
||||
// Start the sprocket script if it exists and is enabled
|
||||
if enabled {
|
||||
go sm.startSprocketIfExists()
|
||||
// Start periodic check for sprocket script availability
|
||||
go sm.periodicCheck()
|
||||
}
|
||||
|
||||
return sm
|
||||
}
|
||||
|
||||
// disableSprocket disables sprocket due to failure
|
||||
func (sm *SprocketManager) disableSprocket() {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
if !sm.disabled {
|
||||
sm.disabled = true
|
||||
log.W.F("sprocket disabled due to failure - all events will be rejected (script location: %s)", sm.scriptPath)
|
||||
}
|
||||
}
|
||||
|
||||
// enableSprocket re-enables sprocket and attempts to start it
|
||||
func (sm *SprocketManager) enableSprocket() {
|
||||
sm.mutex.Lock()
|
||||
defer sm.mutex.Unlock()
|
||||
|
||||
if sm.disabled {
|
||||
sm.disabled = false
|
||||
log.I.F("sprocket re-enabled, attempting to start")
|
||||
|
||||
// Attempt to start sprocket in background
|
||||
go func() {
|
||||
if _, err := os.Stat(sm.scriptPath); err == nil {
|
||||
if err := sm.StartSprocket(); err != nil {
|
||||
log.E.F("failed to restart sprocket: %v", err)
|
||||
sm.disableSprocket()
|
||||
} else {
|
||||
log.I.F("sprocket restarted successfully")
|
||||
}
|
||||
} else {
|
||||
log.W.F("sprocket script still not found, keeping disabled")
|
||||
sm.disableSprocket()
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
// periodicCheck periodically checks if sprocket script becomes available
|
||||
func (sm *SprocketManager) periodicCheck() {
|
||||
ticker := time.NewTicker(30 * time.Second) // Check every 30 seconds
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sm.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
sm.mutex.RLock()
|
||||
disabled := sm.disabled
|
||||
running := sm.isRunning
|
||||
sm.mutex.RUnlock()
|
||||
|
||||
// Only check if sprocket is disabled or not running
|
||||
if disabled || !running {
|
||||
if _, err := os.Stat(sm.scriptPath); err == nil {
|
||||
// Script is available, try to enable/restart
|
||||
if disabled {
|
||||
sm.enableSprocket()
|
||||
} else if !running {
|
||||
// Script exists but sprocket isn't running, try to start
|
||||
go func() {
|
||||
if err := sm.StartSprocket(); err != nil {
|
||||
log.E.F("failed to restart sprocket: %v", err)
|
||||
sm.disableSprocket()
|
||||
} else {
|
||||
log.I.F("sprocket restarted successfully")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// startSprocketIfExists starts the sprocket script if the file exists
|
||||
func (sm *SprocketManager) startSprocketIfExists() {
|
||||
if _, err := os.Stat(sm.scriptPath); err == nil {
|
||||
sm.StartSprocket()
|
||||
if err := sm.StartSprocket(); err != nil {
|
||||
log.E.F("failed to start sprocket: %v", err)
|
||||
sm.disableSprocket()
|
||||
}
|
||||
} else {
|
||||
log.W.F("sprocket script not found at %s, disabling sprocket", sm.scriptPath)
|
||||
sm.disableSprocket()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,6 +558,13 @@ func (sm *SprocketManager) IsRunning() bool {
|
||||
return sm.isRunning
|
||||
}
|
||||
|
||||
// IsDisabled returns whether sprocket is disabled due to failure
|
||||
func (sm *SprocketManager) IsDisabled() bool {
|
||||
sm.mutex.RLock()
|
||||
defer sm.mutex.RUnlock()
|
||||
return sm.disabled
|
||||
}
|
||||
|
||||
// monitorProcess monitors the sprocket process and cleans up when it exits
|
||||
func (sm *SprocketManager) monitorProcess() {
|
||||
if sm.currentCmd == nil {
|
||||
@@ -504,6 +596,9 @@ func (sm *SprocketManager) monitorProcess() {
|
||||
|
||||
if err != nil {
|
||||
log.E.F("sprocket process exited with error: %v", err)
|
||||
// Auto-disable sprocket on failure
|
||||
sm.disabled = true
|
||||
log.W.F("sprocket disabled due to process failure - all events will be rejected (script location: %s)", sm.scriptPath)
|
||||
} else {
|
||||
log.I.F("sprocket process exited normally")
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
v0.12.0
|
||||
v0.12.2
|
||||
336
readme.adoc
336
readme.adoc
@@ -49,7 +49,7 @@ To build with the embedded web interface:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Build the React web application
|
||||
# Build the Svelte web application
|
||||
cd app/web
|
||||
bun install
|
||||
bun run build
|
||||
@@ -59,13 +59,25 @@ cd ../../
|
||||
go build -o orly
|
||||
----
|
||||
|
||||
You can automate this process with a build script:
|
||||
The recommended way to build and embed the web UI is using the provided script:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./scripts/update-embedded-web.sh
|
||||
----
|
||||
|
||||
This script will:
|
||||
- Build the Svelte app in `app/web` to `app/web/dist` using Bun (preferred) or fall back to npm/yarn/pnpm
|
||||
- Run `go install` from the repository root so the binary picks up the new embedded assets
|
||||
- Automatically detect and use the best available JavaScript package manager
|
||||
|
||||
For manual builds, you can also use:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
#!/bin/bash
|
||||
# build.sh
|
||||
echo "Building React app..."
|
||||
echo "Building Svelte app..."
|
||||
cd app/web
|
||||
bun install
|
||||
bun run build
|
||||
@@ -79,6 +91,324 @@ echo "Build complete!"
|
||||
|
||||
Make it executable with `chmod +x build.sh` and run with `./build.sh`.
|
||||
|
||||
== web UI
|
||||
|
||||
ORLY includes a modern web-based user interface built with link:https://svelte.dev/[Svelte] that provides comprehensive relay management capabilities.
|
||||
|
||||
=== features
|
||||
|
||||
The web UI offers:
|
||||
|
||||
* **Authentication**: Secure login using Nostr key pairs with challenge-response authentication
|
||||
* **Event Management**: View, export, and import Nostr events with advanced filtering and search
|
||||
* **User Administration**: Manage user permissions and roles (admin/owner)
|
||||
* **Sprocket Management**: Configure and manage external event processing scripts
|
||||
* **Real-time Updates**: Live event streaming and status updates
|
||||
* **Dark/Light Theme**: Toggle between themes with persistent preferences
|
||||
* **Responsive Design**: Works on desktop and mobile devices
|
||||
|
||||
=== authentication
|
||||
|
||||
The web UI uses Nostr-native authentication:
|
||||
|
||||
1. **Challenge Generation**: Server generates a cryptographic challenge
|
||||
2. **Signature Verification**: Client signs the challenge with their private key
|
||||
3. **Session Management**: Authenticated sessions with role-based permissions
|
||||
|
||||
Supported authentication methods:
|
||||
- Direct private key input
|
||||
- Nostr extension integration
|
||||
- Hardware wallet support
|
||||
|
||||
=== user roles
|
||||
|
||||
* **Guest**: Read-only access to public events
|
||||
* **User**: Can publish events and manage their own content
|
||||
* **Admin**: Full relay management except sprocket configuration
|
||||
* **Owner**: Complete control including sprocket management and system configuration
|
||||
|
||||
=== event management
|
||||
|
||||
The interface provides comprehensive event management:
|
||||
|
||||
* **Event Browser**: Paginated view of all events with filtering by kind, author, and content
|
||||
* **Export Functionality**: Export events in JSON format with configurable date ranges
|
||||
* **Import Capability**: Bulk import events (admin/owner only)
|
||||
* **Search**: Full-text search across event content and metadata
|
||||
* **Event Details**: Expandable view showing full event JSON and metadata
|
||||
|
||||
=== sprocket integration
|
||||
|
||||
The web UI includes a dedicated sprocket management interface:
|
||||
|
||||
* **Status Monitoring**: Real-time status of sprocket scripts
|
||||
* **Script Upload**: Upload and manage sprocket scripts
|
||||
* **Version Control**: Track and manage multiple script versions
|
||||
* **Configuration**: Configure sprocket parameters and settings
|
||||
* **Logs**: View sprocket execution logs and errors
|
||||
|
||||
=== development mode
|
||||
|
||||
For development, the web UI supports hot-reloading:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Enable development proxy
|
||||
export ORLY_WEB_DISABLE_EMBEDDED=true
|
||||
export ORLY_WEB_DEV_PROXY_URL=localhost:5000
|
||||
|
||||
# Start relay
|
||||
./orly
|
||||
|
||||
# In another terminal, start Svelte dev server
|
||||
cd app/web
|
||||
bun run dev
|
||||
----
|
||||
|
||||
This allows for rapid development with automatic reloading of changes.
|
||||
|
||||
== sprocket event sifter interface
|
||||
|
||||
The sprocket system provides a powerful interface for external event processing scripts, allowing you to implement custom filtering, validation, and processing logic for Nostr events before they are stored in the relay.
|
||||
|
||||
=== overview
|
||||
|
||||
Sprocket scripts receive events via stdin and respond with JSONL (JSON Lines) format, enabling real-time event processing with three possible actions:
|
||||
|
||||
* **accept**: Continue with normal event processing
|
||||
* **reject**: Return OK false to client with rejection message
|
||||
* **shadowReject**: Return OK true to client but abort processing (useful for spam filtering)
|
||||
|
||||
=== how it works
|
||||
|
||||
1. **Event Reception**: Events are sent to the sprocket script as JSON objects via stdin
|
||||
2. **Processing**: Script analyzes the event and applies custom logic
|
||||
3. **Response**: Script responds with JSONL containing the decision and optional message
|
||||
4. **Action**: Relay processes the response and either accepts, rejects, or shadow rejects the event
|
||||
|
||||
=== script protocol
|
||||
|
||||
==== input format
|
||||
|
||||
Events are sent as JSON objects, one per line:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "event_id_here",
|
||||
"kind": 1,
|
||||
"content": "Hello, world!",
|
||||
"pubkey": "author_pubkey",
|
||||
"tags": [["t", "hashtag"], ["p", "reply_pubkey"]],
|
||||
"created_at": 1640995200,
|
||||
"sig": "signature_here"
|
||||
}
|
||||
```
|
||||
|
||||
==== output format
|
||||
|
||||
Scripts must respond with JSONL format:
|
||||
|
||||
```json
|
||||
{"id": "event_id", "action": "accept", "msg": ""}
|
||||
{"id": "event_id", "action": "reject", "msg": "reason for rejection"}
|
||||
{"id": "event_id", "action": "shadowReject", "msg": ""}
|
||||
```
|
||||
|
||||
=== configuration
|
||||
|
||||
Enable sprocket processing:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
export ORLY_SPROCKET_ENABLED=true
|
||||
export ORLY_APP_NAME="ORLY"
|
||||
----
|
||||
|
||||
The sprocket script should be placed at:
|
||||
`~/.config/{ORLY_APP_NAME}/sprocket.sh`
|
||||
|
||||
For example, with default `ORLY_APP_NAME="ORLY"`:
|
||||
`~/.config/ORLY/sprocket.sh`
|
||||
|
||||
Backup files are automatically created when updating sprocket scripts via the web UI, with timestamps like:
|
||||
`~/.config/ORLY/sprocket.sh.20240101120000`
|
||||
|
||||
=== manual sprocket updates
|
||||
|
||||
For manual sprocket script updates, you can use the stop/write/restart method:
|
||||
|
||||
1. **Stop the relay**:
|
||||
```bash
|
||||
# Send SIGINT to gracefully stop
|
||||
kill -INT <relay_pid>
|
||||
```
|
||||
|
||||
2. **Write new sprocket script**:
|
||||
```bash
|
||||
# Create/update the sprocket script
|
||||
cat > ~/.config/ORLY/sprocket.sh << 'EOF'
|
||||
#!/bin/bash
|
||||
while read -r line; do
|
||||
if [[ -n "$line" ]]; then
|
||||
event_id=$(echo "$line" | jq -r '.id')
|
||||
echo "{\"id\":\"$event_id\",\"action\":\"accept\",\"msg\":\"\"}"
|
||||
fi
|
||||
done
|
||||
EOF
|
||||
|
||||
# Make it executable
|
||||
chmod +x ~/.config/ORLY/sprocket.sh
|
||||
```
|
||||
|
||||
3. **Restart the relay**:
|
||||
```bash
|
||||
./orly
|
||||
```
|
||||
|
||||
The relay will automatically detect the new sprocket script and start it. If the script fails, sprocket will be disabled and all events rejected until the script is fixed.
|
||||
|
||||
=== failure handling
|
||||
|
||||
When sprocket is enabled but fails to start or crashes:
|
||||
|
||||
1. **Automatic Disable**: Sprocket is automatically disabled
|
||||
2. **Event Rejection**: All incoming events are rejected with error message
|
||||
3. **Periodic Recovery**: Every 30 seconds, the system checks if the sprocket script becomes available
|
||||
4. **Auto-Restart**: If the script is found, sprocket is automatically re-enabled and restarted
|
||||
|
||||
This ensures that:
|
||||
- Relay continues running even when sprocket fails
|
||||
- No events are processed without proper sprocket filtering
|
||||
- Sprocket automatically recovers when the script is fixed
|
||||
- Clear error messages inform users about the sprocket status
|
||||
- Error messages include the exact file location for easy fixes
|
||||
|
||||
When sprocket fails, the error message will show:
|
||||
`sprocket disabled due to failure - all events will be rejected (script location: ~/.config/ORLY/sprocket.sh)`
|
||||
|
||||
This makes it easy to locate and fix the sprocket script file.
|
||||
|
||||
=== example script
|
||||
|
||||
Here's a Python example that implements various filtering criteria:
|
||||
|
||||
[source,python]
|
||||
----
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import sys
|
||||
|
||||
def process_event(event_json):
|
||||
event_id = event_json.get('id', '')
|
||||
event_content = event_json.get('content', '')
|
||||
event_kind = event_json.get('kind', 0)
|
||||
|
||||
# Reject spam content
|
||||
if 'spam' in event_content.lower():
|
||||
return {
|
||||
'id': event_id,
|
||||
'action': 'reject',
|
||||
'msg': 'Content contains spam'
|
||||
}
|
||||
|
||||
# Shadow reject test events
|
||||
if event_kind == 9999:
|
||||
return {
|
||||
'id': event_id,
|
||||
'action': 'shadowReject',
|
||||
'msg': ''
|
||||
}
|
||||
|
||||
# Accept all other events
|
||||
return {
|
||||
'id': event_id,
|
||||
'action': 'accept',
|
||||
'msg': ''
|
||||
}
|
||||
|
||||
# Main processing loop
|
||||
for line in sys.stdin:
|
||||
if line.strip():
|
||||
try:
|
||||
event = json.loads(line)
|
||||
response = process_event(event)
|
||||
print(json.dumps(response))
|
||||
sys.stdout.flush()
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
----
|
||||
|
||||
=== bash example
|
||||
|
||||
A simple bash script example:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
#!/bin/bash
|
||||
while read -r line; do
|
||||
if [[ -n "$line" ]]; then
|
||||
# Extract event ID
|
||||
event_id=$(echo "$line" | jq -r '.id')
|
||||
|
||||
# Check for spam content
|
||||
if echo "$line" | jq -r '.content' | grep -qi "spam"; then
|
||||
echo "{\"id\":\"$event_id\",\"action\":\"reject\",\"msg\":\"Spam detected\"}"
|
||||
else
|
||||
echo "{\"id\":\"$event_id\",\"action\":\"accept\",\"msg\":\"\"}"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
----
|
||||
|
||||
=== testing
|
||||
|
||||
Test your sprocket script directly:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
# Test with sample event
|
||||
echo '{"id":"test","kind":1,"content":"spam test"}' | python3 sprocket.py
|
||||
|
||||
# Expected output:
|
||||
# {"id": "test", "action": "reject", "msg": "Content contains spam"}
|
||||
----
|
||||
|
||||
Run the comprehensive test suite:
|
||||
|
||||
[source,bash]
|
||||
----
|
||||
./test-sprocket-complete.sh
|
||||
----
|
||||
|
||||
=== web UI management
|
||||
|
||||
The web UI provides a complete sprocket management interface:
|
||||
|
||||
* **Status Monitoring**: View real-time sprocket status and health
|
||||
* **Script Upload**: Upload new sprocket scripts via the web interface
|
||||
* **Version Management**: Track and manage multiple script versions
|
||||
* **Configuration**: Configure sprocket parameters and settings
|
||||
* **Logs**: View execution logs and error messages
|
||||
* **Restart**: Restart sprocket scripts without relay restart
|
||||
|
||||
=== use cases
|
||||
|
||||
Common sprocket use cases include:
|
||||
|
||||
* **Spam Filtering**: Detect and reject spam content
|
||||
* **Content Moderation**: Implement custom content policies
|
||||
* **Rate Limiting**: Control event publishing rates
|
||||
* **Event Validation**: Additional validation beyond Nostr protocol
|
||||
* **Analytics**: Log and analyze event patterns
|
||||
* **Integration**: Connect with external services and APIs
|
||||
|
||||
=== performance considerations
|
||||
|
||||
* Sprocket scripts run synchronously and can impact relay performance
|
||||
* Keep processing logic efficient and fast
|
||||
* Use appropriate timeouts to prevent blocking
|
||||
* Consider using shadow reject for non-critical filtering to maintain user experience
|
||||
|
||||
== secp256k1 dependency
|
||||
|
||||
ORLY uses the optimized `libsecp256k1` C library from Bitcoin Core for schnorr signatures, providing 4x faster signing and ECDH operations compared to pure Go implementations.
|
||||
|
||||
0
test-sprocket-complete.sh → scripts/sprocket/test-sprocket-complete.sh
Executable file → Normal file
0
test-sprocket-complete.sh → scripts/sprocket/test-sprocket-complete.sh
Executable file → Normal file
0
test-sprocket-demo.sh → scripts/sprocket/test-sprocket-demo.sh
Executable file → Normal file
0
test-sprocket-demo.sh → scripts/sprocket/test-sprocket-demo.sh
Executable file → Normal file
0
test-sprocket-example.sh → scripts/sprocket/test-sprocket-example.sh
Executable file → Normal file
0
test-sprocket-example.sh → scripts/sprocket/test-sprocket-example.sh
Executable file → Normal file
0
test-sprocket-final.sh → scripts/sprocket/test-sprocket-final.sh
Executable file → Normal file
0
test-sprocket-final.sh → scripts/sprocket/test-sprocket-final.sh
Executable file → Normal file
0
test-sprocket-manual.sh → scripts/sprocket/test-sprocket-manual.sh
Executable file → Normal file
0
test-sprocket-manual.sh → scripts/sprocket/test-sprocket-manual.sh
Executable file → Normal file
0
test-sprocket-simple.sh → scripts/sprocket/test-sprocket-simple.sh
Executable file → Normal file
0
test-sprocket-simple.sh → scripts/sprocket/test-sprocket-simple.sh
Executable file → Normal file
0
test-sprocket-working.sh → scripts/sprocket/test-sprocket-working.sh
Executable file → Normal file
0
test-sprocket-working.sh → scripts/sprocket/test-sprocket-working.sh
Executable file → Normal file
0
test-sprocket.py → scripts/sprocket/test-sprocket.py
Executable file → Normal file
0
test-sprocket.py → scripts/sprocket/test-sprocket.py
Executable file → Normal file
0
test-sprocket.sh → scripts/sprocket/test-sprocket.sh
Executable file → Normal file
0
test-sprocket.sh → scripts/sprocket/test-sprocket.sh
Executable file → Normal file
Reference in New Issue
Block a user