+129
Diff
round #0
+84
internal/handlers/admin.go
+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
+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
+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
pdewey.com
submitted
#0
1 commit
expand
collapse
feat: export witness cache admin action
merge conflicts detected
expand
collapse
expand
collapse
- internal/firehose/consumer.go:27
- internal/firehose/index.go:459
- internal/firehose/index_test.go:609
- internal/firehose/profile_watcher.go:68