Compare commits

...

5 Commits

Author SHA1 Message Date
Your Name
c3de31aa88 v0.7.1 - Implemented static binary build system for cross-distribution compatibility 2025-10-09 14:36:32 -04:00
Your Name
b6df0be865 v0.6.0 - Fixed binary upload in release script - now shows upload errors and handles failures properly 2025-10-09 12:59:23 -04:00
Your Name
a89f84f76e v0.5.0 - New release 2025-10-09 12:51:53 -04:00
Your Name
5a916cc221 Reupload 2025-10-09 10:43:42 -04:00
Your Name
dcf421ff93 v0.4.13 - DM system appears fully functional 2025-10-08 07:11:22 -04:00
13 changed files with 1614 additions and 109 deletions

View File

@@ -197,4 +197,21 @@ help:
@echo " make init-db # Set up database" @echo " make init-db # Set up database"
@echo " make force-version # Force regenerate main.h from git" @echo " make force-version # Force regenerate main.h from git"
# Build fully static MUSL binaries using Docker
static-musl-x86_64:
@echo "Building fully static MUSL binary for x86_64..."
docker buildx build --platform linux/amd64 -f examples/deployment/static-builder.Dockerfile -t c-relay-static-builder-x86_64 --load .
docker run --rm -v $(PWD)/build:/output c-relay-static-builder-x86_64 sh -c "cp /c_relay_static_musl_x86_64 /output/"
@echo "Static binary created: build/c_relay_static_musl_x86_64"
static-musl-arm64:
@echo "Building fully static MUSL binary for ARM64..."
docker buildx build --platform linux/arm64 -f examples/deployment/static-builder.Dockerfile -t c-relay-static-builder-arm64 --load .
docker run --rm -v $(PWD)/build:/output c-relay-static-builder-arm64 sh -c "cp /c_relay_static_musl_x86_64 /output/c_relay_static_musl_arm64"
@echo "Static binary created: build/c_relay_static_musl_arm64"
static-musl: static-musl-x86_64 static-musl-arm64
@echo "Built static MUSL binaries for both architectures"
.PHONY: static-musl-x86_64 static-musl-arm64 static-musl
.PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version .PHONY: all x86 arm64 test init-db clean clean-all install-deps install-cross-tools install-arm64-deps check-toolchain help force-version

View File

