v0.4.4 - Just waking up
This commit is contained in:
@@ -18,7 +18,7 @@ Do NOT modify the formatting, add emojis, or change the text. Keep the simple fo
|
|||||||
- [x] NIP-33: Parameterized Replaceable Events
|
- [x] NIP-33: Parameterized Replaceable Events
|
||||||
- [x] NIP-40: Expiration Timestamp
|
- [x] NIP-40: Expiration Timestamp
|
||||||
- [x] NIP-42: Authentication of clients to relays
|
- [x] NIP-42: Authentication of clients to relays
|
||||||
- [ ] NIP-45: Counting results
|
- [x] NIP-45: Counting results
|
||||||
- [ ] NIP-50: Keywords filter
|
- [ ] NIP-50: Keywords filter
|
||||||
- [ ] NIP-70: Protected Events
|
- [ ] NIP-70: Protected Events
|
||||||
|
|
||||||
|
|||||||
8
c-relay.code-workspace
Normal file
8
c-relay.code-workspace
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
42
src/main.c
42
src/main.c
@@ -120,6 +120,9 @@ int nostr_validate_unified_request(const char* json_string, size_t json_length);
|
|||||||
// Forward declaration for admin event processing (kind 23456)
|
// Forward declaration for admin event processing (kind 23456)
|
||||||
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
int process_admin_event_in_config(cJSON* event, char* error_message, size_t error_size, struct lws* wsi);
|
||||||
|
|
||||||
|
// Forward declaration for NIP-45 COUNT message handling
|
||||||
|
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
|
||||||
|
|
||||||
// Forward declaration for enhanced admin event authorization
|
// Forward declaration for enhanced admin event authorization
|
||||||
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
|
int is_authorized_admin_event(cJSON* event, char* error_message, size_t error_size);
|
||||||
|
|
||||||
@@ -881,7 +884,7 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
|
// Build SQL query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
|
||||||
char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND kind < 20000";
|
char sql[1024] = "SELECT id, pubkey, created_at, kind, content, sig, tags FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
|
||||||
char* sql_ptr = sql + strlen(sql);
|
char* sql_ptr = sql + strlen(sql);
|
||||||
int remaining = sizeof(sql) - strlen(sql);
|
int remaining = sizeof(sql) - strlen(sql);
|
||||||
|
|
||||||
@@ -972,6 +975,43 @@ int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, stru
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle tag filters (#e, #p, #t, etc.)
|
||||||
|
cJSON* filter_item = NULL;
|
||||||
|
cJSON_ArrayForEach(filter_item, filter) {
|
||||||
|
const char* filter_key = filter_item->string;
|
||||||
|
if (filter_key && filter_key[0] == '#' && strlen(filter_key) > 1) {
|
||||||
|
// This is a tag filter like "#e", "#p", etc.
|
||||||
|
const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.)
|
||||||
|
|
||||||
|
if (cJSON_IsArray(filter_item)) {
|
||||||
|
int tag_value_count = cJSON_GetArraySize(filter_item);
|
||||||
|
if (tag_value_count > 0) {
|
||||||
|
// Use EXISTS with LIKE to check for matching tags
|
||||||
|
snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name);
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
|
||||||
|
for (int i = 0; i < tag_value_count; i++) {
|
||||||
|
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||||
|
if (cJSON_IsString(tag_value)) {
|
||||||
|
if (i > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, ",");
|
||||||
|
sql_ptr++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value));
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, "))");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle since filter
|
// Handle since filter
|
||||||
cJSON* since = cJSON_GetObjectItem(filter, "since");
|
cJSON* since = cJSON_GetObjectItem(filter, "since");
|
||||||
if (since && cJSON_IsNumber(since)) {
|
if (since && cJSON_IsNumber(since)) {
|
||||||
|
|||||||
@@ -12,10 +12,10 @@
|
|||||||
#define MAIN_H
|
#define MAIN_H
|
||||||
|
|
||||||
// Version information (auto-updated by build_and_push.sh)
|
// Version information (auto-updated by build_and_push.sh)
|
||||||
#define VERSION "v0.4.3"
|
#define VERSION "v0.4.4"
|
||||||
#define VERSION_MAJOR 0
|
#define VERSION_MAJOR 0
|
||||||
#define VERSION_MINOR 4
|
#define VERSION_MINOR 4
|
||||||
#define VERSION_PATCH 3
|
#define VERSION_PATCH 4
|
||||||
|
|
||||||
// Relay metadata (authoritative source for NIP-11 information)
|
// Relay metadata (authoritative source for NIP-11 information)
|
||||||
#define RELAY_NAME "C-Relay"
|
#define RELAY_NAME "C-Relay"
|
||||||
|
|||||||
259
src/websockets.c
259
src/websockets.c
@@ -74,6 +74,7 @@ int is_event_expired(cJSON* event, time_t current_time);
|
|||||||
|
|
||||||
// Forward declarations for subscription handling
|
// Forward declarations for subscription handling
|
||||||
int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
|
int handle_req_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
|
||||||
|
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss);
|
||||||
|
|
||||||
// Forward declarations for NOTICE message support
|
// Forward declarations for NOTICE message support
|
||||||
void send_notice_message(struct lws* wsi, const char* message);
|
void send_notice_message(struct lws* wsi, const char* message);
|
||||||
@@ -619,6 +620,41 @@ static int nostr_relay_callback(struct lws *wsi, enum lws_callback_reasons reaso
|
|||||||
}
|
}
|
||||||
cJSON_Delete(eose_response);
|
cJSON_Delete(eose_response);
|
||||||
}
|
}
|
||||||
|
} 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");
|
||||||
|
log_warning("COUNT rejected: NIP-42 authentication required");
|
||||||
|
}
|
||||||
|
cJSON_Delete(json);
|
||||||
|
free(message);
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
} else if (strcmp(msg_type, "CLOSE") == 0) {
|
||||||
// Handle CLOSE message
|
// Handle CLOSE message
|
||||||
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
cJSON* sub_id = cJSON_GetArrayItem(json, 1);
|
||||||
@@ -899,3 +935,226 @@ int start_websocket_relay(int port_override, int strict_port) {
|
|||||||
log_success("WebSocket relay shut down cleanly");
|
log_success("WebSocket relay shut down cleanly");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle NIP-45 COUNT message
|
||||||
|
int handle_count_message(const char* sub_id, cJSON* filters, struct lws *wsi, struct per_session_data *pss) {
|
||||||
|
(void)pss; // Suppress unused parameter warning
|
||||||
|
log_info("Handling COUNT message for subscription");
|
||||||
|
|
||||||
|
if (!cJSON_IsArray(filters)) {
|
||||||
|
log_error("COUNT filters is not an array");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total_count = 0;
|
||||||
|
|
||||||
|
// Process each filter in the array
|
||||||
|
for (int i = 0; i < cJSON_GetArraySize(filters); i++) {
|
||||||
|
cJSON* filter = cJSON_GetArrayItem(filters, i);
|
||||||
|
if (!filter || !cJSON_IsObject(filter)) {
|
||||||
|
log_warning("Invalid filter object in COUNT");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build SQL COUNT query based on filter - exclude ephemeral events (kinds 20000-29999) from historical queries
|
||||||
|
char sql[1024] = "SELECT COUNT(*) FROM events WHERE 1=1 AND (kind < 20000 OR kind >= 30000)";
|
||||||
|
char* sql_ptr = sql + strlen(sql);
|
||||||
|
int remaining = sizeof(sql) - strlen(sql);
|
||||||
|
|
||||||
|
// Note: Expiration filtering will be done at application level
|
||||||
|
// after retrieving events to ensure compatibility with all SQLite versions
|
||||||
|
|
||||||
|
// Handle kinds filter
|
||||||
|
cJSON* kinds = cJSON_GetObjectItem(filter, "kinds");
|
||||||
|
if (kinds && cJSON_IsArray(kinds)) {
|
||||||
|
int kind_count = cJSON_GetArraySize(kinds);
|
||||||
|
if (kind_count > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, " AND kind IN (");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
|
||||||
|
for (int k = 0; k < kind_count; k++) {
|
||||||
|
cJSON* kind = cJSON_GetArrayItem(kinds, k);
|
||||||
|
if (cJSON_IsNumber(kind)) {
|
||||||
|
if (k > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, ",");
|
||||||
|
sql_ptr++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, "%d", (int)cJSON_GetNumberValue(kind));
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, ")");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle authors filter
|
||||||
|
cJSON* authors = cJSON_GetObjectItem(filter, "authors");
|
||||||
|
if (authors && cJSON_IsArray(authors)) {
|
||||||
|
int author_count = cJSON_GetArraySize(authors);
|
||||||
|
if (author_count > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, " AND pubkey IN (");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
|
||||||
|
for (int a = 0; a < author_count; a++) {
|
||||||
|
cJSON* author = cJSON_GetArrayItem(authors, a);
|
||||||
|
if (cJSON_IsString(author)) {
|
||||||
|
if (a > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, ",");
|
||||||
|
sql_ptr++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(author));
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, ")");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle ids filter
|
||||||
|
cJSON* ids = cJSON_GetObjectItem(filter, "ids");
|
||||||
|
if (ids && cJSON_IsArray(ids)) {
|
||||||
|
int id_count = cJSON_GetArraySize(ids);
|
||||||
|
if (id_count > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, " AND id IN (");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
|
||||||
|
for (int i = 0; i < id_count; i++) {
|
||||||
|
cJSON* id = cJSON_GetArrayItem(ids, i);
|
||||||
|
if (cJSON_IsString(id)) {
|
||||||
|
if (i > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, ",");
|
||||||
|
sql_ptr++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(id));
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, ")");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tag filters (#e, #p, #t, etc.)
|
||||||
|
cJSON* filter_item = NULL;
|
||||||
|
cJSON_ArrayForEach(filter_item, filter) {
|
||||||
|
const char* filter_key = filter_item->string;
|
||||||
|
if (filter_key && filter_key[0] == '#' && strlen(filter_key) > 1) {
|
||||||
|
// This is a tag filter like "#e", "#p", etc.
|
||||||
|
const char* tag_name = filter_key + 1; // Get the tag name (e, p, t, type, etc.)
|
||||||
|
|
||||||
|
if (cJSON_IsArray(filter_item)) {
|
||||||
|
int tag_value_count = cJSON_GetArraySize(filter_item);
|
||||||
|
if (tag_value_count > 0) {
|
||||||
|
// Use EXISTS with JSON extraction to check for matching tags
|
||||||
|
snprintf(sql_ptr, remaining, " AND EXISTS (SELECT 1 FROM json_each(json(tags)) WHERE json_extract(value, '$[0]') = '%s' AND json_extract(value, '$[1]') IN (", tag_name);
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
|
||||||
|
for (int i = 0; i < tag_value_count; i++) {
|
||||||
|
cJSON* tag_value = cJSON_GetArrayItem(filter_item, i);
|
||||||
|
if (cJSON_IsString(tag_value)) {
|
||||||
|
if (i > 0) {
|
||||||
|
snprintf(sql_ptr, remaining, ",");
|
||||||
|
sql_ptr++;
|
||||||
|
remaining--;
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, "'%s'", cJSON_GetStringValue(tag_value));
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snprintf(sql_ptr, remaining, "))");
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle since filter
|
||||||
|
cJSON* since = cJSON_GetObjectItem(filter, "since");
|
||||||
|
if (since && cJSON_IsNumber(since)) {
|
||||||
|
snprintf(sql_ptr, remaining, " AND created_at >= %ld", (long)cJSON_GetNumberValue(since));
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle until filter
|
||||||
|
cJSON* until = cJSON_GetObjectItem(filter, "until");
|
||||||
|
if (until && cJSON_IsNumber(until)) {
|
||||||
|
snprintf(sql_ptr, remaining, " AND created_at <= %ld", (long)cJSON_GetNumberValue(until));
|
||||||
|
sql_ptr += strlen(sql_ptr);
|
||||||
|
remaining = sizeof(sql) - strlen(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug: Log the SQL query being executed
|
||||||
|
char debug_msg[1280];
|
||||||
|
snprintf(debug_msg, sizeof(debug_msg), "Executing COUNT SQL: %s", sql);
|
||||||
|
log_info(debug_msg);
|
||||||
|
|
||||||
|
// Execute count query
|
||||||
|
sqlite3_stmt* stmt;
|
||||||
|
int rc = sqlite3_prepare_v2(g_db, sql, -1, &stmt, NULL);
|
||||||
|
if (rc != SQLITE_OK) {
|
||||||
|
char error_msg[256];
|
||||||
|
snprintf(error_msg, sizeof(error_msg), "Failed to prepare COUNT query: %s", sqlite3_errmsg(g_db));
|
||||||
|
log_error(error_msg);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int filter_count = 0;
|
||||||
|
if (sqlite3_step(stmt) == SQLITE_ROW) {
|
||||||
|
filter_count = sqlite3_column_int(stmt, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
char count_debug[128];
|
||||||
|
snprintf(count_debug, sizeof(count_debug), "Filter %d returned count: %d", i + 1, filter_count);
|
||||||
|
log_info(count_debug);
|
||||||
|
|
||||||
|
sqlite3_finalize(stmt);
|
||||||
|
total_count += filter_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
char total_debug[128];
|
||||||
|
snprintf(total_debug, sizeof(total_debug), "Total COUNT result: %d", total_count);
|
||||||
|
log_info(total_debug);
|
||||||
|
|
||||||
|
// Send COUNT response - NIP-45 format: ["COUNT", <subscription_id>, {"count": <count>}]
|
||||||
|
cJSON* count_response = cJSON_CreateArray();
|
||||||
|
cJSON_AddItemToArray(count_response, cJSON_CreateString("COUNT"));
|
||||||
|
cJSON_AddItemToArray(count_response, cJSON_CreateString(sub_id));
|
||||||
|
|
||||||
|
// Create count object as per NIP-45 specification
|
||||||
|
cJSON* count_obj = cJSON_CreateObject();
|
||||||
|
cJSON_AddNumberToObject(count_obj, "count", total_count);
|
||||||
|
cJSON_AddItemToArray(count_response, count_obj);
|
||||||
|
|
||||||
|
char *count_str = cJSON_Print(count_response);
|
||||||
|
if (count_str) {
|
||||||
|
size_t count_len = strlen(count_str);
|
||||||
|
unsigned char *buf = malloc(LWS_PRE + count_len);
|
||||||
|
if (buf) {
|
||||||
|
memcpy(buf + LWS_PRE, count_str, count_len);
|
||||||
|
lws_write(wsi, buf + LWS_PRE, count_len, LWS_WRITE_TEXT);
|
||||||
|
free(buf);
|
||||||
|
}
|
||||||
|
free(count_str);
|
||||||
|
}
|
||||||
|
cJSON_Delete(count_response);
|
||||||
|
|
||||||
|
return total_count;
|
||||||
|
}
|
||||||
|
|||||||
360
tests/45_nip_test.sh
Executable file
360
tests/45_nip_test.sh
Executable file
@@ -0,0 +1,360 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# NIP-45 COUNT Message Test - Test counting functionality
|
||||||
|
# Tests COUNT messages with various filters to verify correct event counting
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Color constants
|
||||||
|
RED='\033[31m'
|
||||||
|
GREEN='\033[32m'
|
||||||
|
YELLOW='\033[33m'
|
||||||
|
BLUE='\033[34m'
|
||||||
|
BOLD='\033[1m'
|
||||||
|
RESET='\033[0m'
|
||||||
|
|
||||||
|
# Test configuration
|
||||||
|
RELAY_URL="ws://127.0.0.1:8888"
|
||||||
|
TEST_PRIVATE_KEY="nsec1j4c6269y9w0q2er2xjw8sv2ehyrtfxq3jwgdlxj6qfn8z4gjsq5qfvfk99"
|
||||||
|
|
||||||
|
# Print functions
|
||||||
|
print_header() {
|
||||||
|
echo -e "${BLUE}${BOLD}=== $1 ===${RESET}"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_step() {
|
||||||
|
echo -e "${YELLOW}[STEP]${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✓${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}✗${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_info() {
|
||||||
|
echo -e "${BLUE}[INFO]${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}[WARNING]${RESET} $1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Global arrays to store event IDs for counting tests
|
||||||
|
declare -a REGULAR_EVENT_IDS=()
|
||||||
|
declare -a REPLACEABLE_EVENT_IDS=()
|
||||||
|
declare -a EPHEMERAL_EVENT_IDS=()
|
||||||
|
declare -a ADDRESSABLE_EVENT_IDS=()
|
||||||
|
|
||||||
|
# Helper function to publish event and extract ID
|
||||||
|
publish_event() {
|
||||||
|
local event_json="$1"
|
||||||
|
local event_type="$2"
|
||||||
|
local description="$3"
|
||||||
|
|
||||||
|
# Extract event ID
|
||||||
|
local event_id=$(echo "$event_json" | jq -r '.id' 2>/dev/null)
|
||||||
|
if [[ "$event_id" == "null" || -z "$event_id" ]]; then
|
||||||
|
print_error "Could not extract event ID from $description"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_info "Publishing $description..."
|
||||||
|
|
||||||
|
# Create EVENT message in Nostr format
|
||||||
|
local event_message="[\"EVENT\",$event_json]"
|
||||||
|
|
||||||
|
# Publish to relay
|
||||||
|
local response=""
|
||||||
|
if command -v websocat &> /dev/null; then
|
||||||
|
response=$(echo "$event_message" | timeout 5s websocat "$RELAY_URL" 2>&1 || echo "Connection failed")
|
||||||
|
else
|
||||||
|
print_error "websocat not found - required for testing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check response
|
||||||
|
if [[ "$response" == *"Connection failed"* ]]; then
|
||||||
|
print_error "Failed to connect to relay for $description"
|
||||||
|
return 1
|
||||||
|
elif [[ "$response" == *"true"* ]]; then
|
||||||
|
print_success "$description uploaded (ID: ${event_id:0:16}...)"
|
||||||
|
|
||||||
|
# Store event ID in appropriate array
|
||||||
|
case "$event_type" in
|
||||||
|
"regular") REGULAR_EVENT_IDS+=("$event_id") ;;
|
||||||
|
"replaceable") REPLACEABLE_EVENT_IDS+=("$event_id") ;;
|
||||||
|
"ephemeral") EPHEMERAL_EVENT_IDS+=("$event_id") ;;
|
||||||
|
"addressable") ADDRESSABLE_EVENT_IDS+=("$event_id") ;;
|
||||||
|
esac
|
||||||
|
echo # Add blank line for readability
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_warning "$description might have failed: $response"
|
||||||
|
echo # Add blank line for readability
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Helper function to send COUNT message and check response
|
||||||
|
test_count() {
|
||||||
|
local sub_id="$1"
|
||||||
|
local filter="$2"
|
||||||
|
local description="$3"
|
||||||
|
local expected_count="$4"
|
||||||
|
|
||||||
|
print_step "Testing COUNT: $description"
|
||||||
|
|
||||||
|
# Create COUNT message
|
||||||
|
local count_message="[\"COUNT\",\"$sub_id\",$filter]"
|
||||||
|
|
||||||
|
print_info "Sending filter: $filter"
|
||||||
|
|
||||||
|
# Send COUNT message and get response
|
||||||
|
local response=""
|
||||||
|
if command -v websocat &> /dev/null; then
|
||||||
|
response=$(echo "$count_message" | timeout 3s websocat "$RELAY_URL" 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Parse COUNT response
|
||||||
|
local count_result=""
|
||||||
|
if [[ -n "$response" ]]; then
|
||||||
|
# Look for COUNT response: ["COUNT","sub_id",{"count":N}]
|
||||||
|
count_result=$(echo "$response" | grep '"COUNT"' | head -1)
|
||||||
|
if [[ -n "$count_result" ]]; then
|
||||||
|
local actual_count=$(echo "$count_result" | jq -r '.[2].count' 2>/dev/null)
|
||||||
|
if [[ "$actual_count" =~ ^[0-9]+$ ]]; then
|
||||||
|
print_info "Received count: $actual_count"
|
||||||
|
|
||||||
|
# Check if count matches expected
|
||||||
|
if [[ "$expected_count" == "any" ]]; then
|
||||||
|
print_success "$description - Count: $actual_count"
|
||||||
|
return 0
|
||||||
|
elif [[ "$actual_count" -eq "$expected_count" ]]; then
|
||||||
|
print_success "$description - Expected: $expected_count, Got: $actual_count"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
print_error "$description - Expected: $expected_count, Got: $actual_count"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "$description - Invalid count response: $count_result"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "$description - No COUNT response received"
|
||||||
|
print_error "Raw response: $response"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "$description - No response from relay"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main test function
|
||||||
|
run_count_test() {
|
||||||
|
print_header "NIP-45 COUNT Message Test"
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
print_step "Checking dependencies..."
|
||||||
|
if ! command -v nak &> /dev/null; then
|
||||||
|
print_error "nak command not found"
|
||||||
|
print_info "Please install nak: go install github.com/fiatjaf/nak@latest"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! command -v websocat &> /dev/null; then
|
||||||
|
print_error "websocat command not found"
|
||||||
|
print_info "Please install websocat for testing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! command -v jq &> /dev/null; then
|
||||||
|
print_error "jq command not found"
|
||||||
|
print_info "Please install jq for JSON processing"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
print_success "All dependencies found"
|
||||||
|
|
||||||
|
print_header "PHASE 1: Publishing Test Events"
|
||||||
|
|
||||||
|
# Test 1: Regular Events (kind 1)
|
||||||
|
print_step "Creating regular events (kind 1)..."
|
||||||
|
local regular1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #1 for counting" -k 1 --ts $(($(date +%s) - 100)) -t "type=regular" -t "test=nip45" 2>/dev/null)
|
||||||
|
local regular2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #2 for counting" -k 1 --ts $(($(date +%s) - 90)) -t "type=regular" -t "test=nip45" 2>/dev/null)
|
||||||
|
local regular3=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Regular event #3 for counting" -k 1 --ts $(($(date +%s) - 80)) -t "type=regular" -t "test=nip45" 2>/dev/null)
|
||||||
|
|
||||||
|
publish_event "$regular1" "regular" "Regular event #1"
|
||||||
|
publish_event "$regular2" "regular" "Regular event #2"
|
||||||
|
publish_event "$regular3" "regular" "Regular event #3"
|
||||||
|
|
||||||
|
# Test 2: Replaceable Events (kind 0 - metadata)
|
||||||
|
print_step "Creating replaceable events (kind 0)..."
|
||||||
|
local replaceable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User","about":"Testing NIP-45 COUNT"}' -k 0 --ts $(($(date +%s) - 70)) -t "type=replaceable" 2>/dev/null)
|
||||||
|
local replaceable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c '{"name":"Test User Updated","about":"Updated for NIP-45"}' -k 0 --ts $(($(date +%s) - 60)) -t "type=replaceable" 2>/dev/null)
|
||||||
|
|
||||||
|
publish_event "$replaceable1" "replaceable" "Replaceable event #1 (metadata)"
|
||||||
|
publish_event "$replaceable2" "replaceable" "Replaceable event #2 (metadata update)"
|
||||||
|
|
||||||
|
# Test 3: Ephemeral Events (kind 20000+) - should NOT be counted
|
||||||
|
print_step "Creating ephemeral events (kind 20001)..."
|
||||||
|
local ephemeral1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Ephemeral event - should not be counted" -k 20001 --ts $(date +%s) -t "type=ephemeral" 2>/dev/null)
|
||||||
|
|
||||||
|
publish_event "$ephemeral1" "ephemeral" "Ephemeral event (should not be counted)"
|
||||||
|
|
||||||
|
# Test 4: Addressable Events (kind 30000+)
|
||||||
|
print_step "Creating addressable events (kind 30001)..."
|
||||||
|
local addressable1=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event #1" -k 30001 --ts $(($(date +%s) - 50)) -t "d=test-article" -t "type=addressable" 2>/dev/null)
|
||||||
|
local addressable2=$(nak event --sec "$TEST_PRIVATE_KEY" -c "Addressable event #2" -k 30001 --ts $(($(date +%s) - 40)) -t "d=test-article" -t "type=addressable" 2>/dev/null)
|
||||||
|
|
||||||
|
publish_event "$addressable1" "addressable" "Addressable event #1"
|
||||||
|
publish_event "$addressable2" "addressable" "Addressable event #2"
|
||||||
|
|
||||||
|
# Brief pause to let events settle
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
print_header "PHASE 2: Testing COUNT Messages"
|
||||||
|
|
||||||
|
local test_failures=0
|
||||||
|
|
||||||
|
# Test 1: Count all events
|
||||||
|
if ! test_count "count_all" '{}' "Count all events" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: Count events by kind
|
||||||
|
if ! test_count "count_kind1" '{"kinds":[1]}' "Count kind 1 events" "3"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
if ! test_count "count_kind0" '{"kinds":[0]}' "Count kind 0 events" "1"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
if ! test_count "count_kind30001" '{"kinds":[30001]}' "Count kind 30001 events" "1"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: Count events by author (pubkey)
|
||||||
|
local test_pubkey=$(echo "$regular1" | jq -r '.pubkey' 2>/dev/null)
|
||||||
|
if ! test_count "count_author" "{\"authors\":[\"$test_pubkey\"]}" "Count events by specific author" "5"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4: Count recent events (time-based)
|
||||||
|
local recent_timestamp=$(($(date +%s) - 200))
|
||||||
|
if ! test_count "count_recent" "{\"since\":$recent_timestamp}" "Count recent events" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 5: Count events with specific tags
|
||||||
|
if ! test_count "count_tag_type" '{"#type":["regular"]}' "Count events with type=regular tag" "3"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
if ! test_count "count_tag_test" '{"#test":["nip45"]}' "Count events with test=nip45 tag" "3"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 6: Count multiple kinds
|
||||||
|
if ! test_count "count_multi_kinds" '{"kinds":[0,1]}' "Count multiple kinds (0,1)" "4"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 7: Count with time range
|
||||||
|
local start_time=$(($(date +%s) - 120))
|
||||||
|
local end_time=$(($(date +%s) - 60))
|
||||||
|
if ! test_count "count_time_range" "{\"since\":$start_time,\"until\":$end_time}" "Count events in time range" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 8: Count specific event IDs
|
||||||
|
if [[ ${#REGULAR_EVENT_IDS[@]} -gt 0 ]]; then
|
||||||
|
local test_event_id="${REGULAR_EVENT_IDS[0]}"
|
||||||
|
if ! test_count "count_specific_id" "{\"ids\":[\"$test_event_id\"]}" "Count specific event ID" "1"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 9: Count with multiple filters combined
|
||||||
|
if ! test_count "count_combined" '{"kinds":[1],"#type":["regular"],"#test":["nip45"]}' "Count with combined filters" "3"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 10: Count ephemeral events (should be 0 since they're not stored)
|
||||||
|
if ! test_count "count_ephemeral" '{"kinds":[20001]}' "Count ephemeral events (should be 0)" "0"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 11: Count with limit (should still count all matching, ignore limit)
|
||||||
|
if ! test_count "count_with_limit" '{"kinds":[1],"limit":1}' "Count with limit (should ignore limit)" "3"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 12: Count non-existent kind
|
||||||
|
if ! test_count "count_nonexistent" '{"kinds":[99999]}' "Count non-existent kind" "0"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 13: Count with empty filter
|
||||||
|
if ! test_count "count_empty_filter" '{}' "Count with empty filter" "any"; then
|
||||||
|
((test_failures++))
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Report test results
|
||||||
|
if [[ $test_failures -gt 0 ]]; then
|
||||||
|
print_error "COUNT TESTS FAILED: $test_failures test(s) failed"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
print_success "All COUNT tests passed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
print_header "PHASE 3: Database Verification"
|
||||||
|
|
||||||
|
# Check what's actually stored in the database
|
||||||
|
print_step "Verifying database contents..."
|
||||||
|
|
||||||
|
if command -v sqlite3 &> /dev/null; then
|
||||||
|
# Find the database file (should be in build/ directory with relay pubkey as filename)
|
||||||
|
local db_file=""
|
||||||
|
if [[ -d "../build" ]]; then
|
||||||
|
db_file=$(find ../build -name "*.db" -type f | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$db_file" && -f "$db_file" ]]; then
|
||||||
|
print_info "Events by type in database ($db_file):"
|
||||||
|
sqlite3 "$db_file" "SELECT event_type, COUNT(*) as count FROM events GROUP BY event_type;" 2>/dev/null | while read line; do
|
||||||
|
echo " $line"
|
||||||
|
done
|
||||||
|
|
||||||
|
print_info "Total events in database:"
|
||||||
|
sqlite3 "$db_file" "SELECT COUNT(*) FROM events;" 2>/dev/null
|
||||||
|
|
||||||
|
print_success "Database verification complete"
|
||||||
|
else
|
||||||
|
print_warning "Database file not found in build/ directory"
|
||||||
|
print_info "Expected database files: build/*.db (named after relay pubkey)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "sqlite3 not available for database verification"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run the COUNT test
|
||||||
|
print_header "Starting NIP-45 COUNT Message Test Suite"
|
||||||
|
echo
|
||||||
|
|
||||||
|
if run_count_test; then
|
||||||
|
echo
|
||||||
|
print_success "All NIP-45 COUNT tests completed successfully!"
|
||||||
|
print_info "The C-Relay COUNT functionality is working correctly"
|
||||||
|
print_info "✅ COUNT messages are processed and return correct event counts"
|
||||||
|
echo
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo
|
||||||
|
print_error "❌ NIP-45 COUNT TESTS FAILED!"
|
||||||
|
print_error "The COUNT functionality has issues that need to be fixed"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user