this repo has no description
13
fork

Configure Feed

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

status checks

+249
+1
cmd/glot/main.go
··· 38 38 app.Commands = []*cli.Command{ 39 39 cmdLint, 40 40 cmdPull, 41 + cmdStatus, 41 42 } 42 43 return app.Run(context.Background(), args) 43 44 }
+248
cmd/glot/status.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + "io/fs" 8 + "log/slog" 9 + "os" 10 + "path" 11 + "path/filepath" 12 + "reflect" 13 + "sort" 14 + "strings" 15 + 16 + "github.com/bluesky-social/indigo/api/agnostic" 17 + "github.com/bluesky-social/indigo/atproto/client" 18 + "github.com/bluesky-social/indigo/atproto/data" 19 + "github.com/bluesky-social/indigo/atproto/identity" 20 + "github.com/bluesky-social/indigo/atproto/lexicon" 21 + "github.com/bluesky-social/indigo/atproto/syntax" 22 + 23 + "github.com/urfave/cli/v3" 24 + ) 25 + 26 + var cmdStatus = &cli.Command{ 27 + Name: "status", 28 + Usage: "check if local lexicons are in-sync with live network", 29 + ArgsUsage: `<file-or-dir>*`, 30 + Flags: []cli.Flag{ 31 + &cli.StringFlag{ 32 + Name: "lexicons-dir", 33 + Value: "./lexicons/", 34 + Usage: "base directory for project Lexicon files", 35 + Sources: cli.EnvVars("LEXICONS_DIR"), 36 + }, 37 + }, 38 + Action: runStatus, 39 + } 40 + 41 + func loadSchemaPath(fpath string) (syntax.NSID, *json.RawMessage, error) { 42 + b, err := os.ReadFile(fpath) 43 + if err != nil { 44 + return "", nil, err 45 + } 46 + 47 + // parse file to check for errors 48 + // TODO: use json/v2 when available for case-sensitivity 49 + var sf lexicon.SchemaFile 50 + err = json.Unmarshal(b, &sf) 51 + if err == nil { 52 + err = sf.FinishParse() 53 + } 54 + if err == nil { 55 + err = sf.CheckSchema() 56 + } 57 + if err != nil { 58 + return "", nil, err 59 + } 60 + 61 + var rec json.RawMessage 62 + if err := json.Unmarshal(b, &rec); err != nil { 63 + return "", nil, err 64 + } 65 + return syntax.NSID(sf.ID), &rec, nil 66 + } 67 + 68 + func runStatus(ctx context.Context, cmd *cli.Command) error { 69 + return compareSchemas(ctx, cmd, statusCompare) 70 + } 71 + 72 + func statusCompare(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error { 73 + 74 + // new remote schema (missing local) 75 + if localJSON == nil { 76 + fmt.Printf(" ⭕ %s\n", nsid) 77 + return nil 78 + } 79 + 80 + // new unpublished local schema 81 + if remoteJSON == nil { 82 + fmt.Printf(" 🟠 %s\n", nsid) 83 + return nil 84 + } 85 + 86 + local, err := data.UnmarshalJSON(localJSON) 87 + if err != nil { 88 + return err 89 + } 90 + remote, err := data.UnmarshalJSON(remoteJSON) 91 + if err != nil { 92 + return err 93 + } 94 + delete(local, "$type") 95 + delete(remote, "$type") 96 + if reflect.DeepEqual(local, remote) { 97 + fmt.Printf(" 🟢 %s\n", nsid) 98 + } else { 99 + fmt.Printf(" 🟣 %s\n", nsid) 100 + } 101 + return nil 102 + } 103 + 104 + func compareSchemas(ctx context.Context, cmd *cli.Command, comp func(ctx context.Context, cmd *cli.Command, nsid syntax.NSID, localJSON, remoteJSON json.RawMessage) error) error { 105 + paths := cmd.Args().Slice() 106 + if !cmd.Args().Present() { 107 + paths = []string{cmd.String("lexicons-dir")} 108 + _, err := os.Stat(paths[0]) 109 + if err != nil { 110 + return fmt.Errorf("no path arguments specified and default lexicon directory not found\n%w", err) 111 + } 112 + } 113 + 114 + // collect all NSID/path mappings 115 + localSchemas := map[syntax.NSID]json.RawMessage{} 116 + remoteSchemas := map[syntax.NSID]json.RawMessage{} 117 + 118 + for _, p := range paths { 119 + finfo, err := os.Stat(p) 120 + if err != nil { 121 + return fmt.Errorf("failed loading %s: %w", p, err) 122 + } 123 + if finfo.IsDir() { 124 + if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error { 125 + if d.IsDir() || path.Ext(fp) != ".json" { 126 + return nil 127 + } 128 + nsid, rec, err := loadSchemaPath(fp) 129 + if err != nil { 130 + return err 131 + } 132 + localSchemas[nsid] = *rec 133 + return nil 134 + }); err != nil { 135 + return err 136 + } 137 + continue 138 + } 139 + nsid, rec, err := loadSchemaPath(p) 140 + if err != nil { 141 + return err 142 + } 143 + localSchemas[nsid] = *rec 144 + return nil 145 + } 146 + 147 + localGroups := map[string]bool{} 148 + allNSIDMap := map[syntax.NSID]bool{} 149 + for k := range localSchemas { 150 + parts := strings.Split(string(k), ".") 151 + g := strings.Join(parts[0:len(parts)-1], ".") + "." 152 + localGroups[g] = true 153 + allNSIDMap[k] = true 154 + } 155 + 156 + for g := range localGroups { 157 + if err := fetchLexiconGroup(ctx, cmd, g, &remoteSchemas); err != nil { 158 + return err 159 + } 160 + } 161 + 162 + for k := range remoteSchemas { 163 + allNSIDMap[k] = true 164 + } 165 + allNSID := []string{} 166 + for k := range allNSIDMap { 167 + allNSID = append(allNSID, string(k)) 168 + } 169 + sort.Strings(allNSID) 170 + 171 + for _, k := range allNSID { 172 + nsid := syntax.NSID(k) 173 + if err := comp(ctx, cmd, nsid, localSchemas[nsid], remoteSchemas[nsid]); err != nil { 174 + return err 175 + } 176 + } 177 + 178 + return nil 179 + } 180 + 181 + func fetchLexiconGroup(ctx context.Context, cmd *cli.Command, group string, remote *map[syntax.NSID]json.RawMessage) error { 182 + 183 + slog.Debug("trying to load NSID group", "group", group) 184 + 185 + // TODO: netclient support for listing records 186 + dir := identity.BaseDirectory{} 187 + did, err := dir.ResolveNSID(ctx, syntax.NSID(group+"name")) 188 + if err != nil { 189 + // if NSID isn't registered, just skip comparison 190 + slog.Warn("skipping NSID pattern which did not resolve", "group", group) 191 + return nil 192 + } 193 + ident, err := dir.LookupDID(ctx, did) 194 + if err != nil { 195 + return err 196 + } 197 + c := client.NewAPIClient(ident.PDSEndpoint()) 198 + 199 + cursor := "" 200 + for { 201 + // collection string, cursor string, limit int64, repo string, reverse bool 202 + resp, err := agnostic.RepoListRecords(ctx, c, "com.atproto.lexicon.schema", cursor, 100, ident.DID.String(), false) 203 + if err != nil { 204 + return err 205 + } 206 + for _, rec := range resp.Records { 207 + aturi, err := syntax.ParseATURI(rec.Uri) 208 + if err != nil { 209 + return err 210 + } 211 + nsid, err := syntax.ParseNSID(aturi.RecordKey().String()) 212 + if err != nil { 213 + slog.Warn("ignoring invalid schema NSID", "did", ident.DID, "rkey", aturi.RecordKey()) 214 + continue 215 + } 216 + if !strings.HasPrefix(nsid.String(), group) { 217 + // ignoring other NSIDs 218 + continue 219 + } 220 + if rec.Value == nil { 221 + return fmt.Errorf("missing record value: %s", nsid) 222 + } 223 + 224 + // parse file to check for errors 225 + // TODO: use json/v2 when available for case-sensitivity 226 + var sf lexicon.SchemaFile 227 + err = json.Unmarshal(*rec.Value, &sf) 228 + if err == nil { 229 + err = sf.FinishParse() 230 + } 231 + if err == nil { 232 + err = sf.CheckSchema() 233 + } 234 + if err != nil { 235 + return fmt.Errorf("invalid lexicon schema record (%s): %w", nsid, err) 236 + } 237 + 238 + (*remote)[nsid] = *rec.Value 239 + 240 + } 241 + if resp.Cursor != nil && *resp.Cursor != "" { 242 + cursor = *resp.Cursor 243 + } else { 244 + break 245 + } 246 + } 247 + return nil 248 + }