187 lines
5.1 KiB
Go
187 lines
5.1 KiB
Go
/*
|
|
Copyright © Brian Buller
|
|
|
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
of this software and associated documentation files (the "Software"), to deal
|
|
in the Software without restriction, including without limitation the rights
|
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
copies of the Software, and to permit persons to whom the Software is
|
|
furnished to do so, subject to the following conditions:
|
|
|
|
The above copyright notice and this permission notice shall be included in
|
|
all copies or substantial portions of the Software.
|
|
|
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
THE SOFTWARE.
|
|
*/
|
|
package models
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"time"
|
|
|
|
"git.bullercodeworks.com/brian/expds/helpers"
|
|
comatproto "github.com/bluesky-social/indigo/api/atproto"
|
|
"github.com/bluesky-social/indigo/atproto/atdata"
|
|
"github.com/bluesky-social/indigo/atproto/identity"
|
|
"github.com/bluesky-social/indigo/atproto/repo"
|
|
"github.com/bluesky-social/indigo/atproto/syntax"
|
|
"github.com/bluesky-social/indigo/xrpc"
|
|
"github.com/ipfs/go-cid"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
type EntryType int
|
|
|
|
const (
|
|
TypeNSID = EntryType(iota)
|
|
TypeRecord
|
|
)
|
|
|
|
type Pds struct {
|
|
AtId syntax.AtIdentifier
|
|
Did syntax.DID
|
|
localPath string
|
|
|
|
Commit string
|
|
NSIDs []syntax.NSID
|
|
RecordIds []string
|
|
nsidToRecordIds map[syntax.NSID][]string
|
|
recordIdsToNSID map[string]syntax.NSID
|
|
Records map[string]map[string]any
|
|
|
|
RefreshTime time.Time
|
|
}
|
|
|
|
func NewPdsFromDid(id string) (*Pds, error) {
|
|
ctx := context.Background()
|
|
atid, err := syntax.ParseAtIdentifier(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// first look up the DID and PDS for this repo
|
|
dir := identity.DefaultDirectory()
|
|
ident, err := dir.Lookup(ctx, atid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create a new API client to connect to the account's PDS
|
|
xrpcc := xrpc.Client{
|
|
Host: ident.PDSEndpoint(),
|
|
}
|
|
if xrpcc.Host == "" {
|
|
return nil, fmt.Errorf("no PDS endpoint for identity")
|
|
}
|
|
|
|
carPath := helpers.Path(viper.GetString("data"), ident.DID.String()+".car")
|
|
repoBytes, err := comatproto.SyncGetRepo(ctx, &xrpcc, ident.DID.String(), "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := os.WriteFile(carPath, repoBytes, 0666); err != nil {
|
|
return nil, err
|
|
}
|
|
ret := &Pds{
|
|
AtId: atid,
|
|
Did: ident.DID,
|
|
localPath: carPath,
|
|
nsidToRecordIds: make(map[syntax.NSID][]string),
|
|
recordIdsToNSID: make(map[string]syntax.NSID),
|
|
Records: make(map[string]map[string]any),
|
|
RefreshTime: time.Now(),
|
|
}
|
|
return ret, ret.unpack()
|
|
}
|
|
|
|
func (p *Pds) unpack() error {
|
|
ctx := context.Background()
|
|
fi, err := os.Open(p.localPath)
|
|
if err != nil {
|
|
return fmt.Errorf("error opening local car: %w", err)
|
|
}
|
|
|
|
c, r, err := repo.LoadRepoFromCAR(ctx, fi)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading repo from car: %w", err)
|
|
}
|
|
|
|
comBytes, err := json.Marshal(c)
|
|
if err != nil {
|
|
return fmt.Errorf("error building commit meta json: %w")
|
|
}
|
|
p.Commit = string(comBytes)
|
|
|
|
err = r.MST.Walk(func(k []byte, v cid.Cid) error {
|
|
col, rkey, err := syntax.ParseRepoPath(string(k))
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing repo path (%s): %w", string(k), err)
|
|
}
|
|
|
|
sCol, sRKey := string(col), string(rkey)
|
|
recBytes, _, err := r.GetRecordBytes(ctx, col, rkey)
|
|
if err != nil {
|
|
return fmt.Errorf("error getting record bytes (%s/%s): %w", sCol, sRKey, err)
|
|
}
|
|
|
|
rec, err := atdata.UnmarshalCBOR(recBytes)
|
|
if err != nil {
|
|
return fmt.Errorf("error unmarshalling cbor (%s/%s): %w", sCol, sRKey, err)
|
|
}
|
|
p.Records[string(k)] = rec
|
|
|
|
if !slices.Contains(p.NSIDs, col) {
|
|
p.NSIDs = append(p.NSIDs, col)
|
|
}
|
|
p.nsidToRecordIds[col] = append(p.nsidToRecordIds[col], sRKey)
|
|
p.recordIdsToNSID[sRKey] = col
|
|
return nil
|
|
})
|
|
//slices.Sort(p.NSIDs)
|
|
return err
|
|
}
|
|
|
|
func (p *Pds) NSIDStringList() []string {
|
|
var ret []string
|
|
for _, wrk := range p.NSIDs {
|
|
ret = append(ret, wrk.String())
|
|
}
|
|
return ret
|
|
}
|
|
|
|
func (p *Pds) List() (map[string]string, error) {
|
|
ret := make(map[string]string)
|
|
ctx := context.Background()
|
|
fi, err := os.Open(p.localPath)
|
|
if err != nil {
|
|
return ret, fmt.Errorf("error opening local car: %w", err)
|
|
}
|
|
// read repository tree in to memory
|
|
_, r, err := repo.LoadRepoFromCAR(ctx, fi)
|
|
if err != nil {
|
|
return ret, fmt.Errorf("error loading repo from car: %w", err)
|
|
}
|
|
|
|
err = r.MST.Walk(func(k []byte, v cid.Cid) error {
|
|
ret[string(k)] = v.String()
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return ret, fmt.Errorf("error walking repo: %w", err)
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
func (p *Pds) GetRecordIdsFor(n syntax.NSID) []string { return p.nsidToRecordIds[n] }
|
|
func (p *Pds) GetNSIDForRecordId(rId string) syntax.NSID { return p.recordIdsToNSID[rId] }
|