Compare commits

..

1 Commits

Author SHA1 Message Date
Your Name
133bb2d002 v0.7.44 - Release v0.8.0 with NIP-59 timestamp randomization and status command fixes 2025-10-27 13:20:50 -04:00
41 changed files with 2245 additions and 3354 deletions

View File

@@ -10,18 +10,18 @@ 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
- [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
@@ -29,37 +29,483 @@ It implements the following NIPs.
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:
For example, you can see my implementation running here:
[Primal link](https://primal.net/p/nprofile1qqswn2jsmm8lq8evas0v9vhqkdpn9nuujt90mtz60nqgsxndy66es4qjjnhr7)
[https://primal.net/p/nprofile1qqswn2jsmm8lq8evas0v9vhqkdpn9nuujt90mtz60nqgsxndy66es4qjjnhr7](https://)
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)
What this means in practice is that when you start the program, 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.
Now the program and the administrator can have verifed communication between the two. 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 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
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/main.png)
Main page with real time updates.
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/config.png)
Set your configuration preferences.
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/subscriptions.png)
View current subscriptions
## Quick Start
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/white-blacklists.png)
Add npubs to white or black lists.
Get your C-Relay up and running in minutes with a static binary (no dependencies required):
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/sqlQuery.png)
Run sql queries on the database.
### 1. Download Static Binary
![](https://git.laantungir.net/laantungir/c-relay/raw/branch/master/screenshots/main-light.png)
Light mode.
Download the latest static release from the [releases page](https://git.laantungir.net/laantungir/c-relay/releases):
```bash
# Static binary - works on all Linux distributions (no dependencies)
wget https://git.laantungir.net/laantungir/c-relay/releases/download/v0.6.0/c-relay-v0.6.0-linux-x86_64-static
chmod +x c-relay-v0.6.0-linux-x86_64-static
mv c-relay-v0.6.0-linux-x86_64-static c-relay
```
### 2. Start the Relay
Simply run the binary - no configuration files needed:
```bash
./c-relay
```
On first startup, you'll see:
- **Admin Private Key**: Save this securely! You'll need it for administration
- **Relay Public Key**: Your relay's identity on the Nostr network
- **Port Information**: Default is 8888, or the next available port
### 3. Access the Web Interface
Open your browser and navigate to:
```
http://localhost:8888/api/
```
The web interface provides:
- Real-time configuration management
- Database statistics dashboard
- Auth rules management
- Secure admin authentication with your Nostr identity
### 4. Test Your Relay
Test basic connectivity:
```bash
# Test WebSocket connection
curl -H "Accept: application/nostr+json" http://localhost:8888
# Test with a Nostr client
# Add ws://localhost:8888 to your client's relay list
```
### 5. Configure Your Relay (Optional)
Use the web interface or send admin commands to customize:
- Relay name and description
- Authentication rules (whitelist/blacklist)
- Connection limits
- Proof-of-work requirements
**That's it!** Your relay is now running with zero configuration required. The event-based configuration system means you can adjust all settings through the web interface or admin API without editing config files.
## Web Admin Interface
C-Relay includes a **built-in web-based administration interface** accessible at `http://localhost:8888/api/`. The interface provides:
- **Real-time Configuration Management**: View and edit all relay settings through a web UI
- **Database Statistics Dashboard**: Monitor event counts, storage usage, and performance metrics
- **Auth Rules Management**: Configure whitelist/blacklist rules for pubkeys
- **NIP-42 Authentication**: Secure access using your Nostr identity
- **Event-Based Updates**: All changes are applied as cryptographically signed Nostr events
The web interface serves embedded static files with no external dependencies and includes proper CORS headers for browser compatibility.
## Administrator API
C-Relay uses an innovative **event-based administration system** where all configuration and management commands are sent as signed Nostr events using the admin private key generated during first startup. All admin commands use **NIP-44 encrypted command arrays** for security and compatibility.
### Authentication
All admin commands require signing with the admin private key displayed during first-time startup. **Save this key securely** - it cannot be recovered and is needed for all administrative operations.
### Event Structure
All admin commands use the same unified event structure with NIP-44 encrypted content:
**Admin Command Event:**
```json
{
"id": "event_id",
"pubkey": "admin_public_key",
"created_at": 1234567890,
"kind": 23456,
"content": "AqHBUgcM7dXFYLQuDVzGwMST1G8jtWYyVvYxXhVGEu4nAb4LVw...",
"tags": [
["p", "relay_public_key"]
],
"sig": "event_signature"
}
```
The `content` field contains a NIP-44 encrypted JSON array representing the command.
**Admin Response Event:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "BpKCVhfN8eYtRmPqSvWxZnMkL2gHjUiOp3rTyEwQaS5dFg...",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
The `content` field contains a NIP-44 encrypted JSON response object.
### Admin Commands
All commands are sent as NIP-44 encrypted JSON arrays in the event content. The following table lists all available commands:
| Command Type | Command Format | Description |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| **Configuration Management** | | |
| `config_update` | `["config_update", [{"key": "auth_enabled", "value": "true", "data_type": "boolean", "category": "auth"}, {"key": "relay_description", "value": "My Relay", "data_type": "string", "category": "relay"}, ...]]` | Update relay configuration parameters (supports multiple updates) |
| `config_query` | `["config_query", "all"]` | Query all configuration parameters |
| **Auth Rules Management** | | |
| `auth_add_blacklist` | `["blacklist", "pubkey", "abc123..."]` | Add pubkey to blacklist |
| `auth_add_whitelist` | `["whitelist", "pubkey", "def456..."]` | Add pubkey to whitelist |
| `auth_delete_rule` | `["delete_auth_rule", "blacklist", "pubkey", "abc123..."]` | Delete specific auth rule |
| `auth_query_all` | `["auth_query", "all"]` | Query all auth rules |
| `auth_query_type` | `["auth_query", "whitelist"]` | Query specific rule type |
| `auth_query_pattern` | `["auth_query", "pattern", "abc123..."]` | Query specific pattern |
| **System Commands** | | |
| `system_clear_auth` | `["system_command", "clear_all_auth_rules"]` | Clear all auth rules |
| `system_status` | `["system_command", "system_status"]` | Get system status |
| `stats_query` | `["stats_query"]` | Get comprehensive database statistics |
| **Database Queries** | | |
| `sql_query` | `["sql_query", "SELECT * FROM events LIMIT 10"]` | Execute read-only SQL query against relay database |
### Available Configuration Keys
**Basic Relay Settings:**
- `relay_name`: Relay name (displayed in NIP-11)
- `relay_description`: Relay description text
- `relay_contact`: Contact information
- `relay_software`: Software URL
- `relay_version`: Software version
- `supported_nips`: Comma-separated list of supported NIP numbers (e.g., "1,2,4,9,11,12,13,15,16,20,22,33,40,42")
- `language_tags`: Comma-separated list of supported language tags (e.g., "en,es,fr" or "*" for all)
- `relay_countries`: Comma-separated list of supported country codes (e.g., "US,CA,MX" or "*" for all)
- `posting_policy`: Posting policy URL or text
- `payments_url`: Payment URL for premium features
- `max_connections`: Maximum concurrent connections
- `max_subscriptions_per_client`: Max subscriptions per client
- `max_event_tags`: Maximum tags per event
- `max_content_length`: Maximum event content length
**Authentication & Access Control:**
- `auth_enabled`: Enable whitelist/blacklist auth rules (`true`/`false`)
- `nip42_auth_required`: Enable NIP-42 cryptographic authentication (`true`/`false`)
- `nip42_auth_required_kinds`: Event kinds requiring NIP-42 auth (comma-separated)
- `nip42_challenge_timeout`: NIP-42 challenge expiration seconds
**Proof of Work & Validation:**
- `pow_min_difficulty`: Minimum proof-of-work difficulty
- `nip40_expiration_enabled`: Enable event expiration (`true`/`false`)
**Monitoring Settings:**
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
### Dynamic Configuration Updates
C-Relay supports **dynamic configuration updates** without requiring a restart for most settings. Configuration parameters are categorized as either **dynamic** (can be updated immediately) or **restart-required** (require relay restart to take effect).
**Dynamic Configuration Parameters (No Restart Required):**
- All relay information (NIP-11) settings: `relay_name`, `relay_description`, `relay_contact`, `relay_software`, `relay_version`, `supported_nips`, `language_tags`, `relay_countries`, `posting_policy`, `payments_url`
- Authentication settings: `auth_enabled`, `nip42_auth_required`, `nip42_auth_required_kinds`, `nip42_challenge_timeout`
- Subscription limits: `max_subscriptions_per_client`, `max_total_subscriptions`
- Event validation limits: `max_event_tags`, `max_content_length`, `max_message_length`
- Proof of Work settings: `pow_min_difficulty`, `pow_mode`
- Event expiration settings: `nip40_expiration_enabled`, `nip40_expiration_strict`, `nip40_expiration_filter`, `nip40_expiration_grace_period`
**Restart-Required Configuration Parameters:**
- Connection settings: `max_connections`, `relay_port`
- Database and core system settings
When updating configuration, the admin API response will indicate whether a restart is required for each parameter. Dynamic updates take effect immediately and are reflected in NIP-11 relay information documents without restart.
### Response Format
All admin commands return **signed EVENT responses** via WebSocket following standard Nostr protocol. Responses use JSON content with structured data.
#### Response Examples
**Success Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"success\", \"message\": \"Operation completed successfully\", \"timestamp\": 1234567890}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**Error Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"invalid configuration value\", \"timestamp\": 1234567890}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**Auth Rules Query Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"auth_rules_all\", \"total_results\": 2, \"timestamp\": 1234567890, \"data\": [{\"rule_type\": \"blacklist\", \"pattern_type\": \"pubkey\", \"pattern_value\": \"abc123...\", \"action\": \"allow\"}]}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**Configuration Query Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_all\", \"total_results\": 27, \"timestamp\": 1234567890, \"data\": [{\"key\": \"auth_enabled\", \"value\": \"false\", \"data_type\": \"boolean\", \"category\": \"auth\", \"description\": \"Enable NIP-42 authentication\"}, {\"key\": \"relay_description\", \"value\": \"My Relay\", \"data_type\": \"string\", \"category\": \"relay\", \"description\": \"Relay description text\"}]}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**Configuration Update Success Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"total_results\": 2, \"timestamp\": 1234567890, \"status\": \"success\", \"data\": [{\"key\": \"auth_enabled\", \"value\": \"true\", \"status\": \"updated\"}, {\"key\": \"relay_description\", \"value\": \"My Updated Relay\", \"status\": \"updated\"}]}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**Configuration Update Error Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"config_update\", \"status\": \"error\", \"error\": \"field validation failed: invalid port number '99999' (must be 1-65535)\", \"timestamp\": 1234567890}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**Database Statistics Query Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"stats_query\", \"timestamp\": 1234567890, \"database_size_bytes\": 1048576, \"total_events\": 15432, \"database_created_at\": 1234567800, \"latest_event_at\": 1234567890, \"event_kinds\": [{\"kind\": 1, \"count\": 12000, \"percentage\": 77.8}, {\"kind\": 0, \"count\": 2500, \"percentage\": 16.2}], \"time_stats\": {\"total\": 15432, \"last_24h\": 234, \"last_7d\": 1456, \"last_30d\": 5432}, \"top_pubkeys\": [{\"pubkey\": \"abc123...\", \"event_count\": 1234, \"percentage\": 8.0}, {\"pubkey\": \"def456...\", \"event_count\": 987, \"percentage\": 6.4}]}",
"tags": [
["p", "admin_public_key"]
],
"sig": "response_event_signature"
}]
```
**SQL Query Response:**
```json
["EVENT", "temp_sub_id", {
"id": "response_event_id",
"pubkey": "relay_public_key",
"created_at": 1234567890,
"kind": 23457,
"content": "nip44 encrypted:{\"query_type\": \"sql_query\", \"request_id\": \"request_event_id\", \"timestamp\": 1234567890, \"query\": \"SELECT * FROM events LIMIT 10\", \"execution_time_ms\": 45, \"row_count\": 10, \"columns\": [\"id\", \"pubkey\", \"created_at\", \"kind\", \"content\"], \"rows\": [[\"abc123...\", \"def456...\", 1234567890, 1, \"Hello world\"], ...]}",
"tags": [
["p", "admin_public_key"],
["e", "request_event_id"]
],
"sig": "response_event_signature"
}]
```
### SQL Query Command
The `sql_query` command allows administrators to execute read-only SQL queries against the relay database. This provides powerful analytics and debugging capabilities through the admin API.
**Request/Response Correlation:**
- Each response includes the request event ID in both the `tags` array (`["e", "request_event_id"]`) and the decrypted content (`"request_id": "request_event_id"`)
- This allows proper correlation when multiple queries are submitted concurrently
- Frontend can track pending queries and match responses to requests
**Security Features:**
- Only SELECT statements allowed (INSERT, UPDATE, DELETE, DROP, etc. are blocked)
- Query timeout: 5 seconds (configurable)
- Result row limit: 1000 rows (configurable)
- All queries logged with execution time
**Available Tables and Views:**
- `events` - All Nostr events
- `config` - Configuration parameters
- `auth_rules` - Authentication rules
- `subscription_events` - Subscription lifecycle log
- `event_broadcasts` - Event broadcast log
- `recent_events` - Last 1000 events (view)
- `event_stats` - Event statistics by type (view)
- `subscription_analytics` - Subscription metrics (view)
- `active_subscriptions_log` - Currently active subscriptions (view)
- `event_kinds_view` - Event distribution by kind (view)
- `top_pubkeys_view` - Top 10 pubkeys by event count (view)
- `time_stats_view` - Time-based statistics (view)
**Example Queries:**
```sql
-- Recent events
SELECT id, pubkey, created_at, kind FROM events ORDER BY created_at DESC LIMIT 20
-- Event distribution by kind
SELECT * FROM event_kinds_view ORDER BY count DESC
-- Active subscriptions
SELECT * FROM active_subscriptions_log ORDER BY created_at DESC
-- Database statistics
SELECT
(SELECT COUNT(*) FROM events) as total_events,
(SELECT COUNT(*) FROM subscription_events) as total_subscriptions
```
## Real-time Monitoring System
C-Relay includes a subscription-based monitoring system that broadcasts real-time relay statistics using ephemeral events (kind 24567).
### Activation
The monitoring system activates automatically when clients subscribe to kind 24567 events:
```json
["REQ", "monitoring-sub", {"kinds": [24567]}]
```
For specific monitoring types, use d-tag filters:
```json
["REQ", "event-kinds-sub", {"kinds": [24567], "#d": ["event_kinds"]}]
["REQ", "time-stats-sub", {"kinds": [24567], "#d": ["time_stats"]}]
["REQ", "top-pubkeys-sub", {"kinds": [24567], "#d": ["top_pubkeys"]}]
```
When no subscriptions exist, monitoring is dormant to conserve resources.
### Monitoring Event Types
| Type | d Tag | Description |
| ---------------------- | ------------------------ | ------------------------------------------- |
| Event Distribution | `event_kinds` | Event count by kind with percentages |
| Time Statistics | `time_stats` | Events in last 24h, 7d, 30d |
| Top Publishers | `top_pubkeys` | Top 10 pubkeys by event count |
| Active Subscriptions | `active_subscriptions` | Current subscription details (admin only) |
| Subscription Details | `subscription_details` | Detailed subscription info (admin only) |
| CPU Metrics | `cpu_metrics` | Process CPU and memory usage |
### Event Structure
```json
{
"kind": 24567,
"pubkey": "<relay_pubkey>",
"created_at": <timestamp>,
"content": "{\"data_type\":\"event_kinds\",\"timestamp\":1234567890,...}",
"tags": [
["d", "event_kinds"]
]
}
```
### Configuration
- `kind_24567_reporting_throttle_sec`: Minimum seconds between monitoring events (default: 5)
### Web Dashboard Integration
The built-in web dashboard (`/api/`) automatically subscribes to monitoring events and displays real-time statistics.
### Performance Considerations
- Monitoring events are ephemeral (not stored in database)
- Throttling prevents excessive event generation
- Automatic activation/deactivation based on subscriptions
- Minimal overhead when no clients are monitoring
## Direct Messaging Admin System
In addition to the above admin API, c-relay allows the administrator to direct message the relay to get information or control some settings. As long as the administrator is signed in with any nostr client that allows sending nip-17 direct messages (DMs), they can control the relay.
The is possible because the relay is a full nostr citizen with it's own private and public key, and it knows the administrator's public key.
**Available DM commands**
The intent is not to be strict in the formatting of the DM. So for example if the relay receives any DM from the administrator with the words "stats" or "statistics" in it, it will respond to the administrator with a reply DM with the current relay statistics.
- `stats`|`statistics`: Relay statistics
- `config`|`configuration`: Relay configuration

View File

@@ -1,612 +0,0 @@
# 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!

View File

@@ -1261,50 +1261,3 @@ body.dark-mode .sql-results-table tbody tr:nth-child(even) {
.header-title.clickable:hover {
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;
}

View File

@@ -212,9 +212,18 @@
<div class="input-group">
<div class="config-table-container">
<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">
<tr>
<td colspan="4" style="text-align: center; font-style: italic;">No subscriptions active</td>
<td colspan="5" style="text-align: center; font-style: italic;">No subscriptions active</td>
</tr>
</tbody>
</table>

View File

@@ -93,11 +93,10 @@ function log(message, type = 'INFO') {
// NIP-59 helper: randomize created_at to thwart time-analysis (past 2 days)
// TEMPORARILY DISABLED: Using current timestamp for debugging
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);
return now; // Math.round(now - Math.random() * TWO_DAYS);
return Math.round(now - Math.random() * TWO_DAYS);
}
// Safe JSON parse with error handling
@@ -1251,7 +1250,7 @@ async function processAdminResponse(event) {
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
let responseData;
@@ -4034,9 +4033,9 @@ function updateStatsFromTimeMonitoringEvent(monitoringData) {
// Extract values from periods array
monitoringData.periods.forEach(period => {
if (period.period === 'last_24h') timeStats.last_24h = period.count;
else if (period.period === 'last_7d') timeStats.last_7d = period.count;
else if (period.period === 'last_30d') timeStats.last_30d = period.count;
if (period.period === '24h') timeStats.last_24h = period.count;
else if (period.period === '7d') timeStats.last_7d = period.count;
else if (period.period === '30d') timeStats.last_30d = period.count;
});
populateStatsTime({ time_stats: timeStats });
@@ -4070,7 +4069,7 @@ function updateStatsFromTopPubkeysMonitoringEvent(monitoringData) {
function updateStatsFromSubscriptionDetailsMonitoringEvent(monitoringData) {
try {
// 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);
if (monitoringData.data_type !== 'subscription_details') {
@@ -4279,187 +4278,103 @@ function populateSubscriptionDetailsTable(subscriptionsData) {
const tableBody = document.getElementById('subscription-details-table-body');
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 = '';
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>';
row.innerHTML = '<td colspan="5" 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);
});
subscriptionsData.forEach((subscription, index) => {
const row = document.createElement('tr');
// 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;
// Calculate duration
const now = Math.floor(Date.now() / 1000);
const oldestDuration = Math.max(...subscriptions.map(s => now - s.created_at));
const oldestDurationStr = formatDuration(oldestDuration);
const duration = now - subscription.created_at;
const durationStr = formatDuration(duration);
// 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');
// Format client IP (show full IP for admin view)
const clientIP = subscription.client_ip || 'unknown';
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>
`;
// Format wsi_pointer (show full pointer)
const wsiPointer = subscription.wsi_pointer || 'N/A';
// Add click handler to toggle expansion
headerRow.addEventListener('click', () => toggleSubscriptionGroup(wsiPointer));
// 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 = [];
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)';
// 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);
});
}
// 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
function formatDuration(seconds) {
if (seconds < 60) return `${seconds}s`;
@@ -4864,7 +4779,7 @@ const SQL_QUERY_TEMPLATES = {
subscriptions: "SELECT * FROM active_subscriptions_log ORDER BY created_at DESC",
top_pubkeys: "SELECT * FROM top_pubkeys_view",
event_kinds: "SELECT * FROM event_kinds_view ORDER BY count DESC",
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)"
time_stats: "SELECT * FROM time_stats_view"
};
// Query history management (localStorage)

