tangled
alpha
login
or
join now
bnewbold.net
/
cobalt
13
fork
atom
go scratch code for atproto
13
fork
atom
overview
issues
pulls
pipelines
basic publish support
bnewbold.net
5 months ago
2fab0ca4
2bd7b392
+216
2 changed files
expand all
collapse all
unified
split
cmd
glot
main.go
publish.go
+1
cmd/glot/main.go
···
35
35
cmdDiff,
36
36
cmdCompat,
37
37
cmdNew,
38
38
+
cmdPublish,
38
39
}
39
40
return app.Run(context.Background(), args)
40
41
}
+215
cmd/glot/publish.go
···
1
1
+
package main
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"encoding/json"
6
6
+
"fmt"
7
7
+
"io/fs"
8
8
+
"os"
9
9
+
"path"
10
10
+
"path/filepath"
11
11
+
"reflect"
12
12
+
"sort"
13
13
+
"strings"
14
14
+
15
15
+
"github.com/bluesky-social/indigo/api/agnostic"
16
16
+
"github.com/bluesky-social/indigo/atproto/client"
17
17
+
"github.com/bluesky-social/indigo/atproto/data"
18
18
+
"github.com/bluesky-social/indigo/atproto/identity"
19
19
+
"github.com/bluesky-social/indigo/atproto/syntax"
20
20
+
21
21
+
"github.com/urfave/cli/v3"
22
22
+
)
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
32
+
33
33
+
var cmdPublish = &cli.Command{
34
34
+
Name: "publish",
35
35
+
Usage: "upload any new or updated lexicons",
36
36
+
ArgsUsage: `<file-or-dir>*`,
37
37
+
Flags: []cli.Flag{
38
38
+
&cli.StringFlag{
39
39
+
Name: "lexicons-dir",
40
40
+
Value: "./lexicons/",
41
41
+
Usage: "base directory for project Lexicon files",
42
42
+
Sources: cli.EnvVars("LEXICONS_DIR"),
43
43
+
},
44
44
+
&cli.StringFlag{
45
45
+
Name: "username",
46
46
+
Aliases: []string{"u"},
47
47
+
Usage: "account identifier (handle or DID) for login",
48
48
+
Sources: cli.EnvVars("GLOT_USERNAME", "ATP_USERNAME"),
49
49
+
},
50
50
+
&cli.StringFlag{
51
51
+
Name: "password",
52
52
+
Aliases: []string{"p"},
53
53
+
Usage: "account password (app password) for login",
54
54
+
Sources: cli.EnvVars("GLOT_PASSWORD", "ATP_PASSWORD", "PASSWORD"),
55
55
+
},
56
56
+
},
57
57
+
Action: runPublish,
58
58
+
}
59
59
+
60
60
+
func runPublish(ctx context.Context, cmd *cli.Command) error {
61
61
+
62
62
+
user := cmd.String("username")
63
63
+
pass := cmd.String("password")
64
64
+
65
65
+
if user == "" || pass == "" {
66
66
+
return fmt.Errorf("requires account credentials")
67
67
+
}
68
68
+
atid, err := syntax.ParseAtIdentifier(user)
69
69
+
if err != nil {
70
70
+
return fmt.Errorf("invalid AT account identifier %s: %w", user, err)
71
71
+
}
72
72
+
cdir := identity.DefaultDirectory()
73
73
+
// TODO: could defer actual login until later?
74
74
+
c, err := client.LoginWithPassword(ctx, cdir, *atid, pass, "", nil)
75
75
+
if err != nil {
76
76
+
return nil
77
77
+
}
78
78
+
79
79
+
paths := cmd.Args().Slice()
80
80
+
if !cmd.Args().Present() {
81
81
+
paths = []string{cmd.String("lexicons-dir")}
82
82
+
_, err := os.Stat(paths[0])
83
83
+
if err != nil {
84
84
+
return fmt.Errorf("no path arguments specified and default lexicon directory not found\n%w", err)
85
85
+
}
86
86
+
}
87
87
+
88
88
+
// collect all NSID/path mappings
89
89
+
localSchemas := map[syntax.NSID]json.RawMessage{}
90
90
+
remoteSchemas := map[syntax.NSID]json.RawMessage{}
91
91
+
92
92
+
for _, p := range paths {
93
93
+
finfo, err := os.Stat(p)
94
94
+
if err != nil {
95
95
+
return fmt.Errorf("failed loading %s: %w", p, err)
96
96
+
}
97
97
+
if finfo.IsDir() {
98
98
+
if err := filepath.WalkDir(p, func(fp string, d fs.DirEntry, err error) error {
99
99
+
if d.IsDir() || path.Ext(fp) != ".json" {
100
100
+
return nil
101
101
+
}
102
102
+
nsid, rec, err := loadSchemaPath(fp)
103
103
+
if err != nil {
104
104
+
return err
105
105
+
}
106
106
+
localSchemas[nsid] = *rec
107
107
+
return nil
108
108
+
}); err != nil {
109
109
+
return err
110
110
+
}
111
111
+
continue
112
112
+
}
113
113
+
nsid, rec, err := loadSchemaPath(p)
114
114
+
if err != nil {
115
115
+
return err
116
116
+
}
117
117
+
localSchemas[nsid] = *rec
118
118
+
}
119
119
+
120
120
+
localGroups := map[string]bool{}
121
121
+
allNSIDMap := map[syntax.NSID]bool{}
122
122
+
for k := range localSchemas {
123
123
+
parts := strings.Split(string(k), ".")
124
124
+
g := strings.Join(parts[0:len(parts)-1], ".") + "."
125
125
+
localGroups[g] = true
126
126
+
allNSIDMap[k] = true
127
127
+
}
128
128
+
129
129
+
for g := range localGroups {
130
130
+
if err := fetchLexiconGroup(ctx, cmd, g, &remoteSchemas); err != nil {
131
131
+
return err
132
132
+
}
133
133
+
}
134
134
+
135
135
+
dir := identity.BaseDirectory{}
136
136
+
groupResolution := map[string]syntax.DID{}
137
137
+
for g := range localGroups {
138
138
+
did, err := dir.ResolveNSID(ctx, syntax.NSID(g+"name"))
139
139
+
if err != nil {
140
140
+
continue
141
141
+
}
142
142
+
groupResolution[g] = did
143
143
+
}
144
144
+
145
145
+
for k := range remoteSchemas {
146
146
+
allNSIDMap[k] = true
147
147
+
}
148
148
+
allNSID := []string{}
149
149
+
for k := range allNSIDMap {
150
150
+
allNSID = append(allNSID, string(k))
151
151
+
}
152
152
+
sort.Strings(allNSID)
153
153
+
154
154
+
for _, k := range allNSID {
155
155
+
nsid := syntax.NSID(k)
156
156
+
157
157
+
localJSON := localSchemas[nsid]
158
158
+
remoteJSON := remoteSchemas[nsid]
159
159
+
160
160
+
if localJSON == nil {
161
161
+
continue
162
162
+
}
163
163
+
164
164
+
if remoteJSON == nil {
165
165
+
if err := publishSchema(ctx, c, nsid, localJSON); err != nil {
166
166
+
return err
167
167
+
}
168
168
+
continue
169
169
+
}
170
170
+
171
171
+
local, err := data.UnmarshalJSON(localJSON)
172
172
+
if err != nil {
173
173
+
return err
174
174
+
}
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
+
if err := publishSchema(ctx, c, nsid, localJSON); err != nil {
183
183
+
return err
184
184
+
}
185
185
+
continue
186
186
+
}
187
187
+
}
188
188
+
189
189
+
return nil
190
190
+
}
191
191
+
192
192
+
func publishSchema(ctx context.Context, c *client.APIClient, nsid syntax.NSID, schemaJSON json.RawMessage) error {
193
193
+
194
194
+
d, err := data.UnmarshalJSON(schemaJSON)
195
195
+
if err != nil {
196
196
+
return err
197
197
+
}
198
198
+
d["$type"] = schemaNSID
199
199
+
200
200
+
if c.AccountDID == nil {
201
201
+
return fmt.Errorf("require API client to have DID configured")
202
202
+
}
203
203
+
_, err = agnostic.RepoPutRecord(ctx, c, &agnostic.RepoPutRecord_Input{
204
204
+
Collection: "com.atproto.lexicon.schema",
205
205
+
Repo: c.AccountDID.String(),
206
206
+
Record: d,
207
207
+
Rkey: nsid.String(),
208
208
+
})
209
209
+
if err != nil {
210
210
+
return err
211
211
+
}
212
212
+
fmt.Printf(" 🟣 %s\n", nsid)
213
213
+
214
214
+
return nil
215
215
+
}