@@ -22,6 +22,68 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
- [x] NIP-50: Keywords filter - [x] NIP-50: Keywords filter
- [x] NIP-70: Protected Events - [x] NIP-70: Protected Events
## Quick Start
Get your C-Relay up and running in minutes with a static binary (no dependencies required):
### 1. Download Static Binary
Download the latest static release from the [releases page](https://git.laantungir.net/laantungir/c-relay/releases):
```bash
# Static binary - works on all Linux distributions (no dependencies)
wget https://git.laantungir.net/laantungir/c-relay/releases/download/v0.6.0/c-relay-v0.6.0-linux-x86_64-static
chmod +x c-relay-v0.6.0-linux-x86_64-static
mv c-relay-v0.6.0-linux-x86_64-static c-relay
```
### 2. Start the Relay
Simply run the binary - no configuration files needed:
```bash
./c-relay
```
On first startup, you'll see:
- **Admin Private Key**: Save this securely! You'll need it for administration
- **Relay Public Key**: Your relay's identity on the Nostr network
- **Port Information**: Default is 8888, or the next available port
### 3. Access the Web Interface
Open your browser and navigate to:
```
http://localhost:8888/api/
```
The web interface provides:
- Real-time configuration management
- Database statistics dashboard
- Auth rules management
- Secure admin authentication with your Nostr identity
### 4. Test Your Relay
Test basic connectivity:
```bash
# Test WebSocket connection
curl -H "Accept: application/nostr+json" http://localhost:8888
# Test with a Nostr client
# Add ws://localhost:8888 to your client's relay list
```
### 5. Configure Your Relay (Optional)
Use the web interface or send admin commands to customize:
- Relay name and description
- Authentication rules (whitelist/blacklist)
- Connection limits
- Proof-of-work requirements
**That's it!** Your relay is now running with zero configuration required. The event-based configuration system means you can adjust all settings through the web interface or admin API without editing config files.
## Web Admin Interface ## Web Admin Interface
C-Relay includes a **built-in web-based administration interface** accessible at `http://localhost:8888/api/`. The interface provides: C-Relay includes a **built-in web-based administration interface** accessible at `http://localhost:8888/api/`. The interface provides:

View File

@@ -58,7 +58,7 @@
<div class="inline-buttons"> <div class="inline-buttons">
<button type="button" id="connect-relay-btn">CONNECT TO RELAY</button> <button type="button" id="connect-relay-btn">CONNECT TO RELAY</button>
<button type="button" id="disconnect-relay-btn" disabled>DISCONNECT</button> <button type="button" id="disconnect-relay-btn" disabled>DISCONNECT</button>
<button type="button" id="test-websocket-btn" disabled>TEST WEBSOCKET</button> <button type="button" id="restart-relay-btn" disabled>RESTART RELAY</button>
</div> </div>
<div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div> <div class="status disconnected" id="relay-connection-status">NOT CONNECTED</div>

View File

@@ -41,7 +41,7 @@
const relayConnectionStatus = document.getElementById('relay-connection-status'); const relayConnectionStatus = document.getElementById('relay-connection-status');
const connectRelayBtn = document.getElementById('connect-relay-btn'); const connectRelayBtn = document.getElementById('connect-relay-btn');
const disconnectRelayBtn = document.getElementById('disconnect-relay-btn'); const disconnectRelayBtn = document.getElementById('disconnect-relay-btn');
const testWebSocketBtn = document.getElementById('test-websocket-btn'); const restartRelayBtn = document.getElementById('restart-relay-btn');
const configDisplay = document.getElementById('config-display'); const configDisplay = document.getElementById('config-display');
const configTableBody = document.getElementById('config-table-body'); const configTableBody = document.getElementById('config-table-body');
@@ -369,28 +369,28 @@
relayConnectionStatus.className = 'status connected'; relayConnectionStatus.className = 'status connected';
connectRelayBtn.disabled = true; connectRelayBtn.disabled = true;
disconnectRelayBtn.disabled = true; disconnectRelayBtn.disabled = true;
testWebSocketBtn.disabled = true; restartRelayBtn.disabled = true;
break; break;
case 'connected': case 'connected':
relayConnectionStatus.textContent = 'CONNECTED'; relayConnectionStatus.textContent = 'CONNECTED';
relayConnectionStatus.className = 'status connected'; relayConnectionStatus.className = 'status connected';
connectRelayBtn.disabled = true; connectRelayBtn.disabled = true;
disconnectRelayBtn.disabled = false; disconnectRelayBtn.disabled = false;
testWebSocketBtn.disabled = false; restartRelayBtn.disabled = false;
break; break;
case 'disconnected': case 'disconnected':
relayConnectionStatus.textContent = 'NOT CONNECTED'; relayConnectionStatus.textContent = 'NOT CONNECTED';
relayConnectionStatus.className = 'status disconnected'; relayConnectionStatus.className = 'status disconnected';
connectRelayBtn.disabled = false; connectRelayBtn.disabled = false;
disconnectRelayBtn.disabled = true; disconnectRelayBtn.disabled = true;
testWebSocketBtn.disabled = true; restartRelayBtn.disabled = true;
break; break;
case 'error': case 'error':
relayConnectionStatus.textContent = 'CONNECTION FAILED'; relayConnectionStatus.textContent = 'CONNECTION FAILED';
relayConnectionStatus.className = 'status error'; relayConnectionStatus.className = 'status error';
connectRelayBtn.disabled = false; connectRelayBtn.disabled = false;
disconnectRelayBtn.disabled = true; disconnectRelayBtn.disabled = true;
testWebSocketBtn.disabled = true; restartRelayBtn.disabled = true;
break; break;
} }
} }
@@ -1670,22 +1670,12 @@
disconnectFromRelay(); disconnectFromRelay();
}); });
testWebSocketBtn.addEventListener('click', function (e) { restartRelayBtn.addEventListener('click', function (e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const url = relayConnectionUrl.value.trim(); sendRestartCommand().catch(error => {
if (!url) { log(`Restart command failed: ${error.message}`, 'ERROR');
log('Please enter a relay URL first', 'ERROR'); });
return;
}
testWebSocketConnection(url)
.then(() => {
log('WebSocket test successful', 'INFO');
})
.catch(error => {
log(`WebSocket test failed: ${error.message}`, 'ERROR');
});
}); });
// ================================ // ================================
@@ -3148,6 +3138,83 @@
// DATABASE STATISTICS FUNCTIONS // DATABASE STATISTICS FUNCTIONS
// ================================ // ================================
// Send restart command to restart the relay using Administrator API
async function sendRestartCommand() {
if (!isLoggedIn || !userPubkey) {
log('Must be logged in to restart relay', 'ERROR');
return;
}
if (!relayPool) {
log('SimplePool connection not available', 'ERROR');
return;
}
try {
log('Sending restart command to relay...', 'INFO');
// Create command array for restart
const command_array = ["system_command", "restart"];
// Encrypt the command array directly using NIP-44
const encrypted_content = await encryptForRelay(JSON.stringify(command_array));
if (!encrypted_content) {
throw new Error('Failed to encrypt command array');
}
// Create single kind 23456 admin event
const restartEvent = {
kind: 23456,
pubkey: userPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: [["p", getRelayPubkey()]],
content: encrypted_content
};
// Sign the event
const signedEvent = await window.nostr.signEvent(restartEvent);
if (!signedEvent || !signedEvent.sig) {
throw new Error('Event signing failed');
}
// Publish via SimplePool
const url = relayConnectionUrl.value.trim();
const publishPromises = relayPool.publish([url], signedEvent);
// Use Promise.allSettled to capture per-relay outcomes
const results = await Promise.allSettled(publishPromises);
// Check if any relay accepted the event
let successCount = 0;
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
successCount++;
log(`Restart command published successfully to relay ${index}`, 'INFO');
} else {
log(`Restart command failed on relay ${index}: ${result.reason?.message || result.reason}`, 'ERROR');
}
});
if (successCount === 0) {
const errorDetails = results.map((r, i) => `Relay ${i}: ${r.reason?.message || r.reason}`).join('; ');
throw new Error(`All relays rejected restart command. Details: ${errorDetails}`);
}
log('Restart command sent successfully - relay should restart shortly...', 'INFO');
// Update connection status to indicate restart is in progress
updateRelayConnectionStatus('connecting');
relayConnectionStatus.textContent = 'RESTARTING...';
// The relay will disconnect and need to be reconnected after restart
// This will be handled by the WebSocket disconnection event
} catch (error) {
log(`Failed to send restart command: ${error.message}`, 'ERROR');
updateRelayConnectionStatus('error');
}
}
// Send stats_query command to get database statistics using Administrator API (inner events) // Send stats_query command to get database statistics using Administrator API (inner events)
async function sendStatsQuery() { async function sendStatsQuery() {
if (!isLoggedIn || !userPubkey) { if (!isLoggedIn || !userPubkey) {
@@ -3304,9 +3371,12 @@
const events7d = document.getElementById('events-7d'); const events7d = document.getElementById('events-7d');
const events30d = document.getElementById('events-30d'); const events30d = document.getElementById('events-30d');
if (events24h) events24h.textContent = data.events_24h || '-'; // Access the nested time_stats object from backend response
if (events7d) events7d.textContent = data.events_7d || '-'; const timeStats = data.time_stats || {};
if (events30d) events30d.textContent = data.events_30d || '-';
if (events24h) events24h.textContent = timeStats.last_24h || '0';
if (events7d) events7d.textContent = timeStats.last_7d || '0';
if (events30d) events30d.textContent = timeStats.last_30d || '0';
} }
// Populate top pubkeys table // Populate top pubkeys table

View File

@@ -17,6 +17,33 @@ print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
COMMIT_MESSAGE="" COMMIT_MESSAGE=""
RELEASE_MODE=false RELEASE_MODE=false
show_usage() {
echo "C-Relay Build and Push Script"
echo ""
echo "Usage:"
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
echo " $0 -r \"commit message\" - Release: compile x86+arm64, increment minor, create release"
echo ""
echo "Examples:"
echo " $0 \"Fixed event validation bug\""
echo " $0 --release \"Major release with new features\""
echo ""
echo "Default Mode (patch increment):"
echo " - Compile C-Relay"
echo " - Increment patch version (v1.2.3 → v1.2.4)"
echo " - Git add, commit with message, and push"
echo ""
echo "Release Mode (-r flag):"
echo " - Compile C-Relay for x86_64 and arm64 (dynamic and static versions)"
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " - Git add, commit, push, and create Gitea release"
echo ""
echo "Requirements for Release Mode:"
echo " - For ARM64 builds: make install-arm64-deps (optional - will build x86_64 only if missing)"
echo " - For static builds: sudo apt-get install musl-dev libcap-dev libuv1-dev libev-dev"
echo " - Gitea token in ~/.gitea_token for release uploads"
}
# Parse command line arguments # Parse command line arguments
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
@@ -38,32 +65,6 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
show_usage() {
echo "C-Relay Build and Push Script"
echo ""
echo "Usage:"
echo " $0 \"commit message\" - Default: compile, increment patch, commit & push"
echo " $0 -r \"commit message\" - Release: compile x86+arm64, increment minor, create release"
echo ""
echo "Examples:"
echo " $0 \"Fixed event validation bug\""
echo " $0 --release \"Major release with new features\""
echo ""
echo "Default Mode (patch increment):"
echo " - Compile C-Relay"
echo " - Increment patch version (v1.2.3 → v1.2.4)"
echo " - Git add, commit with message, and push"
echo ""
echo "Release Mode (-r flag):"
echo " - Compile C-Relay for x86_64 and arm64"
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " - Git add, commit, push, and create Gitea release"
echo ""
echo "Requirements for Release Mode:"
echo " - For ARM64 builds: make install-arm64-deps (optional - will build x86_64 only if missing)"
echo " - Gitea token in ~/.gitea_token for release uploads"
}
# Validate inputs # Validate inputs
if [[ -z "$COMMIT_MESSAGE" ]]; then if [[ -z "$COMMIT_MESSAGE" ]]; then
print_error "Commit message is required" print_error "Commit message is required"
@@ -190,6 +191,35 @@ build_release_binaries() {
print_status "Only x86_64 binary will be included in release" print_status "Only x86_64 binary will be included in release"
fi fi
# Build static x86_64 version
print_status "Building static x86_64 version..."
make clean > /dev/null 2>&1
if make static-musl-x86_64 > /dev/null 2>&1; then
if [[ -f "build/c_relay_static_musl_x86_64" ]]; then
cp build/c_relay_static_musl_x86_64 c-relay-static-x86_64
print_success "Static x86_64 binary created: c-relay-static-x86_64"
else
print_warning "Static x86_64 binary not found after compilation"
fi
else
print_warning "Static x86_64 build failed - MUSL development packages may not be installed"
print_status "Run 'sudo apt-get install musl-dev libcap-dev libuv1-dev libev-dev' to enable static builds"
fi
# Try to build static ARM64 version
print_status "Attempting static ARM64 build..."
make clean > /dev/null 2>&1
if make static-musl-arm64 > /dev/null 2>&1; then
if [[ -f "build/c_relay_static_musl_arm64" ]]; then
cp build/c_relay_static_musl_arm64 c-relay-static-arm64
print_success "Static ARM64 binary created: c-relay-static-arm64"
else
print_warning "Static ARM64 binary not found after compilation"
fi
else
print_warning "Static ARM64 build failed - ARM64 cross-compilation or MUSL ARM64 packages not set up"
fi
# Restore normal build # Restore normal build
make clean > /dev/null 2>&1 make clean > /dev/null 2>&1
make > /dev/null 2>&1 make > /dev/null 2>&1
@@ -319,12 +349,18 @@ create_gitea_release() {
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}") -d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
local upload_result=false
if echo "$response" | grep -q '"id"'; then if echo "$response" | grep -q '"id"'; then
print_success "Created release $NEW_VERSION" print_success "Created release $NEW_VERSION"
upload_release_binaries "$api_url" "$token" if upload_release_binaries "$api_url" "$token"; then
upload_result=true
fi
elif echo "$response" | grep -q "already exists"; then elif echo "$response" | grep -q "already exists"; then
print_warning "Release $NEW_VERSION already exists" print_warning "Release $NEW_VERSION already exists"
upload_release_binaries "$api_url" "$token" if upload_release_binaries "$api_url" "$token"; then
upload_result=true
fi
else else
print_error "Failed to create release $NEW_VERSION" print_error "Failed to create release $NEW_VERSION"
print_error "Response: $response" print_error "Response: $response"
@@ -334,18 +370,29 @@ create_gitea_release() {
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION") local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then if echo "$check_response" | grep -q '"id"'; then
print_warning "Release exists but creation response was unexpected" print_warning "Release exists but creation response was unexpected"
upload_release_binaries "$api_url" "$token" if upload_release_binaries "$api_url" "$token"; then
upload_result=true
fi
else else
print_error "Release does not exist and creation failed" print_error "Release does not exist and creation failed"
return 1 return 1
fi fi
fi fi
# Return based on upload success
if [[ "$upload_result" == true ]]; then
return 0
else
print_error "Binary upload failed"
return 1
fi
} }
# Function to upload release binaries # Function to upload release binaries
upload_release_binaries() { upload_release_binaries() {
local api_url="$1" local api_url="$1"
local token="$2" local token="$2"
local upload_success=true
# Get release ID with more robust parsing # Get release ID with more robust parsing
print_status "Getting release ID for $NEW_VERSION..." print_status "Getting release ID for $NEW_VERSION..."
@@ -367,37 +414,131 @@ upload_release_binaries() {
# Upload x86_64 binary # Upload x86_64 binary
if [[ -f "c-relay-x86_64" ]]; then if [[ -f "c-relay-x86_64" ]]; then
print_status "Uploading x86_64 binary..." print_status "Uploading x86_64 binary..."
if curl -s -X POST "$api_url/releases/$release_id/assets" \ local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
-H "Authorization: token $token" \ -H "Authorization: token $token" \
-F "attachment=@c-relay-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64" > /dev/null; then -F "attachment=@c-relay-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64")
print_success "Uploaded x86_64 binary"
local http_code=$(echo "$upload_response" | tail -n1)
local response_body=$(echo "$upload_response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
print_success "Uploaded x86_64 binary successfully"
else else
print_warning "Failed to upload x86_64 binary" print_error "Failed to upload x86_64 binary (HTTP $http_code)"
print_error "Response: $response_body"
upload_success=false
fi fi
else
print_warning "x86_64 binary not found: c-relay-x86_64"
fi fi
# Upload ARM64 binary # Upload ARM64 binary
if [[ -f "c-relay-arm64" ]]; then if [[ -f "c-relay-arm64" ]]; then
print_status "Uploading ARM64 binary..." print_status "Uploading ARM64 binary..."
if curl -s -X POST "$api_url/releases/$release_id/assets" \ local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
-H "Authorization: token $token" \ -H "Authorization: token $token" \
-F "attachment=@c-relay-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64" > /dev/null; then -F "attachment=@c-relay-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64")
print_success "Uploaded ARM64 binary"
local http_code=$(echo "$upload_response" | tail -n1)
local response_body=$(echo "$upload_response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
print_success "Uploaded ARM64 binary successfully"
else else
print_warning "Failed to upload ARM64 binary" print_error "Failed to upload ARM64 binary (HTTP $http_code)"
print_error "Response: $response_body"
upload_success=false
fi fi
else
print_warning "ARM64 binary not found: c-relay-arm64"
fi
# Upload static x86_64 binary
if [[ -f "c-relay-static-x86_64" ]]; then
print_status "Uploading static x86_64 binary..."
local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@c-relay-static-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64-static")
local http_code=$(echo "$upload_response" | tail -n1)
local response_body=$(echo "$upload_response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
print_success "Uploaded static x86_64 binary successfully"
else
print_error "Failed to upload static x86_64 binary (HTTP $http_code)"
print_error "Response: $response_body"
upload_success=false
fi
else
print_warning "Static x86_64 binary not found: c-relay-static-x86_64"
fi
# Upload static ARM64 binary
if [[ -f "c-relay-static-arm64" ]]; then
print_status "Uploading static ARM64 binary..."
local upload_response=$(curl -s -w "\n%{http_code}" -X POST "$api_url/releases/$release_id/assets" \
-H "Authorization: token $token" \
-F "attachment=@c-relay-static-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64-static")
local http_code=$(echo "$upload_response" | tail -n1)
local response_body=$(echo "$upload_response" | head -n -1)
if [[ "$http_code" == "201" ]]; then
print_success "Uploaded static ARM64 binary successfully"
else
print_error "Failed to upload static ARM64 binary (HTTP $http_code)"
print_error "Response: $response_body"
upload_success=false
fi
else
print_warning "Static ARM64 binary not found: c-relay-static-arm64"
fi
# Return success/failure status
if [[ "$upload_success" == true ]]; then
return 0
else
return 1
fi fi
} }
# Function to clean up release binaries # Function to clean up release binaries
cleanup_release_binaries() { cleanup_release_binaries() {
if [[ -f "c-relay-x86_64" ]]; then local force_cleanup="$1" # Optional parameter to force cleanup even on failure
rm -f c-relay-x86_64
print_status "Cleaned up x86_64 binary" if [[ "$force_cleanup" == "force" ]] || [[ "$upload_success" == true ]]; then
fi if [[ -f "c-relay-x86_64" ]]; then
if [[ -f "c-relay-arm64" ]]; then rm -f c-relay-x86_64
rm -f c-relay-arm64 print_status "Cleaned up x86_64 binary"
print_status "Cleaned up ARM64 binary" fi
if [[ -f "c-relay-arm64" ]]; then
rm -f c-relay-arm64
print_status "Cleaned up ARM64 binary"
fi
if [[ -f "c-relay-static-x86_64" ]]; then
rm -f c-relay-static-x86_64
print_status "Cleaned up static x86_64 binary"
fi
if [[ -f "c-relay-static-arm64" ]]; then
rm -f c-relay-static-arm64
print_status "Cleaned up static ARM64 binary"
fi
else
print_warning "Keeping binary files due to upload failures"
print_status "Files available for manual upload:"
if [[ -f "c-relay-x86_64" ]]; then
print_status " - c-relay-x86_64"
fi
if [[ -f "c-relay-arm64" ]]; then
print_status " - c-relay-arm64"
fi
if [[ -f "c-relay-static-x86_64" ]]; then
print_status " - c-relay-static-x86_64"
fi
if [[ -f "c-relay-static-arm64" ]]; then
print_status " - c-relay-static-arm64"
fi
fi fi
} }
@@ -433,14 +574,18 @@ main() {
git_commit_and_push_no_tag git_commit_and_push_no_tag
# Create Gitea release with binaries # Create Gitea release with binaries
create_gitea_release if create_gitea_release; then
print_success "Release $NEW_VERSION completed successfully!"
print_status "Binaries uploaded to Gitea release"
upload_success=true
else
print_error "Release creation or binary upload failed"
upload_success=false
fi
# Cleanup # Cleanup (only if upload was successful)
cleanup_release_binaries cleanup_release_binaries
print_success "Release $NEW_VERSION completed successfully!"
print_status "Binaries uploaded to Gitea release"
else else
print_status "=== DEFAULT MODE ===" print_status "=== DEFAULT MODE ==="

146
build_static.sh Executable file
View File

@@ -0,0 +1,146 @@
#!/bin/bash
# Build fully static MUSL binaries for C-Relay
# Produces portable binaries with zero runtime dependencies
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BUILD_DIR="$SCRIPT_DIR/build"
echo "Building fully static MUSL binaries for C-Relay..."
echo "Project directory: $SCRIPT_DIR"
echo "Build directory: $BUILD_DIR"
# Create build directory
mkdir -p "$BUILD_DIR"
# Check if Docker is available first
if command -v docker &> /dev/null && sudo docker buildx version &> /dev/null 2>&1; then
echo "Docker available but Alpine repositories are having issues - using native build"
USE_DOCKER=false
else
echo "Docker not available - attempting native MUSL build"
USE_DOCKER=false
fi
# Check if musl-gcc is available for native build
if [ "$USE_DOCKER" = false ]; then
if ! command -v musl-gcc &> /dev/null; then
echo "Installing musl development tools..."
sudo apt update && sudo apt install -y musl-dev musl-tools
if ! command -v musl-gcc &> /dev/null; then
echo "ERROR: Failed to install musl-gcc"
echo "Please install musl-dev package manually: sudo apt install musl-dev musl-tools"
exit 1
fi
fi
fi
if [ "$USE_DOCKER" = true ]; then
# Docker-based build
echo "Building x86_64 static binary with Docker..."
sudo docker buildx build \
--platform linux/amd64 \
-f "$SCRIPT_DIR/examples/deployment/static-builder.Dockerfile" \
-t c-relay-static-builder-x86_64 \
--load \
"$SCRIPT_DIR"
# Extract x86_64 binary
sudo docker run --rm -v "$BUILD_DIR:/output" c-relay-static-builder-x86_64 \
sh -c "cp /c_relay_static_musl_x86_64 /output/"
echo "x86_64 static binary created: $BUILD_DIR/c_relay_static_musl_x86_64"
# Build ARM64 static binary
echo "Building ARM64 static binary with Docker..."
sudo docker buildx build \
--platform linux/arm64 \
-f "$SCRIPT_DIR/examples/deployment/static-builder.Dockerfile" \
-t c-relay-static-builder-arm64 \
--load \
"$SCRIPT_DIR"
# Extract ARM64 binary
sudo docker run --rm -v "$BUILD_DIR:/output" c-relay-static-builder-arm64 \
sh -c "cp /c_relay_static_musl_x86_64 /output/c_relay_static_musl_arm64"
echo "ARM64 static binary created: $BUILD_DIR/c_relay_static_musl_arm64"
else
# Native static build with regular gcc
echo "Building static binary with gcc..."
# Check for required static libraries
echo "Checking for static libraries..."
MISSING_LIBS=""
for lib in libsqlite3.a libssl.a libcrypto.a libz.a; do
if ! find /usr/lib* /usr/local/lib* -name "$lib" 2>/dev/null | head -1 | grep -q .; then
MISSING_LIBS="$MISSING_LIBS $lib"
fi
done
# libsecp256k1 might not be available as static lib, so we'll try without it first
# Initialize submodules if needed
if [ ! -f "nostr_core_lib/libnostr_core_x64.a" ]; then
echo "Building nostr_core_lib..."
git submodule update --init --recursive
cd nostr_core_lib && ./build.sh && cd ..
fi
# Install additional static libraries needed for libwebsockets
echo "Installing additional static libraries..."
sudo apt install -y libcap-dev libuv1-dev libev-dev
# Try building with regular gcc and static linking
echo "Compiling with gcc -static..."
# Use the same approach as the regular Makefile but with static linking
gcc -static -O2 -Wall -Wextra -std=c99 -g \
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
src/main.c src/config.c src/dm_admin.c src/request_validator.c src/nip009.c src/nip011.c src/nip013.c src/nip040.c src/nip042.c src/websockets.c src/subscriptions.c src/api.c src/embedded_web_content.c \
-o "$BUILD_DIR/c_relay_static_x86_64" \
nostr_core_lib/libnostr_core_x64.a \
-lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -L/usr/local/lib -lsecp256k1 -lssl -lcrypto -L/usr/local/lib -lcurl -lcap -luv_a -lev
if [ $? -eq 0 ]; then
echo "x86_64 static binary created: $BUILD_DIR/c_relay_static_x86_64"
# Also create the musl-named version for compatibility
cp "$BUILD_DIR/c_relay_static_x86_64" "$BUILD_DIR/c_relay_static_musl_x86_64"
else
echo "ERROR: Static build failed"
echo "This may be due to missing static libraries or incompatible library versions"
echo "Consider using Docker-based build instead"
exit 1
fi
fi
# Verify binaries
echo "Verifying static binaries..."
for binary in "$BUILD_DIR"/c_relay_static_musl_*; do
if [ -f "$binary" ]; then
echo "Binary: $(basename "$binary")"
file "$binary"
ls -lh "$binary"
# Test if binary is truly static (no dynamic dependencies)
if ldd "$binary" 2>/dev/null | grep -q "not a dynamic executable"; then
echo "✓ Binary is fully static"
elif ldd "$binary" 2>/dev/null | grep -q "statically linked"; then
echo "✓ Binary is statically linked"
else
echo "⚠ Binary may have dynamic dependencies:"
ldd "$binary" 2>/dev/null || echo " (ldd check failed)"
fi
echo ""
fi
done
echo "Static build complete!"
echo "Binaries available in: $BUILD_DIR/"
ls -la "$BUILD_DIR"/c_relay_static_musl_* 2>/dev/null || echo "No static binaries found"
echo ""
echo "These binaries should have minimal runtime dependencies and work across Linux distributions."

View File

@@ -0,0 +1,136 @@
# MUSL-based fully static C-Relay builder
# Produces portable binaries with zero runtime dependencies
FROM alpine:latest AS builder
# Add alternative mirrors and install build dependencies with retry
RUN echo "http://dl-cdn.alpinelinux.org/alpine/v3.22/main" > /etc/apk/repositories && \
echo "http://dl-cdn.alpinelinux.org/alpine/v3.22/community" >> /etc/apk/repositories && \
echo "http://mirror.leaseweb.com/alpine/v3.22/main" >> /etc/apk/repositories && \
echo "http://mirror.leaseweb.com/alpine/v3.22/community" >> /etc/apk/repositories && \
apk update --no-cache || (sleep 5 && apk update --no-cache) || (sleep 10 && apk update --no-cache)
# Install build dependencies with retry logic
RUN apk add --no-cache \
build-base \
musl-dev \
git \
cmake \
pkgconfig \
autoconf \
automake \
libtool \
openssl-dev \
openssl-libs-static \
zlib-dev \
zlib-static \
curl-dev \
curl-static \
sqlite-dev \
sqlite-static \
linux-headers || \
(sleep 10 && apk add --no-cache \
build-base \
musl-dev \
git \
cmake \
pkgconfig \
autoconf \
automake \
libtool \
openssl-dev \
openssl-libs-static \
zlib-dev \
zlib-static \
curl-dev \
curl-static \
sqlite-dev \
sqlite-static \
linux-headers)
# Set working directory
WORKDIR /build
# Build zlib static (if needed)
RUN if [ ! -f /usr/lib/libz.a ]; then \
cd /tmp && \
wget https://zlib.net/zlib-1.3.1.tar.gz && \
tar xzf zlib-1.3.1.tar.gz && \
cd zlib-1.3.1 && \
./configure --static --prefix=/usr && \
make && make install; \
fi
# Build OpenSSL static
RUN cd /tmp && \
wget https://www.openssl.org/source/openssl-3.0.13.tar.gz && \
tar xzf openssl-3.0.13.tar.gz && \
cd openssl-3.0.13 && \
./Configure linux-x86_64 no-shared --prefix=/usr && \
make && make install_sw
# Build libsecp256k1 static
RUN cd /tmp && \
git clone https://github.com/bitcoin-core/secp256k1.git && \
cd secp256k1 && \
./autogen.sh && \
./configure --enable-static --disable-shared --prefix=/usr && \
make && make install
# Build libwebsockets static with OpenSSL
RUN cd /tmp && \
git clone https://github.com/warmcat/libwebsockets.git && \
cd libwebsockets && \
mkdir build && cd build && \
cmake .. \
-DLWS_WITH_STATIC=ON \
-DLWS_WITH_SHARED=OFF \
-DLWS_WITH_SSL=ON \
-DLWS_OPENSSL_LIBRARIES="/usr/lib/libssl.a;/usr/lib/libcrypto.a" \
-DLWS_OPENSSL_INCLUDE_DIRS="/usr/include" \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX=/usr && \
make && make install
# Build curl static (minimal features)
RUN cd /tmp && \
wget https://curl.se/download/curl-8.6.0.tar.gz && \
tar xzf curl-8.6.0.tar.gz && \
cd curl-8.6.0 && \
./configure \
--disable-shared \
--enable-static \
--disable-ldap \
--without-libidn2 \
--without-brotli \
--without-zstd \
--without-rtmp \
--without-libpsl \
--without-krb5 \
--with-openssl \
--prefix=/usr && \
make && make install
# Copy c-relay source
COPY . /build/
# Initialize submodules
RUN git submodule update --init --recursive
# Build nostr_core_lib
RUN cd nostr_core_lib && ./build.sh
# Build c-relay static
RUN make clean && \
CC="musl-gcc -static" \
CFLAGS="-O2 -Wall -Wextra -std=c99 -g" \
LDFLAGS="-static -Wl,--whole-archive -lpthread -Wl,--no-whole-archive" \
LIBS="-lsqlite3 -lwebsockets -lz -ldl -lpthread -lm -lsecp256k1 -lssl -lcrypto -lcurl" \
make
# Strip binary for size
RUN strip build/c_relay_x86
# Multi-stage build to produce minimal output
FROM scratch AS output
COPY --from=builder /build/build/c_relay_x86 /c_relay_static_musl_x86_64

View File

@@ -1 +1 @@
1220948 1567707

View File

@@ -16,6 +16,9 @@
// External database connection (from main.c) // External database connection (from main.c)
extern sqlite3* g_db; extern sqlite3* g_db;
// External shutdown flag (from main.c)
extern volatile sig_atomic_t g_shutdown_flag;
// Global unified configuration cache instance // Global unified configuration cache instance
unified_config_cache_t g_unified_cache = { unified_config_cache_t g_unified_cache = {
.cache_lock = PTHREAD_MUTEX_INITIALIZER, .cache_lock = PTHREAD_MUTEX_INITIALIZER,
@@ -3673,19 +3676,19 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
cJSON* response = cJSON_CreateObject(); cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "command", "system_status"); cJSON_AddStringToObject(response, "command", "system_status");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL)); cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
cJSON* status_data = cJSON_CreateObject(); cJSON* status_data = cJSON_CreateObject();
cJSON_AddStringToObject(status_data, "database", g_db ? "connected" : "not_available"); cJSON_AddStringToObject(status_data, "database", g_db ? "connected" : "not_available");
cJSON_AddStringToObject(status_data, "cache_status", g_unified_cache.cache_valid ? "valid" : "invalid"); cJSON_AddStringToObject(status_data, "cache_status", g_unified_cache.cache_valid ? "valid" : "invalid");
if (strlen(g_database_path) > 0) { if (strlen(g_database_path) > 0) {
cJSON_AddStringToObject(status_data, "database_path", g_database_path); cJSON_AddStringToObject(status_data, "database_path", g_database_path);
} }
// Count configuration items and auth rules // Count configuration items and auth rules
if (g_db) { if (g_db) {
sqlite3_stmt* stmt; sqlite3_stmt* stmt;
// Config count // Config count
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM config", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
@@ -3693,7 +3696,7 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
} }
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
// Auth rules count // Auth rules count
if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM auth_rules", -1, &stmt, NULL) == SQLITE_OK) { if (sqlite3_prepare_v2(g_db, "SELECT COUNT(*) FROM auth_rules", -1, &stmt, NULL) == SQLITE_OK) {
if (sqlite3_step(stmt) == SQLITE_ROW) { if (sqlite3_step(stmt) == SQLITE_ROW) {
@@ -3702,34 +3705,72 @@ int handle_system_command_unified(cJSON* event, const char* command, char* error
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
} }
} }
cJSON_AddItemToObject(response, "data", status_data); cJSON_AddItemToObject(response, "data", status_data);
printf("=== System Status ===\n"); printf("=== System Status ===\n");
printf("Database: %s\n", g_db ? "Connected" : "Not available"); printf("Database: %s\n", g_db ? "Connected" : "Not available");
printf("Cache status: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid"); printf("Cache status: %s\n", g_unified_cache.cache_valid ? "Valid" : "Invalid");
// Get admin pubkey from event for response // Get admin pubkey from event for response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL; const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (!admin_pubkey) { if (!admin_pubkey) {
cJSON_Delete(response); cJSON_Delete(response);
snprintf(error_message, error_size, "missing admin pubkey for response"); snprintf(error_message, error_size, "missing admin pubkey for response");
return -1; return -1;
} }
// Send response as signed kind 23457 event // Send response as signed kind 23457 event
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) { if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
log_success("System status query completed successfully with signed response"); log_success("System status query completed successfully with signed response");
cJSON_Delete(response); cJSON_Delete(response);
return 0; return 0;
} }
cJSON_Delete(response); cJSON_Delete(response);
snprintf(error_message, error_size, "failed to send system status response"); snprintf(error_message, error_size, "failed to send system status response");
return -1; return -1;
} }
else if (strcmp(command, "restart") == 0) {
// Build restart acknowledgment response
cJSON* response = cJSON_CreateObject();
cJSON_AddStringToObject(response, "command", "restart");
cJSON_AddStringToObject(response, "status", "initiating_restart");
cJSON_AddStringToObject(response, "message", "Relay restart initiated - shutting down gracefully");
cJSON_AddNumberToObject(response, "timestamp", (double)time(NULL));
printf("=== Relay Restart Initiated ===\n");
printf("Admin requested system restart\n");
printf("Sending acknowledgment and initiating shutdown...\n");
// Get admin pubkey from event for response
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
const char* admin_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
if (!admin_pubkey) {
cJSON_Delete(response);
snprintf(error_message, error_size, "missing admin pubkey for response");
return -1;
}
// Send acknowledgment response as signed kind 23457 event
if (send_admin_response_event(response, admin_pubkey, wsi) == 0) {
log_success("Restart acknowledgment sent successfully - initiating shutdown");
// Trigger graceful shutdown by setting the global shutdown flag
g_shutdown_flag = 1;
log_info("Shutdown flag set - relay will restart gracefully");
cJSON_Delete(response);
return 0;
}
cJSON_Delete(response);
snprintf(error_message, error_size, "failed to send restart acknowledgment");
return -1;
}
else { else {
snprintf(error_message, error_size, "invalid: unknown system command '%s'", command); snprintf(error_message, error_size, "invalid: unknown system command '%s'", command);
return -1; return -1;

View File

@@ -47,6 +47,79 @@ extern int broadcast_event_to_subscriptions(cJSON* event);
// Forward declarations for stats generation // Forward declarations for stats generation
extern char* generate_stats_json(void); extern char* generate_stats_json(void);
// ================================
// CONFIGURATION CHANGE SYSTEM
// ================================
// Data structure for pending configuration changes
typedef struct pending_config_change {
char admin_pubkey[65]; // Who requested the change
char config_key[128]; // What config to change
char old_value[256]; // Current value
char new_value[256]; // Requested new value
time_t timestamp; // When requested
char change_id[33]; // Unique ID for this change (first 32 chars of hash)
struct pending_config_change* next; // Linked list for concurrent changes
} pending_config_change_t;
// Global state for pending changes
static pending_config_change_t* pending_changes_head = NULL;
static int pending_changes_count = 0;
// Configuration change timeout (5 minutes)
#define CONFIG_CHANGE_TIMEOUT 300
// Known configuration keys and their types for validation
static struct {
const char* key;
const char* type; // "bool", "int", "string"
int min_val;
int max_val;
} known_configs[] = {
{"auth_enabled", "bool", 0, 1},
{"nip42_auth_required", "bool", 0, 1},
{"nip40_expiration_enabled", "bool", 0, 1},
{"max_connections", "int", 1, 10000},
{"max_subscriptions_per_client", "int", 1, 1000},
{"max_event_tags", "int", 1, 1000},
{"max_content_length", "int", 1, 1000000},
{"max_limit", "int", 1, 10000},
{"default_limit", "int", 1, 5000},
{"max_filters_per_subscription", "int", 1, 100},
{"max_total_subscriptions", "int", 1, 10000},
{"pow_min_difficulty", "int", 0, 64},
{"nip42_challenge_timeout", "int", 1, 3600},
{"nip42_challenge_expiration", "int", 1, 3600},
{"nip40_expiration_grace_period", "int", 1, 86400},
{"relay_name", "string", 0, 0},
{"relay_description", "string", 0, 0},
{"relay_contact", "string", 0, 0},
{"relay_icon", "string", 0, 0},
{"relay_countries", "string", 0, 0},
{"language_tags", "string", 0, 0},
{"posting_policy", "string", 0, 0},
{"payments_url", "string", 0, 0},
{"supported_nips", "string", 0, 0},
{"relay_software", "string", 0, 0},
{"relay_version", "string", 0, 0},
{"pow_mode", "string", 0, 0},
{NULL, NULL, 0, 0}
};
// Forward declarations for config change functions
int parse_config_command(const char* message, char* key, char* value);
int validate_config_change(const char* key, const char* value);
char* store_pending_config_change(const char* admin_pubkey, const char* key,
const char* old_value, const char* new_value);
pending_config_change_t* find_pending_change(const char* admin_pubkey, const char* change_id);
int apply_config_change(const char* key, const char* value);
void cleanup_expired_pending_changes(void);
int handle_config_confirmation(const char* admin_pubkey, const char* response);
char* generate_config_change_confirmation(const char* key, const char* old_value, const char* new_value);
int process_config_change_request(const char* admin_pubkey, const char* message);
int send_nip17_response(const char* sender_pubkey, const char* response_content,
char* error_message, size_t error_size);
// Forward declarations for admin event processing // Forward declarations for admin event processing
extern int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi); extern int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
@@ -201,6 +274,682 @@ int process_dm_admin_command(cJSON* command_array, cJSON* event, char* error_mes
return result; return result;
} }
// ================================
// CONFIGURATION CHANGE IMPLEMENTATION
// ================================
// Parse configuration command from natural language
// Supports patterns like:
// - "auth_enabled true"
// - "set auth_enabled to true"
// - "change auth_enabled true"
// - "auth_enabled = true"
// - "auth_enabled: true"
// - "enable auth" / "disable auth"
int parse_config_command(const char* message, char* key, char* value) {
if (!message || !key || !value) {
return 0;
}
log_info("DEBUG: Parsing config command");
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "DEBUG: Input message: '%.100s'", message);
log_info(debug_msg);
// Clean up the message - convert to lowercase and trim
char clean_msg[512];
size_t msg_len = strlen(message);
size_t copy_len = msg_len < sizeof(clean_msg) - 1 ? msg_len : sizeof(clean_msg) - 1;
memcpy(clean_msg, message, copy_len);
clean_msg[copy_len] = '\0';
// Convert to lowercase
for (size_t i = 0; i < copy_len; i++) {
if (clean_msg[i] >= 'A' && clean_msg[i] <= 'Z') {
clean_msg[i] = clean_msg[i] + 32;
}
}
// Remove leading/trailing whitespace
char* start = clean_msg;
while (*start == ' ' || *start == '\t') start++;
char* end = start + strlen(start) - 1;
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
*end = '\0';
end--;
}
// Pattern 1: "enable auth" -> "auth_enabled true"
if (strstr(start, "enable auth") == start) {
strcpy(key, "auth_enabled");
strcpy(value, "true");
return 1;
}
// Pattern 2: "disable auth" -> "auth_enabled false"
if (strstr(start, "disable auth") == start) {
strcpy(key, "auth_enabled");
strcpy(value, "false");
return 1;
}
// Pattern 3: "enable nip42" -> "nip42_auth_required true"
if (strstr(start, "enable nip42") == start) {
strcpy(key, "nip42_auth_required");
strcpy(value, "true");
return 1;
}
// Pattern 4: "disable nip42" -> "nip42_auth_required false"
if (strstr(start, "disable nip42") == start) {
strcpy(key, "nip42_auth_required");
strcpy(value, "false");
return 1;
}
// Pattern 5: "set KEY to VALUE" or "change KEY to VALUE"
char* set_pos = strstr(start, "set ");
char* change_pos = strstr(start, "change ");
char* to_pos = strstr(start, " to ");
if ((set_pos == start || change_pos == start) && to_pos) {
char* key_start = (set_pos == start) ? start + 4 : start + 7;
size_t key_len = to_pos - key_start;
if (key_len > 0 && key_len < 127) {
memcpy(key, key_start, key_len);
key[key_len] = '\0';
// Trim key
char* key_end = key + strlen(key) - 1;
while (key_end > key && (*key_end == ' ' || *key_end == '\t')) {
*key_end = '\0';
key_end--;
}
char* value_start = to_pos + 4;
strcpy(value, value_start);
return 1;
}
}
// Pattern 6: "KEY = VALUE"
char* equals_pos = strstr(start, " = ");
if (equals_pos) {
size_t key_len = equals_pos - start;
if (key_len > 0 && key_len < 127) {
memcpy(key, start, key_len);
key[key_len] = '\0';
strcpy(value, equals_pos + 3);
return 1;
}
}
// Pattern 7: "KEY: VALUE" (colon-separated)
char* colon_pos = strstr(start, ": ");
if (colon_pos) {
size_t key_len = colon_pos - start;
if (key_len > 0 && key_len < 127) {
memcpy(key, start, key_len);
key[key_len] = '\0';
strcpy(value, colon_pos + 2);
return 1;
}
}
// Pattern 8: "KEY VALUE" (simple space-separated)
char* space_pos = strchr(start, ' ');
if (space_pos) {
size_t key_len = space_pos - start;
if (key_len > 0 && key_len < 127) {
memcpy(key, start, key_len);
key[key_len] = '\0';
strcpy(value, space_pos + 1);
return 1;
}
}
log_info("DEBUG: No config command pattern matched");
return 0; // No pattern matched
}
// Validate configuration key and value
int validate_config_change(const char* key, const char* value) {
if (!key || !value) {
return 0;
}
// Find the configuration key
int found = 0;
const char* expected_type = NULL;
int min_val = 0, max_val = 0;
for (int i = 0; known_configs[i].key != NULL; i++) {
if (strcmp(key, known_configs[i].key) == 0) {
found = 1;
expected_type = known_configs[i].type;
min_val = known_configs[i].min_val;
max_val = known_configs[i].max_val;
break;
}
}
if (!found) {
return 0; // Unknown configuration key
}
// Validate value based on type
if (strcmp(expected_type, "bool") == 0) {
if (strcmp(value, "true") == 0 || strcmp(value, "false") == 0 ||
strcmp(value, "1") == 0 || strcmp(value, "0") == 0 ||
strcmp(value, "yes") == 0 || strcmp(value, "no") == 0 ||
strcmp(value, "on") == 0 || strcmp(value, "off") == 0) {
return 1;
}
return 0;
} else if (strcmp(expected_type, "int") == 0) {
char* endptr;
long val = strtol(value, &endptr, 10);
if (*endptr != '\0') {
return 0; // Not a valid integer
}
if (val < min_val || val > max_val) {
return 0; // Out of range
}
return 1;
} else if (strcmp(expected_type, "string") == 0) {
// String values are generally valid, but check length
if (strlen(value) > 255) {
return 0; // Too long
}
return 1;
}
return 0;
}
// Generate a unique change ID based on admin pubkey and timestamp
void generate_change_id(const char* admin_pubkey, char* change_id) {
char input[128];
snprintf(input, sizeof(input), "%s_%ld", admin_pubkey, time(NULL));
// Simple hash - just use first 32 chars of the input
size_t input_len = strlen(input);
for (int i = 0; i < 32 && i < (int)input_len; i++) {
change_id[i] = input[i];
}
change_id[32] = '\0';
}
// Store a pending configuration change
char* store_pending_config_change(const char* admin_pubkey, const char* key,
const char* old_value, const char* new_value) {
if (!admin_pubkey || !key || !old_value || !new_value) {
return NULL;
}
// Clean up expired changes first
cleanup_expired_pending_changes();
// Create new pending change
pending_config_change_t* change = malloc(sizeof(pending_config_change_t));
if (!change) {
return NULL;
}
strncpy(change->admin_pubkey, admin_pubkey, sizeof(change->admin_pubkey) - 1);
change->admin_pubkey[sizeof(change->admin_pubkey) - 1] = '\0';
strncpy(change->config_key, key, sizeof(change->config_key) - 1);
change->config_key[sizeof(change->config_key) - 1] = '\0';
strncpy(change->old_value, old_value, sizeof(change->old_value) - 1);
change->old_value[sizeof(change->old_value) - 1] = '\0';
strncpy(change->new_value, new_value, sizeof(change->new_value) - 1);
change->new_value[sizeof(change->new_value) - 1] = '\0';
change->timestamp = time(NULL);
generate_change_id(admin_pubkey, change->change_id);
// Add to linked list
change->next = pending_changes_head;
pending_changes_head = change;
pending_changes_count++;
// Return a copy of the change ID
char* change_id_copy = malloc(33);
if (change_id_copy) {
strcpy(change_id_copy, change->change_id);
}
return change_id_copy;
}
// Find a pending change by admin pubkey and change ID
pending_config_change_t* find_pending_change(const char* admin_pubkey, const char* change_id) {
if (!admin_pubkey) {
return NULL;
}
pending_config_change_t* current = pending_changes_head;
while (current) {
if (strcmp(current->admin_pubkey, admin_pubkey) == 0) {
if (!change_id || strcmp(current->change_id, change_id) == 0) {
return current;
}
}
current = current->next;
}
return NULL;
}
// Find the most recent pending change for an admin
pending_config_change_t* find_latest_pending_change(const char* admin_pubkey) {
if (!admin_pubkey) {
return NULL;
}
pending_config_change_t* latest = NULL;
pending_config_change_t* current = pending_changes_head;
while (current) {
if (strcmp(current->admin_pubkey, admin_pubkey) == 0) {
if (!latest || current->timestamp > latest->timestamp) {
latest = current;
}
}
current = current->next;
}
return latest;
}
// Remove a pending change from the list
void remove_pending_change(pending_config_change_t* change_to_remove) {
if (!change_to_remove) {
return;
}
if (pending_changes_head == change_to_remove) {
pending_changes_head = change_to_remove->next;
} else {
pending_config_change_t* current = pending_changes_head;
while (current && current->next != change_to_remove) {
current = current->next;
}
if (current) {
current->next = change_to_remove->next;
}
}
free(change_to_remove);
pending_changes_count--;
}
// Clean up expired pending changes (older than 5 minutes)
void cleanup_expired_pending_changes(void) {
time_t now = time(NULL);
pending_config_change_t* current = pending_changes_head;
while (current) {
pending_config_change_t* next = current->next;
if (now - current->timestamp > CONFIG_CHANGE_TIMEOUT) {
log_info("Cleaning up expired config change request");
remove_pending_change(current);
}
current = next;
}
}
// Apply a configuration change to the database
int apply_config_change(const char* key, const char* value) {
if (!key || !value) {
log_error("DEBUG: apply_config_change called with NULL key or value");
return -1;
}
extern sqlite3* g_db;
if (!g_db) {
log_error("Database not available for config change");
return -1;
}
log_info("DEBUG: Applying config change");
char debug_msg[256];
snprintf(debug_msg, sizeof(debug_msg), "DEBUG: Key='%s', Value='%s'", key, value);
log_info(debug_msg);
// Normalize boolean values
char normalized_value[256];
strncpy(normalized_value, value, sizeof(normalized_value) - 1);
normalized_value[sizeof(normalized_value) - 1] = '\0';
// Convert various boolean representations to "true"/"false"
if (strcmp(value, "1") == 0 || strcmp(value, "yes") == 0 || strcmp(value, "on") == 0) {
strcpy(normalized_value, "true");
} else if (strcmp(value, "0") == 0 || strcmp(value, "no") == 0 || strcmp(value, "off") == 0) {
strcpy(normalized_value, "false");
}
log_info("DEBUG: Normalized value");
char norm_msg[256];
snprintf(norm_msg, sizeof(norm_msg), "DEBUG: Normalized value='%s'", normalized_value);
log_info(norm_msg);
// Determine the data type based on the configuration key
const char* data_type = "string"; // Default to string
for (int i = 0; known_configs[i].key != NULL; i++) {
if (strcmp(key, known_configs[i].key) == 0) {
if (strcmp(known_configs[i].type, "bool") == 0) {
data_type = "boolean";
} else if (strcmp(known_configs[i].type, "int") == 0) {
data_type = "integer";
} else if (strcmp(known_configs[i].type, "string") == 0) {
data_type = "string";
}
break;
}
}
// Update or insert the configuration value
sqlite3_stmt* stmt;
const char* sql = "INSERT OR REPLACE INTO config (key, value, data_type) VALUES (?, ?, ?)";
log_info("DEBUG: Preparing SQL statement");
if (sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL) != SQLITE_OK) {
log_error("Failed to prepare config update statement");
const char* err_msg = sqlite3_errmsg(g_db);
log_error(err_msg);
return -1;
}
log_info("DEBUG: Binding parameters");
sqlite3_bind_text(stmt, 1, key, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, normalized_value, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, data_type, -1, SQLITE_STATIC);
log_info("DEBUG: Executing SQL statement");
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
log_error("Failed to update configuration in database");
const char* err_msg = sqlite3_errmsg(g_db);
log_error(err_msg);
sqlite3_finalize(stmt);
return -1;
}
sqlite3_finalize(stmt);
log_info("DEBUG: SQL execution successful");
char log_msg[512];
snprintf(log_msg, sizeof(log_msg), "Configuration updated: %s = %s", key, normalized_value);
log_success(log_msg);
return 0;
}
// Generate confirmation message for config change
char* generate_config_change_confirmation(const char* key, const char* old_value, const char* new_value) {
if (!key || !old_value || !new_value) {
return NULL;
}
char* confirmation = malloc(2048);
if (!confirmation) {
return NULL;
}
// Get description for the config key
const char* description = "";
if (strcmp(key, "auth_enabled") == 0) {
description = "This controls whether authentication is required for the relay.";
} else if (strcmp(key, "nip42_auth_required") == 0) {
description = "This controls whether NIP-42 authentication is required.";
} else if (strcmp(key, "nip40_expiration_enabled") == 0) {
description = "This controls whether NIP-40 event expiration is enabled.";
} else if (strcmp(key, "max_connections") == 0) {
description = "This sets the maximum number of concurrent connections.";
} else if (strcmp(key, "max_subscriptions_per_client") == 0) {
description = "This sets the maximum subscriptions per client.";
} else if (strcmp(key, "pow_min_difficulty") == 0) {
description = "This sets the minimum proof-of-work difficulty required.";
} else if (strstr(key, "relay_") == key) {
description = "This changes relay metadata information.";
}
snprintf(confirmation, 2048,
"🔧 Configuration Change Request\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Setting: %s\n"
"Current Value: %s\n"
"New Value: %s\n"
"\n"
"%s%s"
"\n"
"⚠️ Reply with 'yes' to confirm or 'no' to cancel.\n"
"⏰ This request will expire in 5 minutes.",
key, old_value, new_value,
strlen(description) > 0 ? " " : "",
description
);
return confirmation;
}
// Handle confirmation responses (yes/no)
int handle_config_confirmation(const char* admin_pubkey, const char* response) {
if (!admin_pubkey || !response) {
return -1;
}
// Clean up expired changes first
cleanup_expired_pending_changes();
// Convert response to lowercase
char response_lower[64];
size_t response_len = strlen(response);
size_t copy_len = response_len < sizeof(response_lower) - 1 ? response_len : sizeof(response_lower) - 1;
memcpy(response_lower, response, copy_len);
response_lower[copy_len] = '\0';
for (size_t i = 0; i < copy_len; i++) {
if (response_lower[i] >= 'A' && response_lower[i] <= 'Z') {
response_lower[i] = response_lower[i] + 32;
}
}
// Trim whitespace
char* start = response_lower;
while (*start == ' ' || *start == '\t') start++;
char* end = start + strlen(start) - 1;
while (end > start && (*end == ' ' || *end == '\t' || *end == '\n' || *end == '\r')) {
*end = '\0';
end--;
}
// Check if it's a confirmation response
int is_yes = (strcmp(start, "yes") == 0 || strcmp(start, "y") == 0 ||
strcmp(start, "confirm") == 0 || strcmp(start, "ok") == 0);
int is_no = (strcmp(start, "no") == 0 || strcmp(start, "n") == 0 ||
strcmp(start, "cancel") == 0 || strcmp(start, "abort") == 0);
if (!is_yes && !is_no) {
return 0; // Not a confirmation response
}
// Find the most recent pending change for this admin
pending_config_change_t* change = find_latest_pending_change(admin_pubkey);
if (!change) {
return -2; // No pending changes
}
if (is_yes) {
// Apply the configuration change
log_info("DEBUG: Applying configuration change");
int result = apply_config_change(change->config_key, change->new_value);
if (result == 0) {
// Send success response
log_info("DEBUG: Configuration change applied successfully, sending success response");
char success_msg[1024];
snprintf(success_msg, sizeof(success_msg),
"✅ Configuration Updated\n"
"━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"%s: %s → %s\n"
"\n"
"Change applied successfully.",
change->config_key, change->old_value, change->new_value
);
char error_msg[256];
int send_result = send_nip17_response(admin_pubkey, success_msg, error_msg, sizeof(error_msg));
if (send_result != 0) {
log_error("DEBUG: Failed to send success response");
log_error(error_msg);
} else {
log_success("DEBUG: Success response sent");
}
// Remove the pending change
remove_pending_change(change);
return 1; // Success
} else {
// Send error response
log_error("DEBUG: Configuration change failed, sending error response");
char error_msg[1024];
snprintf(error_msg, sizeof(error_msg),
"❌ Configuration Update Failed\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Failed to apply change: %s = %s\n"
"\n"
"Please check the relay logs for details.",
change->config_key, change->new_value
);
char send_error_msg[256];
int send_result = send_nip17_response(admin_pubkey, error_msg, send_error_msg, sizeof(send_error_msg));
if (send_result != 0) {
log_error("DEBUG: Failed to send error response");
log_error(send_error_msg);
} else {
log_success("DEBUG: Error response sent");
}
// Remove the pending change
remove_pending_change(change);
return -3; // Application failed
}
} else if (is_no) {
// Cancel the change
char cancel_msg[512];
snprintf(cancel_msg, sizeof(cancel_msg),
"🚫 Configuration Change Cancelled\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Change cancelled: %s\n"
"\n"
"No changes were made to the relay configuration.",
change->config_key
);
send_nip17_response(admin_pubkey, cancel_msg, NULL, 0);
// Remove the pending change
remove_pending_change(change);
return 2; // Cancelled
}
return 0;
}
// Process a configuration change request
int process_config_change_request(const char* admin_pubkey, const char* message) {
if (!admin_pubkey || !message) {
return -1;
}
char key[128], value[256];
// Parse the configuration command
if (!parse_config_command(message, key, value)) {
return 0; // Not a config command
}
// Validate the configuration change
if (!validate_config_change(key, value)) {
char error_msg[2048];
snprintf(error_msg, sizeof(error_msg),
"❌ Invalid Configuration\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"Invalid configuration: %s = %s\n"
"\n"
"The configuration key '%s' is either unknown or the value '%s' is invalid.\n"
"\n"
"Supported keys include: auth_enabled, max_connections, default_limit, relay_description, etc.\n"
"Use 'config' command to see all current settings.",
key, value, key, value
);
send_nip17_response(admin_pubkey, error_msg, NULL, 0);
return -2;
}
// Get current value
const char* current_value = get_config_value(key);
if (!current_value) {
current_value = "unset";
}
// Check if the value is already set to the requested value
if (strcmp(current_value, value) == 0) {
char already_set_msg[1024];
snprintf(already_set_msg, sizeof(already_set_msg),
" Configuration Already Set\n"
"━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"%s is already set to: %s\n"
"\n"
"No change needed.",
key, value
);
send_nip17_response(admin_pubkey, already_set_msg, NULL, 0);
return 3;
}
// Store the pending change
char* change_id = store_pending_config_change(admin_pubkey, key, current_value, value);
if (!change_id) {
char error_msg[256];
snprintf(error_msg, sizeof(error_msg),
"❌ Failed to store configuration change request.\n"
"Please try again."
);
send_nip17_response(admin_pubkey, error_msg, NULL, 0);
return -3;
}
// Generate and send confirmation message
log_info("DEBUG: Generating confirmation message");
char* confirmation = generate_config_change_confirmation(key, current_value, value);
if (confirmation) {
log_info("DEBUG: Confirmation message generated, sending response");
char error_msg[256];
int send_result = send_nip17_response(admin_pubkey, confirmation, error_msg, sizeof(error_msg));
if (send_result == 0) {
log_success("DEBUG: Confirmation response sent successfully");
} else {
log_error("DEBUG: Failed to send confirmation response");
log_error(error_msg);
}
free(confirmation);
} else {
log_error("DEBUG: Failed to generate confirmation message");
}
free(change_id);
return 1; // Confirmation sent
}
// Generate stats JSON from database queries // Generate stats JSON from database queries
char* generate_stats_json(void) { char* generate_stats_json(void) {
extern sqlite3* g_db; extern sqlite3* g_db;
@@ -478,7 +1227,7 @@ char* generate_stats_text(void) {
// Parse the JSON to extract values for human-readable format // Parse the JSON to extract values for human-readable format
cJSON* stats_obj = cJSON_Parse(stats_json); cJSON* stats_obj = cJSON_Parse(stats_json);
char* stats_text = malloc(4096); char* stats_text = malloc(16384); // Increased buffer size for comprehensive stats
if (!stats_text) { if (!stats_text) {
free(stats_json); free(stats_json);
if (stats_obj) cJSON_Delete(stats_obj); if (stats_obj) cJSON_Delete(stats_obj);
@@ -486,14 +1235,34 @@ char* generate_stats_text(void) {
} }
if (stats_obj) { if (stats_obj) {
// Extract basic metrics
cJSON* total_events = cJSON_GetObjectItem(stats_obj, "total_events"); cJSON* total_events = cJSON_GetObjectItem(stats_obj, "total_events");
cJSON* db_size = cJSON_GetObjectItem(stats_obj, "database_size_bytes"); cJSON* db_size = cJSON_GetObjectItem(stats_obj, "database_size_bytes");
cJSON* oldest_event = cJSON_GetObjectItem(stats_obj, "database_created_at");
cJSON* newest_event = cJSON_GetObjectItem(stats_obj, "latest_event_at");
cJSON* time_stats = cJSON_GetObjectItem(stats_obj, "time_stats"); cJSON* time_stats = cJSON_GetObjectItem(stats_obj, "time_stats");
cJSON* event_kinds = cJSON_GetObjectItem(stats_obj, "event_kinds");
cJSON* top_pubkeys = cJSON_GetObjectItem(stats_obj, "top_pubkeys");
long long total = total_events ? (long long)cJSON_GetNumberValue(total_events) : 0; long long total = total_events ? (long long)cJSON_GetNumberValue(total_events) : 0;
long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0; long long db_bytes = db_size ? (long long)cJSON_GetNumberValue(db_size) : 0;
double db_mb = db_bytes / (1024.0 * 1024.0); double db_mb = db_bytes / (1024.0 * 1024.0);
// Format timestamps
char oldest_str[64] = "-";
char newest_str[64] = "-";
if (oldest_event && cJSON_GetNumberValue(oldest_event) > 0) {
time_t oldest_ts = (time_t)cJSON_GetNumberValue(oldest_event);
struct tm* tm_info = localtime(&oldest_ts);
strftime(oldest_str, sizeof(oldest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info);
}
if (newest_event && cJSON_GetNumberValue(newest_event) > 0) {
time_t newest_ts = (time_t)cJSON_GetNumberValue(newest_event);
struct tm* tm_info = localtime(&newest_ts);
strftime(newest_str, sizeof(newest_str), "%m/%d/%Y, %I:%M:%S %p", tm_info);
}
// Extract time-based stats
long long last_24h = 0, last_7d = 0, last_30d = 0; long long last_24h = 0, last_7d = 0, last_30d = 0;
if (time_stats) { if (time_stats) {
cJSON* h24 = cJSON_GetObjectItem(time_stats, "last_24h"); cJSON* h24 = cJSON_GetObjectItem(time_stats, "last_24h");
@@ -504,26 +1273,103 @@ char* generate_stats_text(void) {
last_30d = d30 ? (long long)cJSON_GetNumberValue(d30) : 0; last_30d = d30 ? (long long)cJSON_GetNumberValue(d30) : 0;
} }
snprintf(stats_text, 4096, // Start building the comprehensive stats text
int offset = 0;
// Header
offset += snprintf(stats_text + offset, 16384 - offset,
"📊 Relay Statistics\n" "📊 Relay Statistics\n"
"━━━━━━━━━━━━━━━━━━━━\n" "━━━━━━━━━━━━━━━━━━━━\n");
"Total Events: %lld\n"
"Database Size: %.2f MB (%lld bytes)\n" // Database Overview section
"\n" offset += snprintf(stats_text + offset, 16384 - offset,
"📈 Recent Activity\n" "Database Overview:\n"
"━━━━━━━━━━━━━━━━━━━\n" "Metric\tValue\tDescription\n"
"Last 24 hours: %lld events\n" "Database Size\t%.2f MB (%lld bytes)\tCurrent database file size\n"
"Last 7 days: %lld events\n" "Total Events\t%lld\tTotal number of events stored\n"
"Last 30 days: %lld events\n" "Oldest Event\t%s\tTimestamp of oldest event\n"
"\n" "Newest Event\t%s\tTimestamp of newest event\n"
"✅ Statistics retrieved successfully", "\n",
total, db_mb, db_bytes, last_24h, last_7d, last_30d db_mb, db_bytes, total, oldest_str, newest_str);
);
// Event Kind Distribution section
offset += snprintf(stats_text + offset, 16384 - offset,
"Event Kind Distribution:\n"
"Event Kind\tCount\tPercentage\n");
if (event_kinds && cJSON_IsArray(event_kinds)) {
cJSON* kind_item = NULL;
cJSON_ArrayForEach(kind_item, event_kinds) {
cJSON* kind = cJSON_GetObjectItem(kind_item, "kind");
cJSON* count = cJSON_GetObjectItem(kind_item, "count");
cJSON* percentage = cJSON_GetObjectItem(kind_item, "percentage");
if (kind && count && percentage) {
offset += snprintf(stats_text + offset, 16384 - offset,
"%lld\t%lld\t%.1f%%\n",
(long long)cJSON_GetNumberValue(kind),
(long long)cJSON_GetNumberValue(count),
cJSON_GetNumberValue(percentage));
}
}
} else {
offset += snprintf(stats_text + offset, 16384 - offset,
"No event data available\n");
}
offset += snprintf(stats_text + offset, 16384 - offset, "\n");
// Time-based Statistics section
offset += snprintf(stats_text + offset, 16384 - offset,
"Time-based Statistics:\n"
"Period\tEvents\tDescription\n"
"Last 24 Hours\t%lld\tEvents in the last day\n"
"Last 7 Days\t%lld\tEvents in the last week\n"
"Last 30 Days\t%lld\tEvents in the last month\n"
"\n",
last_24h, last_7d, last_30d);
// Top Pubkeys section
offset += snprintf(stats_text + offset, 16384 - offset,
"Top Pubkeys by Event Count:\n"
"Rank\tPubkey\tEvent Count\tPercentage\n");
if (top_pubkeys && cJSON_IsArray(top_pubkeys)) {
int rank = 1;
cJSON* pubkey_item = NULL;
cJSON_ArrayForEach(pubkey_item, top_pubkeys) {
cJSON* pubkey = cJSON_GetObjectItem(pubkey_item, "pubkey");
cJSON* event_count = cJSON_GetObjectItem(pubkey_item, "event_count");
cJSON* percentage = cJSON_GetObjectItem(pubkey_item, "percentage");
if (pubkey && event_count && percentage) {
const char* pubkey_str = cJSON_GetStringValue(pubkey);
char short_pubkey[20] = "...";
if (pubkey_str && strlen(pubkey_str) >= 16) {
snprintf(short_pubkey, sizeof(short_pubkey), "%.16s...", pubkey_str);
}
offset += snprintf(stats_text + offset, 16384 - offset,
"%d\t%s\t%lld\t%.1f%%\n",
rank++,
short_pubkey,
(long long)cJSON_GetNumberValue(event_count),
cJSON_GetNumberValue(percentage));
}
}
} else {
offset += snprintf(stats_text + offset, 16384 - offset,
"No pubkey data available\n");
}
// Footer
offset += snprintf(stats_text + offset, 16384 - offset,
"\n✅ Statistics retrieved successfully");
cJSON_Delete(stats_obj); cJSON_Delete(stats_obj);
} else { } else {
// Fallback if JSON parsing fails // Fallback if JSON parsing fails
snprintf(stats_text, 4096, snprintf(stats_text, 16384,
"📊 Relay Statistics\n" "📊 Relay Statistics\n"
"━━━━━━━━━━━━━━━━━━━━\n" "━━━━━━━━━━━━━━━━━━━━\n"
"Raw data: %s\n" "Raw data: %s\n"
@@ -532,7 +1378,7 @@ char* generate_stats_text(void) {
stats_json stats_json
); );
} }
free(stats_json); free(stats_json);
return stats_text; return stats_text;
} }
@@ -640,6 +1486,11 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
cJSON_Delete(command_array); cJSON_Delete(command_array);
} }
} }
} else if (result > 0) {
// Command was handled and response was sent, don't create generic response
log_info("NIP-17: Command handled with custom response, skipping generic response");
cJSON_Delete(inner_dm);
return NULL;
// Get sender pubkey for response from the decrypted DM event // Get sender pubkey for response from the decrypted DM event
cJSON* sender_pubkey_obj = cJSON_GetObjectItem(inner_dm, "pubkey"); cJSON* sender_pubkey_obj = cJSON_GetObjectItem(inner_dm, "pubkey");
@@ -861,6 +1712,39 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
return 0; return 0;
} }
else { else {
// Check if it's a confirmation response (yes/no)
int confirmation_result = handle_config_confirmation(sender_pubkey, dm_content);
if (confirmation_result != 0) {
if (confirmation_result > 0) {
log_success("NIP-17: Configuration confirmation processed successfully");
} else if (confirmation_result == -2) {
// No pending changes
char no_pending_msg[256];
snprintf(no_pending_msg, sizeof(no_pending_msg),
"❌ No Pending Changes\n"
"━━━━━━━━━━━━━━━━━━━━━━\n"
"\n"
"You don't have any pending configuration changes to confirm.\n"
"\n"
"Send a configuration command first (e.g., 'auth_enabled true')."
);
send_nip17_response(sender_pubkey, no_pending_msg, NULL, 0);
}
return 0;
}
// Check if it's a configuration change request
int config_result = process_config_change_request(sender_pubkey, dm_content);
if (config_result != 0) {
if (config_result > 0) {
log_success("NIP-17: Configuration change request processed successfully");
return 1; // Return positive value to indicate response was handled
} else {
log_error("NIP-17: Configuration change request failed");
return -1; // Return error to prevent generic success response
}
}
log_info("NIP-17: Plain text content from admin not recognized as command, treating as user DM"); log_info("NIP-17: Plain text content from admin not recognized as command, treating as user DM");
return 0; // Admin sent unrecognized plain text, treat as user DM return 0; // Admin sent unrecognized plain text, treat as user DM
} }

