tangled
alpha
login
or
join now
bnewbold.net
/
cobalt
13
fork
atom
go scratch code for atproto
13
fork
atom
overview
issues
pulls
pipelines
unpublish command, and shuffling functions
bnewbold.net
5 months ago
d42c673e
2fab0ca4
+188
-62
6 changed files
expand all
collapse all
unified
split
cmd
glot
main.go
publish.go
pull.go
status.go
unpublish.go
util.go
+1
cmd/glot/main.go
···
36
cmdCompat,
37
cmdNew,
38
cmdPublish,
0
39
}
40
return app.Run(context.Background(), args)
41
}
···
36
cmdCompat,
37
cmdNew,
38
cmdPublish,
39
+
cmdUnpublish,
40
}
41
return app.Run(context.Background(), args)
42
}
+45
-33
cmd/glot/publish.go
···
10
"path/filepath"
11
"reflect"
12
"sort"
13
-
"strings"
14
15
"github.com/bluesky-social/indigo/api/agnostic"
16
"github.com/bluesky-social/indigo/atproto/client"
···
20
21
"github.com/urfave/cli/v3"
22
)
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
33
var cmdPublish = &cli.Command{
34
Name: "publish",
···
53
Usage: "account password (app password) for login",
54
Sources: cli.EnvVars("GLOT_PASSWORD", "ATP_PASSWORD", "PASSWORD"),
55
},
0
0
0
0
0
56
},
57
Action: runPublish,
58
}
59
0
0
0
0
0
0
0
0
60
func runPublish(ctx context.Context, cmd *cli.Command) error {
61
62
user := cmd.String("username")
63
pass := cmd.String("password")
64
-
65
if user == "" || pass == "" {
66
return fmt.Errorf("requires account credentials")
67
}
···
69
if err != nil {
70
return fmt.Errorf("invalid AT account identifier %s: %w", user, err)
71
}
0
72
cdir := identity.DefaultDirectory()
73
// TODO: could defer actual login until later?
74
c, err := client.LoginWithPassword(ctx, cdir, *atid, pass, "", nil)
75
if err != nil {
76
return nil
0
0
0
77
}
78
79
paths := cmd.Args().Slice()
···
120
localGroups := map[string]bool{}
121
allNSIDMap := map[syntax.NSID]bool{}
122
for k := range localSchemas {
123
-
parts := strings.Split(string(k), ".")
124
-
g := strings.Join(parts[0:len(parts)-1], ".") + "."
125
localGroups[g] = true
126
allNSIDMap[k] = true
127
}
···
161
continue
162
}
163
164
-
if remoteJSON == nil {
165
-
if err := publishSchema(ctx, c, nsid, localJSON); err != nil {
0
0
166
return err
167
}
168
-
continue
0
0
0
0
0
0
0
0
169
}
170
171
-
local, err := data.UnmarshalJSON(localJSON)
172
-
if err != nil {
173
-
return err
0
0
0
0
174
}
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
-
if err := publishSchema(ctx, c, nsid, localJSON); err != nil {
183
-
return err
184
-
}
185
-
continue
186
}
187
}
188
···
197
}
198
d["$type"] = schemaNSID
199
200
-
if c.AccountDID == nil {
201
-
return fmt.Errorf("require API client to have DID configured")
202
-
}
203
_, err = agnostic.RepoPutRecord(ctx, c, &agnostic.RepoPutRecord_Input{
204
-
Collection: "com.atproto.lexicon.schema",
205
Repo: c.AccountDID.String(),
206
Record: d,
207
Rkey: nsid.String(),
···
209
if err != nil {
210
return err
211
}
212
-
fmt.Printf(" 🟣 %s\n", nsid)
213
214
return nil
215
}
···
10
"path/filepath"
11
"reflect"
12
"sort"
0
13
14
"github.com/bluesky-social/indigo/api/agnostic"
15
"github.com/bluesky-social/indigo/atproto/client"
···
19
20
"github.com/urfave/cli/v3"
21
)
0
0
0
0
0
0
0
0
0
22
23
var cmdPublish = &cli.Command{
24
Name: "publish",
···
43
Usage: "account password (app password) for login",
44
Sources: cli.EnvVars("GLOT_PASSWORD", "ATP_PASSWORD", "PASSWORD"),
45
},
46
+
&cli.BoolFlag{
47
+
Name: "force",
48
+
Aliases: []string{"f"},
49
+
Usage: "skip NSID DNS resolution match requirement",
50
+
},
51
},
52
Action: runPublish,
53
}
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
+
*/
63
func runPublish(ctx context.Context, cmd *cli.Command) error {
64
65
user := cmd.String("username")
66
pass := cmd.String("password")
0
67
if user == "" || pass == "" {
68
return fmt.Errorf("requires account credentials")
69
}
···
71
if err != nil {
72
return fmt.Errorf("invalid AT account identifier %s: %w", user, err)
73
}
74
+
75
cdir := identity.DefaultDirectory()
76
// TODO: could defer actual login until later?
77
c, err := client.LoginWithPassword(ctx, cdir, *atid, pass, "", nil)
78
if err != nil {
79
return nil
80
+
}
81
+
if c.AccountDID == nil {
82
+
return fmt.Errorf("require API client to have DID configured")
83
}
84
85
paths := cmd.Args().Slice()
···
126
localGroups := map[string]bool{}
127
allNSIDMap := map[syntax.NSID]bool{}
128
for k := range localSchemas {
129
+
g := nsidGroup(k)
0
130
localGroups[g] = true
131
allNSIDMap[k] = true
132
}
···
166
continue
167
}
168
169
+
// skip if no change
170
+
if remoteJSON != nil {
171
+
local, err := data.UnmarshalJSON(localJSON)
172
+
if err != nil {
173
return err
174
}
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
+
}
184
}
185
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
+
}
193
}
194
+
195
+
if err := publishSchema(ctx, c, nsid, localJSON); err != nil {
196
return err
197
}
198
+
if remoteJSON == nil {
199
+
fmt.Printf(" 🟢 %s\n", nsid)
200
+
} else {
201
+
fmt.Printf(" 🟣 %s\n", nsid)
0
0
0
202
}
203
}
204
···
213
}
214
d["$type"] = schemaNSID
215
0
0
0
216
_, err = agnostic.RepoPutRecord(ctx, c, &agnostic.RepoPutRecord_Input{
217
+
Collection: schemaNSID.String(),
218
Repo: c.AccountDID.String(),
219
Record: d,
220
Rkey: nsid.String(),
···
222
if err != nil {
223
return err
224
}
0
225
226
return nil
227
}
+1
-26
cmd/glot/pull.go
···
19
"github.com/urfave/cli/v3"
20
)
21
22
-
var (
23
-
schemaNSID = syntax.NSID("com.atproto.lexicon.schema")
24
-
)
25
-
26
var cmdPull = &cli.Command{
27
Name: "pull",
28
Usage: "fetch (or update) lexicon schemas to local directory",
···
44
Action: runPull,
45
}
46
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
func runPull(ctx context.Context, cmd *cli.Command) error {
63
if !cmd.Args().Present() {
64
return fmt.Errorf("no NSID patterns specified")
···
83
}
84
}
85
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
}
93
94
func pullLexicon(ctx context.Context, cmd *cli.Command, nsid syntax.NSID) error {
···
166
cursor := ""
167
for {
168
// 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)
170
if err != nil {
171
return err
172
}
···
19
"github.com/urfave/cli/v3"
20
)
21
0
0
0
0
22
var cmdPull = &cli.Command{
23
Name: "pull",
24
Usage: "fetch (or update) lexicon schemas to local directory",
···
40
Action: runPull,
41
}
42
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
43
func runPull(ctx context.Context, cmd *cli.Command) error {
44
if !cmd.Args().Present() {
45
return fmt.Errorf("no NSID patterns specified")
···
64
}
65
}
66
return nil
0
0
0
0
0
0
67
}
68
69
func pullLexicon(ctx context.Context, cmd *cli.Command, nsid syntax.NSID) error {
···
141
cursor := ""
142
for {
143
// collection string, cursor string, limit int64, repo string, reverse bool
144
+
resp, err := agnostic.RepoListRecords(ctx, c, schemaNSID.String(), cursor, 100, ident.DID.String(), false)
145
if err != nil {
146
return err
147
}
+2
-3
cmd/glot/status.go
···
146
localGroups := map[string]bool{}
147
allNSIDMap := map[syntax.NSID]bool{}
148
for k := range localSchemas {
149
-
parts := strings.Split(string(k), ".")
150
-
g := strings.Join(parts[0:len(parts)-1], ".") + "."
151
localGroups[g] = true
152
allNSIDMap[k] = true
153
}
···
205
cursor := ""
206
for {
207
// 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)
209
if err != nil {
210
return err
211
}
···
146
localGroups := map[string]bool{}
147
allNSIDMap := map[syntax.NSID]bool{}
148
for k := range localSchemas {
149
+
g := nsidGroup(k)
0
150
localGroups[g] = true
151
allNSIDMap[k] = true
152
}
···
204
cursor := ""
205
for {
206
// collection string, cursor string, limit int64, repo string, reverse bool
207
+
resp, err := agnostic.RepoListRecords(ctx, c, schemaNSID.String(), cursor, 100, ident.DID.String(), false)
208
if err != nil {
209
return err
210
}
+97
cmd/glot/unpublish.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
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
+
}