go scratch code for atproto

unpublish command, and shuffling functions

+188 -62
+1
cmd/glot/main.go
··· 36 36 cmdCompat, 37 37 cmdNew, 38 38 cmdPublish, 39 + cmdUnpublish, 39 40 } 40 41 return app.Run(context.Background(), args) 41 42 }
+45 -33
cmd/glot/publish.go
··· 10 10 "path/filepath" 11 11 "reflect" 12 12 "sort" 13 - "strings" 14 13 15 14 "github.com/bluesky-social/indigo/api/agnostic" 16 15 "github.com/bluesky-social/indigo/atproto/client" ··· 20 19 21 20 "github.com/urfave/cli/v3" 22 21 ) 23 - 24 - /* 25 - publish behavior: 26 - - credentials are required 27 - - load all relevant schemas 28 - - filter no-change schemas 29 - - optionally filter schemas where group DNS is not current account (control w/ arg) 30 - - publish remaining schemas 31 - */ 32 22 33 23 var cmdPublish = &cli.Command{ 34 24 Name: "publish", ··· 53 43 Usage: "account password (app password) for login", 54 44 Sources: cli.EnvVars("GLOT_PASSWORD", "ATP_PASSWORD", "PASSWORD"), 55 45 }, 46 + &cli.BoolFlag{ 47 + Name: "force", 48 + Aliases: []string{"f"}, 49 + Usage: "skip NSID DNS resolution match requirement", 50 + }, 56 51 }, 57 52 Action: runPublish, 58 53 } 59 54 55 + /* 56 + publish behavior: 57 + - credentials are required 58 + - load all relevant schemas 59 + - filter no-change schemas 60 + - optionally filter schemas where group DNS is not current account (control w/ arg) 61 + - publish remaining schemas 62 + */ 60 63 func runPublish(ctx context.Context, cmd *cli.Command) error { 61 64 62 65 user := cmd.String("username") 63 66 pass := cmd.String("password") 64 - 65 67 if user == "" || pass == "" { 66 68 return fmt.Errorf("requires account credentials") 67 69 } ··· 69 71 if err != nil { 70 72 return fmt.Errorf("invalid AT account identifier %s: %w", user, err) 71 73 } 74 + 72 75 cdir := identity.DefaultDirectory() 73 76 // TODO: could defer actual login until later? 74 77 c, err := client.LoginWithPassword(ctx, cdir, *atid, pass, "", nil) 75 78 if err != nil { 76 79 return nil 80 + } 81 + if c.AccountDID == nil { 82 + return fmt.Errorf("require API client to have DID configured") 77 83 } 78 84 79 85 paths := cmd.Args().Slice() ··· 120 126 localGroups := map[string]bool{} 121 127 allNSIDMap := map[syntax.NSID]bool{} 122 128 for k := range localSchemas { 123 - parts := strings.Split(string(k), ".") 124 - g := strings.Join(parts[0:len(parts)-1], ".") + "." 129 + g := nsidGroup(k) 125 130 localGroups[g] = true 126 131 allNSIDMap[k] = true 127 132 } ··· 161 166 continue 162 167 } 163 168 164 - if remoteJSON == nil { 165 - if err := publishSchema(ctx, c, nsid, localJSON); err != nil { 169 + // skip if no change 170 + if remoteJSON != nil { 171 + local, err := data.UnmarshalJSON(localJSON) 172 + if err != nil { 166 173 return err 167 174 } 168 - continue 175 + remote, err := data.UnmarshalJSON(remoteJSON) 176 + if err != nil { 177 + return err 178 + } 179 + delete(local, "$type") 180 + delete(remote, "$type") 181 + if reflect.DeepEqual(local, remote) { 182 + continue 183 + } 169 184 } 170 185 171 - local, err := data.UnmarshalJSON(localJSON) 172 - if err != nil { 173 - return err 186 + if !cmd.Bool("force") { 187 + g := nsidGroup(nsid) 188 + did, ok := groupResolution[g] 189 + if !ok || did != *c.AccountDID { 190 + fmt.Printf(" ⭕ %s\n", nsid) 191 + continue 192 + } 174 193 } 175 - remote, err := data.UnmarshalJSON(remoteJSON) 176 - if err != nil { 194 + 195 + if err := publishSchema(ctx, c, nsid, localJSON); err != nil { 177 196 return err 178 197 } 179 - delete(local, "$type") 180 - delete(remote, "$type") 181 - if !reflect.DeepEqual(local, remote) { 182 - if err := publishSchema(ctx, c, nsid, localJSON); err != nil { 183 - return err 184 - } 185 - continue 198 + if remoteJSON == nil { 199 + fmt.Printf(" 🟢 %s\n", nsid) 200 + } else { 201 + fmt.Printf(" 🟣 %s\n", nsid) 186 202 } 187 203 } 188 204 ··· 197 213 } 198 214 d["$type"] = schemaNSID 199 215 200 - if c.AccountDID == nil { 201 - return fmt.Errorf("require API client to have DID configured") 202 - } 203 216 _, err = agnostic.RepoPutRecord(ctx, c, &agnostic.RepoPutRecord_Input{ 204 - Collection: "com.atproto.lexicon.schema", 217 + Collection: schemaNSID.String(), 205 218 Repo: c.AccountDID.String(), 206 219 Record: d, 207 220 Rkey: nsid.String(), ··· 209 222 if err != nil { 210 223 return err 211 224 } 212 - fmt.Printf(" 🟣 %s\n", nsid) 213 225 214 226 return nil 215 227 }
+1 -26
cmd/glot/pull.go
··· 19 19 "github.com/urfave/cli/v3" 20 20 ) 21 21 22 - var ( 23 - schemaNSID = syntax.NSID("com.atproto.lexicon.schema") 24 - ) 25 - 26 22 var cmdPull = &cli.Command{ 27 23 Name: "pull", 28 24 Usage: "fetch (or update) lexicon schemas to local directory", ··· 44 40 Action: runPull, 45 41 } 46 42 47 - // Checks if a string is a valid NSID group pattern, which is a partial NSID ending in '.' or '.*' 48 - func ParseNSIDGroup(raw string) (string, error) { 49 - if strings.HasSuffix(raw, ".*") { 50 - raw = raw[:len(raw)-1] 51 - } 52 - if !strings.HasSuffix(raw, ".") { 53 - return "", fmt.Errorf("not an NSID group pattern") 54 - } 55 - _, err := syntax.ParseNSID(raw + "name") 56 - if err != nil { 57 - return "", fmt.Errorf("not an NSID group pattern") 58 - } 59 - return raw, nil 60 - } 61 - 62 43 func runPull(ctx context.Context, cmd *cli.Command) error { 63 44 if !cmd.Args().Present() { 64 45 return fmt.Errorf("no NSID patterns specified") ··· 83 64 } 84 65 } 85 66 return nil 86 - } 87 - 88 - func pathForNSID(cmd *cli.Command, nsid syntax.NSID) string { 89 - base := cmd.String("lexicons-dir") 90 - sub := strings.ReplaceAll(nsid.String(), ".", "/") 91 - return path.Join(base, sub+".json") 92 67 } 93 68 94 69 func pullLexicon(ctx context.Context, cmd *cli.Command, nsid syntax.NSID) error { ··· 166 141 cursor := "" 167 142 for { 168 143 // collection string, cursor string, limit int64, repo string, reverse bool 169 - resp, err := agnostic.RepoListRecords(ctx, c, "com.atproto.lexicon.schema", cursor, 100, ident.DID.String(), false) 144 + resp, err := agnostic.RepoListRecords(ctx, c, schemaNSID.String(), cursor, 100, ident.DID.String(), false) 170 145 if err != nil { 171 146 return err 172 147 }
+2 -3
cmd/glot/status.go
··· 146 146 localGroups := map[string]bool{} 147 147 allNSIDMap := map[syntax.NSID]bool{} 148 148 for k := range localSchemas { 149 - parts := strings.Split(string(k), ".") 150 - g := strings.Join(parts[0:len(parts)-1], ".") + "." 149 + g := nsidGroup(k) 151 150 localGroups[g] = true 152 151 allNSIDMap[k] = true 153 152 } ··· 205 204 cursor := "" 206 205 for { 207 206 // collection string, cursor string, limit int64, repo string, reverse bool 208 - resp, err := agnostic.RepoListRecords(ctx, c, "com.atproto.lexicon.schema", cursor, 100, ident.DID.String(), false) 207 + resp, err := agnostic.RepoListRecords(ctx, c, schemaNSID.String(), cursor, 100, ident.DID.String(), false) 209 208 if err != nil { 210 209 return err 211 210 }
+97
cmd/glot/unpublish.go
··· 1 + package main 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "sort" 7 + 8 + comatproto "github.com/bluesky-social/indigo/api/atproto" 9 + "github.com/bluesky-social/indigo/atproto/client" 10 + "github.com/bluesky-social/indigo/atproto/identity" 11 + "github.com/bluesky-social/indigo/atproto/syntax" 12 + 13 + "github.com/urfave/cli/v3" 14 + ) 15 + 16 + var cmdUnpublish = &cli.Command{ 17 + Name: "unpublish", 18 + Usage: "delete lexicon schema records from current account", 19 + ArgsUsage: `<nsid>+`, 20 + Flags: []cli.Flag{ 21 + &cli.StringFlag{ 22 + Name: "username", 23 + Aliases: []string{"u"}, 24 + Usage: "account identifier (handle or DID) for login", 25 + Sources: cli.EnvVars("GLOT_USERNAME", "ATP_USERNAME"), 26 + }, 27 + &cli.StringFlag{ 28 + Name: "password", 29 + Aliases: []string{"p"}, 30 + Usage: "account password (app password) for login", 31 + Sources: cli.EnvVars("GLOT_PASSWORD", "ATP_PASSWORD", "PASSWORD"), 32 + }, 33 + }, 34 + Action: runUnpublish, 35 + } 36 + 37 + func runUnpublish(ctx context.Context, cmd *cli.Command) error { 38 + 39 + user := cmd.String("username") 40 + pass := cmd.String("password") 41 + if user == "" || pass == "" { 42 + return fmt.Errorf("requires account credentials") 43 + } 44 + atid, err := syntax.ParseAtIdentifier(user) 45 + if err != nil { 46 + return fmt.Errorf("invalid AT account identifier %s: %w", user, err) 47 + } 48 + 49 + cdir := identity.DefaultDirectory() 50 + // TODO: could defer actual login until later? 51 + c, err := client.LoginWithPassword(ctx, cdir, *atid, pass, "", nil) 52 + if err != nil { 53 + return nil 54 + } 55 + if c.AccountDID == nil { 56 + return fmt.Errorf("require API client to have DID configured") 57 + } 58 + 59 + nsids := []string{} 60 + for _, arg := range cmd.Args().Slice() { 61 + n, err := syntax.ParseNSID(arg) 62 + if err != nil { 63 + return err 64 + } 65 + nsids = append(nsids, n.String()) 66 + } 67 + sort.Strings(nsids) 68 + 69 + for _, nsid := range nsids { 70 + if err := unpublishSchema(ctx, c, syntax.NSID(nsid)); err != nil { 71 + fmt.Printf(" 🟠 %s\n", nsid) 72 + fmt.Printf(" record deletion failed: %s\n", err.Error()) 73 + continue 74 + } 75 + fmt.Printf(" 🟢 %s\n", nsid) 76 + } 77 + 78 + return nil 79 + } 80 + 81 + func unpublishSchema(ctx context.Context, c *client.APIClient, nsid syntax.NSID) error { 82 + 83 + resp, err := comatproto.RepoDeleteRecord(ctx, c, &comatproto.RepoDeleteRecord_Input{ 84 + Collection: schemaNSID.String(), 85 + Repo: c.AccountDID.String(), 86 + Rkey: nsid.String(), 87 + }) 88 + if err != nil { 89 + return err 90 + } 91 + 92 + if resp.Commit == nil { 93 + return fmt.Errorf("schema record did not exist") 94 + } 95 + 96 + return nil 97 + }
+42
cmd/glot/util.go
··· 1 + package main 2 + 3 + import ( 4 + "fmt" 5 + "path" 6 + "strings" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + 10 + "github.com/urfave/cli/v3" 11 + ) 12 + 13 + var ( 14 + schemaNSID = syntax.NSID("com.atproto.lexicon.schema") 15 + ) 16 + 17 + func nsidGroup(nsid syntax.NSID) string { 18 + parts := strings.Split(string(nsid), ".") 19 + g := strings.Join(parts[0:len(parts)-1], ".") + "." 20 + return g 21 + } 22 + 23 + // Checks if a string is a valid NSID group pattern, which is a partial NSID ending in '.' or '.*' 24 + func ParseNSIDGroup(raw string) (string, error) { 25 + if strings.HasSuffix(raw, ".*") { 26 + raw = raw[:len(raw)-1] 27 + } 28 + if !strings.HasSuffix(raw, ".") { 29 + return "", fmt.Errorf("not an NSID group pattern") 30 + } 31 + _, err := syntax.ParseNSID(raw + "name") 32 + if err != nil { 33 + return "", fmt.Errorf("not an NSID group pattern") 34 + } 35 + return raw, nil 36 + } 37 + 38 + func pathForNSID(cmd *cli.Command, nsid syntax.NSID) string { 39 + base := cmd.String("lexicons-dir") 40 + sub := strings.ReplaceAll(nsid.String(), ".", "/") 41 + return path.Join(base, sub+".json") 42 + }