296 lines
11 KiB
C
296 lines
11 KiB
C
#define _GNU_SOURCE
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
// NIP-09 EVENT DELETION REQUEST HANDLING
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
/////////////////////////////////////////////////////////////////////////////////////////
|
|
#include <cjson/cJSON.h>
|
|
#include "debug.h"
|
|
#include <sqlite3.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <time.h>
|
|
#include <stdio.h>
|
|
|
|
|
|
// 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;
|
|
}
|