handle-nip86-curating.go raw

   1  package app
   2  
   3  import (
   4  	"context"
   5  	"encoding/hex"
   6  	"encoding/json"
   7  	"io"
   8  	"net/http"
   9  	"strconv"
  10  
  11  	"next.orly.dev/pkg/lol/chk"
  12  	"next.orly.dev/pkg/acl"
  13  	"next.orly.dev/pkg/database"
  14  	"next.orly.dev/pkg/nostr/httpauth"
  15  )
  16  
  17  // handleCuratingNIP86Request handles curating NIP-86 requests with pre-authenticated pubkey.
  18  // This is called from the main NIP-86 handler after authentication.
  19  func (s *Server) handleCuratingNIP86Request(w http.ResponseWriter, r *http.Request, pubkey []byte) {
  20  	_ = pubkey // Pubkey already validated by caller
  21  
  22  	// Get the curating ACL instance
  23  	var curatingACL *acl.Curating
  24  	for _, aclInstance := range acl.Registry.ACLs() {
  25  		if aclInstance.Type() == "curating" {
  26  			if curating, ok := aclInstance.(*acl.Curating); ok {
  27  				curatingACL = curating
  28  				break
  29  			}
  30  		}
  31  	}
  32  
  33  	if curatingACL == nil {
  34  		http.Error(w, "Curating ACL not available", http.StatusInternalServerError)
  35  		return
  36  	}
  37  
  38  	// Read and parse the request
  39  	body, err := io.ReadAll(r.Body)
  40  	if chk.E(err) {
  41  		http.Error(w, "Failed to read request body", http.StatusBadRequest)
  42  		return
  43  	}
  44  
  45  	var request NIP86Request
  46  	if err := json.Unmarshal(body, &request); chk.E(err) {
  47  		http.Error(w, "Invalid JSON request", http.StatusBadRequest)
  48  		return
  49  	}
  50  
  51  	// Set response headers
  52  	w.Header().Set("Content-Type", "application/json")
  53  
  54  	// Handle the request based on method
  55  	response := s.handleCuratingNIP86Method(request, curatingACL)
  56  
  57  	// Send response
  58  	jsonData, err := json.Marshal(response)
  59  	if chk.E(err) {
  60  		http.Error(w, "Error generating response", http.StatusInternalServerError)
  61  		return
  62  	}
  63  
  64  	w.Write(jsonData)
  65  }
  66  
  67  // handleCuratingNIP86Management handles NIP-86 management API requests for curating mode (standalone)
  68  func (s *Server) handleCuratingNIP86Management(w http.ResponseWriter, r *http.Request) {
  69  	if r.Method != http.MethodPost {
  70  		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
  71  		return
  72  	}
  73  
  74  	// Check Content-Type
  75  	contentType := r.Header.Get("Content-Type")
  76  	if contentType != "application/nostr+json+rpc" {
  77  		http.Error(w, "Content-Type must be application/nostr+json+rpc", http.StatusBadRequest)
  78  		return
  79  	}
  80  
  81  	// Validate NIP-98 authentication
  82  	valid, pubkey, err := httpauth.CheckAuth(r)
  83  	if chk.E(err) || !valid {
  84  		errorMsg := "NIP-98 authentication validation failed"
  85  		if err != nil {
  86  			errorMsg = err.Error()
  87  		}
  88  		http.Error(w, errorMsg, http.StatusUnauthorized)
  89  		return
  90  	}
  91  
  92  	// Check permissions - require owner or admin level
  93  	accessLevel := acl.Registry.GetAccessLevel(pubkey, r.RemoteAddr)
  94  	if accessLevel != "owner" && accessLevel != "admin" {
  95  		http.Error(w, "Owner or admin permission required", http.StatusForbidden)
  96  		return
  97  	}
  98  
  99  	// Check if curating ACL is active
 100  	if acl.Registry.Type() != "curating" {
 101  		http.Error(w, "Curating ACL mode is not active", http.StatusBadRequest)
 102  		return
 103  	}
 104  
 105  	// Delegate to shared request handler
 106  	s.handleCuratingNIP86Request(w, r, pubkey)
 107  }
 108  
 109  // handleCuratingNIP86Method handles individual NIP-86 methods for curating mode
 110  func (s *Server) handleCuratingNIP86Method(request NIP86Request, curatingACL *acl.Curating) NIP86Response {
 111  	dbACL := curatingACL.GetCuratingACL()
 112  
 113  	switch request.Method {
 114  	case "supportedmethods":
 115  		return s.handleCuratingSupportedMethods()
 116  	case "trustpubkey":
 117  		return s.handleTrustPubkey(request.Params, curatingACL)
 118  	case "untrustpubkey":
 119  		return s.handleUntrustPubkey(request.Params, curatingACL)
 120  	case "listtrustedpubkeys":
 121  		return s.handleListTrustedPubkeys(dbACL)
 122  	case "blacklistpubkey":
 123  		return s.handleBlacklistPubkey(request.Params, curatingACL)
 124  	case "unblacklistpubkey":
 125  		return s.handleUnblacklistPubkey(request.Params, curatingACL)
 126  	case "listblacklistedpubkeys":
 127  		return s.handleListBlacklistedPubkeys(dbACL)
 128  	case "listunclassifiedusers":
 129  		return s.handleListUnclassifiedUsers(request.Params, dbACL)
 130  	case "markspam":
 131  		return s.handleMarkSpam(request.Params, dbACL)
 132  	case "unmarkspam":
 133  		return s.handleUnmarkSpam(request.Params, dbACL)
 134  	case "listspamevents":
 135  		return s.handleListSpamEvents(dbACL)
 136  	case "deleteevent":
 137  		return s.handleDeleteEvent(request.Params)
 138  	case "getcuratingconfig":
 139  		return s.handleGetCuratingConfig(dbACL)
 140  	case "listblockedips":
 141  		return s.handleListCuratingBlockedIPs(dbACL)
 142  	case "unblockip":
 143  		return s.handleUnblockCuratingIP(request.Params, dbACL)
 144  	case "isconfigured":
 145  		return s.handleIsConfigured(dbACL)
 146  	case "scanpubkeys":
 147  		return s.handleScanPubkeys(dbACL)
 148  	case "geteventsforpubkey":
 149  		return s.handleGetEventsForPubkey(request.Params, dbACL)
 150  	case "deleteeventsforpubkey":
 151  		return s.handleDeleteEventsForPubkey(request.Params, dbACL)
 152  	default:
 153  		return NIP86Response{Error: "Unknown method: " + request.Method}
 154  	}
 155  }
 156  
 157  // handleCuratingSupportedMethods returns the list of supported methods for curating mode
 158  func (s *Server) handleCuratingSupportedMethods() NIP86Response {
 159  	methods := []string{
 160  		"supportedmethods",
 161  		"trustpubkey",
 162  		"untrustpubkey",
 163  		"listtrustedpubkeys",
 164  		"blacklistpubkey",
 165  		"unblacklistpubkey",
 166  		"listblacklistedpubkeys",
 167  		"listunclassifiedusers",
 168  		"markspam",
 169  		"unmarkspam",
 170  		"listspamevents",
 171  		"deleteevent",
 172  		"getcuratingconfig",
 173  		"listblockedips",
 174  		"unblockip",
 175  		"isconfigured",
 176  		"scanpubkeys",
 177  		"geteventsforpubkey",
 178  		"deleteeventsforpubkey",
 179  	}
 180  	return NIP86Response{Result: methods}
 181  }
 182  
 183  // handleTrustPubkey adds a pubkey to the trusted list
 184  func (s *Server) handleTrustPubkey(params []interface{}, curatingACL *acl.Curating) NIP86Response {
 185  	if len(params) < 1 {
 186  		return NIP86Response{Error: "Missing required parameter: pubkey"}
 187  	}
 188  
 189  	pubkey, ok := params[0].(string)
 190  	if !ok {
 191  		return NIP86Response{Error: "Invalid pubkey parameter"}
 192  	}
 193  
 194  	if len(pubkey) != 64 {
 195  		return NIP86Response{Error: "Invalid pubkey format (must be 64 hex characters)"}
 196  	}
 197  
 198  	note := ""
 199  	if len(params) > 1 {
 200  		if n, ok := params[1].(string); ok {
 201  			note = n
 202  		}
 203  	}
 204  
 205  	if err := curatingACL.TrustPubkey(pubkey, note); chk.E(err) {
 206  		return NIP86Response{Error: "Failed to trust pubkey: " + err.Error()}
 207  	}
 208  
 209  	return NIP86Response{Result: true}
 210  }
 211  
 212  // handleUntrustPubkey removes a pubkey from the trusted list
 213  func (s *Server) handleUntrustPubkey(params []interface{}, curatingACL *acl.Curating) NIP86Response {
 214  	if len(params) < 1 {
 215  		return NIP86Response{Error: "Missing required parameter: pubkey"}
 216  	}
 217  
 218  	pubkey, ok := params[0].(string)
 219  	if !ok {
 220  		return NIP86Response{Error: "Invalid pubkey parameter"}
 221  	}
 222  
 223  	if err := curatingACL.UntrustPubkey(pubkey); chk.E(err) {
 224  		return NIP86Response{Error: "Failed to untrust pubkey: " + err.Error()}
 225  	}
 226  
 227  	return NIP86Response{Result: true}
 228  }
 229  
 230  // handleListTrustedPubkeys returns the list of trusted pubkeys
 231  func (s *Server) handleListTrustedPubkeys(dbACL *database.CuratingACL) NIP86Response {
 232  	trusted, err := dbACL.ListTrustedPubkeys()
 233  	if chk.E(err) {
 234  		return NIP86Response{Error: "Failed to list trusted pubkeys: " + err.Error()}
 235  	}
 236  
 237  	result := make([]map[string]interface{}, len(trusted))
 238  	for i, t := range trusted {
 239  		result[i] = map[string]interface{}{
 240  			"pubkey": t.Pubkey,
 241  			"note":   t.Note,
 242  			"added":  t.Added.Unix(),
 243  		}
 244  	}
 245  
 246  	return NIP86Response{Result: result}
 247  }
 248  
 249  // handleBlacklistPubkey adds a pubkey to the blacklist
 250  func (s *Server) handleBlacklistPubkey(params []interface{}, curatingACL *acl.Curating) NIP86Response {
 251  	if len(params) < 1 {
 252  		return NIP86Response{Error: "Missing required parameter: pubkey"}
 253  	}
 254  
 255  	pubkey, ok := params[0].(string)
 256  	if !ok {
 257  		return NIP86Response{Error: "Invalid pubkey parameter"}
 258  	}
 259  
 260  	if len(pubkey) != 64 {
 261  		return NIP86Response{Error: "Invalid pubkey format (must be 64 hex characters)"}
 262  	}
 263  
 264  	reason := ""
 265  	if len(params) > 1 {
 266  		if r, ok := params[1].(string); ok {
 267  			reason = r
 268  		}
 269  	}
 270  
 271  	if err := curatingACL.BlacklistPubkey(pubkey, reason); chk.E(err) {
 272  		return NIP86Response{Error: "Failed to blacklist pubkey: " + err.Error()}
 273  	}
 274  
 275  	return NIP86Response{Result: true}
 276  }
 277  
 278  // handleUnblacklistPubkey removes a pubkey from the blacklist
 279  func (s *Server) handleUnblacklistPubkey(params []interface{}, curatingACL *acl.Curating) NIP86Response {
 280  	if len(params) < 1 {
 281  		return NIP86Response{Error: "Missing required parameter: pubkey"}
 282  	}
 283  
 284  	pubkey, ok := params[0].(string)
 285  	if !ok {
 286  		return NIP86Response{Error: "Invalid pubkey parameter"}
 287  	}
 288  
 289  	if err := curatingACL.UnblacklistPubkey(pubkey); chk.E(err) {
 290  		return NIP86Response{Error: "Failed to unblacklist pubkey: " + err.Error()}
 291  	}
 292  
 293  	return NIP86Response{Result: true}
 294  }
 295  
 296  // handleListBlacklistedPubkeys returns the list of blacklisted pubkeys
 297  func (s *Server) handleListBlacklistedPubkeys(dbACL *database.CuratingACL) NIP86Response {
 298  	blacklisted, err := dbACL.ListBlacklistedPubkeys()
 299  	if chk.E(err) {
 300  		return NIP86Response{Error: "Failed to list blacklisted pubkeys: " + err.Error()}
 301  	}
 302  
 303  	result := make([]map[string]interface{}, len(blacklisted))
 304  	for i, b := range blacklisted {
 305  		result[i] = map[string]interface{}{
 306  			"pubkey": b.Pubkey,
 307  			"reason": b.Reason,
 308  			"added":  b.Added.Unix(),
 309  		}
 310  	}
 311  
 312  	return NIP86Response{Result: result}
 313  }
 314  
 315  // handleListUnclassifiedUsers returns unclassified users sorted by event count
 316  func (s *Server) handleListUnclassifiedUsers(params []interface{}, dbACL *database.CuratingACL) NIP86Response {
 317  	limit := 100 // Default limit
 318  	if len(params) > 0 {
 319  		if l, ok := params[0].(float64); ok {
 320  			limit = int(l)
 321  		}
 322  	}
 323  
 324  	users, err := dbACL.ListUnclassifiedUsers(limit)
 325  	if chk.E(err) {
 326  		return NIP86Response{Error: "Failed to list unclassified users: " + err.Error()}
 327  	}
 328  
 329  	result := make([]map[string]interface{}, len(users))
 330  	for i, u := range users {
 331  		result[i] = map[string]interface{}{
 332  			"pubkey":      u.Pubkey,
 333  			"event_count": u.EventCount,
 334  			"last_event":  u.LastEvent.Unix(),
 335  		}
 336  	}
 337  
 338  	return NIP86Response{Result: result}
 339  }
 340  
 341  // handleMarkSpam marks an event as spam
 342  func (s *Server) handleMarkSpam(params []interface{}, dbACL *database.CuratingACL) NIP86Response {
 343  	if len(params) < 1 {
 344  		return NIP86Response{Error: "Missing required parameter: event_id"}
 345  	}
 346  
 347  	eventID, ok := params[0].(string)
 348  	if !ok {
 349  		return NIP86Response{Error: "Invalid event_id parameter"}
 350  	}
 351  
 352  	if len(eventID) != 64 {
 353  		return NIP86Response{Error: "Invalid event_id format (must be 64 hex characters)"}
 354  	}
 355  
 356  	pubkey := ""
 357  	if len(params) > 1 {
 358  		if p, ok := params[1].(string); ok {
 359  			pubkey = p
 360  		}
 361  	}
 362  
 363  	reason := ""
 364  	if len(params) > 2 {
 365  		if r, ok := params[2].(string); ok {
 366  			reason = r
 367  		}
 368  	}
 369  
 370  	if err := dbACL.MarkEventAsSpam(eventID, pubkey, reason); chk.E(err) {
 371  		return NIP86Response{Error: "Failed to mark event as spam: " + err.Error()}
 372  	}
 373  
 374  	return NIP86Response{Result: true}
 375  }
 376  
 377  // handleUnmarkSpam removes the spam flag from an event
 378  func (s *Server) handleUnmarkSpam(params []interface{}, dbACL *database.CuratingACL) NIP86Response {
 379  	if len(params) < 1 {
 380  		return NIP86Response{Error: "Missing required parameter: event_id"}
 381  	}
 382  
 383  	eventID, ok := params[0].(string)
 384  	if !ok {
 385  		return NIP86Response{Error: "Invalid event_id parameter"}
 386  	}
 387  
 388  	if err := dbACL.UnmarkEventAsSpam(eventID); chk.E(err) {
 389  		return NIP86Response{Error: "Failed to unmark event as spam: " + err.Error()}
 390  	}
 391  
 392  	return NIP86Response{Result: true}
 393  }
 394  
 395  // handleListSpamEvents returns the list of spam-flagged events
 396  func (s *Server) handleListSpamEvents(dbACL *database.CuratingACL) NIP86Response {
 397  	spam, err := dbACL.ListSpamEvents()
 398  	if chk.E(err) {
 399  		return NIP86Response{Error: "Failed to list spam events: " + err.Error()}
 400  	}
 401  
 402  	result := make([]map[string]interface{}, len(spam))
 403  	for i, sp := range spam {
 404  		result[i] = map[string]interface{}{
 405  			"event_id": sp.EventID,
 406  			"pubkey":   sp.Pubkey,
 407  			"reason":   sp.Reason,
 408  			"added":    sp.Added.Unix(),
 409  		}
 410  	}
 411  
 412  	return NIP86Response{Result: result}
 413  }
 414  
 415  // handleDeleteEvent permanently deletes an event from the database
 416  func (s *Server) handleDeleteEvent(params []interface{}) NIP86Response {
 417  	if len(params) < 1 {
 418  		return NIP86Response{Error: "Missing required parameter: event_id"}
 419  	}
 420  
 421  	eventIDHex, ok := params[0].(string)
 422  	if !ok {
 423  		return NIP86Response{Error: "Invalid event_id parameter"}
 424  	}
 425  
 426  	if len(eventIDHex) != 64 {
 427  		return NIP86Response{Error: "Invalid event_id format (must be 64 hex characters)"}
 428  	}
 429  
 430  	// Convert hex to bytes
 431  	eventID, err := hex.DecodeString(eventIDHex)
 432  	if err != nil {
 433  		return NIP86Response{Error: "Invalid event_id hex: " + err.Error()}
 434  	}
 435  
 436  	// Delete from database
 437  	if err := s.DB.DeleteEvent(context.Background(), eventID); chk.E(err) {
 438  		return NIP86Response{Error: "Failed to delete event: " + err.Error()}
 439  	}
 440  
 441  	return NIP86Response{Result: true}
 442  }
 443  
 444  // handleGetCuratingConfig returns the current curating configuration
 445  func (s *Server) handleGetCuratingConfig(dbACL *database.CuratingACL) NIP86Response {
 446  	config, err := dbACL.GetConfig()
 447  	if chk.E(err) {
 448  		return NIP86Response{Error: "Failed to get config: " + err.Error()}
 449  	}
 450  
 451  	result := map[string]interface{}{
 452  		"daily_limit":       config.DailyLimit,
 453  		"first_ban_hours":   config.FirstBanHours,
 454  		"second_ban_hours":  config.SecondBanHours,
 455  		"allowed_kinds":     config.AllowedKinds,
 456  		"custom_kinds":      config.AllowedKinds, // Alias for frontend compatibility
 457  		"allowed_ranges":    config.AllowedRanges,
 458  		"kind_ranges":       config.AllowedRanges, // Alias for frontend compatibility
 459  		"kind_categories":   config.KindCategories,
 460  		"categories":        config.KindCategories, // Alias for frontend compatibility
 461  		"config_event_id":   config.ConfigEventID,
 462  		"config_pubkey":     config.ConfigPubkey,
 463  		"configured_at":     config.ConfiguredAt,
 464  		"is_configured":     config.ConfigEventID != "",
 465  	}
 466  
 467  	return NIP86Response{Result: result}
 468  }
 469  
 470  // handleListCuratingBlockedIPs returns the list of blocked IPs in curating mode
 471  func (s *Server) handleListCuratingBlockedIPs(dbACL *database.CuratingACL) NIP86Response {
 472  	blocked, err := dbACL.ListBlockedIPs()
 473  	if chk.E(err) {
 474  		return NIP86Response{Error: "Failed to list blocked IPs: " + err.Error()}
 475  	}
 476  
 477  	result := make([]map[string]interface{}, len(blocked))
 478  	for i, b := range blocked {
 479  		result[i] = map[string]interface{}{
 480  			"ip":         b.IP,
 481  			"reason":     b.Reason,
 482  			"expires_at": b.ExpiresAt.Unix(),
 483  			"added":      b.Added.Unix(),
 484  		}
 485  	}
 486  
 487  	return NIP86Response{Result: result}
 488  }
 489  
 490  // handleUnblockCuratingIP unblocks an IP in curating mode
 491  func (s *Server) handleUnblockCuratingIP(params []interface{}, dbACL *database.CuratingACL) NIP86Response {
 492  	if len(params) < 1 {
 493  		return NIP86Response{Error: "Missing required parameter: ip"}
 494  	}
 495  
 496  	ip, ok := params[0].(string)
 497  	if !ok {
 498  		return NIP86Response{Error: "Invalid ip parameter"}
 499  	}
 500  
 501  	if err := dbACL.UnblockIP(ip); chk.E(err) {
 502  		return NIP86Response{Error: "Failed to unblock IP: " + err.Error()}
 503  	}
 504  
 505  	return NIP86Response{Result: true}
 506  }
 507  
 508  // handleIsConfigured checks if curating mode is configured
 509  func (s *Server) handleIsConfigured(dbACL *database.CuratingACL) NIP86Response {
 510  	configured, err := dbACL.IsConfigured()
 511  	if chk.E(err) {
 512  		return NIP86Response{Error: "Failed to check configuration: " + err.Error()}
 513  	}
 514  
 515  	return NIP86Response{Result: configured}
 516  }
 517  
 518  // GetKindCategoriesInfo returns information about available kind categories
 519  func GetKindCategoriesInfo() []map[string]interface{} {
 520  	categories := []map[string]interface{}{
 521  		{
 522  			"id":          "social",
 523  			"name":        "Social/Notes",
 524  			"description": "Profiles, text notes, follows, reposts, reactions",
 525  			"kinds":       []int{0, 1, 3, 6, 7, 10002},
 526  		},
 527  		{
 528  			"id":          "dm",
 529  			"name":        "Direct Messages",
 530  			"description": "NIP-04 DMs, NIP-17 private messages, gift wraps",
 531  			"kinds":       []int{4, 14, 1059},
 532  		},
 533  		{
 534  			"id":          "longform",
 535  			"name":        "Long-form Content",
 536  			"description": "Articles and drafts",
 537  			"kinds":       []int{30023, 30024},
 538  		},
 539  		{
 540  			"id":          "media",
 541  			"name":        "Media",
 542  			"description": "File metadata, video, audio",
 543  			"kinds":       []int{1063, 20, 21, 22},
 544  		},
 545  		{
 546  			"id":          "marketplace_nip15",
 547  			"name":        "Marketplace (NIP-15)",
 548  			"description": "Legacy NIP-15 stalls and products",
 549  			"kinds":       []int{30017, 30018, 30019, 30020, 1021, 1022},
 550  		},
 551  		{
 552  			"id":          "marketplace_nip99",
 553  			"name":        "Marketplace (NIP-99/Gamma)",
 554  			"description": "NIP-99 classified listings, collections, shipping, reviews (Plebeian Market)",
 555  			"kinds":       []int{30402, 30403, 30405, 30406, 31555},
 556  		},
 557  		{
 558  			"id":          "order_communication",
 559  			"name":        "Order Communication",
 560  			"description": "Gamma Markets order messages and payment receipts",
 561  			"kinds":       []int{16, 17},
 562  		},
 563  		{
 564  			"id":          "groups_nip29",
 565  			"name":        "Group Messaging (NIP-29)",
 566  			"description": "Simple group messages and metadata",
 567  			"kinds":       []int{9, 10, 11, 12, 9000, 9001, 9002, 39000, 39001, 39002},
 568  		},
 569  		{
 570  			"id":          "groups_nip72",
 571  			"name":        "Communities (NIP-72)",
 572  			"description": "Moderated communities and post approvals",
 573  			"kinds":       []int{34550, 1111, 4550},
 574  		},
 575  		{
 576  			"id":          "lists",
 577  			"name":        "Lists/Bookmarks",
 578  			"description": "Mute lists, pins, categorized lists, bookmarks",
 579  			"kinds":       []int{10000, 10001, 10003, 30000, 30001, 30003},
 580  		},
 581  	}
 582  	return categories
 583  }
 584  
 585  // expandKindRange expands a range string like "1000-1999" into individual kinds
 586  func expandKindRange(rangeStr string) []int {
 587  	var kinds []int
 588  	parts := make([]int, 2)
 589  	n, err := parseRange(rangeStr, parts)
 590  	if err != nil || n != 2 {
 591  		return kinds
 592  	}
 593  	for i := parts[0]; i <= parts[1]; i++ {
 594  		kinds = append(kinds, i)
 595  	}
 596  	return kinds
 597  }
 598  
 599  func parseRange(s string, parts []int) (int, error) {
 600  	// Simple parsing of "start-end"
 601  	for i, c := range s {
 602  		if c == '-' && i > 0 {
 603  			start, err := strconv.Atoi(s[:i])
 604  			if err != nil {
 605  				return 0, err
 606  			}
 607  			end, err := strconv.Atoi(s[i+1:])
 608  			if err != nil {
 609  				return 0, err
 610  			}
 611  			parts[0] = start
 612  			parts[1] = end
 613  			return 2, nil
 614  		}
 615  	}
 616  	return 0, nil
 617  }
 618  
 619  // handleScanPubkeys scans the database for all pubkeys and populates event counts
 620  // This is used to retroactively populate the unclassified users list
 621  func (s *Server) handleScanPubkeys(dbACL *database.CuratingACL) NIP86Response {
 622  	result, err := dbACL.ScanAllPubkeys()
 623  	if chk.E(err) {
 624  		return NIP86Response{Error: "Failed to scan pubkeys: " + err.Error()}
 625  	}
 626  
 627  	return NIP86Response{Result: map[string]interface{}{
 628  		"total_pubkeys": result.TotalPubkeys,
 629  		"total_events":  result.TotalEvents,
 630  		"skipped":       result.Skipped,
 631  	}}
 632  }
 633  
 634  // handleGetEventsForPubkey returns events for a specific pubkey
 635  // Params: [pubkey, limit (optional, default 100), offset (optional, default 0)]
 636  func (s *Server) handleGetEventsForPubkey(params []interface{}, dbACL *database.CuratingACL) NIP86Response {
 637  	if len(params) < 1 {
 638  		return NIP86Response{Error: "Missing required parameter: pubkey"}
 639  	}
 640  
 641  	pubkey, ok := params[0].(string)
 642  	if !ok {
 643  		return NIP86Response{Error: "Invalid pubkey parameter"}
 644  	}
 645  
 646  	if len(pubkey) != 64 {
 647  		return NIP86Response{Error: "Invalid pubkey format (must be 64 hex characters)"}
 648  	}
 649  
 650  	// Parse optional limit (default 100)
 651  	limit := 100
 652  	if len(params) > 1 {
 653  		if l, ok := params[1].(float64); ok {
 654  			limit = int(l)
 655  			if limit > 500 {
 656  				limit = 500 // Cap at 500
 657  			}
 658  			if limit < 1 {
 659  				limit = 1
 660  			}
 661  		}
 662  	}
 663  
 664  	// Parse optional offset (default 0)
 665  	offset := 0
 666  	if len(params) > 2 {
 667  		if o, ok := params[2].(float64); ok {
 668  			offset = int(o)
 669  			if offset < 0 {
 670  				offset = 0
 671  			}
 672  		}
 673  	}
 674  
 675  	events, total, err := dbACL.GetEventsForPubkey(pubkey, limit, offset)
 676  	if chk.E(err) {
 677  		return NIP86Response{Error: "Failed to get events: " + err.Error()}
 678  	}
 679  
 680  	// Convert to response format
 681  	eventList := make([]map[string]interface{}, len(events))
 682  	for i, ev := range events {
 683  		eventList[i] = map[string]interface{}{
 684  			"id":         ev.ID,
 685  			"kind":       ev.Kind,
 686  			"content":    ev.Content,
 687  			"created_at": ev.CreatedAt,
 688  		}
 689  	}
 690  
 691  	return NIP86Response{Result: map[string]interface{}{
 692  		"events": eventList,
 693  		"total":  total,
 694  		"limit":  limit,
 695  		"offset": offset,
 696  	}}
 697  }
 698  
 699  // handleDeleteEventsForPubkey deletes all events for a specific pubkey
 700  // This is only allowed for blacklisted pubkeys as a safety measure
 701  // Params: [pubkey]
 702  func (s *Server) handleDeleteEventsForPubkey(params []interface{}, dbACL *database.CuratingACL) NIP86Response {
 703  	if len(params) < 1 {
 704  		return NIP86Response{Error: "Missing required parameter: pubkey"}
 705  	}
 706  
 707  	pubkey, ok := params[0].(string)
 708  	if !ok {
 709  		return NIP86Response{Error: "Invalid pubkey parameter"}
 710  	}
 711  
 712  	if len(pubkey) != 64 {
 713  		return NIP86Response{Error: "Invalid pubkey format (must be 64 hex characters)"}
 714  	}
 715  
 716  	// Safety check: only allow deletion of events from blacklisted users
 717  	isBlacklisted, err := dbACL.IsPubkeyBlacklisted(pubkey)
 718  	if chk.E(err) {
 719  		return NIP86Response{Error: "Failed to check blacklist status: " + err.Error()}
 720  	}
 721  
 722  	if !isBlacklisted {
 723  		return NIP86Response{Error: "Can only delete events from blacklisted users. Blacklist the user first."}
 724  	}
 725  
 726  	// Delete all events for this pubkey
 727  	deleted, err := dbACL.DeleteEventsForPubkey(pubkey)
 728  	if chk.E(err) {
 729  		return NIP86Response{Error: "Failed to delete events: " + err.Error()}
 730  	}
 731  
 732  	return NIP86Response{Result: map[string]interface{}{
 733  		"deleted": deleted,
 734  		"pubkey":  pubkey,
 735  	}}
 736  }
 737