#define _GNU_SOURCE ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// // NIP-09 EVENT DELETION REQUEST HANDLING ///////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////// #include #include "debug.h" #include #include #include #include #include // Forward declaration for database functions int store_event(cJSON* event); // Forward declarations for deletion functions int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids); int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp); // Global database variable extern sqlite3* g_db; // Handle NIP-09 deletion request event (kind 5) int handle_deletion_request(cJSON* event, char* error_message, size_t error_size) { if (!event) { snprintf(error_message, error_size, "invalid: null deletion request"); return -1; } // Extract event details cJSON* kind_obj = cJSON_GetObjectItem(event, "kind"); cJSON* pubkey_obj = cJSON_GetObjectItem(event, "pubkey"); cJSON* created_at_obj = cJSON_GetObjectItem(event, "created_at"); cJSON* tags_obj = cJSON_GetObjectItem(event, "tags"); cJSON* content_obj = cJSON_GetObjectItem(event, "content"); cJSON* event_id_obj = cJSON_GetObjectItem(event, "id"); if (!kind_obj || !pubkey_obj || !created_at_obj || !tags_obj || !event_id_obj) { snprintf(error_message, error_size, "invalid: incomplete deletion request"); return -1; } int kind = (int)cJSON_GetNumberValue(kind_obj); if (kind != 5) { snprintf(error_message, error_size, "invalid: not a deletion request"); return -1; } const char* requester_pubkey = cJSON_GetStringValue(pubkey_obj); // Extract deletion event ID and reason (for potential logging) const char* deletion_event_id = cJSON_GetStringValue(event_id_obj); const char* reason = content_obj ? cJSON_GetStringValue(content_obj) : ""; (void)deletion_event_id; // Mark as intentionally unused for now (void)reason; // Mark as intentionally unused for now long deletion_timestamp = (long)cJSON_GetNumberValue(created_at_obj); if (!cJSON_IsArray(tags_obj)) { snprintf(error_message, error_size, "invalid: deletion request tags must be an array"); return -1; } // Collect event IDs and addresses from tags cJSON* event_ids = cJSON_CreateArray(); cJSON* addresses = cJSON_CreateArray(); cJSON* kinds_to_delete = cJSON_CreateArray(); int deletion_targets_found = 0; cJSON* tag = NULL; cJSON_ArrayForEach(tag, tags_obj) { if (!cJSON_IsArray(tag) || cJSON_GetArraySize(tag) < 2) { continue; } cJSON* tag_name = cJSON_GetArrayItem(tag, 0); cJSON* tag_value = cJSON_GetArrayItem(tag, 1); if (!cJSON_IsString(tag_name) || !cJSON_IsString(tag_value)) { continue; } const char* name = cJSON_GetStringValue(tag_name); const char* value = cJSON_GetStringValue(tag_value); if (strcmp(name, "e") == 0) { // Event ID reference cJSON_AddItemToArray(event_ids, cJSON_CreateString(value)); deletion_targets_found++; } else if (strcmp(name, "a") == 0) { // Addressable event reference (kind:pubkey:d-identifier) cJSON_AddItemToArray(addresses, cJSON_CreateString(value)); deletion_targets_found++; } else if (strcmp(name, "k") == 0) { // Kind hint - store for validation but not required int kind_hint = atoi(value); if (kind_hint > 0) { cJSON_AddItemToArray(kinds_to_delete, cJSON_CreateNumber(kind_hint)); } } } if (deletion_targets_found == 0) { cJSON_Delete(event_ids); cJSON_Delete(addresses); cJSON_Delete(kinds_to_delete); snprintf(error_message, error_size, "invalid: deletion request must contain 'e' or 'a' tags"); return -1; } int deleted_count = 0; // Process event ID deletions if (cJSON_GetArraySize(event_ids) > 0) { int result = delete_events_by_id(requester_pubkey, event_ids); if (result > 0) { deleted_count += result; } } // Process addressable event deletions if (cJSON_GetArraySize(addresses) > 0) { int result = delete_events_by_address(requester_pubkey, addresses, deletion_timestamp); if (result > 0) { deleted_count += result; } } // Clean up cJSON_Delete(event_ids); cJSON_Delete(addresses); cJSON_Delete(kinds_to_delete); // Store the deletion request itself (it should be kept according to NIP-09) if (store_event(event) != 0) { DEBUG_WARN("Failed to store deletion request event"); } error_message[0] = '\0'; // Success - empty error message return 0; } // Delete events by ID (with pubkey authorization) int delete_events_by_id(const char* requester_pubkey, cJSON* event_ids) { if (!g_db || !requester_pubkey || !event_ids || !cJSON_IsArray(event_ids)) { return 0; } int deleted_count = 0; cJSON* event_id = NULL; cJSON_ArrayForEach(event_id, event_ids) { if (!cJSON_IsString(event_id)) { continue; } const char* id = cJSON_GetStringValue(event_id); // First check if event exists and if requester is authorized const char* check_sql = "SELECT pubkey FROM events WHERE id = ?"; sqlite3_stmt* check_stmt; int rc = sqlite3_prepare_v2(g_db, check_sql, -1, &check_stmt, NULL); if (rc != SQLITE_OK) { continue; } sqlite3_bind_text(check_stmt, 1, id, -1, SQLITE_STATIC); if (sqlite3_step(check_stmt) == SQLITE_ROW) { const char* event_pubkey = (char*)sqlite3_column_text(check_stmt, 0); // Only delete if the requester is the author if (event_pubkey && strcmp(event_pubkey, requester_pubkey) == 0) { sqlite3_finalize(check_stmt); // Delete the event const char* delete_sql = "DELETE FROM events WHERE id = ? AND pubkey = ?"; sqlite3_stmt* delete_stmt; rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_text(delete_stmt, 1, id, -1, SQLITE_STATIC); sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC); if (sqlite3_step(delete_stmt) == SQLITE_DONE && sqlite3_changes(g_db) > 0) { deleted_count++; } sqlite3_finalize(delete_stmt); } } else { sqlite3_finalize(check_stmt); char warning_msg[128]; snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for event: %.16s...", id); DEBUG_WARN(warning_msg); } } else { sqlite3_finalize(check_stmt); } } return deleted_count; } // Delete events by addressable reference (kind:pubkey:d-identifier) int delete_events_by_address(const char* requester_pubkey, cJSON* addresses, long deletion_timestamp) { if (!g_db || !requester_pubkey || !addresses || !cJSON_IsArray(addresses)) { return 0; } int deleted_count = 0; cJSON* address = NULL; cJSON_ArrayForEach(address, addresses) { if (!cJSON_IsString(address)) { continue; } const char* addr = cJSON_GetStringValue(address); // Parse address format: kind:pubkey:d-identifier char* addr_copy = strdup(addr); if (!addr_copy) continue; char* kind_str = strtok(addr_copy, ":"); char* pubkey_str = strtok(NULL, ":"); char* d_identifier = strtok(NULL, ":"); if (!kind_str || !pubkey_str) { free(addr_copy); continue; } int kind = atoi(kind_str); // Only delete if the requester is the author if (strcmp(pubkey_str, requester_pubkey) != 0) { free(addr_copy); char warning_msg[128]; snprintf(warning_msg, sizeof(warning_msg), "Unauthorized deletion attempt for address: %.32s...", addr); DEBUG_WARN(warning_msg); continue; } // Build deletion query based on whether we have d-identifier const char* delete_sql; sqlite3_stmt* delete_stmt; if (d_identifier && strlen(d_identifier) > 0) { // Delete specific addressable event with d-tag delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ? " "AND json_extract(tags, '$[*]') LIKE '%[\"d\",\"' || ? || '\"]%'"; } else { // Delete all events of this kind by this author up to deletion timestamp delete_sql = "DELETE FROM events WHERE kind = ? AND pubkey = ? AND created_at <= ?"; } int rc = sqlite3_prepare_v2(g_db, delete_sql, -1, &delete_stmt, NULL); if (rc == SQLITE_OK) { sqlite3_bind_int(delete_stmt, 1, kind); sqlite3_bind_text(delete_stmt, 2, requester_pubkey, -1, SQLITE_STATIC); sqlite3_bind_int64(delete_stmt, 3, deletion_timestamp); if (d_identifier && strlen(d_identifier) > 0) { sqlite3_bind_text(delete_stmt, 4, d_identifier, -1, SQLITE_STATIC); } if (sqlite3_step(delete_stmt) == SQLITE_DONE) { int changes = sqlite3_changes(g_db); if (changes > 0) { deleted_count += changes; } } sqlite3_finalize(delete_stmt); } free(addr_copy); } return deleted_count; } // Mark event as deleted (alternative to hard deletion - not used in current implementation) int mark_event_as_deleted(const char* event_id, const char* deletion_event_id, const char* reason) { (void)event_id; (void)deletion_event_id; (void)reason; // Suppress unused warnings // This function could be used if we wanted to implement soft deletion // For now, NIP-09 implementation uses hard deletion as specified return 0; }