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