Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b35f463ae | ||
|
|
7b74486519 | ||
|
|
b276b44ded | ||
|
|
3792649ed9 | ||
|
|
5f08956605 | ||
|
|
643d89ed7b | ||
|
|
8ca459593c | ||
|
|
ee4208cc19 | ||
|
|
f6330e4bb8 | ||
|
|
4f3cf10a5c | ||
|
|
aa1954e81e | ||
|
|
482597bd0e | ||
|
|
d4b90e681c | ||
|
|
fcf9e43c4c | ||
|
|
b8d8cd19d3 | ||
|
|
536c2d966c | ||
|
|
f49cb0a5ac | ||
|
|
cef6bb2340 | ||
|
|
4c03253b30 | ||
|
|
ed09bb7370 | ||
|
|
5c46a25173 | ||
|
|
d1538f00df | ||
|
|
afa4acbbfb | ||
|
|
d9a530485f | ||
|
|
b2ad70b028 | ||
|
|
f49aae8ab0 | ||
|
|
f6debcf799 | ||
|
|
edbc4f1359 |
65
NOSTR_RELEASE.md
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
# Relay
|
||||||
|
|
||||||
|
I am releasing the code for the nostr relay that I wrote use myself. The code is free for anyone to use in any way that they wish.
|
||||||
|
|
||||||
|
Some of the features of this relay are conventional, and some are unconventional.
|
||||||
|
|
||||||
|
## The conventional
|
||||||
|
|
||||||
|
This relay is written in C99 with a sqlite database.
|
||||||
|
|
||||||
|
It implements the following NIPs.
|
||||||
|
|
||||||
|
- [x] NIP-01: Basic protocol flow implementation
|
||||||
|
- [x] NIP-09: Event deletion
|
||||||
|
- [x] NIP-11: Relay information document
|
||||||
|
- [x] NIP-13: Proof of Work
|
||||||
|
- [x] NIP-15: End of Stored Events Notice
|
||||||
|
- [x] NIP-20: Command Results
|
||||||
|
- [x] NIP-33: Parameterized Replaceable Events
|
||||||
|
- [x] NIP-40: Expiration Timestamp
|
||||||
|
- [x] NIP-42: Authentication of clients to relays
|
||||||
|
- [x] NIP-45: Counting results
|
||||||
|
- [x] NIP-50: Keywords filter
|
||||||
|
- [x] NIP-70: Protected Events
|
||||||
|
|
||||||
|
## The unconventional
|
||||||
|
|
||||||
|
### The binaries are fully self contained.
|
||||||
|
|
||||||
|
It should just run in linux without having to worry about what you have on your system. I want to download and run. No docker. No dependency hell.
|
||||||
|
|
||||||
|
I'm not bothering with other operating systems.
|
||||||
|
|
||||||
|
### The relay is a full nostr citizen with it's own public and private keys.
|
||||||
|
|
||||||
|
For example, you can see my relay's profile (wss://relay.laantungir.net) running here:
|
||||||
|
|
||||||
|
[Primal link](https://primal.net/p/nprofile1qqswn2jsmm8lq8evas0v9vhqkdpn9nuujt90mtz60nqgsxndy66es4qjjnhr7)
|
||||||
|
|
||||||
|
What this means in practice is that when you start the relay, it generates keys for itself, and for it's administrator (You can specify these if you wish)
|
||||||
|
|
||||||
|
Now the program and the administrator can have verifed communication between the two, simply by using nostr events. For example, the administrator can send DMs to the relay, asking it's status, and changing it's configuration through any client that can handle nip17 DMs. The relay can also send notifications to the administrator about it's current status, or it can publish it's status on a regular schedule directly to NOSTR as kind-1 notes.
|
||||||
|
|
||||||
|
Also included is a more standard administrative web front end. This front end communicates to the relay using an extensive api, which again is nostr events signed by the administrator. You can completely control the relay through signed nostr events.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|
Main page with real time updates.
|
||||||
|
|
||||||
|

|
||||||
|
Set your configuration preferences.
|
||||||
|
|
||||||
|

|
||||||
|
View current subscriptions
|
||||||
|
|
||||||
|

|
||||||
|
Add npubs to white or black lists.
|
||||||
|
|
||||||
|

|
||||||
|
Run sql queries on the database.
|
||||||
|
|
||||||
|

|
||||||
|
Light mode.
|
||||||
|
|
||||||
612
STATIC_MUSL_GUIDE.md
Normal file
@@ -0,0 +1,612 @@
|
|||||||
|
# Static MUSL Build Guide for C Programs
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide explains how to build truly portable static binaries using Alpine Linux and MUSL libc. These binaries have **zero runtime dependencies** and work on any Linux distribution without modification.
|
||||||
|
|
||||||
|
This guide is specifically tailored for C programs that use:
|
||||||
|
- **nostr_core_lib** - Nostr protocol implementation
|
||||||
|
- **nostr_login_lite** - Nostr authentication library
|
||||||
|
- Common dependencies: libwebsockets, OpenSSL, SQLite, curl, secp256k1
|
||||||
|
|
||||||
|
## Why MUSL Static Binaries?
|
||||||
|
|
||||||
|
### Advantages Over glibc
|
||||||
|
|
||||||
|
| Feature | MUSL Static | glibc Static | glibc Dynamic |
|
||||||
|
|---------|-------------|--------------|---------------|
|
||||||
|
| **Portability** | ✓ Any Linux | ⚠ glibc only | ✗ Requires matching libs |
|
||||||
|
| **Binary Size** | ~7-10 MB | ~12-15 MB | ~2-3 MB |
|
||||||
|
| **Dependencies** | None | NSS libs | Many system libs |
|
||||||
|
| **Deployment** | Single file | Single file + NSS | Binary + libraries |
|
||||||
|
| **Compatibility** | Universal | glibc version issues | Library version hell |
|
||||||
|
|
||||||
|
### Key Benefits
|
||||||
|
|
||||||
|
1. **True Portability**: Works on Alpine, Ubuntu, Debian, CentOS, Arch, etc.
|
||||||
|
2. **No Library Hell**: No `GLIBC_2.XX not found` errors
|
||||||
|
3. **Simple Deployment**: Just copy one file
|
||||||
|
4. **Reproducible Builds**: Same Docker image = same binary
|
||||||
|
5. **Security**: No dependency on system libraries with vulnerabilities
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- Docker installed and running
|
||||||
|
- Your C project with source code
|
||||||
|
- Internet connection for downloading dependencies
|
||||||
|
|
||||||
|
### Basic Build Process
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copy the Dockerfile template (see below)
|
||||||
|
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
|
||||||
|
|
||||||
|
# 2. Customize for your project (see Customization section)
|
||||||
|
vim Dockerfile.static
|
||||||
|
|
||||||
|
# 3. Build the static binary
|
||||||
|
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-builder .
|
||||||
|
|
||||||
|
# 4. Extract the binary
|
||||||
|
docker create --name temp-container my-app-builder
|
||||||
|
docker cp temp-container:/build/my_app_static ./my_app_static
|
||||||
|
docker rm temp-container
|
||||||
|
|
||||||
|
# 5. Verify it's static
|
||||||
|
ldd ./my_app_static # Should show "not a dynamic executable"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dockerfile Template
|
||||||
|
|
||||||
|
Here's a complete Dockerfile template you can customize for your project:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
# Alpine-based MUSL static binary builder
|
||||||
|
# Produces truly portable binaries with zero runtime dependencies
|
||||||
|
|
||||||
|
FROM alpine:3.19 AS builder
|
||||||
|
|
||||||
|
# Install build dependencies
|
||||||
|
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 \
|
||||||
|
wget \
|
||||||
|
bash
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build libsecp256k1 static (required for Nostr)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||||
|
cd secp256k1 && \
|
||||||
|
./autogen.sh && \
|
||||||
|
./configure --enable-static --disable-shared --prefix=/usr \
|
||||||
|
CFLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install && \
|
||||||
|
rm -rf /tmp/secp256k1
|
||||||
|
|
||||||
|
# Build libwebsockets static (if needed for WebSocket support)
|
||||||
|
RUN cd /tmp && \
|
||||||
|
git clone --depth 1 --branch v4.3.3 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_WITHOUT_TESTAPPS=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_SERVER=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_CLIENT=ON \
|
||||||
|
-DLWS_WITHOUT_TEST_PING=ON \
|
||||||
|
-DLWS_WITH_HTTP2=OFF \
|
||||||
|
-DLWS_WITH_LIBUV=OFF \
|
||||||
|
-DLWS_WITH_LIBEVENT=OFF \
|
||||||
|
-DLWS_IPV6=ON \
|
||||||
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DCMAKE_INSTALL_PREFIX=/usr \
|
||||||
|
-DCMAKE_C_FLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && \
|
||||||
|
make install && \
|
||||||
|
rm -rf /tmp/libwebsockets
|
||||||
|
|
||||||
|
# Copy git configuration for submodules
|
||||||
|
COPY .gitmodules /build/.gitmodules
|
||||||
|
COPY .git /build/.git
|
||||||
|
|
||||||
|
# Initialize submodules
|
||||||
|
RUN git submodule update --init --recursive
|
||||||
|
|
||||||
|
# Copy and build nostr_core_lib
|
||||||
|
COPY nostr_core_lib /build/nostr_core_lib/
|
||||||
|
RUN cd nostr_core_lib && \
|
||||||
|
chmod +x build.sh && \
|
||||||
|
sed -i 's/CFLAGS="-Wall -Wextra -std=c99 -fPIC -O2"/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall -Wextra -std=c99 -fPIC -O2"/' build.sh && \
|
||||||
|
rm -f *.o *.a 2>/dev/null || true && \
|
||||||
|
./build.sh --nips=1,6,13,17,19,44,59
|
||||||
|
|
||||||
|
# Copy and build nostr_login_lite (if used)
|
||||||
|
# COPY nostr_login_lite /build/nostr_login_lite/
|
||||||
|
# RUN cd nostr_login_lite && make static
|
||||||
|
|
||||||
|
# Copy your application source
|
||||||
|
COPY src/ /build/src/
|
||||||
|
COPY Makefile /build/Makefile
|
||||||
|
|
||||||
|
# Build your application with full static linking
|
||||||
|
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
-I. -Inostr_core_lib -Inostr_core_lib/nostr_core \
|
||||||
|
-Inostr_core_lib/cjson -Inostr_core_lib/nostr_websocket \
|
||||||
|
src/*.c \
|
||||||
|
-o /build/my_app_static \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lwebsockets -lssl -lcrypto -lsqlite3 -lsecp256k1 \
|
||||||
|
-lcurl -lz -lpthread -lm -ldl && \
|
||||||
|
strip /build/my_app_static
|
||||||
|
|
||||||
|
# Verify it's truly static
|
||||||
|
RUN echo "=== Binary Information ===" && \
|
||||||
|
file /build/my_app_static && \
|
||||||
|
ls -lh /build/my_app_static && \
|
||||||
|
echo "=== Checking for dynamic dependencies ===" && \
|
||||||
|
(ldd /build/my_app_static 2>&1 || echo "Binary is static")
|
||||||
|
|
||||||
|
# Output stage - just the binary
|
||||||
|
FROM scratch AS output
|
||||||
|
COPY --from=builder /build/my_app_static /my_app_static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization Guide
|
||||||
|
|
||||||
|
### 1. Adjust Dependencies
|
||||||
|
|
||||||
|
**Add dependencies** by modifying the `apk add` section:
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
musl-dev \
|
||||||
|
# Add your dependencies here:
|
||||||
|
libpng-dev \
|
||||||
|
libpng-static \
|
||||||
|
libjpeg-turbo-dev \
|
||||||
|
libjpeg-turbo-static
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remove unused dependencies** to speed up builds:
|
||||||
|
- Remove `libwebsockets` section if you don't need WebSocket support
|
||||||
|
- Remove `sqlite` if you don't use databases
|
||||||
|
- Remove `curl` if you don't make HTTP requests
|
||||||
|
|
||||||
|
### 2. Configure nostr_core_lib NIPs
|
||||||
|
|
||||||
|
Specify which NIPs your application needs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh --nips=1,6,19 # Minimal: Basic protocol, keys, bech32
|
||||||
|
./build.sh --nips=1,6,13,17,19,44,59 # Full: All common NIPs
|
||||||
|
./build.sh --nips=all # Everything available
|
||||||
|
```
|
||||||
|
|
||||||
|
**Common NIP combinations:**
|
||||||
|
- **Basic client**: `1,6,19` (events, keys, bech32)
|
||||||
|
- **With encryption**: `1,6,19,44` (add modern encryption)
|
||||||
|
- **With DMs**: `1,6,17,19,44,59` (add private messages)
|
||||||
|
- **Relay/server**: `1,6,13,17,19,42,44,59` (add PoW, auth)
|
||||||
|
|
||||||
|
### 3. Modify Compilation Flags
|
||||||
|
|
||||||
|
**For your application:**
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN gcc -static -O2 -Wall -Wextra -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \ # REQUIRED for MUSL
|
||||||
|
-I. -Inostr_core_lib \ # Include paths
|
||||||
|
src/*.c \ # Your source files
|
||||||
|
-o /build/my_app_static \ # Output binary
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \ # Nostr library
|
||||||
|
-lwebsockets -lssl -lcrypto \ # Link libraries
|
||||||
|
-lsqlite3 -lsecp256k1 -lcurl \
|
||||||
|
-lz -lpthread -lm -ldl
|
||||||
|
```
|
||||||
|
|
||||||
|
**Debug build** (with symbols, no optimization):
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN gcc -static -g -O0 -DDEBUG \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
# ... rest of flags
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Multi-Architecture Support
|
||||||
|
|
||||||
|
Build for different architectures:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# x86_64 (Intel/AMD)
|
||||||
|
docker build --platform linux/amd64 -f Dockerfile.static -t my-app-x86 .
|
||||||
|
|
||||||
|
# ARM64 (Apple Silicon, Raspberry Pi 4+)
|
||||||
|
docker build --platform linux/arm64 -f Dockerfile.static -t my-app-arm64 .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Build Script Template
|
||||||
|
|
||||||
|
Create a `build_static.sh` script for convenience:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
BUILD_DIR="$SCRIPT_DIR/build"
|
||||||
|
DOCKERFILE="$SCRIPT_DIR/Dockerfile.static"
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
case "$ARCH" in
|
||||||
|
x86_64)
|
||||||
|
PLATFORM="linux/amd64"
|
||||||
|
OUTPUT_NAME="my_app_static_x86_64"
|
||||||
|
;;
|
||||||
|
aarch64|arm64)
|
||||||
|
PLATFORM="linux/arm64"
|
||||||
|
OUTPUT_NAME="my_app_static_arm64"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown architecture: $ARCH"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Building for platform: $PLATFORM"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
# Build Docker image
|
||||||
|
docker build \
|
||||||
|
--platform "$PLATFORM" \
|
||||||
|
-f "$DOCKERFILE" \
|
||||||
|
-t my-app-builder:latest \
|
||||||
|
--progress=plain \
|
||||||
|
.
|
||||||
|
|
||||||
|
# Extract binary
|
||||||
|
CONTAINER_ID=$(docker create my-app-builder:latest)
|
||||||
|
docker cp "$CONTAINER_ID:/build/my_app_static" "$BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
docker rm "$CONTAINER_ID"
|
||||||
|
|
||||||
|
chmod +x "$BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
|
||||||
|
echo "✓ Build complete: $BUILD_DIR/$OUTPUT_NAME"
|
||||||
|
echo "✓ Size: $(du -h "$BUILD_DIR/$OUTPUT_NAME" | cut -f1)"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
if ldd "$BUILD_DIR/$OUTPUT_NAME" 2>&1 | grep -q "not a dynamic executable"; then
|
||||||
|
echo "✓ Binary is fully static"
|
||||||
|
else
|
||||||
|
echo "⚠ Warning: Binary may have dynamic dependencies"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Make it executable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x build_static.sh
|
||||||
|
./build_static.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues and Solutions
|
||||||
|
|
||||||
|
### Issue 1: Fortification Errors
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
undefined reference to '__snprintf_chk'
|
||||||
|
undefined reference to '__fprintf_chk'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: GCC's `-O2` enables fortification by default, which uses glibc-specific functions.
|
||||||
|
|
||||||
|
**Solution**: Add these flags to **all** compilation commands:
|
||||||
|
```bash
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0
|
||||||
|
```
|
||||||
|
|
||||||
|
This must be applied to:
|
||||||
|
1. nostr_core_lib build.sh
|
||||||
|
2. Your application compilation
|
||||||
|
3. Any other libraries you build
|
||||||
|
|
||||||
|
### Issue 2: Missing Symbols from nostr_core_lib
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
undefined reference to 'nostr_create_event'
|
||||||
|
undefined reference to 'nostr_sign_event'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause**: Required NIPs not included in nostr_core_lib build.
|
||||||
|
|
||||||
|
**Solution**: Add missing NIPs:
|
||||||
|
```bash
|
||||||
|
./build.sh --nips=1,6,19 # Add the NIPs you need
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 3: Docker Permission Denied
|
||||||
|
|
||||||
|
**Error:**
|
||||||
|
```
|
||||||
|
permission denied while trying to connect to the Docker daemon socket
|
||||||
|
```
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
```bash
|
||||||
|
sudo usermod -aG docker $USER
|
||||||
|
newgrp docker # Or logout and login
|
||||||
|
```
|
||||||
|
|
||||||
|
### Issue 4: Binary Won't Run on Target System
|
||||||
|
|
||||||
|
**Checks**:
|
||||||
|
```bash
|
||||||
|
# 1. Verify it's static
|
||||||
|
ldd my_app_static # Should show "not a dynamic executable"
|
||||||
|
|
||||||
|
# 2. Check architecture
|
||||||
|
file my_app_static # Should match target system
|
||||||
|
|
||||||
|
# 3. Test on different distributions
|
||||||
|
docker run --rm -v $(pwd):/app alpine:latest /app/my_app_static --version
|
||||||
|
docker run --rm -v $(pwd):/app ubuntu:latest /app/my_app_static --version
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure Example
|
||||||
|
|
||||||
|
Organize your project for easy static builds:
|
||||||
|
|
||||||
|
```
|
||||||
|
my-nostr-app/
|
||||||
|
├── src/
|
||||||
|
│ ├── main.c
|
||||||
|
│ ├── handlers.c
|
||||||
|
│ └── utils.c
|
||||||
|
├── nostr_core_lib/ # Git submodule
|
||||||
|
├── nostr_login_lite/ # Git submodule (if used)
|
||||||
|
├── Dockerfile.static # Static build Dockerfile
|
||||||
|
├── build_static.sh # Build script
|
||||||
|
├── Makefile # Regular build
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Makefile Integration
|
||||||
|
|
||||||
|
Add static build targets to your Makefile:
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Regular dynamic build
|
||||||
|
all: my_app
|
||||||
|
|
||||||
|
my_app: src/*.c
|
||||||
|
gcc -O2 src/*.c -o my_app \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lssl -lcrypto -lsecp256k1 -lz -lpthread -lm
|
||||||
|
|
||||||
|
# Static MUSL build via Docker
|
||||||
|
static:
|
||||||
|
./build_static.sh
|
||||||
|
|
||||||
|
# Clean
|
||||||
|
clean:
|
||||||
|
rm -f my_app build/my_app_static_*
|
||||||
|
|
||||||
|
.PHONY: all static clean
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
### Single Binary Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Copy to server
|
||||||
|
scp build/my_app_static_x86_64 user@server:/opt/my-app/
|
||||||
|
|
||||||
|
# Run (no dependencies needed!)
|
||||||
|
ssh user@server
|
||||||
|
/opt/my-app/my_app_static_x86_64
|
||||||
|
```
|
||||||
|
|
||||||
|
### SystemD Service
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[Unit]
|
||||||
|
Description=My Nostr Application
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=myapp
|
||||||
|
WorkingDirectory=/opt/my-app
|
||||||
|
ExecStart=/opt/my-app/my_app_static_x86_64
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker Container (Minimal)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM scratch
|
||||||
|
COPY my_app_static_x86_64 /app
|
||||||
|
ENTRYPOINT ["/app"]
|
||||||
|
```
|
||||||
|
|
||||||
|
Build and run:
|
||||||
|
```bash
|
||||||
|
docker build -t my-app:latest .
|
||||||
|
docker run --rm my-app:latest --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## Reusing c-relay Files
|
||||||
|
|
||||||
|
You can directly copy these files from c-relay:
|
||||||
|
|
||||||
|
### 1. Dockerfile.alpine-musl
|
||||||
|
```bash
|
||||||
|
cp /path/to/c-relay/Dockerfile.alpine-musl ./Dockerfile.static
|
||||||
|
```
|
||||||
|
|
||||||
|
Then customize:
|
||||||
|
- Change binary name (line 125)
|
||||||
|
- Adjust source files (line 122-124)
|
||||||
|
- Modify include paths (line 120-121)
|
||||||
|
|
||||||
|
### 2. build_static.sh
|
||||||
|
```bash
|
||||||
|
cp /path/to/c-relay/build_static.sh ./
|
||||||
|
```
|
||||||
|
|
||||||
|
Then customize:
|
||||||
|
- Change `OUTPUT_NAME` variable (lines 66, 70)
|
||||||
|
- Update Docker image name (line 98)
|
||||||
|
- Modify verification commands (lines 180-184)
|
||||||
|
|
||||||
|
### 3. .dockerignore (Optional)
|
||||||
|
```bash
|
||||||
|
cp /path/to/c-relay/.dockerignore ./
|
||||||
|
```
|
||||||
|
|
||||||
|
Helps speed up Docker builds by excluding unnecessary files.
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Version Control**: Commit your Dockerfile and build script
|
||||||
|
2. **Tag Builds**: Include git commit hash in binary version
|
||||||
|
3. **Test Thoroughly**: Verify on multiple distributions
|
||||||
|
4. **Document Dependencies**: List required NIPs and libraries
|
||||||
|
5. **Automate**: Use CI/CD to build on every commit
|
||||||
|
6. **Archive Binaries**: Keep old versions for rollback
|
||||||
|
|
||||||
|
## Performance Comparison
|
||||||
|
|
||||||
|
| Metric | MUSL Static | glibc Dynamic |
|
||||||
|
|--------|-------------|---------------|
|
||||||
|
| Binary Size | 7-10 MB | 2-3 MB + libs |
|
||||||
|
| Startup Time | ~50ms | ~40ms |
|
||||||
|
| Memory Usage | Similar | Similar |
|
||||||
|
| Portability | ✓ Universal | ✗ System-dependent |
|
||||||
|
| Deployment | Single file | Binary + libraries |
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [MUSL libc](https://musl.libc.org/)
|
||||||
|
- [Alpine Linux](https://alpinelinux.org/)
|
||||||
|
- [nostr_core_lib](https://github.com/chebizarro/nostr_core_lib)
|
||||||
|
- [Static Linking Best Practices](https://www.musl-libc.org/faq.html)
|
||||||
|
- [c-relay Implementation](./docs/musl_static_build.md)
|
||||||
|
|
||||||
|
## Example: Minimal Nostr Client
|
||||||
|
|
||||||
|
Here's a complete example of building a minimal Nostr client:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// minimal_client.c
|
||||||
|
#include "nostr_core/nostr_core.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Generate keypair
|
||||||
|
char nsec[64], npub[64];
|
||||||
|
nostr_generate_keypair(nsec, npub);
|
||||||
|
|
||||||
|
printf("Generated keypair:\n");
|
||||||
|
printf("Private key (nsec): %s\n", nsec);
|
||||||
|
printf("Public key (npub): %s\n", npub);
|
||||||
|
|
||||||
|
// Create event
|
||||||
|
cJSON *event = nostr_create_event(1, "Hello, Nostr!", NULL);
|
||||||
|
nostr_sign_event(event, nsec);
|
||||||
|
|
||||||
|
char *json = cJSON_Print(event);
|
||||||
|
printf("\nSigned event:\n%s\n", json);
|
||||||
|
|
||||||
|
free(json);
|
||||||
|
cJSON_Delete(event);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Dockerfile.static:**
|
||||||
|
```dockerfile
|
||||||
|
FROM alpine:3.19 AS builder
|
||||||
|
RUN apk add --no-cache build-base musl-dev git autoconf automake libtool \
|
||||||
|
openssl-dev openssl-libs-static zlib-dev zlib-static
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Build secp256k1
|
||||||
|
RUN cd /tmp && git clone https://github.com/bitcoin-core/secp256k1.git && \
|
||||||
|
cd secp256k1 && ./autogen.sh && \
|
||||||
|
./configure --enable-static --disable-shared --prefix=/usr CFLAGS="-fPIC" && \
|
||||||
|
make -j$(nproc) && make install
|
||||||
|
|
||||||
|
# Copy and build nostr_core_lib
|
||||||
|
COPY nostr_core_lib /build/nostr_core_lib/
|
||||||
|
RUN cd nostr_core_lib && \
|
||||||
|
sed -i 's/CFLAGS="-Wall/CFLAGS="-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 -Wall/' build.sh && \
|
||||||
|
./build.sh --nips=1,6,19
|
||||||
|
|
||||||
|
# Build application
|
||||||
|
COPY minimal_client.c /build/
|
||||||
|
RUN gcc -static -O2 -Wall -std=c99 \
|
||||||
|
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=0 \
|
||||||
|
-Inostr_core_lib -Inostr_core_lib/nostr_core -Inostr_core_lib/cjson \
|
||||||
|
minimal_client.c -o /build/minimal_client_static \
|
||||||
|
nostr_core_lib/libnostr_core_x64.a \
|
||||||
|
-lssl -lcrypto -lsecp256k1 -lz -lpthread -lm -ldl && \
|
||||||
|
strip /build/minimal_client_static
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=builder /build/minimal_client_static /minimal_client_static
|
||||||
|
```
|
||||||
|
|
||||||
|
**Build and run:**
|
||||||
|
```bash
|
||||||
|
docker build -f Dockerfile.static -t minimal-client .
|
||||||
|
docker create --name temp minimal-client
|
||||||
|
docker cp temp:/minimal_client_static ./
|
||||||
|
docker rm temp
|
||||||
|
|
||||||
|
./minimal_client_static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
Static MUSL binaries provide the best portability for C applications. While they're slightly larger than dynamic binaries, the benefits of zero dependencies and universal compatibility make them ideal for:
|
||||||
|
|
||||||
|
- Server deployments across different Linux distributions
|
||||||
|
- Embedded systems and IoT devices
|
||||||
|
- Docker containers (FROM scratch)
|
||||||
|
- Distribution to users without dependency management
|
||||||
|
- Long-term archival and reproducibility
|
||||||
|
|
||||||
|
Follow this guide to create portable, self-contained binaries for your Nostr applications!
|
||||||
@@ -1261,3 +1261,50 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
|
|||||||
.header-title.clickable:hover {
|
.header-title.clickable:hover {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ================================
|
||||||
|
SUBSCRIPTION TABLE COLLAPSIBLE GROUPS
|
||||||
|
================================ */
|
||||||
|
|
||||||
|
/* Subscription group header styles */
|
||||||
|
.subscription-group-header {
|
||||||
|
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-group-header:hover {
|
||||||
|
background-color: var(--secondary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail row styles */
|
||||||
|
.subscription-detail-row {
|
||||||
|
/* background-color: var(--secondary-color); */
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-detail-row:hover {
|
||||||
|
background-color: var(--muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail row cell styles */
|
||||||
|
.subscription-detail-prefix {
|
||||||
|
padding-left: 30px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-detail-id {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -212,18 +212,9 @@
|
|||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="config-table-container">
|
<div class="config-table-container">
|
||||||
<table class="config-table" id="subscription-details-table">
|
<table class="config-table" id="subscription-details-table">
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Subscription ID</th>
|
|
||||||
<th>Client IP</th>
|
|
||||||
<th>WSI Pointer</th>
|
|
||||||
<th>Duration</th>
|
|
||||||
<th>Filters</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="subscription-details-table-body">
|
<tbody id="subscription-details-table-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" style="text-align: center; font-style: italic;">No subscriptions active</td>
|
<td colspan="4" style="text-align: center; font-style: italic;">No subscriptions active</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
273
api/index.js
@@ -93,10 +93,11 @@ function log(message, type = 'INFO') {
|
|||||||
|
|
||||||
|
|
||||||
// NIP-59 helper: randomize created_at to thwart time-analysis (past 2 days)
|
// NIP-59 helper: randomize created_at to thwart time-analysis (past 2 days)
|
||||||
|
// TEMPORARILY DISABLED: Using current timestamp for debugging
|
||||||
function randomNow() {
|
function randomNow() {
|
||||||
const TWO_DAYS = 2 * 24 * 60 * 60; // 172800 seconds
|
// const TWO_DAYS = 2 * 24 * 60 * 60; // 172800 seconds
|
||||||
const now = Math.round(Date.now() / 1000);
|
const now = Math.round(Date.now() / 1000);
|
||||||
return Math.round(now - Math.random() * TWO_DAYS);
|
return now; // Math.round(now - Math.random() * TWO_DAYS);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safe JSON parse with error handling
|
// Safe JSON parse with error handling
|
||||||
@@ -1250,7 +1251,7 @@ async function processAdminResponse(event) {
|
|||||||
throw new Error('Failed to decrypt admin response content');
|
throw new Error('Failed to decrypt admin response content');
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Decrypted admin response:', decryptedContent);
|
// console.log('Decrypted admin response:', decryptedContent);
|
||||||
|
|
||||||
// Try to parse as JSON first, if it fails treat as plain text
|
// Try to parse as JSON first, if it fails treat as plain text
|
||||||
let responseData;
|
let responseData;
|
||||||
@@ -4033,9 +4034,9 @@ function updateStatsFromTimeMonitoringEvent(monitoringData) {
|
|||||||
|
|
||||||
// Extract values from periods array
|
// Extract values from periods array
|
||||||
monitoringData.periods.forEach(period => {
|
monitoringData.periods.forEach(period => {
|
||||||
if (period.period === '24h') timeStats.last_24h = period.count;
|
if (period.period === 'last_24h') timeStats.last_24h = period.count;
|
||||||
else if (period.period === '7d') timeStats.last_7d = period.count;
|
else if (period.period === 'last_7d') timeStats.last_7d = period.count;
|
||||||
else if (period.period === '30d') timeStats.last_30d = period.count;
|
else if (period.period === 'last_30d') timeStats.last_30d = period.count;
|
||||||
});
|
});
|
||||||
|
|
||||||
populateStatsTime({ time_stats: timeStats });
|
populateStatsTime({ time_stats: timeStats });
|
||||||
@@ -4069,7 +4070,7 @@ function updateStatsFromTopPubkeysMonitoringEvent(monitoringData) {
|
|||||||
function updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData) {
|
function updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData) {
|
||||||
try {
|
try {
|
||||||
// DEBUG: Log every subscription_details event that arrives at the webpage
|
// DEBUG: Log every subscription_details event that arrives at the webpage
|
||||||
console.log('subscription_details', JSON.stringify(monitoringData, null, 2));
|
// console.log('subscription_details', JSON.stringify(monitoringData, null, 2));
|
||||||
console.log('subscription_details decoded:', monitoringData);
|
console.log('subscription_details decoded:', monitoringData);
|
||||||
|
|
||||||
if (monitoringData.data_type !== 'subscription_details') {
|
if (monitoringData.data_type !== 'subscription_details') {
|
||||||
@@ -4278,101 +4279,185 @@ function populateSubscriptionDetailsTable(subscriptionsData) {
|
|||||||
const tableBody = document.getElementById('subscription-details-table-body');
|
const tableBody = document.getElementById('subscription-details-table-body');
|
||||||
if (!tableBody || !subscriptionsData || !Array.isArray(subscriptionsData)) return;
|
if (!tableBody || !subscriptionsData || !Array.isArray(subscriptionsData)) return;
|
||||||
|
|
||||||
|
// Store current expand/collapse state before rebuilding
|
||||||
|
const expandedGroups = new Set();
|
||||||
|
const headerRows = tableBody.querySelectorAll('.subscription-group-header');
|
||||||
|
headerRows.forEach(header => {
|
||||||
|
const wsiPointer = header.getAttribute('data-wsi-pointer');
|
||||||
|
const isExpanded = header.getAttribute('data-expanded') === 'true';
|
||||||
|
if (isExpanded) {
|
||||||
|
expandedGroups.add(wsiPointer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tableBody.innerHTML = '';
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
if (subscriptionsData.length === 0) {
|
if (subscriptionsData.length === 0) {
|
||||||
const row = document.createElement('tr');
|
const row = document.createElement('tr');
|
||||||
row.innerHTML = '<td colspan="5" style="text-align: center; font-style: italic;">No active subscriptions</td>';
|
row.innerHTML = '<td colspan="4" style="text-align: center; font-style: italic;">No active subscriptions</td>';
|
||||||
tableBody.appendChild(row);
|
tableBody.appendChild(row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptionsData.forEach((subscription, index) => {
|
// Sort subscriptions by wsi_pointer to group them together
|
||||||
const row = document.createElement('tr');
|
subscriptionsData.sort((a, b) => {
|
||||||
|
const wsiA = a.wsi_pointer || '';
|
||||||
// Calculate duration
|
const wsiB = b.wsi_pointer || '';
|
||||||
const now = Math.floor(Date.now() / 1000);
|
return wsiA.localeCompare(wsiB);
|
||||||
const duration = now - subscription.created_at;
|
|
||||||
const durationStr = formatDuration(duration);
|
|
||||||
|
|
||||||
// Format client IP (show full IP for admin view)
|
|
||||||
const clientIP = subscription.client_ip || 'unknown';
|
|
||||||
|
|
||||||
// Format wsi_pointer (show full pointer)
|
|
||||||
const wsiPointer = subscription.wsi_pointer || 'N/A';
|
|
||||||
|
|
||||||
// Format filters (show actual filter details)
|
|
||||||
let filtersDisplay = 'None';
|
|
||||||
if (subscription.filters && subscription.filters.length > 0) {
|
|
||||||
const filterDetails = [];
|
|
||||||
subscription.filters.forEach((filter, index) => {
|
|
||||||
const parts = [];
|
|
||||||
|
|
||||||
// Add kinds if present
|
|
||||||
if (filter.kinds && Array.isArray(filter.kinds) && filter.kinds.length > 0) {
|
|
||||||
parts.push(`kinds:[${filter.kinds.join(',')}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add authors if present (truncate for display)
|
|
||||||
if (filter.authors && Array.isArray(filter.authors) && filter.authors.length > 0) {
|
|
||||||
const authorCount = filter.authors.length;
|
|
||||||
if (authorCount === 1) {
|
|
||||||
const shortPubkey = filter.authors[0].substring(0, 8) + '...';
|
|
||||||
parts.push(`authors:[${shortPubkey}]`);
|
|
||||||
} else {
|
|
||||||
parts.push(`authors:[${authorCount} pubkeys]`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add ids if present
|
|
||||||
if (filter.ids && Array.isArray(filter.ids) && filter.ids.length > 0) {
|
|
||||||
const idCount = filter.ids.length;
|
|
||||||
parts.push(`ids:[${idCount} event${idCount > 1 ? 's' : ''}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add time range if present
|
|
||||||
const timeParts = [];
|
|
||||||
if (filter.since && filter.since > 0) {
|
|
||||||
const sinceDate = new Date(filter.since * 1000).toLocaleString();
|
|
||||||
timeParts.push(`since:${sinceDate}`);
|
|
||||||
}
|
|
||||||
if (filter.until && filter.until > 0) {
|
|
||||||
const untilDate = new Date(filter.until * 1000).toLocaleString();
|
|
||||||
timeParts.push(`until:${untilDate}`);
|
|
||||||
}
|
|
||||||
if (timeParts.length > 0) {
|
|
||||||
parts.push(timeParts.join(', '));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add limit if present
|
|
||||||
if (filter.limit && filter.limit > 0) {
|
|
||||||
parts.push(`limit:${filter.limit}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add tag filters if present
|
|
||||||
if (filter.tag_filters && Array.isArray(filter.tag_filters) && filter.tag_filters.length > 0) {
|
|
||||||
parts.push(`tags:[${filter.tag_filters.length} filter${filter.tag_filters.length > 1 ? 's' : ''}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parts.length > 0) {
|
|
||||||
filterDetails.push(parts.join(', '));
|
|
||||||
} else {
|
|
||||||
filterDetails.push('empty filter');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
filtersDisplay = filterDetails.join(' | ');
|
|
||||||
}
|
|
||||||
|
|
||||||
row.innerHTML = `
|
|
||||||
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${subscription.id || 'N/A'}</td>
|
|
||||||
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${clientIP}</td>
|
|
||||||
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${wsiPointer}</td>
|
|
||||||
<td>${durationStr}</td>
|
|
||||||
<td>${filtersDisplay}</td>
|
|
||||||
`;
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Group subscriptions by wsi_pointer
|
||||||
|
const groupedSubscriptions = {};
|
||||||
|
subscriptionsData.forEach(sub => {
|
||||||
|
const wsiKey = sub.wsi_pointer || 'N/A';
|
||||||
|
if (!groupedSubscriptions[wsiKey]) {
|
||||||
|
groupedSubscriptions[wsiKey] = [];
|
||||||
|
}
|
||||||
|
groupedSubscriptions[wsiKey].push(sub);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create rows for each group
|
||||||
|
Object.entries(groupedSubscriptions).forEach(([wsiPointer, subscriptions]) => {
|
||||||
|
// Calculate group summary
|
||||||
|
const subCount = subscriptions.length;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const oldestDuration = Math.max(...subscriptions.map(s => now - s.created_at));
|
||||||
|
const oldestDurationStr = formatDuration(oldestDuration);
|
||||||
|
|
||||||
|
// Create header row (summary)
|
||||||
|
const headerRow = document.createElement('tr');
|
||||||
|
headerRow.className = 'subscription-group-header';
|
||||||
|
headerRow.setAttribute('data-wsi-pointer', wsiPointer);
|
||||||
|
const wasExpanded = expandedGroups.has(wsiPointer);
|
||||||
|
headerRow.setAttribute('data-expanded', wasExpanded ? 'true' : 'false');
|
||||||
|
|
||||||
|
headerRow.innerHTML = `
|
||||||
|
<td colspan="4" style="padding: 8px;">
|
||||||
|
<span class="expand-icon" style="display: inline-block; width: 20px; transition: transform 0.2s;">▶</span>
|
||||||
|
<strong style="font-family: 'Courier New', monospace; font-size: 12px;">Websocket: ${wsiPointer}</strong>
|
||||||
|
<span style="color: #666; margin-left: 15px;">
|
||||||
|
Subscriptions: ${subCount} | Oldest: ${oldestDurationStr}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add click handler to toggle expansion
|
||||||
|
headerRow.addEventListener('click', () => toggleSubscriptionGroup(wsiPointer));
|
||||||
|
|
||||||
|
tableBody.appendChild(headerRow);
|
||||||
|
|
||||||
|
// Create detail rows (initially hidden)
|
||||||
|
subscriptions.forEach((subscription, index) => {
|
||||||
|
const detailRow = document.createElement('tr');
|
||||||
|
detailRow.className = 'subscription-detail-row';
|
||||||
|
detailRow.setAttribute('data-wsi-group', wsiPointer);
|
||||||
|
detailRow.style.display = 'none';
|
||||||
|
|
||||||
|
// Calculate duration
|
||||||
|
const duration = now - subscription.created_at;
|
||||||
|
const durationStr = formatDuration(duration);
|
||||||
|
|
||||||
|
// Format filters
|
||||||
|
let filtersDisplay = 'None';
|
||||||
|
if (subscription.filters && subscription.filters.length > 0) {
|
||||||
|
const filterDetails = [];
|
||||||
|
subscription.filters.forEach((filter) => {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (filter.kinds && Array.isArray(filter.kinds) && filter.kinds.length > 0) {
|
||||||
|
parts.push(`kinds:[${filter.kinds.join(',')}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.authors && Array.isArray(filter.authors) && filter.authors.length > 0) {
|
||||||
|
const authorCount = filter.authors.length;
|
||||||
|
if (authorCount === 1) {
|
||||||
|
const shortPubkey = filter.authors[0].substring(0, 8) + '...';
|
||||||
|
parts.push(`authors:[${shortPubkey}]`);
|
||||||
|
} else {
|
||||||
|
parts.push(`authors:[${authorCount} pubkeys]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.ids && Array.isArray(filter.ids) && filter.ids.length > 0) {
|
||||||
|
const idCount = filter.ids.length;
|
||||||
|
parts.push(`ids:[${idCount} event${idCount > 1 ? 's' : ''}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeParts = [];
|
||||||
|
if (filter.since && filter.since > 0) {
|
||||||
|
const sinceDate = new Date(filter.since * 1000).toLocaleString();
|
||||||
|
timeParts.push(`since:${sinceDate}`);
|
||||||
|
}
|
||||||
|
if (filter.until && filter.until > 0) {
|
||||||
|
const untilDate = new Date(filter.until * 1000).toLocaleString();
|
||||||
|
timeParts.push(`until:${untilDate}`);
|
||||||
|
}
|
||||||
|
if (timeParts.length > 0) {
|
||||||
|
parts.push(timeParts.join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.limit && filter.limit > 0) {
|
||||||
|
parts.push(`limit:${filter.limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.tag_filters && Array.isArray(filter.tag_filters) && filter.tag_filters.length > 0) {
|
||||||
|
parts.push(`tags:[${filter.tag_filters.length} filter${filter.tag_filters.length > 1 ? 's' : ''}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length > 0) {
|
||||||
|
filterDetails.push(parts.join(', '));
|
||||||
|
} else {
|
||||||
|
filterDetails.push('empty filter');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filtersDisplay = filterDetails.join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
detailRow.innerHTML = `
|
||||||
|
<td class="subscription-detail-prefix">└─</td>
|
||||||
|
<td class="subscription-detail-id">${subscription.id || 'N/A'}</td>
|
||||||
|
<td class="subscription-detail-duration">${durationStr}</td>
|
||||||
|
<td class="subscription-detail-filters">${filtersDisplay}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(detailRow);
|
||||||
|
|
||||||
|
// Restore expand/collapse state after adding all rows
|
||||||
|
if (wasExpanded) {
|
||||||
|
const detailRows = tableBody.querySelectorAll(`.subscription-detail-row[data-wsi-group="${wsiPointer}"]`);
|
||||||
|
detailRows.forEach(row => row.style.display = 'table-row');
|
||||||
|
const expandIcon = headerRow.querySelector('.expand-icon');
|
||||||
|
if (expandIcon) {
|
||||||
|
expandIcon.textContent = '▼';
|
||||||
|
expandIcon.style.transform = 'rotate(90deg)';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle function for expanding/collapsing groups
|
||||||
|
function toggleSubscriptionGroup(wsiPointer) {
|
||||||
|
const headerRow = document.querySelector(`.subscription-group-header[data-wsi-pointer="${wsiPointer}"]`);
|
||||||
|
const detailRows = document.querySelectorAll(`.subscription-detail-row[data-wsi-group="${wsiPointer}"]`);
|
||||||
|
const expandIcon = headerRow.querySelector('.expand-icon');
|
||||||
|
|
||||||
|
const isExpanded = headerRow.getAttribute('data-expanded') === 'true';
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
// Collapse
|
||||||
|
detailRows.forEach(row => row.style.display = 'none');
|
||||||
|
expandIcon.textContent = '▶';
|
||||||
|
expandIcon.style.transform = 'rotate(0deg)';
|
||||||
|
headerRow.setAttribute('data-expanded', 'false');
|
||||||
|
} else {
|
||||||
|
// Expand
|
||||||
|
detailRows.forEach(row => row.style.display = 'table-row');
|
||||||
|
expandIcon.textContent = '▼';
|
||||||
|
expandIcon.style.transform = 'rotate(90deg)';
|
||||||
|
headerRow.setAttribute('data-expanded', 'true');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to format duration in human-readable format
|
// Helper function to format duration in human-readable format
|
||||||
@@ -4779,7 +4864,7 @@ const SQL_QUERY_TEMPLATES = {
|
|||||||
subscriptions: "SELECT * FROM active_subscriptions_log ORDER BY created_at DESC",
|
subscriptions: "SELECT * FROM active_subscriptions_log ORDER BY created_at DESC",
|
||||||
top_pubkeys: "SELECT * FROM top_pubkeys_view",
|
top_pubkeys: "SELECT * FROM top_pubkeys_view",
|
||||||
event_kinds: "SELECT * FROM event_kinds_view ORDER BY count DESC",
|
event_kinds: "SELECT * FROM event_kinds_view ORDER BY count DESC",
|
||||||
time_stats: "SELECT * FROM time_stats_view"
|
time_stats: "SELECT 'total' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events UNION ALL SELECT '24h' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events WHERE created_at >= (strftime('%s', 'now') - 86400) UNION ALL SELECT '7d' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events WHERE created_at >= (strftime('%s', 'now') - 604800) UNION ALL SELECT '30d' as period, COUNT(*) as total_events, COUNT(DISTINCT pubkey) as unique_pubkeys, MIN(created_at) as oldest_event, MAX(created_at) as newest_event FROM events WHERE created_at >= (strftime('%s', 'now') - 2592000)"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Query history management (localStorage)
|
// Query history management (localStorage)
|
||||||
|
|||||||
24
debug.log
@@ -1,24 +0,0 @@
|
|||||||
|
|
||||||
=== NOSTR WebSocket Debug Log Started ===
|
|
||||||
[14:16:28.243] SEND localhost:8888: ["EVENT", {
|
|
||||||
"pubkey": "193279d1459ba1399aadb954422bf8595aa77367dccf482c682f5f208e435844",
|
|
||||||
"created_at": 1761499411,
|
|
||||||
"kind": 1059,
|
|
||||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
|
||||||
"content": "AmWNi4P5J126kk69XH2o5mYvGj+69+Fjfr/nZx892I2z8edkwtp2IH7XAnPUqdGPu7x1xiZF1sNfr21VKThOhE54K/uQHLFydZN3acgUfX13sCeWhrvnQD0EvjvZC6QzW9DfFayYoYl+rEPYcra1/N68a+N1R7XnNcf1K/ZFh5Grcnln0H5YdXKRBhQI9aai4iFp1VGy2V0IR+6gDJGbJ7TbAbD3wgGWv1i77C03skH3RgzH+f2b7VBtm+vjKX6q7v6v8j3w1lRFE5Qh0Tqgedh3+UsnwqQta7OCzF9OyAVPK7EqKQBss5LzYSRUpcCE1vw5b7I7yeBFwU9WfnGLUW+uZxMJ2C3P4NBBrVO8UFIkBrPL2cqkoD5c8DgMLJjXGmc4EWfB4ZWb3KjbfLbgi6DVQ++cDjBbnCOPhX+/4qOnWq+gI28e/xk3cBvQtgUOkvWX3oGl3/Q33u4UGtxkFEXGfzdKHVDkR86kqf7RMZjIwTjLGpx4uov0cNmzj07hYEdoG/lJ4yA1v/GyF7viJdnnz3tE0hCZaViqSCev0rfUHWRDDMXJzJ9SS+OwpVswSG4NKvYsDhDM89BjhFs08HshTFdIh2AY45jR/16CsZM9JudH5BwqcX23wToYdZ+lrerOA0EkYb0DJUzGVe4lMpdJZoB8qXLHxMAKwKu0UEWEkeBnnZbvTGwCRbfGorxwPrnyqUCy9tzJx0GOLhRIzBmt6lki607VLDYjK97VIz0dff3fyWPAfy/yBlO2nHhVubUgpPaAjcaYNkO/iZwuP8oJkClWWmKwAQoNoxt+Ly2llrkz+Ne8oXMQdJSq416x6MLHo2JbKH8uwjx0yKG0oldLyWaz3A8OHYkJuxOi7HPVTlOOJrsjG4kMn2g97rVUXLs5v9F/StOjzxtiQWmCBtCsvK2LEEK/DzfavcJstEMxQztJjhiYRO3MJanL7lN2zu1ZHO149FJrgqGV6RQ8DDXf55yuabqHilBuUSDKpI0gl0+Efuor1my+L9J7MjJQ83aSwGizX7uXedMsGQRcvU3++Uvbw7sd2l67fb7IoYU04TPGZkIm120qwf7GAUpnDL7Lhulu/9LFMFs3UnGl9cLzY6EAJtDANHjMAoXbGbYclnoSiNW4yr3X9PBHO5o2YhIxfpTyEgLebJLOkzoziuCTpX8/MdhOhFtlIyo5B8Mbt5GDOHh4x1ZMKOl02J00Vvgui0hLw4Vri8Lz/ErPIRSlrEOB+8K5zPzJy/bD8XrOKlOwSbF5j9dsqs+8uCTC/v9YNQ0cC9wP7gVAxErQ3suJVeV7pzY+eGR051AcW7ppTs1gShhxDDaSaKdMlrkBdFDZcCJ+tomSgW56bOi45erpmk8Lcv4RrBzjBtq1hz+XSaTBAtEnGtHNH2uOn7KP/NNaD38dYkpb3N1VR3zuV67RcuPZeB+5WR9jhnLoSMGox2s=",
|
|
||||||
"id": "c6c18d902744fc0aaa4ca9172b3bcd0dde3fd7d943b41b2a39a16927ede67804",
|
|
||||||
"sig": "d67e0e914aa361c528510efd216548b6734a5fa68c46426571fbc87626bf19a9ec46e16883e7fad700f4fee5cfffd9bba03c3c08e57938fbca77a28b30a32bb7"
|
|
||||||
}]
|
|
||||||
[14:16:28.256] RECV localhost:8888: ["OK", "c6c18d902744fc0aaa4ca9172b3bcd0dde3fd7d943b41b2a39a16927ede67804", true, ""]
|
|
||||||
|
|
||||||
=== NOSTR WebSocket Debug Log Started ===
|
|
||||||
[15:01:18.592] SEND localhost:8888: ["EVENT", {
|
|
||||||
"pubkey": "ec9578ade9e74358ed35d8091d41bfa277e86d649614a8865e3725e38ebe5bc9",
|
|
||||||
"created_at": 1761502101,
|
|
||||||
"kind": 1059,
|
|
||||||
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
|
|
||||||
"content": "AlgLnVwti8Dk2nu0e4bMrXeZiR/u+RPnA85kpts2svaFGfMByS4iap7xqdiSrXpSQPjQsix6jP9Qiy1a6rrvC6MutqTi3JfsMexLR61/ZKTK41sWTXNDTT3keH543vx3fVQH1mq+LgG4mjNzkPe0RqkYFvC8R0nxyAcCecHDxZUlmXQmAGiB5JB2GvstA4eoZLP1OI3fcLA3qaITLNRJOwRUoTYKqUkENHwz74CW0TnYDrKRZVe9zKNWQBLmtsgVoGd5CXNAVgXwmm2h0eCNIcRGnFqDHzpegpEGO+A7tvB0KJwlj4j/GmRgmnWO4pkrM2fmsTdlb5KNqe7NPuTVgYfdvld70zWpenp7jF/0psaEQEl8R7FbG2rNCv8fXtH+womvJQj4S0eUBxfvsUU1wWYmhusEzvyTfpV/nw0Er+pmAUZ2eGk7LEB2GMsJrkT+G5oohm0n+5c72iWJqW9A1eAzjR6Z21FkH4kAEJOl70fw9Xeig+s9rYk3GcKlMvj42zf7DepMXHPy62TbqUeclcm5W/semyasGP521GBuw152IN+dS67OVVmEvEJ89xhwiTeIty78enR4Gq7d1eNK+rqStdtJ7FN6kD/8gv4sFojUXyi0sIxzaSPrwI3ohOqbpEK1dTs6fmTUiyT/Buq++IhD9UwsZgz/kYpZfm1NVnWx+yTEv4I1H80FDxmMzbYnTHuIdRJFeh/NJRy9h+gXoZlZnteHkbwm1w2AejTkVnGs2Pz7aUZgC+1Za8fhtXq3Z9N3R8f8gtVFnnjRzApg5U89QrXUBS0R6F9dTqINk4qti94JWO4dYcPuudCutME5hfCYBoo+LHuRmdPKry7vSK1WgLYQsHuG+r313Ak8DhZYNbL+0d5UJ9kDFlFKaP3xLahSbEc/7u+AyuN68IyM1NwEehllxqVUsX8dsD4bZ2yW5rVjAQm9tT8Ypm73kJEb+DYVqT0WjFx88ee+HX89a9NgszWf1HE1KNQ9gWjn4eH6xbwrOkS4/v2O2tQoAd00vyPKAWly3Zlrz2cRnrSnxTZ5Lt7HtwAt6Err8MhD/w5rMLXHTBCMrroG1VfMo1OgL1YPafKDZmwVcHWacqtZiB0heRx742WipmTonqMjCOTNufdwxQcRPLLio0mtqiIrzgJqqIQenBXSa1jaG6Lvb5PCUKThbg4sSFfgssoUNKM7ytmBAe+PPmOVe11/gGaFWoQeUordbvmiCtzIPUYiKsuhfeK4I3jKvEofQU37hOam8ZxUczXvX6dgOOto002EWyCVfAzFgyey6wI+FGEbhXqlw7nB+azpqLQMJnHg7pfb1stXk3d8rjgVrRsVRJe/5KrXyZ5cd7ftJuJLxpTYmfFu6CKoUE0L5eRxXiwa16Pi0BehxOLaZteiTzttyfj+ClMKs2J/2/T1BVya1oGUW2Wg6ri/qS8oXv8bqiXBZ1/BwfI=",
|
|
||||||
"id": "ea5bb419a8efea8ee86bb8696406a70a0387a7d0ac6e60760026d1aea28b427f",
|
|
||||||
"sig": "0ffde3fd0d83c80693aa656668f2553807f8d474738ff3d9676090a5b8748a8e8e0c75a1d64963e4604046e18a806c4371a9cf2af2fd72f9db50f15bc78a4e25"
|
|
||||||
}]
|
|
||||||
[15:01:18.604] RECV localhost:8888: ["OK", "ea5bb419a8efea8ee86bb8696406a70a0387a7d0ac6e60760026d1aea28b427f", true, ""]
|
|
||||||
@@ -3,8 +3,8 @@
|
|||||||
# Copy the binary to the deployment location
|
# Copy the binary to the deployment location
|
||||||
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
cp build/c_relay_x86 ~/Storage/c_relay/crelay
|
||||||
|
|
||||||
# Copy the local service file to systemd
|
# Copy the service file to systemd (use the main service file)
|
||||||
sudo cp systemd/c-relay-local.service /etc/systemd/system/
|
sudo cp systemd/c-relay.service /etc/systemd/system/c-relay-local.service
|
||||||
|
|
||||||
# Reload systemd daemon to pick up the new service
|
# Reload systemd daemon to pick up the new service
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
|
|||||||
@@ -175,6 +175,18 @@ Configuration events follow the standard Nostr event format with kind 33334:
|
|||||||
- **Impact**: Allows some flexibility in expiration timing
|
- **Impact**: Allows some flexibility in expiration timing
|
||||||
- **Example**: `"600"` (10 minute grace period)
|
- **Example**: `"600"` (10 minute grace period)
|
||||||
|
|
||||||
|
### NIP-59 Gift Wrap Timestamp Configuration
|
||||||
|
|
||||||
|
#### `nip59_timestamp_max_delay_sec`
|
||||||
|
- **Description**: Controls timestamp randomization for NIP-59 gift wraps
|
||||||
|
- **Default**: `"0"` (no randomization)
|
||||||
|
- **Range**: `0` to `604800` (7 days)
|
||||||
|
- **Impact**: Affects compatibility with other Nostr clients for direct messaging
|
||||||
|
- **Values**:
|
||||||
|
- `"0"`: No randomization (maximum compatibility)
|
||||||
|
- `"1-604800"`: Random timestamp between now and N seconds ago
|
||||||
|
- **Example**: `"172800"` (2 days randomization for privacy)
|
||||||
|
|
||||||
## Configuration Examples
|
## Configuration Examples
|
||||||
|
|
||||||
### Basic Relay Setup
|
### Basic Relay Setup
|
||||||
|
|||||||
517
docs/nip59_timestamp_configuration_plan.md
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
# NIP-59 Timestamp Configuration Implementation Plan
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
Add configurable timestamp randomization for NIP-59 gift wraps to improve compatibility with Nostr apps that don't implement timestamp randomization.
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
The NIP-59 protocol specifies that timestamps on gift wraps should have randomness to prevent time-analysis attacks. However, some Nostr platforms don't implement this, causing compatibility issues with direct messaging (NIP-17).
|
||||||
|
|
||||||
|
## Solution
|
||||||
|
Add a configuration parameter `nip59_timestamp_max_delay_sec` that controls the maximum random delay applied to timestamps:
|
||||||
|
- **Value = 0**: Use current timestamp (no randomization) for maximum compatibility
|
||||||
|
- **Value > 0**: Use random timestamp between now and N seconds ago
|
||||||
|
- **Default = 0**: Maximum compatibility mode (no randomization)
|
||||||
|
|
||||||
|
## Implementation Approach: Option B (Direct Parameter Addition)
|
||||||
|
We chose Option B because:
|
||||||
|
1. Explicit and stateless - value flows through call chain
|
||||||
|
2. Thread-safe by design
|
||||||
|
3. No global state needed in nostr_core_lib
|
||||||
|
4. DMs are sent rarely, so database query per call is acceptable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Detailed Implementation Steps
|
||||||
|
|
||||||
|
### Phase 1: Configuration Setup in c-relay
|
||||||
|
|
||||||
|
#### 1.1 Add Configuration Parameter
|
||||||
|
**File:** `src/default_config_event.h`
|
||||||
|
**Location:** Line 82 (after `trust_proxy_headers`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// NIP-59 Gift Wrap Timestamp Configuration
|
||||||
|
{"nip59_timestamp_max_delay_sec", "0"} // Default: 0 (no randomization for compatibility)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- Default of 0 seconds (no randomization) for maximum compatibility
|
||||||
|
- Placed after proxy settings, before closing brace
|
||||||
|
- Follows existing naming convention
|
||||||
|
|
||||||
|
#### 1.2 Add Configuration Validation
|
||||||
|
**File:** `src/config.c`
|
||||||
|
**Function:** `validate_config_field()` (around line 923)
|
||||||
|
|
||||||
|
Add validation case:
|
||||||
|
```c
|
||||||
|
else if (strcmp(key, "nip59_timestamp_max_delay_sec") == 0) {
|
||||||
|
long value = strtol(value_str, NULL, 10);
|
||||||
|
if (value < 0 || value > 604800) { // Max 7 days
|
||||||
|
snprintf(error_msg, error_size,
|
||||||
|
"nip59_timestamp_max_delay_sec must be between 0 and 604800 (7 days)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rationale:**
|
||||||
|
- 0 = no randomization (compatibility mode)
|
||||||
|
- 604800 = 7 days maximum (reasonable upper bound)
|
||||||
|
- Prevents negative values or excessive delays
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 2: Modify nostr_core_lib Functions
|
||||||
|
|
||||||
|
#### 2.1 Update random_past_timestamp() Function
|
||||||
|
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
||||||
|
**Current Location:** Lines 31-36
|
||||||
|
|
||||||
|
**Current Code:**
|
||||||
|
```c
|
||||||
|
static time_t random_past_timestamp(void) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
// Random time up to 2 days (172800 seconds) in the past
|
||||||
|
long random_offset = (long)(rand() % 172800);
|
||||||
|
return now - random_offset;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```c
|
||||||
|
static time_t random_past_timestamp(long max_delay_sec) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
|
||||||
|
// If max_delay_sec is 0, return current timestamp (no randomization)
|
||||||
|
if (max_delay_sec == 0) {
|
||||||
|
return now;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Random time up to max_delay_sec in the past
|
||||||
|
long random_offset = (long)(rand() % max_delay_sec);
|
||||||
|
return now - random_offset;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Changes:**
|
||||||
|
- Add `long max_delay_sec` parameter
|
||||||
|
- Handle special case: `max_delay_sec == 0` returns current time
|
||||||
|
- Use `max_delay_sec` instead of hardcoded 172800
|
||||||
|
|
||||||
|
#### 2.2 Update nostr_nip59_create_seal() Function
|
||||||
|
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
||||||
|
**Current Location:** Lines 144-215
|
||||||
|
|
||||||
|
**Function Signature Change:**
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
||||||
|
const unsigned char* sender_private_key,
|
||||||
|
const unsigned char* recipient_public_key);
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
||||||
|
const unsigned char* sender_private_key,
|
||||||
|
const unsigned char* recipient_public_key,
|
||||||
|
long max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code Change at Line 181:**
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
time_t seal_time = random_past_timestamp();
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
time_t seal_time = random_past_timestamp(max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.3 Update nostr_nip59_create_gift_wrap() Function
|
||||||
|
**File:** `nostr_core_lib/nostr_core/nip059.c`
|
||||||
|
**Current Location:** Lines 220-323
|
||||||
|
|
||||||
|
**Function Signature Change:**
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
||||||
|
const char* recipient_public_key_hex);
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
||||||
|
const char* recipient_public_key_hex,
|
||||||
|
long max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code Change at Line 275:**
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
time_t wrap_time = random_past_timestamp();
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
time_t wrap_time = random_past_timestamp(max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4 Update nip059.h Header
|
||||||
|
**File:** `nostr_core_lib/nostr_core/nip059.h`
|
||||||
|
**Locations:** Lines 38-39 and 48
|
||||||
|
|
||||||
|
**Update Function Declarations:**
|
||||||
|
```c
|
||||||
|
// Line 38-39: Update nostr_nip59_create_seal
|
||||||
|
cJSON* nostr_nip59_create_seal(cJSON* rumor,
|
||||||
|
const unsigned char* sender_private_key,
|
||||||
|
const unsigned char* recipient_public_key,
|
||||||
|
long max_delay_sec);
|
||||||
|
|
||||||
|
// Line 48: Update nostr_nip59_create_gift_wrap
|
||||||
|
cJSON* nostr_nip59_create_gift_wrap(cJSON* seal,
|
||||||
|
const char* recipient_public_key_hex,
|
||||||
|
long max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update Documentation Comments:**
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* NIP-59: Create a seal (kind 13) wrapping a rumor
|
||||||
|
*
|
||||||
|
* @param rumor The rumor event to seal (cJSON object)
|
||||||
|
* @param sender_private_key 32-byte sender private key
|
||||||
|
* @param recipient_public_key 32-byte recipient public key (x-only)
|
||||||
|
* @param max_delay_sec Maximum random delay in seconds (0 = no randomization)
|
||||||
|
* @return cJSON object representing the seal event, or NULL on error
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NIP-59: Create a gift wrap (kind 1059) wrapping a seal
|
||||||
|
*
|
||||||
|
* @param seal The seal event to wrap (cJSON object)
|
||||||
|
* @param recipient_public_key_hex Recipient's public key in hex format
|
||||||
|
* @param max_delay_sec Maximum random delay in seconds (0 = no randomization)
|
||||||
|
* @return cJSON object representing the gift wrap event, or NULL on error
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 3: Update NIP-17 Integration
|
||||||
|
|
||||||
|
#### 3.1 Update nostr_nip17_send_dm() Function
|
||||||
|
**File:** `nostr_core_lib/nostr_core/nip017.c`
|
||||||
|
**Current Location:** Lines 260-320
|
||||||
|
|
||||||
|
**Function Signature Change:**
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
int nostr_nip17_send_dm(cJSON* dm_event,
|
||||||
|
const char** recipient_pubkeys,
|
||||||
|
int num_recipients,
|
||||||
|
const unsigned char* sender_private_key,
|
||||||
|
cJSON** gift_wraps_out,
|
||||||
|
int max_gift_wraps);
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
int nostr_nip17_send_dm(cJSON* dm_event,
|
||||||
|
const char** recipient_pubkeys,
|
||||||
|
int num_recipients,
|
||||||
|
const unsigned char* sender_private_key,
|
||||||
|
cJSON** gift_wraps_out,
|
||||||
|
int max_gift_wraps,
|
||||||
|
long max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Code Changes:**
|
||||||
|
|
||||||
|
At line 281 (seal creation):
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key);
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
cJSON* seal = nostr_nip59_create_seal(dm_event, sender_private_key, recipient_public_key, max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
At line 287 (gift wrap creation):
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i]);
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
cJSON* gift_wrap = nostr_nip59_create_gift_wrap(seal, recipient_pubkeys[i], max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
At line 306 (sender seal creation):
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key);
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
cJSON* sender_seal = nostr_nip59_create_seal(dm_event, sender_private_key, sender_public_key, max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
At line 309 (sender gift wrap creation):
|
||||||
|
```c
|
||||||
|
// OLD:
|
||||||
|
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex);
|
||||||
|
|
||||||
|
// NEW:
|
||||||
|
cJSON* sender_gift_wrap = nostr_nip59_create_gift_wrap(sender_seal, sender_pubkey_hex, max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 Update nip017.h Header
|
||||||
|
**File:** `nostr_core_lib/nostr_core/nip017.h`
|
||||||
|
**Location:** Lines 102-107
|
||||||
|
|
||||||
|
**Update Function Declaration:**
|
||||||
|
```c
|
||||||
|
int nostr_nip17_send_dm(cJSON* dm_event,
|
||||||
|
const char** recipient_pubkeys,
|
||||||
|
int num_recipients,
|
||||||
|
const unsigned char* sender_private_key,
|
||||||
|
cJSON** gift_wraps_out,
|
||||||
|
int max_gift_wraps,
|
||||||
|
long max_delay_sec);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update Documentation Comment (lines 88-100):**
|
||||||
|
```c
|
||||||
|
/**
|
||||||
|
* NIP-17: Send a direct message to recipients
|
||||||
|
*
|
||||||
|
* This function creates the appropriate rumor, seals it, gift wraps it,
|
||||||
|
* and returns the final gift wrap events ready for publishing.
|
||||||
|
*
|
||||||
|
* @param dm_event The unsigned DM event (kind 14 or 15)
|
||||||
|
* @param recipient_pubkeys Array of recipient public keys (hex strings)
|
||||||
|
* @param num_recipients Number of recipients
|
||||||
|
* @param sender_private_key 32-byte sender private key
|
||||||
|
* @param gift_wraps_out Array to store resulting gift wrap events (caller must free)
|
||||||
|
* @param max_gift_wraps Maximum number of gift wraps to create
|
||||||
|
* @param max_delay_sec Maximum random timestamp delay in seconds (0 = no randomization)
|
||||||
|
* @return Number of gift wrap events created, or -1 on error
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 4: Update c-relay Call Sites
|
||||||
|
|
||||||
|
#### 4.1 Update src/api.c
|
||||||
|
**Location:** Line 1319
|
||||||
|
|
||||||
|
**Current Code:**
|
||||||
|
```c
|
||||||
|
int send_result = nostr_nip17_send_dm(
|
||||||
|
dm_response, // dm_event
|
||||||
|
recipient_pubkeys, // recipient_pubkeys
|
||||||
|
1, // num_recipients
|
||||||
|
relay_privkey, // sender_private_key
|
||||||
|
gift_wraps, // gift_wraps_out
|
||||||
|
1 // max_gift_wraps
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```c
|
||||||
|
// Get timestamp delay configuration
|
||||||
|
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
||||||
|
|
||||||
|
int send_result = nostr_nip17_send_dm(
|
||||||
|
dm_response, // dm_event
|
||||||
|
recipient_pubkeys, // recipient_pubkeys
|
||||||
|
1, // num_recipients
|
||||||
|
relay_privkey, // sender_private_key
|
||||||
|
gift_wraps, // gift_wraps_out
|
||||||
|
1, // max_gift_wraps
|
||||||
|
max_delay_sec // max_delay_sec
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 Update src/dm_admin.c
|
||||||
|
**Location:** Line 371
|
||||||
|
|
||||||
|
**Current Code:**
|
||||||
|
```c
|
||||||
|
int send_result = nostr_nip17_send_dm(
|
||||||
|
success_dm, // dm_event
|
||||||
|
sender_pubkey_array, // recipient_pubkeys
|
||||||
|
1, // num_recipients
|
||||||
|
relay_privkey, // sender_private_key
|
||||||
|
success_gift_wraps, // gift_wraps_out
|
||||||
|
1 // max_gift_wraps
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**New Code:**
|
||||||
|
```c
|
||||||
|
// Get timestamp delay configuration
|
||||||
|
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
||||||
|
|
||||||
|
int send_result = nostr_nip17_send_dm(
|
||||||
|
success_dm, // dm_event
|
||||||
|
sender_pubkey_array, // recipient_pubkeys
|
||||||
|
1, // num_recipients
|
||||||
|
relay_privkey, // sender_private_key
|
||||||
|
success_gift_wraps, // gift_wraps_out
|
||||||
|
1, // max_gift_wraps
|
||||||
|
max_delay_sec // max_delay_sec
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Both files already include `config.h`, so `get_config_int()` is available.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing Plan
|
||||||
|
|
||||||
|
### Test Case 1: No Randomization (Compatibility Mode)
|
||||||
|
**Configuration:** `nip59_timestamp_max_delay_sec = 0`
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
- Gift wrap timestamps should equal current time
|
||||||
|
- Seal timestamps should equal current time
|
||||||
|
- No random delay applied
|
||||||
|
|
||||||
|
**Test Command:**
|
||||||
|
```bash
|
||||||
|
# Set config via admin API
|
||||||
|
# Send test DM
|
||||||
|
# Verify timestamps are current (within 1 second of send time)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Case 2: Custom Delay
|
||||||
|
**Configuration:** `nip59_timestamp_max_delay_sec = 1000`
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
- Gift wrap timestamps should be between now and 1000 seconds ago
|
||||||
|
- Seal timestamps should be between now and 1000 seconds ago
|
||||||
|
- Random delay applied within specified range
|
||||||
|
|
||||||
|
**Test Command:**
|
||||||
|
```bash
|
||||||
|
# Set config via admin API
|
||||||
|
# Send test DM
|
||||||
|
# Verify timestamps are in past but within 1000 seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Case 3: Default Behavior
|
||||||
|
**Configuration:** `nip59_timestamp_max_delay_sec = 0` (default)
|
||||||
|
|
||||||
|
**Expected Behavior:**
|
||||||
|
- Gift wrap timestamps should equal current time
|
||||||
|
- Seal timestamps should equal current time
|
||||||
|
- No randomization (maximum compatibility)
|
||||||
|
|
||||||
|
**Test Command:**
|
||||||
|
```bash
|
||||||
|
# Use default config
|
||||||
|
# Send test DM
|
||||||
|
# Verify timestamps are current (within 1 second of send time)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test Case 4: Configuration Validation
|
||||||
|
**Test Invalid Values:**
|
||||||
|
- Negative value: Should be rejected
|
||||||
|
- Value > 604800: Should be rejected
|
||||||
|
- Valid boundary values (0, 604800): Should be accepted
|
||||||
|
|
||||||
|
### Test Case 5: Interoperability
|
||||||
|
**Test with Other Nostr Clients:**
|
||||||
|
- Send DM with `max_delay_sec = 0` to clients that don't randomize
|
||||||
|
- Send DM with `max_delay_sec = 172800` to clients that do randomize
|
||||||
|
- Verify both scenarios work correctly
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Updates
|
||||||
|
|
||||||
|
### Update docs/configuration_guide.md
|
||||||
|
|
||||||
|
Add new section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### NIP-59 Gift Wrap Timestamp Configuration
|
||||||
|
|
||||||
|
#### nip59_timestamp_max_delay_sec
|
||||||
|
- **Type:** Integer
|
||||||
|
- **Default:** 0 (no randomization)
|
||||||
|
- **Range:** 0 to 604800 (7 days)
|
||||||
|
- **Description:** Controls timestamp randomization for NIP-59 gift wraps
|
||||||
|
|
||||||
|
The NIP-59 protocol recommends randomizing timestamps on gift wraps to prevent
|
||||||
|
time-analysis attacks. However, some Nostr platforms don't implement this,
|
||||||
|
causing compatibility issues.
|
||||||
|
|
||||||
|
**Values:**
|
||||||
|
- `0` (default): No randomization - uses current timestamp (maximum compatibility)
|
||||||
|
- `1-604800`: Random timestamp between now and N seconds ago
|
||||||
|
|
||||||
|
**Use Cases:**
|
||||||
|
- Keep default `0` for maximum compatibility with clients that don't randomize
|
||||||
|
- Set to `172800` for privacy per NIP-59 specification (2 days randomization)
|
||||||
|
- Set to custom value (e.g., `3600`) for 1-hour randomization window
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```json
|
||||||
|
["nip59_timestamp_max_delay_sec", "0"] // Default: compatibility mode
|
||||||
|
["nip59_timestamp_max_delay_sec", "3600"] // 1 hour randomization
|
||||||
|
["nip59_timestamp_max_delay_sec", "172800"] // 2 days randomization
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Checklist
|
||||||
|
|
||||||
|
### nostr_core_lib Changes
|
||||||
|
- [ ] Modify `random_past_timestamp()` to accept `max_delay_sec` parameter
|
||||||
|
- [ ] Update `nostr_nip59_create_seal()` signature and implementation
|
||||||
|
- [ ] Update `nostr_nip59_create_gift_wrap()` signature and implementation
|
||||||
|
- [ ] Update `nip059.h` function declarations and documentation
|
||||||
|
- [ ] Update `nostr_nip17_send_dm()` signature and implementation
|
||||||
|
- [ ] Update `nip017.h` function declaration and documentation
|
||||||
|
|
||||||
|
### c-relay Changes
|
||||||
|
- [ ] Add `nip59_timestamp_max_delay_sec` to `default_config_event.h`
|
||||||
|
- [ ] Add validation in `config.c` for new parameter
|
||||||
|
- [ ] Update `src/api.c` call site to pass `max_delay_sec`
|
||||||
|
- [ ] Update `src/dm_admin.c` call site to pass `max_delay_sec`
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- [ ] Test with `max_delay_sec = 0` (no randomization)
|
||||||
|
- [ ] Test with `max_delay_sec = 1000` (custom delay)
|
||||||
|
- [ ] Test with `max_delay_sec = 172800` (default behavior)
|
||||||
|
- [ ] Test configuration validation (invalid values)
|
||||||
|
- [ ] Test interoperability with other Nostr clients
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- [ ] Update `docs/configuration_guide.md`
|
||||||
|
- [ ] Add this implementation plan to docs
|
||||||
|
- [ ] Update README if needed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollback Plan
|
||||||
|
|
||||||
|
If issues arise:
|
||||||
|
1. Revert nostr_core_lib changes (git revert in submodule)
|
||||||
|
2. Revert c-relay changes
|
||||||
|
3. Configuration parameter will be ignored if not used
|
||||||
|
4. Default behavior (0) provides maximum compatibility
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The configuration is read on each DM send, allowing runtime changes
|
||||||
|
- No restart required when changing `nip59_timestamp_max_delay_sec`
|
||||||
|
- Thread-safe by design (no global state)
|
||||||
|
- Default value of 0 provides maximum compatibility with other Nostr clients
|
||||||
|
- Can be changed to 172800 or other values for NIP-59 privacy features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [NIP-59: Gift Wrap](https://github.com/nostr-protocol/nips/blob/master/59.md)
|
||||||
|
- [NIP-17: Private Direct Messages](https://github.com/nostr-protocol/nips/blob/master/17.md)
|
||||||
|
- [NIP-44: Versioned Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
|
||||||
358
docs/subscription_table_collapse_plan.md
Normal file
@@ -0,0 +1,358 @@
|
|||||||
|
# Subscription Table Collapsible Groups Implementation Plan
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Add collapsible/expandable functionality to subscription groups, where:
|
||||||
|
1. Each WSI Pointer group starts collapsed, showing only a summary row
|
||||||
|
2. Clicking the summary row expands to show all subscription details for that WSI Pointer
|
||||||
|
3. Clicking again collapses the group back to the summary row
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
- All subscriptions are displayed in a flat list
|
||||||
|
- WSI Pointer value shown only on first row of each group
|
||||||
|
- No interaction or collapse functionality
|
||||||
|
|
||||||
|
## Desired Behavior
|
||||||
|
- **Collapsed state**: One row per WSI Pointer showing summary information
|
||||||
|
- **Expanded state**: Header row + detail rows for each subscription in that group
|
||||||
|
- **Toggle**: Click on header row to expand/collapse
|
||||||
|
- **Visual indicator**: Arrow or icon showing expand/collapse state
|
||||||
|
|
||||||
|
## Design Approach
|
||||||
|
|
||||||
|
### Option 1: Summary Row + Detail Rows (Recommended)
|
||||||
|
```
|
||||||
|
[▶] WSI Pointer: 0x12345678 | Subscriptions: 3 | Total Duration: 15m
|
||||||
|
(detail rows hidden)
|
||||||
|
|
||||||
|
When clicked:
|
||||||
|
[▼] WSI Pointer: 0x12345678 | Subscriptions: 3 | Total Duration: 15m
|
||||||
|
| sub-001 | 5m 30s | kinds:[1]
|
||||||
|
| sub-002 | 3m 15s | kinds:[3]
|
||||||
|
| sub-003 | 6m 15s | kinds:[1,3]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Option 2: First Row as Header (Alternative)
|
||||||
|
```
|
||||||
|
[▶] 0x12345678 | sub-001 | 5m 30s | kinds:[1] (+ 2 more)
|
||||||
|
|
||||||
|
When clicked:
|
||||||
|
[▼] 0x12345678 | sub-001 | 5m 30s | kinds:[1]
|
||||||
|
| sub-002 | 3m 15s | kinds:[3]
|
||||||
|
| sub-003 | 6m 15s | kinds:[1,3]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation**: Option 1 provides clearer UX and better summary information.
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### Files to Modify
|
||||||
|
1. `api/index.js` - Function `populateSubscriptionDetailsTable()` (lines 4277-4384)
|
||||||
|
2. `api/index.css` - Add styles for collapsible rows
|
||||||
|
|
||||||
|
### Data Structure Changes
|
||||||
|
|
||||||
|
Need to group subscriptions by WSI Pointer first:
|
||||||
|
```javascript
|
||||||
|
// Group subscriptions by wsi_pointer
|
||||||
|
const groupedSubscriptions = {};
|
||||||
|
subscriptionsData.forEach(sub => {
|
||||||
|
const wsiKey = sub.wsi_pointer || 'N/A';
|
||||||
|
if (!groupedSubscriptions[wsiKey]) {
|
||||||
|
groupedSubscriptions[wsiKey] = [];
|
||||||
|
}
|
||||||
|
groupedSubscriptions[wsiKey].push(sub);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTML Structure
|
||||||
|
|
||||||
|
#### Summary Row (Header)
|
||||||
|
```html
|
||||||
|
<tr class="subscription-group-header" data-wsi-pointer="0x12345678" data-expanded="false">
|
||||||
|
<td colspan="4" style="cursor: pointer; user-select: none;">
|
||||||
|
<span class="expand-icon">▶</span>
|
||||||
|
<strong>WSI Pointer:</strong> 0x12345678
|
||||||
|
<span class="group-summary">
|
||||||
|
| Subscriptions: 3 | Oldest: 15m 30s
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Detail Rows (Initially Hidden)
|
||||||
|
```html
|
||||||
|
<tr class="subscription-detail-row" data-wsi-group="0x12345678" style="display: none;">
|
||||||
|
<td style="padding-left: 30px;"><!-- Empty for WSI Pointer --></td>
|
||||||
|
<td>sub-001</td>
|
||||||
|
<td>5m 30s</td>
|
||||||
|
<td>kinds:[1]</td>
|
||||||
|
</tr>
|
||||||
|
```
|
||||||
|
|
||||||
|
### JavaScript Implementation
|
||||||
|
|
||||||
|
#### Modified populateSubscriptionDetailsTable Function
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function populateSubscriptionDetailsTable(subscriptionsData) {
|
||||||
|
const tableBody = document.getElementById('subscription-details-table-body');
|
||||||
|
if (!tableBody || !subscriptionsData || !Array.isArray(subscriptionsData)) return;
|
||||||
|
|
||||||
|
tableBody.innerHTML = '';
|
||||||
|
|
||||||
|
if (subscriptionsData.length === 0) {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = '<td colspan="4" style="text-align: center; font-style: italic;">No active subscriptions</td>';
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort subscriptions by wsi_pointer to group them together
|
||||||
|
subscriptionsData.sort((a, b) => {
|
||||||
|
const wsiA = a.wsi_pointer || '';
|
||||||
|
const wsiB = b.wsi_pointer || '';
|
||||||
|
return wsiA.localeCompare(wsiB);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Group subscriptions by wsi_pointer
|
||||||
|
const groupedSubscriptions = {};
|
||||||
|
subscriptionsData.forEach(sub => {
|
||||||
|
const wsiKey = sub.wsi_pointer || 'N/A';
|
||||||
|
if (!groupedSubscriptions[wsiKey]) {
|
||||||
|
groupedSubscriptions[wsiKey] = [];
|
||||||
|
}
|
||||||
|
groupedSubscriptions[wsiKey].push(sub);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create rows for each group
|
||||||
|
Object.entries(groupedSubscriptions).forEach(([wsiPointer, subscriptions]) => {
|
||||||
|
// Calculate group summary
|
||||||
|
const subCount = subscriptions.length;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const oldestDuration = Math.max(...subscriptions.map(s => now - s.created_at));
|
||||||
|
const oldestDurationStr = formatDuration(oldestDuration);
|
||||||
|
|
||||||
|
// Create header row (summary)
|
||||||
|
const headerRow = document.createElement('tr');
|
||||||
|
headerRow.className = 'subscription-group-header';
|
||||||
|
headerRow.setAttribute('data-wsi-pointer', wsiPointer);
|
||||||
|
headerRow.setAttribute('data-expanded', 'false');
|
||||||
|
headerRow.style.cursor = 'pointer';
|
||||||
|
headerRow.style.userSelect = 'none';
|
||||||
|
headerRow.style.backgroundColor = 'var(--hover-color, #f5f5f5)';
|
||||||
|
|
||||||
|
headerRow.innerHTML = `
|
||||||
|
<td colspan="4" style="padding: 8px;">
|
||||||
|
<span class="expand-icon" style="display: inline-block; width: 20px; transition: transform 0.2s;">▶</span>
|
||||||
|
<strong style="font-family: 'Courier New', monospace; font-size: 12px;">WSI: ${wsiPointer}</strong>
|
||||||
|
<span style="color: #666; margin-left: 15px;">
|
||||||
|
Subscriptions: ${subCount} | Oldest: ${oldestDurationStr}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Add click handler to toggle expansion
|
||||||
|
headerRow.addEventListener('click', () => toggleSubscriptionGroup(wsiPointer));
|
||||||
|
|
||||||
|
tableBody.appendChild(headerRow);
|
||||||
|
|
||||||
|
// Create detail rows (initially hidden)
|
||||||
|
subscriptions.forEach((subscription, index) => {
|
||||||
|
const detailRow = document.createElement('tr');
|
||||||
|
detailRow.className = 'subscription-detail-row';
|
||||||
|
detailRow.setAttribute('data-wsi-group', wsiPointer);
|
||||||
|
detailRow.style.display = 'none';
|
||||||
|
|
||||||
|
// Calculate duration
|
||||||
|
const duration = now - subscription.created_at;
|
||||||
|
const durationStr = formatDuration(duration);
|
||||||
|
|
||||||
|
// Format filters
|
||||||
|
let filtersDisplay = 'None';
|
||||||
|
if (subscription.filters && subscription.filters.length > 0) {
|
||||||
|
const filterDetails = [];
|
||||||
|
subscription.filters.forEach((filter) => {
|
||||||
|
const parts = [];
|
||||||
|
|
||||||
|
if (filter.kinds && Array.isArray(filter.kinds) && filter.kinds.length > 0) {
|
||||||
|
parts.push(`kinds:[${filter.kinds.join(',')}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.authors && Array.isArray(filter.authors) && filter.authors.length > 0) {
|
||||||
|
const authorCount = filter.authors.length;
|
||||||
|
if (authorCount === 1) {
|
||||||
|
const shortPubkey = filter.authors[0].substring(0, 8) + '...';
|
||||||
|
parts.push(`authors:[${shortPubkey}]`);
|
||||||
|
} else {
|
||||||
|
parts.push(`authors:[${authorCount} pubkeys]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.ids && Array.isArray(filter.ids) && filter.ids.length > 0) {
|
||||||
|
const idCount = filter.ids.length;
|
||||||
|
parts.push(`ids:[${idCount} event${idCount > 1 ? 's' : ''}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeParts = [];
|
||||||
|
if (filter.since && filter.since > 0) {
|
||||||
|
const sinceDate = new Date(filter.since * 1000).toLocaleString();
|
||||||
|
timeParts.push(`since:${sinceDate}`);
|
||||||
|
}
|
||||||
|
if (filter.until && filter.until > 0) {
|
||||||
|
const untilDate = new Date(filter.until * 1000).toLocaleString();
|
||||||
|
timeParts.push(`until:${untilDate}`);
|
||||||
|
}
|
||||||
|
if (timeParts.length > 0) {
|
||||||
|
parts.push(timeParts.join(', '));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.limit && filter.limit > 0) {
|
||||||
|
parts.push(`limit:${filter.limit}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filter.tag_filters && Array.isArray(filter.tag_filters) && filter.tag_filters.length > 0) {
|
||||||
|
parts.push(`tags:[${filter.tag_filters.length} filter${filter.tag_filters.length > 1 ? 's' : ''}]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length > 0) {
|
||||||
|
filterDetails.push(parts.join(', '));
|
||||||
|
} else {
|
||||||
|
filterDetails.push('empty filter');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
filtersDisplay = filterDetails.join(' | ');
|
||||||
|
}
|
||||||
|
|
||||||
|
detailRow.innerHTML = `
|
||||||
|
<td style="padding-left: 30px; font-family: 'Courier New', monospace; font-size: 11px; color: #999;">└─</td>
|
||||||
|
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${subscription.id || 'N/A'}</td>
|
||||||
|
<td>${durationStr}</td>
|
||||||
|
<td>${filtersDisplay}</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tableBody.appendChild(detailRow);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle function for expanding/collapsing groups
|
||||||
|
function toggleSubscriptionGroup(wsiPointer) {
|
||||||
|
const headerRow = document.querySelector(`.subscription-group-header[data-wsi-pointer="${wsiPointer}"]`);
|
||||||
|
const detailRows = document.querySelectorAll(`.subscription-detail-row[data-wsi-group="${wsiPointer}"]`);
|
||||||
|
const expandIcon = headerRow.querySelector('.expand-icon');
|
||||||
|
|
||||||
|
const isExpanded = headerRow.getAttribute('data-expanded') === 'true';
|
||||||
|
|
||||||
|
if (isExpanded) {
|
||||||
|
// Collapse
|
||||||
|
detailRows.forEach(row => row.style.display = 'none');
|
||||||
|
expandIcon.textContent = '▶';
|
||||||
|
expandIcon.style.transform = 'rotate(0deg)';
|
||||||
|
headerRow.setAttribute('data-expanded', 'false');
|
||||||
|
} else {
|
||||||
|
// Expand
|
||||||
|
detailRows.forEach(row => row.style.display = 'table-row');
|
||||||
|
expandIcon.textContent = '▼';
|
||||||
|
expandIcon.style.transform = 'rotate(90deg)';
|
||||||
|
headerRow.setAttribute('data-expanded', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Additions (api/index.css)
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Subscription group header styles */
|
||||||
|
.subscription-group-header {
|
||||||
|
background-color: var(--hover-color, #f5f5f5);
|
||||||
|
font-weight: 500;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-group-header:hover {
|
||||||
|
background-color: var(--primary-color-light, #e8e8e8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expand-icon {
|
||||||
|
display: inline-block;
|
||||||
|
width: 20px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Detail row styles */
|
||||||
|
.subscription-detail-row {
|
||||||
|
background-color: var(--background-color, #ffffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.subscription-detail-row:hover {
|
||||||
|
background-color: var(--hover-color-light, #fafafa);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode support */
|
||||||
|
.dark-mode .subscription-group-header {
|
||||||
|
background-color: var(--hover-color-dark, #2a2a2a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .subscription-group-header:hover {
|
||||||
|
background-color: var(--primary-color-dark, #333333);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .subscription-detail-row {
|
||||||
|
background-color: var(--background-color-dark, #1a1a1a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode .subscription-detail-row:hover {
|
||||||
|
background-color: var(--hover-color-dark, #252525);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features Included
|
||||||
|
|
||||||
|
1. ✅ **Collapsible groups**: Each WSI Pointer group can be collapsed/expanded
|
||||||
|
2. ✅ **Visual indicator**: Arrow icon (▶/▼) shows current state
|
||||||
|
3. ✅ **Summary information**: Shows subscription count and oldest duration
|
||||||
|
4. ✅ **Smooth transitions**: Icon rotation animation
|
||||||
|
5. ✅ **Hover effects**: Visual feedback on header rows
|
||||||
|
6. ✅ **Tree structure**: Detail rows indented with └─ character
|
||||||
|
7. ✅ **Dark mode support**: Proper styling for both themes
|
||||||
|
8. ✅ **Keyboard accessible**: Can be enhanced with keyboard navigation
|
||||||
|
|
||||||
|
## User Experience Flow
|
||||||
|
|
||||||
|
1. **Initial load**: All groups collapsed, showing summary rows only
|
||||||
|
2. **Click header**: Group expands, showing all subscription details
|
||||||
|
3. **Click again**: Group collapses back to summary
|
||||||
|
4. **Multiple groups**: Each group can be independently expanded/collapsed
|
||||||
|
5. **Visual feedback**: Hover effects and smooth animations
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
1. ✅ Verify groups start collapsed by default
|
||||||
|
2. ✅ Verify clicking header expands group
|
||||||
|
3. ✅ Verify clicking again collapses group
|
||||||
|
4. ✅ Verify multiple groups can be expanded simultaneously
|
||||||
|
5. ✅ Verify summary information is accurate
|
||||||
|
6. ✅ Verify detail rows display correctly when expanded
|
||||||
|
7. ✅ Verify styling works in both light and dark modes
|
||||||
|
8. ✅ Verify no console errors
|
||||||
|
9. ✅ Verify performance with many subscriptions
|
||||||
|
|
||||||
|
## Optional Enhancements
|
||||||
|
|
||||||
|
1. **Expand/Collapse All**: Add buttons to expand or collapse all groups at once
|
||||||
|
2. **Remember State**: Store expansion state in localStorage
|
||||||
|
3. **Keyboard Navigation**: Add keyboard shortcuts (Space/Enter to toggle)
|
||||||
|
4. **Animation**: Add slide-down/up animation for detail rows
|
||||||
|
5. **Search/Filter**: Add ability to search within subscriptions
|
||||||
|
6. **Export**: Add ability to export subscription data
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Review this plan
|
||||||
|
2. Switch to Code mode
|
||||||
|
3. Implement the changes in `api/index.js`
|
||||||
|
4. Add CSS styles in `api/index.css`
|
||||||
|
5. Test the collapsible functionality
|
||||||
|
6. Verify all edge cases work correctly
|
||||||
155
docs/subscription_table_grouping_plan.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Subscription Table WSI Pointer Grouping Implementation Plan
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Modify the subscription details table to show the WSI Pointer value only once per group - on the first row of each WSI Pointer group, leaving it blank for subsequent rows with the same WSI Pointer.
|
||||||
|
|
||||||
|
## Current Behavior
|
||||||
|
- All rows show the WSI Pointer value
|
||||||
|
- Rows are sorted by WSI Pointer (grouping is working)
|
||||||
|
- Visual grouping is not clear
|
||||||
|
|
||||||
|
## Desired Behavior
|
||||||
|
- First row of each WSI Pointer group shows the full WSI Pointer value
|
||||||
|
- Subsequent rows in the same group have an empty cell for WSI Pointer
|
||||||
|
- This creates a clear visual grouping effect
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### File to Modify
|
||||||
|
`api/index.js` - Function `populateSubscriptionDetailsTable()` (lines 4277-4384)
|
||||||
|
|
||||||
|
### Code Changes Required
|
||||||
|
|
||||||
|
#### Current Code (lines 4291-4383):
|
||||||
|
```javascript
|
||||||
|
// Sort subscriptions by wsi_pointer to group them together
|
||||||
|
subscriptionsData.sort((a, b) => {
|
||||||
|
const wsiA = a.wsi_pointer || '';
|
||||||
|
const wsiB = b.wsi_pointer || '';
|
||||||
|
return wsiA.localeCompare(wsiB);
|
||||||
|
});
|
||||||
|
|
||||||
|
subscriptionsData.forEach((subscription, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
// Calculate duration
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const duration = now - subscription.created_at;
|
||||||
|
const durationStr = formatDuration(duration);
|
||||||
|
|
||||||
|
// Format client IP (show full IP for admin view)
|
||||||
|
const clientIP = subscription.client_ip || 'unknown';
|
||||||
|
|
||||||
|
// Format wsi_pointer (show full pointer)
|
||||||
|
const wsiPointer = subscription.wsi_pointer || 'N/A';
|
||||||
|
|
||||||
|
// Format filters (show actual filter details)
|
||||||
|
let filtersDisplay = 'None';
|
||||||
|
// ... filter formatting code ...
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${wsiPointer}</td>
|
||||||
|
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${subscription.id || 'N/A'}</td>
|
||||||
|
<!-- <td style="font-family: 'Courier New', monospace; font-size: 12px;">${clientIP}</td> -->
|
||||||
|
<td>${durationStr}</td>
|
||||||
|
<td>${filtersDisplay}</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Modified Code:
|
||||||
|
```javascript
|
||||||
|
// Sort subscriptions by wsi_pointer to group them together
|
||||||
|
subscriptionsData.sort((a, b) => {
|
||||||
|
const wsiA = a.wsi_pointer || '';
|
||||||
|
const wsiB = b.wsi_pointer || '';
|
||||||
|
return wsiA.localeCompare(wsiB);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Track previous WSI Pointer to detect group changes
|
||||||
|
let previousWsiPointer = null;
|
||||||
|
|
||||||
|
subscriptionsData.forEach((subscription, index) => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
// Calculate duration
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const duration = now - subscription.created_at;
|
||||||
|
const durationStr = formatDuration(duration);
|
||||||
|
|
||||||
|
// Format client IP (show full IP for admin view)
|
||||||
|
const clientIP = subscription.client_ip || 'unknown';
|
||||||
|
|
||||||
|
// Format wsi_pointer - only show if it's different from previous row
|
||||||
|
const currentWsiPointer = subscription.wsi_pointer || 'N/A';
|
||||||
|
let wsiPointerDisplay = '';
|
||||||
|
|
||||||
|
if (currentWsiPointer !== previousWsiPointer) {
|
||||||
|
// This is the first row of a new group - show the WSI Pointer
|
||||||
|
wsiPointerDisplay = currentWsiPointer;
|
||||||
|
previousWsiPointer = currentWsiPointer;
|
||||||
|
} else {
|
||||||
|
// This is a continuation of the same group - leave blank
|
||||||
|
wsiPointerDisplay = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format filters (show actual filter details)
|
||||||
|
let filtersDisplay = 'None';
|
||||||
|
// ... filter formatting code remains the same ...
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${wsiPointerDisplay}</td>
|
||||||
|
<td style="font-family: 'Courier New', monospace; font-size: 12px;">${subscription.id || 'N/A'}</td>
|
||||||
|
<!-- <td style="font-family: 'Courier New', monospace; font-size: 12px;">${clientIP}</td> -->
|
||||||
|
<td>${durationStr}</td>
|
||||||
|
<td>${filtersDisplay}</td>
|
||||||
|
`;
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Changes Explained
|
||||||
|
|
||||||
|
1. **Add tracking variable**: `let previousWsiPointer = null;` before the forEach loop
|
||||||
|
2. **Store current WSI Pointer**: `const currentWsiPointer = subscription.wsi_pointer || 'N/A';`
|
||||||
|
3. **Compare with previous**: Check if `currentWsiPointer !== previousWsiPointer`
|
||||||
|
4. **Conditional display**:
|
||||||
|
- If different: Show the WSI Pointer value and update `previousWsiPointer`
|
||||||
|
- If same: Show empty string (blank cell)
|
||||||
|
5. **Use display variable**: Replace `${wsiPointer}` with `${wsiPointerDisplay}` in the row HTML
|
||||||
|
|
||||||
|
### Visual Result
|
||||||
|
|
||||||
|
**Before:**
|
||||||
|
```
|
||||||
|
WSI Pointer | Subscription ID | Duration | Filters
|
||||||
|
0x12345678 | sub-001 | 5m 30s | kinds:[1]
|
||||||
|
0x12345678 | sub-002 | 3m 15s | kinds:[3]
|
||||||
|
0x87654321 | sub-003 | 1m 45s | kinds:[1,3]
|
||||||
|
```
|
||||||
|
|
||||||
|
**After:**
|
||||||
|
```
|
||||||
|
WSI Pointer | Subscription ID | Duration | Filters
|
||||||
|
0x12345678 | sub-001 | 5m 30s | kinds:[1]
|
||||||
|
| sub-002 | 3m 15s | kinds:[3]
|
||||||
|
0x87654321 | sub-003 | 1m 45s | kinds:[1,3]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Checklist
|
||||||
|
|
||||||
|
1. ✅ Verify first row of each group shows WSI Pointer
|
||||||
|
2. ✅ Verify subsequent rows in same group are blank
|
||||||
|
3. ✅ Verify grouping works with multiple subscriptions per WSI Pointer
|
||||||
|
4. ✅ Verify single subscription per WSI Pointer still shows the value
|
||||||
|
5. ✅ Verify empty/null WSI Pointers are handled correctly
|
||||||
|
6. ✅ Verify table still displays correctly when no subscriptions exist
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Review this plan
|
||||||
|
2. Switch to Code mode
|
||||||
|
3. Implement the changes in `api/index.js`
|
||||||
|
4. Test the implementation
|
||||||
|
5. Verify the visual grouping effect
|
||||||
@@ -8,36 +8,59 @@ YELLOW='\033[1;33m'
|
|||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
|
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
|
||||||
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
|
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
|
||||||
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
|
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
|
||||||
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
|
||||||
|
|
||||||
# Global variables
|
# Global variables
|
||||||
COMMIT_MESSAGE=""
|
COMMIT_MESSAGE=""
|
||||||
RELEASE_MODE=false
|
RELEASE_MODE=false
|
||||||
|
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
|
||||||
|
|
||||||
show_usage() {
|
show_usage() {
|
||||||
echo "C-Relay Increment and Push Script"
|
echo "C-Relay Increment and Push Script"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Usage:"
|
echo "USAGE:"
|
||||||
echo " $0 \"commit message\" - Default: increment patch, commit & push"
|
echo " $0 [OPTIONS] \"commit message\""
|
||||||
echo " $0 -r \"commit message\" - Release: increment minor, create release"
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Examples:"
|
echo "COMMANDS:"
|
||||||
|
echo " $0 \"commit message\" Default: increment patch, commit & push"
|
||||||
|
echo " $0 -p \"commit message\" Increment patch version"
|
||||||
|
echo " $0 -m \"commit message\" Increment minor version"
|
||||||
|
echo " $0 -M \"commit message\" Increment major version"
|
||||||
|
echo " $0 -r \"commit message\" Create release with assets (no version increment)"
|
||||||
|
echo " $0 -r -m \"commit message\" Create release with minor version increment"
|
||||||
|
echo " $0 -h Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "OPTIONS:"
|
||||||
|
echo " -p, --patch Increment patch version (default)"
|
||||||
|
echo " -m, --minor Increment minor version"
|
||||||
|
echo " -M, --major Increment major version"
|
||||||
|
echo " -r, --release Create release with assets"
|
||||||
|
echo " -h, --help Show this help message"
|
||||||
|
echo ""
|
||||||
|
echo "EXAMPLES:"
|
||||||
echo " $0 \"Fixed event validation bug\""
|
echo " $0 \"Fixed event validation bug\""
|
||||||
echo " $0 --release \"Major release with new features\""
|
echo " $0 -m \"Added new features\""
|
||||||
|
echo " $0 -M \"Breaking API changes\""
|
||||||
|
echo " $0 -r \"Release current version\""
|
||||||
|
echo " $0 -r -m \"Release with minor increment\""
|
||||||
echo ""
|
echo ""
|
||||||
echo "Default Mode (patch increment):"
|
echo "VERSION INCREMENT MODES:"
|
||||||
echo " - Increment patch version (v1.2.3 → v1.2.4)"
|
echo " -p, --patch (default): Increment patch version (v1.2.3 → v1.2.4)"
|
||||||
echo " - Git add, commit with message, and push"
|
echo " -m, --minor: Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
||||||
|
echo " -M, --major: Increment major version, zero minor+patch (v1.2.3 → v2.0.0)"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Release Mode (-r flag):"
|
echo "RELEASE MODE (-r flag):"
|
||||||
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
|
echo " - Build static binary using build_static.sh"
|
||||||
echo " - Git add, commit, push, and create Gitea release"
|
echo " - Create source tarball"
|
||||||
|
echo " - Git add, commit, push, and create Gitea release with assets"
|
||||||
|
echo " - Can be combined with version increment flags"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Requirements for Release Mode:"
|
echo "REQUIREMENTS FOR RELEASE MODE:"
|
||||||
echo " - Gitea token in ~/.gitea_token for release uploads"
|
echo " - Gitea token in ~/.gitea_token for release uploads"
|
||||||
|
echo " - Docker installed for static binary builds"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Parse command line arguments
|
# Parse command line arguments
|
||||||
@@ -47,6 +70,18 @@ while [[ $# -gt 0 ]]; do
|
|||||||
RELEASE_MODE=true
|
RELEASE_MODE=true
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
-p|--patch)
|
||||||
|
VERSION_INCREMENT_TYPE="patch"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-m|--minor)
|
||||||
|
VERSION_INCREMENT_TYPE="minor"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-M|--major)
|
||||||
|
VERSION_INCREMENT_TYPE="major"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
show_usage
|
show_usage
|
||||||
exit 0
|
exit 0
|
||||||
@@ -79,7 +114,7 @@ check_git_repo() {
|
|||||||
|
|
||||||
# Function to get current version and increment appropriately
|
# Function to get current version and increment appropriately
|
||||||
increment_version() {
|
increment_version() {
|
||||||
local increment_type="$1" # "patch" or "minor"
|
local increment_type="$1" # "patch", "minor", or "major"
|
||||||
|
|
||||||
print_status "Getting current version..."
|
print_status "Getting current version..."
|
||||||
|
|
||||||
@@ -105,24 +140,34 @@ increment_version() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Increment version based on type
|
# Increment version based on type
|
||||||
if [[ "$increment_type" == "minor" ]]; then
|
if [[ "$increment_type" == "major" ]]; then
|
||||||
|
# Major release: increment major, zero minor and patch
|
||||||
|
NEW_MAJOR=$((MAJOR + 1))
|
||||||
|
NEW_MINOR=0
|
||||||
|
NEW_PATCH=0
|
||||||
|
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||||
|
print_status "Major version increment: incrementing major version"
|
||||||
|
elif [[ "$increment_type" == "minor" ]]; then
|
||||||
# Minor release: increment minor, zero patch
|
# Minor release: increment minor, zero patch
|
||||||
|
NEW_MAJOR=$MAJOR
|
||||||
NEW_MINOR=$((MINOR + 1))
|
NEW_MINOR=$((MINOR + 1))
|
||||||
NEW_PATCH=0
|
NEW_PATCH=0
|
||||||
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||||
print_status "Release mode: incrementing minor version"
|
print_status "Minor version increment: incrementing minor version"
|
||||||
else
|
else
|
||||||
# Default: increment patch
|
# Default: increment patch
|
||||||
|
NEW_MAJOR=$MAJOR
|
||||||
|
NEW_MINOR=$MINOR
|
||||||
NEW_PATCH=$((PATCH + 1))
|
NEW_PATCH=$((PATCH + 1))
|
||||||
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
|
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
|
||||||
print_status "Default mode: incrementing patch version"
|
print_status "Patch version increment: incrementing patch version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
print_status "Current version: $LATEST_TAG"
|
print_status "Current version: $LATEST_TAG"
|
||||||
print_status "New version: $NEW_VERSION"
|
print_status "New version: $NEW_VERSION"
|
||||||
|
|
||||||
# Update version in src/main.h
|
# Update version in src/main.h
|
||||||
update_version_in_header "$NEW_VERSION" "$MAJOR" "${NEW_MINOR:-$MINOR}" "${NEW_PATCH:-$PATCH}"
|
update_version_in_header "$NEW_VERSION" "$NEW_MAJOR" "$NEW_MINOR" "$NEW_PATCH"
|
||||||
|
|
||||||
# Export for use in other functions
|
# Export for use in other functions
|
||||||
export NEW_VERSION
|
export NEW_VERSION
|
||||||
@@ -261,6 +306,115 @@ git_commit_and_push_no_tag() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Function to build release binary
|
||||||
|
build_release_binary() {
|
||||||
|
print_status "Building release binary..."
|
||||||
|
|
||||||
|
# Check if build_static.sh exists
|
||||||
|
if [[ ! -f "build_static.sh" ]]; then
|
||||||
|
print_error "build_static.sh not found"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the static build script
|
||||||
|
if ./build_static.sh > /dev/null 2>&1; then
|
||||||
|
print_success "Built static binary successfully"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Failed to build static binary"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create source tarball
|
||||||
|
create_source_tarball() {
|
||||||
|
print_status "Creating source tarball..."
|
||||||
|
|
||||||
|
local tarball_name="c-relay-${NEW_VERSION#v}.tar.gz"
|
||||||
|
|
||||||
|
# Create tarball excluding build artifacts and git files
|
||||||
|
if tar -czf "$tarball_name" \
|
||||||
|
--exclude='build/*' \
|
||||||
|
--exclude='.git*' \
|
||||||
|
--exclude='*.db' \
|
||||||
|
--exclude='*.db-*' \
|
||||||
|
--exclude='*.log' \
|
||||||
|
--exclude='*.tar.gz' \
|
||||||
|
. > /dev/null 2>&1; then
|
||||||
|
print_success "Created source tarball: $tarball_name"
|
||||||
|
echo "$tarball_name"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "Failed to create source tarball"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to upload release assets to Gitea
|
||||||
|
upload_release_assets() {
|
||||||
|
local release_id="$1"
|
||||||
|
local binary_path="$2"
|
||||||
|
local tarball_path="$3"
|
||||||
|
|
||||||
|
print_status "Uploading release assets..."
|
||||||
|
|
||||||
|
# Check for Gitea token
|
||||||
|
if [[ ! -f "$HOME/.gitea_token" ]]; then
|
||||||
|
print_warning "No ~/.gitea_token found. Skipping asset uploads."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local token=$(cat "$HOME/.gitea_token" | tr -d '\n\r')
|
||||||
|
local api_url="https://git.laantungir.net/api/v1/repos/laantungir/c-relay"
|
||||||
|
local assets_url="$api_url/releases/$release_id/assets"
|
||||||
|
print_status "Assets URL: $assets_url"
|
||||||
|
|
||||||
|
# Upload binary
|
||||||
|
if [[ -f "$binary_path" ]]; then
|
||||||
|
print_status "Uploading binary: $(basename "$binary_path")"
|
||||||
|
|
||||||
|
# Retry loop for eventual consistency
|
||||||
|
local max_attempts=3
|
||||||
|
local attempt=1
|
||||||
|
while [[ $attempt -le $max_attempts ]]; do
|
||||||
|
print_status "Upload attempt $attempt/$max_attempts"
|
||||||
|
local binary_response=$(curl -fS -X POST "$assets_url" \
|
||||||
|
-H "Authorization: token $token" \
|
||||||
|
-F "attachment=@$binary_path;filename=$(basename "$binary_path")" \
|
||||||
|
-F "name=$(basename "$binary_path")")
|
||||||
|
|
||||||
|
if echo "$binary_response" | grep -q '"id"'; then
|
||||||
|
print_success "Uploaded binary successfully"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
print_warning "Upload attempt $attempt failed"
|
||||||
|
if [[ $attempt -lt $max_attempts ]]; then
|
||||||
|
print_status "Retrying in 2 seconds..."
|
||||||
|
sleep 2
|
||||||
|
else
|
||||||
|
print_error "Failed to upload binary after $max_attempts attempts"
|
||||||
|
print_error "Response: $binary_response"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
((attempt++))
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Upload source tarball
|
||||||
|
if [[ -f "$tarball_path" ]]; then
|
||||||
|
print_status "Uploading source tarball: $(basename "$tarball_path")"
|
||||||
|
local tarball_response=$(curl -s -X POST "$api_url/releases/$release_id/assets" \
|
||||||
|
-H "Authorization: token $token" \
|
||||||
|
-F "attachment=@$tarball_path;filename=$(basename "$tarball_path")")
|
||||||
|
|
||||||
|
if echo "$tarball_response" | grep -q '"id"'; then
|
||||||
|
print_success "Uploaded source tarball successfully"
|
||||||
|
else
|
||||||
|
print_warning "Failed to upload source tarball: $tarball_response"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# Function to create Gitea release
|
# Function to create Gitea release
|
||||||
create_gitea_release() {
|
create_gitea_release() {
|
||||||
print_status "Creating Gitea release..."
|
print_status "Creating Gitea release..."
|
||||||
@@ -278,16 +432,27 @@ create_gitea_release() {
|
|||||||
# Create release
|
# Create release
|
||||||
print_status "Creating release $NEW_VERSION..."
|
print_status "Creating release $NEW_VERSION..."
|
||||||
local response=$(curl -s -X POST "$api_url/releases" \
|
local response=$(curl -s -X POST "$api_url/releases" \
|
||||||
-H "Authorization: token $token" \
|
-H "Authorization: token $token" \
|
||||||
-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\"}")
|
||||||
|
|
||||||
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"
|
||||||
return 0
|
# Extract release ID for asset uploads
|
||||||
|
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||||
|
echo $release_id
|
||||||
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"
|
||||||
return 0
|
# Try to get existing release ID
|
||||||
|
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
|
||||||
|
if echo "$check_response" | grep -q '"id"'; then
|
||||||
|
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||||
|
print_status "Using existing release ID: $release_id"
|
||||||
|
echo $release_id
|
||||||
|
else
|
||||||
|
print_error "Could not find existing release ID"
|
||||||
|
return 1
|
||||||
|
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"
|
||||||
@@ -297,7 +462,8 @@ 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"
|
||||||
return 0
|
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||||
|
echo $release_id
|
||||||
else
|
else
|
||||||
print_error "Release does not exist and creation failed"
|
print_error "Release does not exist and creation failed"
|
||||||
return 1
|
return 1
|
||||||
@@ -315,8 +481,15 @@ main() {
|
|||||||
if [[ "$RELEASE_MODE" == true ]]; then
|
if [[ "$RELEASE_MODE" == true ]]; then
|
||||||
print_status "=== RELEASE MODE ==="
|
print_status "=== RELEASE MODE ==="
|
||||||
|
|
||||||
# Increment minor version for releases
|
# Only increment version if explicitly requested (not just because of -r flag)
|
||||||
increment_version "minor"
|
if [[ "$VERSION_INCREMENT_TYPE" != "patch" ]]; then
|
||||||
|
increment_version "$VERSION_INCREMENT_TYPE"
|
||||||
|
else
|
||||||
|
# In release mode without version increment, get current version
|
||||||
|
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.0.0")
|
||||||
|
NEW_VERSION="$LATEST_TAG"
|
||||||
|
export NEW_VERSION
|
||||||
|
fi
|
||||||
|
|
||||||
# Create new git tag BEFORE compilation so version.h picks it up
|
# Create new git tag BEFORE compilation so version.h picks it up
|
||||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||||
@@ -330,9 +503,42 @@ main() {
|
|||||||
# Commit and push (but skip tag creation since we already did it)
|
# Commit and push (but skip tag creation since we already did it)
|
||||||
git_commit_and_push_no_tag
|
git_commit_and_push_no_tag
|
||||||
|
|
||||||
|
# Build release binary
|
||||||
|
if build_release_binary; then
|
||||||
|
local binary_path="build/c_relay_static_x86_64"
|
||||||
|
else
|
||||||
|
print_warning "Binary build failed, continuing with release creation"
|
||||||
|
# Check if binary exists from previous build
|
||||||
|
if [[ -f "build/c_relay_static_x86_64" ]]; then
|
||||||
|
print_status "Using existing binary from previous build"
|
||||||
|
binary_path="build/c_relay_static_x86_64"
|
||||||
|
else
|
||||||
|
binary_path=""
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create source tarball
|
||||||
|
local tarball_path=""
|
||||||
|
if tarball_path=$(create_source_tarball); then
|
||||||
|
: # tarball_path is set by the function
|
||||||
|
else
|
||||||
|
print_warning "Source tarball creation failed, continuing with release creation"
|
||||||
|
fi
|
||||||
|
|
||||||
# Create Gitea release
|
# Create Gitea release
|
||||||
if create_gitea_release; then
|
local release_id=""
|
||||||
print_success "Release $NEW_VERSION completed successfully!"
|
if release_id=$(create_gitea_release); then
|
||||||
|
# Validate release_id is numeric
|
||||||
|
if [[ "$release_id" =~ ^[0-9]+$ ]]; then
|
||||||
|
# Upload assets if we have a release ID and assets
|
||||||
|
if [[ -n "$release_id" && (-n "$binary_path" || -n "$tarball_path") ]]; then
|
||||||
|
upload_release_assets "$release_id" "$binary_path" "$tarball_path"
|
||||||
|
fi
|
||||||
|
print_success "Release $NEW_VERSION completed successfully!"
|
||||||
|
else
|
||||||
|
print_error "Invalid release_id: $release_id"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
print_error "Release creation failed"
|
print_error "Release creation failed"
|
||||||
fi
|
fi
|
||||||
@@ -340,8 +546,8 @@ main() {
|
|||||||
else
|
else
|
||||||
print_status "=== DEFAULT MODE ==="
|
print_status "=== DEFAULT MODE ==="
|
||||||
|
|
||||||
# Increment patch version for regular commits
|
# Increment version based on type (default to patch)
|
||||||
increment_version "patch"
|
increment_version "$VERSION_INCREMENT_TYPE"
|
||||||
|
|
||||||
# Create new git tag BEFORE compilation so version.h picks it up
|
# Create new git tag BEFORE compilation so version.h picks it up
|
||||||
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
|
||||||
|
|||||||
1
nips
BIN
screenshots/2025-10-29_15-33.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
screenshots/2025-10-29_15-35.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
screenshots/2025-10-29_15-35_1.png
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
BIN
screenshots/2025-10-29_16-04.png
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
BIN
screenshots/2025-11-11_11-57.png
Normal file
|
After Width: | Height: | Size: 738 KiB |
BIN
screenshots/2025-11-13_15-53.png
Normal file
|
After Width: | Height: | Size: 2.2 MiB |
BIN
screenshots/2025-11-13_15-53_1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
screenshots/DM.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
screenshots/ProfileEvents.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
screenshots/config.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
screenshots/light-mode.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
screenshots/main-light.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
screenshots/main.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
screenshots/raffles.png
Normal file
|
After Width: | Height: | Size: 296 KiB |
BIN
screenshots/sqlQuery.png
Normal file
|
After Width: | Height: | Size: 154 KiB |
BIN
screenshots/subscriptions.png
Normal file
|
After Width: | Height: | Size: 157 KiB |
BIN
screenshots/white-blacklists.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
243
src/api.c
@@ -271,6 +271,7 @@ cJSON* query_subscription_details(void) {
|
|||||||
long long events_sent = sqlite3_column_int64(stmt, 3);
|
long long events_sent = sqlite3_column_int64(stmt, 3);
|
||||||
long long created_at = sqlite3_column_int64(stmt, 4);
|
long long created_at = sqlite3_column_int64(stmt, 4);
|
||||||
long long duration_seconds = sqlite3_column_int64(stmt, 5);
|
long long duration_seconds = sqlite3_column_int64(stmt, 5);
|
||||||
|
const char* wsi_pointer = (const char*)sqlite3_column_text(stmt, 6);
|
||||||
|
|
||||||
// DEBUG: Log each subscription found
|
// DEBUG: Log each subscription found
|
||||||
DEBUG_LOG("Row %d: sub_id=%s, client_ip=%s, events_sent=%lld, created_at=%lld",
|
DEBUG_LOG("Row %d: sub_id=%s, client_ip=%s, events_sent=%lld, created_at=%lld",
|
||||||
@@ -284,6 +285,7 @@ cJSON* query_subscription_details(void) {
|
|||||||
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds);
|
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds);
|
||||||
cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent);
|
cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent);
|
||||||
cJSON_AddBoolToObject(sub_obj, "active", 1); // All from this view are active
|
cJSON_AddBoolToObject(sub_obj, "active", 1); // All from this view are active
|
||||||
|
cJSON_AddStringToObject(sub_obj, "wsi_pointer", wsi_pointer ? wsi_pointer : "N/A");
|
||||||
|
|
||||||
// Parse and add filter JSON if available
|
// Parse and add filter JSON if available
|
||||||
if (filter_json) {
|
if (filter_json) {
|
||||||
@@ -499,6 +501,76 @@ void monitoring_on_subscription_change(void) {
|
|||||||
generate_subscription_driven_monitoring();
|
generate_subscription_driven_monitoring();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate and post a kind 1 status event with relay statistics
|
||||||
|
* Called periodically from main event loop based on configuration
|
||||||
|
* Returns: 0 on success, -1 on error
|
||||||
|
*/
|
||||||
|
int generate_and_post_status_event(void) {
|
||||||
|
// Check if feature is enabled
|
||||||
|
int hours_interval = get_config_int("kind_1_status_posts_hours", 0);
|
||||||
|
if (hours_interval <= 0) {
|
||||||
|
return 0; // Feature disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate statistics text content using existing function
|
||||||
|
char* stats_text = generate_stats_text();
|
||||||
|
if (!stats_text) {
|
||||||
|
DEBUG_ERROR("Failed to generate statistics text for status post");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get relay private key
|
||||||
|
char* relay_privkey_hex = get_relay_private_key();
|
||||||
|
if (!relay_privkey_hex) {
|
||||||
|
DEBUG_ERROR("Failed to get relay private key for status post");
|
||||||
|
free(stats_text);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex private key to bytes
|
||||||
|
unsigned char relay_privkey_bytes[32];
|
||||||
|
if (nostr_hex_to_bytes(relay_privkey_hex, relay_privkey_bytes, 32) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to convert relay private key to bytes");
|
||||||
|
free(stats_text);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create empty tags array (kind 1 doesn't require special tags)
|
||||||
|
cJSON* tags = cJSON_CreateArray();
|
||||||
|
|
||||||
|
// Create and sign the kind 1 event
|
||||||
|
cJSON* signed_event = nostr_create_and_sign_event(
|
||||||
|
1, // kind 1 = text note
|
||||||
|
stats_text, // content = statistics
|
||||||
|
tags, // empty tags
|
||||||
|
relay_privkey_bytes, // relay's private key
|
||||||
|
time(NULL) // current timestamp
|
||||||
|
);
|
||||||
|
|
||||||
|
free(stats_text);
|
||||||
|
|
||||||
|
if (!signed_event) {
|
||||||
|
DEBUG_ERROR("Failed to create and sign status event");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store event in database (kind 1 events MUST be stored)
|
||||||
|
if (store_event(signed_event) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to store status event in database");
|
||||||
|
cJSON_Delete(signed_event);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broadcast event to subscriptions
|
||||||
|
broadcast_event_to_subscriptions(signed_event);
|
||||||
|
|
||||||
|
DEBUG_LOG("Successfully posted kind 1 status event");
|
||||||
|
cJSON_Delete(signed_event);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Forward declaration for known_configs (defined in config.c)
|
// Forward declaration for known_configs (defined in config.c)
|
||||||
typedef struct {
|
typedef struct {
|
||||||
const char* key;
|
const char* key;
|
||||||
@@ -565,6 +637,9 @@ static const config_definition_t known_configs[] = {
|
|||||||
{"relay_privkey", "string", 0, 65},
|
{"relay_privkey", "string", 0, 65},
|
||||||
{"admin_pubkey", "string", 0, 65},
|
{"admin_pubkey", "string", 0, 65},
|
||||||
|
|
||||||
|
// Kind 1 Status Posts
|
||||||
|
{"kind_1_status_posts_hours", "int", 0, 8760},
|
||||||
|
|
||||||
// Sentinel
|
// Sentinel
|
||||||
{NULL, NULL, 0, 0}
|
{NULL, NULL, 0, 0}
|
||||||
};
|
};
|
||||||
@@ -1314,6 +1389,9 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get timestamp delay configuration
|
||||||
|
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
||||||
|
|
||||||
// Create and sign gift wrap using library function
|
// Create and sign gift wrap using library function
|
||||||
cJSON* gift_wraps[1];
|
cJSON* gift_wraps[1];
|
||||||
int send_result = nostr_nip17_send_dm(
|
int send_result = nostr_nip17_send_dm(
|
||||||
@@ -1322,7 +1400,8 @@ int send_nip17_response(const char* sender_pubkey, const char* response_content,
|
|||||||
1, // num_recipients
|
1, // num_recipients
|
||||||
relay_privkey, // sender_private_key
|
relay_privkey, // sender_private_key
|
||||||
gift_wraps, // gift_wraps_out
|
gift_wraps, // gift_wraps_out
|
||||||
1 // max_gift_wraps
|
1, // max_gift_wraps
|
||||||
|
max_delay_sec // max_delay_sec
|
||||||
);
|
);
|
||||||
|
|
||||||
cJSON_Delete(dm_response);
|
cJSON_Delete(dm_response);
|
||||||
@@ -1419,12 +1498,37 @@ char* generate_config_text(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
offset += snprintf(config_text + offset, 8192 - offset,
|
offset += snprintf(config_text + offset, 8192 - offset, "\n");
|
||||||
"\n✅ Configuration retrieved successfully");
|
|
||||||
|
|
||||||
return config_text;
|
return config_text;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to format numbers with commas
|
||||||
|
void format_number_with_commas(long long num, char* buffer, size_t buffer_size) {
|
||||||
|
char temp[32];
|
||||||
|
int len = snprintf(temp, sizeof(temp), "%lld", num);
|
||||||
|
int comma_count = (len - 1) / 3;
|
||||||
|
int result_len = len + comma_count;
|
||||||
|
|
||||||
|
if (result_len >= (int)buffer_size) {
|
||||||
|
buffer[0] = '\0';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int j = result_len - 1;
|
||||||
|
int k = len - 1;
|
||||||
|
int comma_pos = 0;
|
||||||
|
|
||||||
|
while (k >= 0) {
|
||||||
|
buffer[j--] = temp[k--];
|
||||||
|
comma_pos++;
|
||||||
|
if (comma_pos % 3 == 0 && k >= 0) {
|
||||||
|
buffer[j--] = ',';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer[result_len] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
// Generate human-readable stats text
|
// Generate human-readable stats text
|
||||||
char* generate_stats_text(void) {
|
char* generate_stats_text(void) {
|
||||||
char* stats_json = generate_stats_json();
|
char* stats_json = generate_stats_json();
|
||||||
@@ -1450,7 +1554,7 @@ char* generate_stats_text(void) {
|
|||||||
cJSON* newest_event = cJSON_GetObjectItem(stats_obj, "latest_event_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* event_kinds = cJSON_GetObjectItem(stats_obj, "event_kinds");
|
||||||
cJSON* top_pubkeys = cJSON_GetObjectItem(stats_obj, "top_pubkeys");
|
// 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;
|
||||||
@@ -1491,25 +1595,23 @@ char* generate_stats_text(void) {
|
|||||||
|
|
||||||
// Header
|
// Header
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"📊 Relay Statistics\n"
|
"\nRelay Statistics\n"
|
||||||
"━━━━━━━━━━━━━━━━━━━━\n");
|
"━━━━━━━━━━━━━━━━━━━━\n");
|
||||||
|
|
||||||
// Database Overview section
|
// Database Overview section
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"Database Overview:\n"
|
|
||||||
"Metric\tValue\tDescription\n"
|
"Database Size %.2f MB (%lld bytes)\n"
|
||||||
"Database Size\t%.2f MB (%lld bytes)\tCurrent database file size\n"
|
"Total Events %lld\n"
|
||||||
"Total Events\t%lld\tTotal number of events stored\n"
|
"Active Subscriptions %d\n"
|
||||||
"Active Subscriptions\t%d\tCurrent active WebSocket subscriptions\n"
|
"Oldest Event %s\n"
|
||||||
"Oldest Event\t%s\tTimestamp of oldest event\n"
|
"Newest Event %s\n"
|
||||||
"Newest Event\t%s\tTimestamp of newest event\n"
|
"\n",
|
||||||
"\n",
|
db_mb, db_bytes, total, active_subs, oldest_str, newest_str);
|
||||||
db_mb, db_bytes, total, active_subs, oldest_str, newest_str);
|
|
||||||
|
|
||||||
// Event Kind Distribution section
|
// Event Kind Distribution section
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"Event Kind Distribution:\n"
|
"Event_Kind_Distribution:\n");
|
||||||
"Event Kind\tCount\tPercentage\n");
|
|
||||||
|
|
||||||
if (event_kinds && cJSON_IsArray(event_kinds)) {
|
if (event_kinds && cJSON_IsArray(event_kinds)) {
|
||||||
cJSON* kind_item = NULL;
|
cJSON* kind_item = NULL;
|
||||||
@@ -1519,11 +1621,45 @@ char* generate_stats_text(void) {
|
|||||||
cJSON* percentage = cJSON_GetObjectItem(kind_item, "percentage");
|
cJSON* percentage = cJSON_GetObjectItem(kind_item, "percentage");
|
||||||
|
|
||||||
if (kind && count && percentage) {
|
if (kind && count && percentage) {
|
||||||
|
// Format event kind (right-justified, minimum 5 chars wide with underscores)
|
||||||
|
char kind_str[16];
|
||||||
|
int kind_val = (int)cJSON_GetNumberValue(kind);
|
||||||
|
char temp_kind[16];
|
||||||
|
snprintf(temp_kind, sizeof(temp_kind), "%d", kind_val);
|
||||||
|
|
||||||
|
// Calculate padding needed (minimum 5 chars, but more if kind is longer)
|
||||||
|
int kind_len = strlen(temp_kind);
|
||||||
|
int min_width = 5;
|
||||||
|
int total_width = (kind_len > min_width) ? kind_len : min_width;
|
||||||
|
int padding = total_width - kind_len;
|
||||||
|
|
||||||
|
// Create padded string with underscores
|
||||||
|
memset(kind_str, '_', padding);
|
||||||
|
strcpy(kind_str + padding, temp_kind);
|
||||||
|
|
||||||
|
// Format count with commas (right-justified, 11 chars wide with underscores)
|
||||||
|
char count_str[16];
|
||||||
|
format_number_with_commas((long long)cJSON_GetNumberValue(count), count_str, sizeof(count_str));
|
||||||
|
char count_formatted[16];
|
||||||
|
snprintf(count_formatted, sizeof(count_formatted), "%11s", count_str);
|
||||||
|
// Replace spaces with underscores for alignment
|
||||||
|
for (int i = 0; count_formatted[i]; i++) {
|
||||||
|
if (count_formatted[i] == ' ') count_formatted[i] = '_';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format percentage (right-justified, 7 chars wide including %, with underscores)
|
||||||
|
double pct = cJSON_GetNumberValue(percentage);
|
||||||
|
char pct_str[8];
|
||||||
|
snprintf(pct_str, sizeof(pct_str), "%.1f%%", pct);
|
||||||
|
// Pad with underscores to make 7 chars total (right-justified)
|
||||||
|
char pct_formatted[8];
|
||||||
|
int pct_len = strlen(pct_str);
|
||||||
|
int underscores_needed = 7 - pct_len;
|
||||||
|
memset(pct_formatted, '_', underscores_needed);
|
||||||
|
strcpy(pct_formatted + underscores_needed, pct_str);
|
||||||
|
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"%lld\t%lld\t%.1f%%\n",
|
"%s%s_%s\n", kind_str, count_formatted, pct_formatted);
|
||||||
(long long)cJSON_GetNumberValue(kind),
|
|
||||||
(long long)cJSON_GetNumberValue(count),
|
|
||||||
cJSON_GetNumberValue(percentage));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1536,49 +1672,48 @@ char* generate_stats_text(void) {
|
|||||||
// Time-based Statistics section
|
// Time-based Statistics section
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"Time-based Statistics:\n"
|
"Time-based Statistics:\n"
|
||||||
"Period\tEvents\tDescription\n"
|
|
||||||
"Last 24 Hours\t%lld\tEvents in the last day\n"
|
"%lld\tEvents in the last day\n"
|
||||||
"Last 7 Days\t%lld\tEvents in the last week\n"
|
"%lld\tEvents in the last week\n"
|
||||||
"Last 30 Days\t%lld\tEvents in the last month\n"
|
"%lld\tEvents in the last month\n"
|
||||||
"\n",
|
"\n",
|
||||||
last_24h, last_7d, last_30d);
|
last_24h, last_7d, last_30d);
|
||||||
|
|
||||||
// Top Pubkeys section
|
// Top Pubkeys section
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
// offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"Top Pubkeys by Event Count:\n"
|
// "Top Pubkeys by Event Count:\n"
|
||||||
"Rank\tPubkey\tEvent Count\tPercentage\n");
|
// "Rank\tPubkey\tEvent Count\tPercentage\n");
|
||||||
|
|
||||||
if (top_pubkeys && cJSON_IsArray(top_pubkeys)) {
|
// if (top_pubkeys && cJSON_IsArray(top_pubkeys)) {
|
||||||
int rank = 1;
|
// int rank = 1;
|
||||||
cJSON* pubkey_item = NULL;
|
// cJSON* pubkey_item = NULL;
|
||||||
cJSON_ArrayForEach(pubkey_item, top_pubkeys) {
|
// cJSON_ArrayForEach(pubkey_item, top_pubkeys) {
|
||||||
cJSON* pubkey = cJSON_GetObjectItem(pubkey_item, "pubkey");
|
// cJSON* pubkey = cJSON_GetObjectItem(pubkey_item, "pubkey");
|
||||||
cJSON* event_count = cJSON_GetObjectItem(pubkey_item, "event_count");
|
// cJSON* event_count = cJSON_GetObjectItem(pubkey_item, "event_count");
|
||||||
cJSON* percentage = cJSON_GetObjectItem(pubkey_item, "percentage");
|
// cJSON* percentage = cJSON_GetObjectItem(pubkey_item, "percentage");
|
||||||
|
|
||||||
if (pubkey && event_count && percentage) {
|
// if (pubkey && event_count && percentage) {
|
||||||
const char* pubkey_str = cJSON_GetStringValue(pubkey);
|
// const char* pubkey_str = cJSON_GetStringValue(pubkey);
|
||||||
char short_pubkey[20] = "...";
|
// char short_pubkey[20] = "...";
|
||||||
if (pubkey_str && strlen(pubkey_str) >= 16) {
|
// if (pubkey_str && strlen(pubkey_str) >= 16) {
|
||||||
snprintf(short_pubkey, sizeof(short_pubkey), "%.16s...", pubkey_str);
|
// snprintf(short_pubkey, sizeof(short_pubkey), "%.16s...", pubkey_str);
|
||||||
}
|
// }
|
||||||
|
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
// offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"%d\t%s\t%lld\t%.1f%%\n",
|
// "%d\t%s\t%lld\t%.1f%%\n",
|
||||||
rank++,
|
// rank++,
|
||||||
short_pubkey,
|
// short_pubkey,
|
||||||
(long long)cJSON_GetNumberValue(event_count),
|
// (long long)cJSON_GetNumberValue(event_count),
|
||||||
cJSON_GetNumberValue(percentage));
|
// cJSON_GetNumberValue(percentage));
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
// offset += snprintf(stats_text + offset, 16384 - offset,
|
||||||
"No pubkey data available\n");
|
// "No pubkey data available\n");
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Footer
|
// Footer
|
||||||
offset += snprintf(stats_text + offset, 16384 - offset,
|
offset += snprintf(stats_text + offset, 16384 - offset, "\n");
|
||||||
"\n✅ Statistics retrieved successfully");
|
|
||||||
|
|
||||||
cJSON_Delete(stats_obj);
|
cJSON_Delete(stats_obj);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -64,4 +64,7 @@ void monitoring_on_event_stored(void);
|
|||||||
void monitoring_on_subscription_change(void);
|
void monitoring_on_subscription_change(void);
|
||||||
int get_monitoring_throttle_seconds(void);
|
int get_monitoring_throttle_seconds(void);
|
||||||
|
|
||||||
|
// Kind 1 status posts
|
||||||
|
int generate_and_post_status_event(void);
|
||||||
|
|
||||||
#endif // API_H
|
#endif // API_H
|
||||||
19
src/config.c
@@ -9,6 +9,9 @@
|
|||||||
#ifdef VERSION
|
#ifdef VERSION
|
||||||
#undef VERSION
|
#undef VERSION
|
||||||
#endif
|
#endif
|
||||||
|
#ifdef VERSION_MAJOR
|
||||||
|
#undef VERSION_MAJOR
|
||||||
|
#endif
|
||||||
#ifdef VERSION_MINOR
|
#ifdef VERSION_MINOR
|
||||||
#undef VERSION_MINOR
|
#undef VERSION_MINOR
|
||||||
#endif
|
#endif
|
||||||
@@ -1144,6 +1147,20 @@ static int validate_config_field(const char* key, const char* value, char* error
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NIP-59 Gift Wrap Timestamp Configuration
|
||||||
|
if (strcmp(key, "nip59_timestamp_max_delay_sec") == 0) {
|
||||||
|
if (!is_valid_non_negative_integer(value)) {
|
||||||
|
snprintf(error_msg, error_size, "invalid nip59_timestamp_max_delay_sec '%s' (must be non-negative integer)", value);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
long val = strtol(value, NULL, 10);
|
||||||
|
if (val > 604800) { // Max 7 days
|
||||||
|
snprintf(error_msg, error_size, "nip59_timestamp_max_delay_sec '%s' too large (max 604800 seconds = 7 days)", value);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(key, "nip42_auth_required_kinds") == 0) {
|
if (strcmp(key, "nip42_auth_required_kinds") == 0) {
|
||||||
// Validate comma-separated list of kind numbers
|
// Validate comma-separated list of kind numbers
|
||||||
@@ -2538,7 +2555,7 @@ int handle_kind_23456_unified(cJSON* event, char* error_message, size_t error_si
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Perform NIP-44 decryption (relay as recipient, admin as sender)
|
// Perform NIP-44 decryption (relay as recipient, admin as sender)
|
||||||
char decrypted_text[4096]; // Buffer for decrypted content
|
char decrypted_text[16384]; // Buffer for decrypted content (16KB)
|
||||||
int decrypt_result = nostr_nip44_decrypt(relay_privkey_bytes, sender_pubkey_bytes, content, decrypted_text, sizeof(decrypted_text));
|
int decrypt_result = nostr_nip44_decrypt(relay_privkey_bytes, sender_pubkey_bytes, content, decrypted_text, sizeof(decrypted_text));
|
||||||
|
|
||||||
// Clean up private key immediately after use
|
// Clean up private key immediately after use
|
||||||
|
|||||||
@@ -78,7 +78,13 @@ static const struct {
|
|||||||
// Trust proxy headers (X-Forwarded-For, X-Real-IP) for accurate client IP detection
|
// Trust proxy headers (X-Forwarded-For, X-Real-IP) for accurate client IP detection
|
||||||
// Safe for informational/debugging use. Only becomes a security concern if you implement
|
// Safe for informational/debugging use. Only becomes a security concern if you implement
|
||||||
// IP-based rate limiting or access control (which would require firewall protection anyway)
|
// IP-based rate limiting or access control (which would require firewall protection anyway)
|
||||||
{"trust_proxy_headers", "true"}
|
{"trust_proxy_headers", "true"},
|
||||||
|
|
||||||
|
// NIP-59 Gift Wrap Timestamp Configuration
|
||||||
|
{"nip59_timestamp_max_delay_sec", "0"},
|
||||||
|
|
||||||
|
// Kind 1 Status Posts
|
||||||
|
{"kind_1_status_posts_hours", "1"}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Number of default configuration values
|
// Number of default configuration values
|
||||||
|
|||||||
BIN
src/default_config_event.h.gch
Normal file
@@ -368,13 +368,17 @@ cJSON* process_nip17_admin_message(cJSON* gift_wrap_event, char* error_message,
|
|||||||
|
|
||||||
if (success_dm) {
|
if (success_dm) {
|
||||||
cJSON* success_gift_wraps[1];
|
cJSON* success_gift_wraps[1];
|
||||||
|
// Get timestamp delay configuration
|
||||||
|
long max_delay_sec = get_config_int("nip59_timestamp_max_delay_sec", 0);
|
||||||
|
|
||||||
int send_result = nostr_nip17_send_dm(
|
int send_result = nostr_nip17_send_dm(
|
||||||
success_dm, // dm_event
|
success_dm, // dm_event
|
||||||
(const char**)&sender_pubkey, // recipient_pubkeys
|
(const char**)&sender_pubkey, // recipient_pubkeys
|
||||||
1, // num_recipients
|
1, // num_recipients
|
||||||
relay_privkey, // sender_private_key
|
relay_privkey, // sender_private_key
|
||||||
success_gift_wraps, // gift_wraps_out
|
success_gift_wraps, // gift_wraps_out
|
||||||
1 // max_gift_wraps
|
1, // max_gift_wraps
|
||||||
|
max_delay_sec // max_delay_sec
|
||||||
);
|
);
|
||||||
|
|
||||||
cJSON_Delete(success_dm);
|
cJSON_Delete(success_dm);
|
||||||
@@ -565,6 +569,35 @@ int process_nip17_admin_command(cJSON* dm_event, char* error_message, size_t err
|
|||||||
DEBUG_INFO("DM_ADMIN: Config command processed successfully");
|
DEBUG_INFO("DM_ADMIN: Config command processed successfully");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
// Check for status commands
|
||||||
|
else if (strstr(content_lower, "status") != NULL) {
|
||||||
|
DEBUG_INFO("DM_ADMIN: Processing status command");
|
||||||
|
|
||||||
|
// Create synthetic event for system_command handler
|
||||||
|
cJSON* synthetic_event = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(synthetic_event, "kind", 23456);
|
||||||
|
cJSON_AddStringToObject(synthetic_event, "pubkey", sender_pubkey);
|
||||||
|
|
||||||
|
// Create tags array with system_command
|
||||||
|
cJSON* tags = cJSON_CreateArray();
|
||||||
|
cJSON* cmd_tag = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(cmd_tag, cJSON_CreateString("system_command"));
|
||||||
|
cJSON_AddItemToArray(cmd_tag, cJSON_CreateString("system_status"));
|
||||||
|
cJSON_AddItemToArray(tags, cmd_tag);
|
||||||
|
cJSON_AddItemToObject(synthetic_event, "tags", tags);
|
||||||
|
|
||||||
|
char error_msg[256];
|
||||||
|
int result = handle_system_command_unified(synthetic_event, "system_status", error_msg, sizeof(error_msg), wsi);
|
||||||
|
cJSON_Delete(synthetic_event);
|
||||||
|
|
||||||
|
if (result != 0) {
|
||||||
|
DEBUG_ERROR(error_msg);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_INFO("DM_ADMIN: Status command processed successfully");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
DEBUG_INFO("DM_ADMIN: Checking for confirmation or config change requests");
|
DEBUG_INFO("DM_ADMIN: Checking for confirmation or config change requests");
|
||||||
// Check if it's a confirmation response (yes/no)
|
// Check if it's a confirmation response (yes/no)
|
||||||
|
|||||||
12
src/main.h
@@ -10,10 +10,14 @@
|
|||||||
#define MAIN_H
|
#define MAIN_H
|
||||||
|
|
||||||
// Version information (auto-updated by build system)
|
// Version information (auto-updated by build system)
|
||||||
#define VERSION "v0.7.42"
|
#define VERSION_MAJOR 1
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MINOR 0
|
||||||
#define VERSION_MINOR 7
|
#define VERSION_PATCH 5
|
||||||
#define VERSION_PATCH 42
|
#define VERSION "v1.0.5"
|
||||||
|
|
||||||
|
// Avoid VERSION_MAJOR redefinition warning from nostr_core_lib
|
||||||
|
#undef VERSION_MAJOR
|
||||||
|
#define VERSION_MAJOR 1
|
||||||
|
|
||||||
// Relay metadata (authoritative source for NIP-11 information)
|
// Relay metadata (authoritative source for NIP-11 information)
|
||||||
#define RELAY_NAME "C-Relay"
|
#define RELAY_NAME "C-Relay"
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
/* Embedded SQL Schema for C Nostr Relay
|
/* Embedded SQL Schema for C Nostr Relay
|
||||||
* Generated from db/schema.sql - Do not edit manually
|
|
||||||
* Schema Version: 8
|
* Schema Version: 8
|
||||||
*/
|
*/
|
||||||
#ifndef SQL_SCHEMA_H
|
#ifndef SQL_SCHEMA_H
|
||||||
@@ -242,7 +241,8 @@ SELECT\n\
|
|||||||
s.filter_json,\n\
|
s.filter_json,\n\
|
||||||
s.events_sent,\n\
|
s.events_sent,\n\
|
||||||
s.created_at,\n\
|
s.created_at,\n\
|
||||||
(strftime('%s', 'now') - s.created_at) as duration_seconds\n\
|
(strftime('%s', 'now') - s.created_at) as duration_seconds,\n\
|
||||||
|
s.wsi_pointer\n\
|
||||||
FROM subscriptions s\n\
|
FROM subscriptions s\n\
|
||||||
WHERE s.event_type = 'created'\n\
|
WHERE s.event_type = 'created'\n\
|
||||||
AND NOT EXISTS (\n\
|
AND NOT EXISTS (\n\
|
||||||
|
|||||||
772
src/websockets.c
@@ -44,6 +44,9 @@ void send_nip42_auth_challenge(struct lws* wsi, struct per_session_data* pss);
|
|||||||
void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event);
|
void handle_nip42_auth_signed_event(struct lws* wsi, struct per_session_data* pss, cJSON* auth_event);
|
||||||
void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge);
|
void handle_nip42_auth_challenge_response(struct lws* wsi, struct per_session_data* pss, const char* challenge);
|
||||||
|
|
||||||
|
// Forward declaration for status posts
|
||||||
|
int generate_and_post_status_event(void);
|
||||||
|
|
||||||
// Forward declarations for NIP-11 relay information handling
|
// Forward declarations for NIP-11 relay information handling
|
||||||
int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
|
int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
|
||||||
|
|
||||||
@@ -282,7 +285,19 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
|
|
||||||
// Check if this is a GET request to the root path
|
// Check if this is a GET request to the root path
|
||||||
if (strcmp(requested_uri, "/") == 0) {
|
if (strcmp(requested_uri, "/") == 0) {
|
||||||
// Get Accept header
|
// Check if this is a WebSocket upgrade request
|
||||||
|
char upgrade_header[64] = {0};
|
||||||
|
int upgrade_len = lws_hdr_copy(wsi, upgrade_header, sizeof(upgrade_header) - 1, WSI_TOKEN_UPGRADE);
|
||||||
|
|
||||||
|
if (upgrade_len > 0) {
|
||||||
|
upgrade_header[upgrade_len] = '\0';
|
||||||
|
if (strstr(upgrade_header, "websocket") != NULL) {
|
||||||
|
DEBUG_LOG("WebSocket upgrade request - allowing connection");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not a WebSocket upgrade, check for NIP-11 request
|
||||||
char accept_header[256] = {0};
|
char accept_header[256] = {0};
|
||||||
int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT);
|
int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT);
|
||||||
|
|
||||||
@@ -300,7 +315,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Root path without NIP-11 Accept header - return 404
|
// Root path without NIP-11 Accept header and not WebSocket - return 404
|
||||||
|
DEBUG_WARN("Rejecting root path request - not WebSocket upgrade and not NIP-11");
|
||||||
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
|
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -455,18 +471,720 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
|
|
||||||
case LWS_CALLBACK_RECEIVE:
|
case LWS_CALLBACK_RECEIVE:
|
||||||
if (len > 0) {
|
if (len > 0) {
|
||||||
|
DEBUG_TRACE("LWS_CALLBACK_RECEIVE: received %zu bytes", len);
|
||||||
|
|
||||||
// Check if client is rate limited for malformed requests
|
// Check if client is rate limited for malformed requests
|
||||||
if (is_client_rate_limited_for_malformed_requests(pss)) {
|
if (is_client_rate_limited_for_malformed_requests(pss)) {
|
||||||
send_notice_message(wsi, "error: too many malformed requests - temporarily blocked");
|
send_notice_message(wsi, "error: too many malformed requests - temporarily blocked");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if this is a fragmented message
|
||||||
|
int is_first_fragment = lws_is_first_fragment(wsi);
|
||||||
|
int is_final_fragment = lws_is_final_fragment(wsi);
|
||||||
|
size_t remaining_payload = lws_remaining_packet_payload(wsi);
|
||||||
|
|
||||||
|
DEBUG_TRACE("Fragment info: first=%d, final=%d, remaining=%zu, reassembly_active=%d",
|
||||||
|
is_first_fragment, is_final_fragment, remaining_payload, pss->reassembly_active);
|
||||||
|
|
||||||
|
// Handle message reassembly for fragmented messages
|
||||||
|
// Only use reassembly if message is actually fragmented (not both first and final)
|
||||||
|
int is_fragmented = (is_first_fragment && !is_final_fragment) || pss->reassembly_active;
|
||||||
|
if (is_fragmented) {
|
||||||
|
// Start or continue reassembly
|
||||||
|
if (is_first_fragment) {
|
||||||
|
// First fragment - initialize reassembly buffer
|
||||||
|
if (pss->reassembly_buffer) {
|
||||||
|
DEBUG_WARN("Starting new reassembly but buffer already exists - cleaning up");
|
||||||
|
free(pss->reassembly_buffer);
|
||||||
|
pss->reassembly_buffer = NULL; // Ensure buffer is NULL after cleanup
|
||||||
|
}
|
||||||
|
pss->reassembly_size = 0;
|
||||||
|
pss->reassembly_capacity = 0;
|
||||||
|
pss->reassembly_active = 1;
|
||||||
|
DEBUG_TRACE("Starting message reassembly");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure buffer has enough capacity
|
||||||
|
size_t needed_capacity = pss->reassembly_size + len + 1; // +1 for null terminator
|
||||||
|
if (needed_capacity > pss->reassembly_capacity) {
|
||||||
|
size_t new_capacity = pss->reassembly_capacity == 0 ? 8192 : pss->reassembly_capacity * 2;
|
||||||
|
while (new_capacity < needed_capacity) {
|
||||||
|
new_capacity *= 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* new_buffer = realloc(pss->reassembly_buffer, new_capacity);
|
||||||
|
if (!new_buffer) {
|
||||||
|
DEBUG_ERROR("Failed to allocate reassembly buffer (capacity %zu)", new_capacity);
|
||||||
|
// Clean up and abort reassembly
|
||||||
|
free(pss->reassembly_buffer);
|
||||||
|
pss->reassembly_buffer = NULL;
|
||||||
|
pss->reassembly_size = 0;
|
||||||
|
pss->reassembly_capacity = 0;
|
||||||
|
pss->reassembly_active = 0;
|
||||||
|
send_notice_message(wsi, "error: message too large - memory allocation failed");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
pss->reassembly_buffer = new_buffer;
|
||||||
|
pss->reassembly_capacity = new_capacity;
|
||||||
|
DEBUG_TRACE("Expanded reassembly buffer to %zu bytes", new_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append fragment to buffer
|
||||||
|
memcpy(pss->reassembly_buffer + pss->reassembly_size, in, len);
|
||||||
|
pss->reassembly_size += len;
|
||||||
|
|
||||||
|
// Check if this is the final fragment
|
||||||
|
if (is_final_fragment) {
|
||||||
|
// Message complete - process it
|
||||||
|
pss->reassembly_buffer[pss->reassembly_size] = '\0';
|
||||||
|
pss->reassembly_active = 0;
|
||||||
|
|
||||||
|
DEBUG_TRACE("Message reassembly complete: total size %zu bytes", pss->reassembly_size);
|
||||||
|
|
||||||
|
// Process the complete message
|
||||||
|
char* complete_message = pss->reassembly_buffer;
|
||||||
|
size_t message_len = pss->reassembly_size;
|
||||||
|
|
||||||
|
// Reset reassembly state (keep buffer pointer for processing)
|
||||||
|
pss->reassembly_size = 0;
|
||||||
|
pss->reassembly_active = 0;
|
||||||
|
|
||||||
|
// Parse JSON message
|
||||||
|
DEBUG_TRACE("Parsing reassembled JSON message of length %zu", message_len);
|
||||||
|
cJSON* json = cJSON_Parse(complete_message);
|
||||||
|
|
||||||
|
// Process the message (same logic as before)
|
||||||
|
if (json && cJSON_IsArray(json)) {
|
||||||
|
// Get message type
|
||||||
|
cJSON* type = cJSON_GetArrayItem(json, 0);
|
||||||
|
if (type && cJSON_IsString(type)) {
|
||||||
|
const char* msg_type = cJSON_GetStringValue(type);
|
||||||
|
|
||||||
|
if (strcmp(msg_type, "EVENT") == 0) {
|
||||||
|
// Handle EVENT message
|
||||||
|
cJSON* event = cJSON_GetArrayItem(json, 1);
|
||||||
|
if (event && cJSON_IsObject(event)) {
|
||||||
|
// Extract event JSON string for unified validator
|
||||||
|
char *event_json_str = cJSON_Print(event);
|
||||||
|
if (!event_json_str) {
|
||||||
|
DEBUG_ERROR("Failed to serialize event JSON for validation");
|
||||||
|
cJSON* error_response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateString("OK"));
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateString("unknown"));
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateBool(0));
|
||||||
|
cJSON_AddItemToArray(error_response, cJSON_CreateString("error: failed to process event"));
|
||||||
|
|
||||||
|
char *error_str = cJSON_Print(error_response);
|
||||||
|
if (error_str) {
|
||||||
|
size_t error_len = strlen(error_str);
|
||||||
|
// Use proper message queue system instead of direct lws_write
|
||||||
|
if (queue_message(wsi, pss, error_str, error_len, LWS_WRITE_TEXT) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to queue error response message");
|
||||||
|
}
|
||||||
|
free(error_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(error_response);
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call unified validator with JSON string
|
||||||
|
size_t event_json_len = strlen(event_json_str);
|
||||||
|
int validation_result = nostr_validate_unified_request(event_json_str, event_json_len);
|
||||||
|
|
||||||
|
// Map validation result to old result format (0 = success, -1 = failure)
|
||||||
|
int result = (validation_result == NOSTR_SUCCESS) ? 0 : -1;
|
||||||
|
|
||||||
|
// Generate error message based on validation result
|
||||||
|
char error_message[512] = {0};
|
||||||
|
if (result != 0) {
|
||||||
|
switch (validation_result) {
|
||||||
|
case NOSTR_ERROR_INVALID_INPUT:
|
||||||
|
strncpy(error_message, "invalid: malformed event structure", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_EVENT_INVALID_SIGNATURE:
|
||||||
|
strncpy(error_message, "invalid: signature verification failed", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_EVENT_INVALID_ID:
|
||||||
|
strncpy(error_message, "invalid: event id verification failed", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case NOSTR_ERROR_EVENT_INVALID_PUBKEY:
|
||||||
|
strncpy(error_message, "invalid: invalid pubkey format", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case -103: // NOSTR_ERROR_EVENT_EXPIRED
|
||||||
|
strncpy(error_message, "rejected: event expired", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case -102: // NOSTR_ERROR_NIP42_DISABLED
|
||||||
|
strncpy(error_message, "auth-required: NIP-42 authentication required", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
case -101: // NOSTR_ERROR_AUTH_REQUIRED
|
||||||
|
strncpy(error_message, "blocked: pubkey not authorized", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
strncpy(error_message, "error: validation failed", sizeof(error_message) - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cleanup event JSON string
|
||||||
|
free(event_json_str);
|
||||||
|
|
||||||
|
// Check for NIP-70 protected events
|
||||||
|
if (result == 0) {
|
||||||
|
// Check if event has protected tag ["-"]
|
||||||
|
int is_protected_event = 0;
|
||||||
|
cJSON* tags = cJSON_GetObjectItem(event, "tags");
|
||||||
|
if (tags && cJSON_IsArray(tags)) {
|
||||||
|
cJSON* tag = NULL;
|
||||||
|
cJSON_ArrayForEach(tag, tags) {
|
||||||
|
if (cJSON_IsArray(tag) && cJSON_GetArraySize(tag) >= 1) {
|
||||||
|
cJSON* tag_name = cJSON_GetArrayItem(tag, 0);
|
||||||
|
if (tag_name && cJSON_IsString(tag_name) &&
|
||||||
|
strcmp(cJSON_GetStringValue(tag_name), "-") == 0) {
|
||||||
|
is_protected_event = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_protected_event) {
|
||||||
|
// Check if protected events are enabled using config
|
||||||
|
int protected_events_enabled = get_config_bool("nip70_protected_events_enabled", 0);
|
||||||
|
|
||||||
|
if (!protected_events_enabled) {
|
||||||
|
// Protected events not supported
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "blocked: protected events not supported", sizeof(error_message) - 1);
|
||||||
|
error_message[sizeof(error_message) - 1] = '\0';
|
||||||
|
DEBUG_WARN("Protected event rejected: protected events not enabled");
|
||||||
|
} else {
|
||||||
|
// Protected events enabled - check authentication
|
||||||
|
cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey");
|
||||||
|
const char* event_pubkey = pubkey_obj ? cJSON_GetStringValue(pubkey_obj) : NULL;
|
||||||
|
|
||||||
|
if (!pss || !pss->authenticated ||
|
||||||
|
!event_pubkey || strcmp(pss->authenticated_pubkey, event_pubkey) != 0) {
|
||||||
|
// Not authenticated or pubkey mismatch
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "auth-required: protected event requires authentication", sizeof(error_message) - 1);
|
||||||
|
error_message[sizeof(error_message) - 1] = '\0';
|
||||||
|
DEBUG_WARN("Protected event rejected: authentication required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for admin events (kind 23456) and intercept them
|
||||||
|
if (result == 0) {
|
||||||
|
cJSON* kind_obj = cJSON_GetObjectItem(event, "kind");
|
||||||
|
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||||
|
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||||
|
|
||||||
|
DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, message_len);
|
||||||
|
|
||||||
|
// Log reception of Kind 23456 events
|
||||||
|
if (event_kind == 23456) {
|
||||||
|
DEBUG_LOG("Admin event (kind 23456) received");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event_kind == 23456) {
|
||||||
|
// Enhanced admin event security - check authorization first
|
||||||
|
|
||||||
|
char auth_error[512] = {0};
|
||||||
|
int auth_result = is_authorized_admin_event(event, auth_error, sizeof(auth_error));
|
||||||
|
|
||||||
|
if (auth_result != 0) {
|
||||||
|
// Authorization failed - log and reject
|
||||||
|
DEBUG_WARN("Admin event authorization failed");
|
||||||
|
result = -1;
|
||||||
|
size_t error_len = strlen(auth_error);
|
||||||
|
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||||
|
memcpy(error_message, auth_error, copy_len);
|
||||||
|
error_message[copy_len] = '\0';
|
||||||
|
} else {
|
||||||
|
// Authorization successful - process through admin API
|
||||||
|
|
||||||
|
char admin_error[512] = {0};
|
||||||
|
int admin_result = process_admin_event_in_config(event, admin_error, sizeof(admin_error), wsi);
|
||||||
|
|
||||||
|
// Log results for Kind 23456 events
|
||||||
|
if (event_kind == 23456) {
|
||||||
|
if (admin_result != 0) {
|
||||||
|
char error_result_msg[512];
|
||||||
|
if (strlen(admin_error) > 0) {
|
||||||
|
// Safely truncate admin_error if too long
|
||||||
|
size_t max_error_len = sizeof(error_result_msg) - 50; // Leave room for prefix
|
||||||
|
size_t error_len = strlen(admin_error);
|
||||||
|
if (error_len > max_error_len) {
|
||||||
|
error_len = max_error_len;
|
||||||
|
}
|
||||||
|
char truncated_error[512];
|
||||||
|
memcpy(truncated_error, admin_error, error_len);
|
||||||
|
truncated_error[error_len] = '\0';
|
||||||
|
|
||||||
|
// Use a safer approach to avoid truncation warning
|
||||||
|
size_t prefix_len = snprintf(error_result_msg, sizeof(error_result_msg),
|
||||||
|
"ERROR: Kind %d event processing failed: ", event_kind);
|
||||||
|
if (prefix_len < sizeof(error_result_msg)) {
|
||||||
|
size_t remaining = sizeof(error_result_msg) - prefix_len;
|
||||||
|
size_t copy_len = strlen(truncated_error);
|
||||||
|
if (copy_len >= remaining) {
|
||||||
|
copy_len = remaining - 1;
|
||||||
|
}
|
||||||
|
memcpy(error_result_msg + prefix_len, truncated_error, copy_len);
|
||||||
|
error_result_msg[prefix_len + copy_len] = '\0';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
snprintf(error_result_msg, sizeof(error_result_msg),
|
||||||
|
"ERROR: Kind %d event processing failed", event_kind);
|
||||||
|
}
|
||||||
|
DEBUG_ERROR(error_result_msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (admin_result != 0) {
|
||||||
|
DEBUG_ERROR("Failed to process admin event");
|
||||||
|
result = -1;
|
||||||
|
size_t error_len = strlen(admin_error);
|
||||||
|
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||||
|
memcpy(error_message, admin_error, copy_len);
|
||||||
|
error_message[copy_len] = '\0';
|
||||||
|
} else {
|
||||||
|
// Admin events are processed by the admin API, not broadcast to subscriptions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event_kind == 1059) {
|
||||||
|
// Check for NIP-17 gift wrap admin messages
|
||||||
|
|
||||||
|
char nip17_error[512] = {0};
|
||||||
|
cJSON* response_event = process_nip17_admin_message(event, nip17_error, sizeof(nip17_error), wsi);
|
||||||
|
|
||||||
|
if (!response_event) {
|
||||||
|
// Check if this is an error or if the command was already handled
|
||||||
|
if (strlen(nip17_error) > 0) {
|
||||||
|
// There was an actual error
|
||||||
|
DEBUG_ERROR("NIP-17 admin message processing failed");
|
||||||
|
result = -1;
|
||||||
|
size_t error_len = strlen(nip17_error);
|
||||||
|
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||||
|
memcpy(error_message, nip17_error, copy_len);
|
||||||
|
error_message[copy_len] = '\0';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// No error message means the command was already handled (plain text commands)
|
||||||
|
// Store the original gift wrap event in database
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to store gift wrap event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Store the original gift wrap event in database (unlike kind 23456)
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to store gift wrap event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store gift wrap event", sizeof(error_message) - 1);
|
||||||
|
cJSON_Delete(response_event);
|
||||||
|
} else {
|
||||||
|
// Broadcast RESPONSE event to matching persistent subscriptions
|
||||||
|
broadcast_event_to_subscriptions(response_event);
|
||||||
|
|
||||||
|
// Clean up response event
|
||||||
|
cJSON_Delete(response_event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event_kind == 14) {
|
||||||
|
// Check for DM stats commands addressed to relay
|
||||||
|
|
||||||
|
char dm_error[512] = {0};
|
||||||
|
int dm_result = process_dm_stats_command(event, dm_error, sizeof(dm_error), wsi);
|
||||||
|
|
||||||
|
if (dm_result != 0) {
|
||||||
|
DEBUG_ERROR("DM stats command processing failed");
|
||||||
|
result = -1;
|
||||||
|
size_t error_len = strlen(dm_error);
|
||||||
|
size_t copy_len = (error_len < sizeof(error_message) - 1) ? error_len : sizeof(error_message) - 1;
|
||||||
|
memcpy(error_message, dm_error, copy_len);
|
||||||
|
error_message[copy_len] = '\0';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Store the DM event in database
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to store DM event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store DM event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
// Broadcast DM event to matching persistent subscriptions
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Check if this is an ephemeral event (kinds 20000-29999)
|
||||||
|
// Per NIP-01: ephemeral events are broadcast but never stored
|
||||||
|
if (event_kind >= 20000 && event_kind < 30000) {
|
||||||
|
DEBUG_TRACE("Ephemeral event (kind %d) - broadcasting without storage", event_kind);
|
||||||
|
// Broadcast directly to subscriptions without database storage
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
} else {
|
||||||
|
DEBUG_TRACE("Storing regular event in database");
|
||||||
|
// Regular event - store in database and broadcast
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to store event in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
DEBUG_LOG("Event stored and broadcast (kind %d)", event_kind);
|
||||||
|
// Broadcast event to matching persistent subscriptions
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Event without valid kind - try normal storage
|
||||||
|
DEBUG_WARN("Event without valid kind - trying normal storage");
|
||||||
|
if (store_event(event) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to store event without kind in database");
|
||||||
|
result = -1;
|
||||||
|
strncpy(error_message, "error: failed to store event", sizeof(error_message) - 1);
|
||||||
|
} else {
|
||||||
|
broadcast_event_to_subscriptions(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send OK response
|
||||||
|
cJSON* event_id = cJSON_GetObjectItem(event, "id");
|
||||||
|
if (event_id && cJSON_IsString(event_id)) {
|
||||||
|
cJSON* response = cJSON_CreateArray();
|
||||||
|
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(strlen(error_message) > 0 ? error_message : ""));
|
||||||
|
|
||||||
|
char *response_str = cJSON_Print(response);
|
||||||
|
if (response_str) {
|
||||||
|
size_t response_len = strlen(response_str);
|
||||||
|
|
||||||
|
// DEBUG: Log WebSocket frame details before sending
|
||||||
|
DEBUG_TRACE("WS_FRAME_SEND: type=OK len=%zu data=%.100s%s",
|
||||||
|
response_len,
|
||||||
|
response_str,
|
||||||
|
response_len > 100 ? "..." : "");
|
||||||
|
|
||||||
|
// Queue message for proper libwebsockets pattern
|
||||||
|
if (queue_message(wsi, pss, response_str, response_len, LWS_WRITE_TEXT) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to queue OK response message");
|
||||||
|
}
|
||||||
|
|
||||||
|
free(response_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "REQ") == 0) {
|
||||||
|
DEBUG_TRACE("REQ message received, starting processing");
|
||||||
|
|
||||||
|
// Check NIP-42 authentication for REQ subscriptions if required
|
||||||
|
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
||||||
|
DEBUG_TRACE("REQ rejected: NIP-42 authentication required");
|
||||||
|
if (!pss->auth_challenge_sent) {
|
||||||
|
send_nip42_auth_challenge(wsi, pss);
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "NIP-42 authentication required for subscriptions");
|
||||||
|
DEBUG_WARN("REQ rejected: NIP-42 authentication required");
|
||||||
|
}
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ message passed authentication check");
|
||||||
|
|
||||||
|
// Handle REQ message
|
||||||
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
|
|
||||||
|
if (sub_id && cJSON_IsString(sub_id)) {
|
||||||
|
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||||
|
|
||||||
|
DEBUG_TRACE("Processing REQ message for subscription %s", subscription_id);
|
||||||
|
|
||||||
|
// Validate subscription ID before processing
|
||||||
|
if (!subscription_id) {
|
||||||
|
DEBUG_TRACE("REQ rejected: NULL subscription ID");
|
||||||
|
send_notice_message(wsi, "error: invalid subscription ID");
|
||||||
|
DEBUG_WARN("REQ rejected: NULL subscription ID");
|
||||||
|
record_malformed_request(pss);
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate subscription ID
|
||||||
|
if (!validate_subscription_id(subscription_id)) {
|
||||||
|
DEBUG_TRACE("REQ rejected: invalid subscription ID format");
|
||||||
|
send_notice_message(wsi, "error: invalid subscription ID");
|
||||||
|
DEBUG_WARN("REQ rejected: invalid subscription ID");
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id);
|
||||||
|
|
||||||
|
// Create array of filter objects from position 2 onwards
|
||||||
|
cJSON* filters = cJSON_CreateArray();
|
||||||
|
if (!filters) {
|
||||||
|
DEBUG_TRACE("REQ failed: could not create filters array");
|
||||||
|
send_notice_message(wsi, "error: failed to process filters");
|
||||||
|
DEBUG_ERROR("REQ failed: could not create filters array");
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int json_size = cJSON_GetArraySize(json);
|
||||||
|
int filter_count = 0;
|
||||||
|
for (int i = 2; i < json_size; i++) {
|
||||||
|
cJSON* filter = cJSON_GetArrayItem(json, i);
|
||||||
|
if (filter) {
|
||||||
|
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
||||||
|
filter_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ created %d filters from message", filter_count);
|
||||||
|
|
||||||
|
// Validate filters before processing
|
||||||
|
char filter_error[512] = {0};
|
||||||
|
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
|
||||||
|
DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error);
|
||||||
|
send_notice_message(wsi, filter_error);
|
||||||
|
DEBUG_WARN("REQ rejected: invalid filters");
|
||||||
|
record_malformed_request(pss);
|
||||||
|
cJSON_Delete(filters);
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ filters validated successfully");
|
||||||
|
|
||||||
|
DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id);
|
||||||
|
handle_req_message(subscription_id, filters, wsi, pss);
|
||||||
|
DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id);
|
||||||
|
|
||||||
|
// Clean up the filters array we created
|
||||||
|
cJSON_Delete(filters);
|
||||||
|
|
||||||
|
DEBUG_LOG("REQ subscription %s processed, sending EOSE", subscription_id);
|
||||||
|
|
||||||
|
// Send EOSE (End of Stored Events)
|
||||||
|
cJSON* eose_response = cJSON_CreateArray();
|
||||||
|
if (eose_response) {
|
||||||
|
cJSON_AddItemToArray(eose_response, cJSON_CreateString("EOSE"));
|
||||||
|
cJSON_AddItemToArray(eose_response, cJSON_CreateString(subscription_id));
|
||||||
|
|
||||||
|
char *eose_str = cJSON_Print(eose_response);
|
||||||
|
if (eose_str) {
|
||||||
|
size_t eose_len = strlen(eose_str);
|
||||||
|
|
||||||
|
// DEBUG: Log WebSocket frame details before sending
|
||||||
|
DEBUG_TRACE("WS_FRAME_SEND: type=EOSE len=%zu data=%.100s%s",
|
||||||
|
eose_len,
|
||||||
|
eose_str,
|
||||||
|
eose_len > 100 ? "..." : "");
|
||||||
|
|
||||||
|
// Queue message for proper libwebsockets pattern
|
||||||
|
if (queue_message(wsi, pss, eose_str, eose_len, LWS_WRITE_TEXT) != 0) {
|
||||||
|
DEBUG_ERROR("Failed to queue EOSE message");
|
||||||
|
}
|
||||||
|
|
||||||
|
free(eose_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(eose_response);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "error: missing or invalid subscription ID in REQ");
|
||||||
|
DEBUG_WARN("REQ rejected: missing or invalid subscription ID");
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "COUNT") == 0) {
|
||||||
|
// Check NIP-42 authentication for COUNT requests if required
|
||||||
|
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
||||||
|
if (!pss->auth_challenge_sent) {
|
||||||
|
send_nip42_auth_challenge(wsi, pss);
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "NIP-42 authentication required for count requests");
|
||||||
|
DEBUG_WARN("COUNT rejected: NIP-42 authentication required");
|
||||||
|
}
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle COUNT message
|
||||||
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
|
|
||||||
|
if (sub_id && cJSON_IsString(sub_id)) {
|
||||||
|
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||||
|
|
||||||
|
// Create array of filter objects from position 2 onwards
|
||||||
|
cJSON* filters = cJSON_CreateArray();
|
||||||
|
int json_size = cJSON_GetArraySize(json);
|
||||||
|
for (int i = 2; i < json_size; i++) {
|
||||||
|
cJSON* filter = cJSON_GetArrayItem(json, i);
|
||||||
|
if (filter) {
|
||||||
|
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate filters before processing
|
||||||
|
char filter_error[512] = {0};
|
||||||
|
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
|
||||||
|
send_notice_message(wsi, filter_error);
|
||||||
|
DEBUG_WARN("COUNT rejected: invalid filters");
|
||||||
|
record_malformed_request(pss);
|
||||||
|
cJSON_Delete(filters);
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
handle_count_message(subscription_id, filters, wsi, pss);
|
||||||
|
|
||||||
|
// Clean up the filters array we created
|
||||||
|
cJSON_Delete(filters);
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "CLOSE") == 0) {
|
||||||
|
// Handle CLOSE message
|
||||||
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
|
if (sub_id && cJSON_IsString(sub_id)) {
|
||||||
|
const char* subscription_id = cJSON_GetStringValue(sub_id);
|
||||||
|
|
||||||
|
// Validate subscription ID before processing
|
||||||
|
if (!subscription_id) {
|
||||||
|
send_notice_message(wsi, "error: invalid subscription ID in CLOSE");
|
||||||
|
DEBUG_WARN("CLOSE rejected: NULL subscription ID");
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate subscription ID
|
||||||
|
if (!validate_subscription_id(subscription_id)) {
|
||||||
|
send_notice_message(wsi, "error: invalid subscription ID in CLOSE");
|
||||||
|
DEBUG_WARN("CLOSE rejected: invalid subscription ID");
|
||||||
|
cJSON_Delete(json);
|
||||||
|
// Note: complete_message points to reassembly_buffer, which is managed separately
|
||||||
|
// and should not be freed here - it will be cleaned up in LWS_CALLBACK_CLOSED
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRITICAL FIX: Mark subscription as inactive in global manager FIRST
|
||||||
|
// This prevents other threads from accessing it during removal
|
||||||
|
pthread_mutex_lock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
subscription_t* target_sub = g_subscription_manager.active_subscriptions;
|
||||||
|
while (target_sub) {
|
||||||
|
if (strcmp(target_sub->id, subscription_id) == 0 && target_sub->wsi == wsi) {
|
||||||
|
target_sub->active = 0; // Mark as inactive immediately
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
target_sub = target_sub->next;
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&g_subscription_manager.subscriptions_lock);
|
||||||
|
|
||||||
|
// Now safe to remove from session list
|
||||||
|
if (pss) {
|
||||||
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
|
struct subscription** current = &pss->subscriptions;
|
||||||
|
while (*current) {
|
||||||
|
if (strcmp((*current)->id, subscription_id) == 0) {
|
||||||
|
struct subscription* to_remove = *current;
|
||||||
|
*current = to_remove->session_next;
|
||||||
|
pss->subscription_count--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current = &((*current)->session_next);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&pss->session_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally remove from global manager (which will free it)
|
||||||
|
remove_subscription_from_manager(subscription_id, wsi);
|
||||||
|
|
||||||
|
// Subscription closed
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "error: missing or invalid subscription ID in CLOSE");
|
||||||
|
DEBUG_WARN("CLOSE rejected: missing or invalid subscription ID");
|
||||||
|
}
|
||||||
|
} else if (strcmp(msg_type, "AUTH") == 0) {
|
||||||
|
// Handle NIP-42 AUTH message
|
||||||
|
if (cJSON_GetArraySize(json) >= 2) {
|
||||||
|
cJSON* auth_payload = cJSON_GetArrayItem(json, 1);
|
||||||
|
|
||||||
|
if (cJSON_IsString(auth_payload)) {
|
||||||
|
// AUTH challenge response: ["AUTH", <challenge>] (unusual)
|
||||||
|
handle_nip42_auth_challenge_response(wsi, pss, cJSON_GetStringValue(auth_payload));
|
||||||
|
} else if (cJSON_IsObject(auth_payload)) {
|
||||||
|
// AUTH signed event: ["AUTH", <event>] (standard NIP-42)
|
||||||
|
handle_nip42_auth_signed_event(wsi, pss, auth_payload);
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "Invalid AUTH message format");
|
||||||
|
DEBUG_WARN("Received AUTH message with invalid payload type");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
send_notice_message(wsi, "AUTH message requires payload");
|
||||||
|
DEBUG_WARN("Received AUTH message without payload");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Unknown message type
|
||||||
|
char unknown_msg[128];
|
||||||
|
snprintf(unknown_msg, sizeof(unknown_msg), "Unknown message type: %.32s", msg_type);
|
||||||
|
DEBUG_WARN(unknown_msg);
|
||||||
|
send_notice_message(wsi, "Unknown message type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the reassembled message
|
||||||
|
if (json) cJSON_Delete(json);
|
||||||
|
// Free the reassembly buffer now that processing is complete
|
||||||
|
free(complete_message);
|
||||||
|
pss->reassembly_buffer = NULL;
|
||||||
|
pss->reassembly_capacity = 0;
|
||||||
|
|
||||||
|
return 0; // Fragmented message processed
|
||||||
|
} else {
|
||||||
|
// Not the final fragment - continue accumulating
|
||||||
|
DEBUG_TRACE("Accumulated %zu bytes so far, waiting for more fragments", pss->reassembly_size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-fragmented messages (original code path)
|
||||||
char *message = malloc(len + 1);
|
char *message = malloc(len + 1);
|
||||||
if (message) {
|
if (message) {
|
||||||
memcpy(message, in, len);
|
memcpy(message, in, len);
|
||||||
message[len] = '\0';
|
message[len] = '\0';
|
||||||
|
|
||||||
// Parse JSON message (this is the normal program flow)
|
// Parse JSON message (this is the normal program flow)
|
||||||
|
DEBUG_TRACE("Parsing JSON message of length %zu", strlen(message));
|
||||||
cJSON* json = cJSON_Parse(message);
|
cJSON* json = cJSON_Parse(message);
|
||||||
if (json && cJSON_IsArray(json)) {
|
if (json && cJSON_IsArray(json)) {
|
||||||
// Get message type
|
// Get message type
|
||||||
@@ -666,7 +1384,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
if (kind_obj && cJSON_IsNumber(kind_obj)) {
|
||||||
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
int event_kind = (int)cJSON_GetNumberValue(kind_obj);
|
||||||
|
|
||||||
DEBUG_TRACE("Processing event kind %d", event_kind);
|
DEBUG_TRACE("Processing event kind %d, message length: %zu", event_kind, strlen(message));
|
||||||
|
|
||||||
// Log reception of Kind 23456 events
|
// Log reception of Kind 23456 events
|
||||||
if (event_kind == 23456) {
|
if (event_kind == 23456) {
|
||||||
@@ -869,10 +1587,13 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (strcmp(msg_type, "REQ") == 0) {
|
} else if (strcmp(msg_type, "REQ") == 0) {
|
||||||
|
DEBUG_TRACE("REQ message received, starting processing");
|
||||||
|
|
||||||
// Log the full REQ message for debugging
|
// Log the full REQ message for debugging
|
||||||
|
|
||||||
// Check NIP-42 authentication for REQ subscriptions if required
|
// Check NIP-42 authentication for REQ subscriptions if required
|
||||||
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
if (pss && pss->nip42_auth_required_subscriptions && !pss->authenticated) {
|
||||||
|
DEBUG_TRACE("REQ rejected: NIP-42 authentication required");
|
||||||
if (!pss->auth_challenge_sent) {
|
if (!pss->auth_challenge_sent) {
|
||||||
send_nip42_auth_challenge(wsi, pss);
|
send_nip42_auth_challenge(wsi, pss);
|
||||||
} else {
|
} else {
|
||||||
@@ -884,6 +1605,8 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ message passed authentication check");
|
||||||
|
|
||||||
// Handle REQ message
|
// Handle REQ message
|
||||||
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
|
|
||||||
@@ -894,6 +1617,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
|
|
||||||
// Validate subscription ID before processing
|
// Validate subscription ID before processing
|
||||||
if (!subscription_id) {
|
if (!subscription_id) {
|
||||||
|
DEBUG_TRACE("REQ rejected: NULL subscription ID");
|
||||||
send_notice_message(wsi, "error: invalid subscription ID");
|
send_notice_message(wsi, "error: invalid subscription ID");
|
||||||
DEBUG_WARN("REQ rejected: NULL subscription ID");
|
DEBUG_WARN("REQ rejected: NULL subscription ID");
|
||||||
record_malformed_request(pss);
|
record_malformed_request(pss);
|
||||||
@@ -904,6 +1628,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
|
|
||||||
// Validate subscription ID
|
// Validate subscription ID
|
||||||
if (!validate_subscription_id(subscription_id)) {
|
if (!validate_subscription_id(subscription_id)) {
|
||||||
|
DEBUG_TRACE("REQ rejected: invalid subscription ID format");
|
||||||
send_notice_message(wsi, "error: invalid subscription ID");
|
send_notice_message(wsi, "error: invalid subscription ID");
|
||||||
DEBUG_WARN("REQ rejected: invalid subscription ID");
|
DEBUG_WARN("REQ rejected: invalid subscription ID");
|
||||||
cJSON_Delete(json);
|
cJSON_Delete(json);
|
||||||
@@ -911,9 +1636,12 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ subscription ID validated: %s", subscription_id);
|
||||||
|
|
||||||
// Create array of filter objects from position 2 onwards
|
// Create array of filter objects from position 2 onwards
|
||||||
cJSON* filters = cJSON_CreateArray();
|
cJSON* filters = cJSON_CreateArray();
|
||||||
if (!filters) {
|
if (!filters) {
|
||||||
|
DEBUG_TRACE("REQ failed: could not create filters array");
|
||||||
send_notice_message(wsi, "error: failed to process filters");
|
send_notice_message(wsi, "error: failed to process filters");
|
||||||
DEBUG_ERROR("REQ failed: could not create filters array");
|
DEBUG_ERROR("REQ failed: could not create filters array");
|
||||||
cJSON_Delete(json);
|
cJSON_Delete(json);
|
||||||
@@ -922,16 +1650,21 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
}
|
}
|
||||||
|
|
||||||
int json_size = cJSON_GetArraySize(json);
|
int json_size = cJSON_GetArraySize(json);
|
||||||
|
int filter_count = 0;
|
||||||
for (int i = 2; i < json_size; i++) {
|
for (int i = 2; i < json_size; i++) {
|
||||||
cJSON* filter = cJSON_GetArrayItem(json, i);
|
cJSON* filter = cJSON_GetArrayItem(json, i);
|
||||||
if (filter) {
|
if (filter) {
|
||||||
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
cJSON_AddItemToArray(filters, cJSON_Duplicate(filter, 1));
|
||||||
|
filter_count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ created %d filters from message", filter_count);
|
||||||
|
|
||||||
// Validate filters before processing
|
// Validate filters before processing
|
||||||
char filter_error[512] = {0};
|
char filter_error[512] = {0};
|
||||||
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
|
if (!validate_filter_array(filters, filter_error, sizeof(filter_error))) {
|
||||||
|
DEBUG_TRACE("REQ rejected: filter validation failed - %s", filter_error);
|
||||||
send_notice_message(wsi, filter_error);
|
send_notice_message(wsi, filter_error);
|
||||||
DEBUG_WARN("REQ rejected: invalid filters");
|
DEBUG_WARN("REQ rejected: invalid filters");
|
||||||
record_malformed_request(pss);
|
record_malformed_request(pss);
|
||||||
@@ -941,7 +1674,11 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DEBUG_TRACE("REQ filters validated successfully");
|
||||||
|
|
||||||
|
DEBUG_TRACE("About to call handle_req_message for subscription %s", subscription_id);
|
||||||
handle_req_message(subscription_id, filters, wsi, pss);
|
handle_req_message(subscription_id, filters, wsi, pss);
|
||||||
|
DEBUG_TRACE("handle_req_message completed for subscription %s", subscription_id);
|
||||||
|
|
||||||
// Clean up the filters array we created
|
// Clean up the filters array we created
|
||||||
cJSON_Delete(filters);
|
cJSON_Delete(filters);
|
||||||
@@ -1175,6 +1912,15 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
pss->message_queue_count = 0;
|
pss->message_queue_count = 0;
|
||||||
pss->writeable_requested = 0;
|
pss->writeable_requested = 0;
|
||||||
|
|
||||||
|
// Clean up message reassembly buffer
|
||||||
|
if (pss->reassembly_buffer) {
|
||||||
|
free(pss->reassembly_buffer);
|
||||||
|
pss->reassembly_buffer = NULL;
|
||||||
|
}
|
||||||
|
pss->reassembly_size = 0;
|
||||||
|
pss->reassembly_capacity = 0;
|
||||||
|
pss->reassembly_active = 0;
|
||||||
|
|
||||||
// Clean up session subscriptions - copy IDs first to avoid use-after-free
|
// Clean up session subscriptions - copy IDs first to avoid use-after-free
|
||||||
pthread_mutex_lock(&pss->session_lock);
|
pthread_mutex_lock(&pss->session_lock);
|
||||||
|
|
||||||
@@ -1244,7 +1990,7 @@ static struct lws_protocols protocols[] = {
|
|||||||
"nostr-relay-protocol",
|
"nostr-relay-protocol",
|
||||||
nostr_relay_callback,
|
nostr_relay_callback,
|
||||||
sizeof(struct per_session_data),
|
sizeof(struct per_session_data),
|
||||||
4096, // rx buffer size
|
65536, // rx buffer size
|
||||||
0, NULL, 0
|
0, NULL, 0
|
||||||
},
|
},
|
||||||
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
|
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
|
||||||
@@ -1404,6 +2150,9 @@ int start_websocket_relay(int port_override, int strict_port) {
|
|||||||
snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port);
|
snprintf(startup_msg, sizeof(startup_msg), "WebSocket relay started on ws://127.0.0.1:%d", actual_port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Static variable for status post timing (initialize to 0 for immediate first post)
|
||||||
|
static time_t last_status_post_time = 0;
|
||||||
|
|
||||||
// Main event loop with proper signal handling
|
// Main event loop with proper signal handling
|
||||||
while (g_server_running && !g_shutdown_flag) {
|
while (g_server_running && !g_shutdown_flag) {
|
||||||
int result = lws_service(ws_context, 1000);
|
int result = lws_service(ws_context, 1000);
|
||||||
@@ -1412,6 +2161,19 @@ int start_websocket_relay(int port_override, int strict_port) {
|
|||||||
DEBUG_ERROR("libwebsockets service error");
|
DEBUG_ERROR("libwebsockets service error");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's time to post status update
|
||||||
|
time_t current_time = time(NULL);
|
||||||
|
int status_post_hours = get_config_int("kind_1_status_posts_hours", 0);
|
||||||
|
|
||||||
|
if (status_post_hours > 0) {
|
||||||
|
int seconds_interval = status_post_hours * 3600; // Convert hours to seconds
|
||||||
|
|
||||||
|
if (current_time - last_status_post_time >= seconds_interval) {
|
||||||
|
last_status_post_time = current_time;
|
||||||
|
generate_and_post_status_event();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lws_context_destroy(ws_context);
|
lws_context_destroy(ws_context);
|
||||||
@@ -1512,7 +2274,7 @@ int process_dm_stats_command(cJSON* dm_event, char* error_message, size_t error_
|
|||||||
const char* encrypted_content = cJSON_GetStringValue(content_obj);
|
const char* encrypted_content = cJSON_GetStringValue(content_obj);
|
||||||
|
|
||||||
// Decrypt content
|
// Decrypt content
|
||||||
char decrypted_content[4096];
|
char decrypted_content[16384];
|
||||||
int decrypt_result = nostr_nip44_decrypt(relay_privkey, sender_pubkey_bytes,
|
int decrypt_result = nostr_nip44_decrypt(relay_privkey, sender_pubkey_bytes,
|
||||||
encrypted_content, decrypted_content, sizeof(decrypted_content));
|
encrypted_content, decrypted_content, sizeof(decrypted_content));
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
|
|
||||||
// Filter validation constants
|
// Filter validation constants
|
||||||
#define MAX_FILTERS_PER_REQUEST 10
|
#define MAX_FILTERS_PER_REQUEST 10
|
||||||
#define MAX_AUTHORS_PER_FILTER 100
|
#define MAX_AUTHORS_PER_FILTER 1000
|
||||||
#define MAX_IDS_PER_FILTER 100
|
#define MAX_IDS_PER_FILTER 1000
|
||||||
#define MAX_KINDS_PER_FILTER 50
|
#define MAX_KINDS_PER_FILTER 500
|
||||||
#define MAX_TAG_VALUES_PER_FILTER 100
|
#define MAX_TAG_VALUES_PER_FILTER 1000
|
||||||
#define MAX_KIND_VALUE 65535
|
#define MAX_KIND_VALUE 65535
|
||||||
#define MAX_TIMESTAMP_VALUE 2147483647 // Max 32-bit signed int
|
#define MAX_TIMESTAMP_VALUE 2147483647 // Max 32-bit signed int
|
||||||
#define MAX_LIMIT_VALUE 5000
|
#define MAX_LIMIT_VALUE 5000
|
||||||
@@ -73,6 +73,12 @@ struct per_session_data {
|
|||||||
struct message_queue_node* message_queue_tail; // Tail of message queue
|
struct message_queue_node* message_queue_tail; // Tail of message queue
|
||||||
int message_queue_count; // Number of messages in queue
|
int message_queue_count; // Number of messages in queue
|
||||||
int writeable_requested; // Flag: 1 if writeable callback requested
|
int writeable_requested; // Flag: 1 if writeable callback requested
|
||||||
|
|
||||||
|
// Message reassembly for handling fragmented WebSocket messages
|
||||||
|
char* reassembly_buffer; // Buffer for accumulating message fragments (NULL when not reassembling)
|
||||||
|
size_t reassembly_size; // Current size of accumulated data
|
||||||
|
size_t reassembly_capacity; // Allocated capacity of reassembly buffer
|
||||||
|
int reassembly_active; // Flag: 1 if currently reassembling a message
|
||||||
};
|
};
|
||||||
|
|
||||||
// NIP-11 HTTP session data structure for managing buffer lifetime
|
// NIP-11 HTTP session data structure for managing buffer lifetime
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=C Nostr Relay Server (Local Development)
|
|
||||||
Documentation=https://github.com/your-repo/c-relay
|
|
||||||
After=network.target
|
|
||||||
Wants=network-online.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=teknari
|
|
||||||
WorkingDirectory=/home/teknari/Storage/c_relay
|
|
||||||
Environment=DEBUG_LEVEL=0
|
|
||||||
ExecStart=/home/teknari/Storage/c_relay/crelay --port 7777 --debug-level=$DEBUG_LEVEL
|
|
||||||
Restart=always
|
|
||||||
RestartSec=5
|
|
||||||
StandardOutput=journal
|
|
||||||
StandardError=journal
|
|
||||||
SyslogIdentifier=c-relay-local
|
|
||||||
|
|
||||||
# Security settings (relaxed for local development)
|
|
||||||
NoNewPrivileges=true
|
|
||||||
ProtectSystem=strict
|
|
||||||
ProtectHome=true
|
|
||||||
ReadWritePaths=/home/teknari/Storage/c_relay
|
|
||||||
PrivateTmp=true
|
|
||||||
|
|
||||||
# Network security
|
|
||||||
PrivateNetwork=false
|
|
||||||
RestrictAddressFamilies=AF_INET AF_INET6
|
|
||||||
|
|
||||||
# Resource limits
|
|
||||||
LimitNOFILE=65536
|
|
||||||
LimitNPROC=4096
|
|
||||||
|
|
||||||
# Event-based configuration system
|
|
||||||
# No environment variables needed - all configuration is stored as Nostr events
|
|
||||||
# Database files (<relay_pubkey>.db) are created automatically in WorkingDirectory
|
|
||||||
# Admin keys are generated and displayed only during first startup
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
747
tests/test_requests.mjs
Normal file
@@ -0,0 +1,747 @@
|
|||||||
|
|
||||||
|
/**
|
||||||
|
* Nostr Relay Pubkey Filter Test
|
||||||
|
* Tests how many pubkeys different relays can handle in a single filter request
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { WebSocket } from 'ws';
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
const RELAYS = [
|
||||||
|
// "wss://relay.laantungir.net"
|
||||||
|
"ws://127.0.0.1:8888"
|
||||||
|
];
|
||||||
|
|
||||||
|
// Test parameters
|
||||||
|
const STEP_SIZE = 25; // Increment pubkey count by 25 each test
|
||||||
|
const MAX_PUBKEYS = 500; // Maximum pubkeys to test
|
||||||
|
const EVENT_KIND = 1; // Kind 1 = text notes
|
||||||
|
const EVENT_LIMIT = 2; // Only request 2 events per test
|
||||||
|
|
||||||
|
// Generate test pubkey arrays of increasing sizes
|
||||||
|
function generateTestPubkeyArrays() {
|
||||||
|
const testArrays = [];
|
||||||
|
for (let count = STEP_SIZE; count <= MAX_PUBKEYS; count += STEP_SIZE) {
|
||||||
|
testArrays.push(PUBKEYS.slice(0, count));
|
||||||
|
}
|
||||||
|
return testArrays;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PUBKEYS = [
|
||||||
|
"85080d3bad70ccdcd7f74c29a44f55bb85cbcd3dd0cbb957da1d215bdb931204",
|
||||||
|
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
|
||||||
|
"916b7aca250f43b9f842faccc831db4d155088632a8c27c0d140f2043331ba57",
|
||||||
|
"2645caf5706a31767c921532975a079f85950e1006bd5065f5dd0213e6848a96",
|
||||||
|
"8fe3f243e91121818107875d51bca4f3fcf543437aa9715150ec8036358939c5",
|
||||||
|
"83e818dfbeccea56b0f551576b3fd39a7a50e1d8159343500368fa085ccd964b",
|
||||||
|
"a341f45ff9758f570a21b000c17d4e53a3a497c8397f26c0e6d61e5acffc7a98",
|
||||||
|
"e88a691e98d9987c964521dff60025f60700378a4879180dcbbb4a5027850411",
|
||||||
|
"2cde0e02bda47eaeeed65e341619cc5f2afce990164669da4e1e5989180a96b9",
|
||||||
|
"edcd20558f17d99327d841e4582f9b006331ac4010806efa020ef0d40078e6da",
|
||||||
|
"34d2f5274f1958fcd2cb2463dabeaddf8a21f84ace4241da888023bf05cc8095",
|
||||||
|
"c48b5cced5ada74db078df6b00fa53fc1139d73bf0ed16de325d52220211dbd5",
|
||||||
|
"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9",
|
||||||
|
"e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42",
|
||||||
|
"1306edd66f1da374adc417cf884bbcff57c6399656236c1f872ee10403c01b2d",
|
||||||
|
"eaf27aa104833bcd16f671488b01d65f6da30163b5848aea99677cc947dd00aa",
|
||||||
|
"472f440f29ef996e92a186b8d320ff180c855903882e59d50de1b8bd5669301e",
|
||||||
|
"be1d89794bf92de5dd64c1e60f6a2c70c140abac9932418fee30c5c637fe9479",
|
||||||
|
"c49d52a573366792b9a6e4851587c28042fb24fa5625c6d67b8c95c8751aca15",
|
||||||
|
"c037a6897df86bfd4df5496ca7e2318992b4766897fb18fbd1d347a4f4459f5e",
|
||||||
|
"e41e883f1ef62485a074c1a1fa1d0a092a5d678ad49bedc2f955ab5e305ba94e",
|
||||||
|
"020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e",
|
||||||
|
"e2f28c1ac6dff5a7b755635af4c8436d2fec89b888a9d9548a51b2c63f779555",
|
||||||
|
"29fbc05acee671fb579182ca33b0e41b455bb1f9564b90a3d8f2f39dee3f2779",
|
||||||
|
"090254801a7e8e5085b02e711622f0dfa1a85503493af246aa42af08f5e4d2df",
|
||||||
|
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d",
|
||||||
|
"6b0d4c8d9dc59e110d380b0429a02891f1341a0fa2ba1b1cf83a3db4d47e3964",
|
||||||
|
"b0b8fbd9578ac23e782d97a32b7b3a72cda0760761359bd65661d42752b4090a",
|
||||||
|
"b7996c183e036df27802945b80bbdc8b0bf5971b6621a86bf3569c332117f07d",
|
||||||
|
"1833ee04459feb2ca4ae690d5f31269ad488c69e5fe903a42b532c677c4a8170",
|
||||||
|
"4adb4ff2dc72bbf1f6da19fc109008a25013c837cf712016972fad015b19513f",
|
||||||
|
"c4eabae1be3cf657bc1855ee05e69de9f059cb7a059227168b80b89761cbc4e0",
|
||||||
|
"368f4e0027fd223fdb69b6ec6e1c06d1f027a611b1ed38eeb32493eb2878bb35",
|
||||||
|
"703e26b4f8bc0fa57f99d815dbb75b086012acc24fc557befa310f5aa08d1898",
|
||||||
|
"50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63",
|
||||||
|
"6e1534f56fc9e937e06237c8ba4b5662bcacc4e1a3cfab9c16d89390bec4fca3",
|
||||||
|
"a5e93aef8e820cbc7ab7b6205f854b87aed4b48c5f6b30fbbeba5c99e40dcf3f",
|
||||||
|
"1989034e56b8f606c724f45a12ce84a11841621aaf7182a1f6564380b9c4276b",
|
||||||
|
"19fefd7f39c96d2ff76f87f7627ae79145bc971d8ab23205005939a5a913bc2f",
|
||||||
|
"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93",
|
||||||
|
"a3b0ce5d70d0db22885706b2b1f144c6864a7e4828acff3f8f01ca6b3f54ad15",
|
||||||
|
"aef0d6b212827f3ba1de6189613e6d4824f181f567b1205273c16895fdaf0b23",
|
||||||
|
"826e9f895b81ab41a4522268b249e68d02ca81608def562a493cee35ffc5c759",
|
||||||
|
"460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c",
|
||||||
|
"e1055729d51e037b3c14e8c56e2c79c22183385d94aadb32e5dc88092cd0fef4",
|
||||||
|
"27f211f4542fd89d673cfad15b6d838cc5d525615aae8695ed1dcebc39b2dadb",
|
||||||
|
"eab0e756d32b80bcd464f3d844b8040303075a13eabc3599a762c9ac7ab91f4f",
|
||||||
|
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245",
|
||||||
|
"63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed",
|
||||||
|
"00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700",
|
||||||
|
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194",
|
||||||
|
"22aa81510ee63fe2b16cae16e0921f78e9ba9882e2868e7e63ad6d08ae9b5954",
|
||||||
|
"175f568d77fb0cb7400f0ddd8aed1738cd797532b314ef053a1669d4dba7433a",
|
||||||
|
"6c535d95a8659b234d5a0805034f5f0a67e3c0ceffcc459f61f680fe944424bf",
|
||||||
|
"9579444852221038dcba34512257b66a1c6e5bdb4339b6794826d4024b3e4ce9",
|
||||||
|
"58c741aa630c2da35a56a77c1d05381908bd10504fdd2d8b43f725efa6d23196",
|
||||||
|
"b8e6bf46e109314616fe24e6c7e265791a5f2f4ec95ae8aa15d7107ad250dc63",
|
||||||
|
"84dee6e676e5bb67b4ad4e042cf70cbd8681155db535942fcc6a0533858a7240",
|
||||||
|
"387519cafd325668ecffe59577f37238638da4cf2d985b82f932fc81d33da1e8",
|
||||||
|
"b9e76546ba06456ed301d9e52bc49fa48e70a6bf2282be7a1ae72947612023dc",
|
||||||
|
"4d62dd5e6ac55ae2405940f59f6f030a994ec2b3ecc5556c8dc542cce20e46dd",
|
||||||
|
"9c612f8b770f0e3fd35cdac2bc57fcee8561e560504ea25c8b9eff8e03512b3e",
|
||||||
|
"3eeb3de14ec5c48c6c4c9ff80908c4186170eabb74b2a6705a7db9f9922cd61e",
|
||||||
|
"51d7f1b736d1958fa56f113e82a27987a3aca4f0e6d237fa8fc369cc1608c5c0",
|
||||||
|
"c2622c916d9b90e10a81b2ba67b19bdfc5d6be26c25756d1f990d3785ce1361b",
|
||||||
|
"b111d517452f9ef015e16d60ae623a6b66af63024eec941b0653bfee0dd667d4",
|
||||||
|
"d897efcd971f8e5eae08c86b7da66f89b30e761a4a86ac41d907425d15b630fe",
|
||||||
|
"9dea27855974a08fceb48c40fab8432c1a8e3a53a1da22a1ad568595d6010649",
|
||||||
|
"47b630bbcdfa88b1c85f84aa3b68fe6c0102b651ba5d9a23cbd2d07b4f6eecc1",
|
||||||
|
"eb0dc09a61fdfc0df5db1f20c7fc7d83f00c690580fea2e5bac8f99c13f65065",
|
||||||
|
"5c0775b1ae0a5140da9599aa9cd1c5beea55c2d55a5d681808525eb5fce37b32",
|
||||||
|
"b474e6999980aa9e8c9dd6e3720fb03136bfa05aba5fab1634dc0bd8767d412f",
|
||||||
|
"759f7abf05ca710bf2c8da7ad7a9a7df6d0c85db7b2217da524e94e3627b2fbd",
|
||||||
|
"060e7c6ed0dbeb9c8cdc61445ee38b9b08d899d6b617e28064b0916e243ddddb",
|
||||||
|
"f728d9e6e7048358e70930f5ca64b097770d989ccd86854fe618eda9c8a38106",
|
||||||
|
"bf2376e17ba4ec269d10fcc996a4746b451152be9031fa48e74553dde5526bce",
|
||||||
|
"b99dbca0184a32ce55904cb267b22e434823c97f418f36daf5d2dff0dd7b5c27",
|
||||||
|
"c7dccba4fe4426a7b1ea239a5637ba40fab9862c8c86b3330fe65e9f667435f6",
|
||||||
|
"ad46db12ee250a108756ab4f0f3007b04d7e699f45eac3ab696077296219d207",
|
||||||
|
"59fbee7369df7713dbbfa9bbdb0892c62eba929232615c6ff2787da384cb770f",
|
||||||
|
"d7f0e3917c466f1e2233e9624fbd6d4bd1392dbcfcaf3574f457569d496cb731",
|
||||||
|
"e9e4276490374a0daf7759fd5f475deff6ffb9b0fc5fa98c902b5f4b2fe3bba2",
|
||||||
|
"6f35047caf7432fc0ab54a28fed6c82e7b58230bf98302bf18350ff71e10430a",
|
||||||
|
"fdd5e8f6ae0db817be0b71da20498c1806968d8a6459559c249f322fa73464a7",
|
||||||
|
"883fea4c071fda4406d2b66be21cb1edaf45a3e058050d6201ecf1d3596bbc39",
|
||||||
|
"a1808558470389142e297d4729e081ab8bdff1ab50d0ebe22ffa78958f7a6ab7",
|
||||||
|
"330fb1431ff9d8c250706bbcdc016d5495a3f744e047a408173e92ae7ee42dac",
|
||||||
|
"a4cb51f4618cfcd16b2d3171c466179bed8e197c43b8598823b04de266cef110",
|
||||||
|
"9c163c7351f8832b08b56cbb2e095960d1c5060dd6b0e461e813f0f07459119e",
|
||||||
|
"0a722ca20e1ccff0adfdc8c2abb097957f0e0bf32db18c4281f031756d50eb8d",
|
||||||
|
"5cad82c898ee66013711945d687f7d9549f645a0118467dae2f5e274c598d6ff",
|
||||||
|
"03b593ef3d95102b54bdff77728131a7c3bdfe9007b0b92cd7c4ad4a0021de25",
|
||||||
|
"d0debf9fb12def81f43d7c69429bb784812ac1e4d2d53a202db6aac7ea4b466c",
|
||||||
|
"60d53675f07dee9e7d77910efa44682d87cb532313ba66b8f4449d649172296b",
|
||||||
|
"d3ab33199eb48c6f785072b4a66a8e57814e35d31375cca8c3ceeecc171f30ba",
|
||||||
|
"772bd267dffbff318d1a89f257c3371410111a8b89571dbbefa77af6bfa179f3",
|
||||||
|
"11b9a89404dbf3034e7e1886ba9dc4c6d376f239a118271bd2ec567a889850ce",
|
||||||
|
"0497384b57b43c107a778870462901bf68e0e8583b32e2816563543c059784a4",
|
||||||
|
"5d9ba2c5ee0e86e2c4477b145eb301f2df06063a19f4c4ab9042bd347188ec8e",
|
||||||
|
"5683ffc7ff8a732565135aad56cdff94ebacd9a616d1313aea8ad48a446bfe99",
|
||||||
|
"3004d45a0ab6352c61a62586a57c50f11591416c29db1143367a4f0623b491ca",
|
||||||
|
"b24e32ee9a1c18f2771b53345ed8dbc55b59cbe958e5a165dc01704c3aaa6196",
|
||||||
|
"0a2df905acd5b5be3214a84cb2d4f61b0efb4d9bf05739d51112252504959688",
|
||||||
|
"95361a2b42a26c22bac3b6b6ba4c5cac4d36906eb0cfb98268681c45a301c518",
|
||||||
|
"b07d216f2f0422ec0252dd81a6513b8d0b0c7ef85291fbf5a85ef23f8df78fa7",
|
||||||
|
"064de2497ce621aee2a5b4b926a08b1ca01bce9da85b0c714e883e119375140c",
|
||||||
|
"5a8e581f16a012e24d2a640152ad562058cb065e1df28e907c1bfa82c150c8ba",
|
||||||
|
"a36bdc7952e973b31cb32d4ce3ce21447db66c3149c1b7a3d2450f77f9c7e8f9",
|
||||||
|
"e03cfe011d81424bb60a12e9eb0cb0c9c688c34712c3794c0752e0718b369ef2",
|
||||||
|
"2edbcea694d164629854a52583458fd6d965b161e3c48b57d3aff01940558884",
|
||||||
|
"b9003833fabff271d0782e030be61b7ec38ce7d45a1b9a869fbdb34b9e2d2000",
|
||||||
|
"4379e76bfa76a80b8db9ea759211d90bb3e67b2202f8880cc4f5ffe2065061ad",
|
||||||
|
"76c71aae3a491f1d9eec47cba17e229cda4113a0bbb6e6ae1776d7643e29cafa",
|
||||||
|
"d307643547703537dfdef811c3dea96f1f9e84c8249e200353425924a9908cf8",
|
||||||
|
"da0cc82154bdf4ce8bf417eaa2d2fa99aa65c96c77867d6656fccdbf8e781b18",
|
||||||
|
"3511ad63cd9ad760780044b7c815ee55e8e00722b5de271c47ff29367653456c",
|
||||||
|
"f9acb0b034c4c1177e985f14639f317ef0fedee7657c060b146ee790024317ec",
|
||||||
|
"0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb",
|
||||||
|
"ee11a5dff40c19a555f41fe42b48f00e618c91225622ae37b6c2bb67b76c4e49",
|
||||||
|
"053935081a69624466034446eda3374d905652ddbf8217c88708182687a33066",
|
||||||
|
"a305cc8926861bdde5c71bbb6fd394bb4cea6ef5f5f86402b249fc5ceb0ce220",
|
||||||
|
"03a6e50be223dbb49e282764388f6f2ca8826eae8c5a427aa82bb1b61e51d5e6",
|
||||||
|
"a197639863cf175adc96348382a73b4a4a361c6b2e6fc1de61a14244a2f926a1",
|
||||||
|
"3ca7ca157b5975ace02225caf99fdce43f11207c072cb4899c80a414a9c7539d",
|
||||||
|
"02d9f5676fffc339ffe94dfab38bebe21ce117c6f1509d9922a82d454f420da2",
|
||||||
|
"08634a74c9d14479b462389b307695815f9a189e8fb6e058b92e18bd3f537405",
|
||||||
|
"ec7de4aa8758ba9e09a8c89d2757a1fa0e2cc61c20b757af52ae058931c1a33f",
|
||||||
|
"2250f69694c2a43929e77e5de0f6a61ae5e37a1ee6d6a3baef1706ed9901248b",
|
||||||
|
"a9434ee165ed01b286becfc2771ef1705d3537d051b387288898cc00d5c885be",
|
||||||
|
"bd625f1b8c49a79f075f3ebd2d111ff625504cf2ad12442fd70d191dd2f4a562",
|
||||||
|
"25e5c82273a271cb1a840d0060391a0bf4965cafeb029d5ab55350b418953fbb",
|
||||||
|
"42224859763652914db53052103f0b744df79dfc4efef7e950fc0802fc3df3c5",
|
||||||
|
"11d0b66747887ba9a6d34b23eb31287374b45b1a1b161eac54cb183c53e00ef7",
|
||||||
|
"2544cfcb89d7c2f8d3a31ea2ed386ac5189a18f484672436580eec215f9b039c",
|
||||||
|
"d4338b7c3306491cfdf54914d1a52b80a965685f7361311eae5f3eaff1d23a5b",
|
||||||
|
"c43e382ee4835010b9fad18e0a6f50f1ae143b98e089b8bb974232fce4d1f295",
|
||||||
|
"92de68b21302fa2137b1cbba7259b8ba967b535a05c6d2b0847d9f35ff3cf56a",
|
||||||
|
"55f04590674f3648f4cdc9dc8ce32da2a282074cd0b020596ee033d12d385185",
|
||||||
|
"2af01e0d6bd1b9fbb9e3d43157d64590fb27dcfbcabe28784a5832e17befb87b",
|
||||||
|
"35b23cd02d2d75e55cee38fdee26bc82f1d15d3c9580800b04b0da2edb7517ea",
|
||||||
|
"7e0c255fd3d0f9b48789a944baf19bf42c205a9c55199805eb13573b32137488",
|
||||||
|
"ee0e01eb17fc6cb4cd2d9300d2f8945b51056514f156c6bc6d491b74496d161a",
|
||||||
|
"cbc5ef6b01cbd1ffa2cb95a954f04c385a936c1a86e1bb9ccdf2cf0f4ebeaccb",
|
||||||
|
"ec6e36d5c9eb874f1db4253ef02377f7cc70697cda40fbfb24ded6b5d14cce4c",
|
||||||
|
"2779f3d9f42c7dee17f0e6bcdcf89a8f9d592d19e3b1bbd27ef1cffd1a7f98d1",
|
||||||
|
"976713246c36db1a4364f917b98633cbe0805d46af880f6b50a505d4eb32ed47",
|
||||||
|
"8766a54ef9a170b3860bc66fd655abb24b5fda75d7d7ff362f44442fbdeb47b9",
|
||||||
|
"ea2e3c814d08a378f8a5b8faecb2884d05855975c5ca4b5c25e2d6f936286f14",
|
||||||
|
"e2ccf7cf20403f3f2a4a55b328f0de3be38558a7d5f33632fdaaefc726c1c8eb",
|
||||||
|
"07eced8b63b883cedbd8520bdb3303bf9c2b37c2c7921ca5c59f64e0f79ad2a6",
|
||||||
|
"532d830dffe09c13e75e8b145c825718fc12b0003f61d61e9077721c7fff93cb",
|
||||||
|
"1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d",
|
||||||
|
"c708943ea349519dcf56b2a5c138fd9ed064ad65ddecae6394eabd87a62f1770",
|
||||||
|
"ccaa58e37c99c85bc5e754028a718bd46485e5d3cb3345691ecab83c755d48cc",
|
||||||
|
"5b0e8da6fdfba663038690b37d216d8345a623cc33e111afd0f738ed7792bc54",
|
||||||
|
"f2c96c97f6419a538f84cf3fa72e2194605e1848096e6e5170cce5b76799d400",
|
||||||
|
"aa55a479ad6934d0fd78f3dbd88515cd1ca0d7a110812e711380d59df7598935",
|
||||||
|
"bd9eb657c25b4f6cda68871ce26259d1f9bc62420487e3224905b674a710a45a",
|
||||||
|
"69a80567e79b6b9bc7282ad595512df0b804784616bedb623c122fad420a2635",
|
||||||
|
"fa984bd7dbb282f07e16e7ae87b26a2a7b9b90b7246a44771f0cf5ae58018f52",
|
||||||
|
"df173277182f3155d37b330211ba1de4a81500c02d195e964f91be774ec96708",
|
||||||
|
"675b84fe75e216ab947c7438ee519ca7775376ddf05dadfba6278bd012e1d728",
|
||||||
|
"91c9a5e1a9744114c6fe2d61ae4de82629eaaa0fb52f48288093c7e7e036f832",
|
||||||
|
"24ebb6b58d0b984a965b76f82ce9eff8795cc95085a4d09dedc56949ed596ada",
|
||||||
|
"bb1cf5250435ff475cd8b32acb23e3ee7bbe8fc38f6951704b4798513947672c",
|
||||||
|
"922945779f93fd0b3759f1157e3d9fa20f3fd24c4b8f2bcf520cacf649af776d",
|
||||||
|
"3d842afecd5e293f28b6627933704a3fb8ce153aa91d790ab11f6a752d44a42d",
|
||||||
|
"e8d67c435a4a59304e1414280e952efe17be4254fca27916bf63f9f73e54aba4",
|
||||||
|
"c1fc7771f5fa418fd3ac49221a18f19b42ccb7a663da8f04cbbf6c08c80d20b1",
|
||||||
|
"8eee8f5a002e533e9f9ffef14c713da449c23f56f4415e7995552075a02d1d37",
|
||||||
|
"c998a5739f04f7fff202c54962aa5782b34ecb10d6f915bdfdd7582963bf9171",
|
||||||
|
"a536ab1f7f3c0133baadbdf472b1ac7ad4b774ed432c1989284193572788bca0",
|
||||||
|
"c9b19ffcd43e6a5f23b3d27106ce19e4ad2df89ba1031dd4617f1b591e108965",
|
||||||
|
"e6ee5b449c220defea6373b8a7e147cabd67c2bdb5016886bf6096a3c7435a61",
|
||||||
|
"a619cf1a888a73211bbf32e0c438319f23e91444d45d7bc88816ed5fcb7e8fa3",
|
||||||
|
"56a6b75373c8f7b93c53bcae86d8ffbaba9f2a1b38122054fcdb7f3bf645b727",
|
||||||
|
"89bfe407c647eb1888871f756516bb1906254fba3132d516ce9099614e37d10c",
|
||||||
|
"b7ed68b062de6b4a12e51fd5285c1e1e0ed0e5128cda93ab11b4150b55ed32fc",
|
||||||
|
"4657dfe8965be8980a93072bcfb5e59a65124406db0f819215ee78ba47934b3e",
|
||||||
|
"d61f3bc5b3eb4400efdae6169a5c17cabf3246b514361de939ce4a1a0da6ef4a",
|
||||||
|
"58ead82fa15b550094f7f5fe4804e0fe75b779dbef2e9b20511eccd69e6d08f9",
|
||||||
|
"fcf6fee0e959c7195dadc5f36fe5a873003b389e7033293b06057c821fcbc9c5",
|
||||||
|
"6681268ace4748d41a4cfcc1e64006fb935bbc359782b3d9611f64d51c6752d9",
|
||||||
|
"e76450df94f84c1c0b71677a45d75b7918f0b786113c2d038e6ab8841b99f276",
|
||||||
|
"a44dbc9aaa357176a7d4f5c3106846ea096b66de0b50ee39aff54baab6c4bf4b",
|
||||||
|
"281e109d2a2899bb0555cf0c3a69b24b3debd61885ca29ef39b95b632be25fe7",
|
||||||
|
"5be6446aa8a31c11b3b453bf8dafc9b346ff328d1fa11a0fa02a1e6461f6a9b1",
|
||||||
|
"e1ff3bfdd4e40315959b08b4fcc8245eaa514637e1d4ec2ae166b743341be1af",
|
||||||
|
"0d6c8388dcb049b8dd4fc8d3d8c3bb93de3da90ba828e4f09c8ad0f346488a33",
|
||||||
|
"9be0be0e64d38a29a9cec9a5c8ef5d873c2bfa5362a4b558da5ff69bc3cbb81e",
|
||||||
|
"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11",
|
||||||
|
"4c7f826edf647462f744b3f16d485f53c797eabdb21cc8a7bb0713283b88e629",
|
||||||
|
"d1621db4d91b23180707b8d4eb9b599fa8ec1dfc2453793a1f83878bd4bbc9d8",
|
||||||
|
"4b74667f89358cd582ad82b16a2d24d5bfcb89ac4b1347ee80e5674a13ba78b2",
|
||||||
|
"83d999a148625c3d2bb819af3064c0f6a12d7da88f68b2c69221f3a746171d19",
|
||||||
|
"b6494a74d18a2dfa3f80ced9fadae35807716fce1071e4de19e2d746b6d87606",
|
||||||
|
"b9c411db4036219e3dfcbe28d60e550b46cce86260fcf2c65d281258e437556f",
|
||||||
|
"2590201e2919a8aa6568c88900192aa54ef00e6c0974a5b0432f52614a841ec8",
|
||||||
|
"c15a5a65986e7ab4134dee3ab85254da5c5d4b04e78b4f16c82837192d355185",
|
||||||
|
"dab6c6065c439b9bafb0b0f1ff5a0c68273bce5c1959a4158ad6a70851f507b6",
|
||||||
|
"baf27a4cc4da49913e7fdecc951fd3b971c9279959af62b02b761a043c33384c",
|
||||||
|
"6c237d8b3b120251c38c230c06d9e48f0d3017657c5b65c8c36112eb15c52aeb",
|
||||||
|
"f173040998481bcb2534a53433eafb8d6ea4c7b0e1fc64572830471fe43fc77d",
|
||||||
|
"36732cc35fe56185af1b11160a393d6c73a1fe41ddf1184c10394c28ca5d627b",
|
||||||
|
"126103bfddc8df256b6e0abfd7f3797c80dcc4ea88f7c2f87dd4104220b4d65f",
|
||||||
|
"457e17b7ea97a845a0d1fa8feda9976596678e3a8af46dc6671d40e050ce857d",
|
||||||
|
"1739d937dc8c0c7370aa27585938c119e25c41f6c441a5d34c6d38503e3136ef",
|
||||||
|
"b676ded7c768d66a757aa3967b1243d90bf57afb09d1044d3219d8d424e4aea0",
|
||||||
|
"33bd77e5394520747faae1394a4af5fa47f404389676375b6dc7be865ed81452",
|
||||||
|
"fe7f6bc6f7338b76bbf80db402ade65953e20b2f23e66e898204b63cc42539a3",
|
||||||
|
"4f83ef69228b3e09b0abc11ded9e6b85319c0b7fef1a044b8ee9970e38441817",
|
||||||
|
"4523be58d395b1b196a9b8c82b038b6895cb02b683d0c253a955068dba1facd0",
|
||||||
|
"d8f38b894b42f7008305cebf17b48925654f22b180c5861b81141f80ccf72848",
|
||||||
|
"9c557e253213c127a86e333ff01c9f12f63091957efafd878d220a0e2cb1193e",
|
||||||
|
"9eab64e92219ccedb15ea9b75ababaa4ae831451019394e0e3336390c3a742d8",
|
||||||
|
"43dedbafef3748c3f9146b961c9b87a3f5cdb1ccb50b4f5890e408702a27a506",
|
||||||
|
"17717ad4d20e2a425cda0a2195624a0a4a73c4f6975f16b1593fc87fa46f2d58",
|
||||||
|
"ee6ea13ab9fe5c4a68eaf9b1a34fe014a66b40117c50ee2a614f4cda959b6e74",
|
||||||
|
"4d023ce9dfd75a7f3075b8e8e084008be17a1f750c63b5de721e6ef883adc765",
|
||||||
|
"d91191e30e00444b942c0e82cad470b32af171764c2275bee0bd99377efd4075",
|
||||||
|
"4eb88310d6b4ed95c6d66a395b3d3cf559b85faec8f7691dafd405a92e055d6d",
|
||||||
|
"0aeb0814c99a13df50643ca27b831a92aaae6366f54e9c276166347aa037d63a",
|
||||||
|
"16f1a0100d4cfffbcc4230e8e0e4290cc5849c1adc64d6653fda07c031b1074b",
|
||||||
|
"148d1366a5e4672b1321adf00321778f86a2371a4bdbe99133f28df0b3d32fa1",
|
||||||
|
"7076f6592de184f9e912c617c46e5e83bad91d3b7f88b7b54cc73bf8ca493321",
|
||||||
|
"dea51494fec5947d27ca659b73dd281ff5bdba3f89f5da1977a731ad0c22e725",
|
||||||
|
"aa97e3d392b97c21327081cdb3cb674dfa8c9c663493db43799a4079555ad1b1",
|
||||||
|
"b7dfbebf760efb2c756b3cae22baafdbbdf55abb782f85e93b4db804d5cba7e3",
|
||||||
|
"ca696d7edb4a86be7c7d9872bd2f9a44440cf8e2de7853536cbb3a60ae89641f",
|
||||||
|
"ff27d01cb1e56fb58580306c7ba76bb037bf211c5b573c56e4e70ca858755af0",
|
||||||
|
"58dece9ff68d021afe76d549b9e48e6cb7b06a5c14cdf45332c8ed7321d6f211",
|
||||||
|
"78688c1f371e7b923d95368c9298cca06c1ec0a89ea897aa181bd60091121fea",
|
||||||
|
"9e8dd91d21e867dec464868a8d1f4a27c0e113c53e32f2bec0a7c6e25ad2e9d5",
|
||||||
|
"6d028aa49aa1f584b3d35aee9fcee8e3c0d81108114289aa046a7969b21eb5f5",
|
||||||
|
"2c470abbac95a49cd0ed5b3b9e628ffda1dbb03c14caba1a225a9b8bf1dc9d5f",
|
||||||
|
"9d7af6946b320b3ba6b4d386de2b2cf3f8ac52fdcb63f3343d1a8362693a3ce5",
|
||||||
|
"d7a4345c3ead1ea7a34bd6aae43c63cbd81941d9ba019fe972843e5ce78e3187",
|
||||||
|
"00f471f6312ce408f7eb56592a2b6c6b5f54ac2967c77f4c1498903b598e1b16",
|
||||||
|
"41e9e2c8398583b90204f8e35c2a4c036aeebac6d05dbdc3e7fb44a1d6bd65a2",
|
||||||
|
"0d978064b9054c023111926050d983573dac2aff16bb8a7497fde8ad725357c0",
|
||||||
|
"507f5bf8367c1883f115ddf9ee95f79ea693c720eb5a5a8718443a99fa308954",
|
||||||
|
"9a090f86adf9fbdc37de2a14745e73d6e1fd096d3da0670b6795ce5ad3cfeea3",
|
||||||
|
"f2b7c5787424c8f9cf6c4480eb99f4a3770cc06337a4f0d1b109ba849b464193",
|
||||||
|
"6ffe93bb72d4ac788fd8be2dadf5bb4a2f14a330d530b0e68bd733c9744c6619",
|
||||||
|
"3ebc74907d1f928f209ef210e872cac033eaf3ff89e6853286d45d91e351ef9e",
|
||||||
|
"da56c54b5e6749d65ad038c196478794af94e4fa5a4efdc20b49981e4ec566c3",
|
||||||
|
"266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5",
|
||||||
|
"5b705e6cb602425c019202dd070a0c009b040ac19960eeef2d8a8fab25c1efe5",
|
||||||
|
"9ec7a778167afb1d30c4833de9322da0c08ba71a69e1911d5578d3144bb56437",
|
||||||
|
"e771af0b05c8e95fcdf6feb3500544d2fb1ccd384788e9f490bb3ee28e8ed66f",
|
||||||
|
"40b9c85fffeafc1cadf8c30a4e5c88660ff6e4971a0dc723d5ab674b5e61b451",
|
||||||
|
"efe5d120df0cc290fa748727fb45ac487caad346d4f2293ab069e8f01fc51981",
|
||||||
|
"408f636bd26fcc5f29889033b447cb2411f60ab1b8a5fc8cb3842dab758fdeb5",
|
||||||
|
"d8a2c33f2e2ff3a9d4ff2a5593f3d5a59e9167fa5ded063d0e49891776611e0c",
|
||||||
|
"02a11d1545114ab63c29958093c91b9f88618e56fee037b9d2fabcff32f62ea9",
|
||||||
|
"1bbd7fdf68eaf5c19446c3aaf63b39dd4a8e33548bc96f6bd239a4124d8f229e",
|
||||||
|
"726a1e261cc6474674e8285e3951b3bb139be9a773d1acf49dc868db861a1c11",
|
||||||
|
"167e7fe01a76b6bec9d2a9b196b18c72e150e985fbeb46ee651869e7b4032785",
|
||||||
|
"c88f94f0a391b9aaa1ffefd645253b1a968b0a422a876ea48920a95d45c33f47",
|
||||||
|
"cd169bd8fbd5179e2a8d498ffc31d3ae0e40825ff2b8a85ea359c4455a107ca8",
|
||||||
|
"fd38f135ef675eac5e93d5b2a738c41777c250188031caf1dcf07b1687a1fe49",
|
||||||
|
"6b4c612991132cf4c6c390dceaae75041b9954ba4f9c465aca70beb350815a57",
|
||||||
|
"8fec426247845bdd26f36ae4f737508c15dbec07d43ce18f8c136ab9e35ac212",
|
||||||
|
"af551accea482000bdccb34bd3c521558e1f353773a3caed83a147921c369ea1",
|
||||||
|
"a664a4973cd23e9f3b35a62429f7671aba2c2ae68c03313913b5c2d77269d906",
|
||||||
|
"18f54af1e10c5bb7a35468b0f62b295d12347903c9f95738d065c84bef1402ef",
|
||||||
|
"be39043cc12efbddfee564d95da751a71df6c139e2def45c431cadeb4a573ca3",
|
||||||
|
"01ddee289b1a2e90874ca3428a7a414764a6cad1abfaa985c201e7aada16d38c",
|
||||||
|
"da25cf7b457bddb6b7bc8e1b0146c0fa85373807d6efdac955199fd01fd53c1f",
|
||||||
|
"ec380784d96b93d404166a6bf1a778227a94a02bdf499f94b5f48f61f5b1350f",
|
||||||
|
"6538925ebfb661f418d8c7d074bee2e8afd778701dd89070c2da936d571e55c3",
|
||||||
|
"9edd72eb23222c969379d90d60ec82891b7c827188bb28510a863f59cb697b0a",
|
||||||
|
"09222857afceb23c66c99fc93d8e5ebda6d7aad901eb38af73c508f117685012",
|
||||||
|
"744ecc9a119a92da88b1f448b4030cdbc2fec5c37ea06ebdd026e742b002af7f",
|
||||||
|
"f531a8672baa2415b271e866dbe11fb08640f6c0e5d98f918bd0308e7169b5b7",
|
||||||
|
"44dc1c2db9c3fbd7bee9257eceb52be3cf8c40baf7b63f46e56b58a131c74f0b",
|
||||||
|
"89e14be49ed0073da83b678279cd29ba5ad86cf000b6a3d1a4c3dc4aa4fdd02c",
|
||||||
|
"8fb140b4e8ddef97ce4b821d247278a1a4353362623f64021484b372f948000c",
|
||||||
|
"72f9755501e1a4464f7277d86120f67e7f7ec3a84ef6813cc7606bf5e0870ff3",
|
||||||
|
"3d99feac152027ede63326aa4f43d4ca88e4cd27296b96fe18c55d496a8f6340",
|
||||||
|
"2540d50aeb9be889c3bd050c9cc849b57b156a2759b48084a83db59aa9056eb4",
|
||||||
|
"b66be78da89991544a05c3a2b63da1d15eefe8e9a1bb6a4369f8616865bd6b7c",
|
||||||
|
"2f5de0003db84ecd5449128350c66c7fb63e9d02b250d84af84f463e2f9bcef1",
|
||||||
|
"2045369fc115b138d1438f98d3c29916986c9fde6b8203f7ff8699f0faee1c93",
|
||||||
|
"ae1008d23930b776c18092f6eab41e4b09fcf3f03f3641b1b4e6ee3aa166d760",
|
||||||
|
"1ec454734dcbf6fe54901ce25c0c7c6bca5edd89443416761fadc321d38df139",
|
||||||
|
"b2d670de53b27691c0c3400225b65c35a26d06093bcc41f48ffc71e0907f9d4a",
|
||||||
|
"ac3f6afe17593f61810513dac9a1e544e87b9ce91b27d37b88ec58fbaa9014aa",
|
||||||
|
"d1f3c71639ae3bba17ffc6c8deb1fdb3a56506b3492213d033528cc291523704",
|
||||||
|
"6b4a29bbd43d1d0eeead384f512dbb591ce9407d27dba48ad54b00d9d2e1972b",
|
||||||
|
"84de08882b6e36705cf6592ee58e632dd6e092dd61c13192fc80cbbc0cbc82cc",
|
||||||
|
"d3d74124ddfb5bdc61b8f18d17c3335bbb4f8c71182a35ee27314a49a4eb7b1d",
|
||||||
|
"a008def15796fba9a0d6fab04e8fd57089285d9fd505da5a83fe8aad57a3564d",
|
||||||
|
"eb7246eb8e26b0c48dd4f9c2a822a0f4d5c84138937195090932b61a2d756051",
|
||||||
|
"683211bd155c7b764e4b99ba263a151d81209be7a566a2bb1971dc1bbd3b715e",
|
||||||
|
"5468bceeb74ce35cb4173dcc9974bddac9e894a74bf3d44f9ca8b7554605c9ed",
|
||||||
|
"78ce6faa72264387284e647ba6938995735ec8c7d5c5a65737e55130f026307d",
|
||||||
|
"2754fc862d6bc0b7c3971046612d942563d181c187a391e180ed6b00f80e7e5b",
|
||||||
|
"f1725586a402c06aec818d1478a45aaa0dc16c7a9c4869d97c350336d16f8e43",
|
||||||
|
"6a359852238dc902aed19fbbf6a055f9abf21c1ca8915d1c4e27f50df2f290d9",
|
||||||
|
"9e4954853fca260cecf983f098e5204c68b2bdfebde91f1f7b25c10b566d50f8",
|
||||||
|
"3356de61b39647931ce8b2140b2bab837e0810c0ef515bbe92de0248040b8bdd",
|
||||||
|
"3e294d2fd339bb16a5403a86e3664947dd408c4d87a0066524f8a573ae53ca8e",
|
||||||
|
"21335073401a310cc9179fe3a77e9666710cfdf630dfd840f972c183a244b1ad",
|
||||||
|
"987096ef8a2853fea1a31b0ed5276503da291536f167bbf7f3f991c9f05d6d7f",
|
||||||
|
"7a78fbfec68c2b3ab6084f1f808321ba3b5ea47502c41115902013e648e76288",
|
||||||
|
"c12a2bcb002fd74b4d342f9b942c24c44cc46d5ed39201245a8b6f4162e7efce",
|
||||||
|
"8867bed93e89c93d0d8ac98b2443c5554799edb9190346946b12e03f13664450",
|
||||||
|
"9b61cd02adac4b18fbcc06237e7469b07e276faf6ec4ecb34b030c2e385892a0",
|
||||||
|
"0463223adf38df9a22a7fb07999a638fdd42d8437573e0bf19c43e013b14d673",
|
||||||
|
"9989500413fb756d8437912cc32be0730dbe1bfc6b5d2eef759e1456c239f905",
|
||||||
|
"17e2889fba01021d048a13fd0ba108ad31c38326295460c21e69c43fa8fbe515",
|
||||||
|
"6c6c253fe26a5b2abf440124e35dcaa39e891cd28274431ba49da5c11d89747d",
|
||||||
|
"9d065f84c0cba7b0ef86f5d2d155e6ce01178a8a33e194f9999b7497b1b2201b",
|
||||||
|
"5ffb8e1b6b629c0e34a013f9298ebb0759b98a3d24029916321d5eb4255b6735",
|
||||||
|
"3fc5f8553abd753ac47967c4c468cfd08e8cb9dee71b79e12d5adab205bc04d3",
|
||||||
|
"ff82c8b53aa53a9705200690b91c572e2e4918f1a88de5d923ac06fa4560fa19",
|
||||||
|
"4d4ab737e2fbb5af0fd590b4b7e8c6fe76d3a02a9791ef7fdacf601f9e50fad8",
|
||||||
|
"5eca50a04afaefe55659fb74810b42654e2268c1acca6e53801b9862db74a83a",
|
||||||
|
"d700fc10d457eeae4f02eb04d715a054837e68a2e2d010971382c5e1016dc99e",
|
||||||
|
"af321973db865bb33fbc50a4de67fc0e6808d812c6e4dfd9cbc2fd50275b1dfd",
|
||||||
|
"bbf923aa9246065f88c40c7d9bf61cccc0ff3fcff065a8cb2ff4cfbb62088f1e",
|
||||||
|
"268b948b5aab4bab0e5430ee49e3cff11776cf183df93b32159f9670ed541495",
|
||||||
|
"3d2e51508699f98f0f2bdbe7a45b673c687fe6420f466dc296d90b908d51d594",
|
||||||
|
"4d4fb5ff0afb8c04e6c6e03f51281b664576f985e5bc34a3a7ee310a1e821f47",
|
||||||
|
"9b12847f3d28bf8850ebc03f8d495a1ae8f9a2c86dbda295c90556619a3ee831",
|
||||||
|
"733c5427f55ceba01a0f6607ab0fd11832bbb27d7db17b570e7eb7b68a081d9a",
|
||||||
|
"4bc7982c4ee4078b2ada5340ae673f18d3b6a664b1f97e8d6799e6074cb5c39d",
|
||||||
|
"afa0f26dbf3e674630d1cd6499e86c14f316cd4f78c6ab73bb85b00aa9c50a57",
|
||||||
|
"c301f13372c8f0d9bc8186d874fa45fa33aede13e66f4187a3bd22ee41c95b2a",
|
||||||
|
"548a29f145187fc97689f8ae67944627723c315c163b0dbb88842e50c681d7ca",
|
||||||
|
"d0a1ffb8761b974cec4a3be8cbcb2e96a7090dcf465ffeac839aa4ca20c9a59e",
|
||||||
|
"faaf47af27e3de06e83f346fc6ccea0aabfc7520d82ffe90c48dfcd740c69caa",
|
||||||
|
"3eacaa768326d7dce80f6ee17ada199bebe7eb3c1a60b39b14e0a58bbac66fe4",
|
||||||
|
"7f5237e9f77a22c4a89624c7ac31cae797d8ac4144b02493890d54fee7399bcd",
|
||||||
|
"d84517802a434757c56ae8642bffb4d26e5ade0712053750215680f5896e579b",
|
||||||
|
"bdb96ad31ac6af123c7683c55775ee2138da0f8f011e3994d56a27270e692575",
|
||||||
|
"aab1b0caf13b9bd26a62cf8b3b20f9bfaa0e56f3ec42196a00fedf432e07d739",
|
||||||
|
"c230edd34ca5c8318bf4592ac056cde37519d395c0904c37ea1c650b8ad4a712",
|
||||||
|
"ce41c1698a8c042218bc586f0b9ec8d5bffa3dcbcea09bd59db9d0d92c3fc0b4",
|
||||||
|
"b9a537523bba2fcdae857d90d8a760de4f2139c9f90d986f747ce7d0ec0d173d",
|
||||||
|
"1a6e0aeff1dba7ba121fbeb33bf3162901495df3fcb4e4a40423e1c10edf0dca",
|
||||||
|
"21b419102da8fc0ba90484aec934bf55b7abcf75eedb39124e8d75e491f41a5e",
|
||||||
|
"2183e94758481d0f124fbd93c56ccaa45e7e545ceeb8d52848f98253f497b975",
|
||||||
|
"e3f98bfb9cbeb7563a139983602e50f616cb7ebb06c3295b8ee377328f051206",
|
||||||
|
"b5b9b84d1723994d06013606227fb5b91f9de8820b04cf848d1dccc23d054f39",
|
||||||
|
"07adfda9c5adc80881bb2a5220f6e3181e0c043b90fa115c4f183464022968e6",
|
||||||
|
"facdaf1ce758bdf04cdf1a1fa32a3564a608d4abc2481a286ffc178f86953ef0",
|
||||||
|
"efc83f01c8fb309df2c8866b8c7924cc8b6f0580afdde1d6e16e2b6107c2862c",
|
||||||
|
"52b4a076bcbbbdc3a1aefa3735816cf74993b1b8db202b01c883c58be7fad8bd",
|
||||||
|
"c6f7077f1699d50cf92a9652bfebffac05fc6842b9ee391089d959b8ad5d48fd",
|
||||||
|
"e7424ad457e512fdf4764a56bf6d428a06a13a1006af1fb8e0fe32f6d03265c7",
|
||||||
|
"27797bd4e5ee52db0a197668c92b9a3e7e237e1f9fa73a10c38d731c294cfc9a",
|
||||||
|
"7bdef7bdebb8721f77927d0e77c66059360fa62371fdf15f3add93923a613229",
|
||||||
|
"3335d373e6c1b5bc669b4b1220c08728ea8ce622e5a7cfeeb4c0001d91ded1de",
|
||||||
|
"645681b9d067b1a362c4bee8ddff987d2466d49905c26cb8fec5e6fb73af5c84",
|
||||||
|
"06b7819d7f1c7f5472118266ed7bca8785dceae09e36ea3a4af665c6d1d8327c",
|
||||||
|
"7a6b8c7de171955c214ded7e35cc782cd6dddfd141abb1929c632f69348e6f49",
|
||||||
|
"eb882b0bb659bf72235020a0b884c4a7d817e0af3903715736b146188b1d0868",
|
||||||
|
"2ae6c71323a225ecfa8cf655600ebbe12b1019ff36bf02726d82d095aab29729",
|
||||||
|
"c2f85a06279a7bfa7f2477a3cee907990231a13d17b54524738504bd12e0c86c",
|
||||||
|
"f1b911af1c7a56073e3b83ba7eaa681467040e0fbbdd265445aa80e65c274c22",
|
||||||
|
"a54c2ae6ec6ac06b4d7b45c483eab86ac226b8ecfa99163ef7cc000da9b40895",
|
||||||
|
"bbc73cae41502ddad7a4112586dcaf4422810d60aa4b57c637ccd1a746b07844",
|
||||||
|
"218238431393959d6c8617a3bd899303a96609b44a644e973891038a7de8622d",
|
||||||
|
"59cacbd83ad5c54ad91dacf51a49c06e0bef730ac0e7c235a6f6fa29b9230f02",
|
||||||
|
"ba80990666ef0b6f4ba5059347beb13242921e54669e680064ca755256a1e3a6",
|
||||||
|
"031db544f3158556508b321db59afd62c5bb1592021e5dfd9ff87dca0ad27d8c",
|
||||||
|
"ee85604f8ec6e4e24f8eaf2a624d042ebd431dae448fe11779adcfb6bb78575e",
|
||||||
|
"266ee74062e8dae0aeddfcd0f72725107598efaa80c1a7176d6ee6dd302bce4c",
|
||||||
|
"b83a28b7e4e5d20bd960c5faeb6625f95529166b8bdb045d42634a2f35919450",
|
||||||
|
"dbe0605a9c73172bad7523a327b236d55ea4b634e80e78a9013db791f8fd5b2c",
|
||||||
|
"1e53e900c3bbc5ead295215efe27b2c8d5fbd15fb3dd810da3063674cb7213b2",
|
||||||
|
"832a2b3cef4b1754c4a7572964a44db64d19edf627ec45179b519d0a5eae8199",
|
||||||
|
"4c800257a588a82849d049817c2bdaad984b25a45ad9f6dad66e47d3b47e3b2f",
|
||||||
|
"3743244390be53473a7e3b3b8d04dce83f6c9514b81a997fb3b123c072ef9f78",
|
||||||
|
"f96c3d76497074c4c83a7b3823380e77dc73d5a9494fd2e053e4a1453e17824b",
|
||||||
|
"d04ecf33a303a59852fdb681ed8b412201ba85d8d2199aec73cb62681d62aa90",
|
||||||
|
"0cca6201658d5d98239c1511ef402562ff7d72446fb201a8d1857c39e369c9fa",
|
||||||
|
"61066504617ee79387021e18c89fb79d1ddbc3e7bff19cf2298f40466f8715e9",
|
||||||
|
"7adb520c3ac7cb6dc8253508df0ce1d975da49fefda9b5c956744a049d230ace",
|
||||||
|
"7579076d9aff0a4cfdefa7e2045f2486c7e5d8bc63bfc6b45397233e1bbfcb19",
|
||||||
|
"9ec078ef9ca31e1bdbb97175dde1cb00bf9f7225e6f622ccc8d367302e220497",
|
||||||
|
"93e174736c4719f80627854aca8b67efd0b59558c8ece267a8eccbbd2e7c5535",
|
||||||
|
"e62f419a0e16607b96ff10ecb00249af7d4b69c7d121e4b04130c61cc998c32e",
|
||||||
|
"171ddd43dab1af0d1fb14029287152a4c89296890e0607cf5e7ba73c73fdf1a5",
|
||||||
|
"604e96e099936a104883958b040b47672e0f048c98ac793f37ffe4c720279eb2",
|
||||||
|
"7726c437ccf791f6ded97dbac1846e62019e5fbd24f42e9db2f640f231c3c09a",
|
||||||
|
"1bf9f239dca1636149bc2f3fc334077ae959ea9607cacf945ef8f8bb227dc5e1",
|
||||||
|
"fcd818454002a6c47a980393f0549ac6e629d28d5688114bb60d831b5c1832a7",
|
||||||
|
"56172b53f730750b40e63c501b16068dd96a245e7f0551675c0fec9817ee96e0",
|
||||||
|
"260d3a820b7f8de20f4972725999b1af88b0cc5554ca38f9681c8d657e043cc3",
|
||||||
|
"9ba8c688f091ca48de2b0f9bc998e3bc36a0092149f9201767da592849777f1c",
|
||||||
|
"61594d714aa94fe551f604123578c4a6592145f4228ad8601224b1b89ce401b0",
|
||||||
|
"416ca193aa5448b8cca1f09642807765cc0ee299609f972df0614cfb8ea2f2b1",
|
||||||
|
"9b6d95b76a01191a4c778185681ed7f3bced2fffa8e41516ec78240b213285f5",
|
||||||
|
"ee0304bae0d4679bb34347ce3b1b80482262b9812bd0c0d5e19a5e2445043b75",
|
||||||
|
"8027a1877f39e603dafc63279e004b4ed9df861d18ce81d9c43c7d7135da8f65",
|
||||||
|
"42b409ff9b261a985227b1ab92707e706777ac14de24654d7e05f0501b37e003",
|
||||||
|
"99097983b74c70800b182abc6f64046ab70407e9cabcd6cf570a38ada9ef75d5",
|
||||||
|
"de8ca7a6b3f7314e91921d4dc5e915fb7bc2bd32129ea6966322effa48050c4c",
|
||||||
|
"dcb302978215f54f33c3d2d7157ef69fd5058cf488fc43dd75c32b5dcaf47e7a",
|
||||||
|
"7c765d407d3a9d5ea117cb8b8699628560787fc084a0c76afaa449bfbd121d84",
|
||||||
|
"06639a386c9c1014217622ccbcf40908c4f1a0c33e23f8d6d68f4abf655f8f71",
|
||||||
|
"59f1b5faf29904fe94a6a042e2d82d80d68fc16ad7651eba90a8de39f63f8fe8",
|
||||||
|
"174398550d1468a41b98a09f496c38d3694feadef0f0073fd557610384bafb10",
|
||||||
|
"00d52016bd7e4aae8bf8eaa23f42276b649fe557483b5d7684702633dd0fd944",
|
||||||
|
"9a39bf837c868d61ed8cce6a4c7a0eb96f5e5bcc082ad6afdd5496cb614a23fb",
|
||||||
|
"74ffc51cc30150cf79b6cb316d3a15cf332ab29a38fec9eb484ab1551d6d1856",
|
||||||
|
"97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322",
|
||||||
|
"6c6e3e05e1c9d2aae0ed2431544aea411771dd9d81017539af0fd818b2389f28",
|
||||||
|
"23a2cf63ec81e65561acafc655898d2fd0ef190084653fa452413f75e5a3d5bc",
|
||||||
|
"f1f9b0996d4ff1bf75e79e4cc8577c89eb633e68415c7faf74cf17a07bf80bd8",
|
||||||
|
"e3aefda887252a72cee3578d33b2dcd90e9fe53b8bed6347ef5e26f74211adbb",
|
||||||
|
"6b4ec98f02e647e01440b473bbd92a7fae21e01b6aa6c65e32db94a36092272e",
|
||||||
|
"623ed218de81311783656783d6ce690b521a89c4dc09f28962e5bfd4fa549249",
|
||||||
|
"9ce71f1506ccf4b99f234af49bd6202be883a80f95a155c6e9a1c36fd7e780c7",
|
||||||
|
"139fcc6bb304b2879974c59cda61d86d7816ad4ac0f38ee7a724df488060e65d",
|
||||||
|
"0e8c41eb946657188ea6f2aac36c25e393fff4d4149a83679220d66595ff0faa",
|
||||||
|
"59ffbe1fc829decf90655438bd2df3a7b746ef4a04634d4ee9e280bb6ce5f14e",
|
||||||
|
"e4f695f05bb05b231255ccce3d471b8d79c64a65bccc014662d27f0f7e921092",
|
||||||
|
"39cc53c9e3f7d4980b21bea5ebc8a5b9cdf7fa6539430b5a826e8ad527168656",
|
||||||
|
"685fb49563864326e78df461468795b7e47849a27e713281cd8bb75c0547936d",
|
||||||
|
"05e4649832dfb8d1bfa81ea7cbf1c92c4f1cd5052bfc8d5465ba744aa6fa5eb8",
|
||||||
|
"e5cece49ae3fc2c81f50c8e7a93a5fb1e1585380c467e4822234b64a94add617",
|
||||||
|
"dda028cd1b806b4d494cc7f2789b6c2bd7e3c28ff6a267d03acc5ac6e69a05e0",
|
||||||
|
"046c436b2a525059867b40c81e469b6d83001442fc65312c87a7ce7abeb022ff",
|
||||||
|
"676ffea2ec31426a906d7795d7ebae2ba5e61f0b9fa815995b4a299dd085d510",
|
||||||
|
"15b5cf6cdf4fd1c02f28bcce0f197cafae4c8c7c66a3e2e23af9fe610875315e",
|
||||||
|
"c0fb5367cfcb803c5383f98e26524bed9176e6211588f53ec63fe6079cbfd3df",
|
||||||
|
"7ab00f596b0286b77f78af567ee1be2536feee41daee67bd872f1480b7aa65b9",
|
||||||
|
"e8d66519e43b1214ac68f9f2bdbc4386d41ac66b20c5a260b9b04102784074e9",
|
||||||
|
"e6618db6961dc7b91478e0fa78c4c1b6699009981526693bd5e273972550860c",
|
||||||
|
"b1e1185884a6d14bbfce3899cb53e8183adde642f264d0ff4f1745371e06134c",
|
||||||
|
"cae5b7ea348afefc4c102bb7b125c4928f114739a27b877c6bcfbe5a79280384",
|
||||||
|
"ecbe372132a9323b813eeb48f8dfcedaeca00e2887af181b063c6cfa13ed8ea1",
|
||||||
|
"52387c6b99cc42aac51916b08b7b51d2baddfc19f2ba08d82a48432849dbdfb2",
|
||||||
|
"3c39a7b53dec9ac85acf08b267637a9841e6df7b7b0f5e2ac56a8cf107de37da",
|
||||||
|
"f8e6c64342f1e052480630e27e1016dce35fc3a614e60434fef4aa2503328ca9",
|
||||||
|
"fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b",
|
||||||
|
"433e80c14ff7b8e16e179ccec35f55833df7dd5a5a063d23117b4b01b6f97170",
|
||||||
|
"b7c6f6915cfa9a62fff6a1f02604de88c23c6c6c6d1b8f62c7cc10749f307e81",
|
||||||
|
"ddf03aca85ade039e6742d5bef3df352df199d0d31e22b9858e7eda85cb3bbbe",
|
||||||
|
"d36e8083fa7b36daee646cb8b3f99feaa3d89e5a396508741f003e21ac0b6bec",
|
||||||
|
"ec79b568bdea63ca6091f5b84b0c639c10a0919e175fa09a4de3154f82906f25",
|
||||||
|
"8cd2d0f8310f7009e94f50231870756cb39ba68f37506044910e2f71482b1788",
|
||||||
|
"0eef96197f5c6be3859b6817e6a5736685856c416e29a2925bd5a15b2a57c8b1",
|
||||||
|
"04918dfc36c93e7db6cc0d60f37e1522f1c36b64d3f4b424c532d7c595febbc5",
|
||||||
|
"c8383d81dd24406745b68409be40d6721c301029464067fcc50a25ddf9139549",
|
||||||
|
"3b3a42d34cf0a1402d18d536c9d2ac2eb1c6019a9153be57084c8165d192e325",
|
||||||
|
"da18e9860040f3bf493876fc16b1a912ae5a6f6fa8d5159c3de2b8233a0d9851",
|
||||||
|
"e3fc673fc5f99cc554d0ff47756795647d25cb6e6658f912d114ae6429d35d35",
|
||||||
|
"3aa5817273c3b2f94f491840e0472f049d0f10009e23de63006166bca9b36ea3",
|
||||||
|
"bbb5dda0e15567979f0543407bdc2033d6f0bbb30f72512a981cfdb2f09e2747",
|
||||||
|
"1096f6be0a4d7f0ecc2df4ed2c8683f143efc81eeba3ece6daadd2fca74c7ecc",
|
||||||
|
"d76726da1b64e8679d8b6e66facf551ba96f2612de5a171fac818ee85ce3e5fe",
|
||||||
|
"27487c9600b16b24a1bfb0519cfe4a5d1ad84959e3cce5d6d7a99d48660a1f78",
|
||||||
|
"5d3ab876c206a37ad3b094e20bfc3941df3fa21a15ac8ea76d6918473789669a",
|
||||||
|
"6b1b8dac34ffc61d464dfeef00e4a84a604e172ef6391fb629293d6f1666148c",
|
||||||
|
"6fb266012c3008303e54ae55140b46957e9978098401dda34f4d921a275bf8bb",
|
||||||
|
"53a91e3a64d1f658e983ac1e4f9e0c697f8f33e01d8debe439f4c1a92113f592",
|
||||||
|
"5082984480f3b27891840a2037512739149678efc2ac981ca8cd016d02304efd",
|
||||||
|
"7b849efa5604b58d50c419637b9873847dbf957081d526136c3a49b7357cd617",
|
||||||
|
"f53b9d91a8cd177fb4a1cf081a1b6d58759a381ef120a7c5a18c0e70cae80983",
|
||||||
|
"cfd7df62799a22e384a4ab5da8c4026c875b119d0f47c2716b20cdac9cc1f1a6",
|
||||||
|
"d83b5ef189df7e884627294b752969547814c3cfe38995cf207c040e03bbe7a4",
|
||||||
|
"96f652249b0946e1575d78a8bc7450123c8e64f1c56f6b2f93bc23fb249ed85a",
|
||||||
|
"d60bdad03468f5f8c85b1b10db977e310a5aafab33750dfadb37488b02bfc8d7",
|
||||||
|
"9839f160d893daae661c84168e07f46f0e1e9746feb8439a6d76738b4ad32eaa",
|
||||||
|
"453a656903a031395d450f318211a6ec54cd79049a851f92cd6702c65ff5f5bd",
|
||||||
|
"1634b87b5fcfd4a6c4ff2f2de17450ccce46f9abe0b02a71876c596ec165bfed",
|
||||||
|
"24480686b56234a240fd9827209b584847f3d4f9657f0d9a97aec5320a264acb",
|
||||||
|
"f4d1866e8599563c52ceeedf11c28b8567e465c6e9a91df92add535d57f02ab0",
|
||||||
|
"805b34f708837dfb3e7f05815ac5760564628b58d5a0ce839ccbb6ef3620fac3",
|
||||||
|
"659a74f6cfbc7c252c58d93452b9d9575e36c464aa6544c6375227c9166a6ed9",
|
||||||
|
"75d737c3472471029c44876b330d2284288a42779b591a2ed4daa1c6c07efaf7",
|
||||||
|
"dac1d8c5a9fe94f224e095b52577c33c2cc2b8f3a2d6ad9cbd46845af8c987f0",
|
||||||
|
"be7358c4fe50148cccafc02ea205d80145e253889aa3958daafa8637047c840e",
|
||||||
|
"30e8cbf1427c137fa60674a639431c19a9d6f4c07fd2959df83158e674fccbaa",
|
||||||
|
"7f573f55d875ce8edc528edf822949fd2ab9f9c65d914a40225663b0a697be07",
|
||||||
|
"781a1527055f74c1f70230f10384609b34548f8ab6a0a6caa74025827f9fdae5",
|
||||||
|
"d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f",
|
||||||
|
"a4237e420cdb0b3231d171fe879bcae37a2db7abf2f12a337b975337618c3ac2",
|
||||||
|
"7ff4d89f90845ac4d7a50a259163798e0f446e61d4c943cc89637beff567ad02",
|
||||||
|
"48dbb5e717a6221d64fd13ba12794bc28e5067ac1d7632ee9437d533772750df",
|
||||||
|
"efba340bd479176486e5a2281c97ac4a90fdcf86ec9c13a78c3182ab877cd19b",
|
||||||
|
"1021c8921548fa89abb4cc7e8668a3a8dcebae0a4c323ffeaf570438832d6993",
|
||||||
|
"c67cd3e1a83daa56cff16f635db2fdb9ed9619300298d4701a58e68e84098345",
|
||||||
|
"4ad6fa2d16e2a9b576c863b4cf7404a70d4dc320c0c447d10ad6ff58993eacc8",
|
||||||
|
"e568a76a4f8836be296d405eb41034260d55e2361e4b2ef88350a4003bbd5f9b",
|
||||||
|
"ebdee92945ef05283be0ac3de25787c81a6a58a10f568f9c6b61d9dd513adbad",
|
||||||
|
"6e8f3edfa28bfc8057d735794f76b697bcf18fb894a5a37a132693ebda31a464",
|
||||||
|
"576d23dc3db2056d208849462fee358cf9f0f3310a2c63cb6c267a4b9f5848f9",
|
||||||
|
"18905d0a5d623ab81a98ba98c582bd5f57f2506c6b808905fc599d5a0b229b08",
|
||||||
|
"a9046cc9175dc5a45fb93a2c890f9a8b18c707fa6d695771aab9300081d3e21a",
|
||||||
|
"7a69e5f62fcc20e81beea7461a945e6531f8c7944200d0b3cb4cc63556c44106",
|
||||||
|
"fd0266485777bd73e97c7c37f520c83c82e362fe4c25a6be93f3380083d4646b",
|
||||||
|
"4b29db7a76f3b4fbc0a4fffc092e41c14f1a1a975a462d87e82827af03719cb2",
|
||||||
|
"df1a6cb6c95a5bdd2a69e4fa921061da950fc0bb0b3529d04ca75e0c11f871df",
|
||||||
|
"08bfc00b7f72e015f45c326f486bec16e4d5236b70e44543f1c5e86a8e21c76a",
|
||||||
|
"1e908fbc1d131c17a87f32069f53f64f45c75f91a2f6d43f8aa6410974da5562",
|
||||||
|
"b3a737d014a7e75f44b0f5afbd752f9bcc2abe54f60dbbebc3681b6e16611967",
|
||||||
|
"d3052ca3e3d523b1ec80671eb1bba0517a2f522e195778dc83dd03a8d84a170e",
|
||||||
|
"b98ded4ceaea20790dbcb3c31400692009d34c7f9927c286835a99b7481a5c22",
|
||||||
|
"9e1e498420bc7c35f3e3a78d20045b4c8343986dae48739759bccb2a27e88c53",
|
||||||
|
"141d2053cb29535ad45aa9e865cdec492524f0ec0066496b98b7099daab5d658",
|
||||||
|
"8722c3843c85ddd6162a5cb506e1cb4d6ab0cafb966034f426e55a2ef89a345e",
|
||||||
|
"52dfd21724329920c5c95f5361464e468584136d30030eb29247a7fe6c2c6e36",
|
||||||
|
"d82a91e1013170b10ca7fa0ec800fd0dc6e9335b70c303dadba603fc36802b5f",
|
||||||
|
"6bbb7d71eaa2544215a877e136cd7f490f4625eb56459a0da856cc8296d5df30",
|
||||||
|
"1ebb28301aa1a48248d3723a0ea434bb7d4612ec920fa749583e3f41ce25849f",
|
||||||
|
"00000001505e7e48927046e9bbaa728b1f3b511227e2200c578d6e6bb0c77eb9",
|
||||||
|
"a01b5ba26421374250442e0d23f96e6a4bce429e0175cd0769ad8c585dd5a892",
|
||||||
|
"26d6a946675e603f8de4bf6f9cef442037b70c7eee170ff06ed7673fc34c98f1",
|
||||||
|
"1dd7992ea0ecbda7480ceed35748c3655691133c8c78af947fd24299db8f481f",
|
||||||
|
"cdee943cbb19c51ab847a66d5d774373aa9f63d287246bb59b0827fa5e637400",
|
||||||
|
"3d03c53608415b1d718c7786ee10bdb4e67bced32207e32880ee9e44301a19ec",
|
||||||
|
"170dc4045d6c06275b40bd39f68ca31dbb962094e9763ee460f8341bd40bebca",
|
||||||
|
"db1abbff170320730e5ace672ad7217161b8935afc1a896ae2fecf903c159932",
|
||||||
|
"b0ac2c26eabdb0e0a9b0d10fd1458ca73c575b19d65e13f0e7484cbee84038b3",
|
||||||
|
"c1e7fc21b4f9c199e6086e095639f0f16a4e4884544547ce8a653ed7b5b6c4a7",
|
||||||
|
"813c2662366a12f6337b951c048552fd3c4894e403cab701634dcd803786dc09",
|
||||||
|
"fd0bcf8cd1aee83fe75e6c0fdfc543845e5bc3f50d26d2d5e5c6d3fa521f98c0",
|
||||||
|
"45fd1955f590da87c1fd2edb99d17accf235ec3ebf0afe1d3306ade42693c6e9",
|
||||||
|
"27938497226683c701e2843c6db580e2f0e25f5a198f4c3397e3a0a27764215d",
|
||||||
|
"2321edfd415f9558605b4d7a7083c52624e8922ae86bb2ae359fbf829724111a",
|
||||||
|
"0461fcbecc4c3374439932d6b8f11269ccdb7cc973ad7a50ae362db135a474dd",
|
||||||
|
"1f8e182bf72d61cb78dcd6b96dd3be8b874b8881da6630757b6508970f67230c",
|
||||||
|
"c6603b0f1ccfec625d9c08b753e4f774eaf7d1cf2769223125b5fd4da728019e",
|
||||||
|
"296842eaaed9be5ae0668da09fe48aac0521c4af859ad547d93145e5ac34c17e",
|
||||||
|
"88f8707a45e825a13ed383332abe6e2f104ab44d877918be22550083a2b59e60",
|
||||||
|
"27a20b41a66b35d442302a50ca1baad72c2ed844c8d1224c9f6d50a12752084e",
|
||||||
|
"280e847ef0c82a2a7c4e877c91cd7567474c1431b815d27bbc1017e147d9d77c",
|
||||||
|
"ad9d42203fd2480ea2e5c4c64593a027708aebe2b02aa60bd7b1d666daa5b08d",
|
||||||
|
"bb0174ae21a6cac1a0a9c8b4ac6ebfda56ce51605c315b1824970bc275f7239a",
|
||||||
|
"edb470271297ac5a61f277f3cd14de54c67eb5ccd20ef0d9df29be18685bb004",
|
||||||
|
"9609b093450dd7e0afb389619acdaf2e6a0d6817c93552f3911e05b50ae73e3d",
|
||||||
|
"3e33fd7124f174fc535151937f8718634dd9d856143d4cefb5a10ddaf2f615c0",
|
||||||
|
"463555bb4b0f80fd1376fae628fabfaf7e5e31cd2741d80aa4d225c926bc287e",
|
||||||
|
"916cb5ff07d3b51cef7f6b6b7f5479b1001b401c0e82558ee1a22504c7d507c9",
|
||||||
|
"cc5f259f036683798e4a52071dbb97238702ffb6f0c85af6d273c8ddbe5c0afb",
|
||||||
|
"2ad91f1dca2dcd5fc89e7208d1e5059f0bac0870d63fc3bac21c7a9388fa18fd",
|
||||||
|
"8bf629b3d519a0f8a8390137a445c0eb2f5f2b4a8ed71151de898051e8006f13",
|
||||||
|
"94215f42a96335c87fcb9e881a0bbb62b9a795519e109cf5f9d2ef617681f622",
|
||||||
|
"bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95",
|
||||||
|
"ba5115c37b0f911e530ed6c487ccbd9b737da33fd4b88a9f590860378c06af62",
|
||||||
|
"609f186ca023d658c0fe019570472f59565c8be1dc163b1541fac9d90aa4e8af",
|
||||||
|
"4e5622b575cdbb4d5ded093e48c68cd3f724fad547142f0c1b0aec2f2b2a0b2e",
|
||||||
|
"4df7b43b3a4db4b99e3dbad6bd0f84226726efd63ae7e027f91acbd91b4dba48",
|
||||||
|
"bcbf9644d3f475d00eb9c6e467385ce16d4546c1a24222ccfa542bf776eaba95",
|
||||||
|
"3a06add309fd8419ea4d4e475e9c0dff5909c635d9769bf0728232f3a0683a84",
|
||||||
|
"d2384c4ac623eb9f15ae4cb32ee7a9b03e0202802d52f2395f2ee8f6433151aa",
|
||||||
|
"1d4cc828b657da8c7a101e8657365459b9dc74139bed5d35bd8295b00be2a1ae",
|
||||||
|
"76fcec0e0638351f1d0e0dc4ebaf6dd3d67404126d664547674070f3175273d9",
|
||||||
|
"6707c39e6c53ef945c5df29af78667dc941ed80094994bd264fd806a6e0a3230",
|
||||||
|
"80482e60178c2ce996da6d67577f56a2b2c47ccb1c84c81f2b7960637cb71b78",
|
||||||
|
"147784df719c09fad62bff0493a60b4f5dbbe8579e73f00d207350e8ffdfd65f",
|
||||||
|
"afc0295d2c6e0a1820c214c07312070a4070d52083163d7fe410fa02bf85d9d2",
|
||||||
|
"6a5e3cc17279cbdf051c06d96e3f843cdb296f351d8ca35a6a190c0ab90dbf9a",
|
||||||
|
"3b7fc823611f1aeaea63ee3bf69b25b8aa16ec6e81d1afc39026808fe194354f",
|
||||||
|
"d96fe9c5478d1bb36e9ec40cc678b0bf7ff67e017922a151f925640a8884f291",
|
||||||
|
"1d80e5588de010d137a67c42b03717595f5f510e73e42cfc48f31bae91844d59",
|
||||||
|
"06dde95f0268ce40128bf73ca6e85567b8567688ea52f24dcd5734e77c50f2d9",
|
||||||
|
"f683e87035f7ad4f44e0b98cfbd9537e16455a92cd38cefc4cb31db7557f5ef2",
|
||||||
|
"036533caa872376946d4e4fdea4c1a0441eda38ca2d9d9417bb36006cbaabf58",
|
||||||
|
"7cc328a08ddb2afdf9f9be77beff4c83489ff979721827d628a542f32a247c0e",
|
||||||
|
"f240be2b684f85cc81566f2081386af81d7427ea86250c8bde6b7a8500c761ba",
|
||||||
|
"fb61b93d864e4f0eba766bb8556f2dc0262e8e985012e29ba28508dd52067d98",
|
||||||
|
"0e52122d1eb95cdd8ba5f65815f7d1c9125a8c14d82989eae52ab369eea6c7e4",
|
||||||
|
"04dcaf2552801937d1c20b69adf89646f21b53c17906271d22c7be9bcadb96c0",
|
||||||
|
"0ee827a36e8bb0cfc483cf1872781182c4a16c58acba3ae2d7b155e0370e93b8",
|
||||||
|
"adc14fa3ad590856dd8b80815d367f7c1e6735ad00fd98a86d002fbe9fb535e1",
|
||||||
|
"2bc9be7569515701581d5d765422a17ee3500d8e1f4e7aa53f6be86ae6ba274d",
|
||||||
|
"9a21569255d0a3a9e75f1de2e4c883c9be2e5615887f22b2ecf6b1813bcd587d",
|
||||||
|
"3f3ff7adb39159c42c0aa16d53c0483bfbfad57df22a9e9e9364a306741eb2cf",
|
||||||
|
"e9aa50decff01f2cec1ec2b2e0b34332cf9c92cafdac5a7cc0881a6d26b59854",
|
||||||
|
"c7eda660a6bc8270530e82b4a7712acdea2e31dc0a56f8dc955ac009efd97c86",
|
||||||
|
"787338757fc25d65cd929394d5e7713cf43638e8d259e8dcf5c73b834eb851f2",
|
||||||
|
"7e8ffe414a53ce18a85536eda74d3cbda0da6a98d4fe6c514948002472178c95",
|
||||||
|
"6a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3",
|
||||||
|
"0c371f5ed95076613443e8331c4b60828ed67bcdefaa1698fb5ce9d7b3285ffb",
|
||||||
|
"daa41bedb68591363bf4407f687cb9789cc543ed024bb77c22d2c84d88f54153",
|
||||||
|
"12ee03d11684a125dd87be879c28190415be3f3b1eca6b4ed743bd74ffd880e6",
|
||||||
|
"00dfb20815a71e24572394efdfbf772e56a507921b8287201ab8937496ee8e6d",
|
||||||
|
"94a6a78a5aebbba40bd1aaa2234810132c2d8004bb9177616c413d3c0ddf320e"
|
||||||
|
];
|
||||||
|
const PUBKEY_ARRAYS = generateTestPubkeyArrays();
|
||||||
|
|
||||||
|
// Main testing function
|
||||||
|
async function testRelayPubkeyLimits() {
|
||||||
|
console.log('Starting Nostr Relay Pubkey Filter Test');
|
||||||
|
console.log(`Testing ${RELAYS.length} relays with up to ${MAX_PUBKEYS} pubkeys`);
|
||||||
|
console.log(`Incrementing by ${STEP_SIZE} pubkeys per test, requesting ${EVENT_LIMIT} events per test\n`);
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
|
||||||
|
// Initialize results for each relay
|
||||||
|
for (const relayUrl of RELAYS) {
|
||||||
|
results[relayUrl] = {
|
||||||
|
maxPubkeys: 0,
|
||||||
|
failures: 0,
|
||||||
|
lastSuccess: 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test each pubkey array size
|
||||||
|
for (const pubkeyArray of PUBKEY_ARRAYS) {
|
||||||
|
const pubkeyCount = pubkeyArray.length;
|
||||||
|
console.log(`\nTesting with ${pubkeyCount} pubkeys...`);
|
||||||
|
|
||||||
|
// Test each relay with this pubkey count
|
||||||
|
for (const relayUrl of RELAYS) {
|
||||||
|
if (results[relayUrl].failures >= 2) {
|
||||||
|
console.log(` ${relayUrl}: Skipping (already failed 2 times)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const success = await testRelayWithPubkeys(relayUrl, pubkeyArray);
|
||||||
|
if (success) {
|
||||||
|
results[relayUrl].maxPubkeys = pubkeyCount;
|
||||||
|
results[relayUrl].lastSuccess = pubkeyCount;
|
||||||
|
results[relayUrl].failures = 0; // Reset failures on success
|
||||||
|
console.log(` ${relayUrl}: ✓ Success`);
|
||||||
|
} else {
|
||||||
|
results[relayUrl].failures++;
|
||||||
|
console.log(` ${relayUrl}: ✗ Failed (${results[relayUrl].failures}/2)`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results[relayUrl].failures++;
|
||||||
|
console.log(` ${relayUrl}: ✗ Error (${results[relayUrl].failures}/2): ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print final results
|
||||||
|
console.log('\n=== FINAL RESULTS ===');
|
||||||
|
for (const relayUrl of RELAYS) {
|
||||||
|
const result = results[relayUrl];
|
||||||
|
console.log(`${relayUrl}: ${result.maxPubkeys} pubkeys (last success: ${result.lastSuccess})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test a single relay with a specific pubkey array
|
||||||
|
async function testRelayWithPubkeys(relayUrl, pubkeys) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const ws = new WebSocket(relayUrl);
|
||||||
|
let receivedEOSE = false;
|
||||||
|
let subscriptionId = 'test_' + Math.random().toString(36).substr(2, 9);
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
ws.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.on('open', () => {
|
||||||
|
console.log(`Connected to ${relayUrl}`);
|
||||||
|
// Send subscription request
|
||||||
|
const filter = {
|
||||||
|
kinds: [EVENT_KIND],
|
||||||
|
authors: pubkeys,
|
||||||
|
limit: EVENT_LIMIT
|
||||||
|
};
|
||||||
|
const subscriptionMessage = ['REQ', subscriptionId, filter];
|
||||||
|
const messageString = JSON.stringify(subscriptionMessage);
|
||||||
|
const messageSize = Buffer.byteLength(messageString, 'utf8');
|
||||||
|
console.log(`Sending request with ${pubkeys.length} pubkeys (${messageSize} bytes)`);
|
||||||
|
ws.send(messageString);
|
||||||
|
|
||||||
|
// Set timeout for EOSE response
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
if (!receivedEOSE) {
|
||||||
|
console.log('Timeout waiting for EOSE');
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}, 10000); // 10 second timeout
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('message', (data) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(data.toString());
|
||||||
|
if (message[0] === 'EVENT' && message[1] === subscriptionId) {
|
||||||
|
// Skip event content, just log that we received an event
|
||||||
|
console.log(`Received EVENT for subscription ${subscriptionId}`);
|
||||||
|
} else if (message[0] === 'EOSE' && message[1] === subscriptionId) {
|
||||||
|
console.log(`Received EOSE: ${JSON.stringify(message)}`);
|
||||||
|
receivedEOSE = true;
|
||||||
|
cleanup();
|
||||||
|
resolve(true);
|
||||||
|
} else {
|
||||||
|
console.log(`Received other message: ${JSON.stringify(message)}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log(`Received non-JSON: ${data.toString()}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
console.log(`WebSocket error: ${error.message}`);
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overall connection timeout
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!receivedEOSE) {
|
||||||
|
cleanup();
|
||||||
|
resolve(false);
|
||||||
|
}
|
||||||
|
}, 15000); // 15 second total timeout
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the test
|
||||||
|
testRelayPubkeyLimits().catch(console.error);
|
||||||