Coffee journaling on ATProto (alpha) alpha.arabica.social
coffee
17
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: export witness cache admin action #18

open opened by pdewey.com targeting main from push-qtmrwnrptzrz
Labels

None yet.

assignee

None yet.

Participants 1
AT URI
at://did:plc:hm5f3dnm6jdhrc55qp2npdja/sh.tangled.repo.pull/3ml7z32myjc22
+129
Diff #0
+84
internal/handlers/admin.go
··· 2 2 3 3 import ( 4 4 "context" 5 + "encoding/json" 6 + "fmt" 5 7 "net/http" 8 + "strings" 6 9 "time" 7 10 8 11 "tangled.org/arabica.social/arabica/internal/atproto" 9 12 "tangled.org/arabica.social/arabica/internal/database/boltstore" 13 + "tangled.org/arabica.social/arabica/internal/firehose" 10 14 "tangled.org/arabica.social/arabica/internal/metrics" 11 15 "tangled.org/arabica.social/arabica/internal/middleware" 12 16 "tangled.org/arabica.social/arabica/internal/moderation" ··· 707 711 http.Error(w, "Failed to render", http.StatusInternalServerError) 708 712 } 709 713 } 714 + 715 + // exportedRecord is the per-record shape in the witness export payload. 716 + type exportedRecord struct { 717 + URI string `json:"uri"` 718 + RKey string `json:"rkey"` 719 + CID string `json:"cid"` 720 + CreatedAt time.Time `json:"createdAt"` 721 + IndexedAt time.Time `json:"indexedAt"` 722 + Record json.RawMessage `json:"record"` 723 + } 724 + 725 + // witnessExport is the top-level payload returned by HandleAdminExportDID. 726 + type witnessExport struct { 727 + DID string `json:"did"` 728 + ExportedAt time.Time `json:"exportedAt"` 729 + Source string `json:"source"` 730 + Collections map[string][]exportedRecord `json:"collections"` 731 + } 732 + 733 + // HandleAdminExportDID exports every witness-cached record for a given DID as 734 + // a single JSON document. Records come from the firehose-backed SQLite index, 735 + // not the user's PDS. Auth and admin checks are handled by RequireAdmin. 736 + func (h *Handler) HandleAdminExportDID(w http.ResponseWriter, r *http.Request) { 737 + rawDID := strings.TrimSpace(r.URL.Query().Get("did")) 738 + if rawDID == "" { 739 + http.Error(w, "missing 'did' query parameter", http.StatusBadRequest) 740 + return 741 + } 742 + did, err := syntax.ParseDID(rawDID) 743 + if err != nil { 744 + http.Error(w, fmt.Sprintf("invalid DID: %v", err), http.StatusBadRequest) 745 + return 746 + } 747 + if h.witnessCache == nil { 748 + http.Error(w, "witness cache not configured", http.StatusServiceUnavailable) 749 + return 750 + } 751 + 752 + didStr := did.String() 753 + out := witnessExport{ 754 + DID: didStr, 755 + ExportedAt: time.Now().UTC(), 756 + Source: "witness-cache", 757 + Collections: make(map[string][]exportedRecord, len(firehose.ArabicaCollections)), 758 + } 759 + 760 + for _, collection := range firehose.ArabicaCollections { 761 + records, err := h.witnessCache.ListWitnessRecords(r.Context(), didStr, collection) 762 + if err != nil { 763 + log.Error().Err(err).Str("did", didStr).Str("collection", collection).Msg("witness export: list failed") 764 + http.Error(w, "failed to read witness cache", http.StatusInternalServerError) 765 + return 766 + } 767 + exported := make([]exportedRecord, 0, len(records)) 768 + for _, rec := range records { 769 + exported = append(exported, exportedRecord{ 770 + URI: rec.URI, 771 + RKey: rec.RKey, 772 + CID: rec.CID, 773 + CreatedAt: rec.CreatedAt, 774 + IndexedAt: rec.IndexedAt, 775 + Record: rec.Record, 776 + }) 777 + } 778 + out.Collections[collection] = exported 779 + } 780 + 781 + filename := fmt.Sprintf("arabica-witness-%s-%s.json", 782 + strings.ReplaceAll(didStr, ":", "_"), 783 + out.ExportedAt.Format("20060102-150405")) 784 + 785 + w.Header().Set("Content-Type", "application/json") 786 + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) 787 + 788 + enc := json.NewEncoder(w) 789 + enc.SetIndent("", " ") 790 + if err := enc.Encode(out); err != nil { 791 + log.Error().Err(err).Str("did", didStr).Msg("witness export: encode failed") 792 + } 793 + }
+2
internal/routing/routing.go
··· 196 196 middleware.RequireAdmin(modSvc, http.HandlerFunc(h.HandleDismissJoinRequest)))) 197 197 mux.Handle("GET /_mod/stats", middleware.RequireAdmin(modSvc, 198 198 middleware.RequireHTMXMiddleware(http.HandlerFunc(h.HandleAdminStats)))) 199 + mux.Handle("GET /_mod/export", middleware.RequireAdmin(modSvc, 200 + http.HandlerFunc(h.HandleAdminExportDID))) 199 201 200 202 // Static files (must come after specific routes) 201 203 fs := http.FileServer(http.Dir("static"))
+43
internal/web/pages/admin.templ
··· 171 171 > 172 172 Stats 173 173 </button> 174 + <button 175 + type="button" 176 + @click="activeTab = 'export'" 177 + :class="activeTab === 'export' ? 'bg-brown-300 text-brown-900 border-brown-400' : 'bg-brown-100 text-brown-600 border-brown-200 hover:bg-brown-200'" 178 + class="px-3 py-1.5 rounded-lg border font-medium text-sm transition-colors" 179 + > 180 + Export 181 + </button> 174 182 } 175 183 </nav> 176 184 <!-- Hidden Records Tab --> ··· 370 378 </div> 371 379 </div> 372 380 } 381 + <!-- Export Tab (admin only) --> 382 + if props.IsAdmin { 383 + <div x-show="activeTab === 'export'" x-cloak x-transition:enter="transition ease-out duration-200" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"> 384 + <div class="card card-inner"> 385 + <h2 class="section-title">Export Witness Cache</h2> 386 + <p class="text-sm text-brown-600 mb-4"> 387 + Download every witness-cached record for a DID as a single JSON file. Reads 388 + from the local firehose index โ€” does not contact the user's PDS. 389 + </p> 390 + <form 391 + action="/_mod/export" 392 + method="get" 393 + class="flex flex-col gap-3 sm:flex-row sm:items-end" 394 + > 395 + <div class="flex-1"> 396 + <label for="export-did" class="block text-sm font-medium text-brown-700 mb-1">DID</label> 397 + <input 398 + id="export-did" 399 + type="text" 400 + name="did" 401 + required 402 + placeholder="did:plc:..." 403 + class="w-full px-3 py-2 border border-brown-300 rounded-lg bg-white text-brown-900 text-sm font-mono focus:ring-2 focus:ring-amber-500 focus:border-amber-500" 404 + /> 405 + </div> 406 + <button 407 + type="submit" 408 + class="text-sm bg-brown-300 text-brown-900 hover:bg-brown-400 px-4 py-2 rounded font-medium transition-colors" 409 + > 410 + Export JSON 411 + </button> 412 + </form> 413 + </div> 414 + </div> 415 + } 373 416 </div> 374 417 } 375 418

History

1 round 0 comments
sign up or login to add to the discussion
pdewey.com submitted #0
1 commit
expand
feat: export witness cache admin action
merge conflicts detected
expand
  • internal/firehose/consumer.go:27
  • internal/firehose/index.go:459
  • internal/firehose/index_test.go:609
  • internal/firehose/profile_watcher.go:68
expand 0 comments