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