72
debug.log Normal file
View File

@@ -0,0 +1,72 @@
=== 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, ""]
=== NOSTR WebSocket Debug Log Started ===
[07:46:36.863] SEND localhost:8888: ["EVENT", {
"pubkey": "99e37bc774d260b464e936ad8945deec62e8f5f8af53e9db662038a717d39bd5",
"created_at": 1761562419,
"kind": 1059,
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
"content": "AjYCV8Esqa1L9LQE2G8cDVn+hSXjAJlFVp5nAaC6nuag/1AphKpsAJFGrWZvJ+rte5+4dbmdk+osvlxxfRQHtaZqjaTbVDKJA2b2KTYLgICe7O9rqTqR8oC4sYOVQViEjo30ox3IfDgdR5ONlaprvQ8r71E+oplWOjahUNvf5Yb9OOHbFphOqqWtbYRYqAqvO0bj3rB+tmyUJ8v3mU8NsKJtOTOIvN+jTIwU8cbN6AM6/A87WSi7J7X9/wLpFigBNxrx82MJ025ryApNWyt5PuBia3krPDa51F/A+jFVp1QicVwSt6tP01ktYJn3uyR3qizIiZiXzmxV9+TXopq+mOTlAiwcZBm1ZkS/PgfoUMDUOOVcCAW6ppZlg3oK5jScuDl1d4cZgTmmAPneaHhgB9A/DbWWr03W1vJAXCmDQRUoACfwsvLQf5esXkPcJV+ANgLl8sKd4EPmDAzr946uDcs2BUDftr4++jbdTkg9yIHb4SHnI10osbsqP7BqTrF1TbZHnxev4l5XyaIqhGm6WdQ90uGn1VDSUXXuou3IbqwhZReifYQAL9/5PAeSH6RrrL4neEzBjosSNkcMmtAxqBfd8dOHfqT6r15osKXc1eSmO9qDjZxUHUb5zIJjrkDW0jY8vAfiMqZhKd5Vl7stYf2iJJxdm04r4zpxGBjlYQva2LyrPclBIGxax6sTQTxoRyUBwyis7OnxUC0HO7bqr464RzCX1/OHMOYhFQu3BY8Rvytl9E1hS+rJpWgNkHsbjr5zs1l/B+qwt+zqnYlAtjZZ2T56Pjd+jZcRTt/NKDtyQnLPxreBWgNy8IykNI0q2fgFiWJwh7GnlbYrx5zco04Ory/P+nW2/Xosp0232I48e2KhxtH6L1e6dOWqbZXFQXBqzKNlVTRkPyS9ykSSs7NAVknRz/vF86+jJVJa2z32Y4oQtJna8vK5J5HA3rRSlSEINwmcSiFUmuxeFAcFjYjyVGlBhmH3B/98CtT2+JHgUYpMiG51+HR+OI9qBGgsF5SI9JKai7CFC3OqfaW1rZHN96uta4VVGQ1mJetz/xB3W+QThsZ0IJ6/wBnbUpPBoab4rfnYeeVwOMxiK5B2UIZ1+ihRrSMsjMC8DAEbUAn9XNABJHhDo0KJcYqtpHBIkQgbqfuSKTLmc4mZNJCp8wmry9Tc9ZQo2jT0dZa/NZO+qtqWXWqZWbMngXFer4AtR+Vethhg6BdhYOYI/j8gOW1m8qodBlj9BHiKEU3Ig9z6WawsOD95VosxhqrQDuyO07igXNWMNK5exRfvp2QiHgILuC9diZZGBXPRLIDlKERTotPc5IdutkTG6qVh6+r6wbwtVhiWJVmfVy/D0hvDvlaqzVk3FRVuRuMZI+LmF3OdNGIf0+lfMUeMAABhDNTWyyS8gG21JJZQOBxGc12x49xWvMLbXaPCKBKrqw4FLF4PTCc=",
"id": "9899324517c0e1796ea513cfc9fa0a2592cf5532774abc7e2a1bac7bb16c4fbb",
"sig": "0d73ac599d0d6d99dd9afa0c92d741e459bc53102557acba5d868089776bb36a521ae800303ce5ceceabc8d643116a74560744243b3a1c7749d6a52117343637"
}]
[07:46:36.876] RECV localhost:8888: ["OK", "9899324517c0e1796ea513cfc9fa0a2592cf5532774abc7e2a1bac7bb16c4fbb", true, ""]
=== NOSTR WebSocket Debug Log Started ===
[07:46:57.426] SEND localhost:8888: ["EVENT", {
"pubkey": "a1efe929139f3f195159389a6eb7199c127c88e32a0264cd826e95806a7c7db3",
"created_at": 1761562440,
"kind": 1059,
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
"content": "ArP2HEobkU/QYXy2R94zSkKM1OfQT5SabPeebj9dQVGbUKaKDwuN2RUYTRJ5rD8euyiXat8YzYO7PJ0CHzxclXxO8AWpdN4P76srm3zJ5z6kpQpcCFgInV7k4v6LZmmCrrtdTWqLjuLTPJJd6W9J7HqbTA3Dt4200BSpA4Por1TQAncplwH7O4vBfbPYtdv9w1RL1uSInMWcGwxttTXlyTtAJ0G0hQNofowFCMWuQCKjV5LxPfoXdOCrsp/We6x+hYKDxphsDjQ1tbdtYFmj/YRy6MJm1id4mr+i8fEnyimshE/fAhavOXUg6239MmYj8nR2RT9LMuhVckX+V5MZnVyC3mZlfzkPJiTHxiDkEREjljNOX9I+9yChg6MvyU41s7GjBlPyWiyeXedPcU2Q8ypGsFLhBl5i+IGSn6wCcBH8+h3euG8jtBgKxIP1qBYsXPTlYpSXQcisIZlW2Rubcawf/RF7HYbIRu884mpdVnURcHqfN9yquoVyfvIgQR5Fs6IbKatJ64LfMLkLNs4UIlumtQRdW3NFjglgb/rF9btKYVFHRG9dWwOpZBd0zvXtWKbts2AKQFU30/WegaPh2LT5rN9HfMsA1tI9YwZm//T2NiLaPwJCuOFWBOUiB7jIObQKtOHrICI/jXIGOAfgox5+fcAE6CaysHHzluVcwiw7GioShidaIDsZ10rWJOv1HeRpuiAJJTWk2FOBJzpOxli6s6jGj2S481Xa99I13TihgL0wAPhjsnQhz0kh40g89mipzVO5hbki101zIJCEBrDeT4Ptabc9GminXedq9k1G98usM0JSHsgtdZdztme/UyvYyAKMdez1yNgOp7YgOU15Rpz/KGL6W5Wk3MbUwpuVRzUWEMoBcyMzssn5Sa3mkh1RQqpTcoQaktTNwkhR1R5bgedka61JmcK4Uq3Hi/HfKYHbeUeta6Olu+U19PEwZia1iq+y0ZQm5gMwCK8BsoV44OLsjeDKlyRGCtIjkTc/L2LyuAZFhw560vKflkigQVcajaQVtEDgaT5odgFwvYEMOjbBDloDs589hAn8ZLyRJo3tIXNwqhctKTSqbit5qs85pOHkXSC3gsRQvDfq4qVh8iWXFotmOHlBEh4OZk89xwAnP0wiv5kd8N2c2CTB84SB224GinMhs0gkaCIXPPYv8IfVcow9+3sjnNov4dRRIB80fRXP9X3IyR7tXYCuq1uQO2iWiWKhNaqJRoTM1BUhLv0ebKYjfPevSVHUuV51CcsoFakNT8S0UnW7QHfmsESvCJLLT8ttrJqpRX2tf6SpzofHmzQHVrHFn8C7WKMVelndptmaOt/9Lek2UrZiKmzRP0CtBL+HoPRmZHF9t7y0qEhoApkrB9FPukH/IGV6jx891rH4nC1fLKc6zgkdjnYB7HDB+lWp2JKpV8Z3CbZXtR28kwIvZZIABZ23/U5cFds=",
"id": "c8cdf8992fbc17a0ccb74f6dcb7b851f3fdd53317f5a5ea4e202a91b22e15ac6",
"sig": "b9efba3448d67de8855838044427396af1958269642a975129fe877e48e5c0e0818d638264f8aa80404886559a7d29464339f63704044dbf11ff09eb0bdeda2b"
}]
[07:46:57.439] RECV localhost:8888: ["OK", "c8cdf8992fbc17a0ccb74f6dcb7b851f3fdd53317f5a5ea4e202a91b22e15ac6", true, ""]
=== NOSTR WebSocket Debug Log Started ===
[07:48:51.631] SEND localhost:8888: ["EVENT", {
"pubkey": "52feea8d0da247ed1537c88e12b2f6bc88697b69abe33bf4f059f9f10c0f2b43",
"created_at": 1761562554,
"kind": 1059,
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
"content": "AoU024VIO6IgceC43yYPvKxOb5PuuZQRAUQLC6Crdn5dtIVuHE8M/UUmmNXmWq3jB6kFbFWxNgFWuxCEG9sQHDEngk+tDOGlt+r0vx3jUZG09lzNzcghl/4l/Do48rcy0cfSm+mSHrJsy7N+MSAXQ1heKahKF2fSyfYFM+6EOSEl0sJSq09iWGFft0lWfeZ3AFpji0gp0Z5QY1hQPH3Te/TRuDCoR8GXG3NguLD22Ed8byOQzf/b7nWr70z2Sqg15zhMwyqkl//dOp9iIXE69FONqDfvFF0xttQ1L9PbzQizMt65CqbMuxGMiMA1zZsgQ8iN+xbIN4xv4DrzCtBZnYt3aJSt7cv+8Co5OmGNXu1RNUvxpZZTO8Dq/m08Y5JDYiRCvdh8kTASMVt0MfGsvWgmcHiiCINQUe7n5ynieayFpbl9j1Vtml0lmrfIOYnDQYmuqDDyG7PxhRt3G/SpiabWBwsqmTqCvrclXfTm4t5YYSr/5lKbwHzPk4qtdEs+LqH2/zd4egZnT4Xt/vIP/c55NrEWPmr50G37DpsSVbDxMQXs4dpldDntjEDFuL8VTkAzqibmiZSQnb6l+DpKYNdQCyg1S1ttnzYp2cRTAPzAVbRMqk1R6jagWnZyOs/JIK4on51JHczaUTCMypDLJFoOaFPTAedHfR/Hn2Nm05W8oQ/m/RGmxLgok6WgH3KJ8wvN+8X+XYpTgyxej/hYPqJnq3uaNqMbcD5katWRBmZtyZa3Cn2nZDqmFFJSABWacXNCyHL41Z+MhyYalYzvUev1ozUgx2NEWwxnSTvMkpOfvSDrs5Ncosle0itL9j0QVBrKFjHgq2BJ4FApv/Iq0af+8JEqhVEsMNpGwRJst1kn7kO+Q7O68PQF6PPlNqh7DNea0Bz1sN1QDt2yZTApi2b3IJTsbekye/WsOB0J+bLvxNQ/UoULcyq3SLRSqQEQMLBPz7JrijMzBdPglWpeZ58UDrbmd1KhnHzx7o1NvHyjPRuKj4M094+2/mTEFGOOIF+Ogjqj+wCSDnT5C0d/l2llQkIXCKcLONWT4bKkmTOjvNs6lX+VpBynaegGjzGOvJw1beRZIkRegTpV4pnMZH9833s175rcMcDjPnfT9FD+pDv1DkmXfww1k6MgfHTbwjSgj0K9862xFNwL3mC2g8XFNlcflC0Rd8PzXRg7TBn7855r+urqujCUqZrzdUMHvp08rEEzSJKljk4XN4DqZeWn2evv7UCbYjq46sVf2lEHCvHdqKoPf5ENU72y5Fb9tQJAyUoBTdPdb2SZ2Y2jSF+6+H2wVXrOlm8EwBquaREl25fs7Yqwjru7qz1rO9EA1jlNybFvALHEFQzHEpi8JeNi5T/mI+VleoUDrk/og2mucQVFqAQzRjASsaeDq9fZNqhv3Q3DBIpftmI4g6ZXqJhPRK8wF7Ym3mmC7eFHwalUprA=",
"id": "9bc4b5ad293085272bf52ff17abb585f7e63bc155a5a39cfe1a5c046f141e571",
"sig": "ee6b917761031a06bc50da0173aef881a61213473d4f533a8a4a96247edcdbd17dbf87919c4d92f8ea8719d5311d51a8028fbf62e3f40f9b8004ccbe9f3adabd"
}]
[07:49:01.659] RECV localhost:8888: ["OK", "9bc4b5ad293085272bf52ff17abb585f7e63bc155a5a39cfe1a5c046f141e571", false, "error: failed to store gift wrap event"]
=== NOSTR WebSocket Debug Log Started ===
[07:50:47.319] SEND localhost:8888: ["EVENT", {
"pubkey": "f206ef335cc3b360cf739680cd4540b852fb9d75aac552b58014a41cfc4c6c65",
"created_at": 1761562670,
"kind": 1059,
"tags": [["p", "4f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"]],
"content": "AuzXkvYpH0IX/T0BKtOAjO5QtglT3nXGXF20awgDX6T1qoWV8qykYY01qPlGLSDkOuOvhG5NZuFPs/hPMnctmnskvHTHqKJdUeT10Qe0JmZiP5y6fZSlrtLMKfyoLpNYFDXOfwooSD2Q0UN8ePfTkkB61ri7avsq8w1WjoVTUSG5kouJfQAgvh75uXNkcvTNWSX5gbCXxoL1D7twPSUXvuZBJTN0iVdh3jg8X1uZWhvpZJIIXceZIUdxaIp2EmrYVW0VEZZSbAFGAldtKasHrP3cJKwTk1IenMFXaPkJnsbvyzUZWKBwTCeBLBhMNzbBWOp5A7SgFW1vf9gm00MSQE/JjwOzDZIaIQ3vRCMbO29XLlcOCevs8FwusZ86LuXZ8EQac9vnc/7gEul/SkOQaSq7v4oGzwfY4iQ8c8VdX/+syE+ClUEjiKdN3/lcRGWhdMGYgK/uajLd9jpfY8CBzP0BZM0Oq6ZgSJt4ydntPfiUq4PtDww+56+bUUQb5V3eZ9SUnQgebO1doRgtvb6LiLGN3D9XolEiHBE3KDt/InfWPUeuf1HEf5IbDc2w7zhgzWbvXK9G4NnsAmOHQNIGbPHXLWEOOhRtcEnrHILKYgs4wfuvSnfSUzfWxHVlhXkuXj4pq/EmJKmQg3zB1C2QzKMx7O/oHplnQFGUfAMDQY9GpBWLCOhQH3ZiHWQ02AjXSze7PGB9ac7KoPmyKafUAgXbASp/G9t7n3pxarzwBNm4zQ6wPRpR5OF+mFYQ9ClJ+3MlUDNq1T8fGuJVIduxFgyWMAgKoJBQe+xP5qHEuCxhG1B2KtoKHbzAtXpV+sduXQOlm0Jq7req59VgtgrIVVoLNyn7ulUFmJvWbaPWyuMrC1z+MdjNw/oJ1mE4+zYSB4Yho7DCwVdIxWrkFSx1yH9s1WPPyERCy4/UQfHpPmAD/JyAnnkKsc7v3MpAWAYCsiil1/PgFFPRRrO7jS+Ez+veQl5tx376ac9MwaN+ZbFADqYaf8CCcWXhMlAYl/zcMWLXKqL/wKb6orpTTHiWU/iJIvbuT0MIN68LIX+G/S5QCIcAQez+G35n5pDUkKikVQguKcJG51iDZqRAc+fnjSa7ifu8HBJ8HIKZjHEEyp6oGU0LCEWH60iIa7toKAwRx8rLPP2tWo+5u41nUrhpUXhUquQu8Dr+LrNdB30qYlH123R0NBBtXG7ngW8WDv2GQcul33ftiI/14QofOthA8SiExW5B7OsWJQON8sS1ZTc5l/M6f5B17CwqmAGd4NdKPQy1SZWGD61jkefwzKW/w4fZFXfploGwuYvFI/G8/YnaJ60p/k+2Aftcst9ikAHZF4xBtuJr4IrT6/f+snv12G4EdowmaSMjXRZv30d4yKwFmwiuoDHWLyYVwBkO+UO3r0WEe1DId0Z1FZnXfgdnM+zAZwITtCVQjZMcsOSNskKd1eE=",
"id": "ce28dc9c653a4f5451266bc215942be9a54e4777a27862fddce351a59cc2dbf3",
"sig": "539f314c0f0fd685647da358c4153272baf671f1a1bc42b8ff61231c4b5f1f03cb8d15a36fb78437dbf094c546e9ffe8e03de7ddb3b62a981c135a714ec57f93"
}]
[07:50:47.325] RECV localhost:8888: ["OK", "ce28dc9c653a4f5451266bc215942be9a54e4777a27862fddce351a59cc2dbf3", true, ""]