File diff suppressed because one or more lines are too long

View File

@@ -45,6 +45,8 @@ int nostr_nip42_verify_auth_event(cJSON *event, const char *challenge_id,
// Global state // Global state
sqlite3* g_db = NULL; // Non-static so config.c can access it sqlite3* g_db = NULL; // Non-static so config.c can access it
int g_server_running = 1; // Non-static so websockets.c can access it int g_server_running = 1; // Non-static so websockets.c can access it
volatile sig_atomic_t g_shutdown_flag = 0; // Non-static so config.c can access it for restart functionality
int g_restart_requested = 0; // Non-static so config.c can access it for restart functionality
struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it struct lws_context *ws_context = NULL; // Non-static so websockets.c can access it
// NIP-11 relay information structure // NIP-11 relay information structure

View File

@@ -95,6 +95,8 @@ extern unified_config_cache_t g_unified_cache;
// Forward declarations for global state // Forward declarations for global state
extern sqlite3* g_db; extern sqlite3* g_db;
extern int g_server_running; extern int g_server_running;
extern volatile sig_atomic_t g_shutdown_flag;
extern int g_restart_requested;
extern struct lws_context *ws_context; extern struct lws_context *ws_context;
// Global subscription manager // Global subscription manager
@@ -1153,7 +1155,7 @@ int start_websocket_relay(int port_override, int strict_port) {
log_success(startup_msg); log_success(startup_msg);
// Main event loop with proper signal handling // Main event loop with proper signal handling
while (g_server_running) { while (g_server_running && !g_shutdown_flag) {
int result = lws_service(ws_context, 1000); int result = lws_service(ws_context, 1000);
if (result < 0) { if (result < 0) {