tangled
alpha
login
or
join now
stream.place
/
streamplace
Live video on the AT Protocol
74
fork
atom
overview
issues
1
pulls
pipelines
atproto: save lexicon commit events
Eli Mallon
7 months ago
e2802e8e
f803a841
+154
-12
5 changed files
expand all
collapse all
unified
split
pkg
atproto
lexicon_repo.go
lexicon_repo_test.go
cmd
streamplace.go
model
model.go
xrpc_stream_event.go
+42
-2
pkg/atproto/lexicon_repo.go
···
9
"io"
10
"io/fs"
11
"strings"
0
12
0
13
atcrypto "github.com/bluesky-social/indigo/atproto/crypto"
14
"github.com/bluesky-social/indigo/atproto/data"
15
"github.com/bluesky-social/indigo/atproto/lexicon"
···
18
"github.com/bluesky-social/indigo/models"
19
"github.com/bluesky-social/indigo/mst"
20
atrepo "github.com/bluesky-social/indigo/repo"
0
21
"github.com/ipfs/go-cid"
22
cbg "github.com/whyrusleeping/cbor-gen"
23
"gorm.io/driver/sqlite"
···
27
"stream.place/streamplace/lexicons"
28
"stream.place/streamplace/pkg/config"
29
"stream.place/streamplace/pkg/log"
0
30
)
31
32
var LexiconRepo *atrepo.Repo
33
var LexiconPubMultibase string
34
var RepoUser models.Uid = models.Uid(1)
35
var CarStore carstore.CarStore
0
0
0
36
37
func walkLexicons(ctx context.Context, bundle fs.FS, path string) ([][]byte, error) {
38
ret := [][]byte{}
···
117
Close() error
118
}
119
120
-
func MakeLexiconRepo(ctx context.Context, cli *config.CLI) (Closer, error) {
121
ctx = log.WithLogValues(ctx, "func", "MakeLexiconRepo")
122
fd, err := cli.DataFileCreate([]string{"carstore", "empty"}, true)
123
if err != nil {
···
211
if err != nil {
212
return nil, fmt.Errorf("failed to walk lexicon files: %w", err)
213
}
0
0
0
214
for _, lex := range lexs {
215
lexFile := lexicon.SchemaFile{}
216
err := json.Unmarshal(lex, &lexFile)
···
226
if err != nil {
227
return nil, err
228
}
0
229
230
oldCid, _, err := LexiconRepo.GetRecord(ctx, rpath)
231
if errors.Is(err, mst.ErrNotFound) {
···
234
return nil, err
235
}
236
log.Log(ctx, "created new lexicon record", "rpath", rpath, "cid", newCid.String())
0
0
0
0
0
237
} else if err != nil {
238
return nil, err
239
} else {
···
246
if err != nil {
247
return nil, err
248
}
0
0
0
0
0
0
0
249
}
250
}
251
currentRoot, currentRev, err = LexiconRepo.Commit(ctx, signer)
252
if err != nil {
253
return nil, fmt.Errorf("failed to commit: %w", err)
254
}
0
255
log.Log(ctx, "LexiconRepo committed", "cid", currentRoot.String(), "rev", currentRev)
256
}
257
-
_, err = ses.CloseWithRoot(ctx, currentRoot, currentRev)
258
if err != nil {
259
return nil, fmt.Errorf("failed to close delta session: %w", err)
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
260
}
261
262
return sqlDB, nil
···
9
"io"
10
"io/fs"
11
"strings"
12
+
"time"
13
14
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
15
atcrypto "github.com/bluesky-social/indigo/atproto/crypto"
16
"github.com/bluesky-social/indigo/atproto/data"
17
"github.com/bluesky-social/indigo/atproto/lexicon"
···
20
"github.com/bluesky-social/indigo/models"
21
"github.com/bluesky-social/indigo/mst"
22
atrepo "github.com/bluesky-social/indigo/repo"
23
+
"github.com/bluesky-social/indigo/util"
24
"github.com/ipfs/go-cid"
25
cbg "github.com/whyrusleeping/cbor-gen"
26
"gorm.io/driver/sqlite"
···
30
"stream.place/streamplace/lexicons"
31
"stream.place/streamplace/pkg/config"
32
"stream.place/streamplace/pkg/log"
33
+
"stream.place/streamplace/pkg/model"
34
)
35
36
var LexiconRepo *atrepo.Repo
37
var LexiconPubMultibase string
38
var RepoUser models.Uid = models.Uid(1)
39
var CarStore carstore.CarStore
40
+
var ActionCreate = "create"
41
+
var ActionUpdate = "update"
42
+
var ActionDelete = "delete"
43
44
func walkLexicons(ctx context.Context, bundle fs.FS, path string) ([][]byte, error) {
45
ret := [][]byte{}
···
124
Close() error
125
}
126
127
+
func MakeLexiconRepo(ctx context.Context, cli *config.CLI, mod model.Model) (Closer, error) {
128
ctx = log.WithLogValues(ctx, "func", "MakeLexiconRepo")
129
fd, err := cli.DataFileCreate([]string{"carstore", "empty"}, true)
130
if err != nil {
···
218
if err != nil {
219
return nil, fmt.Errorf("failed to walk lexicon files: %w", err)
220
}
221
+
222
+
ops := []*comatproto.SyncSubscribeRepos_RepoOp{}
223
+
224
for _, lex := range lexs {
225
lexFile := lexicon.SchemaFile{}
226
err := json.Unmarshal(lex, &lexFile)
···
236
if err != nil {
237
return nil, err
238
}
239
+
cidLink := lexutil.LexLink(*newCid)
240
241
oldCid, _, err := LexiconRepo.GetRecord(ctx, rpath)
242
if errors.Is(err, mst.ErrNotFound) {
···
245
return nil, err
246
}
247
log.Log(ctx, "created new lexicon record", "rpath", rpath, "cid", newCid.String())
248
+
ops = append(ops, &comatproto.SyncSubscribeRepos_RepoOp{
249
+
Action: ActionCreate,
250
+
Path: rpath,
251
+
Cid: &cidLink,
252
+
})
253
} else if err != nil {
254
return nil, err
255
} else {
···
262
if err != nil {
263
return nil, err
264
}
265
+
oldLink := lexutil.LexLink(oldCid)
266
+
ops = append(ops, &comatproto.SyncSubscribeRepos_RepoOp{
267
+
Action: ActionUpdate,
268
+
Path: rpath,
269
+
Prev: &oldLink,
270
+
Cid: &cidLink,
271
+
})
272
}
273
}
274
currentRoot, currentRev, err = LexiconRepo.Commit(ctx, signer)
275
if err != nil {
276
return nil, fmt.Errorf("failed to commit: %w", err)
277
}
278
+
279
log.Log(ctx, "LexiconRepo committed", "cid", currentRoot.String(), "rev", currentRev)
280
}
281
+
blocks, err := ses.CloseWithRoot(ctx, currentRoot, currentRev)
282
if err != nil {
283
return nil, fmt.Errorf("failed to close delta session: %w", err)
284
+
}
285
+
if len(ops) > 0 {
286
+
commit := &comatproto.SyncSubscribeRepos_Commit{
287
+
Repo: cli.MyDID(),
288
+
Blocks: blocks,
289
+
Rev: currentRev,
290
+
// Since: currentRev,
291
+
Commit: lexutil.LexLink(currentRoot),
292
+
Time: time.Now().Format(util.ISO8601),
293
+
Ops: ops,
294
+
TooBig: false,
295
+
}
296
+
err := mod.CreateCommitEvent(commit)
297
+
if err != nil {
298
+
return nil, fmt.Errorf("failed to create commit event: %w", err)
299
+
}
300
}
301
302
return sqlDB, nil
+23
-5
pkg/atproto/lexicon_repo_test.go
···
6
"io/fs"
7
"testing"
8
"testing/fstest"
0
9
10
"github.com/stretchr/testify/require"
11
"stream.place/streamplace/lexicons"
12
"stream.place/streamplace/pkg/config"
0
13
)
14
15
func TestLexiconRepo(t *testing.T) {
16
-
cli := config.CLI{}
0
0
17
cli.DataDir = t.TempDir()
0
0
18
19
// creating a new repo
20
-
handle, err := MakeLexiconRepo(context.Background(), &cli)
21
require.NoError(t, err)
22
r, sess, err := OpenLexiconRepo(context.Background())
23
require.NoError(t, err)
···
30
require.NotNil(t, rec)
31
handle.Close()
32
0
0
0
0
0
33
// opening an existing repo
34
-
handle, err = MakeLexiconRepo(context.Background(), &cli)
35
require.NoError(t, err)
36
handle.Close()
37
···
81
AllFiles = modifiedFS
82
83
// opening an existing repo with modified lexicon
84
-
handle, err = MakeLexiconRepo(context.Background(), &cli)
85
require.NoError(t, err)
86
handle.Close()
87
88
-
// Now modifiedFS is a fs.FS with the first file modified
0
0
0
0
0
0
0
89
}
···
6
"io/fs"
7
"testing"
8
"testing/fstest"
9
+
"time"
10
11
"github.com/stretchr/testify/require"
12
"stream.place/streamplace/lexicons"
13
"stream.place/streamplace/pkg/config"
14
+
"stream.place/streamplace/pkg/model"
15
)
16
17
func TestLexiconRepo(t *testing.T) {
18
+
cli := config.CLI{
19
+
PublicHost: "example.com",
20
+
}
21
cli.DataDir = t.TempDir()
22
+
mod, err := model.MakeDB(":memory:")
23
+
require.NoError(t, err)
24
25
// creating a new repo
26
+
handle, err := MakeLexiconRepo(context.Background(), &cli, mod)
27
require.NoError(t, err)
28
r, sess, err := OpenLexiconRepo(context.Background())
29
require.NoError(t, err)
···
36
require.NotNil(t, rec)
37
handle.Close()
38
39
+
evts, err := mod.GetCommitEventsSince(cli.MyDID(), time.Time{})
40
+
require.NoError(t, err)
41
+
require.Len(t, evts, 1)
42
+
require.Equal(t, evts[0].RepoDID, cli.MyDID())
43
+
44
// opening an existing repo
45
+
handle, err = MakeLexiconRepo(context.Background(), &cli, mod)
46
require.NoError(t, err)
47
handle.Close()
48
···
92
AllFiles = modifiedFS
93
94
// opening an existing repo with modified lexicon
95
+
handle, err = MakeLexiconRepo(context.Background(), &cli, mod)
96
require.NoError(t, err)
97
handle.Close()
98
99
+
evts, err = mod.GetCommitEventsSince(cli.MyDID(), time.Time{})
100
+
require.NoError(t, err)
101
+
require.Len(t, evts, 2)
102
+
require.Equal(t, evts[0].RepoDID, cli.MyDID())
103
+
require.Equal(t, evts[1].RepoDID, cli.MyDID())
104
+
commit, err := evts[1].ToCommitEvent()
105
+
require.NoError(t, err)
106
+
require.Equal(t, commit.Since, &evts[0].CID)
107
}
+5
-5
pkg/cmd/streamplace.go
···
158
if err != nil {
159
return fmt.Errorf("error creating streamplace dir at %s:%w", cli.DataDir, err)
160
}
161
-
handle, err := atproto.MakeLexiconRepo(ctx, &cli)
162
-
if err != nil {
163
-
return err
164
-
}
165
-
defer handle.Close()
166
schema, err := v0.MakeV0Schema()
167
if err != nil {
168
return err
···
252
if err != nil {
253
return err
254
}
0
0
0
0
0
255
var noter notifications.FirebaseNotifier
256
if cli.FirebaseServiceAccount != "" {
257
noter, err = notifications.MakeFirebaseNotifier(ctx, cli.FirebaseServiceAccount)
···
158
if err != nil {
159
return fmt.Errorf("error creating streamplace dir at %s:%w", cli.DataDir, err)
160
}
0
0
0
0
0
161
schema, err := v0.MakeV0Schema()
162
if err != nil {
163
return err
···
247
if err != nil {
248
return err
249
}
250
+
handle, err := atproto.MakeLexiconRepo(ctx, &cli, mod)
251
+
if err != nil {
252
+
return err
253
+
}
254
+
defer handle.Close()
255
var noter notifications.FirebaseNotifier
256
if cli.FirebaseServiceAccount != "" {
257
noter, err = notifications.MakeFirebaseNotifier(ctx, cli.FirebaseServiceAccount)
+6
pkg/model/model.go
···
8
"strings"
9
"time"
10
0
11
"github.com/bluesky-social/indigo/api/bsky"
12
"github.com/lmittmann/tint"
13
slogGorm "github.com/orandin/slog-gorm"
···
96
UpdateServerSettings(ctx context.Context, settings *ServerSettings) error
97
GetServerSettings(ctx context.Context, server string, repoDID string) (*ServerSettings, error)
98
DeleteServerSettings(ctx context.Context, server string, repoDID string) error
0
0
0
0
99
}
100
101
func MakeDB(dbURL string) (Model, error) {
···
156
ChatProfile{},
157
oatproxy.OAuthSession{},
158
ServerSettings{},
0
159
} {
160
err = db.AutoMigrate(model)
161
if err != nil {
···
8
"strings"
9
"time"
10
11
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
12
"github.com/bluesky-social/indigo/api/bsky"
13
"github.com/lmittmann/tint"
14
slogGorm "github.com/orandin/slog-gorm"
···
97
UpdateServerSettings(ctx context.Context, settings *ServerSettings) error
98
GetServerSettings(ctx context.Context, server string, repoDID string) (*ServerSettings, error)
99
DeleteServerSettings(ctx context.Context, server string, repoDID string) error
100
+
101
+
CreateCommitEvent(commit *comatproto.SyncSubscribeRepos_Commit) error
102
+
GetCommitEventsSince(repoDID string, t time.Time) ([]*XrpcStreamEvent, error)
103
+
GetMostRecentCommitEvent(repoDID string) (*XrpcStreamEvent, error)
104
}
105
106
func MakeDB(dbURL string) (Model, error) {
···
161
ChatProfile{},
162
oatproxy.OAuthSession{},
163
ServerSettings{},
164
+
XrpcStreamEvent{},
165
} {
166
err = db.AutoMigrate(model)
167
if err != nil {
+78
pkg/model/xrpc_stream_event.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
···
1
+
package model
2
+
3
+
import (
4
+
"bytes"
5
+
"errors"
6
+
"time"
7
+
8
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
+
"github.com/bluesky-social/indigo/util"
10
+
"gorm.io/gorm"
11
+
)
12
+
13
+
type XrpcStreamEvent struct {
14
+
CID string `json:"cid" gorm:"primaryKey"`
15
+
RepoDID string `json:"repoDID" gorm:"index:idx_repo_timestamp,priority:1;column:repo_did"`
16
+
Timestamp time.Time `json:"timestamp" gorm:"index:idx_repo_timestamp,priority:2;column:timestamp"`
17
+
Data []byte `json:"data"`
18
+
}
19
+
20
+
func (ev *XrpcStreamEvent) ToCommitEvent() (*comatproto.SyncSubscribeRepos_Commit, error) {
21
+
commit := &comatproto.SyncSubscribeRepos_Commit{}
22
+
err := commit.UnmarshalCBOR(bytes.NewReader(ev.Data))
23
+
if err != nil {
24
+
return nil, err
25
+
}
26
+
return commit, nil
27
+
}
28
+
29
+
func (m *DBModel) CreateCommitEvent(commit *comatproto.SyncSubscribeRepos_Commit) error {
30
+
prev, err := m.GetMostRecentCommitEvent(commit.Repo)
31
+
if err != nil {
32
+
return err
33
+
}
34
+
if prev != nil {
35
+
commit.Since = &prev.CID
36
+
}
37
+
buf := bytes.Buffer{}
38
+
err = commit.MarshalCBOR(&buf)
39
+
if err != nil {
40
+
return err
41
+
}
42
+
timestamp, err := time.Parse(util.ISO8601, commit.Time)
43
+
if err != nil {
44
+
return err
45
+
}
46
+
event := &XrpcStreamEvent{
47
+
CID: commit.Commit.String(),
48
+
RepoDID: commit.Repo,
49
+
Timestamp: timestamp.UTC(),
50
+
Data: buf.Bytes(),
51
+
}
52
+
return m.DB.Create(event).Error
53
+
}
54
+
55
+
func (m *DBModel) GetCommitEventsSince(repoDID string, t time.Time) ([]*XrpcStreamEvent, error) {
56
+
var events []*XrpcStreamEvent
57
+
query := m.DB.Where("repo_did = ?", repoDID)
58
+
query = query.Where("timestamp > ?", t.UTC())
59
+
err := query.Order("timestamp ASC").Find(&events).Error
60
+
if err != nil {
61
+
return nil, err
62
+
}
63
+
return events, nil
64
+
}
65
+
66
+
func (m *DBModel) GetMostRecentCommitEvent(repoDID string) (*XrpcStreamEvent, error) {
67
+
var event XrpcStreamEvent
68
+
err := m.DB.Where("repo_did = ?", repoDID).
69
+
Order("timestamp DESC").
70
+
Limit(1).
71
+
First(&event).Error
72
+
if errors.Is(err, gorm.ErrRecordNotFound) {
73
+
return nil, nil
74
+
} else if err != nil {
75
+
return nil, err
76
+
}
77
+
return &event, nil
78
+
}