1445
debug.txt Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@
# Copy the binary to the deployment location
cp build/c_relay_x86 ~/Storage/c_relay/crelay
# Copy the service file to systemd (use the main service file)
sudo cp systemd/c-relay.service /etc/systemd/system/c-relay-local.service
# Copy the local service file to systemd
sudo cp systemd/c-relay-local.service /etc/systemd/system/
# Reload systemd daemon to pick up the new service
sudo systemctl daemon-reload

View File

@@ -1,358 +0,0 @@
# 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

View File

@@ -1,155 +0,0 @@
# 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

View File

@@ -8,59 +8,36 @@ YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
print_status() { echo -e "${BLUE}[INFO]${NC} $1" >&2; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" >&2; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" >&2; }
print_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; }
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Global variables
COMMIT_MESSAGE=""
RELEASE_MODE=false
VERSION_INCREMENT_TYPE="patch" # "patch", "minor", or "major"
show_usage() {
echo "C-Relay Increment and Push Script"
echo ""
echo "USAGE:"
echo " $0 [OPTIONS] \"commit message\""
echo "Usage:"
echo " $0 \"commit message\" - Default: increment patch, commit & push"
echo " $0 -r \"commit message\" - Release: increment minor, create release"
echo ""
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 "Examples:"
echo " $0 \"Fixed event validation bug\""
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 " $0 --release \"Major release with new features\""
echo ""
echo "VERSION INCREMENT MODES:"
echo " -p, --patch (default): Increment patch version (v1.2.3 → v1.2.4)"
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 "Default Mode (patch increment):"
echo " - Increment patch version (v1.2.3 → v1.2.4)"
echo " - Git add, commit with message, and push"
echo ""
echo "RELEASE MODE (-r flag):"
echo " - Build static binary using build_static.sh"
echo " - Create source tarball"
echo " - Git add, commit, push, and create Gitea release with assets"
echo " - Can be combined with version increment flags"
echo "Release Mode (-r flag):"
echo " - Increment minor version, zero patch (v1.2.3 → v1.3.0)"
echo " - Git add, commit, push, and create Gitea release"
echo ""
echo "REQUIREMENTS FOR RELEASE MODE:"
echo "Requirements for Release Mode:"
echo " - Gitea token in ~/.gitea_token for release uploads"
echo " - Docker installed for static binary builds"
}
# Parse command line arguments
@@ -70,18 +47,6 @@ while [[ $# -gt 0 ]]; do
RELEASE_MODE=true
shift
;;
-p|--patch)
VERSION_INCREMENT_TYPE="patch"
shift
;;
-m|--minor)
VERSION_INCREMENT_TYPE="minor"
shift
;;
-M|--major)
VERSION_INCREMENT_TYPE="major"
shift
;;
-h|--help)
show_usage
exit 0
@@ -114,7 +79,7 @@ check_git_repo() {
# Function to get current version and increment appropriately
increment_version() {
local increment_type="$1" # "patch", "minor", or "major"
local increment_type="$1" # "patch" or "minor"
print_status "Getting current version..."
@@ -140,34 +105,24 @@ increment_version() {
fi
# Increment version based on type
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
if [[ "$increment_type" == "minor" ]]; then
# Minor release: increment minor, zero patch
NEW_MAJOR=$MAJOR
NEW_MINOR=$((MINOR + 1))
NEW_PATCH=0
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Minor version increment: incrementing minor version"
NEW_VERSION="v${MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Release mode: incrementing minor version"
else
# Default: increment patch
NEW_MAJOR=$MAJOR
NEW_MINOR=$MINOR
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="v${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}"
print_status "Patch version increment: incrementing patch version"
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
print_status "Default mode: incrementing patch version"
fi
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
# Update version in src/main.h
update_version_in_header "$NEW_VERSION" "$NEW_MAJOR" "$NEW_MINOR" "$NEW_PATCH"
update_version_in_header "$NEW_VERSION" "$MAJOR" "${NEW_MINOR:-$MINOR}" "${NEW_PATCH:-$PATCH}"
# Export for use in other functions
export NEW_VERSION
@@ -306,115 +261,6 @@ git_commit_and_push_no_tag() {
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
create_gitea_release() {
print_status "Creating Gitea release..."
@@ -432,27 +278,16 @@ create_gitea_release() {
# Create release
print_status "Creating release $NEW_VERSION..."
local response=$(curl -s -X POST "$api_url/releases" \
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
-H "Authorization: token $token" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"$NEW_VERSION\", \"name\": \"$NEW_VERSION\", \"body\": \"$COMMIT_MESSAGE\"}")
if echo "$response" | grep -q '"id"'; then
print_success "Created release $NEW_VERSION"
# Extract release ID for asset uploads
local release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
return 0
elif echo "$response" | grep -q "already exists"; then
print_warning "Release $NEW_VERSION already exists"
# 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
return 0
else
print_error "Failed to create release $NEW_VERSION"
print_error "Response: $response"
@@ -462,8 +297,7 @@ create_gitea_release() {
local check_response=$(curl -s -H "Authorization: token $token" "$api_url/releases/tags/$NEW_VERSION")
if echo "$check_response" | grep -q '"id"'; then
print_warning "Release exists but creation response was unexpected"
local release_id=$(echo "$check_response" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
echo $release_id
return 0
else
print_error "Release does not exist and creation failed"
return 1
@@ -481,15 +315,8 @@ main() {
if [[ "$RELEASE_MODE" == true ]]; then
print_status "=== RELEASE MODE ==="
# Only increment version if explicitly requested (not just because of -r flag)
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
# Increment minor version for releases
increment_version "minor"
# Create new git tag BEFORE compilation so version.h picks it up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then
@@ -503,42 +330,9 @@ main() {
# Commit and push (but skip tag creation since we already did it)
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
local release_id=""
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
if create_gitea_release; then
print_success "Release $NEW_VERSION completed successfully!"
else
print_error "Release creation failed"
fi
@@ -546,8 +340,8 @@ main() {
else
print_status "=== DEFAULT MODE ==="
# Increment version based on type (default to patch)
increment_version "$VERSION_INCREMENT_TYPE"
# Increment patch version for regular commits
increment_version "patch"
# Create new git tag BEFORE compilation so version.h picks it up
if git tag "$NEW_VERSION" > /dev/null 2>&1; then

1
nips Submodule

Submodule nips added at 8c45ff5d96

View File

@@ -1 +1 @@
198023
2595240

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

237
src/api.c
View File

@@ -271,7 +271,6 @@ cJSON* query_subscription_details(void) {
long long events_sent = sqlite3_column_int64(stmt, 3);
long long created_at = sqlite3_column_int64(stmt, 4);
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("Row %d: sub_id=%s, client_ip=%s, events_sent=%lld, created_at=%lld",
@@ -285,7 +284,6 @@ cJSON* query_subscription_details(void) {
cJSON_AddNumberToObject(sub_obj, "duration_seconds", (double)duration_seconds);
cJSON_AddNumberToObject(sub_obj, "events_sent", events_sent);
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
if (filter_json) {
@@ -501,76 +499,6 @@ void monitoring_on_subscription_change(void) {
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)
typedef struct {
const char* key;
@@ -637,9 +565,6 @@ static const config_definition_t known_configs[] = {
{"relay_privkey", "string", 0, 65},
{"admin_pubkey", "string", 0, 65},
// Kind 1 Status Posts
{"kind_1_status_posts_hours", "int", 0, 8760},
// Sentinel
{NULL, NULL, 0, 0}
};
@@ -1498,37 +1423,12 @@ char* generate_config_text(void) {
}
// Footer
offset += snprintf(config_text + offset, 8192 - offset, "\n");
offset += snprintf(config_text + offset, 8192 - offset,
"\n✅ Configuration retrieved successfully");
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
char* generate_stats_text(void) {
char* stats_json = generate_stats_json();
@@ -1554,7 +1454,7 @@ char* generate_stats_text(void) {
cJSON* newest_event = cJSON_GetObjectItem(stats_obj, "latest_event_at");
cJSON* time_stats = cJSON_GetObjectItem(stats_obj, "time_stats");
cJSON* event_kinds = cJSON_GetObjectItem(stats_obj, "event_kinds");
// cJSON* top_pubkeys = cJSON_GetObjectItem(stats_obj, "top_pubkeys");
cJSON* top_pubkeys = cJSON_GetObjectItem(stats_obj, "top_pubkeys");
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;
@@ -1595,23 +1495,25 @@ char* generate_stats_text(void) {
// Header
offset += snprintf(stats_text + offset, 16384 - offset,
"\nRelay Statistics\n"
"📊 Relay Statistics\n"
"━━━━━━━━━━━━━━━━━━━━\n");
// Database Overview section
offset += snprintf(stats_text + offset, 16384 - offset,
"Database Size %.2f MB (%lld bytes)\n"
"Total Events %lld\n"
"Active Subscriptions %d\n"
"Oldest Event %s\n"
"Newest Event %s\n"
"\n",
db_mb, db_bytes, total, active_subs, oldest_str, newest_str);
"Database Overview:\n"
"Metric\tValue\tDescription\n"
"Database Size\t%.2f MB (%lld bytes)\tCurrent database file size\n"
"Total Events\t%lld\tTotal number of events stored\n"
"Active Subscriptions\t%d\tCurrent active WebSocket subscriptions\n"
"Oldest Event\t%s\tTimestamp of oldest event\n"
"Newest Event\t%s\tTimestamp of newest event\n"
"\n",
db_mb, db_bytes, total, active_subs, oldest_str, newest_str);
// Event Kind Distribution section
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)) {
cJSON* kind_item = NULL;
@@ -1621,45 +1523,11 @@ char* generate_stats_text(void) {
cJSON* percentage = cJSON_GetObjectItem(kind_item, "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,
"%s%s_%s\n", kind_str, count_formatted, pct_formatted);
"%lld\t%lld\t%.1f%%\n",
(long long)cJSON_GetNumberValue(kind),
(long long)cJSON_GetNumberValue(count),
cJSON_GetNumberValue(percentage));
}
}
} else {
@@ -1672,48 +1540,49 @@ char* generate_stats_text(void) {
// Time-based Statistics section
offset += snprintf(stats_text + offset, 16384 - offset,
"Time-based Statistics:\n"
"%lld\tEvents in the last day\n"
"%lld\tEvents in the last week\n"
"%lld\tEvents in the last month\n"
"Period\tEvents\tDescription\n"
"Last 24 Hours\t%lld\tEvents in the last day\n"
"Last 7 Days\t%lld\tEvents in the last week\n"
"Last 30 Days\t%lld\tEvents in the last month\n"
"\n",
last_24h, last_7d, last_30d);
// Top Pubkeys section
// offset += snprintf(stats_text + offset, 16384 - offset,
// "Top Pubkeys by Event Count:\n"
// "Rank\tPubkey\tEvent Count\tPercentage\n");
offset += snprintf(stats_text + offset, 16384 - offset,
"Top Pubkeys by Event Count:\n"
"Rank\tPubkey\tEvent Count\tPercentage\n");
// if (top_pubkeys && cJSON_IsArray(top_pubkeys)) {
// int rank = 1;
// cJSON* pubkey_item = NULL;
// cJSON_ArrayForEach(pubkey_item, top_pubkeys) {
// cJSON* pubkey = cJSON_GetObjectItem(pubkey_item, "pubkey");
// cJSON* event_count = cJSON_GetObjectItem(pubkey_item, "event_count");
// cJSON* percentage = cJSON_GetObjectItem(pubkey_item, "percentage");
if (top_pubkeys && cJSON_IsArray(top_pubkeys)) {
int rank = 1;
cJSON* pubkey_item = NULL;
cJSON_ArrayForEach(pubkey_item, top_pubkeys) {
cJSON* pubkey = cJSON_GetObjectItem(pubkey_item, "pubkey");
cJSON* event_count = cJSON_GetObjectItem(pubkey_item, "event_count");
cJSON* percentage = cJSON_GetObjectItem(pubkey_item, "percentage");
// if (pubkey && event_count && percentage) {
// const char* pubkey_str = cJSON_GetStringValue(pubkey);
// char short_pubkey[20] = "...";
// if (pubkey_str && strlen(pubkey_str) >= 16) {
// snprintf(short_pubkey, sizeof(short_pubkey), "%.16s...", pubkey_str);
// }
if (pubkey && event_count && percentage) {
const char* pubkey_str = cJSON_GetStringValue(pubkey);
char short_pubkey[20] = "...";
if (pubkey_str && strlen(pubkey_str) >= 16) {
snprintf(short_pubkey, sizeof(short_pubkey), "%.16s...", pubkey_str);
}
// offset += snprintf(stats_text + offset, 16384 - offset,
// "%d\t%s\t%lld\t%.1f%%\n",
// rank++,
// short_pubkey,
// (long long)cJSON_GetNumberValue(event_count),
// cJSON_GetNumberValue(percentage));
// }
// }
// } else {
// offset += snprintf(stats_text + offset, 16384 - offset,
// "No pubkey data available\n");
// }
offset += snprintf(stats_text + offset, 16384 - offset,
"%d\t%s\t%lld\t%.1f%%\n",
rank++,
short_pubkey,
(long long)cJSON_GetNumberValue(event_count),
cJSON_GetNumberValue(percentage));
}
}
} else {
offset += snprintf(stats_text + offset, 16384 - offset,
"No pubkey data available\n");
}
// Footer
offset += snprintf(stats_text + offset, 16384 - offset, "\n");
offset += snprintf(stats_text + offset, 16384 - offset,
"\n✅ Statistics retrieved successfully");
cJSON_Delete(stats_obj);
} else {

View File

@@ -64,7 +64,4 @@ void monitoring_on_event_stored(void);
void monitoring_on_subscription_change(void);
int get_monitoring_throttle_seconds(void);
// Kind 1 status posts
int generate_and_post_status_event(void);
#endif // API_H

View File

@@ -9,9 +9,6 @@
#ifdef VERSION
#undef VERSION
#endif
#ifdef VERSION_MAJOR
#undef VERSION_MAJOR
#endif
#ifdef VERSION_MINOR
#undef VERSION_MINOR
#endif

View File

@@ -81,10 +81,7 @@ static const struct {
{"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"}
{"nip59_timestamp_max_delay_sec", "0"}
};
// Number of default configuration values

File diff suppressed because one or more lines are too long

View File

@@ -10,14 +10,10 @@
#define MAIN_H
// Version information (auto-updated by build system)
#define VERSION_MAJOR 1
#define VERSION_MINOR 0
#define VERSION_PATCH 5
#define VERSION "v1.0.5"
// Avoid VERSION_MAJOR redefinition warning from nostr_core_lib
#undef VERSION_MAJOR
#define VERSION_MAJOR 1
#define VERSION "v0.8.0"
#define VERSION_MAJOR 0
#define VERSION_MINOR 7
#define VERSION_PATCH 44
// Relay metadata (authoritative source for NIP-11 information)
#define RELAY_NAME "C-Relay"

View File

@@ -1,4 +1,5 @@
/* Embedded SQL Schema for C Nostr Relay
* Generated from db/schema.sql - Do not edit manually
* Schema Version: 8
*/
#ifndef SQL_SCHEMA_H
@@ -241,8 +242,7 @@ SELECT\n\
s.filter_json,\n\
s.events_sent,\n\
s.created_at,\n\
(strftime('%s', 'now') - s.created_at) as duration_seconds,\n\
s.wsi_pointer\n\
(strftime('%s', 'now') - s.created_at) as duration_seconds\n\
FROM subscriptions s\n\
WHERE s.event_type = 'created'\n\
AND NOT EXISTS (\n\

View File

@@ -44,9 +44,6 @@ 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_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
int handle_nip11_http_request(struct lws* wsi, const char* accept_header);
@@ -285,19 +282,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// Check if this is a GET request to the root path
if (strcmp(requested_uri, "/") == 0) {
// 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
// Get Accept header
char accept_header[256] = {0};
int header_len = lws_hdr_copy(wsi, accept_header, sizeof(accept_header) - 1, WSI_TOKEN_HTTP_ACCEPT);
@@ -315,8 +300,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
}
// 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");
// Root path without NIP-11 Accept header - return 404
lws_return_http_status(wsi, HTTP_STATUS_NOT_FOUND, NULL);
return -1;
}
@@ -471,720 +455,18 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
case LWS_CALLBACK_RECEIVE:
if (len > 0) {
DEBUG_TRACE("LWS_CALLBACK_RECEIVE: received %zu bytes", len);
// Check if client is rate limited for malformed requests
if (is_client_rate_limited_for_malformed_requests(pss)) {
send_notice_message(wsi, "error: too many malformed requests - temporarily blocked");
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);
if (message) {
memcpy(message, in, len);
message[len] = '\0';
// Parse JSON message (this is the normal program flow)
DEBUG_TRACE("Parsing JSON message of length %zu", strlen(message));
cJSON* json = cJSON_Parse(message);
if (json && cJSON_IsArray(json)) {
// Get message type
@@ -1384,7 +666,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
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, strlen(message));
DEBUG_TRACE("Processing event kind %d", event_kind);
// Log reception of Kind 23456 events
if (event_kind == 23456) {
@@ -1587,13 +869,10 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
}
} else if (strcmp(msg_type, "REQ") == 0) {
DEBUG_TRACE("REQ message received, starting processing");
// Log the full REQ message for debugging
// 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 {
@@ -1605,8 +884,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
return 0;
}
DEBUG_TRACE("REQ message passed authentication check");
// Handle REQ message
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
@@ -1617,7 +894,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// 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);
@@ -1628,7 +904,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
// 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);
@@ -1636,12 +911,9 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
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);
@@ -1650,21 +922,16 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
}
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);
@@ -1674,11 +941,7 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
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);
@@ -1912,15 +1175,6 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
pss->message_queue_count = 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
pthread_mutex_lock(&pss->session_lock);
@@ -1990,7 +1244,7 @@ static struct lws_protocols protocols[] = {
"nostr-relay-protocol",
nostr_relay_callback,
sizeof(struct per_session_data),
65536, // rx buffer size
4096, // rx buffer size
0, NULL, 0
},
{ NULL, NULL, 0, 0, 0, NULL, 0 } // terminator
@@ -2150,9 +1404,6 @@ 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);
}
// 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
while (g_server_running && !g_shutdown_flag) {
int result = lws_service(ws_context, 1000);
@@ -2161,19 +1412,6 @@ int start_websocket_relay(int port_override, int strict_port) {
DEBUG_ERROR("libwebsockets service error");
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);

View File

@@ -21,10 +21,10 @@
// Filter validation constants
#define MAX_FILTERS_PER_REQUEST 10
#define MAX_AUTHORS_PER_FILTER 1000
#define MAX_IDS_PER_FILTER 1000
#define MAX_KINDS_PER_FILTER 500
#define MAX_TAG_VALUES_PER_FILTER 1000
#define MAX_AUTHORS_PER_FILTER 100
#define MAX_IDS_PER_FILTER 100
#define MAX_KINDS_PER_FILTER 50
#define MAX_TAG_VALUES_PER_FILTER 100
#define MAX_KIND_VALUE 65535
#define MAX_TIMESTAMP_VALUE 2147483647 // Max 32-bit signed int
#define MAX_LIMIT_VALUE 5000
@@ -73,12 +73,6 @@ struct per_session_data {
struct message_queue_node* message_queue_tail; // Tail of message queue
int message_queue_count; // Number of messages in queue
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

View File

@@ -0,0 +1,40 @@
[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

View File

@@ -1,747 +0,0 @@
/**
* 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);