v0.0.4 - nip09 implemented
This commit is contained in:
11
README.md
11
README.md
@@ -1,14 +1,15 @@
|
||||
A nostr relay in C with sqlite on the back end.
|
||||
|
||||
|
||||
<!--
|
||||
NOTE FOR ASSISTANTS: When updating the NIPs checklist below, ONLY change [ ] to [x] to mark as complete.
|
||||
Do NOT modify the formatting, add emojis, or change the text. Keep the simple format consistent.
|
||||
-->
|
||||
|
||||
|
||||
### [NIPs](https://github.com/nostr-protocol/nips)
|
||||
|
||||
- [x] NIP-01: Basic protocol flow description
|
||||
- [ ] NIP-02: Contact list and petnames
|
||||
- [ ] NIP-04: Encrypted Direct Message
|
||||
- [ ] NIP-09: Event deletion
|
||||
- [x] NIP-01: Basic protocol flow implementation
|
||||
- [x] NIP-09: Event deletion
|
||||
- [ ] NIP-11: Relay information document
|
||||
- [ ] NIP-12: Generic tag queries
|
||||
- [ ] NIP-13: Proof of Work
|
||||
|
||||
389
build_and_push.sh
Executable file
389
build_and_push.sh
Executable file
@@ -0,0 +1,389 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m'
|
||||
|
||||
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||||
|
||||
# Global variables
|
||||
COMMIT_MESSAGE=""
|
||||
RELEASE_MODE=false
|
||||
|
||||
# Parse command line arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-r|--release)
|
||||
RELEASE_MODE=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
show_usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
# First non-flag argument is the commit message
|
||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||
COMMIT_MESSAGE="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
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 " - ARM64 cross-compiler: sudo apt install gcc-aarch64-linux-gnu"
|
||||
echo " - Gitea token in ~/.gitea_token for release uploads"
|
||||
}
|
||||
|
||||
# Validate inputs
|
||||
if [[ -z "$COMMIT_MESSAGE" ]]; then
|
||||
print_error "Commit message is required"
|
||||
echo ""
|
||||
show_usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if we're in a git repository
|
||||
check_git_repo() {
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not in a git repository"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to get current version and increment appropriately
|
||||
increment_version() {
|
||||
local increment_type="$1" # "patch" or "minor"
|
||||
|
||||
print_status "Getting current version..."
|
||||
|
||||
# Get the highest version tag (not chronologically latest)
|
||||
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "")
|
||||
if [[ -z "$LATEST_TAG" ]]; then
|
||||
LATEST_TAG="v0.0.0"
|
||||
print_warning "No version tags found, starting from $LATEST_TAG"
|
||||
fi
|
||||
|
||||
# Extract version components (remove 'v' prefix)
|
||||
VERSION=${LATEST_TAG#v}
|
||||
|
||||
# Parse major.minor.patch using regex
|
||||
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
|
||||
MAJOR=${BASH_REMATCH[1]}
|
||||
MINOR=${BASH_REMATCH[2]}
|
||||
PATCH=${BASH_REMATCH[3]}
|
||||
else
|
||||
print_error "Invalid version format in tag: $LATEST_TAG"
|
||||
print_error "Expected format: v0.1.0"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Increment version based on type
|
||||
if [[ "$increment_type" == "minor" ]]; then
|
||||
# Minor release: increment minor, zero patch
|
||||
NEW_MINOR=$((MINOR + 1))
|
||||
NEW_PATCH=0
|
||||
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||
print_status "Release mode: incrementing minor version"
|
||||
else
|
||||
# Default: increment patch
|
||||
NEW_PATCH=$((PATCH + 1))
|
||||
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
|
||||
print_status "Default mode: incrementing patch version"
|
||||
fi
|
||||
|
||||
print_status "Current version: $LATEST_TAG"
|
||||
print_status "New version: $NEW_VERSION"
|
||||
|
||||
# Export for use in other functions
|
||||
export NEW_VERSION
|
||||
}
|
||||
|
||||
# Function to compile the C-Relay project
|
||||
compile_project() {
|
||||
print_status "Compiling C-Relay..."
|
||||
|
||||
# Clean previous build
|
||||
if make clean > /dev/null 2>&1; then
|
||||
print_success "Cleaned previous build"
|
||||
else
|
||||
print_warning "Clean failed or no Makefile found"
|
||||
fi
|
||||
|
||||
# Compile the project
|
||||
if make > /dev/null 2>&1; then
|
||||
print_success "C-Relay compiled successfully"
|
||||
else
|
||||
print_error "Compilation failed"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for ARM64 cross-compiler
|
||||
check_cross_compiler() {
|
||||
if ! command -v aarch64-linux-gnu-gcc > /dev/null 2>&1; then
|
||||
print_error "ARM64/AArch64 cross-compiler not found!"
|
||||
print_error "Install with: sudo apt install gcc-aarch64-linux-gnu"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Function to build release binaries
|
||||
build_release_binaries() {
|
||||
print_status "Building release binaries..."
|
||||
|
||||
# Build x86_64 version
|
||||
print_status "Building x86_64 version..."
|
||||
make clean > /dev/null 2>&1
|
||||
if make CC=gcc > /dev/null 2>&1; then
|
||||
if [[ -f "src/main" ]]; then
|
||||
cp src/main c-relay-x86_64
|
||||
print_success "x86_64 binary created: c-relay-x86_64"
|
||||
else
|
||||
print_error "x86_64 binary not found after compilation"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "x86_64 build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for ARM64 cross-compiler
|
||||
if check_cross_compiler; then
|
||||
# Build ARM64 version
|
||||
print_status "Building ARM64 version..."
|
||||
make clean > /dev/null 2>&1
|
||||
if make CC=aarch64-linux-gnu-gcc > /dev/null 2>&1; then
|
||||
if [[ -f "src/main" ]]; then
|
||||
cp src/main c-relay-arm64
|
||||
print_success "ARM64 binary created: c-relay-arm64"
|
||||
else
|
||||
print_error "ARM64 binary not found after compilation"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_error "ARM64 build failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
print_warning "ARM64 cross-compiler not available, skipping ARM64 build"
|
||||
fi
|
||||
|
||||
# Restore normal build
|
||||
make clean > /dev/null 2>&1
|
||||
make > /dev/null 2>&1
|
||||
}
|
||||
|
||||
# Function to commit and push changes
|
||||
git_commit_and_push() {
|
||||
print_status "Preparing git commit..."
|
||||
|
||||
# Stage all changes
|
||||
if git add . > /dev/null 2>&1; then
|
||||
print_success "Staged all changes"
|
||||
else
|
||||
print_error "Failed to stage changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if there are changes to commit
|
||||
if git diff --staged --quiet; then
|
||||
print_warning "No changes to commit"
|
||||
else
|
||||
# Commit changes
|
||||
if git commit -m "$NEW_VERSION - $COMMIT_MESSAGE" > /dev/null 2>&1; then
|
||||
print_success "Committed changes"
|
||||
else
|
||||
print_error "Failed to commit changes"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Create new git tag
|
||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||
print_success "Created tag: $NEW_VERSION"
|
||||
else
|
||||
print_warning "Tag $NEW_VERSION already exists"
|
||||
fi
|
||||
|
||||
# Push changes and tags
|
||||
print_status "Pushing to remote repository..."
|
||||
if git push > /dev/null 2>&1; then
|
||||
print_success "Pushed changes"
|
||||
else
|
||||
print_error "Failed to push changes"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if git push --tags > /dev/null 2>&1; then
|
||||
print_success "Pushed tags"
|
||||
else
|
||||
print_warning "Failed to push tags"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to create Gitea release
|
||||
create_gitea_release() {
|
||||
print_status "Creating Gitea release..."
|
||||
|
||||
# Check for Gitea token
|
||||
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||
print_warning "No ~/.gitea_token found. Skipping release creation."
|
||||
print_warning "Create ~/.gitea_token with your Gitea access token to enable releases."
|
||||
return 0
|
||||
fi
|
||||
|
||||
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
|
||||
local api_url="https://git.laantungir.net/api/v1/repos/teknari/c-relay"
|
||||
|
||||
# Create release
|
||||
print_status "Creating release $NEW_VERSION..."
|
||||
local response=$(curl -s -X POST "$api_url/releases" \
|
||||
-H "Authorization: token $token" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
|
||||
|
||||
if echo "$response" | grep -q '"id"'; then
|
||||
print_success "Created release $NEW_VERSION"
|
||||
|
||||
# Upload binaries
|
||||
upload_release_binaries "$api_url" "$token"
|
||||
else
|
||||
print_warning "Release may already exist or creation failed"
|
||||
print_status "Attempting to upload to existing release..."
|
||||
upload_release_binaries "$api_url" "$token"
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to upload release binaries
|
||||
upload_release_binaries() {
|
||||
local api_url="$1"
|
||||
local token="$2"
|
||||
|
||||
# Get release ID
|
||||
local release_id=$(curl -s -H "Authorization: token $token" \
|
||||
"$api_url/releases/tags/$NEW_VERSION" | \
|
||||
grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
|
||||
if [[ -z "$release_id" ]]; then
|
||||
print_error "Could not get release ID for $NEW_VERSION"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Upload x86_64 binary
|
||||
if [[ -f "c-relay-x86_64" ]]; then
|
||||
print_status "Uploading x86_64 binary..."
|
||||
if curl -s -X POST "$api_url/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@c-relay-x86_64;filename=c-relay-${NEW_VERSION}-linux-x86_64" > /dev/null; then
|
||||
print_success "Uploaded x86_64 binary"
|
||||
else
|
||||
print_warning "Failed to upload x86_64 binary"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Upload ARM64 binary
|
||||
if [[ -f "c-relay-arm64" ]]; then
|
||||
print_status "Uploading ARM64 binary..."
|
||||
if curl -s -X POST "$api_url/releases/$release_id/assets" \
|
||||
-H "Authorization: token $token" \
|
||||
-F "attachment=@c-relay-arm64;filename=c-relay-${NEW_VERSION}-linux-arm64" > /dev/null; then
|
||||
print_success "Uploaded ARM64 binary"
|
||||
else
|
||||
print_warning "Failed to upload ARM64 binary"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# Function to clean up release binaries
|
||||
cleanup_release_binaries() {
|
||||
if [[ -f "c-relay-x86_64" ]]; then
|
||||
rm -f c-relay-x86_64
|
||||
print_status "Cleaned up x86_64 binary"
|
||||
fi
|
||||
if [[ -f "c-relay-arm64" ]]; then
|
||||
rm -f c-relay-arm64
|
||||
print_status "Cleaned up ARM64 binary"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
main() {
|
||||
print_status "C-Relay Build and Push Script"
|
||||
|
||||
# Check prerequisites
|
||||
check_git_repo
|
||||
|
||||
if [[ "$RELEASE_MODE" == true ]]; then
|
||||
print_status "=== RELEASE MODE ==="
|
||||
|
||||
# Increment minor version for releases
|
||||
increment_version "minor"
|
||||
|
||||
# Compile project first
|
||||
compile_project
|
||||
|
||||
# Build release binaries
|
||||
build_release_binaries
|
||||
|
||||
# Commit and push
|
||||
git_commit_and_push
|
||||
|
||||
# Create Gitea release with binaries
|
||||
create_gitea_release
|
||||
|
||||
# Cleanup
|
||||
cleanup_release_binaries
|
||||
|
||||
print_success "Release $NEW_VERSION completed successfully!"
|
||||
print_status "Binaries uploaded to Gitea release"
|
||||
|
||||
else
|
||||
print_status "=== DEFAULT MODE ==="
|
||||
|
||||
# Increment patch version for regular commits
|
||||
increment_version "patch"
|
||||
|
||||
# Compile project
|
||||
compile_project
|
||||
|
||||
# Commit and push
|
||||
git_commit_and_push
|
||||
|
||||
print_success "Build and push completed successfully!"
|
||||
print_status "Version $NEW_VERSION pushed to repository"
|
||||
fi
|
||||
}
|
||||
|
||||
# Execute main function
|
||||
main
|
||||
Binary file not shown.
Binary file not shown.
126
relay.log
126
relay.log
@@ -3,3 +3,129 @@
|
||||
[34m[INFO][0m Starting relay server...
|
||||
[34m[INFO][0m Starting libwebsockets-based Nostr relay server...
|
||||
[32m[SUCCESS][0m WebSocket relay started on ws://127.0.0.1:8888
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
||||
[32m[SUCCESS][0m Event stored in database
|
||||
[32m[SUCCESS][0m Event validated and stored successfully
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
||||
[32m[SUCCESS][0m Event stored in database
|
||||
[32m[SUCCESS][0m Event validated and stored successfully
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
||||
[32m[SUCCESS][0m Event stored in database
|
||||
[32m[SUCCESS][0m Event validated and stored successfully
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[33m[WARNING][0m Subscription 'exists_1757082297' not found for removal
|
||||
[34m[INFO][0m Closed subscription: exists_1757082297
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[33m[WARNING][0m Subscription 'exists_1757082298' not found for removal
|
||||
[34m[INFO][0m Closed subscription: exists_1757082298
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
||||
[34m[INFO][0m Event not found for deletion: [34m[INFO][0m ...
|
||||
[34m[INFO][0m Event not found for deletion: [34m[INFO][0m ...
|
||||
[32m[SUCCESS][0m Event stored in database
|
||||
[34m[INFO][0m Deletion request processed: 0 events deleted
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[33m[WARNING][0m Subscription 'exists_1757082301' not found for removal
|
||||
[34m[INFO][0m Closed subscription: exists_1757082301
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[33m[WARNING][0m Subscription 'exists_1757082301' not found for removal
|
||||
[34m[INFO][0m Closed subscription: exists_1757082301
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[33m[WARNING][0m Subscription 'exists_1757082301' not found for removal
|
||||
[34m[INFO][0m Closed subscription: exists_1757082301
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
||||
[32m[SUCCESS][0m Event stored in database
|
||||
[34m[INFO][0m Deletion request processed: 0 events deleted
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[33m[WARNING][0m Subscription 'exists_1757082305' not found for removal
|
||||
[34m[INFO][0m Closed subscription: exists_1757082305
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
||||
[34m[INFO][0m Event not found for deletion: [31m✗[0m Cou...
|
||||
[32m[SUCCESS][0m Event stored in database
|
||||
[34m[INFO][0m Deletion request processed: 0 events deleted
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling REQ message for persistent subscription
|
||||
[34m[INFO][0m Added subscription 'exists_1757082309' (total: 1)
|
||||
[34m[INFO][0m Executing SQL: SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 ORDER BY created_at DESC LIMIT 500
|
||||
[34m[INFO][0m Query returned 25 rows
|
||||
[34m[INFO][0m Total events sent: 25
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Removed subscription 'exists_1757082309' (total: 0)
|
||||
[34m[INFO][0m Closed subscription: exists_1757082309
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[33m[WARNING][0m Subscription 'z[<5B><>.Y' not found for removal
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling EVENT message with full NIP-01 validation
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[34m[INFO][0m WebSocket connection established
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Handling REQ message for persistent subscription
|
||||
[34m[INFO][0m Added subscription 'kind5_1757082309' (total: 1)
|
||||
[34m[INFO][0m Executing SQL: SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind IN (5) ORDER BY created_at DESC LIMIT 500
|
||||
[34m[INFO][0m Query returned 3 rows
|
||||
[34m[INFO][0m Total events sent: 3
|
||||
[34m[INFO][0m Received WebSocket message
|
||||
[34m[INFO][0m Removed subscription 'kind5_1757082309' (total: 0)
|
||||
[34m[INFO][0m Closed subscription: kind5_1757082309
|
||||
[34m[INFO][0m WebSocket connection closed
|
||||
[33m[WARNING][0m Subscription '<27>f<EFBFBD><66>.Y' not found for removal
|
||||
|
||||
560
src/main.c
560
src/main.c
@@ -133,6 +133,24 @@ void log_subscription_disconnected(const char* client_ip);
|
||||
void log_event_broadcast(const char* event_id, const char* sub_id, const char* client_ip);
|
||||
void update_subscription_events_sent(const char* sub_id, int events_sent);
|
||||
|
||||
// Forward declarations for NIP-01 event handling
|
||||
const char* extract_d_tag_value(cJSON* tags);
|
||||
int check_and_handle_replaceable_event(int kind, const char* pubkey, long created_at);
|
||||
int check_and_handle_addressable_event(int kind, const char* pubkey, const char* d_tag_value, long created_at);
|
||||
int handle_event_message(cJSON* event, char* error_message, size_t error_size);
|
||||
|
||||
// Forward declaration for NOTICE message support
|
||||
void send_notice_message(struct lws* wsi, const char* message);
|
||||
|
||||
// Forward declarations for NIP-09 deletion request handling
|
||||
int handle_deletion_request(cJSON* event, char* error_message, size_t error_size);
|
||||
int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids);
|
||||
int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp);
|
||||
int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason);
|
||||
|
||||
// Forward declaration for database functions
|
||||
int store_event(cJSON* event);
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -842,6 +860,322 @@ void signal_handler(int sig) {
|
||||
}
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// NOTICE MESSAGE SUPPORT
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Send NOTICE message to client (NIP-01)
|
||||
void send_notice_message(struct lws* wsi, const char* message) {
|
||||
if (!wsi || !message) return;
|
||||
|
||||
cJSON* notice_msg = cJSON_CreateArray();
|
||||
cJSON_AddItemToArray(notice_msg, cJSON_CreateString("NOTICE"));
|
||||
cJSON_AddItemToArray(notice_msg, cJSON_CreateString(message));
|
||||
|
||||
char* msg_str = cJSON_Print(notice_msg);
|
||||
if (msg_str) {
|
||||
size_t msg_len = strlen(msg_str);
|
||||
unsigned char* buf = malloc(LWS_PRE + msg_len);
|
||||
if (buf) {
|
||||
memcpy(buf + LWS_PRE, msg_str, msg_len);
|
||||
lws_write(wsi, buf + LWS_PRE, msg_len, LWS_WRITE_TEXT);
|
||||
free(buf);
|
||||
}
|
||||
free(msg_str);
|
||||
}
|
||||
|
||||
cJSON_Delete(notice_msg);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// NIP-09 EVENT DELETION REQUEST HANDLING
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Handle NIP-09 deletion request event (kind 5)
|
||||
int handle_deletion_request(cJSON* event, char* error_message, size_t error_size) {
|
||||
if (!event) {
|
||||
snprintf(error_message, error_size, "invalid: null deletion request");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Extract event details
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at");
|
||||
cJSON* tags_obj = cJSON_GetObjectItem(event, "tags");
|
||||
cJSON* content_obj = cJSON_GetObjectItem(event, "content");
|
||||
cJSON* event_id_obj = cJSON_GetObjectItem(event, "id");
|
||||
|
||||
if (!kind_obj || !pubkey_obj || !created_at_obj || !tags_obj || !event_id_obj) {
|
||||
snprintf(error_message, error_size, "invalid: incomplete deletion request");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
if (kind != 5) {
|
||||
snprintf(error_message, error_size, "invalid: not a deletion request");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const char* requester_pubkey = cJSON_GetStringValue(pubkey_obj);
|
||||
const char* deletion_event_id = cJSON_GetStringValue(event_id_obj);
|
||||
const char* reason = content_obj ? cJSON_GetStringValue(content_obj) : "";
|
||||
long deletion_timestamp = (long)cJSON_GetNumberValue(created_at_obj);
|
||||
|
||||
if (!cJSON_IsArray(tags_obj)) {
|
||||
snprintf(error_message, error_size, "invalid: deletion request tags must be an array");
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Collect event IDs and addresses from tags
|
||||
cJSON* event_ids = cJSON_CreateArray();
|
||||
cJSON* addresses = cJSON_CreateArray();
|
||||
cJSON* kinds_to_delete = cJSON_CreateArray();
|
||||
|
||||
int deletion_targets_found = 0;
|
||||
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags_obj) {
|
||||
if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) {
|
||||
continue;
|
||||
}
|
||||
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* name = cJSON_GetStringValue(tag_name);
|
||||
const char* value = cJSON_GetStringValue(tag_value);
|
||||
|
||||
if (strcmp(name, "e") == 0) {
|
||||
// Event ID reference
|
||||
cJSON_AddItemToArray(event_ids, cJSON_CreateString(value));
|
||||
deletion_targets_found++;
|
||||
} else if (strcmp(name, "a") == 0) {
|
||||
// Addressable event reference (kind:pubkey:d-identifier)
|
||||
cJSON_AddItemToArray(addresses, cJSON_CreateString(value));
|
||||
deletion_targets_found++;
|
||||
} else if (strcmp(name, "k") == 0) {
|
||||
// Kind hint - store for validation but not required
|
||||
int kind_hint = atoi(value);
|
||||
if (kind_hint > 0) {
|
||||
cJSON_AddItemToArray(kinds_to_delete, cJSON_CreateNumber(kind_hint));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (deletion_targets_found == 0) {
|
||||
cJSON_Delete(event_ids);
|
||||
cJSON_Delete(addresses);
|
||||
cJSON_Delete(kinds_to_delete);
|
||||
snprintf(error_message, error_size, "invalid: deletion request must contain 'e' or 'a' tags");
|
||||
return -1;
|
||||
}
|
||||
|
||||
int deleted_count = 0;
|
||||
|
||||
// Process event ID deletions
|
||||
if (cJSON_GetArraySize(event_ids) > 0) {
|
||||
int result = delete_events_by_id(requester_pubkey, event_ids);
|
||||
if (result > 0) {
|
||||
deleted_count += result;
|
||||
}
|
||||
}
|
||||
|
||||
// Process addressable event deletions
|
||||
if (cJSON_GetArraySize(addresses) > 0) {
|
||||
int result = delete_events_by_address(requester_pubkey, addresses, deletion_timestamp);
|
||||
if (result > 0) {
|
||||
deleted_count += result;
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up
|
||||
cJSON_Delete(event_ids);
|
||||
cJSON_Delete(addresses);
|
||||
cJSON_Delete(kinds_to_delete);
|
||||
|
||||
// Store the deletion request itself (it should be kept according to NIP-09)
|
||||
if (store_event(event) != 0) {
|
||||
log_warning("Failed to store deletion request event");
|
||||
}
|
||||
|
||||
char debug_msg[256];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Deletion request processed: %d events deleted", deleted_count);
|
||||
log_info(debug_msg);
|
||||
|
||||
snprintf(error_message, error_size, ""); // Success
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Delete events by ID (with pubkey authorization)
|
||||
int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) {
|
||||
if (!g_db || !requester_pubkey || !event_ids || !cJSON_IsArray(event_ids)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int deleted_count = 0;
|
||||
|
||||
cJSON* event_id = NULL;
|
||||
cJSON_ArrayForEach(event_id, event_ids) {
|
||||
if (!cJSON_IsString(event_id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* id = cJSON_GetStringValue(event_id);
|
||||
|
||||
// First check if event exists and if requester is authorized
|
||||
const char* check_sql = "SELECT pubkey FROM events WHERE id = ?";
|
||||
sqlite3_stmt* check_stmt;
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
continue;
|
||||
}
|
||||
|
||||
sqlite3_bind_text(check_stmt, 1, id, -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(check_stmt) == SQLITE_ROW) {
|
||||
const char* event_pubkey = (char*)sqlite3_column_text(check_stmt, 0);
|
||||
|
||||
// Only delete if the requester is the author
|
||||
if (event_pubkey && strcmp(event_pubkey, requester_pubkey) == 0) {
|
||||
sqlite3_finalize(check_stmt);
|
||||
|
||||
// Delete the event
|
||||
const char* delete_sql = "DELETE FROM events WHERE id = ? AND pubkey = ?";
|
||||
sqlite3_stmt* delete_stmt;
|
||||
|
||||
rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_text(delete_stmt, 1, id, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
|
||||
|
||||
if (sqlite3_step(delete_stmt) == SQLITE_DONE && sqlite3_changes(g_db) > 0) {
|
||||
deleted_count++;
|
||||
|
||||
char debug_msg[128];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Deleted event by ID: %.16s...", id);
|
||||
log_info(debug_msg);
|
||||
}
|
||||
sqlite3_finalize(delete_stmt);
|
||||
}
|
||||
} else {
|
||||
sqlite3_finalize(check_stmt);
|
||||
char warning_msg[128];
|
||||
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id);
|
||||
log_warning(warning_msg);
|
||||
}
|
||||
} else {
|
||||
sqlite3_finalize(check_stmt);
|
||||
char debug_msg[128];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Event not found for deletion: %.16s...", id);
|
||||
log_info(debug_msg);
|
||||
}
|
||||
}
|
||||
|
||||
return deleted_count;
|
||||
}
|
||||
|
||||
// Delete events by addressable reference (kind:pubkey:d-identifier)
|
||||
int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp) {
|
||||
if (!g_db || !requester_pubkey || !addresses || !cJSON_IsArray(addresses)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int deleted_count = 0;
|
||||
|
||||
cJSON* address = NULL;
|
||||
cJSON_ArrayForEach(address, addresses) {
|
||||
if (!cJSON_IsString(address)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const char* addr = cJSON_GetStringValue(address);
|
||||
|
||||
// Parse address format: kind:pubkey:d-identifier
|
||||
char* addr_copy = strdup(addr);
|
||||
if (!addr_copy) continue;
|
||||
|
||||
char* kind_str = strtok(addr_copy, ":");
|
||||
char* pubkey_str = strtok(NULL, ":");
|
||||
char* d_identifier = strtok(NULL, ":");
|
||||
|
||||
if (!kind_str || !pubkey_str) {
|
||||
free(addr_copy);
|
||||
continue;
|
||||
}
|
||||
|
||||
int kind = atoi(kind_str);
|
||||
|
||||
// Only delete if the requester is the author
|
||||
if (strcmp(pubkey_str, requester_pubkey) != 0) {
|
||||
free(addr_copy);
|
||||
char warning_msg[128];
|
||||
snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr);
|
||||
log_warning(warning_msg);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build deletion query based on whether we have d-identifier
|
||||
const char* delete_sql;
|
||||
sqlite3_stmt* delete_stmt;
|
||||
|
||||
if (d_identifier && strlen(d_identifier) > 0) {
|
||||
// Delete specific addressable event with d-tag
|
||||
delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? "
|
||||
"AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'";
|
||||
} else {
|
||||
// Delete all events of this kind by this author up to deletion timestamp
|
||||
delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?";
|
||||
}
|
||||
|
||||
int rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL);
|
||||
if (rc == SQLITE_OK) {
|
||||
sqlite3_bind_int(delete_stmt, 1, kind);
|
||||
sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(delete_stmt, 3, deletion_timestamp);
|
||||
|
||||
if (d_identifier && strlen(d_identifier) > 0) {
|
||||
sqlite3_bind_text(delete_stmt, 4, d_identifier, -1, SQLITE_STATIC);
|
||||
}
|
||||
|
||||
if (sqlite3_step(delete_stmt) == SQLITE_DONE) {
|
||||
int changes = sqlite3_changes(g_db);
|
||||
if (changes > 0) {
|
||||
deleted_count += changes;
|
||||
|
||||
char debug_msg[128];
|
||||
snprintf(debug_msg, sizeof(debug_msg), "Deleted %d events by address: %.32s...", changes, addr);
|
||||
log_info(debug_msg);
|
||||
}
|
||||
}
|
||||
sqlite3_finalize(delete_stmt);
|
||||
}
|
||||
|
||||
free(addr_copy);
|
||||
}
|
||||
|
||||
return deleted_count;
|
||||
}
|
||||
|
||||
// Mark event as deleted (alternative to hard deletion - not used in current implementation)
|
||||
int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason) {
|
||||
(void)event_id; (void)deletion_event_id; (void)reason; // Suppress unused warnings
|
||||
|
||||
// This function could be used if we wanted to implement soft deletion
|
||||
// For now, NIP-09 implementation uses hard deletion as specified
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////////////////////////////////////////////////////////////////////////////////
|
||||
// DATABASE FUNCTIONS
|
||||
@@ -907,6 +1241,118 @@ const char* event_type_to_string(event_type_t type) {
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to extract d tag value from tags array
|
||||
const char* extract_d_tag_value(cJSON* tags) {
|
||||
if (!tags || !cJSON_IsArray(tags)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cJSON* tag = NULL;
|
||||
cJSON_ArrayForEach(tag, tags) {
|
||||
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 2) {
|
||||
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||
cJSON* tag_value = cJSON_GetArrayItem(tag, 1);
|
||||
|
||||
if (cJSON_IsString(tag_name) && cJSON_IsString(tag_value)) {
|
||||
const char* name = cJSON_GetStringValue(tag_name);
|
||||
if (name && strcmp(name, "d") == 0) {
|
||||
return cJSON_GetStringValue(tag_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Check and handle replaceable events according to NIP-01
|
||||
int check_and_handle_replaceable_event(int kind, const char* pubkey, long created_at) {
|
||||
if (!g_db || !pubkey) return 0;
|
||||
|
||||
const char* sql =
|
||||
"SELECT created_at FROM events WHERE kind = ? AND pubkey = ? ORDER BY created_at DESC LIMIT 1";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return 0; // Allow storage on DB error
|
||||
}
|
||||
|
||||
sqlite3_bind_int(stmt, 1, kind);
|
||||
sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC);
|
||||
|
||||
int result = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
long existing_created_at = sqlite3_column_int64(stmt, 0);
|
||||
if (created_at <= existing_created_at) {
|
||||
result = -1; // Older or same timestamp, reject
|
||||
} else {
|
||||
// Delete older versions
|
||||
const char* delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ?";
|
||||
sqlite3_stmt* delete_stmt;
|
||||
if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int(delete_stmt, 1, kind);
|
||||
sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(delete_stmt, 3, created_at);
|
||||
sqlite3_step(delete_stmt);
|
||||
sqlite3_finalize(delete_stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check and handle addressable events according to NIP-01
|
||||
int check_and_handle_addressable_event(int kind, const char* pubkey, const char* d_tag_value, long created_at) {
|
||||
if (!g_db || !pubkey) return 0;
|
||||
|
||||
// If no d tag, treat as regular replaceable
|
||||
if (!d_tag_value) {
|
||||
return check_and_handle_replaceable_event(kind, pubkey, created_at);
|
||||
}
|
||||
|
||||
const char* sql =
|
||||
"SELECT created_at FROM events WHERE kind = ? AND pubkey = ? AND json_extract(tags, '$[*][1]') = ? "
|
||||
"AND json_extract(tags, '$[*][0]') = 'd' ORDER BY created_at DESC LIMIT 1";
|
||||
|
||||
sqlite3_stmt* stmt;
|
||||
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||
if (rc != SQLITE_OK) {
|
||||
return 0; // Allow storage on DB error
|
||||
}
|
||||
|
||||
sqlite3_bind_int(stmt, 1, kind);
|
||||
sqlite3_bind_text(stmt, 2, pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_text(stmt, 3, d_tag_value, -1, SQLITE_STATIC);
|
||||
|
||||
int result = 0;
|
||||
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||
long existing_created_at = sqlite3_column_int64(stmt, 0);
|
||||
if (created_at <= existing_created_at) {
|
||||
result = -1; // Older or same timestamp, reject
|
||||
} else {
|
||||
// Delete older versions with same kind, pubkey, and d tag
|
||||
const char* delete_sql =
|
||||
"DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at < ? "
|
||||
"AND json_extract(tags, '$[*][1]') = ? AND json_extract(tags, '$[*][0]') = 'd'";
|
||||
sqlite3_stmt* delete_stmt;
|
||||
if (sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL) == SQLITE_OK) {
|
||||
sqlite3_bind_int(delete_stmt, 1, kind);
|
||||
sqlite3_bind_text(delete_stmt, 2, pubkey, -1, SQLITE_STATIC);
|
||||
sqlite3_bind_int64(delete_stmt, 3, created_at);
|
||||
sqlite3_bind_text(delete_stmt, 4, d_tag_value, -1, SQLITE_STATIC);
|
||||
sqlite3_step(delete_stmt);
|
||||
sqlite3_finalize(delete_stmt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sqlite3_finalize(stmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Store event in database
|
||||
int store_event(cJSON* event) {
|
||||
if (!g_db || !event) {
|
||||
@@ -1303,22 +1749,113 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
||||
}
|
||||
|
||||
// Handle EVENT message (publish)
|
||||
int handle_event_message(cJSON* event) {
|
||||
log_info("Handling EVENT message");
|
||||
int handle_event_message(cJSON* event, char* error_message, size_t error_size) {
|
||||
log_info("Handling EVENT message with full NIP-01 validation");
|
||||
|
||||
// Validate event structure (basic check)
|
||||
cJSON* id = cJSON_GetObjectItem(event, "id");
|
||||
if (!id || !cJSON_IsString(id)) {
|
||||
log_error("Invalid event - no ID");
|
||||
return -1;
|
||||
if (!event) {
|
||||
snprintf(error_message, error_size, "invalid: null event");
|
||||
return NOSTR_ERROR_INVALID_INPUT;
|
||||
}
|
||||
|
||||
// Store event in database
|
||||
// Step 1: Validate event structure
|
||||
int structure_result = nostr_validate_event_structure(event);
|
||||
if (structure_result != NOSTR_SUCCESS) {
|
||||
switch (structure_result) {
|
||||
case NOSTR_ERROR_EVENT_INVALID_STRUCTURE:
|
||||
snprintf(error_message, error_size, "invalid: malformed event structure");
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_ID:
|
||||
snprintf(error_message, error_size, "invalid: invalid event id format");
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
|
||||
snprintf(error_message, error_size, "invalid: invalid pubkey format");
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_CREATED_AT:
|
||||
snprintf(error_message, error_size, "invalid: invalid created_at timestamp");
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_KIND:
|
||||
snprintf(error_message, error_size, "invalid: invalid event kind");
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_TAGS:
|
||||
snprintf(error_message, error_size, "invalid: invalid tags format");
|
||||
break;
|
||||
case NOSTR_ERROR_EVENT_INVALID_CONTENT:
|
||||
snprintf(error_message, error_size, "invalid: invalid content");
|
||||
break;
|
||||
default:
|
||||
snprintf(error_message, error_size, "invalid: event structure validation failed");
|
||||
}
|
||||
return structure_result;
|
||||
}
|
||||
|
||||
// Step 2: Verify event signature
|
||||
int signature_result = nostr_verify_event_signature(event);
|
||||
if (signature_result != NOSTR_SUCCESS) {
|
||||
if (signature_result == NOSTR_ERROR_EVENT_INVALID_SIGNATURE) {
|
||||
snprintf(error_message, error_size, "invalid: event signature verification failed");
|
||||
} else if (signature_result == NOSTR_ERROR_EVENT_INVALID_ID) {
|
||||
snprintf(error_message, error_size, "invalid: event id does not match computed hash");
|
||||
} else {
|
||||
snprintf(error_message, error_size, "invalid: cryptographic validation failed");
|
||||
}
|
||||
return signature_result;
|
||||
}
|
||||
|
||||
// Step 3: Complete event validation (combines structure + signature + additional checks)
|
||||
int validation_result = nostr_validate_event(event);
|
||||
if (validation_result != NOSTR_SUCCESS) {
|
||||
snprintf(error_message, error_size, "invalid: complete event validation failed");
|
||||
return validation_result;
|
||||
}
|
||||
|
||||
// Step 4: Check for special event types and handle accordingly
|
||||
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||
cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at");
|
||||
|
||||
if (kind_obj && pubkey_obj && created_at_obj) {
|
||||
int kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||
const char* pubkey = cJSON_GetStringValue(pubkey_obj);
|
||||
long created_at = (long)cJSON_GetNumberValue(created_at_obj);
|
||||
|
||||
// NIP-09: Handle deletion requests (kind 5)
|
||||
if (kind == 5) {
|
||||
return handle_deletion_request(event, error_message, error_size);
|
||||
}
|
||||
|
||||
// Handle replaceable events (NIP-01)
|
||||
event_type_t event_type = classify_event_kind(kind);
|
||||
if (event_type == EVENT_TYPE_REPLACEABLE) {
|
||||
// For replaceable events, check if we have a newer version
|
||||
if (check_and_handle_replaceable_event(kind, pubkey, created_at) < 0) {
|
||||
snprintf(error_message, error_size, "duplicate: older replaceable event ignored");
|
||||
return -2; // Special code for duplicate/older event
|
||||
}
|
||||
} else if (event_type == EVENT_TYPE_ADDRESSABLE) {
|
||||
// For addressable events, check d tag
|
||||
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||
if (tags && cJSON_IsArray(tags)) {
|
||||
const char* d_tag_value = extract_d_tag_value(tags);
|
||||
if (check_and_handle_addressable_event(kind, pubkey, d_tag_value, created_at) < 0) {
|
||||
snprintf(error_message, error_size, "duplicate: older addressable event ignored");
|
||||
return -2;
|
||||
}
|
||||
}
|
||||
} else if (event_type == EVENT_TYPE_EPHEMERAL) {
|
||||
// Ephemeral events should not be stored
|
||||
snprintf(error_message, error_size, ""); // Success but no storage
|
||||
return 0; // Accept but don't store
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Store event in database
|
||||
if (store_event(event) == 0) {
|
||||
log_success("Event stored successfully");
|
||||
snprintf(error_message, error_size, ""); // Success
|
||||
log_success("Event validated and stored successfully");
|
||||
return 0;
|
||||
}
|
||||
|
||||
snprintf(error_message, error_size, "error: failed to store event in database");
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -1366,7 +1903,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
// Handle EVENT message
|
||||
cJSON* event = cJSON_GetArrayItem(json, 1);
|
||||
if (event && cJSON_IsObject(event)) {
|
||||
int result = handle_event_message(event);
|
||||
char error_message[512] = {0};
|
||||
int result = handle_event_message(event, error_message, sizeof(error_message));
|
||||
|
||||
// Broadcast event to matching persistent subscriptions
|
||||
if (result == 0) {
|
||||
@@ -1380,7 +1918,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString("OK"));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(cJSON_GetStringValue(event_id)));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateBool(result == 0));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(result == 0 ? "" : "error: failed to store event"));
|
||||
cJSON_AddItemToArray(response, cJSON_CreateString(strlen(error_message) > 0 ? error_message : ""));
|
||||
|
||||
char *response_str = cJSON_Print(response);
|
||||
if (response_str) {
|
||||
|
||||
@@ -99,6 +99,47 @@ publish_event() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to publish invalid event and expect rejection
|
||||
publish_invalid_event() {
|
||||
local event_json="$1"
|
||||
local description="$2"
|
||||
local expected_error="$3"
|
||||
|
||||
print_info "Publishing invalid $description..."
|
||||
|
||||
# Create EVENT message in Nostr format
|
||||
local event_message="[\"EVENT\",$event_json]"
|
||||
|
||||
# Publish to relay
|
||||
local response=""
|
||||
if command -v websocat &> /dev/null; then
|
||||
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
|
||||
else
|
||||
print_error "websocat not found - required for testing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check response - should contain "false" and error message
|
||||
if [[ "$response" == *"Connection failed"* ]]; then
|
||||
print_error "Failed to connect to relay for $description"
|
||||
return 1
|
||||
elif [[ "$response" == *"false"* ]]; then
|
||||
# Extract error message
|
||||
local error_msg=$(echo "$response" | grep -o '"[^"]*invalid[^"]*"' | head -1 | sed 's/"//g' 2>/dev/null || echo "rejected")
|
||||
print_success "$description correctly rejected: $error_msg"
|
||||
echo # Add blank line for readability
|
||||
return 0
|
||||
elif [[ "$response" == *"true"* ]]; then
|
||||
print_error "$description was incorrectly accepted (should have been rejected)"
|
||||
echo # Add blank line for readability
|
||||
return 1
|
||||
else
|
||||
print_warning "$description response unclear: $response"
|
||||
echo # Add blank line for readability
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Test subscription with filters
|
||||
test_subscription() {
|
||||
local sub_id="$1"
|
||||
@@ -211,7 +252,41 @@ run_comprehensive_test() {
|
||||
# Brief pause to let events settle
|
||||
sleep 2
|
||||
|
||||
print_header "PHASE 2: Testing Subscriptions and Filters"
|
||||
print_header "PHASE 2: Testing Invalid Events (NIP-01 Validation)"
|
||||
|
||||
print_step "Testing various invalid events that should be rejected..."
|
||||
|
||||
# Test 1: Event with invalid JSON structure (malformed)
|
||||
local malformed_event='{"id":"invalid","pubkey":"invalid_pubkey","created_at":"not_a_number","kind":1,"tags":[],"content":"test"}'
|
||||
publish_invalid_event "$malformed_event" "malformed event with invalid created_at" "invalid"
|
||||
|
||||
# Test 2: Event with missing required fields
|
||||
local missing_field_event='{"id":"test123","pubkey":"valid_pubkey","kind":1,"tags":[],"content":"test"}'
|
||||
publish_invalid_event "$missing_field_event" "event missing created_at and sig" "invalid"
|
||||
|
||||
# Test 3: Event with invalid pubkey format (not hex)
|
||||
local invalid_pubkey_event='{"id":"abc123","pubkey":"not_valid_hex_pubkey","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"fake_sig"}'
|
||||
publish_invalid_event "$invalid_pubkey_event" "event with invalid pubkey format" "invalid"
|
||||
|
||||
# Test 4: Event with invalid event ID format
|
||||
local invalid_id_event='{"id":"not_64_char_hex","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
|
||||
publish_invalid_event "$invalid_id_event" "event with invalid ID format" "invalid"
|
||||
|
||||
# Test 5: Event with invalid signature
|
||||
local invalid_sig_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":[],"content":"test","sig":"invalid_signature_format"}'
|
||||
publish_invalid_event "$invalid_sig_event" "event with invalid signature format" "invalid"
|
||||
|
||||
# Test 6: Event with invalid kind (negative)
|
||||
local invalid_kind_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":-1,"tags":[],"content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
|
||||
publish_invalid_event "$invalid_kind_event" "event with negative kind" "invalid"
|
||||
|
||||
# Test 7: Event with invalid tags format (not array)
|
||||
local invalid_tags_event='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","created_at":1234567890,"kind":1,"tags":"not_an_array","content":"test","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
|
||||
publish_invalid_event "$invalid_tags_event" "event with invalid tags format" "invalid"
|
||||
|
||||
print_success "Invalid event tests completed - all should have been rejected"
|
||||
|
||||
print_header "PHASE 3: Testing Subscriptions and Filters"
|
||||
|
||||
# Test subscription filters
|
||||
print_step "Testing various subscription filters..."
|
||||
@@ -240,7 +315,7 @@ run_comprehensive_test() {
|
||||
# Test 7: Limit results
|
||||
test_subscription "test_limit" '{"kinds":[1],"limit":1}' "Limited to 1 event" "1"
|
||||
|
||||
print_header "PHASE 3: Database Verification"
|
||||
print_header "PHASE 4: Database Verification"
|
||||
|
||||
# Check what's actually stored in the database
|
||||
print_step "Verifying database contents..."
|
||||
@@ -265,13 +340,14 @@ run_comprehensive_test() {
|
||||
}
|
||||
|
||||
# Run the comprehensive test
|
||||
print_header "Starting C-Relay Comprehensive Test Suite"
|
||||
print_header "Starting C-Relay Comprehensive Test Suite with NIP-01 Validation"
|
||||
echo
|
||||
|
||||
if run_comprehensive_test; then
|
||||
echo
|
||||
print_success "All tests completed successfully!"
|
||||
print_info "The C-Relay hybrid schema implementation is working correctly"
|
||||
print_info "The C-Relay with full NIP-01 validation is working correctly"
|
||||
print_info "✅ Event validation, signature verification, and error handling all working"
|
||||
echo
|
||||
exit 0
|
||||
else
|
||||
|
||||
386
tests/9_delete_test.sh
Executable file
386
tests/9_delete_test.sh
Executable file
@@ -0,0 +1,386 @@
|
||||
#!/bin/bash
|
||||
|
||||
# NIP-09 Event Deletion Request Test for C-Relay
|
||||
# Tests deletion request functionality - assumes relay is already running
|
||||
# Based on the pattern from 1_nip_test.sh
|
||||
|
||||
set -e
|
||||
|
||||
# Color constants
|
||||
RED='\033[31m'
|
||||
GREEN='\033[32m'
|
||||
YELLOW='\033[33m'
|
||||
BLUE='\033[34m'
|
||||
BOLD='\033[1m'
|
||||
RESET='\033[0m'
|
||||
|
||||
# Test configuration
|
||||
RELAY_URL="ws://127.0.0.1:8888"
|
||||
TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"
|
||||
|
||||
# Print functions
|
||||
print_header() {
|
||||
echo -e "${BLUE}${BOLD}=== $1 ===${RESET}"
|
||||
}
|
||||
|
||||
print_step() {
|
||||
echo -e "${YELLOW}[STEP]${RESET} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}✓${RESET} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}✗${RESET} $1"
|
||||
}
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${RESET} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${RESET} $1"
|
||||
}
|
||||
|
||||
# Helper function to publish event and extract ID
|
||||
publish_event() {
|
||||
local event_json="$1"
|
||||
local description="$2"
|
||||
|
||||
# Extract event ID
|
||||
local event_id=$(echo "$event_json" | jq -r '.id' 2>/dev/null)
|
||||
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
|
||||
print_error "Could not extract event ID from $description"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Publishing $description..."
|
||||
|
||||
# Create EVENT message in Nostr format
|
||||
local event_message="[\"EVENT\",$event_json]"
|
||||
|
||||
# Publish to relay
|
||||
local response=""
|
||||
if command -v websocat &> /dev/null; then
|
||||
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
|
||||
else
|
||||
print_error "websocat not found - required for testing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check response
|
||||
if [[ "$response" == *"Connection failed"* ]]; then
|
||||
print_error "Failed to connect to relay for $description"
|
||||
return 1
|
||||
elif [[ "$response" == *"true"* ]]; then
|
||||
print_success "$description uploaded (ID: ${event_id:0:16}...)"
|
||||
echo "$event_id"
|
||||
return 0
|
||||
else
|
||||
print_warning "$description might have failed: $response"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to publish deletion request
|
||||
publish_deletion_request() {
|
||||
local deletion_event_json="$1"
|
||||
local description="$2"
|
||||
|
||||
# Extract event ID
|
||||
local event_id=$(echo "$deletion_event_json" | jq -r '.id' 2>/dev/null)
|
||||
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
|
||||
print_error "Could not extract event ID from $description"
|
||||
return 1
|
||||
fi
|
||||
|
||||
print_info "Publishing $description..."
|
||||
|
||||
# Create EVENT message in Nostr format
|
||||
local event_message="[\"EVENT\",$deletion_event_json]"
|
||||
|
||||
# Publish to relay
|
||||
local response=""
|
||||
if command -v websocat &> /dev/null; then
|
||||
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
|
||||
else
|
||||
print_error "websocat not found - required for testing"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Check response
|
||||
if [[ "$response" == *"Connection failed"* ]]; then
|
||||
print_error "Failed to connect to relay for $description"
|
||||
return 1
|
||||
elif [[ "$response" == *"true"* ]]; then
|
||||
print_success "$description accepted (ID: ${event_id:0:16}...)"
|
||||
echo "$event_id"
|
||||
return 0
|
||||
else
|
||||
print_warning "$description might have failed: $response"
|
||||
echo ""
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Helper function to check if event exists via subscription
|
||||
check_event_exists() {
|
||||
local event_id="$1"
|
||||
local sub_id="exists_$(date +%s%N | cut -c1-10)"
|
||||
|
||||
# Create REQ message to query for specific event ID
|
||||
local req_message="[\"REQ\",\"$sub_id\",{\"ids\":[\"$event_id\"]}]"
|
||||
|
||||
# Send subscription and collect events
|
||||
local response=""
|
||||
if command -v websocat &> /dev/null; then
|
||||
response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Count EVENT responses
|
||||
local event_count=0
|
||||
if [[ -n "$response" ]]; then
|
||||
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
echo "$event_count"
|
||||
}
|
||||
|
||||
# Helper function to query events by kind
|
||||
query_events_by_kind() {
|
||||
local kind="$1"
|
||||
local sub_id="kind${kind}_$(date +%s%N | cut -c1-10)"
|
||||
|
||||
# Create REQ message to query for events of specific kind
|
||||
local req_message="[\"REQ\",\"$sub_id\",{\"kinds\":[$kind]}]"
|
||||
|
||||
# Send subscription and collect events
|
||||
local response=""
|
||||
if command -v websocat &> /dev/null; then
|
||||
response=$(echo -e "$req_message\n[\"CLOSE\",\"$sub_id\"]" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
|
||||
fi
|
||||
|
||||
# Count EVENT responses
|
||||
local event_count=0
|
||||
if [[ -n "$response" ]]; then
|
||||
event_count=$(echo "$response" | grep -c "\"EVENT\"" 2>/dev/null || echo "0")
|
||||
fi
|
||||
|
||||
echo "$event_count"
|
||||
}
|
||||
|
||||
# Main test function
|
||||
run_deletion_test() {
|
||||
print_header "NIP-09 Event Deletion Request Test"
|
||||
|
||||
# Check dependencies
|
||||
print_step "Checking dependencies..."
|
||||
if ! command -v nak &> /dev/null; then
|
||||
print_error "nak command not found"
|
||||
print_info "Please install nak: go install github.com/fiatjaf/nak@latest"
|
||||
return 1
|
||||
fi
|
||||
if ! command -v websocat &> /dev/null; then
|
||||
print_error "websocat command not found"
|
||||
print_info "Please install websocat for testing"
|
||||
return 1
|
||||
fi
|
||||
if ! command -v jq &> /dev/null; then
|
||||
print_error "jq command not found"
|
||||
print_info "Please install jq for JSON processing"
|
||||
return 1
|
||||
fi
|
||||
print_success "All dependencies found"
|
||||
|
||||
print_header "PHASE 1: Publishing Events to be Deleted"
|
||||
|
||||
# Create test events that will be deleted
|
||||
print_step "Creating events for deletion testing..."
|
||||
|
||||
# Create regular events (kind 1) - these will be deleted by ID
|
||||
local event1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Event to be deleted #1" -k 1 --ts $(($(date +%s) - 100)) -t "type=test" -t "phase=deletion" 2>/dev/null)
|
||||
local event2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Event to be deleted #2" -k 1 --ts $(($(date +%s) - 90)) -t "type=test" -t "phase=deletion" 2>/dev/null)
|
||||
|
||||
# Publish the events
|
||||
event1_id=$(publish_event "$event1" "Event to be deleted #1")
|
||||
if [[ -z "$event1_id" ]]; then
|
||||
print_error "Failed to publish test event #1"
|
||||
return 1
|
||||
fi
|
||||
|
||||
event2_id=$(publish_event "$event2" "Event to be deleted #2")
|
||||
if [[ -z "$event2_id" ]]; then
|
||||
print_error "Failed to publish test event #2"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create an addressable event (kind 30001) - will be deleted by address
|
||||
local addr_event=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event to be deleted" -k 30001 --ts $(($(date +%s) - 80)) -t "d=test-delete" -t "type=addressable" 2>/dev/null)
|
||||
|
||||
addr_event_id=$(publish_event "$addr_event" "Addressable event to be deleted")
|
||||
if [[ -z "$addr_event_id" ]]; then
|
||||
print_error "Failed to publish addressable test event"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Create an event by a different author (to test unauthorized deletion)
|
||||
local different_key="nsec1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab"
|
||||
local unauth_event=$(nak event --sec "$different_key" -c "Event by different author" -k 1 --ts $(($(date +%s) - 70)) -t "type=unauthorized" 2>/dev/null)
|
||||
|
||||
unauth_event_id=$(publish_event "$unauth_event" "Event by different author")
|
||||
if [[ -z "$unauth_event_id" ]]; then
|
||||
print_warning "Failed to publish unauthorized test event - continuing anyway"
|
||||
fi
|
||||
|
||||
# Let events settle
|
||||
sleep 2
|
||||
|
||||
print_header "PHASE 2: Testing Event Deletion by ID"
|
||||
|
||||
print_step "Verifying events exist before deletion..."
|
||||
local event1_before=$(check_event_exists "$event1_id")
|
||||
local event2_before=$(check_event_exists "$event2_id")
|
||||
print_info "Event1 exists: $event1_before, Event2 exists: $event2_before"
|
||||
|
||||
# Create deletion request targeting the two events by ID
|
||||
print_step "Creating deletion request for events by ID..."
|
||||
local deletion_by_id=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Deleting events by ID" -k 5 --ts $(date +%s) -e "$event1_id" -e "$event2_id" -t "k=1" 2>/dev/null)
|
||||
|
||||
deletion_id=$(publish_deletion_request "$deletion_by_id" "Deletion request for events by ID")
|
||||
if [[ -z "$deletion_id" ]]; then
|
||||
print_error "Failed to publish deletion request"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Wait for deletion to process
|
||||
sleep 3
|
||||
|
||||
# Check if events were deleted
|
||||
print_step "Verifying events were deleted..."
|
||||
local event1_after=$(check_event_exists "$event1_id")
|
||||
local event2_after=$(check_event_exists "$event2_id")
|
||||
print_info "Event1 exists after deletion: $event1_after, Event2 exists after deletion: $event2_after"
|
||||
|
||||
if [[ "$event1_after" == "0" && "$event2_after" == "0" ]]; then
|
||||
print_success "✓ Events successfully deleted by ID"
|
||||
else
|
||||
print_error "✗ Events were not properly deleted"
|
||||
fi
|
||||
|
||||
print_header "PHASE 3: Testing Address-based Deletion"
|
||||
|
||||
if [[ -n "$addr_event_id" ]]; then
|
||||
print_step "Verifying addressable event exists before deletion..."
|
||||
local addr_before=$(check_event_exists "$addr_event_id")
|
||||
print_info "Addressable event exists: $addr_before"
|
||||
|
||||
# Create deletion request for addressable event using 'a' tag
|
||||
print_step "Creating deletion request for addressable event..."
|
||||
local test_pubkey=$(echo "$addr_event" | jq -r '.pubkey' 2>/dev/null)
|
||||
local deletion_by_addr=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Deleting addressable event" -k 5 --ts $(date +%s) -t "a=30001:${test_pubkey}:test-delete" -t "k=30001" 2>/dev/null)
|
||||
|
||||
addr_deletion_id=$(publish_deletion_request "$deletion_by_addr" "Deletion request for addressable event")
|
||||
if [[ -n "$addr_deletion_id" ]]; then
|
||||
# Wait for deletion to process
|
||||
sleep 3
|
||||
|
||||
# Check if addressable event was deleted
|
||||
print_step "Verifying addressable event was deleted..."
|
||||
local addr_after=$(check_event_exists "$addr_event_id")
|
||||
print_info "Addressable event exists after deletion: $addr_after"
|
||||
|
||||
if [[ "$addr_after" == "0" ]]; then
|
||||
print_success "✓ Addressable event successfully deleted"
|
||||
else
|
||||
print_error "✗ Addressable event was not properly deleted"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
print_header "PHASE 4: Testing Unauthorized Deletion"
|
||||
|
||||
if [[ -n "$unauth_event_id" ]]; then
|
||||
print_step "Testing unauthorized deletion attempt..."
|
||||
|
||||
# Try to delete the unauthorized event (should fail)
|
||||
local unauth_deletion=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Attempting unauthorized deletion" -k 5 --ts $(date +%s) -e "$unauth_event_id" -t "k=1" 2>/dev/null)
|
||||
|
||||
unauth_deletion_id=$(publish_deletion_request "$unauth_deletion" "Unauthorized deletion request")
|
||||
if [[ -n "$unauth_deletion_id" ]]; then
|
||||
# Wait for processing
|
||||
sleep 3
|
||||
|
||||
# Check if unauthorized event still exists (should still exist)
|
||||
local unauth_after=$(check_event_exists "$unauth_event_id")
|
||||
print_info "Unauthorized event exists after deletion attempt: $unauth_after"
|
||||
|
||||
if [[ "$unauth_after" == "1" ]]; then
|
||||
print_success "✓ Unauthorized deletion properly rejected - event still exists"
|
||||
else
|
||||
print_error "✗ Unauthorized deletion succeeded - security vulnerability!"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
print_header "PHASE 5: Testing Invalid Deletion Requests"
|
||||
|
||||
print_step "Testing deletion request with no targets..."
|
||||
|
||||
# Create deletion request with no 'e' or 'a' tags (should be rejected)
|
||||
local invalid_deletion='{"id":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef","pubkey":"aa4fc8665f5696e33db7e1a572e3b0f5b3d615837b0f362dcb1c8068b098c7b4","created_at":'$(date +%s)',"kind":5,"tags":[["k","1"]],"content":"Invalid deletion request with no targets","sig":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
|
||||
|
||||
# Create EVENT message in Nostr format
|
||||
local invalid_message="[\"EVENT\",$invalid_deletion]"
|
||||
|
||||
# Publish to relay
|
||||
local invalid_response=""
|
||||
if command -v websocat &> /dev/null; then
|
||||
invalid_response=$(echo "$invalid_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
|
||||
fi
|
||||
|
||||
# Check response - should be rejected
|
||||
if [[ "$invalid_response" == *"false"* ]]; then
|
||||
print_success "✓ Invalid deletion request properly rejected"
|
||||
elif [[ "$invalid_response" == *"true"* ]]; then
|
||||
print_warning "⚠ Invalid deletion request was accepted (should have been rejected)"
|
||||
else
|
||||
print_info "Invalid deletion request response: $invalid_response"
|
||||
fi
|
||||
|
||||
print_header "PHASE 6: Verification"
|
||||
|
||||
# Verify deletion requests themselves are stored
|
||||
print_step "Verifying deletion requests are stored..."
|
||||
local deletion_count=$(query_events_by_kind 5)
|
||||
print_info "Deletion requests accessible via query: $deletion_count"
|
||||
|
||||
if [[ "$deletion_count" -gt 0 ]]; then
|
||||
print_success "✓ Deletion requests properly stored and queryable"
|
||||
else
|
||||
print_warning "⚠ No deletion requests found via query"
|
||||
fi
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
# Run the test
|
||||
print_header "Starting NIP-09 Event Deletion Request Test Suite"
|
||||
echo
|
||||
|
||||
if run_deletion_test; then
|
||||
echo
|
||||
print_success "All NIP-09 deletion tests completed successfully!"
|
||||
print_info "The C-Relay NIP-09 implementation is working correctly"
|
||||
print_info "✅ Event deletion by ID working"
|
||||
print_info "✅ Address-based deletion working"
|
||||
print_info "✅ Authorization validation working"
|
||||
print_info "✅ Invalid deletion rejection working"
|
||||
echo
|
||||
exit 0
|
||||
else
|
||||
echo
|
||||
print_error "Some NIP-09 tests failed"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user