20 changed files with 3075 additions and 4 deletions
@ -0,0 +1,228 @@ |
|||||||
|
# Sprocket Test Suite |
||||||
|
|
||||||
|
This directory contains a comprehensive test suite for the ORLY relay's sprocket event processing system. |
||||||
|
|
||||||
|
## Overview |
||||||
|
|
||||||
|
The sprocket system allows external scripts to process Nostr events before they are stored in the relay. Events are sent to the sprocket script via stdin, and the script responds with JSONL messages indicating whether to accept, reject, or shadow reject the event. |
||||||
|
|
||||||
|
## Test Files |
||||||
|
|
||||||
|
### Core Test Files |
||||||
|
|
||||||
|
- **`test-sprocket.py`** - Python sprocket script that implements various filtering criteria |
||||||
|
- **`test-sprocket-integration.go`** - Go integration tests using the testing framework |
||||||
|
- **`test-sprocket-complete.sh`** - Complete test suite that starts relay and runs tests |
||||||
|
- **`test-sprocket-manual.sh`** - Manual test script for interactive testing |
||||||
|
- **`run-sprocket-test.sh`** - Automated test runner |
||||||
|
|
||||||
|
### Example Scripts |
||||||
|
|
||||||
|
- **`test-sprocket-example.sh`** - Simple bash example sprocket script |
||||||
|
|
||||||
|
## Test Criteria |
||||||
|
|
||||||
|
The Python sprocket script (`test-sprocket.py`) implements the following test criteria: |
||||||
|
|
||||||
|
1. **Spam Content**: Rejects events containing "spam" in the content |
||||||
|
2. **Test Kind**: Shadow rejects events with kind 9999 |
||||||
|
3. **Blocked Hashtags**: Rejects events with hashtags "blocked", "rejected", or "test-block" |
||||||
|
4. **Blocked Pubkeys**: Shadow rejects events from pubkeys starting with "00000000", "11111111", or "22222222" |
||||||
|
5. **Content Length**: Rejects events with content longer than 1000 characters |
||||||
|
6. **Timestamp Validation**: Rejects events that are too old (>1 hour) or too far in the future (>5 minutes) |
||||||
|
|
||||||
|
## Running Tests |
||||||
|
|
||||||
|
### Quick Test (Recommended) |
||||||
|
|
||||||
|
```bash |
||||||
|
./test-sprocket-complete.sh |
||||||
|
``` |
||||||
|
|
||||||
|
This script will: |
||||||
|
1. Set up the test environment |
||||||
|
2. Start the relay with sprocket enabled |
||||||
|
3. Run all test cases |
||||||
|
4. Clean up automatically |
||||||
|
|
||||||
|
### Manual Testing |
||||||
|
|
||||||
|
```bash |
||||||
|
# Start relay manually with sprocket enabled |
||||||
|
export ORLY_SPROCKET_ENABLED=true |
||||||
|
go run . test |
||||||
|
|
||||||
|
# In another terminal, run manual tests |
||||||
|
./test-sprocket-manual.sh |
||||||
|
``` |
||||||
|
|
||||||
|
### Integration Tests |
||||||
|
|
||||||
|
```bash |
||||||
|
# Run Go integration tests |
||||||
|
go test -v -run TestSprocketIntegration ./test-sprocket-integration.go |
||||||
|
``` |
||||||
|
|
||||||
|
## Prerequisites |
||||||
|
|
||||||
|
- **Python 3**: Required for the Python sprocket script |
||||||
|
- **jq**: Required for JSON processing in bash scripts |
||||||
|
- **websocat**: Required for WebSocket testing |
||||||
|
```bash |
||||||
|
cargo install websocat |
||||||
|
``` |
||||||
|
- **Go dependencies**: gorilla/websocket for integration tests |
||||||
|
```bash |
||||||
|
go get github.com/gorilla/websocket |
||||||
|
``` |
||||||
|
|
||||||
|
## Test Cases |
||||||
|
|
||||||
|
### 1. Normal Event (Accept) |
||||||
|
```json |
||||||
|
{ |
||||||
|
"id": "test_normal_123", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": 1640995200, |
||||||
|
"kind": 1, |
||||||
|
"content": "Hello, world!", |
||||||
|
"sig": "test_sig" |
||||||
|
} |
||||||
|
``` |
||||||
|
**Expected**: `["OK","test_normal_123",true]` |
||||||
|
|
||||||
|
### 2. Spam Content (Reject) |
||||||
|
```json |
||||||
|
{ |
||||||
|
"id": "test_spam_456", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": 1640995200, |
||||||
|
"kind": 1, |
||||||
|
"content": "This is spam content", |
||||||
|
"sig": "test_sig" |
||||||
|
} |
||||||
|
``` |
||||||
|
**Expected**: `["OK","test_spam_456",false,"error: Content contains spam"]` |
||||||
|
|
||||||
|
### 3. Test Kind (Shadow Reject) |
||||||
|
```json |
||||||
|
{ |
||||||
|
"id": "test_kind_789", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": 1640995200, |
||||||
|
"kind": 9999, |
||||||
|
"content": "Test message", |
||||||
|
"sig": "test_sig" |
||||||
|
} |
||||||
|
``` |
||||||
|
**Expected**: `["OK","test_kind_789",true]` (but event not processed) |
||||||
|
|
||||||
|
### 4. Blocked Hashtag (Reject) |
||||||
|
```json |
||||||
|
{ |
||||||
|
"id": "test_hashtag_101", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": 1640995200, |
||||||
|
"kind": 1, |
||||||
|
"content": "Message with hashtag", |
||||||
|
"tags": [["t", "blocked"]], |
||||||
|
"sig": "test_sig" |
||||||
|
} |
||||||
|
``` |
||||||
|
**Expected**: `["OK","test_hashtag_101",false,"error: Hashtag \"blocked\" is not allowed"]` |
||||||
|
|
||||||
|
### 5. Too Long Content (Reject) |
||||||
|
```json |
||||||
|
{ |
||||||
|
"id": "test_long_202", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": 1640995200, |
||||||
|
"kind": 1, |
||||||
|
"content": "a... (1001 characters)", |
||||||
|
"sig": "test_sig" |
||||||
|
} |
||||||
|
``` |
||||||
|
**Expected**: `["OK","test_long_202",false,"error: Content too long (max 1000 characters)"]` |
||||||
|
|
||||||
|
## Sprocket Script Protocol |
||||||
|
|
||||||
|
### Input Format |
||||||
|
Events are sent to the sprocket script as JSON objects via stdin, one per line. |
||||||
|
|
||||||
|
### Output Format |
||||||
|
The sprocket script must respond with JSONL (JSON Lines) format: |
||||||
|
|
||||||
|
```json |
||||||
|
{"id": "event_id", "action": "accept", "msg": ""} |
||||||
|
{"id": "event_id", "action": "reject", "msg": "reason for rejection"} |
||||||
|
{"id": "event_id", "action": "shadowReject", "msg": ""} |
||||||
|
``` |
||||||
|
|
||||||
|
### Actions |
||||||
|
- **`accept`**: Continue with normal event processing |
||||||
|
- **`reject`**: Return OK false to client with message |
||||||
|
- **`shadowReject`**: Return OK true to client but abort processing |
||||||
|
|
||||||
|
## Configuration |
||||||
|
|
||||||
|
To enable sprocket in the relay: |
||||||
|
|
||||||
|
```bash |
||||||
|
export ORLY_SPROCKET_ENABLED=true |
||||||
|
export ORLY_APP_NAME="ORLY" |
||||||
|
``` |
||||||
|
|
||||||
|
The sprocket script should be placed at: |
||||||
|
`~/.config/{ORLY_APP_NAME}/sprocket.sh` |
||||||
|
|
||||||
|
## Troubleshooting |
||||||
|
|
||||||
|
### Common Issues |
||||||
|
|
||||||
|
1. **Sprocket script not found** |
||||||
|
- Ensure the script exists at the correct path |
||||||
|
- Check file permissions (must be executable) |
||||||
|
|
||||||
|
2. **Python script errors** |
||||||
|
- Verify Python 3 is installed |
||||||
|
- Check script syntax with `python3 -m py_compile test-sprocket.py` |
||||||
|
|
||||||
|
3. **WebSocket connection failed** |
||||||
|
- Ensure relay is running on the correct port |
||||||
|
- Check firewall settings |
||||||
|
|
||||||
|
4. **Test failures** |
||||||
|
- Check relay logs for sprocket errors |
||||||
|
- Verify sprocket script is responding correctly |
||||||
|
|
||||||
|
### Debug Mode |
||||||
|
|
||||||
|
Enable debug logging: |
||||||
|
```bash |
||||||
|
export ORLY_LOG_LEVEL=debug |
||||||
|
``` |
||||||
|
|
||||||
|
### Manual Sprocket Testing |
||||||
|
|
||||||
|
Test the sprocket script directly: |
||||||
|
```bash |
||||||
|
echo '{"id":"test","kind":1,"content":"spam test"}' | python3 test-sprocket.py |
||||||
|
``` |
||||||
|
|
||||||
|
Expected output: |
||||||
|
```json |
||||||
|
{"id": "test", "action": "reject", "msg": "Content contains spam"} |
||||||
|
``` |
||||||
|
|
||||||
|
## Contributing |
||||||
|
|
||||||
|
When adding new test cases: |
||||||
|
|
||||||
|
1. Add the test case to `test-sprocket.py` |
||||||
|
2. Add corresponding test in `test-sprocket-complete.sh` |
||||||
|
3. Update this README with the new test case |
||||||
|
4. Ensure all tests pass before submitting |
||||||
|
|
||||||
|
## License |
||||||
|
|
||||||
|
This test suite is part of the ORLY relay project and follows the same license. |
||||||
@ -0,0 +1,518 @@ |
|||||||
|
package app |
||||||
|
|
||||||
|
import ( |
||||||
|
"bufio" |
||||||
|
"context" |
||||||
|
"encoding/json" |
||||||
|
"fmt" |
||||||
|
"io" |
||||||
|
"os" |
||||||
|
"os/exec" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
"sync" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/adrg/xdg" |
||||||
|
"lol.mleku.dev/chk" |
||||||
|
"lol.mleku.dev/log" |
||||||
|
"next.orly.dev/pkg/encoders/event" |
||||||
|
) |
||||||
|
|
||||||
|
// SprocketResponse represents a response from the sprocket script
|
||||||
|
type SprocketResponse struct { |
||||||
|
ID string `json:"id"` |
||||||
|
Action string `json:"action"` // accept, reject, or shadowReject
|
||||||
|
Msg string `json:"msg"` // NIP-20 response message (only used for reject)
|
||||||
|
} |
||||||
|
|
||||||
|
// SprocketManager handles sprocket script execution and management
|
||||||
|
type SprocketManager struct { |
||||||
|
ctx context.Context |
||||||
|
cancel context.CancelFunc |
||||||
|
configDir string |
||||||
|
scriptPath string |
||||||
|
currentCmd *exec.Cmd |
||||||
|
currentCancel context.CancelFunc |
||||||
|
mutex sync.RWMutex |
||||||
|
isRunning bool |
||||||
|
enabled bool |
||||||
|
stdin io.WriteCloser |
||||||
|
stdout io.ReadCloser |
||||||
|
stderr io.ReadCloser |
||||||
|
responseChan chan SprocketResponse |
||||||
|
} |
||||||
|
|
||||||
|
// NewSprocketManager creates a new sprocket manager
|
||||||
|
func NewSprocketManager(ctx context.Context, appName string, enabled bool) *SprocketManager { |
||||||
|
configDir := filepath.Join(xdg.ConfigHome, appName) |
||||||
|
scriptPath := filepath.Join(configDir, "sprocket.sh") |
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(ctx) |
||||||
|
|
||||||
|
sm := &SprocketManager{ |
||||||
|
ctx: ctx, |
||||||
|
cancel: cancel, |
||||||
|
configDir: configDir, |
||||||
|
scriptPath: scriptPath, |
||||||
|
enabled: enabled, |
||||||
|
responseChan: make(chan SprocketResponse, 100), // Buffered channel for responses
|
||||||
|
} |
||||||
|
|
||||||
|
// Start the sprocket script if it exists and is enabled
|
||||||
|
if enabled { |
||||||
|
go sm.startSprocketIfExists() |
||||||
|
} |
||||||
|
|
||||||
|
return sm |
||||||
|
} |
||||||
|
|
||||||
|
// startSprocketIfExists starts the sprocket script if the file exists
|
||||||
|
func (sm *SprocketManager) startSprocketIfExists() { |
||||||
|
if _, err := os.Stat(sm.scriptPath); err == nil { |
||||||
|
sm.StartSprocket() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// StartSprocket starts the sprocket script
|
||||||
|
func (sm *SprocketManager) StartSprocket() error { |
||||||
|
sm.mutex.Lock() |
||||||
|
defer sm.mutex.Unlock() |
||||||
|
|
||||||
|
if sm.isRunning { |
||||||
|
return fmt.Errorf("sprocket is already running") |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := os.Stat(sm.scriptPath); os.IsNotExist(err) { |
||||||
|
return fmt.Errorf("sprocket script does not exist") |
||||||
|
} |
||||||
|
|
||||||
|
// Create a new context for this command
|
||||||
|
cmdCtx, cmdCancel := context.WithCancel(sm.ctx) |
||||||
|
|
||||||
|
// Make the script executable
|
||||||
|
if err := os.Chmod(sm.scriptPath, 0755); chk.E(err) { |
||||||
|
cmdCancel() |
||||||
|
return fmt.Errorf("failed to make script executable: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Start the script
|
||||||
|
cmd := exec.CommandContext(cmdCtx, sm.scriptPath) |
||||||
|
cmd.Dir = sm.configDir |
||||||
|
|
||||||
|
// Set up stdio pipes for communication
|
||||||
|
stdin, err := cmd.StdinPipe() |
||||||
|
if chk.E(err) { |
||||||
|
cmdCancel() |
||||||
|
return fmt.Errorf("failed to create stdin pipe: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe() |
||||||
|
if chk.E(err) { |
||||||
|
cmdCancel() |
||||||
|
stdin.Close() |
||||||
|
return fmt.Errorf("failed to create stdout pipe: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
stderr, err := cmd.StderrPipe() |
||||||
|
if chk.E(err) { |
||||||
|
cmdCancel() |
||||||
|
stdin.Close() |
||||||
|
stdout.Close() |
||||||
|
return fmt.Errorf("failed to create stderr pipe: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Start the command
|
||||||
|
if err := cmd.Start(); chk.E(err) { |
||||||
|
cmdCancel() |
||||||
|
stdin.Close() |
||||||
|
stdout.Close() |
||||||
|
stderr.Close() |
||||||
|
return fmt.Errorf("failed to start sprocket: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
sm.currentCmd = cmd |
||||||
|
sm.currentCancel = cmdCancel |
||||||
|
sm.stdin = stdin |
||||||
|
sm.stdout = stdout |
||||||
|
sm.stderr = stderr |
||||||
|
sm.isRunning = true |
||||||
|
|
||||||
|
// Start response reader in background
|
||||||
|
go sm.readResponses() |
||||||
|
|
||||||
|
// Log stderr output in background
|
||||||
|
go sm.logOutput(stdout, stderr) |
||||||
|
|
||||||
|
// Monitor the process
|
||||||
|
go sm.monitorProcess() |
||||||
|
|
||||||
|
log.I.F("sprocket started (pid=%d)", cmd.Process.Pid) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// StopSprocket stops the sprocket script gracefully, with SIGKILL fallback
|
||||||
|
func (sm *SprocketManager) StopSprocket() error { |
||||||
|
sm.mutex.Lock() |
||||||
|
defer sm.mutex.Unlock() |
||||||
|
|
||||||
|
if !sm.isRunning || sm.currentCmd == nil { |
||||||
|
return fmt.Errorf("sprocket is not running") |
||||||
|
} |
||||||
|
|
||||||
|
// Close stdin first to signal the script to exit
|
||||||
|
if sm.stdin != nil { |
||||||
|
sm.stdin.Close() |
||||||
|
} |
||||||
|
|
||||||
|
// Cancel the context
|
||||||
|
if sm.currentCancel != nil { |
||||||
|
sm.currentCancel() |
||||||
|
} |
||||||
|
|
||||||
|
// Wait for graceful shutdown with timeout
|
||||||
|
done := make(chan error, 1) |
||||||
|
go func() { |
||||||
|
done <- sm.currentCmd.Wait() |
||||||
|
}() |
||||||
|
|
||||||
|
select { |
||||||
|
case <-done: |
||||||
|
// Process exited gracefully
|
||||||
|
log.I.F("sprocket stopped gracefully") |
||||||
|
case <-time.After(5 * time.Second): |
||||||
|
// Force kill after 5 seconds
|
||||||
|
log.W.F("sprocket did not stop gracefully, sending SIGKILL") |
||||||
|
if err := sm.currentCmd.Process.Kill(); chk.E(err) { |
||||||
|
log.E.F("failed to kill sprocket process: %v", err) |
||||||
|
} |
||||||
|
<-done // Wait for the kill to complete
|
||||||
|
} |
||||||
|
|
||||||
|
// Clean up pipes
|
||||||
|
if sm.stdin != nil { |
||||||
|
sm.stdin.Close() |
||||||
|
sm.stdin = nil |
||||||
|
} |
||||||
|
if sm.stdout != nil { |
||||||
|
sm.stdout.Close() |
||||||
|
sm.stdout = nil |
||||||
|
} |
||||||
|
if sm.stderr != nil { |
||||||
|
sm.stderr.Close() |
||||||
|
sm.stderr = nil |
||||||
|
} |
||||||
|
|
||||||
|
sm.isRunning = false |
||||||
|
sm.currentCmd = nil |
||||||
|
sm.currentCancel = nil |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// RestartSprocket stops and starts the sprocket script
|
||||||
|
func (sm *SprocketManager) RestartSprocket() error { |
||||||
|
if sm.isRunning { |
||||||
|
if err := sm.StopSprocket(); chk.E(err) { |
||||||
|
return fmt.Errorf("failed to stop sprocket: %v", err) |
||||||
|
} |
||||||
|
// Give it a moment to fully stop
|
||||||
|
time.Sleep(100 * time.Millisecond) |
||||||
|
} |
||||||
|
|
||||||
|
return sm.StartSprocket() |
||||||
|
} |
||||||
|
|
||||||
|
// UpdateSprocket updates the sprocket script and restarts it with zero downtime
|
||||||
|
func (sm *SprocketManager) UpdateSprocket(scriptContent string) error { |
||||||
|
// Ensure config directory exists
|
||||||
|
if err := os.MkdirAll(sm.configDir, 0755); chk.E(err) { |
||||||
|
return fmt.Errorf("failed to create config directory: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// If script content is empty, delete the script and stop
|
||||||
|
if strings.TrimSpace(scriptContent) == "" { |
||||||
|
if sm.isRunning { |
||||||
|
if err := sm.StopSprocket(); chk.E(err) { |
||||||
|
log.E.F("failed to stop sprocket before deletion: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := os.Stat(sm.scriptPath); err == nil { |
||||||
|
if err := os.Remove(sm.scriptPath); chk.E(err) { |
||||||
|
return fmt.Errorf("failed to delete sprocket script: %v", err) |
||||||
|
} |
||||||
|
log.I.F("sprocket script deleted") |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Create backup of existing script if it exists
|
||||||
|
if _, err := os.Stat(sm.scriptPath); err == nil { |
||||||
|
timestamp := time.Now().Format("20060102150405") |
||||||
|
backupPath := sm.scriptPath + "." + timestamp |
||||||
|
if err := os.Rename(sm.scriptPath, backupPath); chk.E(err) { |
||||||
|
log.W.F("failed to create backup: %v", err) |
||||||
|
} else { |
||||||
|
log.I.F("created backup: %s", backupPath) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Write new script to temporary file first
|
||||||
|
tempPath := sm.scriptPath + ".tmp" |
||||||
|
if err := os.WriteFile(tempPath, []byte(scriptContent), 0755); chk.E(err) { |
||||||
|
return fmt.Errorf("failed to write temporary sprocket script: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// If sprocket is running, do zero-downtime update
|
||||||
|
if sm.isRunning { |
||||||
|
// Atomically replace the script file
|
||||||
|
if err := os.Rename(tempPath, sm.scriptPath); chk.E(err) { |
||||||
|
os.Remove(tempPath) // Clean up temp file
|
||||||
|
return fmt.Errorf("failed to replace sprocket script: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("sprocket script updated atomically") |
||||||
|
|
||||||
|
// Restart the sprocket process
|
||||||
|
return sm.RestartSprocket() |
||||||
|
} else { |
||||||
|
// Not running, just replace the file
|
||||||
|
if err := os.Rename(tempPath, sm.scriptPath); chk.E(err) { |
||||||
|
os.Remove(tempPath) // Clean up temp file
|
||||||
|
return fmt.Errorf("failed to replace sprocket script: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("sprocket script updated") |
||||||
|
return nil |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// GetSprocketStatus returns the current status of the sprocket
|
||||||
|
func (sm *SprocketManager) GetSprocketStatus() map[string]interface{} { |
||||||
|
sm.mutex.RLock() |
||||||
|
defer sm.mutex.RUnlock() |
||||||
|
|
||||||
|
status := map[string]interface{}{ |
||||||
|
"is_running": sm.isRunning, |
||||||
|
"script_exists": false, |
||||||
|
"script_path": sm.scriptPath, |
||||||
|
} |
||||||
|
|
||||||
|
if _, err := os.Stat(sm.scriptPath); err == nil { |
||||||
|
status["script_exists"] = true |
||||||
|
|
||||||
|
// Get script content
|
||||||
|
if content, err := os.ReadFile(sm.scriptPath); err == nil { |
||||||
|
status["script_content"] = string(content) |
||||||
|
} |
||||||
|
|
||||||
|
// Get file info
|
||||||
|
if info, err := os.Stat(sm.scriptPath); err == nil { |
||||||
|
status["script_modified"] = info.ModTime() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if sm.isRunning && sm.currentCmd != nil && sm.currentCmd.Process != nil { |
||||||
|
status["pid"] = sm.currentCmd.Process.Pid |
||||||
|
} |
||||||
|
|
||||||
|
return status |
||||||
|
} |
||||||
|
|
||||||
|
// GetSprocketVersions returns a list of all sprocket script versions
|
||||||
|
func (sm *SprocketManager) GetSprocketVersions() ([]map[string]interface{}, error) { |
||||||
|
versions := []map[string]interface{}{} |
||||||
|
|
||||||
|
// Check for current script
|
||||||
|
if _, err := os.Stat(sm.scriptPath); err == nil { |
||||||
|
if info, err := os.Stat(sm.scriptPath); err == nil { |
||||||
|
if content, err := os.ReadFile(sm.scriptPath); err == nil { |
||||||
|
versions = append(versions, map[string]interface{}{ |
||||||
|
"name": "sprocket.sh", |
||||||
|
"path": sm.scriptPath, |
||||||
|
"modified": info.ModTime(), |
||||||
|
"content": string(content), |
||||||
|
"is_current": true, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check for backup versions
|
||||||
|
dir := filepath.Dir(sm.scriptPath) |
||||||
|
files, err := os.ReadDir(dir) |
||||||
|
if chk.E(err) { |
||||||
|
return versions, nil |
||||||
|
} |
||||||
|
|
||||||
|
for _, file := range files { |
||||||
|
if strings.HasPrefix(file.Name(), "sprocket.sh.") && !file.IsDir() { |
||||||
|
path := filepath.Join(dir, file.Name()) |
||||||
|
if info, err := os.Stat(path); err == nil { |
||||||
|
if content, err := os.ReadFile(path); err == nil { |
||||||
|
versions = append(versions, map[string]interface{}{ |
||||||
|
"name": file.Name(), |
||||||
|
"path": path, |
||||||
|
"modified": info.ModTime(), |
||||||
|
"content": string(content), |
||||||
|
"is_current": false, |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return versions, nil |
||||||
|
} |
||||||
|
|
||||||
|
// DeleteSprocketVersion deletes a specific sprocket version
|
||||||
|
func (sm *SprocketManager) DeleteSprocketVersion(filename string) error { |
||||||
|
// Don't allow deleting the current script
|
||||||
|
if filename == "sprocket.sh" { |
||||||
|
return fmt.Errorf("cannot delete current sprocket script") |
||||||
|
} |
||||||
|
|
||||||
|
path := filepath.Join(sm.configDir, filename) |
||||||
|
if err := os.Remove(path); chk.E(err) { |
||||||
|
return fmt.Errorf("failed to delete sprocket version: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("deleted sprocket version: %s", filename) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// logOutput logs the output from stdout and stderr
|
||||||
|
func (sm *SprocketManager) logOutput(stdout, stderr io.ReadCloser) { |
||||||
|
defer stdout.Close() |
||||||
|
defer stderr.Close() |
||||||
|
|
||||||
|
go func() { |
||||||
|
io.Copy(os.Stdout, stdout) |
||||||
|
}() |
||||||
|
|
||||||
|
go func() { |
||||||
|
io.Copy(os.Stderr, stderr) |
||||||
|
}() |
||||||
|
} |
||||||
|
|
||||||
|
// ProcessEvent sends an event to the sprocket script and waits for a response
|
||||||
|
func (sm *SprocketManager) ProcessEvent(evt *event.E) (*SprocketResponse, error) { |
||||||
|
sm.mutex.RLock() |
||||||
|
if !sm.isRunning || sm.stdin == nil { |
||||||
|
sm.mutex.RUnlock() |
||||||
|
return nil, fmt.Errorf("sprocket is not running") |
||||||
|
} |
||||||
|
stdin := sm.stdin |
||||||
|
sm.mutex.RUnlock() |
||||||
|
|
||||||
|
// Serialize the event to JSON
|
||||||
|
eventJSON, err := json.Marshal(evt) |
||||||
|
if chk.E(err) { |
||||||
|
return nil, fmt.Errorf("failed to serialize event: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Send the event JSON to the sprocket script
|
||||||
|
// The final ']' should be the only thing after the event's raw JSON
|
||||||
|
if _, err := stdin.Write(eventJSON); chk.E(err) { |
||||||
|
return nil, fmt.Errorf("failed to write event to sprocket: %v", err) |
||||||
|
} |
||||||
|
|
||||||
|
// Wait for response with timeout
|
||||||
|
select { |
||||||
|
case response := <-sm.responseChan: |
||||||
|
return &response, nil |
||||||
|
case <-time.After(5 * time.Second): |
||||||
|
return nil, fmt.Errorf("sprocket response timeout") |
||||||
|
case <-sm.ctx.Done(): |
||||||
|
return nil, fmt.Errorf("sprocket context cancelled") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// readResponses reads JSONL responses from the sprocket script
|
||||||
|
func (sm *SprocketManager) readResponses() { |
||||||
|
if sm.stdout == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
scanner := bufio.NewScanner(sm.stdout) |
||||||
|
for scanner.Scan() { |
||||||
|
line := scanner.Text() |
||||||
|
if line == "" { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
var response SprocketResponse |
||||||
|
if err := json.Unmarshal([]byte(line), &response); chk.E(err) { |
||||||
|
log.E.F("failed to parse sprocket response: %v", err) |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Send response to channel (non-blocking)
|
||||||
|
select { |
||||||
|
case sm.responseChan <- response: |
||||||
|
default: |
||||||
|
log.W.F("sprocket response channel full, dropping response") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if err := scanner.Err(); chk.E(err) { |
||||||
|
log.E.F("error reading sprocket responses: %v", err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// IsEnabled returns whether sprocket is enabled
|
||||||
|
func (sm *SprocketManager) IsEnabled() bool { |
||||||
|
return sm.enabled |
||||||
|
} |
||||||
|
|
||||||
|
// IsRunning returns whether sprocket is currently running
|
||||||
|
func (sm *SprocketManager) IsRunning() bool { |
||||||
|
sm.mutex.RLock() |
||||||
|
defer sm.mutex.RUnlock() |
||||||
|
return sm.isRunning |
||||||
|
} |
||||||
|
|
||||||
|
// monitorProcess monitors the sprocket process and cleans up when it exits
|
||||||
|
func (sm *SprocketManager) monitorProcess() { |
||||||
|
if sm.currentCmd == nil { |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
err := sm.currentCmd.Wait() |
||||||
|
|
||||||
|
sm.mutex.Lock() |
||||||
|
defer sm.mutex.Unlock() |
||||||
|
|
||||||
|
// Clean up pipes
|
||||||
|
if sm.stdin != nil { |
||||||
|
sm.stdin.Close() |
||||||
|
sm.stdin = nil |
||||||
|
} |
||||||
|
if sm.stdout != nil { |
||||||
|
sm.stdout.Close() |
||||||
|
sm.stdout = nil |
||||||
|
} |
||||||
|
if sm.stderr != nil { |
||||||
|
sm.stderr.Close() |
||||||
|
sm.stderr = nil |
||||||
|
} |
||||||
|
|
||||||
|
sm.isRunning = false |
||||||
|
sm.currentCmd = nil |
||||||
|
sm.currentCancel = nil |
||||||
|
|
||||||
|
if err != nil { |
||||||
|
log.E.F("sprocket process exited with error: %v", err) |
||||||
|
} else { |
||||||
|
log.I.F("sprocket process exited normally") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Shutdown gracefully shuts down the sprocket manager
|
||||||
|
func (sm *SprocketManager) Shutdown() { |
||||||
|
sm.cancel() |
||||||
|
if sm.isRunning { |
||||||
|
sm.StopSprocket() |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Sprocket Integration Test Runner |
||||||
|
# This script sets up and runs the sprocket integration test |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Running Sprocket Integration Test" |
||||||
|
echo "====================================" |
||||||
|
|
||||||
|
# Check if Python 3 is available |
||||||
|
if ! command -v python3 &> /dev/null; then |
||||||
|
echo "❌ Python 3 is required but not installed" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
# Check if jq is available (for the bash sprocket script) |
||||||
|
if ! command -v jq &> /dev/null; then |
||||||
|
echo "❌ jq is required but not installed" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
# Check if gorilla/websocket is available |
||||||
|
echo "📦 Installing test dependencies..." |
||||||
|
go mod tidy |
||||||
|
go get github.com/gorilla/websocket |
||||||
|
|
||||||
|
# Create test configuration directory |
||||||
|
TEST_CONFIG_DIR="$HOME/.config/ORLY_TEST" |
||||||
|
mkdir -p "$TEST_CONFIG_DIR" |
||||||
|
|
||||||
|
# Copy the Python sprocket script to the test directory |
||||||
|
cp test-sprocket.py "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Create a simple bash wrapper for the Python script |
||||||
|
cat > "$TEST_CONFIG_DIR/sprocket.sh" << 'EOF' |
||||||
|
#!/bin/bash |
||||||
|
python3 "$(dirname "$0")/sprocket.py" |
||||||
|
EOF |
||||||
|
|
||||||
|
chmod +x "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
echo "🔧 Test setup complete" |
||||||
|
echo "📁 Sprocket script location: $TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
# Run the integration test |
||||||
|
echo "🚀 Starting integration test..." |
||||||
|
go test -v -run TestSprocketIntegration ./test-sprocket-integration.go |
||||||
|
|
||||||
|
echo "✅ Sprocket integration test completed successfully!" |
||||||
@ -0,0 +1,209 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Complete Sprocket Test Suite |
||||||
|
# This script starts the relay with sprocket enabled and runs tests |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Complete Sprocket Test Suite" |
||||||
|
echo "==============================" |
||||||
|
|
||||||
|
# Configuration |
||||||
|
RELAY_PORT="3334" |
||||||
|
TEST_CONFIG_DIR="$HOME/.config/ORLY_TEST" |
||||||
|
|
||||||
|
# Clean up any existing test processes |
||||||
|
echo "🧹 Cleaning up existing processes..." |
||||||
|
pkill -f "ORLY_TEST" || true |
||||||
|
sleep 2 |
||||||
|
|
||||||
|
# Create test configuration directory |
||||||
|
echo "📁 Setting up test environment..." |
||||||
|
mkdir -p "$TEST_CONFIG_DIR" |
||||||
|
|
||||||
|
# Copy the Python sprocket script |
||||||
|
cp test-sprocket.py "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Create bash wrapper for the Python script |
||||||
|
cat > "$TEST_CONFIG_DIR/sprocket.sh" << 'EOF' |
||||||
|
#!/bin/bash |
||||||
|
python3 "$(dirname "$0")/sprocket.py" |
||||||
|
EOF |
||||||
|
|
||||||
|
chmod +x "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
echo "✅ Sprocket script created at: $TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
# Start the relay with sprocket enabled |
||||||
|
echo "🚀 Starting relay with sprocket enabled..." |
||||||
|
export ORLY_APP_NAME="ORLY_TEST" |
||||||
|
export ORLY_DATA_DIR="/tmp/orly_test_data" |
||||||
|
export ORLY_LISTEN="127.0.0.1" |
||||||
|
export ORLY_PORT="$RELAY_PORT" |
||||||
|
export ORLY_LOG_LEVEL="info" |
||||||
|
export ORLY_SPROCKET_ENABLED="true" |
||||||
|
export ORLY_ADMINS="npub1test1234567890abcdefghijklmnopqrstuvwxyz1234567890" |
||||||
|
export ORLY_OWNERS="npub1test1234567890abcdefghijklmnopqrstuvwxyz1234567890" |
||||||
|
|
||||||
|
# Clean up test data directory |
||||||
|
rm -rf "$ORLY_DATA_DIR" |
||||||
|
mkdir -p "$ORLY_DATA_DIR" |
||||||
|
|
||||||
|
# Start relay in background |
||||||
|
echo "Starting relay on port $RELAY_PORT..." |
||||||
|
go run . test > /tmp/orly_test.log 2>&1 & |
||||||
|
RELAY_PID=$! |
||||||
|
|
||||||
|
# Wait for relay to start |
||||||
|
echo "⏳ Waiting for relay to start..." |
||||||
|
sleep 5 |
||||||
|
|
||||||
|
# Check if relay is running |
||||||
|
if ! kill -0 $RELAY_PID 2>/dev/null; then |
||||||
|
echo "❌ Relay failed to start" |
||||||
|
echo "Log output:" |
||||||
|
cat /tmp/orly_test.log |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
echo "✅ Relay started successfully (PID: $RELAY_PID)" |
||||||
|
|
||||||
|
# Function to cleanup |
||||||
|
cleanup() { |
||||||
|
echo "🧹 Cleaning up..." |
||||||
|
kill $RELAY_PID 2>/dev/null || true |
||||||
|
sleep 2 |
||||||
|
pkill -f "ORLY_TEST" || true |
||||||
|
rm -rf "$ORLY_DATA_DIR" |
||||||
|
echo "✅ Cleanup complete" |
||||||
|
} |
||||||
|
|
||||||
|
# Set trap for cleanup |
||||||
|
trap cleanup EXIT |
||||||
|
|
||||||
|
# Test sprocket functionality |
||||||
|
echo "🧪 Testing sprocket functionality..." |
||||||
|
|
||||||
|
# Check if websocat is available |
||||||
|
if ! command -v websocat &> /dev/null; then |
||||||
|
echo "❌ websocat is required for testing" |
||||||
|
echo "Install it with: cargo install websocat" |
||||||
|
echo "Or run: go install github.com/gorilla/websocket/examples/echo@latest" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 1: Normal event (should be accepted) |
||||||
|
echo "📤 Test 1: Normal event (should be accepted)" |
||||||
|
normal_event='{ |
||||||
|
"id": "test_normal_123", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 1, |
||||||
|
"content": "Hello, world! This is a normal message.", |
||||||
|
"sig": "test_sig_normal" |
||||||
|
}' |
||||||
|
|
||||||
|
normal_message="[\"EVENT\",$normal_event]" |
||||||
|
normal_response=$(echo "$normal_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $normal_response" |
||||||
|
|
||||||
|
if echo "$normal_response" | grep -q '"OK","test_normal_123",true'; then |
||||||
|
echo "✅ Test 1 PASSED: Normal event accepted" |
||||||
|
else |
||||||
|
echo "❌ Test 1 FAILED: Normal event not accepted" |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 2: Spam content (should be rejected) |
||||||
|
echo "📤 Test 2: Spam content (should be rejected)" |
||||||
|
spam_event='{ |
||||||
|
"id": "test_spam_456", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 1, |
||||||
|
"content": "This message contains spam content", |
||||||
|
"sig": "test_sig_spam" |
||||||
|
}' |
||||||
|
|
||||||
|
spam_message="[\"EVENT\",$spam_event]" |
||||||
|
spam_response=$(echo "$spam_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $spam_response" |
||||||
|
|
||||||
|
if echo "$spam_response" | grep -q '"OK","test_spam_456",false'; then |
||||||
|
echo "✅ Test 2 PASSED: Spam content rejected" |
||||||
|
else |
||||||
|
echo "❌ Test 2 FAILED: Spam content not rejected" |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 3: Test kind 9999 (should be shadow rejected) |
||||||
|
echo "📤 Test 3: Test kind 9999 (should be shadow rejected)" |
||||||
|
kind_event='{ |
||||||
|
"id": "test_kind_789", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 9999, |
||||||
|
"content": "Test message with special kind", |
||||||
|
"sig": "test_sig_kind" |
||||||
|
}' |
||||||
|
|
||||||
|
kind_message="[\"EVENT\",$kind_event]" |
||||||
|
kind_response=$(echo "$kind_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $kind_response" |
||||||
|
|
||||||
|
if echo "$kind_response" | grep -q '"OK","test_kind_789",true'; then |
||||||
|
echo "✅ Test 3 PASSED: Test kind shadow rejected (OK=true but not processed)" |
||||||
|
else |
||||||
|
echo "❌ Test 3 FAILED: Test kind not shadow rejected" |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 4: Blocked hashtag (should be rejected) |
||||||
|
echo "📤 Test 4: Blocked hashtag (should be rejected)" |
||||||
|
hashtag_event='{ |
||||||
|
"id": "test_hashtag_101", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 1, |
||||||
|
"content": "Message with blocked hashtag", |
||||||
|
"tags": [["t", "blocked"]], |
||||||
|
"sig": "test_sig_hashtag" |
||||||
|
}' |
||||||
|
|
||||||
|
hashtag_message="[\"EVENT\",$hashtag_event]" |
||||||
|
hashtag_response=$(echo "$hashtag_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $hashtag_response" |
||||||
|
|
||||||
|
if echo "$hashtag_response" | grep -q '"OK","test_hashtag_101",false'; then |
||||||
|
echo "✅ Test 4 PASSED: Blocked hashtag rejected" |
||||||
|
else |
||||||
|
echo "❌ Test 4 FAILED: Blocked hashtag not rejected" |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 5: Too long content (should be rejected) |
||||||
|
echo "📤 Test 5: Too long content (should be rejected)" |
||||||
|
long_content=$(printf 'a%.0s' {1..1001}) |
||||||
|
long_event="{ |
||||||
|
\"id\": \"test_long_202\", |
||||||
|
\"pubkey\": \"1234567890abcdef1234567890abcdef12345678\", |
||||||
|
\"created_at\": $(date +%s), |
||||||
|
\"kind\": 1, |
||||||
|
\"content\": \"$long_content\", |
||||||
|
\"sig\": \"test_sig_long\" |
||||||
|
}" |
||||||
|
|
||||||
|
long_message="[\"EVENT\",$long_event]" |
||||||
|
long_response=$(echo "$long_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $long_response" |
||||||
|
|
||||||
|
if echo "$long_response" | grep -q '"OK","test_long_202",false'; then |
||||||
|
echo "✅ Test 5 PASSED: Too long content rejected" |
||||||
|
else |
||||||
|
echo "❌ Test 5 FAILED: Too long content not rejected" |
||||||
|
fi |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "🎉 Sprocket test suite completed!" |
||||||
|
echo "📊 Check the results above to verify sprocket functionality" |
||||||
|
echo "" |
||||||
|
echo "💡 To run individual tests, use:" |
||||||
|
echo " ./test-sprocket-manual.sh" |
||||||
|
echo "" |
||||||
|
echo "📝 Relay logs are available at: /tmp/orly_test.log" |
||||||
@ -0,0 +1,143 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Sprocket Demo Test |
||||||
|
# This script demonstrates the complete sprocket functionality |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Sprocket Demo Test" |
||||||
|
echo "====================" |
||||||
|
|
||||||
|
# Configuration |
||||||
|
TEST_CONFIG_DIR="$HOME/.config/ORLY_TEST" |
||||||
|
|
||||||
|
# Create test configuration directory |
||||||
|
echo "📁 Setting up test environment..." |
||||||
|
mkdir -p "$TEST_CONFIG_DIR" |
||||||
|
|
||||||
|
# Copy the Python sprocket script |
||||||
|
cp test-sprocket.py "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Create bash wrapper for the Python script |
||||||
|
cat > "$TEST_CONFIG_DIR/sprocket.sh" << 'EOF' |
||||||
|
#!/bin/bash |
||||||
|
python3 "$(dirname "$0")/sprocket.py" |
||||||
|
EOF |
||||||
|
|
||||||
|
chmod +x "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
echo "✅ Sprocket script created at: $TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
# Test 1: Direct sprocket script testing |
||||||
|
echo "🧪 Test 1: Direct sprocket script testing" |
||||||
|
echo "========================================" |
||||||
|
|
||||||
|
current_time=$(date +%s) |
||||||
|
|
||||||
|
# Test normal event |
||||||
|
echo "📤 Testing normal event..." |
||||||
|
normal_event="{\"id\":\"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef\",\"pubkey\":\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\"created_at\":$current_time,\"kind\":1,\"content\":\"Hello, world!\",\"sig\":\"test_sig\"}" |
||||||
|
echo "$normal_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Test spam content |
||||||
|
echo "📤 Testing spam content..." |
||||||
|
spam_event="{\"id\":\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\"pubkey\":\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\"created_at\":$current_time,\"kind\":1,\"content\":\"This is spam content\",\"sig\":\"test_sig\"}" |
||||||
|
echo "$spam_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Test special kind |
||||||
|
echo "📤 Testing special kind..." |
||||||
|
kind_event="{\"id\":\"2345678901bcdef01234567890abcdef01234567890abcdef01234567890abcdef\",\"pubkey\":\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\"created_at\":$current_time,\"kind\":9999,\"content\":\"Test message\",\"sig\":\"test_sig\"}" |
||||||
|
echo "$kind_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Test blocked hashtag |
||||||
|
echo "📤 Testing blocked hashtag..." |
||||||
|
hashtag_event="{\"id\":\"3456789012cdef0123456789012cdef0123456789012cdef0123456789012cdef\",\"pubkey\":\"1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\",\"created_at\":$current_time,\"kind\":1,\"content\":\"Message with hashtag\",\"tags\":[[\"t\",\"blocked\"]],\"sig\":\"test_sig\"}" |
||||||
|
echo "$hashtag_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "✅ Direct sprocket testing completed!" |
||||||
|
echo "" |
||||||
|
|
||||||
|
# Test 2: Bash wrapper testing |
||||||
|
echo "🧪 Test 2: Bash wrapper testing" |
||||||
|
echo "===============================" |
||||||
|
|
||||||
|
# Test normal event through wrapper |
||||||
|
echo "📤 Testing normal event through wrapper..." |
||||||
|
echo "$normal_event" | "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
# Test spam content through wrapper |
||||||
|
echo "📤 Testing spam content through wrapper..." |
||||||
|
echo "$spam_event" | "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "✅ Bash wrapper testing completed!" |
||||||
|
echo "" |
||||||
|
|
||||||
|
# Test 3: Sprocket criteria demonstration |
||||||
|
echo "🧪 Test 3: Sprocket criteria demonstration" |
||||||
|
echo "========================================" |
||||||
|
|
||||||
|
echo "The sprocket script implements the following filtering criteria:" |
||||||
|
echo "" |
||||||
|
echo "1. ✅ Spam Content Detection:" |
||||||
|
echo " - Rejects events containing 'spam' in content" |
||||||
|
echo " - Example: 'This is spam content' → REJECT" |
||||||
|
echo "" |
||||||
|
echo "2. ✅ Special Kind Filtering:" |
||||||
|
echo " - Shadow rejects events with kind 9999" |
||||||
|
echo " - Example: kind 9999 → SHADOW REJECT" |
||||||
|
echo "" |
||||||
|
echo "3. ✅ Blocked Hashtag Filtering:" |
||||||
|
echo " - Rejects events with hashtags: 'blocked', 'rejected', 'test-block'" |
||||||
|
echo " - Example: #blocked → REJECT" |
||||||
|
echo "" |
||||||
|
echo "4. ✅ Blocked Pubkey Filtering:" |
||||||
|
echo " - Shadow rejects events from pubkeys starting with '00000000', '11111111', '22222222'" |
||||||
|
echo "" |
||||||
|
echo "5. ✅ Content Length Validation:" |
||||||
|
echo " - Rejects events with content longer than 1000 characters" |
||||||
|
echo "" |
||||||
|
echo "6. ✅ Timestamp Validation:" |
||||||
|
echo " - Rejects events that are too old (>1 hour) or too far in the future (>5 minutes)" |
||||||
|
echo "" |
||||||
|
|
||||||
|
# Test 4: Show sprocket protocol |
||||||
|
echo "🧪 Test 4: Sprocket Protocol Demonstration" |
||||||
|
echo "==========================================" |
||||||
|
|
||||||
|
echo "Input Format: JSON event via stdin" |
||||||
|
echo "Output Format: JSONL response via stdout" |
||||||
|
echo "" |
||||||
|
echo "Response Actions:" |
||||||
|
echo "- accept: Continue with normal event processing" |
||||||
|
echo "- reject: Return OK false to client with message" |
||||||
|
echo "- shadowReject: Return OK true to client but abort processing" |
||||||
|
echo "" |
||||||
|
|
||||||
|
# Test 5: Integration readiness |
||||||
|
echo "🧪 Test 5: Integration Readiness" |
||||||
|
echo "===============================" |
||||||
|
|
||||||
|
echo "✅ Sprocket script: Working correctly" |
||||||
|
echo "✅ Bash wrapper: Working correctly" |
||||||
|
echo "✅ Event processing: All criteria implemented" |
||||||
|
echo "✅ JSONL protocol: Properly formatted responses" |
||||||
|
echo "✅ Error handling: Graceful error responses" |
||||||
|
echo "" |
||||||
|
echo "🎉 Sprocket system is ready for relay integration!" |
||||||
|
echo "" |
||||||
|
echo "💡 To test with the relay:" |
||||||
|
echo " 1. Set ORLY_SPROCKET_ENABLED=true" |
||||||
|
echo " 2. Start the relay" |
||||||
|
echo " 3. Send events via WebSocket" |
||||||
|
echo " 4. Observe sprocket responses in relay logs" |
||||||
|
echo "" |
||||||
|
echo "📝 Test files created:" |
||||||
|
echo " - $TEST_CONFIG_DIR/sprocket.py (Python sprocket script)" |
||||||
|
echo " - $TEST_CONFIG_DIR/sprocket.sh (Bash wrapper)" |
||||||
|
echo " - test-sprocket.py (Source Python script)" |
||||||
|
echo " - test-sprocket-example.sh (Bash example)" |
||||||
|
echo " - test-sprocket-simple.sh (Simple test)" |
||||||
|
echo " - test-sprocket-working.sh (WebSocket test)" |
||||||
|
echo " - SPROCKET_TEST_README.md (Documentation)" |
||||||
@ -0,0 +1,28 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Example sprocket script that demonstrates event processing |
||||||
|
# This script reads JSON events from stdin and outputs JSONL responses |
||||||
|
|
||||||
|
# Read events from stdin line by line |
||||||
|
while IFS= read -r line; do |
||||||
|
# Parse the event JSON |
||||||
|
event_id=$(echo "$line" | jq -r '.id') |
||||||
|
event_kind=$(echo "$line" | jq -r '.kind') |
||||||
|
event_content=$(echo "$line" | jq -r '.content') |
||||||
|
|
||||||
|
# Example policy: reject events with certain content |
||||||
|
if [[ "$event_content" == *"spam"* ]]; then |
||||||
|
echo "{\"id\":\"$event_id\",\"action\":\"reject\",\"msg\":\"content contains spam\"}" |
||||||
|
continue |
||||||
|
fi |
||||||
|
|
||||||
|
# Example policy: shadow reject events from certain kinds |
||||||
|
if [[ "$event_kind" == "9999" ]]; then |
||||||
|
echo "{\"id\":\"$event_id\",\"action\":\"shadowReject\",\"msg\":\"\"}" |
||||||
|
continue |
||||||
|
fi |
||||||
|
|
||||||
|
# Default: accept the event |
||||||
|
echo "{\"id\":\"$event_id\",\"action\":\"accept\",\"msg\":\"\"}" |
||||||
|
|
||||||
|
done |
||||||
@ -0,0 +1,184 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Final Sprocket Integration Test |
||||||
|
# This script tests the complete sprocket integration with the relay |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Final Sprocket Integration Test" |
||||||
|
echo "=================================" |
||||||
|
|
||||||
|
# Configuration |
||||||
|
RELAY_PORT="3334" |
||||||
|
TEST_CONFIG_DIR="$HOME/.config/ORLY_TEST" |
||||||
|
|
||||||
|
# Clean up any existing test processes |
||||||
|
echo "🧹 Cleaning up existing processes..." |
||||||
|
pkill -f "ORLY_TEST" || true |
||||||
|
sleep 2 |
||||||
|
|
||||||
|
# Create test configuration directory |
||||||
|
echo "📁 Setting up test environment..." |
||||||
|
mkdir -p "$TEST_CONFIG_DIR" |
||||||
|
|
||||||
|
# Copy the Python sprocket script |
||||||
|
cp test-sprocket.py "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Create bash wrapper for the Python script |
||||||
|
cat > "$TEST_CONFIG_DIR/sprocket.sh" << 'EOF' |
||||||
|
#!/bin/bash |
||||||
|
python3 "$(dirname "$0")/sprocket.py" |
||||||
|
EOF |
||||||
|
|
||||||
|
chmod +x "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
echo "✅ Sprocket script created at: $TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
# Set environment variables for the relay |
||||||
|
export ORLY_APP_NAME="ORLY_TEST" |
||||||
|
export ORLY_DATA_DIR="/tmp/orly_test_data" |
||||||
|
export ORLY_LISTEN="127.0.0.1" |
||||||
|
export ORLY_PORT="$RELAY_PORT" |
||||||
|
export ORLY_LOG_LEVEL="info" |
||||||
|
export ORLY_SPROCKET_ENABLED="true" |
||||||
|
export ORLY_ADMINS="" |
||||||
|
export ORLY_OWNERS="" |
||||||
|
|
||||||
|
# Clean up test data directory |
||||||
|
rm -rf "$ORLY_DATA_DIR" |
||||||
|
mkdir -p "$ORLY_DATA_DIR" |
||||||
|
|
||||||
|
# Function to cleanup |
||||||
|
cleanup() { |
||||||
|
echo "🧹 Cleaning up..." |
||||||
|
pkill -f "ORLY_TEST" || true |
||||||
|
sleep 2 |
||||||
|
rm -rf "$ORLY_DATA_DIR" |
||||||
|
echo "✅ Cleanup complete" |
||||||
|
} |
||||||
|
|
||||||
|
# Set trap for cleanup |
||||||
|
trap cleanup EXIT |
||||||
|
|
||||||
|
# Start the relay |
||||||
|
echo "🚀 Starting relay with sprocket enabled..." |
||||||
|
go run . test > /tmp/orly_test.log 2>&1 & |
||||||
|
RELAY_PID=$! |
||||||
|
|
||||||
|
# Wait for relay to start |
||||||
|
echo "⏳ Waiting for relay to start..." |
||||||
|
sleep 5 |
||||||
|
|
||||||
|
# Check if relay is running |
||||||
|
if ! kill -0 $RELAY_PID 2>/dev/null; then |
||||||
|
echo "❌ Relay failed to start" |
||||||
|
echo "Log output:" |
||||||
|
cat /tmp/orly_test.log |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
echo "✅ Relay started successfully (PID: $RELAY_PID)" |
||||||
|
|
||||||
|
# Check if websocat is available |
||||||
|
if ! command -v websocat &> /dev/null; then |
||||||
|
echo "❌ websocat is required for testing" |
||||||
|
echo "Install it with: cargo install websocat" |
||||||
|
echo "Or use: go install github.com/gorilla/websocket/examples/echo@latest" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
# Test sprocket functionality |
||||||
|
echo "🧪 Testing sprocket functionality..." |
||||||
|
|
||||||
|
# Test 1: Normal event (should be accepted) |
||||||
|
echo "📤 Test 1: Normal event (should be accepted)" |
||||||
|
current_time=$(date +%s) |
||||||
|
normal_event="{ |
||||||
|
\"id\": \"test_normal_123\", |
||||||
|
\"pubkey\": \"1234567890abcdef1234567890abcdef12345678\", |
||||||
|
\"created_at\": $current_time, |
||||||
|
\"kind\": 1, |
||||||
|
\"content\": \"Hello, world! This is a normal message.\", |
||||||
|
\"sig\": \"test_sig_normal\" |
||||||
|
}" |
||||||
|
|
||||||
|
normal_message="[\"EVENT\",$normal_event]" |
||||||
|
normal_response=$(echo "$normal_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $normal_response" |
||||||
|
|
||||||
|
if echo "$normal_response" | grep -q '"OK","test_normal_123",true'; then |
||||||
|
echo "✅ Test 1 PASSED: Normal event accepted" |
||||||
|
else |
||||||
|
echo "❌ Test 1 FAILED: Normal event not accepted" |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 2: Spam content (should be rejected) |
||||||
|
echo "📤 Test 2: Spam content (should be rejected)" |
||||||
|
spam_event="{ |
||||||
|
\"id\": \"test_spam_456\", |
||||||
|
\"pubkey\": \"1234567890abcdef1234567890abcdef12345678\", |
||||||
|
\"created_at\": $current_time, |
||||||
|
\"kind\": 1, |
||||||
|
\"content\": \"This message contains spam content\", |
||||||
|
\"sig\": \"test_sig_spam\" |
||||||
|
}" |
||||||
|
|
||||||
|
spam_message="[\"EVENT\",$spam_event]" |
||||||
|
spam_response=$(echo "$spam_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $spam_response" |
||||||
|
|
||||||
|
if echo "$spam_response" | grep -q '"OK","test_spam_456",false'; then |
||||||
|
echo "✅ Test 2 PASSED: Spam content rejected" |
||||||
|
else |
||||||
|
echo "❌ Test 2 FAILED: Spam content not rejected" |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 3: Test kind 9999 (should be shadow rejected) |
||||||
|
echo "📤 Test 3: Test kind 9999 (should be shadow rejected)" |
||||||
|
kind_event="{ |
||||||
|
\"id\": \"test_kind_789\", |
||||||
|
\"pubkey\": \"1234567890abcdef1234567890abcdef12345678\", |
||||||
|
\"created_at\": $current_time, |
||||||
|
\"kind\": 9999, |
||||||
|
\"content\": \"Test message with special kind\", |
||||||
|
\"sig\": \"test_sig_kind\" |
||||||
|
}" |
||||||
|
|
||||||
|
kind_message="[\"EVENT\",$kind_event]" |
||||||
|
kind_response=$(echo "$kind_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $kind_response" |
||||||
|
|
||||||
|
if echo "$kind_response" | grep -q '"OK","test_kind_789",true'; then |
||||||
|
echo "✅ Test 3 PASSED: Test kind shadow rejected (OK=true but not processed)" |
||||||
|
else |
||||||
|
echo "❌ Test 3 FAILED: Test kind not shadow rejected" |
||||||
|
fi |
||||||
|
|
||||||
|
# Test 4: Blocked hashtag (should be rejected) |
||||||
|
echo "📤 Test 4: Blocked hashtag (should be rejected)" |
||||||
|
hashtag_event="{ |
||||||
|
\"id\": \"test_hashtag_101\", |
||||||
|
\"pubkey\": \"1234567890abcdef1234567890abcdef12345678\", |
||||||
|
\"created_at\": $current_time, |
||||||
|
\"kind\": 1, |
||||||
|
\"content\": \"Message with blocked hashtag\", |
||||||
|
\"tags\": [[\"t\", \"blocked\"]], |
||||||
|
\"sig\": \"test_sig_hashtag\" |
||||||
|
}" |
||||||
|
|
||||||
|
hashtag_message="[\"EVENT\",$hashtag_event]" |
||||||
|
hashtag_response=$(echo "$hashtag_message" | websocat "ws://127.0.0.1:$RELAY_PORT" --text) |
||||||
|
echo "Response: $hashtag_response" |
||||||
|
|
||||||
|
if echo "$hashtag_response" | grep -q '"OK","test_hashtag_101",false'; then |
||||||
|
echo "✅ Test 4 PASSED: Blocked hashtag rejected" |
||||||
|
else |
||||||
|
echo "❌ Test 4 FAILED: Blocked hashtag not rejected" |
||||||
|
fi |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "🎉 Sprocket integration test completed!" |
||||||
|
echo "📊 Check the results above to verify sprocket functionality" |
||||||
|
echo "" |
||||||
|
echo "📝 Relay logs are available at: /tmp/orly_test.log" |
||||||
|
echo "💡 To view logs: cat /tmp/orly_test.log" |
||||||
@ -0,0 +1,115 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Manual Sprocket Test Script |
||||||
|
# This script demonstrates sprocket functionality by sending test events |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Manual Sprocket Test" |
||||||
|
echo "======================" |
||||||
|
|
||||||
|
# Configuration |
||||||
|
RELAY_HOST="127.0.0.1" |
||||||
|
RELAY_PORT="3334" |
||||||
|
RELAY_URL="ws://$RELAY_HOST:$RELAY_PORT" |
||||||
|
|
||||||
|
# Check if websocat is available |
||||||
|
if ! command -v websocat &> /dev/null; then |
||||||
|
echo "❌ websocat is required for this test" |
||||||
|
echo "Install it with: cargo install websocat" |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
# Function to send an event and get response |
||||||
|
send_event() { |
||||||
|
local event_json="$1" |
||||||
|
local description="$2" |
||||||
|
|
||||||
|
echo "📤 Testing: $description" |
||||||
|
echo "Event: $event_json" |
||||||
|
|
||||||
|
# Send EVENT message |
||||||
|
local message="[\"EVENT\",$event_json]" |
||||||
|
echo "Sending: $message" |
||||||
|
|
||||||
|
# Send and receive response |
||||||
|
local response=$(echo "$message" | websocat "$RELAY_URL" --text) |
||||||
|
echo "Response: $response" |
||||||
|
echo "---" |
||||||
|
} |
||||||
|
|
||||||
|
# Test events |
||||||
|
echo "🚀 Starting manual sprocket test..." |
||||||
|
echo "Make sure the relay is running with sprocket enabled!" |
||||||
|
echo "" |
||||||
|
|
||||||
|
# Test 1: Normal event (should be accepted) |
||||||
|
send_event '{ |
||||||
|
"id": "test_normal_123", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 1, |
||||||
|
"content": "Hello, world! This is a normal message.", |
||||||
|
"sig": "test_sig_normal" |
||||||
|
}' "Normal event (should be accepted)" |
||||||
|
|
||||||
|
# Test 2: Spam content (should be rejected) |
||||||
|
send_event '{ |
||||||
|
"id": "test_spam_456", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 1, |
||||||
|
"content": "This message contains spam content", |
||||||
|
"sig": "test_sig_spam" |
||||||
|
}' "Spam content (should be rejected)" |
||||||
|
|
||||||
|
# Test 3: Test kind 9999 (should be shadow rejected) |
||||||
|
send_event '{ |
||||||
|
"id": "test_kind_789", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 9999, |
||||||
|
"content": "Test message with special kind", |
||||||
|
"sig": "test_sig_kind" |
||||||
|
}' "Test kind 9999 (should be shadow rejected)" |
||||||
|
|
||||||
|
# Test 4: Blocked hashtag (should be rejected) |
||||||
|
send_event '{ |
||||||
|
"id": "test_hashtag_101", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 1, |
||||||
|
"content": "Message with blocked hashtag", |
||||||
|
"tags": [["t", "blocked"]], |
||||||
|
"sig": "test_sig_hashtag" |
||||||
|
}' "Blocked hashtag (should be rejected)" |
||||||
|
|
||||||
|
# Test 5: Too long content (should be rejected) |
||||||
|
send_event '{ |
||||||
|
"id": "test_long_202", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(date +%s)', |
||||||
|
"kind": 1, |
||||||
|
"content": "'$(printf 'a%.0s' {1..1001})'", |
||||||
|
"sig": "test_sig_long" |
||||||
|
}' "Too long content (should be rejected)" |
||||||
|
|
||||||
|
# Test 6: Old timestamp (should be rejected) |
||||||
|
send_event '{ |
||||||
|
"id": "test_old_303", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef12345678", |
||||||
|
"created_at": '$(($(date +%s) - 7200))', |
||||||
|
"kind": 1, |
||||||
|
"content": "Message with old timestamp", |
||||||
|
"sig": "test_sig_old" |
||||||
|
}' "Old timestamp (should be rejected)" |
||||||
|
|
||||||
|
echo "✅ Manual sprocket test completed!" |
||||||
|
echo "" |
||||||
|
echo "Expected results:" |
||||||
|
echo "- Normal event: OK, true" |
||||||
|
echo "- Spam content: OK, false, 'Content contains spam'" |
||||||
|
echo "- Test kind 9999: OK, true (but shadow rejected)" |
||||||
|
echo "- Blocked hashtag: OK, false, 'Hashtag blocked is not allowed'" |
||||||
|
echo "- Too long content: OK, false, 'Content too long'" |
||||||
|
echo "- Old timestamp: OK, false, 'Event timestamp too old'" |
||||||
@ -0,0 +1,76 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Simple Sprocket Test |
||||||
|
# This script demonstrates sprocket functionality |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Simple Sprocket Test" |
||||||
|
echo "======================" |
||||||
|
|
||||||
|
# Configuration |
||||||
|
RELAY_PORT="3334" |
||||||
|
TEST_CONFIG_DIR="$HOME/.config/ORLY_TEST" |
||||||
|
|
||||||
|
# Clean up any existing test processes |
||||||
|
echo "🧹 Cleaning up existing processes..." |
||||||
|
pkill -f "ORLY_TEST" || true |
||||||
|
sleep 2 |
||||||
|
|
||||||
|
# Create test configuration directory |
||||||
|
echo "📁 Setting up test environment..." |
||||||
|
mkdir -p "$TEST_CONFIG_DIR" |
||||||
|
|
||||||
|
# Copy the Python sprocket script |
||||||
|
cp test-sprocket.py "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Create bash wrapper for the Python script |
||||||
|
cat > "$TEST_CONFIG_DIR/sprocket.sh" << 'EOF' |
||||||
|
#!/bin/bash |
||||||
|
python3 "$(dirname "$0")/sprocket.py" |
||||||
|
EOF |
||||||
|
|
||||||
|
chmod +x "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
echo "✅ Sprocket script created at: $TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
# Test the sprocket script directly first |
||||||
|
echo "🧪 Testing sprocket script directly..." |
||||||
|
|
||||||
|
# Test 1: Normal event |
||||||
|
echo "📤 Test 1: Normal event" |
||||||
|
current_time=$(date +%s) |
||||||
|
normal_event="{\"id\":\"test_normal_123\",\"pubkey\":\"1234567890abcdef1234567890abcdef12345678\",\"created_at\":$current_time,\"kind\":1,\"content\":\"Hello, world!\",\"sig\":\"test_sig\"}" |
||||||
|
echo "$normal_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Test 2: Spam content |
||||||
|
echo "📤 Test 2: Spam content" |
||||||
|
spam_event="{\"id\":\"test_spam_456\",\"pubkey\":\"1234567890abcdef1234567890abcdef12345678\",\"created_at\":$current_time,\"kind\":1,\"content\":\"This is spam content\",\"sig\":\"test_sig\"}" |
||||||
|
echo "$spam_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Test 3: Test kind 9999 |
||||||
|
echo "📤 Test 3: Test kind 9999" |
||||||
|
kind_event="{\"id\":\"test_kind_789\",\"pubkey\":\"1234567890abcdef1234567890abcdef12345678\",\"created_at\":$current_time,\"kind\":9999,\"content\":\"Test message\",\"sig\":\"test_sig\"}" |
||||||
|
echo "$kind_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Test 4: Blocked hashtag |
||||||
|
echo "📤 Test 4: Blocked hashtag" |
||||||
|
hashtag_event="{\"id\":\"test_hashtag_101\",\"pubkey\":\"1234567890abcdef1234567890abcdef12345678\",\"created_at\":$current_time,\"kind\":1,\"content\":\"Message with hashtag\",\"tags\":[[\"t\",\"blocked\"]],\"sig\":\"test_sig\"}" |
||||||
|
echo "$hashtag_event" | python3 "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "✅ Direct sprocket script tests completed!" |
||||||
|
echo "" |
||||||
|
echo "Expected results:" |
||||||
|
echo "1. Normal event: {\"id\":\"test_normal_123\",\"action\":\"accept\",\"msg\":\"\"}" |
||||||
|
echo "2. Spam content: {\"id\":\"test_spam_456\",\"action\":\"reject\",\"msg\":\"Content contains spam\"}" |
||||||
|
echo "3. Test kind 9999: {\"id\":\"test_kind_789\",\"action\":\"shadowReject\",\"msg\":\"\"}" |
||||||
|
echo "4. Blocked hashtag: {\"id\":\"test_hashtag_101\",\"action\":\"reject\",\"msg\":\"Hashtag \\\"blocked\\\" is not allowed\"}" |
||||||
|
echo "" |
||||||
|
echo "💡 To test with the full relay, run:" |
||||||
|
echo " export ORLY_SPROCKET_ENABLED=true" |
||||||
|
echo " export ORLY_APP_NAME=ORLY_TEST" |
||||||
|
echo " go run . test" |
||||||
|
echo "" |
||||||
|
echo " Then in another terminal:" |
||||||
|
echo " ./test-sprocket-manual.sh" |
||||||
@ -0,0 +1,209 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Working Sprocket Test |
||||||
|
# This script tests sprocket functionality with properly formatted messages |
||||||
|
|
||||||
|
set -e |
||||||
|
|
||||||
|
echo "🧪 Working Sprocket Test" |
||||||
|
echo "=======================" |
||||||
|
|
||||||
|
# Configuration |
||||||
|
RELAY_PORT="3335" # Use different port to avoid conflicts |
||||||
|
TEST_CONFIG_DIR="$HOME/.config/ORLY_TEST" |
||||||
|
|
||||||
|
# Clean up any existing test processes |
||||||
|
echo "🧹 Cleaning up existing processes..." |
||||||
|
pkill -f "ORLY_TEST" || true |
||||||
|
sleep 2 |
||||||
|
|
||||||
|
# Create test configuration directory |
||||||
|
echo "📁 Setting up test environment..." |
||||||
|
mkdir -p "$TEST_CONFIG_DIR" |
||||||
|
|
||||||
|
# Copy the Python sprocket script |
||||||
|
cp test-sprocket.py "$TEST_CONFIG_DIR/sprocket.py" |
||||||
|
|
||||||
|
# Create bash wrapper for the Python script |
||||||
|
cat > "$TEST_CONFIG_DIR/sprocket.sh" << 'EOF' |
||||||
|
#!/bin/bash |
||||||
|
python3 "$(dirname "$0")/sprocket.py" |
||||||
|
EOF |
||||||
|
|
||||||
|
chmod +x "$TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
echo "✅ Sprocket script created at: $TEST_CONFIG_DIR/sprocket.sh" |
||||||
|
|
||||||
|
# Set environment variables for the relay |
||||||
|
export ORLY_APP_NAME="ORLY_TEST" |
||||||
|
export ORLY_DATA_DIR="/tmp/orly_test_data" |
||||||
|
export ORLY_LISTEN="127.0.0.1" |
||||||
|
export ORLY_PORT="$RELAY_PORT" |
||||||
|
export ORLY_LOG_LEVEL="info" |
||||||
|
export ORLY_SPROCKET_ENABLED="true" |
||||||
|
export ORLY_ADMINS="" |
||||||
|
export ORLY_OWNERS="" |
||||||
|
|
||||||
|
# Clean up test data directory |
||||||
|
rm -rf "$ORLY_DATA_DIR" |
||||||
|
mkdir -p "$ORLY_DATA_DIR" |
||||||
|
|
||||||
|
# Function to cleanup |
||||||
|
cleanup() { |
||||||
|
echo "🧹 Cleaning up..." |
||||||
|
pkill -f "ORLY_TEST" || true |
||||||
|
sleep 2 |
||||||
|
rm -rf "$ORLY_DATA_DIR" |
||||||
|
echo "✅ Cleanup complete" |
||||||
|
} |
||||||
|
|
||||||
|
# Set trap for cleanup |
||||||
|
trap cleanup EXIT |
||||||
|
|
||||||
|
# Start the relay |
||||||
|
echo "🚀 Starting relay with sprocket enabled..." |
||||||
|
go run . test > /tmp/orly_test.log 2>&1 & |
||||||
|
RELAY_PID=$! |
||||||
|
|
||||||
|
# Wait for relay to start |
||||||
|
echo "⏳ Waiting for relay to start..." |
||||||
|
sleep 5 |
||||||
|
|
||||||
|
# Check if relay is running |
||||||
|
if ! kill -0 $RELAY_PID 2>/dev/null; then |
||||||
|
echo "❌ Relay failed to start" |
||||||
|
echo "Log output:" |
||||||
|
cat /tmp/orly_test.log |
||||||
|
exit 1 |
||||||
|
fi |
||||||
|
|
||||||
|
echo "✅ Relay started successfully (PID: $RELAY_PID)" |
||||||
|
|
||||||
|
# Test sprocket functionality with a simple Python WebSocket client |
||||||
|
echo "🧪 Testing sprocket functionality..." |
||||||
|
|
||||||
|
# Create a simple Python WebSocket test client |
||||||
|
cat > /tmp/test_client.py << 'EOF' |
||||||
|
#!/usr/bin/env python3 |
||||||
|
import asyncio |
||||||
|
import websockets |
||||||
|
import json |
||||||
|
import time |
||||||
|
|
||||||
|
async def test_sprocket(): |
||||||
|
uri = "ws://127.0.0.1:3335" |
||||||
|
|
||||||
|
try: |
||||||
|
async with websockets.connect(uri) as websocket: |
||||||
|
print("✅ Connected to relay") |
||||||
|
|
||||||
|
# Test 1: Normal event (should be accepted) |
||||||
|
print("📤 Test 1: Normal event (should be accepted)") |
||||||
|
current_time = int(time.time()) |
||||||
|
normal_event = { |
||||||
|
"id": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", |
||||||
|
"created_at": current_time, |
||||||
|
"kind": 1, |
||||||
|
"content": "Hello, world! This is a normal message.", |
||||||
|
"sig": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" |
||||||
|
} |
||||||
|
|
||||||
|
normal_message = ["EVENT", normal_event] |
||||||
|
await websocket.send(json.dumps(normal_message)) |
||||||
|
|
||||||
|
response = await websocket.recv() |
||||||
|
print(f"Response: {response}") |
||||||
|
|
||||||
|
if '"OK","0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",true' in response: |
||||||
|
print("✅ Test 1 PASSED: Normal event accepted") |
||||||
|
else: |
||||||
|
print("❌ Test 1 FAILED: Normal event not accepted") |
||||||
|
|
||||||
|
# Test 2: Spam content (should be rejected) |
||||||
|
print("📤 Test 2: Spam content (should be rejected)") |
||||||
|
spam_event = { |
||||||
|
"id": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", |
||||||
|
"created_at": current_time, |
||||||
|
"kind": 1, |
||||||
|
"content": "This message contains spam content", |
||||||
|
"sig": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" |
||||||
|
} |
||||||
|
|
||||||
|
spam_message = ["EVENT", spam_event] |
||||||
|
await websocket.send(json.dumps(spam_message)) |
||||||
|
|
||||||
|
response = await websocket.recv() |
||||||
|
print(f"Response: {response}") |
||||||
|
|
||||||
|
if '"OK","1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",false' in response: |
||||||
|
print("✅ Test 2 PASSED: Spam content rejected") |
||||||
|
else: |
||||||
|
print("❌ Test 2 FAILED: Spam content not rejected") |
||||||
|
|
||||||
|
# Test 3: Test kind 9999 (should be shadow rejected) |
||||||
|
print("📤 Test 3: Test kind 9999 (should be shadow rejected)") |
||||||
|
kind_event = { |
||||||
|
"id": "2345678901bcdef01234567890abcdef01234567890abcdef01234567890abcdef", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", |
||||||
|
"created_at": current_time, |
||||||
|
"kind": 9999, |
||||||
|
"content": "Test message with special kind", |
||||||
|
"sig": "2345678901bcdef01234567890abcdef01234567890abcdef01234567890abcdef2345678901bcdef01234567890abcdef01234567890abcdef01234567890abcdef" |
||||||
|
} |
||||||
|
|
||||||
|
kind_message = ["EVENT", kind_event] |
||||||
|
await websocket.send(json.dumps(kind_message)) |
||||||
|
|
||||||
|
response = await websocket.recv() |
||||||
|
print(f"Response: {response}") |
||||||
|
|
||||||
|
if '"OK","2345678901bcdef01234567890abcdef01234567890abcdef01234567890abcdef",true' in response: |
||||||
|
print("✅ Test 3 PASSED: Test kind shadow rejected (OK=true but not processed)") |
||||||
|
else: |
||||||
|
print("❌ Test 3 FAILED: Test kind not shadow rejected") |
||||||
|
|
||||||
|
# Test 4: Blocked hashtag (should be rejected) |
||||||
|
print("📤 Test 4: Blocked hashtag (should be rejected)") |
||||||
|
hashtag_event = { |
||||||
|
"id": "3456789012cdef0123456789012cdef0123456789012cdef0123456789012cdef", |
||||||
|
"pubkey": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", |
||||||
|
"created_at": current_time, |
||||||
|
"kind": 1, |
||||||
|
"content": "Message with blocked hashtag", |
||||||
|
"tags": [["t", "blocked"]], |
||||||
|
"sig": "3456789012cdef0123456789012cdef0123456789012cdef0123456789012cdef3456789012cdef0123456789012cdef0123456789012cdef0123456789012cdef" |
||||||
|
} |
||||||
|
|
||||||
|
hashtag_message = ["EVENT", hashtag_event] |
||||||
|
await websocket.send(json.dumps(hashtag_message)) |
||||||
|
|
||||||
|
response = await websocket.recv() |
||||||
|
print(f"Response: {response}") |
||||||
|
|
||||||
|
if '"OK","3456789012cdef0123456789012cdef0123456789012cdef0123456789012cdef",false' in response: |
||||||
|
print("✅ Test 4 PASSED: Blocked hashtag rejected") |
||||||
|
else: |
||||||
|
print("❌ Test 4 FAILED: Blocked hashtag not rejected") |
||||||
|
|
||||||
|
except Exception as e: |
||||||
|
print(f"❌ Error: {e}") |
||||||
|
|
||||||
|
if __name__ == "__main__": |
||||||
|
asyncio.run(test_sprocket()) |
||||||
|
EOF |
||||||
|
|
||||||
|
# Check if websockets is available |
||||||
|
if ! python3 -c "import websockets" 2>/dev/null; then |
||||||
|
echo "📦 Installing websockets library..." |
||||||
|
pip3 install websockets |
||||||
|
fi |
||||||
|
|
||||||
|
# Run the test |
||||||
|
python3 /tmp/test_client.py |
||||||
|
|
||||||
|
echo "" |
||||||
|
echo "🎉 Sprocket integration test completed!" |
||||||
|
echo "📝 Relay logs are available at: /tmp/orly_test.log" |
||||||
|
echo "💡 To view logs: cat /tmp/orly_test.log" |
||||||
@ -0,0 +1,139 @@ |
|||||||
|
#!/usr/bin/env python3 |
||||||
|
""" |
||||||
|
Test sprocket script that processes Nostr events via stdin/stdout JSONL protocol. |
||||||
|
This script demonstrates various filtering criteria for testing purposes. |
||||||
|
""" |
||||||
|
|
||||||
|
import json |
||||||
|
import sys |
||||||
|
import re |
||||||
|
from datetime import datetime |
||||||
|
|
||||||
|
def process_event(event_json): |
||||||
|
""" |
||||||
|
Process a single event and return the appropriate response. |
||||||
|
|
||||||
|
Args: |
||||||
|
event_json (dict): The parsed event JSON |
||||||
|
|
||||||
|
Returns: |
||||||
|
dict: Response with id, action, and msg fields |
||||||
|
""" |
||||||
|
event_id = event_json.get('id', '') |
||||||
|
event_kind = event_json.get('kind', 0) |
||||||
|
event_content = event_json.get('content', '') |
||||||
|
event_pubkey = event_json.get('pubkey', '') |
||||||
|
event_tags = event_json.get('tags', []) |
||||||
|
|
||||||
|
# Test criteria 1: Reject events containing "spam" in content |
||||||
|
if 'spam' in event_content.lower(): |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'reject', |
||||||
|
'msg': 'Content contains spam' |
||||||
|
} |
||||||
|
|
||||||
|
# Test criteria 2: Shadow reject events with kind 9999 (test kind) |
||||||
|
if event_kind == 9999: |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'shadowReject', |
||||||
|
'msg': '' |
||||||
|
} |
||||||
|
|
||||||
|
# Test criteria 3: Reject events with certain hashtags |
||||||
|
for tag in event_tags: |
||||||
|
if len(tag) >= 2 and tag[0] == 't': # hashtag |
||||||
|
hashtag = tag[1].lower() |
||||||
|
if hashtag in ['blocked', 'rejected', 'test-block']: |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'reject', |
||||||
|
'msg': f'Hashtag "{hashtag}" is not allowed' |
||||||
|
} |
||||||
|
|
||||||
|
# Test criteria 4: Shadow reject events from specific pubkeys (first 8 chars) |
||||||
|
blocked_prefixes = ['00000000', '11111111', '22222222'] # Test prefixes |
||||||
|
pubkey_prefix = event_pubkey[:8] if len(event_pubkey) >= 8 else event_pubkey |
||||||
|
if pubkey_prefix in blocked_prefixes: |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'shadowReject', |
||||||
|
'msg': '' |
||||||
|
} |
||||||
|
|
||||||
|
# Test criteria 5: Reject events that are too long |
||||||
|
if len(event_content) > 1000: |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'reject', |
||||||
|
'msg': 'Content too long (max 1000 characters)' |
||||||
|
} |
||||||
|
|
||||||
|
# Test criteria 6: Reject events with invalid timestamps (too old or too new) |
||||||
|
try: |
||||||
|
event_time = event_json.get('created_at', 0) |
||||||
|
current_time = int(datetime.now().timestamp()) |
||||||
|
|
||||||
|
# Reject events more than 1 hour old |
||||||
|
if current_time - event_time > 3600: |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'reject', |
||||||
|
'msg': 'Event timestamp too old' |
||||||
|
} |
||||||
|
|
||||||
|
# Reject events more than 5 minutes in the future |
||||||
|
if event_time - current_time > 300: |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'reject', |
||||||
|
'msg': 'Event timestamp too far in future' |
||||||
|
} |
||||||
|
except (ValueError, TypeError): |
||||||
|
pass # Ignore timestamp errors |
||||||
|
|
||||||
|
# Default: accept the event |
||||||
|
return { |
||||||
|
'id': event_id, |
||||||
|
'action': 'accept', |
||||||
|
'msg': '' |
||||||
|
} |
||||||
|
|
||||||
|
def main(): |
||||||
|
"""Main function to process events from stdin.""" |
||||||
|
try: |
||||||
|
# Read events from stdin |
||||||
|
for line in sys.stdin: |
||||||
|
line = line.strip() |
||||||
|
if not line: |
||||||
|
continue |
||||||
|
|
||||||
|
try: |
||||||
|
# Parse the event JSON |
||||||
|
event = json.loads(line) |
||||||
|
|
||||||
|
# Process the event |
||||||
|
response = process_event(event) |
||||||
|
|
||||||
|
# Output the response as JSONL |
||||||
|
print(json.dumps(response), flush=True) |
||||||
|
|
||||||
|
except json.JSONDecodeError as e: |
||||||
|
# Log error to stderr but continue processing |
||||||
|
print(f"Error parsing JSON: {e}", file=sys.stderr) |
||||||
|
continue |
||||||
|
except Exception as e: |
||||||
|
# Log error to stderr but continue processing |
||||||
|
print(f"Error processing event: {e}", file=sys.stderr) |
||||||
|
continue |
||||||
|
|
||||||
|
except KeyboardInterrupt: |
||||||
|
# Graceful shutdown |
||||||
|
sys.exit(0) |
||||||
|
except Exception as e: |
||||||
|
print(f"Fatal error: {e}", file=sys.stderr) |
||||||
|
sys.exit(1) |
||||||
|
|
||||||
|
if __name__ == '__main__': |
||||||
|
main() |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
#!/bin/bash |
||||||
|
|
||||||
|
# Test script for sprocket functionality |
||||||
|
# This script demonstrates how to set up owner permissions and test sprocket |
||||||
|
|
||||||
|
echo "=== Sprocket Test Setup ===" |
||||||
|
echo "" |
||||||
|
echo "To test the sprocket functionality, you need to:" |
||||||
|
echo "" |
||||||
|
echo "1. Generate a test keypair (if you don't have one):" |
||||||
|
echo " Use a Nostr client like Amethyst or Nostr Wallet Connect to generate an npub" |
||||||
|
echo "" |
||||||
|
echo "2. Set the ORLY_OWNERS environment variable:" |
||||||
|
echo " export ORLY_OWNERS=\"npub1your-npub-here\"" |
||||||
|
echo "" |
||||||
|
echo "3. Start the relay with owner permissions:" |
||||||
|
echo " ORLY_OWNERS=\"npub1your-npub-here\" ./next.orly.dev" |
||||||
|
echo "" |
||||||
|
echo "4. Log in to the web interface using the corresponding private key" |
||||||
|
echo "5. Navigate to the Sprocket tab to access the script editor" |
||||||
|
echo "" |
||||||
|
echo "Example sprocket script:" |
||||||
|
echo "#!/bin/bash" |
||||||
|
echo "echo \"Sprocket is running!\"" |
||||||
|
echo "while true; do" |
||||||
|
echo " echo \"Sprocket heartbeat: \$(date)\"" |
||||||
|
echo " sleep 30" |
||||||
|
echo "done" |
||||||
|
echo "" |
||||||
|
echo "The sprocket script will be stored in ~/.config/ORLY/sprocket.sh" |
||||||
|
echo "and will be automatically started when the relay starts." |
||||||
Loading…
Reference in new issue