+34
api/tangled/pipelinecancelPipeline.go
+34
api/tangled/pipelinecancelPipeline.go
···
···
1
+
// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT.
2
+
3
+
package tangled
4
+
5
+
// schema: sh.tangled.pipeline.cancelPipeline
6
+
7
+
import (
8
+
"context"
9
+
10
+
"github.com/bluesky-social/indigo/lex/util"
11
+
)
12
+
13
+
const (
14
+
PipelineCancelPipelineNSID = "sh.tangled.pipeline.cancelPipeline"
15
+
)
16
+
17
+
// PipelineCancelPipeline_Input is the input argument to a sh.tangled.pipeline.cancelPipeline call.
18
+
type PipelineCancelPipeline_Input struct {
19
+
// pipeline: pipeline at-uri
20
+
Pipeline string `json:"pipeline" cborgen:"pipeline"`
21
+
// repo: repo at-uri, spindle can't resolve repo from pipeline at-uri yet
22
+
Repo string `json:"repo" cborgen:"repo"`
23
+
// workflow: workflow name
24
+
Workflow string `json:"workflow" cborgen:"workflow"`
25
+
}
26
+
27
+
// PipelineCancelPipeline calls the XRPC method "sh.tangled.pipeline.cancelPipeline".
28
+
func PipelineCancelPipeline(ctx context.Context, c util.LexClient, input *PipelineCancelPipeline_Input) error {
29
+
if err := c.LexDo(ctx, util.Procedure, "application/json", "sh.tangled.pipeline.cancelPipeline", nil, input, nil); err != nil {
30
+
return err
31
+
}
32
+
33
+
return nil
34
+
}
+3
-2
appview/db/artifact.go
+3
-2
appview/db/artifact.go
···
8
"github.com/go-git/go-git/v5/plumbing"
9
"github.com/ipfs/go-cid"
10
"tangled.org/core/appview/models"
11
)
12
13
func AddArtifact(e Execer, artifact models.Artifact) error {
···
37
return err
38
}
39
40
-
func GetArtifact(e Execer, filters ...filter) ([]models.Artifact, error) {
41
var artifacts []models.Artifact
42
43
var conditions []string
···
109
return artifacts, nil
110
}
111
112
-
func DeleteArtifact(e Execer, filters ...filter) error {
113
var conditions []string
114
var args []any
115
for _, filter := range filters {
···
8
"github.com/go-git/go-git/v5/plumbing"
9
"github.com/ipfs/go-cid"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
func AddArtifact(e Execer, artifact models.Artifact) error {
···
38
return err
39
}
40
41
+
func GetArtifact(e Execer, filters ...orm.Filter) ([]models.Artifact, error) {
42
var artifacts []models.Artifact
43
44
var conditions []string
···
110
return artifacts, nil
111
}
112
113
+
func DeleteArtifact(e Execer, filters ...orm.Filter) error {
114
var conditions []string
115
var args []any
116
for _, filter := range filters {
+4
-3
appview/db/collaborators.go
+4
-3
appview/db/collaborators.go
···
6
"time"
7
8
"tangled.org/core/appview/models"
9
)
10
11
func AddCollaborator(e Execer, c models.Collaborator) error {
···
16
return err
17
}
18
19
-
func DeleteCollaborator(e Execer, filters ...filter) error {
20
var conditions []string
21
var args []any
22
for _, filter := range filters {
···
58
return nil, nil
59
}
60
61
-
return GetRepos(e, 0, FilterIn("at_uri", repoAts))
62
}
63
64
-
func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) {
65
var collaborators []models.Collaborator
66
var conditions []string
67
var args []any
···
6
"time"
7
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
10
)
11
12
func AddCollaborator(e Execer, c models.Collaborator) error {
···
17
return err
18
}
19
20
+
func DeleteCollaborator(e Execer, filters ...orm.Filter) error {
21
var conditions []string
22
var args []any
23
for _, filter := range filters {
···
59
return nil, nil
60
}
61
62
+
return GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
63
}
64
65
+
func GetCollaborators(e Execer, filters ...orm.Filter) ([]models.Collaborator, error) {
66
var collaborators []models.Collaborator
67
var conditions []string
68
var args []any
+24
-137
appview/db/db.go
+24
-137
appview/db/db.go
···
3
import (
4
"context"
5
"database/sql"
6
-
"fmt"
7
"log/slog"
8
-
"reflect"
9
"strings"
10
11
_ "github.com/mattn/go-sqlite3"
12
"tangled.org/core/log"
13
)
14
15
type DB struct {
···
584
}
585
586
// run migrations
587
-
runMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
588
tx.Exec(`
589
alter table repos add column description text check (length(description) <= 200);
590
`)
591
return nil
592
})
593
594
-
runMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
595
// add unconstrained column
596
_, err := tx.Exec(`
597
alter table public_keys
···
614
return nil
615
})
616
617
-
runMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
618
_, err := tx.Exec(`
619
alter table comments drop column comment_at;
620
alter table comments add column rkey text;
···
622
return err
623
})
624
625
-
runMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
626
_, err := tx.Exec(`
627
alter table comments add column deleted text; -- timestamp
628
alter table comments add column edited text; -- timestamp
···
630
return err
631
})
632
633
-
runMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
634
_, err := tx.Exec(`
635
alter table pulls add column source_branch text;
636
alter table pulls add column source_repo_at text;
···
639
return err
640
})
641
642
-
runMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
643
_, err := tx.Exec(`
644
alter table repos add column source text;
645
`)
···
651
//
652
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
653
conn.ExecContext(ctx, "pragma foreign_keys = off;")
654
-
runMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
655
_, err := tx.Exec(`
656
create table pulls_new (
657
-- identifiers
···
708
})
709
conn.ExecContext(ctx, "pragma foreign_keys = on;")
710
711
-
runMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
712
tx.Exec(`
713
alter table repos add column spindle text;
714
`)
···
718
// drop all knot secrets, add unique constraint to knots
719
//
720
// knots will henceforth use service auth for signed requests
721
-
runMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
722
_, err := tx.Exec(`
723
create table registrations_new (
724
id integer primary key autoincrement,
···
741
})
742
743
// recreate and add rkey + created columns with default constraint
744
-
runMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
745
// create new table
746
// - repo_at instead of repo integer
747
// - rkey field
···
795
return err
796
})
797
798
-
runMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
799
_, err := tx.Exec(`
800
alter table issues add column rkey text not null default '';
801
···
807
})
808
809
// repurpose the read-only column to "needs-upgrade"
810
-
runMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
811
_, err := tx.Exec(`
812
alter table registrations rename column read_only to needs_upgrade;
813
`)
···
815
})
816
817
// require all knots to upgrade after the release of total xrpc
818
-
runMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
819
_, err := tx.Exec(`
820
update registrations set needs_upgrade = 1;
821
`)
···
823
})
824
825
// require all knots to upgrade after the release of total xrpc
826
-
runMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
827
_, err := tx.Exec(`
828
alter table spindles add column needs_upgrade integer not null default 0;
829
`)
···
841
//
842
// disable foreign-keys for the next migration
843
conn.ExecContext(ctx, "pragma foreign_keys = off;")
844
-
runMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
845
_, err := tx.Exec(`
846
create table if not exists issues_new (
847
-- identifiers
···
911
// - new columns
912
// * column "reply_to" which can be any other comment
913
// * column "at-uri" which is a generated column
914
-
runMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
915
_, err := tx.Exec(`
916
create table if not exists issue_comments (
917
-- identifiers
···
971
//
972
// disable foreign-keys for the next migration
973
conn.ExecContext(ctx, "pragma foreign_keys = off;")
974
-
runMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
975
_, err := tx.Exec(`
976
create table if not exists pulls_new (
977
-- identifiers
···
1052
//
1053
// disable foreign-keys for the next migration
1054
conn.ExecContext(ctx, "pragma foreign_keys = off;")
1055
-
runMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1056
_, err := tx.Exec(`
1057
create table if not exists pull_submissions_new (
1058
-- identifiers
···
1106
1107
// knots may report the combined patch for a comparison, we can store that on the appview side
1108
// (but not on the pds record), because calculating the combined patch requires a git index
1109
-
runMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1110
_, err := tx.Exec(`
1111
alter table pull_submissions add column combined text;
1112
`)
1113
return err
1114
})
1115
1116
-
runMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1117
_, err := tx.Exec(`
1118
alter table profile add column pronouns text;
1119
`)
1120
return err
1121
})
1122
1123
-
runMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1124
_, err := tx.Exec(`
1125
alter table repos add column website text;
1126
alter table repos add column topics text;
···
1128
return err
1129
})
1130
1131
-
runMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1132
_, err := tx.Exec(`
1133
alter table notification_preferences add column user_mentioned integer not null default 1;
1134
`)
···
1136
})
1137
1138
// remove the foreign key constraints from stars.
1139
-
runMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1140
_, err := tx.Exec(`
1141
create table stars_new (
1142
id integer primary key autoincrement,
···
1180
}, nil
1181
}
1182
1183
-
type migrationFn = func(*sql.Tx) error
1184
-
1185
-
func runMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
1186
-
logger = logger.With("migration", name)
1187
-
1188
-
tx, err := c.BeginTx(context.Background(), nil)
1189
-
if err != nil {
1190
-
return err
1191
-
}
1192
-
defer tx.Rollback()
1193
-
1194
-
var exists bool
1195
-
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
1196
-
if err != nil {
1197
-
return err
1198
-
}
1199
-
1200
-
if !exists {
1201
-
// run migration
1202
-
err = migrationFn(tx)
1203
-
if err != nil {
1204
-
logger.Error("failed to run migration", "err", err)
1205
-
return err
1206
-
}
1207
-
1208
-
// mark migration as complete
1209
-
_, err = tx.Exec("insert into migrations (name) values (?)", name)
1210
-
if err != nil {
1211
-
logger.Error("failed to mark migration as complete", "err", err)
1212
-
return err
1213
-
}
1214
-
1215
-
// commit the transaction
1216
-
if err := tx.Commit(); err != nil {
1217
-
return err
1218
-
}
1219
-
1220
-
logger.Info("migration applied successfully")
1221
-
} else {
1222
-
logger.Warn("skipped migration, already applied")
1223
-
}
1224
-
1225
-
return nil
1226
-
}
1227
-
1228
func (d *DB) Close() error {
1229
return d.DB.Close()
1230
}
1231
-
1232
-
type filter struct {
1233
-
key string
1234
-
arg any
1235
-
cmp string
1236
-
}
1237
-
1238
-
func newFilter(key, cmp string, arg any) filter {
1239
-
return filter{
1240
-
key: key,
1241
-
arg: arg,
1242
-
cmp: cmp,
1243
-
}
1244
-
}
1245
-
1246
-
func FilterEq(key string, arg any) filter { return newFilter(key, "=", arg) }
1247
-
func FilterNotEq(key string, arg any) filter { return newFilter(key, "<>", arg) }
1248
-
func FilterGte(key string, arg any) filter { return newFilter(key, ">=", arg) }
1249
-
func FilterLte(key string, arg any) filter { return newFilter(key, "<=", arg) }
1250
-
func FilterIs(key string, arg any) filter { return newFilter(key, "is", arg) }
1251
-
func FilterIsNot(key string, arg any) filter { return newFilter(key, "is not", arg) }
1252
-
func FilterIn(key string, arg any) filter { return newFilter(key, "in", arg) }
1253
-
func FilterLike(key string, arg any) filter { return newFilter(key, "like", arg) }
1254
-
func FilterNotLike(key string, arg any) filter { return newFilter(key, "not like", arg) }
1255
-
func FilterContains(key string, arg any) filter {
1256
-
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
1257
-
}
1258
-
1259
-
func (f filter) Condition() string {
1260
-
rv := reflect.ValueOf(f.arg)
1261
-
kind := rv.Kind()
1262
-
1263
-
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
1264
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1265
-
if rv.Len() == 0 {
1266
-
// always false
1267
-
return "1 = 0"
1268
-
}
1269
-
1270
-
placeholders := make([]string, rv.Len())
1271
-
for i := range placeholders {
1272
-
placeholders[i] = "?"
1273
-
}
1274
-
1275
-
return fmt.Sprintf("%s %s (%s)", f.key, f.cmp, strings.Join(placeholders, ", "))
1276
-
}
1277
-
1278
-
return fmt.Sprintf("%s %s ?", f.key, f.cmp)
1279
-
}
1280
-
1281
-
func (f filter) Arg() []any {
1282
-
rv := reflect.ValueOf(f.arg)
1283
-
kind := rv.Kind()
1284
-
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
1285
-
if rv.Len() == 0 {
1286
-
return nil
1287
-
}
1288
-
1289
-
out := make([]any, rv.Len())
1290
-
for i := range rv.Len() {
1291
-
out[i] = rv.Index(i).Interface()
1292
-
}
1293
-
return out
1294
-
}
1295
-
1296
-
return []any{f.arg}
1297
-
}
···
3
import (
4
"context"
5
"database/sql"
6
"log/slog"
7
"strings"
8
9
_ "github.com/mattn/go-sqlite3"
10
"tangled.org/core/log"
11
+
"tangled.org/core/orm"
12
)
13
14
type DB struct {
···
583
}
584
585
// run migrations
586
+
orm.RunMigration(conn, logger, "add-description-to-repos", func(tx *sql.Tx) error {
587
tx.Exec(`
588
alter table repos add column description text check (length(description) <= 200);
589
`)
590
return nil
591
})
592
593
+
orm.RunMigration(conn, logger, "add-rkey-to-pubkeys", func(tx *sql.Tx) error {
594
// add unconstrained column
595
_, err := tx.Exec(`
596
alter table public_keys
···
613
return nil
614
})
615
616
+
orm.RunMigration(conn, logger, "add-rkey-to-comments", func(tx *sql.Tx) error {
617
_, err := tx.Exec(`
618
alter table comments drop column comment_at;
619
alter table comments add column rkey text;
···
621
return err
622
})
623
624
+
orm.RunMigration(conn, logger, "add-deleted-and-edited-to-issue-comments", func(tx *sql.Tx) error {
625
_, err := tx.Exec(`
626
alter table comments add column deleted text; -- timestamp
627
alter table comments add column edited text; -- timestamp
···
629
return err
630
})
631
632
+
orm.RunMigration(conn, logger, "add-source-info-to-pulls-and-submissions", func(tx *sql.Tx) error {
633
_, err := tx.Exec(`
634
alter table pulls add column source_branch text;
635
alter table pulls add column source_repo_at text;
···
638
return err
639
})
640
641
+
orm.RunMigration(conn, logger, "add-source-to-repos", func(tx *sql.Tx) error {
642
_, err := tx.Exec(`
643
alter table repos add column source text;
644
`)
···
650
//
651
// [0]: https://sqlite.org/pragma.html#pragma_foreign_keys
652
conn.ExecContext(ctx, "pragma foreign_keys = off;")
653
+
orm.RunMigration(conn, logger, "recreate-pulls-column-for-stacking-support", func(tx *sql.Tx) error {
654
_, err := tx.Exec(`
655
create table pulls_new (
656
-- identifiers
···
707
})
708
conn.ExecContext(ctx, "pragma foreign_keys = on;")
709
710
+
orm.RunMigration(conn, logger, "add-spindle-to-repos", func(tx *sql.Tx) error {
711
tx.Exec(`
712
alter table repos add column spindle text;
713
`)
···
717
// drop all knot secrets, add unique constraint to knots
718
//
719
// knots will henceforth use service auth for signed requests
720
+
orm.RunMigration(conn, logger, "no-more-secrets", func(tx *sql.Tx) error {
721
_, err := tx.Exec(`
722
create table registrations_new (
723
id integer primary key autoincrement,
···
740
})
741
742
// recreate and add rkey + created columns with default constraint
743
+
orm.RunMigration(conn, logger, "rework-collaborators-table", func(tx *sql.Tx) error {
744
// create new table
745
// - repo_at instead of repo integer
746
// - rkey field
···
794
return err
795
})
796
797
+
orm.RunMigration(conn, logger, "add-rkey-to-issues", func(tx *sql.Tx) error {
798
_, err := tx.Exec(`
799
alter table issues add column rkey text not null default '';
800
···
806
})
807
808
// repurpose the read-only column to "needs-upgrade"
809
+
orm.RunMigration(conn, logger, "rename-registrations-read-only-to-needs-upgrade", func(tx *sql.Tx) error {
810
_, err := tx.Exec(`
811
alter table registrations rename column read_only to needs_upgrade;
812
`)
···
814
})
815
816
// require all knots to upgrade after the release of total xrpc
817
+
orm.RunMigration(conn, logger, "migrate-knots-to-total-xrpc", func(tx *sql.Tx) error {
818
_, err := tx.Exec(`
819
update registrations set needs_upgrade = 1;
820
`)
···
822
})
823
824
// require all knots to upgrade after the release of total xrpc
825
+
orm.RunMigration(conn, logger, "migrate-spindles-to-xrpc-owner", func(tx *sql.Tx) error {
826
_, err := tx.Exec(`
827
alter table spindles add column needs_upgrade integer not null default 0;
828
`)
···
840
//
841
// disable foreign-keys for the next migration
842
conn.ExecContext(ctx, "pragma foreign_keys = off;")
843
+
orm.RunMigration(conn, logger, "remove-issue-at-from-issues", func(tx *sql.Tx) error {
844
_, err := tx.Exec(`
845
create table if not exists issues_new (
846
-- identifiers
···
910
// - new columns
911
// * column "reply_to" which can be any other comment
912
// * column "at-uri" which is a generated column
913
+
orm.RunMigration(conn, logger, "rework-issue-comments", func(tx *sql.Tx) error {
914
_, err := tx.Exec(`
915
create table if not exists issue_comments (
916
-- identifiers
···
970
//
971
// disable foreign-keys for the next migration
972
conn.ExecContext(ctx, "pragma foreign_keys = off;")
973
+
orm.RunMigration(conn, logger, "add-at-uri-to-pulls", func(tx *sql.Tx) error {
974
_, err := tx.Exec(`
975
create table if not exists pulls_new (
976
-- identifiers
···
1051
//
1052
// disable foreign-keys for the next migration
1053
conn.ExecContext(ctx, "pragma foreign_keys = off;")
1054
+
orm.RunMigration(conn, logger, "remove-repo-at-pull-id-from-pull-submissions", func(tx *sql.Tx) error {
1055
_, err := tx.Exec(`
1056
create table if not exists pull_submissions_new (
1057
-- identifiers
···
1105
1106
// knots may report the combined patch for a comparison, we can store that on the appview side
1107
// (but not on the pds record), because calculating the combined patch requires a git index
1108
+
orm.RunMigration(conn, logger, "add-combined-column-submissions", func(tx *sql.Tx) error {
1109
_, err := tx.Exec(`
1110
alter table pull_submissions add column combined text;
1111
`)
1112
return err
1113
})
1114
1115
+
orm.RunMigration(conn, logger, "add-pronouns-profile", func(tx *sql.Tx) error {
1116
_, err := tx.Exec(`
1117
alter table profile add column pronouns text;
1118
`)
1119
return err
1120
})
1121
1122
+
orm.RunMigration(conn, logger, "add-meta-column-repos", func(tx *sql.Tx) error {
1123
_, err := tx.Exec(`
1124
alter table repos add column website text;
1125
alter table repos add column topics text;
···
1127
return err
1128
})
1129
1130
+
orm.RunMigration(conn, logger, "add-usermentioned-preference", func(tx *sql.Tx) error {
1131
_, err := tx.Exec(`
1132
alter table notification_preferences add column user_mentioned integer not null default 1;
1133
`)
···
1135
})
1136
1137
// remove the foreign key constraints from stars.
1138
+
orm.RunMigration(conn, logger, "generalize-stars-subject", func(tx *sql.Tx) error {
1139
_, err := tx.Exec(`
1140
create table stars_new (
1141
id integer primary key autoincrement,
···
1179
}, nil
1180
}
1181
1182
func (d *DB) Close() error {
1183
return d.DB.Close()
1184
}
+6
-3
appview/db/follow.go
+6
-3
appview/db/follow.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
func AddFollow(e Execer, follow *models.Follow) error {
···
134
return result, nil
135
}
136
137
-
func GetFollows(e Execer, limit int, filters ...filter) ([]models.Follow, error) {
138
var follows []models.Follow
139
140
var conditions []string
···
166
if err != nil {
167
return nil, err
168
}
169
for rows.Next() {
170
var follow models.Follow
171
var followedAt string
···
191
}
192
193
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
194
-
return GetFollows(e, 0, FilterEq("subject_did", did))
195
}
196
197
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
198
-
return GetFollows(e, 0, FilterEq("user_did", did))
199
}
200
201
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
func AddFollow(e Execer, follow *models.Follow) error {
···
135
return result, nil
136
}
137
138
+
func GetFollows(e Execer, limit int, filters ...orm.Filter) ([]models.Follow, error) {
139
var follows []models.Follow
140
141
var conditions []string
···
167
if err != nil {
168
return nil, err
169
}
170
+
defer rows.Close()
171
+
172
for rows.Next() {
173
var follow models.Follow
174
var followedAt string
···
194
}
195
196
func GetFollowers(e Execer, did string) ([]models.Follow, error) {
197
+
return GetFollows(e, 0, orm.FilterEq("subject_did", did))
198
}
199
200
func GetFollowing(e Execer, did string) ([]models.Follow, error) {
201
+
return GetFollows(e, 0, orm.FilterEq("user_did", did))
202
}
203
204
func getFollowStatuses(e Execer, userDid string, subjectDids []string) (map[string]models.FollowStatus, error) {
+22
-20
appview/db/issues.go
+22
-20
appview/db/issues.go
···
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/pagination"
16
)
17
18
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
27
28
issues, err := GetIssues(
29
tx,
30
-
FilterEq("did", issue.Did),
31
-
FilterEq("rkey", issue.Rkey),
32
)
33
switch {
34
case err != nil:
···
98
return nil
99
}
100
101
-
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...filter) ([]models.Issue, error) {
102
issueMap := make(map[string]*models.Issue) // at-uri -> issue
103
104
var conditions []string
···
114
whereClause = " where " + strings.Join(conditions, " and ")
115
}
116
117
-
pLower := FilterGte("row_num", page.Offset+1)
118
-
pUpper := FilterLte("row_num", page.Offset+page.Limit)
119
120
pageClause := ""
121
if page.Limit > 0 {
···
205
repoAts = append(repoAts, string(issue.RepoAt))
206
}
207
208
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoAts))
209
if err != nil {
210
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
211
}
···
228
// collect comments
229
issueAts := slices.Collect(maps.Keys(issueMap))
230
231
-
comments, err := GetIssueComments(e, FilterIn("issue_at", issueAts))
232
if err != nil {
233
return nil, fmt.Errorf("failed to query comments: %w", err)
234
}
···
240
}
241
242
// collect allLabels for each issue
243
-
allLabels, err := GetLabels(e, FilterIn("subject", issueAts))
244
if err != nil {
245
return nil, fmt.Errorf("failed to query labels: %w", err)
246
}
···
251
}
252
253
// collect references for each issue
254
-
allReferencs, err := GetReferencesAll(e, FilterIn("from_at", issueAts))
255
if err != nil {
256
return nil, fmt.Errorf("failed to query reference_links: %w", err)
257
}
···
277
issues, err := GetIssuesPaginated(
278
e,
279
pagination.Page{},
280
-
FilterEq("repo_at", repoAt),
281
-
FilterEq("issue_id", issueId),
282
)
283
if err != nil {
284
return nil, err
···
290
return &issues[0], nil
291
}
292
293
-
func GetIssues(e Execer, filters ...filter) ([]models.Issue, error) {
294
return GetIssuesPaginated(e, pagination.Page{}, filters...)
295
}
296
···
298
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
299
var ids []int64
300
301
-
var filters []filter
302
openValue := 0
303
if opts.IsOpen {
304
openValue = 1
305
}
306
-
filters = append(filters, FilterEq("open", openValue))
307
if opts.RepoAt != "" {
308
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309
}
310
311
var conditions []string
···
397
return id, nil
398
}
399
400
-
func DeleteIssueComments(e Execer, filters ...filter) error {
401
var conditions []string
402
var args []any
403
for _, filter := range filters {
···
416
return err
417
}
418
419
-
func GetIssueComments(e Execer, filters ...filter) ([]models.IssueComment, error) {
420
commentMap := make(map[string]*models.IssueComment)
421
422
var conditions []string
···
451
if err != nil {
452
return nil, err
453
}
454
455
for rows.Next() {
456
var comment models.IssueComment
···
506
507
// collect references for each comments
508
commentAts := slices.Collect(maps.Keys(commentMap))
509
-
allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts))
510
if err != nil {
511
return nil, fmt.Errorf("failed to query reference_links: %w", err)
512
}
···
548
return nil
549
}
550
551
-
func CloseIssues(e Execer, filters ...filter) error {
552
var conditions []string
553
var args []any
554
for _, filter := range filters {
···
566
return err
567
}
568
569
-
func ReopenIssues(e Execer, filters ...filter) error {
570
var conditions []string
571
var args []any
572
for _, filter := range filters {
···
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/pagination"
16
+
"tangled.org/core/orm"
17
)
18
19
func PutIssue(tx *sql.Tx, issue *models.Issue) error {
···
28
29
issues, err := GetIssues(
30
tx,
31
+
orm.FilterEq("did", issue.Did),
32
+
orm.FilterEq("rkey", issue.Rkey),
33
)
34
switch {
35
case err != nil:
···
99
return nil
100
}
101
102
+
func GetIssuesPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]models.Issue, error) {
103
issueMap := make(map[string]*models.Issue) // at-uri -> issue
104
105
var conditions []string
···
115
whereClause = " where " + strings.Join(conditions, " and ")
116
}
117
118
+
pLower := orm.FilterGte("row_num", page.Offset+1)
119
+
pUpper := orm.FilterLte("row_num", page.Offset+page.Limit)
120
121
pageClause := ""
122
if page.Limit > 0 {
···
206
repoAts = append(repoAts, string(issue.RepoAt))
207
}
208
209
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoAts))
210
if err != nil {
211
return nil, fmt.Errorf("failed to build repo mappings: %w", err)
212
}
···
229
// collect comments
230
issueAts := slices.Collect(maps.Keys(issueMap))
231
232
+
comments, err := GetIssueComments(e, orm.FilterIn("issue_at", issueAts))
233
if err != nil {
234
return nil, fmt.Errorf("failed to query comments: %w", err)
235
}
···
241
}
242
243
// collect allLabels for each issue
244
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", issueAts))
245
if err != nil {
246
return nil, fmt.Errorf("failed to query labels: %w", err)
247
}
···
252
}
253
254
// collect references for each issue
255
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", issueAts))
256
if err != nil {
257
return nil, fmt.Errorf("failed to query reference_links: %w", err)
258
}
···
278
issues, err := GetIssuesPaginated(
279
e,
280
pagination.Page{},
281
+
orm.FilterEq("repo_at", repoAt),
282
+
orm.FilterEq("issue_id", issueId),
283
)
284
if err != nil {
285
return nil, err
···
291
return &issues[0], nil
292
}
293
294
+
func GetIssues(e Execer, filters ...orm.Filter) ([]models.Issue, error) {
295
return GetIssuesPaginated(e, pagination.Page{}, filters...)
296
}
297
···
299
func GetIssueIDs(e Execer, opts models.IssueSearchOptions) ([]int64, error) {
300
var ids []int64
301
302
+
var filters []orm.Filter
303
openValue := 0
304
if opts.IsOpen {
305
openValue = 1
306
}
307
+
filters = append(filters, orm.FilterEq("open", openValue))
308
if opts.RepoAt != "" {
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310
}
311
312
var conditions []string
···
398
return id, nil
399
}
400
401
+
func DeleteIssueComments(e Execer, filters ...orm.Filter) error {
402
var conditions []string
403
var args []any
404
for _, filter := range filters {
···
417
return err
418
}
419
420
+
func GetIssueComments(e Execer, filters ...orm.Filter) ([]models.IssueComment, error) {
421
commentMap := make(map[string]*models.IssueComment)
422
423
var conditions []string
···
452
if err != nil {
453
return nil, err
454
}
455
+
defer rows.Close()
456
457
for rows.Next() {
458
var comment models.IssueComment
···
508
509
// collect references for each comments
510
commentAts := slices.Collect(maps.Keys(commentMap))
511
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
512
if err != nil {
513
return nil, fmt.Errorf("failed to query reference_links: %w", err)
514
}
···
550
return nil
551
}
552
553
+
func CloseIssues(e Execer, filters ...orm.Filter) error {
554
var conditions []string
555
var args []any
556
for _, filter := range filters {
···
568
return err
569
}
570
571
+
func ReopenIssues(e Execer, filters ...orm.Filter) error {
572
var conditions []string
573
var args []any
574
for _, filter := range filters {
+8
-7
appview/db/label.go
+8
-7
appview/db/label.go
···
10
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
)
14
15
// no updating type for now
···
59
return id, nil
60
}
61
62
-
func DeleteLabelDefinition(e Execer, filters ...filter) error {
63
var conditions []string
64
var args []any
65
for _, filter := range filters {
···
75
return err
76
}
77
78
-
func GetLabelDefinitions(e Execer, filters ...filter) ([]models.LabelDefinition, error) {
79
var labelDefinitions []models.LabelDefinition
80
var conditions []string
81
var args []any
···
167
}
168
169
// helper to get exactly one label def
170
-
func GetLabelDefinition(e Execer, filters ...filter) (*models.LabelDefinition, error) {
171
labels, err := GetLabelDefinitions(e, filters...)
172
if err != nil {
173
return nil, err
···
227
return id, nil
228
}
229
230
-
func GetLabelOps(e Execer, filters ...filter) ([]models.LabelOp, error) {
231
var labelOps []models.LabelOp
232
var conditions []string
233
var args []any
···
302
}
303
304
// get labels for a given list of subject URIs
305
-
func GetLabels(e Execer, filters ...filter) (map[syntax.ATURI]models.LabelState, error) {
306
ops, err := GetLabelOps(e, filters...)
307
if err != nil {
308
return nil, err
···
322
}
323
labelAts := slices.Collect(maps.Keys(labelAtSet))
324
325
-
actx, err := NewLabelApplicationCtx(e, FilterIn("at_uri", labelAts))
326
if err != nil {
327
return nil, err
328
}
···
338
return results, nil
339
}
340
341
-
func NewLabelApplicationCtx(e Execer, filters ...filter) (*models.LabelApplicationCtx, error) {
342
labels, err := GetLabelDefinitions(e, filters...)
343
if err != nil {
344
return nil, err
···
10
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
+
"tangled.org/core/orm"
14
)
15
16
// no updating type for now
···
60
return id, nil
61
}
62
63
+
func DeleteLabelDefinition(e Execer, filters ...orm.Filter) error {
64
var conditions []string
65
var args []any
66
for _, filter := range filters {
···
76
return err
77
}
78
79
+
func GetLabelDefinitions(e Execer, filters ...orm.Filter) ([]models.LabelDefinition, error) {
80
var labelDefinitions []models.LabelDefinition
81
var conditions []string
82
var args []any
···
168
}
169
170
// helper to get exactly one label def
171
+
func GetLabelDefinition(e Execer, filters ...orm.Filter) (*models.LabelDefinition, error) {
172
labels, err := GetLabelDefinitions(e, filters...)
173
if err != nil {
174
return nil, err
···
228
return id, nil
229
}
230
231
+
func GetLabelOps(e Execer, filters ...orm.Filter) ([]models.LabelOp, error) {
232
var labelOps []models.LabelOp
233
var conditions []string
234
var args []any
···
303
}
304
305
// get labels for a given list of subject URIs
306
+
func GetLabels(e Execer, filters ...orm.Filter) (map[syntax.ATURI]models.LabelState, error) {
307
ops, err := GetLabelOps(e, filters...)
308
if err != nil {
309
return nil, err
···
323
}
324
labelAts := slices.Collect(maps.Keys(labelAtSet))
325
326
+
actx, err := NewLabelApplicationCtx(e, orm.FilterIn("at_uri", labelAts))
327
if err != nil {
328
return nil, err
329
}
···
339
return results, nil
340
}
341
342
+
func NewLabelApplicationCtx(e Execer, filters ...orm.Filter) (*models.LabelApplicationCtx, error) {
343
labels, err := GetLabelDefinitions(e, filters...)
344
if err != nil {
345
return nil, err
+6
-5
appview/db/language.go
+6
-5
appview/db/language.go
···
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetRepoLanguages(e Execer, filters ...filter) ([]models.RepoLanguage, error) {
13
var conditions []string
14
var args []any
15
for _, filter := range filters {
···
27
whereClause,
28
)
29
rows, err := e.Query(query, args...)
30
-
31
if err != nil {
32
return nil, fmt.Errorf("failed to execute query: %w ", err)
33
}
34
35
var langs []models.RepoLanguage
36
for rows.Next() {
···
85
return nil
86
}
87
88
-
func DeleteRepoLanguages(e Execer, filters ...filter) error {
89
var conditions []string
90
var args []any
91
for _, filter := range filters {
···
107
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
108
err := DeleteRepoLanguages(
109
tx,
110
-
FilterEq("repo_at", repoAt),
111
-
FilterEq("ref", ref),
112
)
113
if err != nil {
114
return fmt.Errorf("failed to delete existing languages: %w", err)
···
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetRepoLanguages(e Execer, filters ...orm.Filter) ([]models.RepoLanguage, error) {
14
var conditions []string
15
var args []any
16
for _, filter := range filters {
···
28
whereClause,
29
)
30
rows, err := e.Query(query, args...)
31
if err != nil {
32
return nil, fmt.Errorf("failed to execute query: %w ", err)
33
}
34
+
defer rows.Close()
35
36
var langs []models.RepoLanguage
37
for rows.Next() {
···
86
return nil
87
}
88
89
+
func DeleteRepoLanguages(e Execer, filters ...orm.Filter) error {
90
var conditions []string
91
var args []any
92
for _, filter := range filters {
···
108
func UpdateRepoLanguages(tx *sql.Tx, repoAt syntax.ATURI, ref string, langs []models.RepoLanguage) error {
109
err := DeleteRepoLanguages(
110
tx,
111
+
orm.FilterEq("repo_at", repoAt),
112
+
orm.FilterEq("ref", ref),
113
)
114
if err != nil {
115
return fmt.Errorf("failed to delete existing languages: %w", err)
+14
-13
appview/db/notifications.go
+14
-13
appview/db/notifications.go
···
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
)
15
16
func CreateNotification(e Execer, notification *models.Notification) error {
···
44
}
45
46
// GetNotificationsPaginated retrieves notifications with filters and pagination
47
-
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...filter) ([]*models.Notification, error) {
48
var conditions []string
49
var args []any
50
···
113
}
114
115
// GetNotificationsWithEntities retrieves notifications with their related entities
116
-
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...filter) ([]*models.NotificationWithEntity, error) {
117
var conditions []string
118
var args []any
119
···
256
}
257
258
// GetNotifications retrieves notifications with filters
259
-
func GetNotifications(e Execer, filters ...filter) ([]*models.Notification, error) {
260
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
261
}
262
263
-
func CountNotifications(e Execer, filters ...filter) (int64, error) {
264
var conditions []string
265
var args []any
266
for _, filter := range filters {
···
285
}
286
287
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
288
-
idFilter := FilterEq("id", notificationID)
289
-
recipientFilter := FilterEq("recipient_did", userDID)
290
291
query := fmt.Sprintf(`
292
UPDATE notifications
···
314
}
315
316
func MarkAllNotificationsRead(e Execer, userDID string) error {
317
-
recipientFilter := FilterEq("recipient_did", userDID)
318
-
readFilter := FilterEq("read", 0)
319
320
query := fmt.Sprintf(`
321
UPDATE notifications
···
334
}
335
336
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
337
-
idFilter := FilterEq("id", notificationID)
338
-
recipientFilter := FilterEq("recipient_did", userDID)
339
340
query := fmt.Sprintf(`
341
DELETE FROM notifications
···
362
}
363
364
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
365
-
prefs, err := GetNotificationPreferences(e, FilterEq("user_did", userDid))
366
if err != nil {
367
return nil, err
368
}
···
375
return p, nil
376
}
377
378
-
func GetNotificationPreferences(e Execer, filters ...filter) (map[syntax.DID]*models.NotificationPreferences, error) {
379
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
380
381
var conditions []string
···
483
484
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
485
cutoff := time.Now().Add(-olderThan)
486
-
createdFilter := FilterLte("created", cutoff)
487
488
query := fmt.Sprintf(`
489
DELETE FROM notifications
···
11
"github.com/bluesky-social/indigo/atproto/syntax"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
)
16
17
func CreateNotification(e Execer, notification *models.Notification) error {
···
45
}
46
47
// GetNotificationsPaginated retrieves notifications with filters and pagination
48
+
func GetNotificationsPaginated(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.Notification, error) {
49
var conditions []string
50
var args []any
51
···
114
}
115
116
// GetNotificationsWithEntities retrieves notifications with their related entities
117
+
func GetNotificationsWithEntities(e Execer, page pagination.Page, filters ...orm.Filter) ([]*models.NotificationWithEntity, error) {
118
var conditions []string
119
var args []any
120
···
257
}
258
259
// GetNotifications retrieves notifications with filters
260
+
func GetNotifications(e Execer, filters ...orm.Filter) ([]*models.Notification, error) {
261
return GetNotificationsPaginated(e, pagination.FirstPage(), filters...)
262
}
263
264
+
func CountNotifications(e Execer, filters ...orm.Filter) (int64, error) {
265
var conditions []string
266
var args []any
267
for _, filter := range filters {
···
286
}
287
288
func MarkNotificationRead(e Execer, notificationID int64, userDID string) error {
289
+
idFilter := orm.FilterEq("id", notificationID)
290
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
291
292
query := fmt.Sprintf(`
293
UPDATE notifications
···
315
}
316
317
func MarkAllNotificationsRead(e Execer, userDID string) error {
318
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
319
+
readFilter := orm.FilterEq("read", 0)
320
321
query := fmt.Sprintf(`
322
UPDATE notifications
···
335
}
336
337
func DeleteNotification(e Execer, notificationID int64, userDID string) error {
338
+
idFilter := orm.FilterEq("id", notificationID)
339
+
recipientFilter := orm.FilterEq("recipient_did", userDID)
340
341
query := fmt.Sprintf(`
342
DELETE FROM notifications
···
363
}
364
365
func GetNotificationPreference(e Execer, userDid string) (*models.NotificationPreferences, error) {
366
+
prefs, err := GetNotificationPreferences(e, orm.FilterEq("user_did", userDid))
367
if err != nil {
368
return nil, err
369
}
···
376
return p, nil
377
}
378
379
+
func GetNotificationPreferences(e Execer, filters ...orm.Filter) (map[syntax.DID]*models.NotificationPreferences, error) {
380
prefsMap := make(map[syntax.DID]*models.NotificationPreferences)
381
382
var conditions []string
···
484
485
func (d *DB) ClearOldNotifications(ctx context.Context, olderThan time.Duration) error {
486
cutoff := time.Now().Add(-olderThan)
487
+
createdFilter := orm.FilterLte("created", cutoff)
488
489
query := fmt.Sprintf(`
490
DELETE FROM notifications
+12
-11
appview/db/pipeline.go
+12
-11
appview/db/pipeline.go
···
6
"strings"
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetPipelines(e Execer, filters ...filter) ([]models.Pipeline, error) {
13
var pipelines []models.Pipeline
14
15
var conditions []string
···
168
169
// this is a mega query, but the most useful one:
170
// get N pipelines, for each one get the latest status of its N workflows
171
-
func GetPipelineStatuses(e Execer, limit int, filters ...filter) ([]models.Pipeline, error) {
172
var conditions []string
173
var args []any
174
for _, filter := range filters {
175
-
filter.key = "p." + filter.key // the table is aliased in the query to `p`
176
conditions = append(conditions, filter.Condition())
177
args = append(args, filter.Arg()...)
178
}
···
215
}
216
defer rows.Close()
217
218
-
pipelines := make(map[string]models.Pipeline)
219
for rows.Next() {
220
var p models.Pipeline
221
var t models.Trigger
···
252
p.Trigger = &t
253
p.Statuses = make(map[string]models.WorkflowStatus)
254
255
-
k := fmt.Sprintf("%s/%s", p.Knot, p.Rkey)
256
-
pipelines[k] = p
257
}
258
259
// get all statuses
···
264
conditions = nil
265
args = nil
266
for _, p := range pipelines {
267
-
knotFilter := FilterEq("pipeline_knot", p.Knot)
268
-
rkeyFilter := FilterEq("pipeline_rkey", p.Rkey)
269
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
270
args = append(args, p.Knot)
271
args = append(args, p.Rkey)
···
313
return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err)
314
}
315
316
-
key := fmt.Sprintf("%s/%s", ps.PipelineKnot, ps.PipelineRkey)
317
318
// extract
319
-
pipeline, ok := pipelines[key]
320
if !ok {
321
continue
322
}
···
330
331
// reassign
332
pipeline.Statuses[ps.Workflow] = statuses
333
-
pipelines[key] = pipeline
334
}
335
336
var all []models.Pipeline
···
6
"strings"
7
"time"
8
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
+
func GetPipelines(e Execer, filters ...orm.Filter) ([]models.Pipeline, error) {
15
var pipelines []models.Pipeline
16
17
var conditions []string
···
170
171
// this is a mega query, but the most useful one:
172
// get N pipelines, for each one get the latest status of its N workflows
173
+
func GetPipelineStatuses(e Execer, limit int, filters ...orm.Filter) ([]models.Pipeline, error) {
174
var conditions []string
175
var args []any
176
for _, filter := range filters {
177
+
filter.Key = "p." + filter.Key // the table is aliased in the query to `p`
178
conditions = append(conditions, filter.Condition())
179
args = append(args, filter.Arg()...)
180
}
···
217
}
218
defer rows.Close()
219
220
+
pipelines := make(map[syntax.ATURI]models.Pipeline)
221
for rows.Next() {
222
var p models.Pipeline
223
var t models.Trigger
···
254
p.Trigger = &t
255
p.Statuses = make(map[string]models.WorkflowStatus)
256
257
+
pipelines[p.AtUri()] = p
258
}
259
260
// get all statuses
···
265
conditions = nil
266
args = nil
267
for _, p := range pipelines {
268
+
knotFilter := orm.FilterEq("pipeline_knot", p.Knot)
269
+
rkeyFilter := orm.FilterEq("pipeline_rkey", p.Rkey)
270
conditions = append(conditions, fmt.Sprintf("(%s and %s)", knotFilter.Condition(), rkeyFilter.Condition()))
271
args = append(args, p.Knot)
272
args = append(args, p.Rkey)
···
314
return nil, fmt.Errorf("invalid status created timestamp %q: %w", created, err)
315
}
316
317
+
pipelineAt := ps.PipelineAt()
318
319
// extract
320
+
pipeline, ok := pipelines[pipelineAt]
321
if !ok {
322
continue
323
}
···
331
332
// reassign
333
pipeline.Statuses[ps.Workflow] = statuses
334
+
pipelines[pipelineAt] = pipeline
335
}
336
337
var all []models.Pipeline
+11
-5
appview/db/profile.go
+11
-5
appview/db/profile.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
const TimeframeMonths = 7
···
44
45
issues, err := GetIssues(
46
e,
47
-
FilterEq("did", forDid),
48
-
FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
49
)
50
if err != nil {
51
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
65
*items = append(*items, &issue)
66
}
67
68
-
repos, err := GetRepos(e, 0, FilterEq("did", forDid))
69
if err != nil {
70
return nil, fmt.Errorf("error getting all repos by did: %w", err)
71
}
···
199
return tx.Commit()
200
}
201
202
-
func GetProfiles(e Execer, filters ...filter) (map[string]*models.Profile, error) {
203
var conditions []string
204
var args []any
205
for _, filter := range filters {
···
229
if err != nil {
230
return nil, err
231
}
232
233
profileMap := make(map[string]*models.Profile)
234
for rows.Next() {
···
269
if err != nil {
270
return nil, err
271
}
272
idxs := make(map[string]int)
273
for did := range profileMap {
274
idxs[did] = 0
···
289
if err != nil {
290
return nil, err
291
}
292
idxs = make(map[string]int)
293
for did := range profileMap {
294
idxs[did] = 0
···
441
}
442
443
// ensure all pinned repos are either own repos or collaborating repos
444
-
repos, err := GetRepos(e, 0, FilterEq("did", profile.Did))
445
if err != nil {
446
log.Printf("getting repos for %s: %s", profile.Did, err)
447
}
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
const TimeframeMonths = 7
···
45
46
issues, err := GetIssues(
47
e,
48
+
orm.FilterEq("did", forDid),
49
+
orm.FilterGte("created", time.Now().AddDate(0, -TimeframeMonths, 0)),
50
)
51
if err != nil {
52
return nil, fmt.Errorf("error getting issues by owner did: %w", err)
···
66
*items = append(*items, &issue)
67
}
68
69
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", forDid))
70
if err != nil {
71
return nil, fmt.Errorf("error getting all repos by did: %w", err)
72
}
···
200
return tx.Commit()
201
}
202
203
+
func GetProfiles(e Execer, filters ...orm.Filter) (map[string]*models.Profile, error) {
204
var conditions []string
205
var args []any
206
for _, filter := range filters {
···
230
if err != nil {
231
return nil, err
232
}
233
+
defer rows.Close()
234
235
profileMap := make(map[string]*models.Profile)
236
for rows.Next() {
···
271
if err != nil {
272
return nil, err
273
}
274
+
defer rows.Close()
275
+
276
idxs := make(map[string]int)
277
for did := range profileMap {
278
idxs[did] = 0
···
293
if err != nil {
294
return nil, err
295
}
296
+
defer rows.Close()
297
+
298
idxs = make(map[string]int)
299
for did := range profileMap {
300
idxs[did] = 0
···
447
}
448
449
// ensure all pinned repos are either own repos or collaborating repos
450
+
repos, err := GetRepos(e, 0, orm.FilterEq("did", profile.Did))
451
if err != nil {
452
log.Printf("getting repos for %s: %s", profile.Did, err)
453
}
+21
-20
appview/db/pulls.go
+21
-20
appview/db/pulls.go
···
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"tangled.org/core/appview/models"
16
)
17
18
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
118
return pullId - 1, err
119
}
120
121
-
func GetPullsWithLimit(e Execer, limit int, filters ...filter) ([]*models.Pull, error) {
122
pulls := make(map[syntax.ATURI]*models.Pull)
123
124
var conditions []string
···
229
for _, p := range pulls {
230
pullAts = append(pullAts, p.AtUri())
231
}
232
-
submissionsMap, err := GetPullSubmissions(e, FilterIn("pull_at", pullAts))
233
if err != nil {
234
return nil, fmt.Errorf("failed to get submissions: %w", err)
235
}
···
241
}
242
243
// collect allLabels for each issue
244
-
allLabels, err := GetLabels(e, FilterIn("subject", pullAts))
245
if err != nil {
246
return nil, fmt.Errorf("failed to query labels: %w", err)
247
}
···
258
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
259
}
260
}
261
-
sourceRepos, err := GetRepos(e, 0, FilterIn("at_uri", sourceAts))
262
if err != nil && !errors.Is(err, sql.ErrNoRows) {
263
return nil, fmt.Errorf("failed to get source repos: %w", err)
264
}
···
274
}
275
}
276
277
-
allReferences, err := GetReferencesAll(e, FilterIn("from_at", pullAts))
278
if err != nil {
279
return nil, fmt.Errorf("failed to query reference_links: %w", err)
280
}
···
295
return orderedByPullId, nil
296
}
297
298
-
func GetPulls(e Execer, filters ...filter) ([]*models.Pull, error) {
299
return GetPullsWithLimit(e, 0, filters...)
300
}
301
302
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
303
var ids []int64
304
305
-
var filters []filter
306
-
filters = append(filters, FilterEq("state", opts.State))
307
if opts.RepoAt != "" {
308
-
filters = append(filters, FilterEq("repo_at", opts.RepoAt))
309
}
310
311
var conditions []string
···
361
}
362
363
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
364
-
pulls, err := GetPullsWithLimit(e, 1, FilterEq("repo_at", repoAt), FilterEq("pull_id", pullId))
365
if err != nil {
366
return nil, err
367
}
···
373
}
374
375
// mapping from pull -> pull submissions
376
-
func GetPullSubmissions(e Execer, filters ...filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
377
var conditions []string
378
var args []any
379
for _, filter := range filters {
···
448
449
// Get comments for all submissions using GetPullComments
450
submissionIds := slices.Collect(maps.Keys(submissionMap))
451
-
comments, err := GetPullComments(e, FilterIn("submission_id", submissionIds))
452
if err != nil {
453
return nil, fmt.Errorf("failed to get pull comments: %w", err)
454
}
···
474
return m, nil
475
}
476
477
-
func GetPullComments(e Execer, filters ...filter) ([]models.PullComment, error) {
478
var conditions []string
479
var args []any
480
for _, filter := range filters {
···
542
543
// collect references for each comments
544
commentAts := slices.Collect(maps.Keys(commentMap))
545
-
allReferencs, err := GetReferencesAll(e, FilterIn("from_at", commentAts))
546
if err != nil {
547
return nil, fmt.Errorf("failed to query reference_links: %w", err)
548
}
···
708
return err
709
}
710
711
-
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...filter) error {
712
var conditions []string
713
var args []any
714
···
732
733
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
734
// otherwise submissions are immutable
735
-
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...filter) error {
736
var conditions []string
737
var args []any
738
···
790
func GetStack(e Execer, stackId string) (models.Stack, error) {
791
unorderedPulls, err := GetPulls(
792
e,
793
-
FilterEq("stack_id", stackId),
794
-
FilterNotEq("state", models.PullDeleted),
795
)
796
if err != nil {
797
return nil, err
···
835
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
836
pulls, err := GetPulls(
837
e,
838
-
FilterEq("stack_id", stackId),
839
-
FilterEq("state", models.PullDeleted),
840
)
841
if err != nil {
842
return nil, err
···
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
15
"tangled.org/core/appview/models"
16
+
"tangled.org/core/orm"
17
)
18
19
func NewPull(tx *sql.Tx, pull *models.Pull) error {
···
119
return pullId - 1, err
120
}
121
122
+
func GetPullsWithLimit(e Execer, limit int, filters ...orm.Filter) ([]*models.Pull, error) {
123
pulls := make(map[syntax.ATURI]*models.Pull)
124
125
var conditions []string
···
230
for _, p := range pulls {
231
pullAts = append(pullAts, p.AtUri())
232
}
233
+
submissionsMap, err := GetPullSubmissions(e, orm.FilterIn("pull_at", pullAts))
234
if err != nil {
235
return nil, fmt.Errorf("failed to get submissions: %w", err)
236
}
···
242
}
243
244
// collect allLabels for each issue
245
+
allLabels, err := GetLabels(e, orm.FilterIn("subject", pullAts))
246
if err != nil {
247
return nil, fmt.Errorf("failed to query labels: %w", err)
248
}
···
259
sourceAts = append(sourceAts, *p.PullSource.RepoAt)
260
}
261
}
262
+
sourceRepos, err := GetRepos(e, 0, orm.FilterIn("at_uri", sourceAts))
263
if err != nil && !errors.Is(err, sql.ErrNoRows) {
264
return nil, fmt.Errorf("failed to get source repos: %w", err)
265
}
···
275
}
276
}
277
278
+
allReferences, err := GetReferencesAll(e, orm.FilterIn("from_at", pullAts))
279
if err != nil {
280
return nil, fmt.Errorf("failed to query reference_links: %w", err)
281
}
···
296
return orderedByPullId, nil
297
}
298
299
+
func GetPulls(e Execer, filters ...orm.Filter) ([]*models.Pull, error) {
300
return GetPullsWithLimit(e, 0, filters...)
301
}
302
303
func GetPullIDs(e Execer, opts models.PullSearchOptions) ([]int64, error) {
304
var ids []int64
305
306
+
var filters []orm.Filter
307
+
filters = append(filters, orm.FilterEq("state", opts.State))
308
if opts.RepoAt != "" {
309
+
filters = append(filters, orm.FilterEq("repo_at", opts.RepoAt))
310
}
311
312
var conditions []string
···
362
}
363
364
func GetPull(e Execer, repoAt syntax.ATURI, pullId int) (*models.Pull, error) {
365
+
pulls, err := GetPullsWithLimit(e, 1, orm.FilterEq("repo_at", repoAt), orm.FilterEq("pull_id", pullId))
366
if err != nil {
367
return nil, err
368
}
···
374
}
375
376
// mapping from pull -> pull submissions
377
+
func GetPullSubmissions(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]*models.PullSubmission, error) {
378
var conditions []string
379
var args []any
380
for _, filter := range filters {
···
449
450
// Get comments for all submissions using GetPullComments
451
submissionIds := slices.Collect(maps.Keys(submissionMap))
452
+
comments, err := GetPullComments(e, orm.FilterIn("submission_id", submissionIds))
453
if err != nil {
454
return nil, fmt.Errorf("failed to get pull comments: %w", err)
455
}
···
475
return m, nil
476
}
477
478
+
func GetPullComments(e Execer, filters ...orm.Filter) ([]models.PullComment, error) {
479
var conditions []string
480
var args []any
481
for _, filter := range filters {
···
543
544
// collect references for each comments
545
commentAts := slices.Collect(maps.Keys(commentMap))
546
+
allReferencs, err := GetReferencesAll(e, orm.FilterIn("from_at", commentAts))
547
if err != nil {
548
return nil, fmt.Errorf("failed to query reference_links: %w", err)
549
}
···
709
return err
710
}
711
712
+
func SetPullParentChangeId(e Execer, parentChangeId string, filters ...orm.Filter) error {
713
var conditions []string
714
var args []any
715
···
733
734
// Only used when stacking to update contents in the event of a rebase (the interdiff should be empty).
735
// otherwise submissions are immutable
736
+
func UpdatePull(e Execer, newPatch, sourceRev string, filters ...orm.Filter) error {
737
var conditions []string
738
var args []any
739
···
791
func GetStack(e Execer, stackId string) (models.Stack, error) {
792
unorderedPulls, err := GetPulls(
793
e,
794
+
orm.FilterEq("stack_id", stackId),
795
+
orm.FilterNotEq("state", models.PullDeleted),
796
)
797
if err != nil {
798
return nil, err
···
836
func GetAbandonedPulls(e Execer, stackId string) ([]*models.Pull, error) {
837
pulls, err := GetPulls(
838
e,
839
+
orm.FilterEq("stack_id", stackId),
840
+
orm.FilterEq("state", models.PullDeleted),
841
)
842
if err != nil {
843
return nil, err
+2
-1
appview/db/punchcard.go
+2
-1
appview/db/punchcard.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
// this adds to the existing count
···
20
return err
21
}
22
23
-
func MakePunchcard(e Execer, filters ...filter) (*models.Punchcard, error) {
24
punchcard := &models.Punchcard{}
25
now := time.Now()
26
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
// this adds to the existing count
···
21
return err
22
}
23
24
+
func MakePunchcard(e Execer, filters ...orm.Filter) (*models.Punchcard, error) {
25
punchcard := &models.Punchcard{}
26
now := time.Now()
27
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
+4
-3
appview/db/reference.go
+4
-3
appview/db/reference.go
···
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/models"
11
)
12
13
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
···
205
return err
206
}
207
208
-
func GetReferencesAll(e Execer, filters ...filter) (map[syntax.ATURI][]syntax.ATURI, error) {
209
var (
210
conditions []string
211
args []any
···
347
if len(aturis) == 0 {
348
return nil, nil
349
}
350
-
filter := FilterIn("c.at_uri", aturis)
351
rows, err := e.Query(
352
fmt.Sprintf(
353
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
···
427
if len(aturis) == 0 {
428
return nil, nil
429
}
430
-
filter := FilterIn("c.comment_at", aturis)
431
rows, err := e.Query(
432
fmt.Sprintf(
433
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
···
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
// ValidateReferenceLinks resolves refLinks to Issue/PR/IssueComment/PullComment ATURIs.
···
206
return err
207
}
208
209
+
func GetReferencesAll(e Execer, filters ...orm.Filter) (map[syntax.ATURI][]syntax.ATURI, error) {
210
var (
211
conditions []string
212
args []any
···
348
if len(aturis) == 0 {
349
return nil, nil
350
}
351
+
filter := orm.FilterIn("c.at_uri", aturis)
352
rows, err := e.Query(
353
fmt.Sprintf(
354
`select r.did, r.name, i.issue_id, c.id, i.title, i.open
···
428
if len(aturis) == 0 {
429
return nil, nil
430
}
431
+
filter := orm.FilterIn("c.comment_at", aturis)
432
rows, err := e.Query(
433
fmt.Sprintf(
434
`select r.did, r.name, p.pull_id, c.id, p.title, p.state
+5
-3
appview/db/registration.go
+5
-3
appview/db/registration.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetRegistrations(e Execer, filters ...filter) ([]models.Registration, error) {
13
var registrations []models.Registration
14
15
var conditions []string
···
37
if err != nil {
38
return nil, err
39
}
40
41
for rows.Next() {
42
var createdAt string
···
69
return registrations, nil
70
}
71
72
-
func MarkRegistered(e Execer, filters ...filter) error {
73
var conditions []string
74
var args []any
75
for _, filter := range filters {
···
94
return err
95
}
96
97
-
func DeleteKnot(e Execer, filters ...filter) error {
98
var conditions []string
99
var args []any
100
for _, filter := range filters {
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetRegistrations(e Execer, filters ...orm.Filter) ([]models.Registration, error) {
14
var registrations []models.Registration
15
16
var conditions []string
···
38
if err != nil {
39
return nil, err
40
}
41
+
defer rows.Close()
42
43
for rows.Next() {
44
var createdAt string
···
71
return registrations, nil
72
}
73
74
+
func MarkRegistered(e Execer, filters ...orm.Filter) error {
75
var conditions []string
76
var args []any
77
for _, filter := range filters {
···
96
return err
97
}
98
99
+
func DeleteKnot(e Execer, filters ...orm.Filter) error {
100
var conditions []string
101
var args []any
102
for _, filter := range filters {
+17
-6
appview/db/repos.go
+17
-6
appview/db/repos.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
-
func GetRepos(e Execer, limit int, filters ...filter) ([]models.Repo, error) {
17
repoMap := make(map[syntax.ATURI]*models.Repo)
18
19
var conditions []string
···
55
limitClause,
56
)
57
rows, err := e.Query(repoQuery, args...)
58
-
59
if err != nil {
60
return nil, fmt.Errorf("failed to execute repo query: %w ", err)
61
}
62
63
for rows.Next() {
64
var repo models.Repo
···
127
if err != nil {
128
return nil, fmt.Errorf("failed to execute labels query: %w ", err)
129
}
130
for rows.Next() {
131
var repoat, labelat string
132
if err := rows.Scan(&repoat, &labelat); err != nil {
···
164
if err != nil {
165
return nil, fmt.Errorf("failed to execute lang query: %w ", err)
166
}
167
for rows.Next() {
168
var repoat, lang string
169
if err := rows.Scan(&repoat, &lang); err != nil {
···
190
if err != nil {
191
return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
192
}
193
for rows.Next() {
194
var repoat string
195
var count int
···
219
if err != nil {
220
return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
221
}
222
for rows.Next() {
223
var repoat string
224
var open, closed int
···
260
if err != nil {
261
return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
262
}
263
for rows.Next() {
264
var repoat string
265
var open, merged, closed, deleted int
···
294
}
295
296
// helper to get exactly one repo
297
-
func GetRepo(e Execer, filters ...filter) (*models.Repo, error) {
298
repos, err := GetRepos(e, 0, filters...)
299
if err != nil {
300
return nil, err
···
311
return &repos[0], nil
312
}
313
314
-
func CountRepos(e Execer, filters ...filter) (int64, error) {
315
var conditions []string
316
var args []any
317
for _, filter := range filters {
···
542
return err
543
}
544
545
-
func UnsubscribeLabel(e Execer, filters ...filter) error {
546
var conditions []string
547
var args []any
548
for _, filter := range filters {
···
560
return err
561
}
562
563
-
func GetRepoLabels(e Execer, filters ...filter) ([]models.RepoLabel, error) {
564
var conditions []string
565
var args []any
566
for _, filter := range filters {
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
+
func GetRepos(e Execer, limit int, filters ...orm.Filter) ([]models.Repo, error) {
18
repoMap := make(map[syntax.ATURI]*models.Repo)
19
20
var conditions []string
···
56
limitClause,
57
)
58
rows, err := e.Query(repoQuery, args...)
59
if err != nil {
60
return nil, fmt.Errorf("failed to execute repo query: %w ", err)
61
}
62
+
defer rows.Close()
63
64
for rows.Next() {
65
var repo models.Repo
···
128
if err != nil {
129
return nil, fmt.Errorf("failed to execute labels query: %w ", err)
130
}
131
+
defer rows.Close()
132
+
133
for rows.Next() {
134
var repoat, labelat string
135
if err := rows.Scan(&repoat, &labelat); err != nil {
···
167
if err != nil {
168
return nil, fmt.Errorf("failed to execute lang query: %w ", err)
169
}
170
+
defer rows.Close()
171
+
172
for rows.Next() {
173
var repoat, lang string
174
if err := rows.Scan(&repoat, &lang); err != nil {
···
195
if err != nil {
196
return nil, fmt.Errorf("failed to execute star-count query: %w ", err)
197
}
198
+
defer rows.Close()
199
+
200
for rows.Next() {
201
var repoat string
202
var count int
···
226
if err != nil {
227
return nil, fmt.Errorf("failed to execute issue-count query: %w ", err)
228
}
229
+
defer rows.Close()
230
+
231
for rows.Next() {
232
var repoat string
233
var open, closed int
···
269
if err != nil {
270
return nil, fmt.Errorf("failed to execute pulls-count query: %w ", err)
271
}
272
+
defer rows.Close()
273
+
274
for rows.Next() {
275
var repoat string
276
var open, merged, closed, deleted int
···
305
}
306
307
// helper to get exactly one repo
308
+
func GetRepo(e Execer, filters ...orm.Filter) (*models.Repo, error) {
309
repos, err := GetRepos(e, 0, filters...)
310
if err != nil {
311
return nil, err
···
322
return &repos[0], nil
323
}
324
325
+
func CountRepos(e Execer, filters ...orm.Filter) (int64, error) {
326
var conditions []string
327
var args []any
328
for _, filter := range filters {
···
553
return err
554
}
555
556
+
func UnsubscribeLabel(e Execer, filters ...orm.Filter) error {
557
var conditions []string
558
var args []any
559
for _, filter := range filters {
···
571
return err
572
}
573
574
+
func GetRepoLabels(e Execer, filters ...orm.Filter) ([]models.RepoLabel, error) {
575
var conditions []string
576
var args []any
577
for _, filter := range filters {
+6
-5
appview/db/spindle.go
+6
-5
appview/db/spindle.go
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
)
11
12
-
func GetSpindles(e Execer, filters ...filter) ([]models.Spindle, error) {
13
var spindles []models.Spindle
14
15
var conditions []string
···
91
return err
92
}
93
94
-
func VerifySpindle(e Execer, filters ...filter) (int64, error) {
95
var conditions []string
96
var args []any
97
for _, filter := range filters {
···
114
return res.RowsAffected()
115
}
116
117
-
func DeleteSpindle(e Execer, filters ...filter) error {
118
var conditions []string
119
var args []any
120
for _, filter := range filters {
···
144
return err
145
}
146
147
-
func RemoveSpindleMember(e Execer, filters ...filter) error {
148
var conditions []string
149
var args []any
150
for _, filter := range filters {
···
163
return err
164
}
165
166
-
func GetSpindleMembers(e Execer, filters ...filter) ([]models.SpindleMember, error) {
167
var members []models.SpindleMember
168
169
var conditions []string
···
7
"time"
8
9
"tangled.org/core/appview/models"
10
+
"tangled.org/core/orm"
11
)
12
13
+
func GetSpindles(e Execer, filters ...orm.Filter) ([]models.Spindle, error) {
14
var spindles []models.Spindle
15
16
var conditions []string
···
92
return err
93
}
94
95
+
func VerifySpindle(e Execer, filters ...orm.Filter) (int64, error) {
96
var conditions []string
97
var args []any
98
for _, filter := range filters {
···
115
return res.RowsAffected()
116
}
117
118
+
func DeleteSpindle(e Execer, filters ...orm.Filter) error {
119
var conditions []string
120
var args []any
121
for _, filter := range filters {
···
145
return err
146
}
147
148
+
func RemoveSpindleMember(e Execer, filters ...orm.Filter) error {
149
var conditions []string
150
var args []any
151
for _, filter := range filters {
···
164
return err
165
}
166
167
+
func GetSpindleMembers(e Execer, filters ...orm.Filter) ([]models.SpindleMember, error) {
168
var members []models.SpindleMember
169
170
var conditions []string
+6
-4
appview/db/star.go
+6
-4
appview/db/star.go
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
)
15
16
func AddStar(e Execer, star *models.Star) error {
···
133
134
// GetRepoStars return a list of stars each holding target repository.
135
// If there isn't known repo with starred at-uri, those stars will be ignored.
136
-
func GetRepoStars(e Execer, limit int, filters ...filter) ([]models.RepoStar, error) {
137
var conditions []string
138
var args []any
139
for _, filter := range filters {
···
164
if err != nil {
165
return nil, err
166
}
167
168
starMap := make(map[string][]models.Star)
169
for rows.Next() {
···
195
return nil, nil
196
}
197
198
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", args))
199
if err != nil {
200
return nil, err
201
}
···
225
return repoStars, nil
226
}
227
228
-
func CountStars(e Execer, filters ...filter) (int64, error) {
229
var conditions []string
230
var args []any
231
for _, filter := range filters {
···
298
}
299
300
// get full repo data
301
-
repos, err := GetRepos(e, 0, FilterIn("at_uri", repoUris))
302
if err != nil {
303
return nil, err
304
}
···
11
12
"github.com/bluesky-social/indigo/atproto/syntax"
13
"tangled.org/core/appview/models"
14
+
"tangled.org/core/orm"
15
)
16
17
func AddStar(e Execer, star *models.Star) error {
···
134
135
// GetRepoStars return a list of stars each holding target repository.
136
// If there isn't known repo with starred at-uri, those stars will be ignored.
137
+
func GetRepoStars(e Execer, limit int, filters ...orm.Filter) ([]models.RepoStar, error) {
138
var conditions []string
139
var args []any
140
for _, filter := range filters {
···
165
if err != nil {
166
return nil, err
167
}
168
+
defer rows.Close()
169
170
starMap := make(map[string][]models.Star)
171
for rows.Next() {
···
197
return nil, nil
198
}
199
200
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", args))
201
if err != nil {
202
return nil, err
203
}
···
227
return repoStars, nil
228
}
229
230
+
func CountStars(e Execer, filters ...orm.Filter) (int64, error) {
231
var conditions []string
232
var args []any
233
for _, filter := range filters {
···
300
}
301
302
// get full repo data
303
+
repos, err := GetRepos(e, 0, orm.FilterIn("at_uri", repoUris))
304
if err != nil {
305
return nil, err
306
}
+4
-3
appview/db/strings.go
+4
-3
appview/db/strings.go
···
8
"time"
9
10
"tangled.org/core/appview/models"
11
)
12
13
func AddString(e Execer, s models.String) error {
···
44
return err
45
}
46
47
-
func GetStrings(e Execer, limit int, filters ...filter) ([]models.String, error) {
48
var all []models.String
49
50
var conditions []string
···
127
return all, nil
128
}
129
130
-
func CountStrings(e Execer, filters ...filter) (int64, error) {
131
var conditions []string
132
var args []any
133
for _, filter := range filters {
···
151
return count, nil
152
}
153
154
-
func DeleteString(e Execer, filters ...filter) error {
155
var conditions []string
156
var args []any
157
for _, filter := range filters {
···
8
"time"
9
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
)
13
14
func AddString(e Execer, s models.String) error {
···
45
return err
46
}
47
48
+
func GetStrings(e Execer, limit int, filters ...orm.Filter) ([]models.String, error) {
49
var all []models.String
50
51
var conditions []string
···
128
return all, nil
129
}
130
131
+
func CountStrings(e Execer, filters ...orm.Filter) (int64, error) {
132
var conditions []string
133
var args []any
134
for _, filter := range filters {
···
152
return count, nil
153
}
154
155
+
func DeleteString(e Execer, filters ...orm.Filter) error {
156
var conditions []string
157
var args []any
158
for _, filter := range filters {
+9
-8
appview/db/timeline.go
+9
-8
appview/db/timeline.go
···
5
6
"github.com/bluesky-social/indigo/atproto/syntax"
7
"tangled.org/core/appview/models"
8
)
9
10
// TODO: this gathers heterogenous events from different sources and aggregates
···
84
}
85
86
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
87
-
filters := make([]filter, 0)
88
if userIsFollowing != nil {
89
-
filters = append(filters, FilterIn("did", userIsFollowing))
90
}
91
92
repos, err := GetRepos(e, limit, filters...)
···
104
105
var origRepos []models.Repo
106
if args != nil {
107
-
origRepos, err = GetRepos(e, 0, FilterIn("at_uri", args))
108
}
109
if err != nil {
110
return nil, err
···
144
}
145
146
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
147
-
filters := make([]filter, 0)
148
if userIsFollowing != nil {
149
-
filters = append(filters, FilterIn("did", userIsFollowing))
150
}
151
152
stars, err := GetRepoStars(e, limit, filters...)
···
180
}
181
182
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
183
-
filters := make([]filter, 0)
184
if userIsFollowing != nil {
185
-
filters = append(filters, FilterIn("user_did", userIsFollowing))
186
}
187
188
follows, err := GetFollows(e, limit, filters...)
···
199
return nil, nil
200
}
201
202
-
profiles, err := GetProfiles(e, FilterIn("did", subjects))
203
if err != nil {
204
return nil, err
205
}
···
5
6
"github.com/bluesky-social/indigo/atproto/syntax"
7
"tangled.org/core/appview/models"
8
+
"tangled.org/core/orm"
9
)
10
11
// TODO: this gathers heterogenous events from different sources and aggregates
···
85
}
86
87
func getTimelineRepos(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
88
+
filters := make([]orm.Filter, 0)
89
if userIsFollowing != nil {
90
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
91
}
92
93
repos, err := GetRepos(e, limit, filters...)
···
105
106
var origRepos []models.Repo
107
if args != nil {
108
+
origRepos, err = GetRepos(e, 0, orm.FilterIn("at_uri", args))
109
}
110
if err != nil {
111
return nil, err
···
145
}
146
147
func getTimelineStars(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
148
+
filters := make([]orm.Filter, 0)
149
if userIsFollowing != nil {
150
+
filters = append(filters, orm.FilterIn("did", userIsFollowing))
151
}
152
153
stars, err := GetRepoStars(e, limit, filters...)
···
181
}
182
183
func getTimelineFollows(e Execer, limit int, loggedInUserDid string, userIsFollowing []string) ([]models.TimelineEvent, error) {
184
+
filters := make([]orm.Filter, 0)
185
if userIsFollowing != nil {
186
+
filters = append(filters, orm.FilterIn("user_did", userIsFollowing))
187
}
188
189
follows, err := GetFollows(e, limit, filters...)
···
200
return nil, nil
201
}
202
203
+
profiles, err := GetProfiles(e, orm.FilterIn("did", subjects))
204
if err != nil {
205
return nil, err
206
}
+25
-24
appview/ingester.go
+25
-24
appview/ingester.go
···
21
"tangled.org/core/appview/serververify"
22
"tangled.org/core/appview/validator"
23
"tangled.org/core/idresolver"
24
"tangled.org/core/rbac"
25
)
26
···
253
254
err = db.AddArtifact(i.Db, artifact)
255
case jmodels.CommitOperationDelete:
256
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
257
}
258
259
if err != nil {
···
350
351
err = db.UpsertProfile(tx, &profile)
352
case jmodels.CommitOperationDelete:
353
-
err = db.DeleteArtifact(i.Db, db.FilterEq("did", did), db.FilterEq("rkey", e.Commit.RKey))
354
}
355
356
if err != nil {
···
424
// get record from db first
425
members, err := db.GetSpindleMembers(
426
ddb,
427
-
db.FilterEq("did", did),
428
-
db.FilterEq("rkey", rkey),
429
)
430
if err != nil || len(members) != 1 {
431
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
440
// remove record by rkey && update enforcer
441
if err = db.RemoveSpindleMember(
442
tx,
443
-
db.FilterEq("did", did),
444
-
db.FilterEq("rkey", rkey),
445
); err != nil {
446
return fmt.Errorf("failed to remove from db: %w", err)
447
}
···
523
// get record from db first
524
spindles, err := db.GetSpindles(
525
ddb,
526
-
db.FilterEq("owner", did),
527
-
db.FilterEq("instance", instance),
528
)
529
if err != nil || len(spindles) != 1 {
530
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
543
// remove spindle members first
544
err = db.RemoveSpindleMember(
545
tx,
546
-
db.FilterEq("owner", did),
547
-
db.FilterEq("instance", instance),
548
)
549
if err != nil {
550
return err
···
552
553
err = db.DeleteSpindle(
554
tx,
555
-
db.FilterEq("owner", did),
556
-
db.FilterEq("instance", instance),
557
)
558
if err != nil {
559
return err
···
621
case jmodels.CommitOperationDelete:
622
if err := db.DeleteString(
623
ddb,
624
-
db.FilterEq("did", did),
625
-
db.FilterEq("rkey", rkey),
626
); err != nil {
627
l.Error("failed to delete", "err", err)
628
return fmt.Errorf("failed to delete string record: %w", err)
···
740
// get record from db first
741
registrations, err := db.GetRegistrations(
742
ddb,
743
-
db.FilterEq("domain", domain),
744
-
db.FilterEq("did", did),
745
)
746
if err != nil {
747
return fmt.Errorf("failed to get registration: %w", err)
···
762
763
err = db.DeleteKnot(
764
tx,
765
-
db.FilterEq("did", did),
766
-
db.FilterEq("domain", domain),
767
)
768
if err != nil {
769
return err
···
915
case jmodels.CommitOperationDelete:
916
if err := db.DeleteIssueComments(
917
ddb,
918
-
db.FilterEq("did", did),
919
-
db.FilterEq("rkey", rkey),
920
); err != nil {
921
return fmt.Errorf("failed to delete issue comment record: %w", err)
922
}
···
969
case jmodels.CommitOperationDelete:
970
if err := db.DeleteLabelDefinition(
971
ddb,
972
-
db.FilterEq("did", did),
973
-
db.FilterEq("rkey", rkey),
974
); err != nil {
975
return fmt.Errorf("failed to delete labeldef record: %w", err)
976
}
···
1010
var repo *models.Repo
1011
switch collection {
1012
case tangled.RepoIssueNSID:
1013
-
i, err := db.GetIssues(ddb, db.FilterEq("at_uri", subject))
1014
if err != nil || len(i) != 1 {
1015
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
1016
}
···
1019
return fmt.Errorf("unsupport label subject: %s", collection)
1020
}
1021
1022
-
actx, err := db.NewLabelApplicationCtx(ddb, db.FilterIn("at_uri", repo.Labels))
1023
if err != nil {
1024
return fmt.Errorf("failed to build label application ctx: %w", err)
1025
}
···
21
"tangled.org/core/appview/serververify"
22
"tangled.org/core/appview/validator"
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
25
"tangled.org/core/rbac"
26
)
27
···
254
255
err = db.AddArtifact(i.Db, artifact)
256
case jmodels.CommitOperationDelete:
257
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
258
}
259
260
if err != nil {
···
351
352
err = db.UpsertProfile(tx, &profile)
353
case jmodels.CommitOperationDelete:
354
+
err = db.DeleteArtifact(i.Db, orm.FilterEq("did", did), orm.FilterEq("rkey", e.Commit.RKey))
355
}
356
357
if err != nil {
···
425
// get record from db first
426
members, err := db.GetSpindleMembers(
427
ddb,
428
+
orm.FilterEq("did", did),
429
+
orm.FilterEq("rkey", rkey),
430
)
431
if err != nil || len(members) != 1 {
432
return fmt.Errorf("failed to get member: %w, len(members) = %d", err, len(members))
···
441
// remove record by rkey && update enforcer
442
if err = db.RemoveSpindleMember(
443
tx,
444
+
orm.FilterEq("did", did),
445
+
orm.FilterEq("rkey", rkey),
446
); err != nil {
447
return fmt.Errorf("failed to remove from db: %w", err)
448
}
···
524
// get record from db first
525
spindles, err := db.GetSpindles(
526
ddb,
527
+
orm.FilterEq("owner", did),
528
+
orm.FilterEq("instance", instance),
529
)
530
if err != nil || len(spindles) != 1 {
531
return fmt.Errorf("failed to get spindles: %w, len(spindles) = %d", err, len(spindles))
···
544
// remove spindle members first
545
err = db.RemoveSpindleMember(
546
tx,
547
+
orm.FilterEq("owner", did),
548
+
orm.FilterEq("instance", instance),
549
)
550
if err != nil {
551
return err
···
553
554
err = db.DeleteSpindle(
555
tx,
556
+
orm.FilterEq("owner", did),
557
+
orm.FilterEq("instance", instance),
558
)
559
if err != nil {
560
return err
···
622
case jmodels.CommitOperationDelete:
623
if err := db.DeleteString(
624
ddb,
625
+
orm.FilterEq("did", did),
626
+
orm.FilterEq("rkey", rkey),
627
); err != nil {
628
l.Error("failed to delete", "err", err)
629
return fmt.Errorf("failed to delete string record: %w", err)
···
741
// get record from db first
742
registrations, err := db.GetRegistrations(
743
ddb,
744
+
orm.FilterEq("domain", domain),
745
+
orm.FilterEq("did", did),
746
)
747
if err != nil {
748
return fmt.Errorf("failed to get registration: %w", err)
···
763
764
err = db.DeleteKnot(
765
tx,
766
+
orm.FilterEq("did", did),
767
+
orm.FilterEq("domain", domain),
768
)
769
if err != nil {
770
return err
···
916
case jmodels.CommitOperationDelete:
917
if err := db.DeleteIssueComments(
918
ddb,
919
+
orm.FilterEq("did", did),
920
+
orm.FilterEq("rkey", rkey),
921
); err != nil {
922
return fmt.Errorf("failed to delete issue comment record: %w", err)
923
}
···
970
case jmodels.CommitOperationDelete:
971
if err := db.DeleteLabelDefinition(
972
ddb,
973
+
orm.FilterEq("did", did),
974
+
orm.FilterEq("rkey", rkey),
975
); err != nil {
976
return fmt.Errorf("failed to delete labeldef record: %w", err)
977
}
···
1011
var repo *models.Repo
1012
switch collection {
1013
case tangled.RepoIssueNSID:
1014
+
i, err := db.GetIssues(ddb, orm.FilterEq("at_uri", subject))
1015
if err != nil || len(i) != 1 {
1016
return fmt.Errorf("failed to find subject: %w || subject count %d", err, len(i))
1017
}
···
1020
return fmt.Errorf("unsupport label subject: %s", collection)
1021
}
1022
1023
+
actx, err := db.NewLabelApplicationCtx(ddb, orm.FilterIn("at_uri", repo.Labels))
1024
if err != nil {
1025
return fmt.Errorf("failed to build label application ctx: %w", err)
1026
}
+46
-45
appview/issues/issues.go
+46
-45
appview/issues/issues.go
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
issues_indexer "tangled.org/core/appview/indexer/issues"
22
"tangled.org/core/appview/models"
23
"tangled.org/core/appview/notify"
24
"tangled.org/core/appview/oauth"
25
"tangled.org/core/appview/pages"
26
"tangled.org/core/appview/pages/repoinfo"
27
"tangled.org/core/appview/pagination"
28
-
"tangled.org/core/appview/refresolver"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/idresolver"
32
"tangled.org/core/rbac"
33
"tangled.org/core/tid"
34
)
35
36
type Issues struct {
37
-
oauth *oauth.OAuth
38
-
repoResolver *reporesolver.RepoResolver
39
-
enforcer *rbac.Enforcer
40
-
pages *pages.Pages
41
-
idResolver *idresolver.Resolver
42
-
refResolver *refresolver.Resolver
43
-
db *db.DB
44
-
config *config.Config
45
-
notifier notify.Notifier
46
-
logger *slog.Logger
47
-
validator *validator.Validator
48
-
indexer *issues_indexer.Indexer
49
}
50
51
func New(
···
54
enforcer *rbac.Enforcer,
55
pages *pages.Pages,
56
idResolver *idresolver.Resolver,
57
-
refResolver *refresolver.Resolver,
58
db *db.DB,
59
config *config.Config,
60
notifier notify.Notifier,
···
63
logger *slog.Logger,
64
) *Issues {
65
return &Issues{
66
-
oauth: oauth,
67
-
repoResolver: repoResolver,
68
-
enforcer: enforcer,
69
-
pages: pages,
70
-
idResolver: idResolver,
71
-
refResolver: refResolver,
72
-
db: db,
73
-
config: config,
74
-
notifier: notifier,
75
-
logger: logger,
76
-
validator: validator,
77
-
indexer: indexer,
78
}
79
}
80
···
113
114
labelDefs, err := db.GetLabelDefinitions(
115
rp.db,
116
-
db.FilterIn("at_uri", f.Labels),
117
-
db.FilterContains("scope", tangled.RepoIssueNSID),
118
)
119
if err != nil {
120
l.Error("failed to fetch labels", "err", err)
···
163
newIssue := issue
164
newIssue.Title = r.FormValue("title")
165
newIssue.Body = r.FormValue("body")
166
-
newIssue.Mentions, newIssue.References = rp.refResolver.Resolve(r.Context(), newIssue.Body)
167
168
if err := rp.validator.ValidateIssue(newIssue); err != nil {
169
l.Error("validation error", "err", err)
···
314
if isIssueOwner || isRepoOwner || isCollaborator {
315
err = db.CloseIssues(
316
rp.db,
317
-
db.FilterEq("id", issue.Id),
318
)
319
if err != nil {
320
l.Error("failed to close issue", "err", err)
···
361
if isCollaborator || isRepoOwner || isIssueOwner {
362
err := db.ReopenIssues(
363
rp.db,
364
-
db.FilterEq("id", issue.Id),
365
)
366
if err != nil {
367
l.Error("failed to reopen issue", "err", err)
···
412
replyTo = &replyToUri
413
}
414
415
-
mentions, references := rp.refResolver.Resolve(r.Context(), body)
416
417
comment := models.IssueComment{
418
Did: user.Did,
···
506
commentId := chi.URLParam(r, "commentId")
507
comments, err := db.GetIssueComments(
508
rp.db,
509
-
db.FilterEq("id", commentId),
510
)
511
if err != nil {
512
l.Error("failed to fetch comment", "id", commentId)
···
542
commentId := chi.URLParam(r, "commentId")
543
comments, err := db.GetIssueComments(
544
rp.db,
545
-
db.FilterEq("id", commentId),
546
)
547
if err != nil {
548
l.Error("failed to fetch comment", "id", commentId)
···
584
newComment := comment
585
newComment.Body = newBody
586
newComment.Edited = &now
587
-
newComment.Mentions, newComment.References = rp.refResolver.Resolve(r.Context(), newBody)
588
589
record := newComment.AsRecord()
590
···
652
commentId := chi.URLParam(r, "commentId")
653
comments, err := db.GetIssueComments(
654
rp.db,
655
-
db.FilterEq("id", commentId),
656
)
657
if err != nil {
658
l.Error("failed to fetch comment", "id", commentId)
···
688
commentId := chi.URLParam(r, "commentId")
689
comments, err := db.GetIssueComments(
690
rp.db,
691
-
db.FilterEq("id", commentId),
692
)
693
if err != nil {
694
l.Error("failed to fetch comment", "id", commentId)
···
724
commentId := chi.URLParam(r, "commentId")
725
comments, err := db.GetIssueComments(
726
rp.db,
727
-
db.FilterEq("id", commentId),
728
)
729
if err != nil {
730
l.Error("failed to fetch comment", "id", commentId)
···
751
752
// optimistic deletion
753
deleted := time.Now()
754
-
err = db.DeleteIssueComments(rp.db, db.FilterEq("id", comment.Id))
755
if err != nil {
756
l.Error("failed to delete comment", "err", err)
757
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
840
841
issues, err = db.GetIssues(
842
rp.db,
843
-
db.FilterIn("id", res.Hits),
844
)
845
if err != nil {
846
l.Error("failed to get issues", "err", err)
···
856
issues, err = db.GetIssuesPaginated(
857
rp.db,
858
page,
859
-
db.FilterEq("repo_at", f.RepoAt()),
860
-
db.FilterEq("open", openInt),
861
)
862
if err != nil {
863
l.Error("failed to get issues", "err", err)
···
868
869
labelDefs, err := db.GetLabelDefinitions(
870
rp.db,
871
-
db.FilterIn("at_uri", f.Labels),
872
-
db.FilterContains("scope", tangled.RepoIssueNSID),
873
)
874
if err != nil {
875
l.Error("failed to fetch labels", "err", err)
···
912
})
913
case http.MethodPost:
914
body := r.FormValue("body")
915
-
mentions, references := rp.refResolver.Resolve(r.Context(), body)
916
917
issue := &models.Issue{
918
RepoAt: f.RepoAt(),
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
issues_indexer "tangled.org/core/appview/indexer/issues"
22
+
"tangled.org/core/appview/mentions"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/notify"
25
"tangled.org/core/appview/oauth"
26
"tangled.org/core/appview/pages"
27
"tangled.org/core/appview/pages/repoinfo"
28
"tangled.org/core/appview/pagination"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/idresolver"
32
+
"tangled.org/core/orm"
33
"tangled.org/core/rbac"
34
"tangled.org/core/tid"
35
)
36
37
type Issues struct {
38
+
oauth *oauth.OAuth
39
+
repoResolver *reporesolver.RepoResolver
40
+
enforcer *rbac.Enforcer
41
+
pages *pages.Pages
42
+
idResolver *idresolver.Resolver
43
+
mentionsResolver *mentions.Resolver
44
+
db *db.DB
45
+
config *config.Config
46
+
notifier notify.Notifier
47
+
logger *slog.Logger
48
+
validator *validator.Validator
49
+
indexer *issues_indexer.Indexer
50
}
51
52
func New(
···
55
enforcer *rbac.Enforcer,
56
pages *pages.Pages,
57
idResolver *idresolver.Resolver,
58
+
mentionsResolver *mentions.Resolver,
59
db *db.DB,
60
config *config.Config,
61
notifier notify.Notifier,
···
64
logger *slog.Logger,
65
) *Issues {
66
return &Issues{
67
+
oauth: oauth,
68
+
repoResolver: repoResolver,
69
+
enforcer: enforcer,
70
+
pages: pages,
71
+
idResolver: idResolver,
72
+
mentionsResolver: mentionsResolver,
73
+
db: db,
74
+
config: config,
75
+
notifier: notifier,
76
+
logger: logger,
77
+
validator: validator,
78
+
indexer: indexer,
79
}
80
}
81
···
114
115
labelDefs, err := db.GetLabelDefinitions(
116
rp.db,
117
+
orm.FilterIn("at_uri", f.Labels),
118
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
119
)
120
if err != nil {
121
l.Error("failed to fetch labels", "err", err)
···
164
newIssue := issue
165
newIssue.Title = r.FormValue("title")
166
newIssue.Body = r.FormValue("body")
167
+
newIssue.Mentions, newIssue.References = rp.mentionsResolver.Resolve(r.Context(), newIssue.Body)
168
169
if err := rp.validator.ValidateIssue(newIssue); err != nil {
170
l.Error("validation error", "err", err)
···
315
if isIssueOwner || isRepoOwner || isCollaborator {
316
err = db.CloseIssues(
317
rp.db,
318
+
orm.FilterEq("id", issue.Id),
319
)
320
if err != nil {
321
l.Error("failed to close issue", "err", err)
···
362
if isCollaborator || isRepoOwner || isIssueOwner {
363
err := db.ReopenIssues(
364
rp.db,
365
+
orm.FilterEq("id", issue.Id),
366
)
367
if err != nil {
368
l.Error("failed to reopen issue", "err", err)
···
413
replyTo = &replyToUri
414
}
415
416
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
417
418
comment := models.IssueComment{
419
Did: user.Did,
···
507
commentId := chi.URLParam(r, "commentId")
508
comments, err := db.GetIssueComments(
509
rp.db,
510
+
orm.FilterEq("id", commentId),
511
)
512
if err != nil {
513
l.Error("failed to fetch comment", "id", commentId)
···
543
commentId := chi.URLParam(r, "commentId")
544
comments, err := db.GetIssueComments(
545
rp.db,
546
+
orm.FilterEq("id", commentId),
547
)
548
if err != nil {
549
l.Error("failed to fetch comment", "id", commentId)
···
585
newComment := comment
586
newComment.Body = newBody
587
newComment.Edited = &now
588
+
newComment.Mentions, newComment.References = rp.mentionsResolver.Resolve(r.Context(), newBody)
589
590
record := newComment.AsRecord()
591
···
653
commentId := chi.URLParam(r, "commentId")
654
comments, err := db.GetIssueComments(
655
rp.db,
656
+
orm.FilterEq("id", commentId),
657
)
658
if err != nil {
659
l.Error("failed to fetch comment", "id", commentId)
···
689
commentId := chi.URLParam(r, "commentId")
690
comments, err := db.GetIssueComments(
691
rp.db,
692
+
orm.FilterEq("id", commentId),
693
)
694
if err != nil {
695
l.Error("failed to fetch comment", "id", commentId)
···
725
commentId := chi.URLParam(r, "commentId")
726
comments, err := db.GetIssueComments(
727
rp.db,
728
+
orm.FilterEq("id", commentId),
729
)
730
if err != nil {
731
l.Error("failed to fetch comment", "id", commentId)
···
752
753
// optimistic deletion
754
deleted := time.Now()
755
+
err = db.DeleteIssueComments(rp.db, orm.FilterEq("id", comment.Id))
756
if err != nil {
757
l.Error("failed to delete comment", "err", err)
758
rp.pages.Notice(w, fmt.Sprintf("comment-%s-status", commentId), "failed to delete comment")
···
841
842
issues, err = db.GetIssues(
843
rp.db,
844
+
orm.FilterIn("id", res.Hits),
845
)
846
if err != nil {
847
l.Error("failed to get issues", "err", err)
···
857
issues, err = db.GetIssuesPaginated(
858
rp.db,
859
page,
860
+
orm.FilterEq("repo_at", f.RepoAt()),
861
+
orm.FilterEq("open", openInt),
862
)
863
if err != nil {
864
l.Error("failed to get issues", "err", err)
···
869
870
labelDefs, err := db.GetLabelDefinitions(
871
rp.db,
872
+
orm.FilterIn("at_uri", f.Labels),
873
+
orm.FilterContains("scope", tangled.RepoIssueNSID),
874
)
875
if err != nil {
876
l.Error("failed to fetch labels", "err", err)
···
913
})
914
case http.MethodPost:
915
body := r.FormValue("body")
916
+
mentions, references := rp.mentionsResolver.Resolve(r.Context(), body)
917
918
issue := &models.Issue{
919
RepoAt: f.RepoAt(),
+19
-18
appview/knots/knots.go
+19
-18
appview/knots/knots.go
···
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/eventconsumer"
23
"tangled.org/core/idresolver"
24
"tangled.org/core/rbac"
25
"tangled.org/core/tid"
26
···
72
user := k.OAuth.GetUser(r)
73
registrations, err := db.GetRegistrations(
74
k.Db,
75
-
db.FilterEq("did", user.Did),
76
)
77
if err != nil {
78
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
102
103
registrations, err := db.GetRegistrations(
104
k.Db,
105
-
db.FilterEq("did", user.Did),
106
-
db.FilterEq("domain", domain),
107
)
108
if err != nil {
109
l.Error("failed to get registrations", "err", err)
···
127
repos, err := db.GetRepos(
128
k.Db,
129
0,
130
-
db.FilterEq("knot", domain),
131
)
132
if err != nil {
133
l.Error("failed to get knot repos", "err", err)
···
293
// get record from db first
294
registrations, err := db.GetRegistrations(
295
k.Db,
296
-
db.FilterEq("did", user.Did),
297
-
db.FilterEq("domain", domain),
298
)
299
if err != nil {
300
l.Error("failed to get registration", "err", err)
···
321
322
err = db.DeleteKnot(
323
tx,
324
-
db.FilterEq("did", user.Did),
325
-
db.FilterEq("domain", domain),
326
)
327
if err != nil {
328
l.Error("failed to delete registration", "err", err)
···
402
// get record from db first
403
registrations, err := db.GetRegistrations(
404
k.Db,
405
-
db.FilterEq("did", user.Did),
406
-
db.FilterEq("domain", domain),
407
)
408
if err != nil {
409
l.Error("failed to get registration", "err", err)
···
493
// Get updated registration to show
494
registrations, err = db.GetRegistrations(
495
k.Db,
496
-
db.FilterEq("did", user.Did),
497
-
db.FilterEq("domain", domain),
498
)
499
if err != nil {
500
l.Error("failed to get registration", "err", err)
···
529
530
registrations, err := db.GetRegistrations(
531
k.Db,
532
-
db.FilterEq("did", user.Did),
533
-
db.FilterEq("domain", domain),
534
-
db.FilterIsNot("registered", "null"),
535
)
536
if err != nil {
537
l.Error("failed to get registration", "err", err)
···
637
638
registrations, err := db.GetRegistrations(
639
k.Db,
640
-
db.FilterEq("did", user.Did),
641
-
db.FilterEq("domain", domain),
642
-
db.FilterIsNot("registered", "null"),
643
)
644
if err != nil {
645
l.Error("failed to get registration", "err", err)
···
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/eventconsumer"
23
"tangled.org/core/idresolver"
24
+
"tangled.org/core/orm"
25
"tangled.org/core/rbac"
26
"tangled.org/core/tid"
27
···
73
user := k.OAuth.GetUser(r)
74
registrations, err := db.GetRegistrations(
75
k.Db,
76
+
orm.FilterEq("did", user.Did),
77
)
78
if err != nil {
79
k.Logger.Error("failed to fetch knot registrations", "err", err)
···
103
104
registrations, err := db.GetRegistrations(
105
k.Db,
106
+
orm.FilterEq("did", user.Did),
107
+
orm.FilterEq("domain", domain),
108
)
109
if err != nil {
110
l.Error("failed to get registrations", "err", err)
···
128
repos, err := db.GetRepos(
129
k.Db,
130
0,
131
+
orm.FilterEq("knot", domain),
132
)
133
if err != nil {
134
l.Error("failed to get knot repos", "err", err)
···
294
// get record from db first
295
registrations, err := db.GetRegistrations(
296
k.Db,
297
+
orm.FilterEq("did", user.Did),
298
+
orm.FilterEq("domain", domain),
299
)
300
if err != nil {
301
l.Error("failed to get registration", "err", err)
···
322
323
err = db.DeleteKnot(
324
tx,
325
+
orm.FilterEq("did", user.Did),
326
+
orm.FilterEq("domain", domain),
327
)
328
if err != nil {
329
l.Error("failed to delete registration", "err", err)
···
403
// get record from db first
404
registrations, err := db.GetRegistrations(
405
k.Db,
406
+
orm.FilterEq("did", user.Did),
407
+
orm.FilterEq("domain", domain),
408
)
409
if err != nil {
410
l.Error("failed to get registration", "err", err)
···
494
// Get updated registration to show
495
registrations, err = db.GetRegistrations(
496
k.Db,
497
+
orm.FilterEq("did", user.Did),
498
+
orm.FilterEq("domain", domain),
499
)
500
if err != nil {
501
l.Error("failed to get registration", "err", err)
···
530
531
registrations, err := db.GetRegistrations(
532
k.Db,
533
+
orm.FilterEq("did", user.Did),
534
+
orm.FilterEq("domain", domain),
535
+
orm.FilterIsNot("registered", "null"),
536
)
537
if err != nil {
538
l.Error("failed to get registration", "err", err)
···
638
639
registrations, err := db.GetRegistrations(
640
k.Db,
641
+
orm.FilterEq("did", user.Did),
642
+
orm.FilterEq("domain", domain),
643
+
orm.FilterIsNot("registered", "null"),
644
)
645
if err != nil {
646
l.Error("failed to get registration", "err", err)
+5
-4
appview/labels/labels.go
+5
-4
appview/labels/labels.go
···
16
"tangled.org/core/appview/oauth"
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/validator"
19
"tangled.org/core/rbac"
20
"tangled.org/core/tid"
21
···
88
repoAt := r.Form.Get("repo")
89
subjectUri := r.Form.Get("subject")
90
91
-
repo, err := db.GetRepo(l.db, db.FilterEq("at_uri", repoAt))
92
if err != nil {
93
fail("Failed to get repository.", err)
94
return
95
}
96
97
// find all the labels that this repo subscribes to
98
-
repoLabels, err := db.GetRepoLabels(l.db, db.FilterEq("repo_at", repoAt))
99
if err != nil {
100
fail("Failed to get labels for this repository.", err)
101
return
···
106
labelAts = append(labelAts, rl.LabelAt.String())
107
}
108
109
-
actx, err := db.NewLabelApplicationCtx(l.db, db.FilterIn("at_uri", labelAts))
110
if err != nil {
111
fail("Invalid form data.", err)
112
return
113
}
114
115
// calculate the start state by applying already known labels
116
-
existingOps, err := db.GetLabelOps(l.db, db.FilterEq("subject", subjectUri))
117
if err != nil {
118
fail("Invalid form data.", err)
119
return
···
16
"tangled.org/core/appview/oauth"
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/validator"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
"tangled.org/core/tid"
22
···
89
repoAt := r.Form.Get("repo")
90
subjectUri := r.Form.Get("subject")
91
92
+
repo, err := db.GetRepo(l.db, orm.FilterEq("at_uri", repoAt))
93
if err != nil {
94
fail("Failed to get repository.", err)
95
return
96
}
97
98
// find all the labels that this repo subscribes to
99
+
repoLabels, err := db.GetRepoLabels(l.db, orm.FilterEq("repo_at", repoAt))
100
if err != nil {
101
fail("Failed to get labels for this repository.", err)
102
return
···
107
labelAts = append(labelAts, rl.LabelAt.String())
108
}
109
110
+
actx, err := db.NewLabelApplicationCtx(l.db, orm.FilterIn("at_uri", labelAts))
111
if err != nil {
112
fail("Invalid form data.", err)
113
return
114
}
115
116
// calculate the start state by applying already known labels
117
+
existingOps, err := db.GetLabelOps(l.db, orm.FilterEq("subject", subjectUri))
118
if err != nil {
119
fail("Invalid form data.", err)
120
return
+67
appview/mentions/resolver.go
+67
appview/mentions/resolver.go
···
···
1
+
package mentions
2
+
3
+
import (
4
+
"context"
5
+
"log/slog"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"tangled.org/core/appview/config"
9
+
"tangled.org/core/appview/db"
10
+
"tangled.org/core/appview/models"
11
+
"tangled.org/core/appview/pages/markup"
12
+
"tangled.org/core/idresolver"
13
+
)
14
+
15
+
type Resolver struct {
16
+
config *config.Config
17
+
idResolver *idresolver.Resolver
18
+
execer db.Execer
19
+
logger *slog.Logger
20
+
}
21
+
22
+
func New(
23
+
config *config.Config,
24
+
idResolver *idresolver.Resolver,
25
+
execer db.Execer,
26
+
logger *slog.Logger,
27
+
) *Resolver {
28
+
return &Resolver{
29
+
config,
30
+
idResolver,
31
+
execer,
32
+
logger,
33
+
}
34
+
}
35
+
36
+
func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) {
37
+
l := r.logger.With("method", "Resolve")
38
+
39
+
rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source)
40
+
l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs)
41
+
42
+
idents := r.idResolver.ResolveIdents(ctx, rawMentions)
43
+
var mentions []syntax.DID
44
+
for _, ident := range idents {
45
+
if ident != nil && !ident.Handle.IsInvalidHandle() {
46
+
mentions = append(mentions, ident.DID)
47
+
}
48
+
}
49
+
l.Debug("found mentions", "mentions", mentions)
50
+
51
+
var resolvedRefs []models.ReferenceLink
52
+
for _, rawRef := range rawRefs {
53
+
ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle)
54
+
if err != nil || ident == nil || ident.Handle.IsInvalidHandle() {
55
+
continue
56
+
}
57
+
rawRef.Handle = string(ident.DID)
58
+
resolvedRefs = append(resolvedRefs, rawRef)
59
+
}
60
+
aturiRefs, err := db.ValidateReferenceLinks(r.execer, resolvedRefs)
61
+
if err != nil {
62
+
l.Error("failed running query", "err", err)
63
+
}
64
+
l.Debug("found references", "refs", aturiRefs)
65
+
66
+
return mentions, aturiRefs
67
+
}
+3
-2
appview/middleware/middleware.go
+3
-2
appview/middleware/middleware.go
···
18
"tangled.org/core/appview/pagination"
19
"tangled.org/core/appview/reporesolver"
20
"tangled.org/core/idresolver"
21
"tangled.org/core/rbac"
22
)
23
···
217
218
repo, err := db.GetRepo(
219
mw.db,
220
-
db.FilterEq("did", id.DID.String()),
221
-
db.FilterEq("name", repoName),
222
)
223
if err != nil {
224
log.Println("failed to resolve repo", "err", err)
···
18
"tangled.org/core/appview/pagination"
19
"tangled.org/core/appview/reporesolver"
20
"tangled.org/core/idresolver"
21
+
"tangled.org/core/orm"
22
"tangled.org/core/rbac"
23
)
24
···
218
219
repo, err := db.GetRepo(
220
mw.db,
221
+
orm.FilterEq("did", id.DID.String()),
222
+
orm.FilterEq("name", repoName),
223
)
224
if err != nil {
225
log.Println("failed to resolve repo", "err", err)
+10
appview/models/pipeline.go
+10
appview/models/pipeline.go
···
1
package models
2
3
import (
4
"slices"
5
"time"
6
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
"github.com/go-git/go-git/v5/plumbing"
9
spindle "tangled.org/core/spindle/models"
10
"tangled.org/core/workflow"
11
)
···
23
// populate when querying for reverse mappings
24
Trigger *Trigger
25
Statuses map[string]WorkflowStatus
26
}
27
28
type WorkflowStatus struct {
···
128
Error *string
129
ExitCode int
130
}
···
1
package models
2
3
import (
4
+
"fmt"
5
"slices"
6
"time"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"github.com/go-git/go-git/v5/plumbing"
10
+
"tangled.org/core/api/tangled"
11
spindle "tangled.org/core/spindle/models"
12
"tangled.org/core/workflow"
13
)
···
25
// populate when querying for reverse mappings
26
Trigger *Trigger
27
Statuses map[string]WorkflowStatus
28
+
}
29
+
30
+
func (p *Pipeline) AtUri() syntax.ATURI {
31
+
return syntax.ATURI(fmt.Sprintf("at://did:web:%s/%s/%s", p.Knot, tangled.PipelineNSID, p.Rkey))
32
}
33
34
type WorkflowStatus struct {
···
134
Error *string
135
ExitCode int
136
}
137
+
138
+
func (ps *PipelineStatus) PipelineAt() syntax.ATURI {
139
+
return syntax.ATURI(fmt.Sprintf("at://did:web:%s/%s/%s", ps.PipelineKnot, tangled.PipelineNSID, ps.PipelineRkey))
140
+
}
+5
-4
appview/notifications/notifications.go
+5
-4
appview/notifications/notifications.go
···
11
"tangled.org/core/appview/oauth"
12
"tangled.org/core/appview/pages"
13
"tangled.org/core/appview/pagination"
14
)
15
16
type Notifications struct {
···
53
54
total, err := db.CountNotifications(
55
n.db,
56
-
db.FilterEq("recipient_did", user.Did),
57
)
58
if err != nil {
59
l.Error("failed to get total notifications", "err", err)
···
64
notifications, err := db.GetNotificationsWithEntities(
65
n.db,
66
page,
67
-
db.FilterEq("recipient_did", user.Did),
68
)
69
if err != nil {
70
l.Error("failed to get notifications", "err", err)
···
96
97
count, err := db.CountNotifications(
98
n.db,
99
-
db.FilterEq("recipient_did", user.Did),
100
-
db.FilterEq("read", 0),
101
)
102
if err != nil {
103
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
···
11
"tangled.org/core/appview/oauth"
12
"tangled.org/core/appview/pages"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
)
16
17
type Notifications struct {
···
54
55
total, err := db.CountNotifications(
56
n.db,
57
+
orm.FilterEq("recipient_did", user.Did),
58
)
59
if err != nil {
60
l.Error("failed to get total notifications", "err", err)
···
65
notifications, err := db.GetNotificationsWithEntities(
66
n.db,
67
page,
68
+
orm.FilterEq("recipient_did", user.Did),
69
)
70
if err != nil {
71
l.Error("failed to get notifications", "err", err)
···
97
98
count, err := db.CountNotifications(
99
n.db,
100
+
orm.FilterEq("recipient_did", user.Did),
101
+
orm.FilterEq("read", 0),
102
)
103
if err != nil {
104
http.Error(w, "Failed to get unread count", http.StatusInternalServerError)
+77
-66
appview/notify/db/db.go
+77
-66
appview/notify/db/db.go
···
3
import (
4
"context"
5
"log"
6
-
"maps"
7
"slices"
8
9
"github.com/bluesky-social/indigo/atproto/syntax"
···
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/notify"
14
"tangled.org/core/idresolver"
15
)
16
17
const (
18
-
maxMentions = 5
19
)
20
21
type databaseNotifier struct {
···
42
return
43
}
44
var err error
45
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(star.RepoAt)))
46
if err != nil {
47
log.Printf("NewStar: failed to get repos: %v", err)
48
return
49
}
50
51
actorDid := syntax.DID(star.Did)
52
-
recipients := []syntax.DID{syntax.DID(repo.Did)}
53
eventType := models.NotificationTypeRepoStarred
54
entityType := "repo"
55
entityId := star.RepoAt.String()
···
74
}
75
76
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
77
-
78
-
// build the recipients list
79
-
// - owner of the repo
80
-
// - collaborators in the repo
81
-
var recipients []syntax.DID
82
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
83
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
84
if err != nil {
85
log.Printf("failed to fetch collaborators: %v", err)
86
return
87
}
88
for _, c := range collaborators {
89
-
recipients = append(recipients, c.SubjectDid)
90
}
91
92
actorDid := syntax.DID(issue.Did)
···
108
)
109
n.notifyEvent(
110
actorDid,
111
-
mentions,
112
models.NotificationTypeUserMentioned,
113
entityType,
114
entityId,
···
119
}
120
121
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
122
-
issues, err := db.GetIssues(n.db, db.FilterEq("at_uri", comment.IssueAt))
123
if err != nil {
124
log.Printf("NewIssueComment: failed to get issues: %v", err)
125
return
···
130
}
131
issue := issues[0]
132
133
-
var recipients []syntax.DID
134
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
135
136
if comment.IsReply() {
137
// if this comment is a reply, then notify everybody in that thread
138
parentAtUri := *comment.ReplyTo
139
-
allThreads := issue.CommentList()
140
141
// find the parent thread, and add all DIDs from here to the recipient list
142
-
for _, t := range allThreads {
143
if t.Self.AtUri().String() == parentAtUri {
144
-
recipients = append(recipients, t.Participants()...)
145
}
146
}
147
} else {
148
// not a reply, notify just the issue author
149
-
recipients = append(recipients, syntax.DID(issue.Did))
150
}
151
152
actorDid := syntax.DID(comment.Did)
···
168
)
169
n.notifyEvent(
170
actorDid,
171
-
mentions,
172
models.NotificationTypeUserMentioned,
173
entityType,
174
entityId,
···
184
185
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
186
actorDid := syntax.DID(follow.UserDid)
187
-
recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
188
eventType := models.NotificationTypeFollowed
189
entityType := "follow"
190
entityId := follow.UserDid
···
207
}
208
209
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
210
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
211
if err != nil {
212
log.Printf("NewPull: failed to get repos: %v", err)
213
return
214
}
215
-
216
-
// build the recipients list
217
-
// - owner of the repo
218
-
// - collaborators in the repo
219
-
var recipients []syntax.DID
220
-
recipients = append(recipients, syntax.DID(repo.Did))
221
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
222
if err != nil {
223
log.Printf("failed to fetch collaborators: %v", err)
224
return
225
}
226
for _, c := range collaborators {
227
-
recipients = append(recipients, c.SubjectDid)
228
}
229
230
actorDid := syntax.DID(pull.OwnerDid)
···
258
return
259
}
260
261
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
262
if err != nil {
263
log.Printf("NewPullComment: failed to get repos: %v", err)
264
return
···
267
// build up the recipients list:
268
// - repo owner
269
// - all pull participants
270
-
var recipients []syntax.DID
271
-
recipients = append(recipients, syntax.DID(repo.Did))
272
for _, p := range pull.Participants() {
273
-
recipients = append(recipients, syntax.DID(p))
274
}
275
276
actorDid := syntax.DID(comment.OwnerDid)
···
294
)
295
n.notifyEvent(
296
actorDid,
297
-
mentions,
298
models.NotificationTypeUserMentioned,
299
entityType,
300
entityId,
···
321
}
322
323
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
324
-
// build up the recipients list:
325
-
// - repo owner
326
-
// - repo collaborators
327
-
// - all issue participants
328
-
var recipients []syntax.DID
329
-
recipients = append(recipients, syntax.DID(issue.Repo.Did))
330
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
331
if err != nil {
332
log.Printf("failed to fetch collaborators: %v", err)
333
return
334
}
335
for _, c := range collaborators {
336
-
recipients = append(recipients, c.SubjectDid)
337
}
338
for _, p := range issue.Participants() {
339
-
recipients = append(recipients, syntax.DID(p))
340
}
341
342
entityType := "pull"
···
366
367
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
368
// Get repo details
369
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
370
if err != nil {
371
log.Printf("NewPullState: failed to get repos: %v", err)
372
return
373
}
374
375
-
// build up the recipients list:
376
-
// - repo owner
377
-
// - all pull participants
378
-
var recipients []syntax.DID
379
-
recipients = append(recipients, syntax.DID(repo.Did))
380
-
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
381
if err != nil {
382
log.Printf("failed to fetch collaborators: %v", err)
383
return
384
}
385
for _, c := range collaborators {
386
-
recipients = append(recipients, c.SubjectDid)
387
}
388
for _, p := range pull.Participants() {
389
-
recipients = append(recipients, syntax.DID(p))
390
}
391
392
entityType := "pull"
···
422
423
func (n *databaseNotifier) notifyEvent(
424
actorDid syntax.DID,
425
-
recipients []syntax.DID,
426
eventType models.NotificationType,
427
entityType string,
428
entityId string,
···
430
issueId *int64,
431
pullId *int64,
432
) {
433
-
if eventType == models.NotificationTypeUserMentioned && len(recipients) > maxMentions {
434
-
recipients = recipients[:maxMentions]
435
}
436
-
recipientSet := make(map[syntax.DID]struct{})
437
-
for _, did := range recipients {
438
-
// everybody except actor themselves
439
-
if did != actorDid {
440
-
recipientSet[did] = struct{}{}
441
-
}
442
-
}
443
444
prefMap, err := db.GetNotificationPreferences(
445
n.db,
446
-
db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
447
)
448
if err != nil {
449
// failed to get prefs for users
···
459
defer tx.Rollback()
460
461
// filter based on preferences
462
-
for recipientDid := range recipientSet {
463
prefs, ok := prefMap[recipientDid]
464
if !ok {
465
prefs = models.DefaultNotificationPreferences(recipientDid)
···
3
import (
4
"context"
5
"log"
6
"slices"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
···
11
"tangled.org/core/appview/models"
12
"tangled.org/core/appview/notify"
13
"tangled.org/core/idresolver"
14
+
"tangled.org/core/orm"
15
+
"tangled.org/core/sets"
16
)
17
18
const (
19
+
maxMentions = 8
20
)
21
22
type databaseNotifier struct {
···
43
return
44
}
45
var err error
46
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(star.RepoAt)))
47
if err != nil {
48
log.Printf("NewStar: failed to get repos: %v", err)
49
return
50
}
51
52
actorDid := syntax.DID(star.Did)
53
+
recipients := sets.Singleton(syntax.DID(repo.Did))
54
eventType := models.NotificationTypeRepoStarred
55
entityType := "repo"
56
entityId := star.RepoAt.String()
···
75
}
76
77
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue, mentions []syntax.DID) {
78
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
79
if err != nil {
80
log.Printf("failed to fetch collaborators: %v", err)
81
return
82
}
83
+
84
+
// build the recipients list
85
+
// - owner of the repo
86
+
// - collaborators in the repo
87
+
// - remove users already mentioned
88
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
89
for _, c := range collaborators {
90
+
recipients.Insert(c.SubjectDid)
91
+
}
92
+
for _, m := range mentions {
93
+
recipients.Remove(m)
94
}
95
96
actorDid := syntax.DID(issue.Did)
···
112
)
113
n.notifyEvent(
114
actorDid,
115
+
sets.Collect(slices.Values(mentions)),
116
models.NotificationTypeUserMentioned,
117
entityType,
118
entityId,
···
123
}
124
125
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment, mentions []syntax.DID) {
126
+
issues, err := db.GetIssues(n.db, orm.FilterEq("at_uri", comment.IssueAt))
127
if err != nil {
128
log.Printf("NewIssueComment: failed to get issues: %v", err)
129
return
···
134
}
135
issue := issues[0]
136
137
+
// built the recipients list:
138
+
// - the owner of the repo
139
+
// - | if the comment is a reply -> everybody on that thread
140
+
// | if the comment is a top level -> just the issue owner
141
+
// - remove mentioned users from the recipients list
142
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
143
144
if comment.IsReply() {
145
// if this comment is a reply, then notify everybody in that thread
146
parentAtUri := *comment.ReplyTo
147
148
// find the parent thread, and add all DIDs from here to the recipient list
149
+
for _, t := range issue.CommentList() {
150
if t.Self.AtUri().String() == parentAtUri {
151
+
for _, p := range t.Participants() {
152
+
recipients.Insert(p)
153
+
}
154
}
155
}
156
} else {
157
// not a reply, notify just the issue author
158
+
recipients.Insert(syntax.DID(issue.Did))
159
+
}
160
+
161
+
for _, m := range mentions {
162
+
recipients.Remove(m)
163
}
164
165
actorDid := syntax.DID(comment.Did)
···
181
)
182
n.notifyEvent(
183
actorDid,
184
+
sets.Collect(slices.Values(mentions)),
185
models.NotificationTypeUserMentioned,
186
entityType,
187
entityId,
···
197
198
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
199
actorDid := syntax.DID(follow.UserDid)
200
+
recipients := sets.Singleton(syntax.DID(follow.SubjectDid))
201
eventType := models.NotificationTypeFollowed
202
entityType := "follow"
203
entityId := follow.UserDid
···
220
}
221
222
func (n *databaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {
223
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
224
if err != nil {
225
log.Printf("NewPull: failed to get repos: %v", err)
226
return
227
}
228
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
229
if err != nil {
230
log.Printf("failed to fetch collaborators: %v", err)
231
return
232
}
233
+
234
+
// build the recipients list
235
+
// - owner of the repo
236
+
// - collaborators in the repo
237
+
recipients := sets.Singleton(syntax.DID(repo.Did))
238
for _, c := range collaborators {
239
+
recipients.Insert(c.SubjectDid)
240
}
241
242
actorDid := syntax.DID(pull.OwnerDid)
···
270
return
271
}
272
273
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", comment.RepoAt))
274
if err != nil {
275
log.Printf("NewPullComment: failed to get repos: %v", err)
276
return
···
279
// build up the recipients list:
280
// - repo owner
281
// - all pull participants
282
+
// - remove those already mentioned
283
+
recipients := sets.Singleton(syntax.DID(repo.Did))
284
for _, p := range pull.Participants() {
285
+
recipients.Insert(syntax.DID(p))
286
+
}
287
+
for _, m := range mentions {
288
+
recipients.Remove(m)
289
}
290
291
actorDid := syntax.DID(comment.OwnerDid)
···
309
)
310
n.notifyEvent(
311
actorDid,
312
+
sets.Collect(slices.Values(mentions)),
313
models.NotificationTypeUserMentioned,
314
entityType,
315
entityId,
···
336
}
337
338
func (n *databaseNotifier) NewIssueState(ctx context.Context, actor syntax.DID, issue *models.Issue) {
339
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", issue.Repo.RepoAt()))
340
if err != nil {
341
log.Printf("failed to fetch collaborators: %v", err)
342
return
343
}
344
+
345
+
// build up the recipients list:
346
+
// - repo owner
347
+
// - repo collaborators
348
+
// - all issue participants
349
+
recipients := sets.Singleton(syntax.DID(issue.Repo.Did))
350
for _, c := range collaborators {
351
+
recipients.Insert(c.SubjectDid)
352
}
353
for _, p := range issue.Participants() {
354
+
recipients.Insert(syntax.DID(p))
355
}
356
357
entityType := "pull"
···
381
382
func (n *databaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
383
// Get repo details
384
+
repo, err := db.GetRepo(n.db, orm.FilterEq("at_uri", string(pull.RepoAt)))
385
if err != nil {
386
log.Printf("NewPullState: failed to get repos: %v", err)
387
return
388
}
389
390
+
collaborators, err := db.GetCollaborators(n.db, orm.FilterEq("repo_at", repo.RepoAt()))
391
if err != nil {
392
log.Printf("failed to fetch collaborators: %v", err)
393
return
394
}
395
+
396
+
// build up the recipients list:
397
+
// - repo owner
398
+
// - all pull participants
399
+
recipients := sets.Singleton(syntax.DID(repo.Did))
400
for _, c := range collaborators {
401
+
recipients.Insert(c.SubjectDid)
402
}
403
for _, p := range pull.Participants() {
404
+
recipients.Insert(syntax.DID(p))
405
}
406
407
entityType := "pull"
···
437
438
func (n *databaseNotifier) notifyEvent(
439
actorDid syntax.DID,
440
+
recipients sets.Set[syntax.DID],
441
eventType models.NotificationType,
442
entityType string,
443
entityId string,
···
445
issueId *int64,
446
pullId *int64,
447
) {
448
+
// if the user is attempting to mention >maxMentions users, this is probably spam, do not mention anybody
449
+
if eventType == models.NotificationTypeUserMentioned && recipients.Len() > maxMentions {
450
+
return
451
}
452
+
453
+
recipients.Remove(actorDid)
454
455
prefMap, err := db.GetNotificationPreferences(
456
n.db,
457
+
orm.FilterIn("user_did", slices.Collect(recipients.All())),
458
)
459
if err != nil {
460
// failed to get prefs for users
···
470
defer tx.Rollback()
471
472
// filter based on preferences
473
+
for recipientDid := range recipients.All() {
474
prefs, ok := prefMap[recipientDid]
475
if !ok {
476
prefs = models.DefaultNotificationPreferences(recipientDid)
-1
appview/notify/merged_notifier.go
-1
appview/notify/merged_notifier.go
+3
-2
appview/oauth/handler.go
+3
-2
appview/oauth/handler.go
···
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/appview/db"
18
"tangled.org/core/consts"
19
"tangled.org/core/tid"
20
)
21
···
97
// and create an sh.tangled.spindle.member record with that
98
spindleMembers, err := db.GetSpindleMembers(
99
o.Db,
100
-
db.FilterEq("instance", "spindle.tangled.sh"),
101
-
db.FilterEq("subject", did),
102
)
103
if err != nil {
104
l.Error("failed to get spindle members", "err", err)
···
16
"tangled.org/core/api/tangled"
17
"tangled.org/core/appview/db"
18
"tangled.org/core/consts"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/tid"
21
)
22
···
98
// and create an sh.tangled.spindle.member record with that
99
spindleMembers, err := db.GetSpindleMembers(
100
o.Db,
101
+
orm.FilterEq("instance", "spindle.tangled.sh"),
102
+
orm.FilterEq("subject", did),
103
)
104
if err != nil {
105
l.Error("failed to get spindle members", "err", err)
+6
-1
appview/pages/funcmap.go
+6
-1
appview/pages/funcmap.go
···
25
"github.com/dustin/go-humanize"
26
"github.com/go-enry/go-enry/v2"
27
"github.com/yuin/goldmark"
28
"tangled.org/core/appview/filetree"
29
"tangled.org/core/appview/models"
30
"tangled.org/core/appview/pages/markup"
···
261
},
262
"description": func(text string) template.HTML {
263
p.rctx.RendererType = markup.RendererTypeDefault
264
-
htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New())
265
sanitized := p.rctx.SanitizeDescription(htmlString)
266
return template.HTML(sanitized)
267
},
···
25
"github.com/dustin/go-humanize"
26
"github.com/go-enry/go-enry/v2"
27
"github.com/yuin/goldmark"
28
+
emoji "github.com/yuin/goldmark-emoji"
29
"tangled.org/core/appview/filetree"
30
"tangled.org/core/appview/models"
31
"tangled.org/core/appview/pages/markup"
···
262
},
263
"description": func(text string) template.HTML {
264
p.rctx.RendererType = markup.RendererTypeDefault
265
+
htmlString := p.rctx.RenderMarkdownWith(text, goldmark.New(
266
+
goldmark.WithExtensions(
267
+
emoji.Emoji,
268
+
),
269
+
))
270
sanitized := p.rctx.SanitizeDescription(htmlString)
271
return template.HTML(sanitized)
272
},
+2
-2
appview/pages/markup/markdown.go
+2
-2
appview/pages/markup/markdown.go
···
12
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
"github.com/alecthomas/chroma/v2/styles"
15
-
treeblood "github.com/wyatt915/goldmark-treeblood"
16
"github.com/yuin/goldmark"
17
highlighting "github.com/yuin/goldmark-highlighting/v2"
18
"github.com/yuin/goldmark/ast"
19
"github.com/yuin/goldmark/extension"
···
65
extension.NewFootnote(
66
extension.WithFootnoteIDPrefix([]byte("footnote")),
67
),
68
-
treeblood.MathML(),
69
callout.CalloutExtention,
70
textension.AtExt,
71
),
72
goldmark.WithParserOptions(
73
parser.WithAutoHeadingID(),
···
12
13
chromahtml "github.com/alecthomas/chroma/v2/formatters/html"
14
"github.com/alecthomas/chroma/v2/styles"
15
"github.com/yuin/goldmark"
16
+
"github.com/yuin/goldmark-emoji"
17
highlighting "github.com/yuin/goldmark-highlighting/v2"
18
"github.com/yuin/goldmark/ast"
19
"github.com/yuin/goldmark/extension"
···
65
extension.NewFootnote(
66
extension.WithFootnoteIDPrefix([]byte("footnote")),
67
),
68
callout.CalloutExtention,
69
textension.AtExt,
70
+
emoji.Emoji,
71
),
72
goldmark.WithParserOptions(
73
parser.WithAutoHeadingID(),
+1
-1
appview/pages/pages.go
+1
-1
appview/pages/pages.go
+5
appview/pages/templates/fragments/starBtn-oob.html
+5
appview/pages/templates/fragments/starBtn-oob.html
+1
-3
appview/pages/templates/fragments/starBtn.html
+1
-3
appview/pages/templates/fragments/starBtn.html
···
1
{{ define "fragments/starBtn" }}
2
<button
3
id="starBtn"
4
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
···
10
{{ end }}
11
12
hx-trigger="click"
13
-
hx-target="this"
14
-
hx-swap="outerHTML"
15
-
hx-swap-oob='outerHTML:#starBtn[data-star-subject-at="{{ .SubjectAt }}"]'
16
hx-disabled-elt="#starBtn"
17
>
18
{{ if .IsStarred }}
···
1
{{ define "fragments/starBtn" }}
2
+
{{/* NOTE: this fragment is always replaced with hx-swap-oob */}}
3
<button
4
id="starBtn"
5
class="btn disabled:opacity-50 disabled:cursor-not-allowed flex gap-2 items-center group"
···
11
{{ end }}
12
13
hx-trigger="click"
14
hx-disabled-elt="#starBtn"
15
>
16
{{ if .IsStarred }}
+1
-1
appview/pages/templates/knots/index.html
+1
-1
appview/pages/templates/knots/index.html
+1
-1
appview/pages/templates/repo/empty.html
+1
-1
appview/pages/templates/repo/empty.html
+6
-6
appview/pages/templates/repo/fragments/backlinks.html
+6
-6
appview/pages/templates/repo/fragments/backlinks.html
···
14
<div class="flex gap-2 items-center">
15
{{ if .State.IsClosed }}
16
<span class="text-gray-500 dark:text-gray-400">
17
-
{{ i "ban" "w-4 h-4" }}
18
</span>
19
{{ else if eq .Kind.String "issues" }}
20
<span class="text-green-600 dark:text-green-500">
21
-
{{ i "circle-dot" "w-4 h-4" }}
22
</span>
23
{{ else if .State.IsOpen }}
24
<span class="text-green-600 dark:text-green-500">
25
-
{{ i "git-pull-request" "w-4 h-4" }}
26
</span>
27
{{ else if .State.IsMerged }}
28
<span class="text-purple-600 dark:text-purple-500">
29
-
{{ i "git-merge" "w-4 h-4" }}
30
</span>
31
{{ else }}
32
<span class="text-gray-600 dark:text-gray-300">
33
-
{{ i "git-pull-request-closed" "w-4 h-4" }}
34
</span>
35
{{ end }}
36
-
<a href="{{ . }}"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
37
</div>
38
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
39
<div>
···
14
<div class="flex gap-2 items-center">
15
{{ if .State.IsClosed }}
16
<span class="text-gray-500 dark:text-gray-400">
17
+
{{ i "ban" "size-3" }}
18
</span>
19
{{ else if eq .Kind.String "issues" }}
20
<span class="text-green-600 dark:text-green-500">
21
+
{{ i "circle-dot" "size-3" }}
22
</span>
23
{{ else if .State.IsOpen }}
24
<span class="text-green-600 dark:text-green-500">
25
+
{{ i "git-pull-request" "size-3" }}
26
</span>
27
{{ else if .State.IsMerged }}
28
<span class="text-purple-600 dark:text-purple-500">
29
+
{{ i "git-merge" "size-3" }}
30
</span>
31
{{ else }}
32
<span class="text-gray-600 dark:text-gray-300">
33
+
{{ i "git-pull-request-closed" "size-3" }}
34
</span>
35
{{ end }}
36
+
<a href="{{ . }}" class="line-clamp-1 text-sm"><span class="text-gray-500 dark:text-gray-400">#{{ .SubjectId }}</span> {{ .Title }}</a>
37
</div>
38
{{ if not (eq $.RepoInfo.FullName $repoUrl) }}
39
<div>
+10
appview/pages/templates/repo/pipelines/workflow.html
+10
appview/pages/templates/repo/pipelines/workflow.html
···
12
{{ block "sidebar" . }} {{ end }}
13
</div>
14
<div class="col-span-1 md:col-span-3">
15
+
<div class="flex justify-end mb-2">
16
+
<button
17
+
class="btn"
18
+
hx-post="/{{ $.RepoInfo.FullName }}/pipelines/{{ .Pipeline.Id }}/workflow/{{ .Workflow }}/cancel"
19
+
hx-swap="none"
20
+
{{ if (index .Pipeline.Statuses .Workflow).Latest.Status.IsFinish -}}
21
+
disabled
22
+
{{- end }}
23
+
>Cancel</button>
24
+
</div>
25
{{ block "logs" . }} {{ end }}
26
</div>
27
</section>
+1
-1
appview/pages/templates/strings/string.html
+1
-1
appview/pages/templates/strings/string.html
···
17
<span class="select-none">/</span>
18
<a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a>
19
</div>
20
-
<div class="flex gap-2 text-base">
21
{{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }}
22
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
23
hx-boost="true"
···
17
<span class="select-none">/</span>
18
<a href="/strings/{{ $ownerId }}/{{ .String.Rkey }}" class="font-bold">{{ .String.Filename }}</a>
19
</div>
20
+
<div class="flex gap-2 items-stretch text-base">
21
{{ if and .LoggedInUser (eq .LoggedInUser.Did .String.Did) }}
22
<a class="btn flex items-center gap-2 no-underline hover:no-underline p-2 group"
23
hx-boost="true"
+2
-2
appview/pages/templates/user/fragments/followCard.html
+2
-2
appview/pages/templates/user/fragments/followCard.html
···
6
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" alt="{{ $userIdent }}" />
7
</div>
8
9
-
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full">
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
<a href="/{{ $userIdent }}">
12
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span>
13
</a>
14
{{ with .Profile }}
15
-
<p class="text-sm pb-2 md:pb-2">{{.Description}}</p>
16
{{ end }}
17
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
18
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
···
6
<img class="object-cover rounded-full p-2" src="{{ fullAvatar $userIdent }}" alt="{{ $userIdent }}" />
7
</div>
8
9
+
<div class="flex flex-col md:flex-row md:items-center md:justify-between gap-2 w-full min-w-0">
10
<div class="flex-1 min-h-0 justify-around flex flex-col">
11
<a href="/{{ $userIdent }}">
12
<span class="font-bold dark:text-white overflow-hidden text-ellipsis whitespace-nowrap max-w-full">{{ $userIdent | truncateAt30 }}</span>
13
</a>
14
{{ with .Profile }}
15
+
<p class="text-sm pb-2 md:pb-2 break-words">{{.Description}}</p>
16
{{ end }}
17
<div class="text-sm flex items-center gap-2 my-2 overflow-hidden text-ellipsis whitespace-nowrap max-w-full">
18
<span class="flex-shrink-0">{{ i "users" "size-4" }}</span>
+9
-6
appview/pages/templates/user/signup.html
+9
-6
appview/pages/templates/user/signup.html
···
43
page to complete your registration.
44
</span>
45
<div class="w-full mt-4 text-center">
46
-
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}"></div>
47
</div>
48
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
49
<span>join now</span>
50
</button>
51
</form>
52
-
<p class="text-sm text-gray-500">
53
-
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
54
-
</p>
55
-
56
-
<p id="signup-msg" class="error w-full"></p>
57
</main>
58
</body>
59
</html>
···
43
page to complete your registration.
44
</span>
45
<div class="w-full mt-4 text-center">
46
+
<div class="cf-turnstile" data-sitekey="{{ .CloudflareSiteKey }}" data-size="flexible"></div>
47
</div>
48
<button class="btn text-base w-full my-2 mt-6" type="submit" id="signup-button" tabindex="7" >
49
<span>join now</span>
50
</button>
51
+
<p class="text-sm text-gray-500">
52
+
Already have an AT Protocol account? <a href="/login" class="underline">Login to Tangled</a>.
53
+
</p>
54
+
55
+
<p id="signup-msg" class="error w-full"></p>
56
+
<p class="text-sm text-gray-500 pt-4">
57
+
By signing up, you agree to our <a href="/terms" class="underline">Terms of Service</a> and <a href="/privacy" class="underline">Privacy Policy</a>.
58
+
</p>
59
</form>
60
</main>
61
</body>
62
</html>
+94
-11
appview/pipelines/pipelines.go
+94
-11
appview/pipelines/pipelines.go
···
4
"bytes"
5
"context"
6
"encoding/json"
7
"log/slog"
8
"net/http"
9
"strings"
10
"time"
11
12
"tangled.org/core/appview/config"
13
"tangled.org/core/appview/db"
14
"tangled.org/core/appview/oauth"
15
"tangled.org/core/appview/pages"
16
"tangled.org/core/appview/reporesolver"
17
"tangled.org/core/eventconsumer"
18
"tangled.org/core/idresolver"
19
"tangled.org/core/rbac"
20
spindlemodel "tangled.org/core/spindle/models"
21
···
40
r.Get("/", p.Index)
41
r.Get("/{pipeline}/workflow/{workflow}", p.Workflow)
42
r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs)
43
44
return r
45
}
···
81
ps, err := db.GetPipelineStatuses(
82
p.db,
83
30,
84
-
db.FilterEq("repo_owner", f.Did),
85
-
db.FilterEq("repo_name", f.Name),
86
-
db.FilterEq("knot", f.Knot),
87
)
88
if err != nil {
89
l.Error("failed to query db", "err", err)
···
122
ps, err := db.GetPipelineStatuses(
123
p.db,
124
1,
125
-
db.FilterEq("repo_owner", f.Did),
126
-
db.FilterEq("repo_name", f.Name),
127
-
db.FilterEq("knot", f.Knot),
128
-
db.FilterEq("id", pipelineId),
129
)
130
if err != nil {
131
l.Error("failed to query db", "err", err)
···
189
ps, err := db.GetPipelineStatuses(
190
p.db,
191
1,
192
-
db.FilterEq("repo_owner", f.Did),
193
-
db.FilterEq("repo_name", f.Name),
194
-
db.FilterEq("knot", f.Knot),
195
-
db.FilterEq("id", pipelineId),
196
)
197
if err != nil || len(ps) != 1 {
198
l.Error("pipeline query failed", "err", err, "count", len(ps))
···
313
}
314
}
315
}
316
}
317
318
// either a message or an error
···
4
"bytes"
5
"context"
6
"encoding/json"
7
+
"fmt"
8
"log/slog"
9
"net/http"
10
"strings"
11
"time"
12
13
+
"tangled.org/core/api/tangled"
14
"tangled.org/core/appview/config"
15
"tangled.org/core/appview/db"
16
+
"tangled.org/core/appview/models"
17
"tangled.org/core/appview/oauth"
18
"tangled.org/core/appview/pages"
19
"tangled.org/core/appview/reporesolver"
20
"tangled.org/core/eventconsumer"
21
"tangled.org/core/idresolver"
22
+
"tangled.org/core/orm"
23
"tangled.org/core/rbac"
24
spindlemodel "tangled.org/core/spindle/models"
25
···
44
r.Get("/", p.Index)
45
r.Get("/{pipeline}/workflow/{workflow}", p.Workflow)
46
r.Get("/{pipeline}/workflow/{workflow}/logs", p.Logs)
47
+
r.Post("/{pipeline}/workflow/{workflow}/cancel", p.Cancel)
48
49
return r
50
}
···
86
ps, err := db.GetPipelineStatuses(
87
p.db,
88
30,
89
+
orm.FilterEq("repo_owner", f.Did),
90
+
orm.FilterEq("repo_name", f.Name),
91
+
orm.FilterEq("knot", f.Knot),
92
)
93
if err != nil {
94
l.Error("failed to query db", "err", err)
···
127
ps, err := db.GetPipelineStatuses(
128
p.db,
129
1,
130
+
orm.FilterEq("repo_owner", f.Did),
131
+
orm.FilterEq("repo_name", f.Name),
132
+
orm.FilterEq("knot", f.Knot),
133
+
orm.FilterEq("id", pipelineId),
134
)
135
if err != nil {
136
l.Error("failed to query db", "err", err)
···
194
ps, err := db.GetPipelineStatuses(
195
p.db,
196
1,
197
+
orm.FilterEq("repo_owner", f.Did),
198
+
orm.FilterEq("repo_name", f.Name),
199
+
orm.FilterEq("knot", f.Knot),
200
+
orm.FilterEq("id", pipelineId),
201
)
202
if err != nil || len(ps) != 1 {
203
l.Error("pipeline query failed", "err", err, "count", len(ps))
···
318
}
319
}
320
}
321
+
}
322
+
323
+
func (p *Pipelines) Cancel(w http.ResponseWriter, r *http.Request) {
324
+
l := p.logger.With("handler", "Cancel")
325
+
326
+
var (
327
+
pipelineId = chi.URLParam(r, "pipeline")
328
+
workflow = chi.URLParam(r, "workflow")
329
+
)
330
+
if pipelineId == "" || workflow == "" {
331
+
http.Error(w, "missing pipeline ID or workflow", http.StatusBadRequest)
332
+
return
333
+
}
334
+
335
+
f, err := p.repoResolver.Resolve(r)
336
+
if err != nil {
337
+
l.Error("failed to get repo and knot", "err", err)
338
+
http.Error(w, "bad repo/knot", http.StatusBadRequest)
339
+
return
340
+
}
341
+
342
+
pipeline, err := func() (models.Pipeline, error) {
343
+
ps, err := db.GetPipelineStatuses(
344
+
p.db,
345
+
1,
346
+
orm.FilterEq("repo_owner", f.Did),
347
+
orm.FilterEq("repo_name", f.Name),
348
+
orm.FilterEq("knot", f.Knot),
349
+
orm.FilterEq("id", pipelineId),
350
+
)
351
+
if err != nil {
352
+
return models.Pipeline{}, err
353
+
}
354
+
if len(ps) != 1 {
355
+
return models.Pipeline{}, fmt.Errorf("wrong pipeline count %d", len(ps))
356
+
}
357
+
return ps[0], nil
358
+
}()
359
+
if err != nil {
360
+
l.Error("pipeline query failed", "err", err)
361
+
http.Error(w, "pipeline not found", http.StatusNotFound)
362
+
}
363
+
var (
364
+
spindle = f.Spindle
365
+
knot = f.Knot
366
+
rkey = pipeline.Rkey
367
+
)
368
+
369
+
if spindle == "" || knot == "" || rkey == "" {
370
+
http.Error(w, "invalid repo info", http.StatusBadRequest)
371
+
return
372
+
}
373
+
374
+
spindleClient, err := p.oauth.ServiceClient(
375
+
r,
376
+
oauth.WithService(f.Spindle),
377
+
oauth.WithLxm(tangled.PipelineCancelPipelineNSID),
378
+
oauth.WithExp(60),
379
+
oauth.WithDev(p.config.Core.Dev),
380
+
oauth.WithTimeout(time.Second*30), // workflow cleanup usually takes time
381
+
)
382
+
383
+
err = tangled.PipelineCancelPipeline(
384
+
r.Context(),
385
+
spindleClient,
386
+
&tangled.PipelineCancelPipeline_Input{
387
+
Repo: string(f.RepoAt()),
388
+
Pipeline: pipeline.AtUri().String(),
389
+
Workflow: workflow,
390
+
},
391
+
)
392
+
errorId := "pipeline-action"
393
+
if err != nil {
394
+
l.Error("failed to cancel pipeline", "err", err)
395
+
p.pages.Notice(w, errorId, "Failed to add secret.")
396
+
return
397
+
}
398
+
l.Debug("canceled pipeline", "uri", pipeline.AtUri())
399
}
400
401
// either a message or an error
+2
-1
appview/pulls/opengraph.go
+2
-1
appview/pulls/opengraph.go
···
13
"tangled.org/core/appview/db"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/ogcard"
16
"tangled.org/core/patchutil"
17
"tangled.org/core/types"
18
)
···
276
}
277
278
// Get comment count from database
279
-
comments, err := db.GetPullComments(s.db, db.FilterEq("pull_id", pull.ID))
280
if err != nil {
281
log.Printf("failed to get pull comments: %v", err)
282
}
···
13
"tangled.org/core/appview/db"
14
"tangled.org/core/appview/models"
15
"tangled.org/core/appview/ogcard"
16
+
"tangled.org/core/orm"
17
"tangled.org/core/patchutil"
18
"tangled.org/core/types"
19
)
···
277
}
278
279
// Get comment count from database
280
+
comments, err := db.GetPullComments(s.db, orm.FilterEq("pull_id", pull.ID))
281
if err != nil {
282
log.Printf("failed to get pull comments: %v", err)
283
}
+56
-47
appview/pulls/pulls.go
+56
-47
appview/pulls/pulls.go
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
pulls_indexer "tangled.org/core/appview/indexer/pulls"
22
"tangled.org/core/appview/models"
23
"tangled.org/core/appview/notify"
24
"tangled.org/core/appview/oauth"
25
"tangled.org/core/appview/pages"
26
"tangled.org/core/appview/pages/markup"
27
"tangled.org/core/appview/pages/repoinfo"
28
-
"tangled.org/core/appview/refresolver"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/appview/xrpcclient"
32
"tangled.org/core/idresolver"
33
"tangled.org/core/patchutil"
34
"tangled.org/core/rbac"
35
"tangled.org/core/tid"
···
44
)
45
46
type Pulls struct {
47
-
oauth *oauth.OAuth
48
-
repoResolver *reporesolver.RepoResolver
49
-
pages *pages.Pages
50
-
idResolver *idresolver.Resolver
51
-
refResolver *refresolver.Resolver
52
-
db *db.DB
53
-
config *config.Config
54
-
notifier notify.Notifier
55
-
enforcer *rbac.Enforcer
56
-
logger *slog.Logger
57
-
validator *validator.Validator
58
-
indexer *pulls_indexer.Indexer
59
}
60
61
func New(
···
63
repoResolver *reporesolver.RepoResolver,
64
pages *pages.Pages,
65
resolver *idresolver.Resolver,
66
-
refResolver *refresolver.Resolver,
67
db *db.DB,
68
config *config.Config,
69
notifier notify.Notifier,
···
73
logger *slog.Logger,
74
) *Pulls {
75
return &Pulls{
76
-
oauth: oauth,
77
-
repoResolver: repoResolver,
78
-
pages: pages,
79
-
idResolver: resolver,
80
-
refResolver: refResolver,
81
-
db: db,
82
-
config: config,
83
-
notifier: notifier,
84
-
enforcer: enforcer,
85
-
logger: logger,
86
-
validator: validator,
87
-
indexer: indexer,
88
}
89
}
90
···
190
ps, err := db.GetPipelineStatuses(
191
s.db,
192
len(shas),
193
-
db.FilterEq("repo_owner", f.Did),
194
-
db.FilterEq("repo_name", f.Name),
195
-
db.FilterEq("knot", f.Knot),
196
-
db.FilterIn("sha", shas),
197
)
198
if err != nil {
199
log.Printf("failed to fetch pipeline statuses: %s", err)
···
217
218
labelDefs, err := db.GetLabelDefinitions(
219
s.db,
220
-
db.FilterIn("at_uri", f.Labels),
221
-
db.FilterContains("scope", tangled.RepoPullNSID),
222
)
223
if err != nil {
224
log.Println("failed to fetch labels", err)
···
597
598
pulls, err := db.GetPulls(
599
s.db,
600
-
db.FilterIn("id", ids),
601
)
602
if err != nil {
603
log.Println("failed to get pulls", err)
···
648
ps, err := db.GetPipelineStatuses(
649
s.db,
650
len(shas),
651
-
db.FilterEq("repo_owner", f.Did),
652
-
db.FilterEq("repo_name", f.Name),
653
-
db.FilterEq("knot", f.Knot),
654
-
db.FilterIn("sha", shas),
655
)
656
if err != nil {
657
log.Printf("failed to fetch pipeline statuses: %s", err)
···
664
665
labelDefs, err := db.GetLabelDefinitions(
666
s.db,
667
-
db.FilterIn("at_uri", f.Labels),
668
-
db.FilterContains("scope", tangled.RepoPullNSID),
669
)
670
if err != nil {
671
log.Println("failed to fetch labels", err)
···
729
return
730
}
731
732
-
mentions, references := s.refResolver.Resolve(r.Context(), body)
733
734
// Start a transaction
735
tx, err := s.db.BeginTx(r.Context(), nil)
···
1205
}
1206
}
1207
1208
-
mentions, references := s.refResolver.Resolve(r.Context(), body)
1209
1210
rkey := tid.TID()
1211
initialSubmission := models.PullSubmission{
···
1365
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1366
return
1367
}
1368
}
1369
1370
if err = tx.Commit(); err != nil {
1371
log.Println("failed to create pull request", err)
1372
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1373
return
1374
}
1375
1376
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
···
1498
// fork repo
1499
repo, err := db.GetRepo(
1500
s.db,
1501
-
db.FilterEq("did", forkOwnerDid),
1502
-
db.FilterEq("name", forkName),
1503
)
1504
if err != nil {
1505
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
2066
tx,
2067
p.ParentChangeId,
2068
// these should be enough filters to be unique per-stack
2069
-
db.FilterEq("repo_at", p.RepoAt.String()),
2070
-
db.FilterEq("owner_did", p.OwnerDid),
2071
-
db.FilterEq("change_id", p.ChangeId),
2072
)
2073
2074
if err != nil {
···
2397
body := fp.Body
2398
rkey := tid.TID()
2399
2400
-
mentions, references := s.refResolver.Resolve(ctx, body)
2401
2402
initialSubmission := models.PullSubmission{
2403
Patch: fp.Raw,
···
19
"tangled.org/core/appview/config"
20
"tangled.org/core/appview/db"
21
pulls_indexer "tangled.org/core/appview/indexer/pulls"
22
+
"tangled.org/core/appview/mentions"
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/notify"
25
"tangled.org/core/appview/oauth"
26
"tangled.org/core/appview/pages"
27
"tangled.org/core/appview/pages/markup"
28
"tangled.org/core/appview/pages/repoinfo"
29
"tangled.org/core/appview/reporesolver"
30
"tangled.org/core/appview/validator"
31
"tangled.org/core/appview/xrpcclient"
32
"tangled.org/core/idresolver"
33
+
"tangled.org/core/orm"
34
"tangled.org/core/patchutil"
35
"tangled.org/core/rbac"
36
"tangled.org/core/tid"
···
45
)
46
47
type Pulls struct {
48
+
oauth *oauth.OAuth
49
+
repoResolver *reporesolver.RepoResolver
50
+
pages *pages.Pages
51
+
idResolver *idresolver.Resolver
52
+
mentionsResolver *mentions.Resolver
53
+
db *db.DB
54
+
config *config.Config
55
+
notifier notify.Notifier
56
+
enforcer *rbac.Enforcer
57
+
logger *slog.Logger
58
+
validator *validator.Validator
59
+
indexer *pulls_indexer.Indexer
60
}
61
62
func New(
···
64
repoResolver *reporesolver.RepoResolver,
65
pages *pages.Pages,
66
resolver *idresolver.Resolver,
67
+
mentionsResolver *mentions.Resolver,
68
db *db.DB,
69
config *config.Config,
70
notifier notify.Notifier,
···
74
logger *slog.Logger,
75
) *Pulls {
76
return &Pulls{
77
+
oauth: oauth,
78
+
repoResolver: repoResolver,
79
+
pages: pages,
80
+
idResolver: resolver,
81
+
mentionsResolver: mentionsResolver,
82
+
db: db,
83
+
config: config,
84
+
notifier: notifier,
85
+
enforcer: enforcer,
86
+
logger: logger,
87
+
validator: validator,
88
+
indexer: indexer,
89
}
90
}
91
···
191
ps, err := db.GetPipelineStatuses(
192
s.db,
193
len(shas),
194
+
orm.FilterEq("repo_owner", f.Did),
195
+
orm.FilterEq("repo_name", f.Name),
196
+
orm.FilterEq("knot", f.Knot),
197
+
orm.FilterIn("sha", shas),
198
)
199
if err != nil {
200
log.Printf("failed to fetch pipeline statuses: %s", err)
···
218
219
labelDefs, err := db.GetLabelDefinitions(
220
s.db,
221
+
orm.FilterIn("at_uri", f.Labels),
222
+
orm.FilterContains("scope", tangled.RepoPullNSID),
223
)
224
if err != nil {
225
log.Println("failed to fetch labels", err)
···
598
599
pulls, err := db.GetPulls(
600
s.db,
601
+
orm.FilterIn("id", ids),
602
)
603
if err != nil {
604
log.Println("failed to get pulls", err)
···
649
ps, err := db.GetPipelineStatuses(
650
s.db,
651
len(shas),
652
+
orm.FilterEq("repo_owner", f.Did),
653
+
orm.FilterEq("repo_name", f.Name),
654
+
orm.FilterEq("knot", f.Knot),
655
+
orm.FilterIn("sha", shas),
656
)
657
if err != nil {
658
log.Printf("failed to fetch pipeline statuses: %s", err)
···
665
666
labelDefs, err := db.GetLabelDefinitions(
667
s.db,
668
+
orm.FilterIn("at_uri", f.Labels),
669
+
orm.FilterContains("scope", tangled.RepoPullNSID),
670
)
671
if err != nil {
672
log.Println("failed to fetch labels", err)
···
730
return
731
}
732
733
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
734
735
// Start a transaction
736
tx, err := s.db.BeginTx(r.Context(), nil)
···
1206
}
1207
}
1208
1209
+
mentions, references := s.mentionsResolver.Resolve(r.Context(), body)
1210
1211
rkey := tid.TID()
1212
initialSubmission := models.PullSubmission{
···
1366
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1367
return
1368
}
1369
+
1370
}
1371
1372
if err = tx.Commit(); err != nil {
1373
log.Println("failed to create pull request", err)
1374
s.pages.Notice(w, "pull", "Failed to create pull request. Try again later.")
1375
return
1376
+
}
1377
+
1378
+
// notify about each pull
1379
+
//
1380
+
// this is performed after tx.Commit, because it could result in a locked DB otherwise
1381
+
for _, p := range stack {
1382
+
s.notifier.NewPull(r.Context(), p)
1383
}
1384
1385
ownerSlashRepo := reporesolver.GetBaseRepoPath(r, repo)
···
1507
// fork repo
1508
repo, err := db.GetRepo(
1509
s.db,
1510
+
orm.FilterEq("did", forkOwnerDid),
1511
+
orm.FilterEq("name", forkName),
1512
)
1513
if err != nil {
1514
log.Println("failed to get repo", "did", forkOwnerDid, "name", forkName, "err", err)
···
2075
tx,
2076
p.ParentChangeId,
2077
// these should be enough filters to be unique per-stack
2078
+
orm.FilterEq("repo_at", p.RepoAt.String()),
2079
+
orm.FilterEq("owner_did", p.OwnerDid),
2080
+
orm.FilterEq("change_id", p.ChangeId),
2081
)
2082
2083
if err != nil {
···
2406
body := fp.Body
2407
rkey := tid.TID()
2408
2409
+
mentions, references := s.mentionsResolver.Resolve(ctx, body)
2410
2411
initialSubmission := models.PullSubmission{
2412
Patch: fp.Raw,
-65
appview/refresolver/resolver.go
-65
appview/refresolver/resolver.go
···
1
-
package refresolver
2
-
3
-
import (
4
-
"context"
5
-
"log/slog"
6
-
7
-
"github.com/bluesky-social/indigo/atproto/syntax"
8
-
"tangled.org/core/appview/config"
9
-
"tangled.org/core/appview/db"
10
-
"tangled.org/core/appview/models"
11
-
"tangled.org/core/appview/pages/markup"
12
-
"tangled.org/core/idresolver"
13
-
)
14
-
15
-
type Resolver struct {
16
-
config *config.Config
17
-
idResolver *idresolver.Resolver
18
-
execer db.Execer
19
-
logger *slog.Logger
20
-
}
21
-
22
-
func New(
23
-
config *config.Config,
24
-
idResolver *idresolver.Resolver,
25
-
execer db.Execer,
26
-
logger *slog.Logger,
27
-
) *Resolver {
28
-
return &Resolver{
29
-
config,
30
-
idResolver,
31
-
execer,
32
-
logger,
33
-
}
34
-
}
35
-
36
-
func (r *Resolver) Resolve(ctx context.Context, source string) ([]syntax.DID, []syntax.ATURI) {
37
-
l := r.logger.With("method", "Resolve")
38
-
rawMentions, rawRefs := markup.FindReferences(r.config.Core.AppviewHost, source)
39
-
l.Debug("found possible references", "mentions", rawMentions, "refs", rawRefs)
40
-
idents := r.idResolver.ResolveIdents(ctx, rawMentions)
41
-
var mentions []syntax.DID
42
-
for _, ident := range idents {
43
-
if ident != nil && !ident.Handle.IsInvalidHandle() {
44
-
mentions = append(mentions, ident.DID)
45
-
}
46
-
}
47
-
l.Debug("found mentions", "mentions", mentions)
48
-
49
-
var resolvedRefs []models.ReferenceLink
50
-
for _, rawRef := range rawRefs {
51
-
ident, err := r.idResolver.ResolveIdent(ctx, rawRef.Handle)
52
-
if err != nil || ident == nil || ident.Handle.IsInvalidHandle() {
53
-
continue
54
-
}
55
-
rawRef.Handle = string(ident.DID)
56
-
resolvedRefs = append(resolvedRefs, rawRef)
57
-
}
58
-
aturiRefs, err := db.ValidateReferenceLinks(r.execer, resolvedRefs)
59
-
if err != nil {
60
-
l.Error("failed running query", "err", err)
61
-
}
62
-
l.Debug("found references", "refs", aturiRefs)
63
-
64
-
return mentions, aturiRefs
65
-
}
···
+10
-9
appview/repo/artifact.go
+10
-9
appview/repo/artifact.go
···
15
"tangled.org/core/appview/models"
16
"tangled.org/core/appview/pages"
17
"tangled.org/core/appview/xrpcclient"
18
"tangled.org/core/tid"
19
"tangled.org/core/types"
20
···
155
156
artifacts, err := db.GetArtifact(
157
rp.db,
158
-
db.FilterEq("repo_at", f.RepoAt()),
159
-
db.FilterEq("tag", tag.Tag.Hash[:]),
160
-
db.FilterEq("name", filename),
161
)
162
if err != nil {
163
log.Println("failed to get artifacts", err)
···
234
235
artifacts, err := db.GetArtifact(
236
rp.db,
237
-
db.FilterEq("repo_at", f.RepoAt()),
238
-
db.FilterEq("tag", tag[:]),
239
-
db.FilterEq("name", filename),
240
)
241
if err != nil {
242
log.Println("failed to get artifacts", err)
···
276
defer tx.Rollback()
277
278
err = db.DeleteArtifact(tx,
279
-
db.FilterEq("repo_at", f.RepoAt()),
280
-
db.FilterEq("tag", artifact.Tag[:]),
281
-
db.FilterEq("name", filename),
282
)
283
if err != nil {
284
log.Println("failed to remove artifact record from db", err)
···
15
"tangled.org/core/appview/models"
16
"tangled.org/core/appview/pages"
17
"tangled.org/core/appview/xrpcclient"
18
+
"tangled.org/core/orm"
19
"tangled.org/core/tid"
20
"tangled.org/core/types"
21
···
156
157
artifacts, err := db.GetArtifact(
158
rp.db,
159
+
orm.FilterEq("repo_at", f.RepoAt()),
160
+
orm.FilterEq("tag", tag.Tag.Hash[:]),
161
+
orm.FilterEq("name", filename),
162
)
163
if err != nil {
164
log.Println("failed to get artifacts", err)
···
235
236
artifacts, err := db.GetArtifact(
237
rp.db,
238
+
orm.FilterEq("repo_at", f.RepoAt()),
239
+
orm.FilterEq("tag", tag[:]),
240
+
orm.FilterEq("name", filename),
241
)
242
if err != nil {
243
log.Println("failed to get artifacts", err)
···
277
defer tx.Rollback()
278
279
err = db.DeleteArtifact(tx,
280
+
orm.FilterEq("repo_at", f.RepoAt()),
281
+
orm.FilterEq("tag", artifact.Tag[:]),
282
+
orm.FilterEq("name", filename),
283
)
284
if err != nil {
285
log.Println("failed to remove artifact record from db", err)
+3
-2
appview/repo/feed.go
+3
-2
appview/repo/feed.go
···
11
"tangled.org/core/appview/db"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
15
"github.com/bluesky-social/indigo/atproto/identity"
16
"github.com/bluesky-social/indigo/atproto/syntax"
···
20
func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) {
21
const feedLimitPerType = 100
22
23
-
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, db.FilterEq("repo_at", repo.RepoAt()))
24
if err != nil {
25
return nil, err
26
}
···
28
issues, err := db.GetIssuesPaginated(
29
rp.db,
30
pagination.Page{Limit: feedLimitPerType},
31
-
db.FilterEq("repo_at", repo.RepoAt()),
32
)
33
if err != nil {
34
return nil, err
···
11
"tangled.org/core/appview/db"
12
"tangled.org/core/appview/models"
13
"tangled.org/core/appview/pagination"
14
+
"tangled.org/core/orm"
15
16
"github.com/bluesky-social/indigo/atproto/identity"
17
"github.com/bluesky-social/indigo/atproto/syntax"
···
21
func (rp *Repo) getRepoFeed(ctx context.Context, repo *models.Repo, ownerSlashRepo string) (*feeds.Feed, error) {
22
const feedLimitPerType = 100
23
24
+
pulls, err := db.GetPullsWithLimit(rp.db, feedLimitPerType, orm.FilterEq("repo_at", repo.RepoAt()))
25
if err != nil {
26
return nil, err
27
}
···
29
issues, err := db.GetIssuesPaginated(
30
rp.db,
31
pagination.Page{Limit: feedLimitPerType},
32
+
orm.FilterEq("repo_at", repo.RepoAt()),
33
)
34
if err != nil {
35
return nil, err
+3
-2
appview/repo/index.go
+3
-2
appview/repo/index.go
···
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/xrpcclient"
26
"tangled.org/core/types"
27
28
"github.com/go-chi/chi/v5"
···
171
// first attempt to fetch from db
172
langs, err := db.GetRepoLanguages(
173
rp.db,
174
-
db.FilterEq("repo_at", repo.RepoAt()),
175
-
db.FilterEq("ref", currentRef),
176
)
177
178
if err != nil || langs == nil {
···
23
"tangled.org/core/appview/models"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/xrpcclient"
26
+
"tangled.org/core/orm"
27
"tangled.org/core/types"
28
29
"github.com/go-chi/chi/v5"
···
172
// first attempt to fetch from db
173
langs, err := db.GetRepoLanguages(
174
rp.db,
175
+
orm.FilterEq("repo_at", repo.RepoAt()),
176
+
orm.FilterEq("ref", currentRef),
177
)
178
179
if err != nil || langs == nil {
+3
-2
appview/repo/opengraph.go
+3
-2
appview/repo/opengraph.go
···
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/models"
18
"tangled.org/core/appview/ogcard"
19
"tangled.org/core/types"
20
)
21
···
338
var languageStats []types.RepoLanguageDetails
339
langs, err := db.GetRepoLanguages(
340
rp.db,
341
-
db.FilterEq("repo_at", f.RepoAt()),
342
-
db.FilterEq("is_default_ref", 1),
343
)
344
if err != nil {
345
log.Printf("failed to get language stats from db: %v", err)
···
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/models"
18
"tangled.org/core/appview/ogcard"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/types"
21
)
22
···
339
var languageStats []types.RepoLanguageDetails
340
langs, err := db.GetRepoLanguages(
341
rp.db,
342
+
orm.FilterEq("repo_at", f.RepoAt()),
343
+
orm.FilterEq("is_default_ref", 1),
344
)
345
if err != nil {
346
log.Printf("failed to get language stats from db: %v", err)
+17
-16
appview/repo/repo.go
+17
-16
appview/repo/repo.go
···
24
xrpcclient "tangled.org/core/appview/xrpcclient"
25
"tangled.org/core/eventconsumer"
26
"tangled.org/core/idresolver"
27
"tangled.org/core/rbac"
28
"tangled.org/core/tid"
29
"tangled.org/core/xrpc/serviceauth"
···
345
// get form values
346
labelId := r.FormValue("label-id")
347
348
-
label, err := db.GetLabelDefinition(rp.db, db.FilterEq("id", labelId))
349
if err != nil {
350
fail("Failed to find label definition.", err)
351
return
···
409
410
err = db.UnsubscribeLabel(
411
tx,
412
-
db.FilterEq("repo_at", f.RepoAt()),
413
-
db.FilterEq("label_at", removedAt),
414
)
415
if err != nil {
416
fail("Failed to unsubscribe label.", err)
417
return
418
}
419
420
-
err = db.DeleteLabelDefinition(tx, db.FilterEq("id", label.Id))
421
if err != nil {
422
fail("Failed to delete label definition.", err)
423
return
···
456
}
457
458
labelAts := r.Form["label"]
459
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
460
if err != nil {
461
fail("Failed to subscribe to label.", err)
462
return
···
542
}
543
544
labelAts := r.Form["label"]
545
-
_, err = db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", labelAts))
546
if err != nil {
547
fail("Failed to unsubscribe to label.", err)
548
return
···
582
583
err = db.UnsubscribeLabel(
584
rp.db,
585
-
db.FilterEq("repo_at", f.RepoAt()),
586
-
db.FilterIn("label_at", labelAts),
587
)
588
if err != nil {
589
fail("Failed to unsubscribe label.", err)
···
612
613
labelDefs, err := db.GetLabelDefinitions(
614
rp.db,
615
-
db.FilterIn("at_uri", f.Labels),
616
-
db.FilterContains("scope", subject.Collection().String()),
617
)
618
if err != nil {
619
l.Error("failed to fetch label defs", "err", err)
···
625
defs[l.AtUri().String()] = &l
626
}
627
628
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
629
if err != nil {
630
l.Error("failed to build label state", "err", err)
631
return
···
660
661
labelDefs, err := db.GetLabelDefinitions(
662
rp.db,
663
-
db.FilterIn("at_uri", f.Labels),
664
-
db.FilterContains("scope", subject.Collection().String()),
665
)
666
if err != nil {
667
l.Error("failed to fetch labels", "err", err)
···
673
defs[l.AtUri().String()] = &l
674
}
675
676
-
states, err := db.GetLabels(rp.db, db.FilterEq("subject", subject))
677
if err != nil {
678
l.Error("failed to build label state", "err", err)
679
return
···
1036
// in the user's account.
1037
existingRepo, err := db.GetRepo(
1038
rp.db,
1039
-
db.FilterEq("did", user.Did),
1040
-
db.FilterEq("name", forkName),
1041
)
1042
if err != nil {
1043
if !errors.Is(err, sql.ErrNoRows) {
···
24
xrpcclient "tangled.org/core/appview/xrpcclient"
25
"tangled.org/core/eventconsumer"
26
"tangled.org/core/idresolver"
27
+
"tangled.org/core/orm"
28
"tangled.org/core/rbac"
29
"tangled.org/core/tid"
30
"tangled.org/core/xrpc/serviceauth"
···
346
// get form values
347
labelId := r.FormValue("label-id")
348
349
+
label, err := db.GetLabelDefinition(rp.db, orm.FilterEq("id", labelId))
350
if err != nil {
351
fail("Failed to find label definition.", err)
352
return
···
410
411
err = db.UnsubscribeLabel(
412
tx,
413
+
orm.FilterEq("repo_at", f.RepoAt()),
414
+
orm.FilterEq("label_at", removedAt),
415
)
416
if err != nil {
417
fail("Failed to unsubscribe label.", err)
418
return
419
}
420
421
+
err = db.DeleteLabelDefinition(tx, orm.FilterEq("id", label.Id))
422
if err != nil {
423
fail("Failed to delete label definition.", err)
424
return
···
457
}
458
459
labelAts := r.Form["label"]
460
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
461
if err != nil {
462
fail("Failed to subscribe to label.", err)
463
return
···
543
}
544
545
labelAts := r.Form["label"]
546
+
_, err = db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", labelAts))
547
if err != nil {
548
fail("Failed to unsubscribe to label.", err)
549
return
···
583
584
err = db.UnsubscribeLabel(
585
rp.db,
586
+
orm.FilterEq("repo_at", f.RepoAt()),
587
+
orm.FilterIn("label_at", labelAts),
588
)
589
if err != nil {
590
fail("Failed to unsubscribe label.", err)
···
613
614
labelDefs, err := db.GetLabelDefinitions(
615
rp.db,
616
+
orm.FilterIn("at_uri", f.Labels),
617
+
orm.FilterContains("scope", subject.Collection().String()),
618
)
619
if err != nil {
620
l.Error("failed to fetch label defs", "err", err)
···
626
defs[l.AtUri().String()] = &l
627
}
628
629
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
630
if err != nil {
631
l.Error("failed to build label state", "err", err)
632
return
···
661
662
labelDefs, err := db.GetLabelDefinitions(
663
rp.db,
664
+
orm.FilterIn("at_uri", f.Labels),
665
+
orm.FilterContains("scope", subject.Collection().String()),
666
)
667
if err != nil {
668
l.Error("failed to fetch labels", "err", err)
···
674
defs[l.AtUri().String()] = &l
675
}
676
677
+
states, err := db.GetLabels(rp.db, orm.FilterEq("subject", subject))
678
if err != nil {
679
l.Error("failed to build label state", "err", err)
680
return
···
1037
// in the user's account.
1038
existingRepo, err := db.GetRepo(
1039
rp.db,
1040
+
orm.FilterEq("did", user.Did),
1041
+
orm.FilterEq("name", forkName),
1042
)
1043
if err != nil {
1044
if !errors.Is(err, sql.ErrNoRows) {
+5
-4
appview/repo/repo_util.go
+5
-4
appview/repo/repo_util.go
···
8
9
"tangled.org/core/appview/db"
10
"tangled.org/core/appview/models"
11
"tangled.org/core/types"
12
)
13
···
102
ps, err := db.GetPipelineStatuses(
103
d,
104
len(shas),
105
-
db.FilterEq("repo_owner", repo.Did),
106
-
db.FilterEq("repo_name", repo.Name),
107
-
db.FilterEq("knot", repo.Knot),
108
-
db.FilterIn("sha", shas),
109
)
110
if err != nil {
111
return nil, err
···
8
9
"tangled.org/core/appview/db"
10
"tangled.org/core/appview/models"
11
+
"tangled.org/core/orm"
12
"tangled.org/core/types"
13
)
14
···
103
ps, err := db.GetPipelineStatuses(
104
d,
105
len(shas),
106
+
orm.FilterEq("repo_owner", repo.Did),
107
+
orm.FilterEq("repo_name", repo.Name),
108
+
orm.FilterEq("knot", repo.Knot),
109
+
orm.FilterIn("sha", shas),
110
)
111
if err != nil {
112
return nil, err
+3
-2
appview/repo/settings.go
+3
-2
appview/repo/settings.go
···
14
"tangled.org/core/appview/oauth"
15
"tangled.org/core/appview/pages"
16
xrpcclient "tangled.org/core/appview/xrpcclient"
17
"tangled.org/core/types"
18
19
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
210
return
211
}
212
213
-
defaultLabels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
214
if err != nil {
215
l.Error("failed to fetch labels", "err", err)
216
rp.pages.Error503(w)
217
return
218
}
219
220
-
labels, err := db.GetLabelDefinitions(rp.db, db.FilterIn("at_uri", f.Labels))
221
if err != nil {
222
l.Error("failed to fetch labels", "err", err)
223
rp.pages.Error503(w)
···
14
"tangled.org/core/appview/oauth"
15
"tangled.org/core/appview/pages"
16
xrpcclient "tangled.org/core/appview/xrpcclient"
17
+
"tangled.org/core/orm"
18
"tangled.org/core/types"
19
20
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
211
return
212
}
213
214
+
defaultLabels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", rp.config.Label.DefaultLabelDefs))
215
if err != nil {
216
l.Error("failed to fetch labels", "err", err)
217
rp.pages.Error503(w)
218
return
219
}
220
221
+
labels, err := db.GetLabelDefinitions(rp.db, orm.FilterIn("at_uri", f.Labels))
222
if err != nil {
223
l.Error("failed to fetch labels", "err", err)
224
rp.pages.Error503(w)
+5
-4
appview/serververify/verify.go
+5
-4
appview/serververify/verify.go
···
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/db"
11
"tangled.org/core/appview/xrpcclient"
12
"tangled.org/core/rbac"
13
)
14
···
76
// mark this spindle as verified in the db
77
rowId, err := db.VerifySpindle(
78
tx,
79
-
db.FilterEq("owner", owner),
80
-
db.FilterEq("instance", instance),
81
)
82
if err != nil {
83
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
115
// mark as registered
116
err = db.MarkRegistered(
117
tx,
118
-
db.FilterEq("did", owner),
119
-
db.FilterEq("domain", domain),
120
)
121
if err != nil {
122
return fmt.Errorf("failed to register domain: %w", err)
···
9
"tangled.org/core/api/tangled"
10
"tangled.org/core/appview/db"
11
"tangled.org/core/appview/xrpcclient"
12
+
"tangled.org/core/orm"
13
"tangled.org/core/rbac"
14
)
15
···
77
// mark this spindle as verified in the db
78
rowId, err := db.VerifySpindle(
79
tx,
80
+
orm.FilterEq("owner", owner),
81
+
orm.FilterEq("instance", instance),
82
)
83
if err != nil {
84
return 0, fmt.Errorf("failed to write to DB: %w", err)
···
116
// mark as registered
117
err = db.MarkRegistered(
118
tx,
119
+
orm.FilterEq("did", owner),
120
+
orm.FilterEq("domain", domain),
121
)
122
if err != nil {
123
return fmt.Errorf("failed to register domain: %w", err)
+25
-24
appview/spindles/spindles.go
+25
-24
appview/spindles/spindles.go
···
20
"tangled.org/core/appview/serververify"
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/idresolver"
23
"tangled.org/core/rbac"
24
"tangled.org/core/tid"
25
···
71
user := s.OAuth.GetUser(r)
72
all, err := db.GetSpindles(
73
s.Db,
74
-
db.FilterEq("owner", user.Did),
75
)
76
if err != nil {
77
s.Logger.Error("failed to fetch spindles", "err", err)
···
101
102
spindles, err := db.GetSpindles(
103
s.Db,
104
-
db.FilterEq("instance", instance),
105
-
db.FilterEq("owner", user.Did),
106
-
db.FilterIsNot("verified", "null"),
107
)
108
if err != nil || len(spindles) != 1 {
109
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
123
repos, err := db.GetRepos(
124
s.Db,
125
0,
126
-
db.FilterEq("spindle", instance),
127
)
128
if err != nil {
129
l.Error("failed to get spindle repos", "err", err)
···
290
291
spindles, err := db.GetSpindles(
292
s.Db,
293
-
db.FilterEq("owner", user.Did),
294
-
db.FilterEq("instance", instance),
295
)
296
if err != nil || len(spindles) != 1 {
297
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
319
// remove spindle members first
320
err = db.RemoveSpindleMember(
321
tx,
322
-
db.FilterEq("did", user.Did),
323
-
db.FilterEq("instance", instance),
324
)
325
if err != nil {
326
l.Error("failed to remove spindle members", "err", err)
···
330
331
err = db.DeleteSpindle(
332
tx,
333
-
db.FilterEq("owner", user.Did),
334
-
db.FilterEq("instance", instance),
335
)
336
if err != nil {
337
l.Error("failed to delete spindle", "err", err)
···
410
411
spindles, err := db.GetSpindles(
412
s.Db,
413
-
db.FilterEq("owner", user.Did),
414
-
db.FilterEq("instance", instance),
415
)
416
if err != nil || len(spindles) != 1 {
417
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
453
454
verifiedSpindle, err := db.GetSpindles(
455
s.Db,
456
-
db.FilterEq("id", rowId),
457
)
458
if err != nil || len(verifiedSpindle) != 1 {
459
l.Error("failed get new spindle", "err", err)
···
486
487
spindles, err := db.GetSpindles(
488
s.Db,
489
-
db.FilterEq("owner", user.Did),
490
-
db.FilterEq("instance", instance),
491
)
492
if err != nil || len(spindles) != 1 {
493
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
622
623
spindles, err := db.GetSpindles(
624
s.Db,
625
-
db.FilterEq("owner", user.Did),
626
-
db.FilterEq("instance", instance),
627
)
628
if err != nil || len(spindles) != 1 {
629
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
672
// get the record from the DB first:
673
members, err := db.GetSpindleMembers(
674
s.Db,
675
-
db.FilterEq("did", user.Did),
676
-
db.FilterEq("instance", instance),
677
-
db.FilterEq("subject", memberId.DID),
678
)
679
if err != nil || len(members) != 1 {
680
l.Error("failed to get member", "err", err)
···
685
// remove from db
686
if err = db.RemoveSpindleMember(
687
tx,
688
-
db.FilterEq("did", user.Did),
689
-
db.FilterEq("instance", instance),
690
-
db.FilterEq("subject", memberId.DID),
691
); err != nil {
692
l.Error("failed to remove spindle member", "err", err)
693
fail()
···
20
"tangled.org/core/appview/serververify"
21
"tangled.org/core/appview/xrpcclient"
22
"tangled.org/core/idresolver"
23
+
"tangled.org/core/orm"
24
"tangled.org/core/rbac"
25
"tangled.org/core/tid"
26
···
72
user := s.OAuth.GetUser(r)
73
all, err := db.GetSpindles(
74
s.Db,
75
+
orm.FilterEq("owner", user.Did),
76
)
77
if err != nil {
78
s.Logger.Error("failed to fetch spindles", "err", err)
···
102
103
spindles, err := db.GetSpindles(
104
s.Db,
105
+
orm.FilterEq("instance", instance),
106
+
orm.FilterEq("owner", user.Did),
107
+
orm.FilterIsNot("verified", "null"),
108
)
109
if err != nil || len(spindles) != 1 {
110
l.Error("failed to get spindle", "err", err, "len(spindles)", len(spindles))
···
124
repos, err := db.GetRepos(
125
s.Db,
126
0,
127
+
orm.FilterEq("spindle", instance),
128
)
129
if err != nil {
130
l.Error("failed to get spindle repos", "err", err)
···
291
292
spindles, err := db.GetSpindles(
293
s.Db,
294
+
orm.FilterEq("owner", user.Did),
295
+
orm.FilterEq("instance", instance),
296
)
297
if err != nil || len(spindles) != 1 {
298
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
320
// remove spindle members first
321
err = db.RemoveSpindleMember(
322
tx,
323
+
orm.FilterEq("did", user.Did),
324
+
orm.FilterEq("instance", instance),
325
)
326
if err != nil {
327
l.Error("failed to remove spindle members", "err", err)
···
331
332
err = db.DeleteSpindle(
333
tx,
334
+
orm.FilterEq("owner", user.Did),
335
+
orm.FilterEq("instance", instance),
336
)
337
if err != nil {
338
l.Error("failed to delete spindle", "err", err)
···
411
412
spindles, err := db.GetSpindles(
413
s.Db,
414
+
orm.FilterEq("owner", user.Did),
415
+
orm.FilterEq("instance", instance),
416
)
417
if err != nil || len(spindles) != 1 {
418
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
454
455
verifiedSpindle, err := db.GetSpindles(
456
s.Db,
457
+
orm.FilterEq("id", rowId),
458
)
459
if err != nil || len(verifiedSpindle) != 1 {
460
l.Error("failed get new spindle", "err", err)
···
487
488
spindles, err := db.GetSpindles(
489
s.Db,
490
+
orm.FilterEq("owner", user.Did),
491
+
orm.FilterEq("instance", instance),
492
)
493
if err != nil || len(spindles) != 1 {
494
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
623
624
spindles, err := db.GetSpindles(
625
s.Db,
626
+
orm.FilterEq("owner", user.Did),
627
+
orm.FilterEq("instance", instance),
628
)
629
if err != nil || len(spindles) != 1 {
630
l.Error("failed to retrieve instance", "err", err, "len(spindles)", len(spindles))
···
673
// get the record from the DB first:
674
members, err := db.GetSpindleMembers(
675
s.Db,
676
+
orm.FilterEq("did", user.Did),
677
+
orm.FilterEq("instance", instance),
678
+
orm.FilterEq("subject", memberId.DID),
679
)
680
if err != nil || len(members) != 1 {
681
l.Error("failed to get member", "err", err)
···
686
// remove from db
687
if err = db.RemoveSpindleMember(
688
tx,
689
+
orm.FilterEq("did", user.Did),
690
+
orm.FilterEq("instance", instance),
691
+
orm.FilterEq("subject", memberId.DID),
692
); err != nil {
693
l.Error("failed to remove spindle member", "err", err)
694
fail()
+6
-5
appview/state/gfi.go
+6
-5
appview/state/gfi.go
···
11
"tangled.org/core/appview/pages"
12
"tangled.org/core/appview/pagination"
13
"tangled.org/core/consts"
14
)
15
16
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
20
21
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
22
23
-
gfiLabelDef, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", goodFirstIssueLabel))
24
if err != nil {
25
log.Println("failed to get gfi label def", err)
26
s.pages.Error500(w)
27
return
28
}
29
30
-
repoLabels, err := db.GetRepoLabels(s.db, db.FilterEq("label_at", goodFirstIssueLabel))
31
if err != nil {
32
log.Println("failed to get repo labels", err)
33
s.pages.Error503(w)
···
55
pagination.Page{
56
Limit: 500,
57
},
58
-
db.FilterIn("repo_at", repoUris),
59
-
db.FilterEq("open", 1),
60
)
61
if err != nil {
62
log.Println("failed to get issues", err)
···
132
}
133
134
if len(uriList) > 0 {
135
-
allLabelDefs, err = db.GetLabelDefinitions(s.db, db.FilterIn("at_uri", uriList))
136
if err != nil {
137
log.Println("failed to fetch labels", err)
138
}
···
11
"tangled.org/core/appview/pages"
12
"tangled.org/core/appview/pagination"
13
"tangled.org/core/consts"
14
+
"tangled.org/core/orm"
15
)
16
17
func (s *State) GoodFirstIssues(w http.ResponseWriter, r *http.Request) {
···
21
22
goodFirstIssueLabel := s.config.Label.GoodFirstIssue
23
24
+
gfiLabelDef, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", goodFirstIssueLabel))
25
if err != nil {
26
log.Println("failed to get gfi label def", err)
27
s.pages.Error500(w)
28
return
29
}
30
31
+
repoLabels, err := db.GetRepoLabels(s.db, orm.FilterEq("label_at", goodFirstIssueLabel))
32
if err != nil {
33
log.Println("failed to get repo labels", err)
34
s.pages.Error503(w)
···
56
pagination.Page{
57
Limit: 500,
58
},
59
+
orm.FilterIn("repo_at", repoUris),
60
+
orm.FilterEq("open", 1),
61
)
62
if err != nil {
63
log.Println("failed to get issues", err)
···
133
}
134
135
if len(uriList) > 0 {
136
+
allLabelDefs, err = db.GetLabelDefinitions(s.db, orm.FilterIn("at_uri", uriList))
137
if err != nil {
138
log.Println("failed to fetch labels", err)
139
}
+17
appview/state/git_http.go
+17
appview/state/git_http.go
···
25
26
}
27
28
+
func (s *State) UploadArchive(w http.ResponseWriter, r *http.Request) {
29
+
user, ok := r.Context().Value("resolvedId").(identity.Identity)
30
+
if !ok {
31
+
http.Error(w, "failed to resolve user", http.StatusInternalServerError)
32
+
return
33
+
}
34
+
repo := r.Context().Value("repo").(*models.Repo)
35
+
36
+
scheme := "https"
37
+
if s.config.Core.Dev {
38
+
scheme = "http"
39
+
}
40
+
41
+
targetURL := fmt.Sprintf("%s://%s/%s/%s/git-upload-archive?%s", scheme, repo.Knot, user.DID, repo.Name, r.URL.RawQuery)
42
+
s.proxyRequest(w, r, targetURL)
43
+
}
44
+
45
func (s *State) UploadPack(w http.ResponseWriter, r *http.Request) {
46
user, ok := r.Context().Value("resolvedId").(identity.Identity)
47
if !ok {
+6
-5
appview/state/knotstream.go
+6
-5
appview/state/knotstream.go
···
16
ec "tangled.org/core/eventconsumer"
17
"tangled.org/core/eventconsumer/cursor"
18
"tangled.org/core/log"
19
"tangled.org/core/rbac"
20
"tangled.org/core/workflow"
21
···
30
31
knots, err := db.GetRegistrations(
32
d,
33
-
db.FilterIsNot("registered", "null"),
34
)
35
if err != nil {
36
return nil, err
···
143
repos, err := db.GetRepos(
144
d,
145
0,
146
-
db.FilterEq("did", record.RepoDid),
147
-
db.FilterEq("name", record.RepoName),
148
)
149
if err != nil {
150
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
209
repos, err := db.GetRepos(
210
d,
211
0,
212
-
db.FilterEq("did", record.TriggerMetadata.Repo.Did),
213
-
db.FilterEq("name", record.TriggerMetadata.Repo.Repo),
214
)
215
if err != nil {
216
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
···
16
ec "tangled.org/core/eventconsumer"
17
"tangled.org/core/eventconsumer/cursor"
18
"tangled.org/core/log"
19
+
"tangled.org/core/orm"
20
"tangled.org/core/rbac"
21
"tangled.org/core/workflow"
22
···
31
32
knots, err := db.GetRegistrations(
33
d,
34
+
orm.FilterIsNot("registered", "null"),
35
)
36
if err != nil {
37
return nil, err
···
144
repos, err := db.GetRepos(
145
d,
146
0,
147
+
orm.FilterEq("did", record.RepoDid),
148
+
orm.FilterEq("name", record.RepoName),
149
)
150
if err != nil {
151
return fmt.Errorf("failed to look for repo in DB (%s/%s): %w", record.RepoDid, record.RepoName, err)
···
210
repos, err := db.GetRepos(
211
d,
212
0,
213
+
orm.FilterEq("did", record.TriggerMetadata.Repo.Did),
214
+
orm.FilterEq("name", record.TriggerMetadata.Repo.Repo),
215
)
216
if err != nil {
217
return fmt.Errorf("failed to look for repo in DB: nsid %s, rkey %s, %w", msg.Nsid, msg.Rkey, err)
+13
-12
appview/state/profile.go
+13
-12
appview/state/profile.go
···
19
"tangled.org/core/appview/db"
20
"tangled.org/core/appview/models"
21
"tangled.org/core/appview/pages"
22
)
23
24
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
56
return nil, fmt.Errorf("failed to get profile: %w", err)
57
}
58
59
-
repoCount, err := db.CountRepos(s.db, db.FilterEq("did", did))
60
if err != nil {
61
return nil, fmt.Errorf("failed to get repo count: %w", err)
62
}
63
64
-
stringCount, err := db.CountStrings(s.db, db.FilterEq("did", did))
65
if err != nil {
66
return nil, fmt.Errorf("failed to get string count: %w", err)
67
}
68
69
-
starredCount, err := db.CountStars(s.db, db.FilterEq("did", did))
70
if err != nil {
71
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
72
}
···
86
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
87
punchcard, err := db.MakePunchcard(
88
s.db,
89
-
db.FilterEq("did", did),
90
-
db.FilterGte("date", startOfYear.Format(time.DateOnly)),
91
-
db.FilterLte("date", now.Format(time.DateOnly)),
92
)
93
if err != nil {
94
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
123
repos, err := db.GetRepos(
124
s.db,
125
0,
126
-
db.FilterEq("did", profile.UserDid),
127
)
128
if err != nil {
129
l.Error("failed to fetch repos", "err", err)
···
193
repos, err := db.GetRepos(
194
s.db,
195
0,
196
-
db.FilterEq("did", profile.UserDid),
197
)
198
if err != nil {
199
l.Error("failed to get repos", "err", err)
···
219
}
220
l = l.With("profileDid", profile.UserDid)
221
222
-
stars, err := db.GetRepoStars(s.db, 0, db.FilterEq("did", profile.UserDid))
223
if err != nil {
224
l.Error("failed to get stars", "err", err)
225
s.pages.Error500(w)
···
248
}
249
l = l.With("profileDid", profile.UserDid)
250
251
-
strings, err := db.GetStrings(s.db, 0, db.FilterEq("did", profile.UserDid))
252
if err != nil {
253
l.Error("failed to get strings", "err", err)
254
s.pages.Error500(w)
···
300
followDids = append(followDids, extractDid(follow))
301
}
302
303
-
profiles, err := db.GetProfiles(s.db, db.FilterIn("did", followDids))
304
if err != nil {
305
l.Error("failed to get profiles", "followDids", followDids, "err", err)
306
return ¶ms, err
···
703
log.Printf("getting profile data for %s: %s", user.Did, err)
704
}
705
706
-
repos, err := db.GetRepos(s.db, 0, db.FilterEq("did", user.Did))
707
if err != nil {
708
log.Printf("getting repos for %s: %s", user.Did, err)
709
}
···
19
"tangled.org/core/appview/db"
20
"tangled.org/core/appview/models"
21
"tangled.org/core/appview/pages"
22
+
"tangled.org/core/orm"
23
)
24
25
func (s *State) Profile(w http.ResponseWriter, r *http.Request) {
···
57
return nil, fmt.Errorf("failed to get profile: %w", err)
58
}
59
60
+
repoCount, err := db.CountRepos(s.db, orm.FilterEq("did", did))
61
if err != nil {
62
return nil, fmt.Errorf("failed to get repo count: %w", err)
63
}
64
65
+
stringCount, err := db.CountStrings(s.db, orm.FilterEq("did", did))
66
if err != nil {
67
return nil, fmt.Errorf("failed to get string count: %w", err)
68
}
69
70
+
starredCount, err := db.CountStars(s.db, orm.FilterEq("did", did))
71
if err != nil {
72
return nil, fmt.Errorf("failed to get starred repo count: %w", err)
73
}
···
87
startOfYear := time.Date(now.Year(), 1, 1, 0, 0, 0, 0, time.UTC)
88
punchcard, err := db.MakePunchcard(
89
s.db,
90
+
orm.FilterEq("did", did),
91
+
orm.FilterGte("date", startOfYear.Format(time.DateOnly)),
92
+
orm.FilterLte("date", now.Format(time.DateOnly)),
93
)
94
if err != nil {
95
return nil, fmt.Errorf("failed to get punchcard for %s: %w", did, err)
···
124
repos, err := db.GetRepos(
125
s.db,
126
0,
127
+
orm.FilterEq("did", profile.UserDid),
128
)
129
if err != nil {
130
l.Error("failed to fetch repos", "err", err)
···
194
repos, err := db.GetRepos(
195
s.db,
196
0,
197
+
orm.FilterEq("did", profile.UserDid),
198
)
199
if err != nil {
200
l.Error("failed to get repos", "err", err)
···
220
}
221
l = l.With("profileDid", profile.UserDid)
222
223
+
stars, err := db.GetRepoStars(s.db, 0, orm.FilterEq("did", profile.UserDid))
224
if err != nil {
225
l.Error("failed to get stars", "err", err)
226
s.pages.Error500(w)
···
249
}
250
l = l.With("profileDid", profile.UserDid)
251
252
+
strings, err := db.GetStrings(s.db, 0, orm.FilterEq("did", profile.UserDid))
253
if err != nil {
254
l.Error("failed to get strings", "err", err)
255
s.pages.Error500(w)
···
301
followDids = append(followDids, extractDid(follow))
302
}
303
304
+
profiles, err := db.GetProfiles(s.db, orm.FilterIn("did", followDids))
305
if err != nil {
306
l.Error("failed to get profiles", "followDids", followDids, "err", err)
307
return ¶ms, err
···
704
log.Printf("getting profile data for %s: %s", user.Did, err)
705
}
706
707
+
repos, err := db.GetRepos(s.db, 0, orm.FilterEq("did", user.Did))
708
if err != nil {
709
log.Printf("getting repos for %s: %s", user.Did, err)
710
}
+3
-2
appview/state/router.go
+3
-2
appview/state/router.go
···
101
102
// These routes get proxied to the knot
103
r.Get("/info/refs", s.InfoRefs)
104
r.Post("/git-upload-pack", s.UploadPack)
105
r.Post("/git-receive-pack", s.ReceivePack)
106
···
266
s.enforcer,
267
s.pages,
268
s.idResolver,
269
-
s.refResolver,
270
s.db,
271
s.config,
272
s.notifier,
···
283
s.repoResolver,
284
s.pages,
285
s.idResolver,
286
-
s.refResolver,
287
s.db,
288
s.config,
289
s.notifier,
···
101
102
// These routes get proxied to the knot
103
r.Get("/info/refs", s.InfoRefs)
104
+
r.Post("/git-upload-archive", s.UploadArchive)
105
r.Post("/git-upload-pack", s.UploadPack)
106
r.Post("/git-receive-pack", s.ReceivePack)
107
···
267
s.enforcer,
268
s.pages,
269
s.idResolver,
270
+
s.mentionsResolver,
271
s.db,
272
s.config,
273
s.notifier,
···
284
s.repoResolver,
285
s.pages,
286
s.idResolver,
287
+
s.mentionsResolver,
288
s.db,
289
s.config,
290
s.notifier,
+2
-1
appview/state/spindlestream.go
+2
-1
appview/state/spindlestream.go
···
17
ec "tangled.org/core/eventconsumer"
18
"tangled.org/core/eventconsumer/cursor"
19
"tangled.org/core/log"
20
"tangled.org/core/rbac"
21
spindle "tangled.org/core/spindle/models"
22
)
···
27
28
spindles, err := db.GetSpindles(
29
d,
30
-
db.FilterIsNot("verified", "null"),
31
)
32
if err != nil {
33
return nil, err
···
17
ec "tangled.org/core/eventconsumer"
18
"tangled.org/core/eventconsumer/cursor"
19
"tangled.org/core/log"
20
+
"tangled.org/core/orm"
21
"tangled.org/core/rbac"
22
spindle "tangled.org/core/spindle/models"
23
)
···
28
29
spindles, err := db.GetSpindles(
30
d,
31
+
orm.FilterIsNot("verified", "null"),
32
)
33
if err != nil {
34
return nil, err
+28
-27
appview/state/state.go
+28
-27
appview/state/state.go
···
15
"tangled.org/core/appview/config"
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/indexer"
18
"tangled.org/core/appview/models"
19
"tangled.org/core/appview/notify"
20
dbnotify "tangled.org/core/appview/notify/db"
21
phnotify "tangled.org/core/appview/notify/posthog"
22
"tangled.org/core/appview/oauth"
23
"tangled.org/core/appview/pages"
24
-
"tangled.org/core/appview/refresolver"
25
"tangled.org/core/appview/reporesolver"
26
"tangled.org/core/appview/validator"
27
xrpcclient "tangled.org/core/appview/xrpcclient"
···
30
"tangled.org/core/jetstream"
31
"tangled.org/core/log"
32
tlog "tangled.org/core/log"
33
"tangled.org/core/rbac"
34
"tangled.org/core/tid"
35
···
43
)
44
45
type State struct {
46
-
db *db.DB
47
-
notifier notify.Notifier
48
-
indexer *indexer.Indexer
49
-
oauth *oauth.OAuth
50
-
enforcer *rbac.Enforcer
51
-
pages *pages.Pages
52
-
idResolver *idresolver.Resolver
53
-
refResolver *refresolver.Resolver
54
-
posthog posthog.Client
55
-
jc *jetstream.JetstreamClient
56
-
config *config.Config
57
-
repoResolver *reporesolver.RepoResolver
58
-
knotstream *eventconsumer.Consumer
59
-
spindlestream *eventconsumer.Consumer
60
-
logger *slog.Logger
61
-
validator *validator.Validator
62
}
63
64
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
100
101
repoResolver := reporesolver.New(config, enforcer, d)
102
103
-
refResolver := refresolver.New(config, res, d, log.SubLogger(logger, "refResolver"))
104
105
wrapper := db.DbWrapper{Execer: d}
106
jc, err := jetstream.NewJetstreamClient(
···
182
enforcer,
183
pages,
184
res,
185
-
refResolver,
186
posthog,
187
jc,
188
config,
···
299
return
300
}
301
302
-
gfiLabel, err := db.GetLabelDefinition(s.db, db.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
303
if err != nil {
304
// non-fatal
305
}
···
323
324
regs, err := db.GetRegistrations(
325
s.db,
326
-
db.FilterEq("did", user.Did),
327
-
db.FilterEq("needs_upgrade", 1),
328
)
329
if err != nil {
330
l.Error("non-fatal: failed to get registrations", "err", err)
···
332
333
spindles, err := db.GetSpindles(
334
s.db,
335
-
db.FilterEq("owner", user.Did),
336
-
db.FilterEq("needs_upgrade", 1),
337
)
338
if err != nil {
339
l.Error("non-fatal: failed to get spindles", "err", err)
···
504
// Check for existing repos
505
existingRepo, err := db.GetRepo(
506
s.db,
507
-
db.FilterEq("did", user.Did),
508
-
db.FilterEq("name", repoName),
509
)
510
if err == nil && existingRepo != nil {
511
l.Info("repo exists")
···
665
}
666
667
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
668
-
defaultLabels, err := db.GetLabelDefinitions(e, db.FilterIn("at_uri", defaults))
669
if err != nil {
670
return err
671
}
···
15
"tangled.org/core/appview/config"
16
"tangled.org/core/appview/db"
17
"tangled.org/core/appview/indexer"
18
+
"tangled.org/core/appview/mentions"
19
"tangled.org/core/appview/models"
20
"tangled.org/core/appview/notify"
21
dbnotify "tangled.org/core/appview/notify/db"
22
phnotify "tangled.org/core/appview/notify/posthog"
23
"tangled.org/core/appview/oauth"
24
"tangled.org/core/appview/pages"
25
"tangled.org/core/appview/reporesolver"
26
"tangled.org/core/appview/validator"
27
xrpcclient "tangled.org/core/appview/xrpcclient"
···
30
"tangled.org/core/jetstream"
31
"tangled.org/core/log"
32
tlog "tangled.org/core/log"
33
+
"tangled.org/core/orm"
34
"tangled.org/core/rbac"
35
"tangled.org/core/tid"
36
···
44
)
45
46
type State struct {
47
+
db *db.DB
48
+
notifier notify.Notifier
49
+
indexer *indexer.Indexer
50
+
oauth *oauth.OAuth
51
+
enforcer *rbac.Enforcer
52
+
pages *pages.Pages
53
+
idResolver *idresolver.Resolver
54
+
mentionsResolver *mentions.Resolver
55
+
posthog posthog.Client
56
+
jc *jetstream.JetstreamClient
57
+
config *config.Config
58
+
repoResolver *reporesolver.RepoResolver
59
+
knotstream *eventconsumer.Consumer
60
+
spindlestream *eventconsumer.Consumer
61
+
logger *slog.Logger
62
+
validator *validator.Validator
63
}
64
65
func Make(ctx context.Context, config *config.Config) (*State, error) {
···
101
102
repoResolver := reporesolver.New(config, enforcer, d)
103
104
+
mentionsResolver := mentions.New(config, res, d, log.SubLogger(logger, "mentionsResolver"))
105
106
wrapper := db.DbWrapper{Execer: d}
107
jc, err := jetstream.NewJetstreamClient(
···
183
enforcer,
184
pages,
185
res,
186
+
mentionsResolver,
187
posthog,
188
jc,
189
config,
···
300
return
301
}
302
303
+
gfiLabel, err := db.GetLabelDefinition(s.db, orm.FilterEq("at_uri", s.config.Label.GoodFirstIssue))
304
if err != nil {
305
// non-fatal
306
}
···
324
325
regs, err := db.GetRegistrations(
326
s.db,
327
+
orm.FilterEq("did", user.Did),
328
+
orm.FilterEq("needs_upgrade", 1),
329
)
330
if err != nil {
331
l.Error("non-fatal: failed to get registrations", "err", err)
···
333
334
spindles, err := db.GetSpindles(
335
s.db,
336
+
orm.FilterEq("owner", user.Did),
337
+
orm.FilterEq("needs_upgrade", 1),
338
)
339
if err != nil {
340
l.Error("non-fatal: failed to get spindles", "err", err)
···
505
// Check for existing repos
506
existingRepo, err := db.GetRepo(
507
s.db,
508
+
orm.FilterEq("did", user.Did),
509
+
orm.FilterEq("name", repoName),
510
)
511
if err == nil && existingRepo != nil {
512
l.Info("repo exists")
···
666
}
667
668
func BackfillDefaultDefs(e db.Execer, r *idresolver.Resolver, defaults []string) error {
669
+
defaultLabels, err := db.GetLabelDefinitions(e, orm.FilterIn("at_uri", defaults))
670
if err != nil {
671
return err
672
}
+7
-6
appview/strings/strings.go
+7
-6
appview/strings/strings.go
···
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/pages/markup"
19
"tangled.org/core/idresolver"
20
"tangled.org/core/tid"
21
22
"github.com/bluesky-social/indigo/api/atproto"
···
108
strings, err := db.GetStrings(
109
s.Db,
110
0,
111
-
db.FilterEq("did", id.DID),
112
-
db.FilterEq("rkey", rkey),
113
)
114
if err != nil {
115
l.Error("failed to fetch string", "err", err)
···
199
all, err := db.GetStrings(
200
s.Db,
201
0,
202
-
db.FilterEq("did", id.DID),
203
-
db.FilterEq("rkey", rkey),
204
)
205
if err != nil {
206
l.Error("failed to fetch string", "err", err)
···
408
409
if err := db.DeleteString(
410
s.Db,
411
-
db.FilterEq("did", user.Did),
412
-
db.FilterEq("rkey", rkey),
413
); err != nil {
414
fail("Failed to delete string.", err)
415
return
···
17
"tangled.org/core/appview/pages"
18
"tangled.org/core/appview/pages/markup"
19
"tangled.org/core/idresolver"
20
+
"tangled.org/core/orm"
21
"tangled.org/core/tid"
22
23
"github.com/bluesky-social/indigo/api/atproto"
···
109
strings, err := db.GetStrings(
110
s.Db,
111
0,
112
+
orm.FilterEq("did", id.DID),
113
+
orm.FilterEq("rkey", rkey),
114
)
115
if err != nil {
116
l.Error("failed to fetch string", "err", err)
···
200
all, err := db.GetStrings(
201
s.Db,
202
0,
203
+
orm.FilterEq("did", id.DID),
204
+
orm.FilterEq("rkey", rkey),
205
)
206
if err != nil {
207
l.Error("failed to fetch string", "err", err)
···
409
410
if err := db.DeleteString(
411
s.Db,
412
+
orm.FilterEq("did", user.Did),
413
+
orm.FilterEq("rkey", rkey),
414
); err != nil {
415
fail("Failed to delete string.", err)
416
return
+2
-1
appview/validator/issue.go
+2
-1
appview/validator/issue.go
···
6
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
)
10
11
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
12
// if comments have parents, only ingest ones that are 1 level deep
13
if comment.ReplyTo != nil {
14
-
parents, err := db.GetIssueComments(v.db, db.FilterEq("at_uri", *comment.ReplyTo))
15
if err != nil {
16
return fmt.Errorf("failed to fetch parent comment: %w", err)
17
}
···
6
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
+
"tangled.org/core/orm"
10
)
11
12
func (v *Validator) ValidateIssueComment(comment *models.IssueComment) error {
13
// if comments have parents, only ingest ones that are 1 level deep
14
if comment.ReplyTo != nil {
15
+
parents, err := db.GetIssueComments(v.db, orm.FilterEq("at_uri", *comment.ReplyTo))
16
if err != nil {
17
return fmt.Errorf("failed to fetch parent comment: %w", err)
18
}
+9
-9
flake.lock
+9
-9
flake.lock
···
35
"systems": "systems"
36
},
37
"locked": {
38
-
"lastModified": 1694529238,
39
-
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
40
"owner": "numtide",
41
"repo": "flake-utils",
42
-
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
43
"type": "github"
44
},
45
"original": {
···
56
]
57
},
58
"locked": {
59
-
"lastModified": 1754078208,
60
-
"narHash": "sha256-YVoIFDCDpYuU3riaDEJ3xiGdPOtsx4sR5eTzHTytPV8=",
61
"owner": "nix-community",
62
"repo": "gomod2nix",
63
-
"rev": "7f963246a71626c7fc70b431a315c4388a0c95cf",
64
"type": "github"
65
},
66
"original": {
···
150
},
151
"nixpkgs": {
152
"locked": {
153
-
"lastModified": 1751984180,
154
-
"narHash": "sha256-LwWRsENAZJKUdD3SpLluwDmdXY9F45ZEgCb0X+xgOL0=",
155
"owner": "nixos",
156
"repo": "nixpkgs",
157
-
"rev": "9807714d6944a957c2e036f84b0ff8caf9930bc0",
158
"type": "github"
159
},
160
"original": {
···
35
"systems": "systems"
36
},
37
"locked": {
38
+
"lastModified": 1731533236,
39
+
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
40
"owner": "numtide",
41
"repo": "flake-utils",
42
+
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
43
"type": "github"
44
},
45
"original": {
···
56
]
57
},
58
"locked": {
59
+
"lastModified": 1763982521,
60
+
"narHash": "sha256-ur4QIAHwgFc0vXiaxn5No/FuZicxBr2p0gmT54xZkUQ=",
61
"owner": "nix-community",
62
"repo": "gomod2nix",
63
+
"rev": "02e63a239d6eabd595db56852535992c898eba72",
64
"type": "github"
65
},
66
"original": {
···
150
},
151
"nixpkgs": {
152
"locked": {
153
+
"lastModified": 1766070988,
154
+
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
155
"owner": "nixos",
156
"repo": "nixpkgs",
157
+
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
158
"type": "github"
159
},
160
"original": {
+31
-4
flake.nix
+31
-4
flake.nix
···
80
}).buildGoApplication;
81
modules = ./nix/gomod2nix.toml;
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
83
-
inherit (pkgs) gcc;
84
inherit sqlite-lib-src;
85
};
86
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
92
spindle = self.callPackage ./nix/pkgs/spindle.nix {};
93
knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {};
94
knot = self.callPackage ./nix/pkgs/knot.nix {};
95
});
96
in {
97
overlays.default = final: prev: {
98
-
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview;
99
};
100
101
packages = forAllSystems (system: let
···
104
staticPackages = mkPackageSet pkgs.pkgsStatic;
105
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
106
in {
107
-
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib;
108
109
pkgsStatic-appview = staticPackages.appview;
110
pkgsStatic-knot = staticPackages.knot;
···
156
nativeBuildInputs = [
157
pkgs.go
158
pkgs.air
159
-
pkgs.tilt
160
pkgs.gopls
161
pkgs.httpie
162
pkgs.litecli
···
304
imports = [./nix/modules/spindle.nix];
305
306
services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle;
307
};
308
};
309
}
···
80
}).buildGoApplication;
81
modules = ./nix/gomod2nix.toml;
82
sqlite-lib = self.callPackage ./nix/pkgs/sqlite-lib.nix {
83
inherit sqlite-lib-src;
84
};
85
lexgen = self.callPackage ./nix/pkgs/lexgen.nix {inherit indigo;};
···
91
spindle = self.callPackage ./nix/pkgs/spindle.nix {};
92
knot-unwrapped = self.callPackage ./nix/pkgs/knot-unwrapped.nix {};
93
knot = self.callPackage ./nix/pkgs/knot.nix {};
94
+
did-method-plc = self.callPackage ./nix/pkgs/did-method-plc.nix {};
95
+
bluesky-jetstream = self.callPackage ./nix/pkgs/bluesky-jetstream.nix {};
96
+
bluesky-relay = self.callPackage ./nix/pkgs/bluesky-relay.nix {};
97
+
tap = self.callPackage ./nix/pkgs/tap.nix {};
98
});
99
in {
100
overlays.default = final: prev: {
101
+
inherit (mkPackageSet final) lexgen goat sqlite-lib spindle knot-unwrapped knot appview did-method-plc bluesky-jetstream bluesky-relay tap;
102
};
103
104
packages = forAllSystems (system: let
···
107
staticPackages = mkPackageSet pkgs.pkgsStatic;
108
crossPackages = mkPackageSet pkgs.pkgsCross.gnu64.pkgsStatic;
109
in {
110
+
inherit (packages) appview appview-static-files lexgen goat spindle knot knot-unwrapped sqlite-lib did-method-plc bluesky-jetstream bluesky-relay tap;
111
112
pkgsStatic-appview = staticPackages.appview;
113
pkgsStatic-knot = staticPackages.knot;
···
159
nativeBuildInputs = [
160
pkgs.go
161
pkgs.air
162
pkgs.gopls
163
pkgs.httpie
164
pkgs.litecli
···
306
imports = [./nix/modules/spindle.nix];
307
308
services.tangled.spindle.package = lib.mkDefault self.packages.${pkgs.stdenv.hostPlatform.system}.spindle;
309
+
services.tangled.spindle.tap-package = lib.mkDefault self.packages.${pkgs.system}.tap;
310
+
};
311
+
nixosModules.did-method-plc = {
312
+
lib,
313
+
pkgs,
314
+
...
315
+
}: {
316
+
imports = [./nix/modules/did-method-plc.nix];
317
+
services.did-method-plc.package = lib.mkDefault self.packages.${pkgs.system}.did-method-plc;
318
+
};
319
+
nixosModules.bluesky-relay = {
320
+
lib,
321
+
pkgs,
322
+
...
323
+
}: {
324
+
imports = [./nix/modules/bluesky-relay.nix];
325
+
services.bluesky-relay.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-relay;
326
+
};
327
+
nixosModules.bluesky-jetstream = {
328
+
lib,
329
+
pkgs,
330
+
...
331
+
}: {
332
+
imports = [./nix/modules/bluesky-jetstream.nix];
333
+
services.bluesky-jetstream.package = lib.mkDefault self.packages.${pkgs.system}.bluesky-jetstream;
334
};
335
};
336
}
+4
-4
go.mod
+4
-4
go.mod
···
1
module tangled.org/core
2
3
-
go 1.24.4
4
5
require (
6
github.com/Blank-Xu/sql-adapter v1.1.1
···
44
github.com/stretchr/testify v1.10.0
45
github.com/urfave/cli/v3 v3.3.3
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
-
github.com/wyatt915/goldmark-treeblood v0.0.1
48
github.com/yuin/goldmark v1.7.13
49
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
50
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
51
golang.org/x/crypto v0.40.0
52
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
53
golang.org/x/image v0.31.0
54
golang.org/x/net v0.42.0
55
-
golang.org/x/sync v0.17.0
56
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
57
gopkg.in/yaml.v3 v3.0.1
58
)
···
132
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
133
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
134
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
135
github.com/hashicorp/golang-lru v1.0.2 // indirect
136
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
137
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
···
190
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
191
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
192
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
193
-
github.com/wyatt915/treeblood v0.1.16 // indirect
194
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
195
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
196
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
···
205
go.uber.org/atomic v1.11.0 // indirect
206
go.uber.org/multierr v1.11.0 // indirect
207
go.uber.org/zap v1.27.0 // indirect
208
golang.org/x/sys v0.34.0 // indirect
209
golang.org/x/text v0.29.0 // indirect
210
golang.org/x/time v0.12.0 // indirect
···
1
module tangled.org/core
2
3
+
go 1.25.0
4
5
require (
6
github.com/Blank-Xu/sql-adapter v1.1.1
···
44
github.com/stretchr/testify v1.10.0
45
github.com/urfave/cli/v3 v3.3.3
46
github.com/whyrusleeping/cbor-gen v0.3.1
47
github.com/yuin/goldmark v1.7.13
48
+
github.com/yuin/goldmark-emoji v1.0.6
49
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
50
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab
51
golang.org/x/crypto v0.40.0
52
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
53
golang.org/x/image v0.31.0
54
golang.org/x/net v0.42.0
55
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da
56
gopkg.in/yaml.v3 v3.0.1
57
)
···
131
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
132
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
133
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
134
+
github.com/hashicorp/go-version v1.8.0 // indirect
135
github.com/hashicorp/golang-lru v1.0.2 // indirect
136
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
137
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
···
190
github.com/vmihailenco/go-tinylfu v0.2.2 // indirect
191
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
192
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
193
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
194
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
195
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
···
204
go.uber.org/atomic v1.11.0 // indirect
205
go.uber.org/multierr v1.11.0 // indirect
206
go.uber.org/zap v1.27.0 // indirect
207
+
golang.org/x/sync v0.17.0 // indirect
208
golang.org/x/sys v0.34.0 // indirect
209
golang.org/x/text v0.29.0 // indirect
210
golang.org/x/time v0.12.0 // indirect
+4
-4
go.sum
+4
-4
go.sum
···
264
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
265
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
266
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
267
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
268
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
269
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
495
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
496
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
497
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
498
-
github.com/wyatt915/goldmark-treeblood v0.0.1 h1:6vLJcjFrHgE4ASu2ga4hqIQmbvQLU37v53jlHZ3pqDs=
499
-
github.com/wyatt915/goldmark-treeblood v0.0.1/go.mod h1:SmcJp5EBaV17rroNlgNQFydYwy0+fv85CUr/ZaCz208=
500
-
github.com/wyatt915/treeblood v0.1.16 h1:byxNbWZhnPDxdTp7W5kQhCeaY8RBVmojTFz1tEHgg8Y=
501
-
github.com/wyatt915/treeblood v0.1.16/go.mod h1:i7+yhhmzdDP17/97pIsOSffw74EK/xk+qJ0029cSXUY=
502
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
503
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
504
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
···
509
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
510
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
511
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
512
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
513
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
514
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
···
264
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
265
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
266
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
267
+
github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=
268
+
github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
269
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
270
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
271
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
···
497
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
498
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
499
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
500
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
501
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
502
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
···
507
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
508
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
509
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
510
+
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
511
+
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
512
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc h1:+IAOyRda+RLrxa1WC7umKOZRsGq4QrFFMYApOeHzQwQ=
513
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
514
gitlab.com/staticnoise/goldmark-callout v0.0.0-20240609120641-6366b799e4ab h1:gK9tS6QJw5F0SIhYJnGG2P83kuabOdmWBbSmZhJkz2A=
+4
-4
hook/hook.go
+4
-4
hook/hook.go
···
48
},
49
Commands: []*cli.Command{
50
{
51
-
Name: "post-recieve",
52
-
Usage: "sends a post-recieve hook to the knot (waits for stdin)",
53
-
Action: postRecieve,
54
},
55
},
56
}
57
}
58
59
-
func postRecieve(ctx context.Context, cmd *cli.Command) error {
60
gitDir := cmd.String("git-dir")
61
userDid := cmd.String("user-did")
62
userHandle := cmd.String("user-handle")
···
48
},
49
Commands: []*cli.Command{
50
{
51
+
Name: "post-receive",
52
+
Usage: "sends a post-receive hook to the knot (waits for stdin)",
53
+
Action: postReceive,
54
},
55
},
56
}
57
}
58
59
+
func postReceive(ctx context.Context, cmd *cli.Command) error {
60
gitDir := cmd.String("git-dir")
61
userDid := cmd.String("user-did")
62
userHandle := cmd.String("user-handle")
+1
-1
hook/setup.go
+1
-1
hook/setup.go
···
138
option_var="GIT_PUSH_OPTION_$i"
139
push_options+=(-push-option "${!option_var}")
140
done
141
-
%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-recieve
142
`, executablePath, config.internalApi)
143
144
return os.WriteFile(hookPath, []byte(hookContent), 0755)
···
138
option_var="GIT_PUSH_OPTION_$i"
139
push_options+=(-push-option "${!option_var}")
140
done
141
+
%s hook -git-dir "$GIT_DIR" -user-did "$GIT_USER_DID" -user-handle "$GIT_USER_HANDLE" -internal-api "%s" "${push_options[@]}" post-receive
142
`, executablePath, config.internalApi)
143
144
return os.WriteFile(hookPath, []byte(hookContent), 0755)
+81
knotserver/db/db.go
+81
knotserver/db/db.go
···
···
1
+
package db
2
+
3
+
import (
4
+
"context"
5
+
"database/sql"
6
+
"log/slog"
7
+
"strings"
8
+
9
+
_ "github.com/mattn/go-sqlite3"
10
+
"tangled.org/core/log"
11
+
)
12
+
13
+
type DB struct {
14
+
db *sql.DB
15
+
logger *slog.Logger
16
+
}
17
+
18
+
func Setup(ctx context.Context, dbPath string) (*DB, error) {
19
+
// https://github.com/mattn/go-sqlite3#connection-string
20
+
opts := []string{
21
+
"_foreign_keys=1",
22
+
"_journal_mode=WAL",
23
+
"_synchronous=NORMAL",
24
+
"_auto_vacuum=incremental",
25
+
}
26
+
27
+
logger := log.FromContext(ctx)
28
+
logger = log.SubLogger(logger, "db")
29
+
30
+
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
31
+
if err != nil {
32
+
return nil, err
33
+
}
34
+
35
+
conn, err := db.Conn(ctx)
36
+
if err != nil {
37
+
return nil, err
38
+
}
39
+
defer conn.Close()
40
+
41
+
_, err = conn.ExecContext(ctx, `
42
+
create table if not exists known_dids (
43
+
did text primary key
44
+
);
45
+
46
+
create table if not exists public_keys (
47
+
id integer primary key autoincrement,
48
+
did text not null,
49
+
key text not null,
50
+
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
51
+
unique(did, key),
52
+
foreign key (did) references known_dids(did) on delete cascade
53
+
);
54
+
55
+
create table if not exists _jetstream (
56
+
id integer primary key autoincrement,
57
+
last_time_us integer not null
58
+
);
59
+
60
+
create table if not exists events (
61
+
rkey text not null,
62
+
nsid text not null,
63
+
event text not null, -- json
64
+
created integer not null default (strftime('%s', 'now')),
65
+
primary key (rkey, nsid)
66
+
);
67
+
68
+
create table if not exists migrations (
69
+
id integer primary key autoincrement,
70
+
name text unique
71
+
);
72
+
`)
73
+
if err != nil {
74
+
return nil, err
75
+
}
76
+
77
+
return &DB{
78
+
db: db,
79
+
logger: logger,
80
+
}, nil
81
+
}
-64
knotserver/db/init.go
-64
knotserver/db/init.go
···
1
-
package db
2
-
3
-
import (
4
-
"database/sql"
5
-
"strings"
6
-
7
-
_ "github.com/mattn/go-sqlite3"
8
-
)
9
-
10
-
type DB struct {
11
-
db *sql.DB
12
-
}
13
-
14
-
func Setup(dbPath string) (*DB, error) {
15
-
// https://github.com/mattn/go-sqlite3#connection-string
16
-
opts := []string{
17
-
"_foreign_keys=1",
18
-
"_journal_mode=WAL",
19
-
"_synchronous=NORMAL",
20
-
"_auto_vacuum=incremental",
21
-
}
22
-
23
-
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
24
-
if err != nil {
25
-
return nil, err
26
-
}
27
-
28
-
// NOTE: If any other migration is added here, you MUST
29
-
// copy the pattern in appview: use a single sql.Conn
30
-
// for every migration.
31
-
32
-
_, err = db.Exec(`
33
-
create table if not exists known_dids (
34
-
did text primary key
35
-
);
36
-
37
-
create table if not exists public_keys (
38
-
id integer primary key autoincrement,
39
-
did text not null,
40
-
key text not null,
41
-
created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
42
-
unique(did, key),
43
-
foreign key (did) references known_dids(did) on delete cascade
44
-
);
45
-
46
-
create table if not exists _jetstream (
47
-
id integer primary key autoincrement,
48
-
last_time_us integer not null
49
-
);
50
-
51
-
create table if not exists events (
52
-
rkey text not null,
53
-
nsid text not null,
54
-
event text not null, -- json
55
-
created integer not null default (strftime('%s', 'now')),
56
-
primary key (rkey, nsid)
57
-
);
58
-
`)
59
-
if err != nil {
60
-
return nil, err
61
-
}
62
-
63
-
return &DB{db: db}, nil
64
-
}
···
+13
-1
knotserver/git/service/service.go
+13
-1
knotserver/git/service/service.go
···
95
return c.RunService(cmd)
96
}
97
98
+
func (c *ServiceCommand) UploadArchive() error {
99
+
cmd := exec.Command("git", []string{
100
+
"upload-archive",
101
+
".",
102
+
}...)
103
+
104
+
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
105
+
cmd.Env = append(cmd.Env, fmt.Sprintf("GIT_PROTOCOL=%s", c.GitProtocol))
106
+
cmd.Dir = c.Dir
107
+
108
+
return c.RunService(cmd)
109
+
}
110
+
111
func (c *ServiceCommand) UploadPack() error {
112
cmd := exec.Command("git", []string{
113
"upload-pack",
114
"--stateless-rpc",
115
".",
+47
knotserver/git.go
+47
knotserver/git.go
···
56
}
57
}
58
59
+
func (h *Knot) UploadArchive(w http.ResponseWriter, r *http.Request) {
60
+
did := chi.URLParam(r, "did")
61
+
name := chi.URLParam(r, "name")
62
+
repo, err := securejoin.SecureJoin(h.c.Repo.ScanPath, filepath.Join(did, name))
63
+
if err != nil {
64
+
gitError(w, err.Error(), http.StatusInternalServerError)
65
+
h.l.Error("git: failed to secure join repo path", "handler", "UploadPack", "error", err)
66
+
return
67
+
}
68
+
69
+
const expectedContentType = "application/x-git-upload-archive-request"
70
+
contentType := r.Header.Get("Content-Type")
71
+
if contentType != expectedContentType {
72
+
gitError(w, fmt.Sprintf("Expected Content-Type: '%s', but received '%s'.", expectedContentType, contentType), http.StatusUnsupportedMediaType)
73
+
}
74
+
75
+
var bodyReader io.ReadCloser = r.Body
76
+
if r.Header.Get("Content-Encoding") == "gzip" {
77
+
gzipReader, err := gzip.NewReader(r.Body)
78
+
if err != nil {
79
+
gitError(w, err.Error(), http.StatusInternalServerError)
80
+
h.l.Error("git: failed to create gzip reader", "handler", "UploadArchive", "error", err)
81
+
return
82
+
}
83
+
defer gzipReader.Close()
84
+
bodyReader = gzipReader
85
+
}
86
+
87
+
w.Header().Set("Content-Type", "application/x-git-upload-archive-result")
88
+
89
+
h.l.Info("git: executing git-upload-archive", "handler", "UploadArchive", "repo", repo)
90
+
91
+
cmd := service.ServiceCommand{
92
+
GitProtocol: r.Header.Get("Git-Protocol"),
93
+
Dir: repo,
94
+
Stdout: w,
95
+
Stdin: bodyReader,
96
+
}
97
+
98
+
w.WriteHeader(http.StatusOK)
99
+
100
+
if err := cmd.UploadArchive(); err != nil {
101
+
h.l.Error("git: failed to execute git-upload-pack", "handler", "UploadPack", "error", err)
102
+
return
103
+
}
104
+
}
105
+
106
func (h *Knot) UploadPack(w http.ResponseWriter, r *http.Request) {
107
did := chi.URLParam(r, "did")
108
name := chi.URLParam(r, "name")
+1
knotserver/router.go
+1
knotserver/router.go
+1
-1
knotserver/server.go
+1
-1
knotserver/server.go
+33
lexicons/pipeline/cancelPipeline.json
+33
lexicons/pipeline/cancelPipeline.json
···
···
1
+
{
2
+
"lexicon": 1,
3
+
"id": "sh.tangled.pipeline.cancelPipeline",
4
+
"defs": {
5
+
"main": {
6
+
"type": "procedure",
7
+
"description": "Cancel a running pipeline",
8
+
"input": {
9
+
"encoding": "application/json",
10
+
"schema": {
11
+
"type": "object",
12
+
"required": ["repo", "pipeline", "workflow"],
13
+
"properties": {
14
+
"repo": {
15
+
"type": "string",
16
+
"format": "at-uri",
17
+
"description": "repo at-uri, spindle can't resolve repo from pipeline at-uri yet"
18
+
},
19
+
"pipeline": {
20
+
"type": "string",
21
+
"format": "at-uri",
22
+
"description": "pipeline at-uri"
23
+
},
24
+
"workflow": {
25
+
"type": "string",
26
+
"description": "workflow name"
27
+
}
28
+
}
29
+
}
30
+
}
31
+
}
32
+
}
33
+
}
+6
-30
nix/gomod2nix.toml
+6
-30
nix/gomod2nix.toml
···
165
[mod."github.com/davecgh/go-spew"]
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
-
[mod."github.com/decred/dcrd/dcrec/secp256k1/v4"]
169
-
version = "v4.4.0"
170
-
hash = "sha256-qrhEIwhDll3cxoVpMbm1NQ9/HTI42S7ms8Buzlo5HCg="
171
[mod."github.com/dgraph-io/ristretto"]
172
version = "v0.2.0"
173
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
307
[mod."github.com/hashicorp/go-sockaddr"]
308
version = "v1.0.7"
309
hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs="
310
[mod."github.com/hashicorp/golang-lru"]
311
version = "v1.0.2"
312
hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="
···
373
[mod."github.com/klauspost/cpuid/v2"]
374
version = "v2.3.0"
375
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
376
-
[mod."github.com/lestrrat-go/blackmagic"]
377
-
version = "v1.0.4"
378
-
hash = "sha256-HmWOpwoPDNMwLdOi7onNn3Sb+ZsAa3Ai3gVBbXmQ0e8="
379
-
[mod."github.com/lestrrat-go/httpcc"]
380
-
version = "v1.0.1"
381
-
hash = "sha256-SMRSwJpqDIs/xL0l2e8vP0W65qtCHX2wigcOeqPJmos="
382
-
[mod."github.com/lestrrat-go/httprc"]
383
-
version = "v1.0.6"
384
-
hash = "sha256-mfZzePEhrmyyu/avEBd2MsDXyto8dq5+fyu5lA8GUWM="
385
-
[mod."github.com/lestrrat-go/iter"]
386
-
version = "v1.0.2"
387
-
hash = "sha256-30tErRf7Qu/NOAt1YURXY/XJSA6sCr6hYQfO8QqHrtw="
388
-
[mod."github.com/lestrrat-go/jwx/v2"]
389
-
version = "v2.1.6"
390
-
hash = "sha256-0LszXRZIba+X8AOrs3T4uanAUafBdlVB8/MpUNEFpbc="
391
-
[mod."github.com/lestrrat-go/option"]
392
-
version = "v1.0.1"
393
-
hash = "sha256-jVcIYYVsxElIS/l2akEw32vdEPR8+anR6oeT1FoYULI="
394
[mod."github.com/lucasb-eyer/go-colorful"]
395
version = "v1.2.0"
396
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
511
[mod."github.com/ryanuber/go-glob"]
512
version = "v1.0.0"
513
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
514
-
[mod."github.com/segmentio/asm"]
515
-
version = "v1.2.0"
516
-
hash = "sha256-zbNuKxNrUDUc6IlmRQNuJQzVe5Ol/mqp7srDg9IMMqs="
517
[mod."github.com/sergi/go-diff"]
518
version = "v1.1.0"
519
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
548
[mod."github.com/whyrusleeping/cbor-gen"]
549
version = "v0.3.1"
550
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
551
-
[mod."github.com/wyatt915/goldmark-treeblood"]
552
-
version = "v0.0.1"
553
-
hash = "sha256-hAVFaktO02MiiqZFffr8ZlvFEfwxw4Y84OZ2t7e5G7g="
554
-
[mod."github.com/wyatt915/treeblood"]
555
-
version = "v0.1.16"
556
-
hash = "sha256-T68sa+iVx0qY7dDjXEAJvRWQEGXYIpUsf9tcWwO1tIw="
557
[mod."github.com/xo/terminfo"]
558
version = "v0.0.0-20220910002029-abceb7e1c41e"
559
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
560
[mod."github.com/yuin/goldmark"]
561
version = "v1.7.13"
562
hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE="
563
[mod."github.com/yuin/goldmark-highlighting/v2"]
564
version = "v2.0.0-20230729083705-37449abec8cc"
565
hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
···
165
[mod."github.com/davecgh/go-spew"]
166
version = "v1.1.2-0.20180830191138-d8f796af33cc"
167
hash = "sha256-fV9oI51xjHdOmEx6+dlq7Ku2Ag+m/bmbzPo6A4Y74qc="
168
[mod."github.com/dgraph-io/ristretto"]
169
version = "v0.2.0"
170
hash = "sha256-bnpxX+oO/Qf7IJevA0gsbloVoqRx+5bh7RQ9d9eLNYw="
···
304
[mod."github.com/hashicorp/go-sockaddr"]
305
version = "v1.0.7"
306
hash = "sha256-p6eDOrGzN1jMmT/F/f/VJMq0cKNFhUcEuVVwTE6vSrs="
307
+
[mod."github.com/hashicorp/go-version"]
308
+
version = "v1.8.0"
309
+
hash = "sha256-KXtqERmYrWdpqPCViWcHbe6jnuH7k16bvBIcuJuevj8="
310
[mod."github.com/hashicorp/golang-lru"]
311
version = "v1.0.2"
312
hash = "sha256-yy+5botc6T5wXgOe2mfNXJP3wr+MkVlUZ2JBkmmrA48="
···
373
[mod."github.com/klauspost/cpuid/v2"]
374
version = "v2.3.0"
375
hash = "sha256-50JhbQyT67BK38HIdJihPtjV7orYp96HknI2VP7A9Yc="
376
[mod."github.com/lucasb-eyer/go-colorful"]
377
version = "v1.2.0"
378
hash = "sha256-Gg9dDJFCTaHrKHRR1SrJgZ8fWieJkybljybkI9x0gyE="
···
493
[mod."github.com/ryanuber/go-glob"]
494
version = "v1.0.0"
495
hash = "sha256-YkMl1utwUhi3E0sHK23ISpAsPyj4+KeXyXKoFYGXGVY="
496
[mod."github.com/sergi/go-diff"]
497
version = "v1.1.0"
498
hash = "sha256-8NJMabldpf40uwQN20T6QXx5KORDibCBJL02KD661xY="
···
527
[mod."github.com/whyrusleeping/cbor-gen"]
528
version = "v0.3.1"
529
hash = "sha256-PAd8M2Z8t6rVRBII+Rg8Bz+QaJIwbW64bfyqsv31kgc="
530
[mod."github.com/xo/terminfo"]
531
version = "v0.0.0-20220910002029-abceb7e1c41e"
532
hash = "sha256-GyCDxxMQhXA3Pi/TsWXpA8cX5akEoZV7CFx4RO3rARU="
533
[mod."github.com/yuin/goldmark"]
534
version = "v1.7.13"
535
hash = "sha256-vBCxZrPYPc8x/nvAAv3Au59dCCyfS80Vw3/a9EXK7TE="
536
+
[mod."github.com/yuin/goldmark-emoji"]
537
+
version = "v1.0.6"
538
+
hash = "sha256-+d6bZzOPE+JSFsZbQNZMCWE+n3jgcQnkPETVk47mxSY="
539
[mod."github.com/yuin/goldmark-highlighting/v2"]
540
version = "v2.0.0-20230729083705-37449abec8cc"
541
hash = "sha256-HpiwU7jIeDUAg2zOpTIiviQir8dpRPuXYh2nqFFccpg="
+64
nix/modules/bluesky-jetstream.nix
+64
nix/modules/bluesky-jetstream.nix
···
···
1
+
{
2
+
config,
3
+
pkgs,
4
+
lib,
5
+
...
6
+
}: let
7
+
cfg = config.services.bluesky-jetstream;
8
+
in
9
+
with lib; {
10
+
options.services.bluesky-jetstream = {
11
+
enable = mkEnableOption "jetstream server";
12
+
package = mkPackageOption pkgs "bluesky-jetstream" {};
13
+
14
+
# dataDir = mkOption {
15
+
# type = types.str;
16
+
# default = "/var/lib/jetstream";
17
+
# description = "directory to store data (pebbleDB)";
18
+
# };
19
+
livenessTtl = mkOption {
20
+
type = types.int;
21
+
default = 15;
22
+
description = "time to restart when no event detected (seconds)";
23
+
};
24
+
websocketUrl = mkOption {
25
+
type = types.str;
26
+
default = "wss://bsky.network/xrpc/com.atproto.sync.subscribeRepos";
27
+
description = "full websocket path to the ATProto SubscribeRepos XRPC endpoint";
28
+
};
29
+
};
30
+
config = mkIf cfg.enable {
31
+
systemd.services.bluesky-jetstream = {
32
+
description = "bluesky jetstream";
33
+
after = ["network.target" "pds.service"];
34
+
wantedBy = ["multi-user.target"];
35
+
36
+
serviceConfig = {
37
+
User = "jetstream";
38
+
Group = "jetstream";
39
+
StateDirectory = "jetstream";
40
+
StateDirectoryMode = "0755";
41
+
# preStart = ''
42
+
# mkdir -p "${cfg.dataDir}"
43
+
# chown -R jetstream:jetstream "${cfg.dataDir}"
44
+
# '';
45
+
# WorkingDirectory = cfg.dataDir;
46
+
Environment = [
47
+
"JETSTREAM_DATA_DIR=/var/lib/jetstream/data"
48
+
"JETSTREAM_LIVENESS_TTL=${toString cfg.livenessTtl}s"
49
+
"JETSTREAM_WS_URL=${cfg.websocketUrl}"
50
+
];
51
+
ExecStart = getExe cfg.package;
52
+
Restart = "always";
53
+
RestartSec = 5;
54
+
};
55
+
};
56
+
users = {
57
+
users.jetstream = {
58
+
group = "jetstream";
59
+
isSystemUser = true;
60
+
};
61
+
groups.jetstream = {};
62
+
};
63
+
};
64
+
}
+48
nix/modules/bluesky-relay.nix
+48
nix/modules/bluesky-relay.nix
···
···
1
+
{
2
+
config,
3
+
pkgs,
4
+
lib,
5
+
...
6
+
}: let
7
+
cfg = config.services.bluesky-relay;
8
+
in
9
+
with lib; {
10
+
options.services.bluesky-relay = {
11
+
enable = mkEnableOption "relay server";
12
+
package = mkPackageOption pkgs "bluesky-relay" {};
13
+
};
14
+
config = mkIf cfg.enable {
15
+
systemd.services.bluesky-relay = {
16
+
description = "bluesky relay";
17
+
after = ["network.target" "pds.service"];
18
+
wantedBy = ["multi-user.target"];
19
+
20
+
serviceConfig = {
21
+
User = "relay";
22
+
Group = "relay";
23
+
StateDirectory = "relay";
24
+
StateDirectoryMode = "0755";
25
+
Environment = [
26
+
"RELAY_ADMIN_PASSWORD=password"
27
+
"RELAY_PLC_HOST=https://plc.tngl.boltless.dev"
28
+
"DATABASE_URL=sqlite:///var/lib/relay/relay.sqlite"
29
+
"RELAY_IP_BIND=:2470"
30
+
"RELAY_PERSIST_DIR=/var/lib/relay"
31
+
"RELAY_DISABLE_REQUEST_CRAWL=0"
32
+
"RELAY_INITIAL_SEQ_NUMBER=1"
33
+
"RELAY_ALLOW_INSECURE_HOSTS=1"
34
+
];
35
+
ExecStart = "${getExe cfg.package} serve";
36
+
Restart = "always";
37
+
RestartSec = 5;
38
+
};
39
+
};
40
+
users = {
41
+
users.relay = {
42
+
group = "relay";
43
+
isSystemUser = true;
44
+
};
45
+
groups.relay = {};
46
+
};
47
+
};
48
+
}
+76
nix/modules/did-method-plc.nix
+76
nix/modules/did-method-plc.nix
···
···
1
+
{
2
+
config,
3
+
pkgs,
4
+
lib,
5
+
...
6
+
}: let
7
+
cfg = config.services.did-method-plc;
8
+
in
9
+
with lib; {
10
+
options.services.did-method-plc = {
11
+
enable = mkEnableOption "did-method-plc server";
12
+
package = mkPackageOption pkgs "did-method-plc" {};
13
+
};
14
+
config = mkIf cfg.enable {
15
+
services.postgresql = {
16
+
enable = true;
17
+
package = pkgs.postgresql_14;
18
+
ensureDatabases = ["plc"];
19
+
ensureUsers = [
20
+
{
21
+
name = "pg";
22
+
# ensurePermissions."DATABASE plc" = "ALL PRIVILEGES";
23
+
}
24
+
];
25
+
authentication = ''
26
+
local all all trust
27
+
host all all 127.0.0.1/32 trust
28
+
'';
29
+
};
30
+
systemd.services.did-method-plc = {
31
+
description = "did-method-plc";
32
+
33
+
after = ["postgresql.service"];
34
+
wants = ["postgresql.service"];
35
+
wantedBy = ["multi-user.target"];
36
+
37
+
environment = let
38
+
db_creds_json = builtins.toJSON {
39
+
username = "pg";
40
+
password = "";
41
+
host = "127.0.0.1";
42
+
port = 5432;
43
+
};
44
+
in {
45
+
# TODO: inherit from config
46
+
DEBUG_MODE = "1";
47
+
LOG_ENABLED = "true";
48
+
LOG_LEVEL = "debug";
49
+
LOG_DESTINATION = "1";
50
+
ENABLE_MIGRATIONS = "true";
51
+
DB_CREDS_JSON = db_creds_json;
52
+
DB_MIGRATE_CREDS_JSON = db_creds_json;
53
+
PLC_VERSION = "0.0.1";
54
+
PORT = "8080";
55
+
};
56
+
57
+
serviceConfig = {
58
+
ExecStart = getExe cfg.package;
59
+
User = "plc";
60
+
Group = "plc";
61
+
StateDirectory = "plc";
62
+
StateDirectoryMode = "0755";
63
+
Restart = "always";
64
+
65
+
# Hardening
66
+
};
67
+
};
68
+
users = {
69
+
users.plc = {
70
+
group = "plc";
71
+
isSystemUser = true;
72
+
};
73
+
groups.plc = {};
74
+
};
75
+
};
76
+
}
+35
nix/modules/spindle.nix
+35
nix/modules/spindle.nix
···
1
{
2
config,
3
lib,
4
...
5
}: let
···
16
package = mkOption {
17
type = types.package;
18
description = "Package to use for the spindle";
19
};
20
21
server = {
···
114
config = mkIf cfg.enable {
115
virtualisation.docker.enable = true;
116
117
systemd.services.spindle = {
118
description = "spindle service";
119
after = ["network.target" "docker.service"];
120
wantedBy = ["multi-user.target"];
121
serviceConfig = {
122
LogsDirectory = "spindle";
123
StateDirectory = "spindle";
···
1
{
2
config,
3
+
pkgs,
4
lib,
5
...
6
}: let
···
17
package = mkOption {
18
type = types.package;
19
description = "Package to use for the spindle";
20
+
};
21
+
tap-package = mkOption {
22
+
type = types.package;
23
+
description = "Package to use for the spindle";
24
+
};
25
+
26
+
atpRelayUrl = mkOption {
27
+
type = types.str;
28
+
default = "https://relay1.us-east.bsky.network";
29
+
description = "atproto relay";
30
};
31
32
server = {
···
125
config = mkIf cfg.enable {
126
virtualisation.docker.enable = true;
127
128
+
systemd.services.spindle-tap = {
129
+
description = "spindle tap service";
130
+
after = ["network.target" "docker.service"];
131
+
wantedBy = ["multi-user.target"];
132
+
serviceConfig = {
133
+
LogsDirectory = "spindle-tap";
134
+
StateDirectory = "spindle-tap";
135
+
Environment = [
136
+
"TAP_BIND=:2480"
137
+
"TAP_PLC_URL=${cfg.server.plcUrl}"
138
+
"TAP_RELAY_URL=${cfg.atpRelayUrl}"
139
+
"TAP_COLLECTION_FILTERS=${concatStringsSep "," [
140
+
"sh.tangled.repo"
141
+
"sh.tangled.repo.collaborator"
142
+
"sh.tangled.spindle.member"
143
+
]}"
144
+
];
145
+
ExecStart = "${getExe cfg.tap-package} run";
146
+
};
147
+
};
148
+
149
systemd.services.spindle = {
150
description = "spindle service";
151
after = ["network.target" "docker.service"];
152
wantedBy = ["multi-user.target"];
153
+
path = [
154
+
pkgs.git
155
+
];
156
serviceConfig = {
157
LogsDirectory = "spindle";
158
StateDirectory = "spindle";
+20
nix/pkgs/bluesky-jetstream.nix
+20
nix/pkgs/bluesky-jetstream.nix
···
···
1
+
{
2
+
buildGoModule,
3
+
fetchFromGitHub,
4
+
}:
5
+
buildGoModule {
6
+
pname = "bluesky-jetstream";
7
+
version = "0.1.0";
8
+
src = fetchFromGitHub {
9
+
owner = "bluesky-social";
10
+
repo = "jetstream";
11
+
rev = "7d7efa58d7f14101a80ccc4f1085953948b7d5de";
12
+
sha256 = "sha256-1e9SL/8gaDPMA4YZed51ffzgpkptbMd0VTbTTDbPTFw=";
13
+
};
14
+
subPackages = ["cmd/jetstream"];
15
+
vendorHash = "sha256-/21XJQH6fo9uPzlABUAbdBwt1O90odmppH6gXu2wkiQ=";
16
+
doCheck = false;
17
+
meta = {
18
+
mainProgram = "jetstream";
19
+
};
20
+
}
+20
nix/pkgs/bluesky-relay.nix
+20
nix/pkgs/bluesky-relay.nix
···
···
1
+
{
2
+
buildGoModule,
3
+
fetchFromGitHub,
4
+
}:
5
+
buildGoModule {
6
+
pname = "bluesky-relay";
7
+
version = "0.1.0";
8
+
src = fetchFromGitHub {
9
+
owner = "boltlessengineer";
10
+
repo = "indigo";
11
+
rev = "b769ea60b7dde5e2bd0b8ee3ce8462a0c0e596fe";
12
+
sha256 = "sha256-jHRY825TBYaH1WkKFUoNbo4UlMSyuHvCGjYPiBnKo44=";
13
+
};
14
+
subPackages = ["cmd/relay"];
15
+
vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8=";
16
+
doCheck = false;
17
+
meta = {
18
+
mainProgram = "relay";
19
+
};
20
+
}
+65
nix/pkgs/did-method-plc.nix
+65
nix/pkgs/did-method-plc.nix
···
···
1
+
# inspired by https://github.com/NixOS/nixpkgs/blob/333bfb7c258fab089a834555ea1c435674c459b4/pkgs/by-name/ga/gatsby-cli/package.nix
2
+
{
3
+
lib,
4
+
stdenv,
5
+
fetchFromGitHub,
6
+
fetchYarnDeps,
7
+
yarnConfigHook,
8
+
yarnBuildHook,
9
+
nodejs,
10
+
makeBinaryWrapper,
11
+
}:
12
+
stdenv.mkDerivation (finalAttrs: {
13
+
pname = "did-method-plc";
14
+
version = "0.0.1";
15
+
16
+
src = fetchFromGitHub {
17
+
owner = "did-method-plc";
18
+
repo = "did-method-plc";
19
+
rev = "158ba5535ac3da4fd4309954bde41deab0b45972";
20
+
sha256 = "sha256-O5smubbrnTDMCvL6iRyMXkddr5G7YHxkQRVMRULHanQ=";
21
+
};
22
+
postPatch = ''
23
+
# remove dd-trace dependency
24
+
sed -i '3d' packages/server/service/index.js
25
+
'';
26
+
27
+
yarnOfflineCache = fetchYarnDeps {
28
+
yarnLock = finalAttrs.src + "/yarn.lock";
29
+
hash = "sha256-g8GzaAbWSnWwbQjJMV2DL5/ZlWCCX0sRkjjvX3tqU4Y=";
30
+
};
31
+
32
+
nativeBuildInputs = [
33
+
yarnConfigHook
34
+
yarnBuildHook
35
+
nodejs
36
+
makeBinaryWrapper
37
+
];
38
+
yarnBuildScript = "lerna";
39
+
yarnBuildFlags = [
40
+
"run"
41
+
"build"
42
+
"--scope"
43
+
"@did-plc/server"
44
+
"--include-dependencies"
45
+
];
46
+
47
+
installPhase = ''
48
+
runHook preInstall
49
+
50
+
mkdir -p $out/lib/node_modules/
51
+
mv packages/ $out/lib/packages/
52
+
mv node_modules/* $out/lib/node_modules/
53
+
54
+
makeWrapper ${lib.getExe nodejs} $out/bin/plc \
55
+
--add-flags $out/lib/packages/server/service/index.js \
56
+
--add-flags --enable-source-maps \
57
+
--set NODE_PATH $out/lib/node_modules
58
+
59
+
runHook postInstall
60
+
'';
61
+
62
+
meta = {
63
+
mainProgram = "plc";
64
+
};
65
+
})
+7
-5
nix/pkgs/sqlite-lib.nix
+7
-5
nix/pkgs/sqlite-lib.nix
···
1
{
2
-
gcc,
3
stdenv,
4
sqlite-lib-src,
5
}:
6
stdenv.mkDerivation {
7
name = "sqlite-lib";
8
src = sqlite-lib-src;
9
-
nativeBuildInputs = [gcc];
10
buildPhase = ''
11
-
gcc -c sqlite3.c
12
-
ar rcs libsqlite3.a sqlite3.o
13
-
ranlib libsqlite3.a
14
mkdir -p $out/include $out/lib
15
cp *.h $out/include
16
cp libsqlite3.a $out/lib
···
1
{
2
stdenv,
3
sqlite-lib-src,
4
}:
5
stdenv.mkDerivation {
6
name = "sqlite-lib";
7
src = sqlite-lib-src;
8
+
9
buildPhase = ''
10
+
$CC -c sqlite3.c
11
+
$AR rcs libsqlite3.a sqlite3.o
12
+
$RANLIB libsqlite3.a
13
+
'';
14
+
15
+
installPhase = ''
16
mkdir -p $out/include $out/lib
17
cp *.h $out/include
18
cp libsqlite3.a $out/lib
+20
nix/pkgs/tap.nix
+20
nix/pkgs/tap.nix
···
···
1
+
{
2
+
buildGoModule,
3
+
fetchFromGitHub,
4
+
}:
5
+
buildGoModule {
6
+
pname = "tap";
7
+
version = "0.1.0";
8
+
src = fetchFromGitHub {
9
+
owner = "bluesky-social";
10
+
repo = "indigo";
11
+
rev = "f92cb29224fcc60f666b20ee3514e431a58ff811";
12
+
sha256 = "sha256-35ltXnq0SJeo3j33D7Nndbcnw5XWBJLRrmZ+nCmZVQw=";
13
+
};
14
+
subPackages = ["cmd/tap"];
15
+
vendorHash = "sha256-UOedwNYnM8Jx6B7Y9tFcZX8IeUBESAFAPTRYk7n0yo8=";
16
+
doCheck = false;
17
+
meta = {
18
+
mainProgram = "tap";
19
+
};
20
+
}
+2
nix/vm.nix
+2
nix/vm.nix
···
19
20
plcUrl = envVarOr "TANGLED_VM_PLC_URL" "https://plc.directory";
21
jetstream = envVarOr "TANGLED_VM_JETSTREAM_ENDPOINT" "wss://jetstream1.us-west.bsky.network/subscribe";
22
in
23
nixpkgs.lib.nixosSystem {
24
inherit system;
···
95
};
96
services.tangled.spindle = {
97
enable = true;
98
server = {
99
owner = envVar "TANGLED_VM_SPINDLE_OWNER";
100
hostname = envVarOr "TANGLED_VM_SPINDLE_HOST" "localhost:6555";
···
19
20
plcUrl = envVarOr "TANGLED_VM_PLC_URL" "https://plc.directory";
21
jetstream = envVarOr "TANGLED_VM_JETSTREAM_ENDPOINT" "wss://jetstream1.us-west.bsky.network/subscribe";
22
+
relayUrl = envVarOr "TANGLED_VM_RELAY_URL" "https://relay1.us-east.bsky.network";
23
in
24
nixpkgs.lib.nixosSystem {
25
inherit system;
···
96
};
97
services.tangled.spindle = {
98
enable = true;
99
+
atpRelayUrl = relayUrl;
100
server = {
101
owner = envVar "TANGLED_VM_SPINDLE_OWNER";
102
hostname = envVarOr "TANGLED_VM_SPINDLE_HOST" "localhost:6555";
+132
orm/orm.go
+132
orm/orm.go
···
···
1
+
package orm
2
+
3
+
import (
4
+
"context"
5
+
"database/sql"
6
+
"fmt"
7
+
"log/slog"
8
+
"reflect"
9
+
"strings"
10
+
)
11
+
12
+
type migrationFn = func(*sql.Tx) error
13
+
14
+
func RunMigration(c *sql.Conn, logger *slog.Logger, name string, migrationFn migrationFn) error {
15
+
logger = logger.With("migration", name)
16
+
17
+
tx, err := c.BeginTx(context.Background(), nil)
18
+
if err != nil {
19
+
return err
20
+
}
21
+
defer tx.Rollback()
22
+
23
+
_, err = tx.Exec(`
24
+
create table if not exists migrations (
25
+
id integer primary key autoincrement,
26
+
name text unique
27
+
);
28
+
`)
29
+
if err != nil {
30
+
return fmt.Errorf("creating migrations table: %w", err)
31
+
}
32
+
33
+
var exists bool
34
+
err = tx.QueryRow("select exists (select 1 from migrations where name = ?)", name).Scan(&exists)
35
+
if err != nil {
36
+
return err
37
+
}
38
+
39
+
if !exists {
40
+
// run migration
41
+
err = migrationFn(tx)
42
+
if err != nil {
43
+
logger.Error("failed to run migration", "err", err)
44
+
return err
45
+
}
46
+
47
+
// mark migration as complete
48
+
_, err = tx.Exec("insert into migrations (name) values (?)", name)
49
+
if err != nil {
50
+
logger.Error("failed to mark migration as complete", "err", err)
51
+
return err
52
+
}
53
+
54
+
// commit the transaction
55
+
if err := tx.Commit(); err != nil {
56
+
return err
57
+
}
58
+
59
+
logger.Info("migration applied successfully")
60
+
} else {
61
+
logger.Warn("skipped migration, already applied")
62
+
}
63
+
64
+
return nil
65
+
}
66
+
67
+
type Filter struct {
68
+
Key string
69
+
arg any
70
+
Cmp string
71
+
}
72
+
73
+
func newFilter(key, cmp string, arg any) Filter {
74
+
return Filter{
75
+
Key: key,
76
+
arg: arg,
77
+
Cmp: cmp,
78
+
}
79
+
}
80
+
81
+
func FilterEq(key string, arg any) Filter { return newFilter(key, "=", arg) }
82
+
func FilterNotEq(key string, arg any) Filter { return newFilter(key, "<>", arg) }
83
+
func FilterGte(key string, arg any) Filter { return newFilter(key, ">=", arg) }
84
+
func FilterLte(key string, arg any) Filter { return newFilter(key, "<=", arg) }
85
+
func FilterIs(key string, arg any) Filter { return newFilter(key, "is", arg) }
86
+
func FilterIsNot(key string, arg any) Filter { return newFilter(key, "is not", arg) }
87
+
func FilterIn(key string, arg any) Filter { return newFilter(key, "in", arg) }
88
+
func FilterLike(key string, arg any) Filter { return newFilter(key, "like", arg) }
89
+
func FilterNotLike(key string, arg any) Filter { return newFilter(key, "not like", arg) }
90
+
func FilterContains(key string, arg any) Filter {
91
+
return newFilter(key, "like", fmt.Sprintf("%%%v%%", arg))
92
+
}
93
+
94
+
func (f Filter) Condition() string {
95
+
rv := reflect.ValueOf(f.arg)
96
+
kind := rv.Kind()
97
+
98
+
// if we have `FilterIn(k, [1, 2, 3])`, compile it down to `k in (?, ?, ?)`
99
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
100
+
if rv.Len() == 0 {
101
+
// always false
102
+
return "1 = 0"
103
+
}
104
+
105
+
placeholders := make([]string, rv.Len())
106
+
for i := range placeholders {
107
+
placeholders[i] = "?"
108
+
}
109
+
110
+
return fmt.Sprintf("%s %s (%s)", f.Key, f.Cmp, strings.Join(placeholders, ", "))
111
+
}
112
+
113
+
return fmt.Sprintf("%s %s ?", f.Key, f.Cmp)
114
+
}
115
+
116
+
func (f Filter) Arg() []any {
117
+
rv := reflect.ValueOf(f.arg)
118
+
kind := rv.Kind()
119
+
if (kind == reflect.Slice && rv.Type().Elem().Kind() != reflect.Uint8) || kind == reflect.Array {
120
+
if rv.Len() == 0 {
121
+
return nil
122
+
}
123
+
124
+
out := make([]any, rv.Len())
125
+
for i := range rv.Len() {
126
+
out[i] = rv.Index(i).Interface()
127
+
}
128
+
return out
129
+
}
130
+
131
+
return []any{f.arg}
132
+
}
+144
rbac2/rbac2.go
+144
rbac2/rbac2.go
···
···
1
+
package rbac2
2
+
3
+
import (
4
+
"database/sql"
5
+
"fmt"
6
+
7
+
adapter "github.com/Blank-Xu/sql-adapter"
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
+
"github.com/casbin/casbin/v2"
10
+
"github.com/casbin/casbin/v2/model"
11
+
"github.com/casbin/casbin/v2/util"
12
+
"tangled.org/core/api/tangled"
13
+
)
14
+
15
+
const (
16
+
Model = `
17
+
[request_definition]
18
+
r = sub, dom, obj, act
19
+
20
+
[policy_definition]
21
+
p = sub, dom, obj, act
22
+
23
+
[role_definition]
24
+
g = _, _, _
25
+
26
+
[policy_effect]
27
+
e = some(where (p.eft == allow))
28
+
29
+
[matchers]
30
+
m = g(r.sub, p.sub, r.dom) && keyMatch4(r.dom, p.dom) && r.obj == p.obj && r.act == p.act
31
+
`
32
+
)
33
+
34
+
type Enforcer struct {
35
+
e *casbin.Enforcer
36
+
}
37
+
38
+
func NewEnforcer(path string) (*Enforcer, error) {
39
+
m, err := model.NewModelFromString(Model)
40
+
if err != nil {
41
+
return nil, err
42
+
}
43
+
44
+
db, err := sql.Open("sqlite3", path+"?_foreign_keys=1")
45
+
if err != nil {
46
+
return nil, err
47
+
}
48
+
49
+
a, err := adapter.NewAdapter(db, "sqlite3", "acl")
50
+
if err != nil {
51
+
return nil, err
52
+
}
53
+
54
+
e, err := casbin.NewEnforcer(m, a)
55
+
if err != nil {
56
+
return nil, err
57
+
}
58
+
59
+
if err := seedTangledPolicies(e); err != nil {
60
+
return nil, err
61
+
}
62
+
63
+
return &Enforcer{e}, nil
64
+
}
65
+
66
+
func seedTangledPolicies(e *casbin.Enforcer) error {
67
+
// policies
68
+
aturi := func(nsid string) string {
69
+
return fmt.Sprintf("at://{did}/%s/{rkey}", nsid)
70
+
}
71
+
72
+
_, err := e.AddPoliciesEx([][]string{
73
+
// sub | dom | obj | act
74
+
{"repo:owner", aturi(tangled.RepoNSID), "/", "write"},
75
+
{"repo:owner", aturi(tangled.RepoNSID), "/collaborator", "write"}, // invite
76
+
{"repo:collaborator", aturi(tangled.RepoNSID), "/settings", "write"},
77
+
{"repo:collaborator", aturi(tangled.RepoNSID), "/git", "write"}, // git push
78
+
79
+
{"server:owner", "/knot/{did}", "/member", "write"}, // invite
80
+
{"server:member", "/knot/{did}", "/git", "write"},
81
+
82
+
{"server:owner", "/spindle/{did}", "/member", "write"}, // invite
83
+
})
84
+
if err != nil {
85
+
return err
86
+
}
87
+
88
+
// grouping policies
89
+
// TODO(boltless): define our own matcher to replace keyMatch4
90
+
e.AddNamedDomainMatchingFunc("g", "keyMatch4", util.KeyMatch4)
91
+
_, err = e.AddGroupingPoliciesEx([][]string{
92
+
// sub | role | dom
93
+
{"repo:owner", "repo:collaborator", aturi(tangled.RepoNSID)},
94
+
95
+
// using '/knot/' prefix here because knot/spindle identifiers don't
96
+
// include the collection type
97
+
{"server:owner", "server:member", "/knot/{did}"},
98
+
{"server:owner", "server:member", "/spindle/{did}"},
99
+
})
100
+
return err
101
+
}
102
+
103
+
func (e *Enforcer) hasImplicitRoleForUser(name string, role string, domain ...string) (bool, error) {
104
+
roles, err := e.e.GetImplicitRolesForUser(name, domain...)
105
+
if err != nil {
106
+
return false, err
107
+
}
108
+
for _, r := range roles {
109
+
if r == role {
110
+
return true, nil
111
+
}
112
+
}
113
+
return false, nil
114
+
}
115
+
116
+
// setRoleForUser sets single user role for specified domain.
117
+
// All existing users with that role will be removed.
118
+
func (e *Enforcer) setRoleForUser(name string, role string, domain ...string) error {
119
+
currentUsers, err := e.e.GetUsersForRole(role, domain...)
120
+
if err != nil {
121
+
return err
122
+
}
123
+
124
+
for _, oldUser := range currentUsers {
125
+
_, err = e.e.DeleteRoleForUser(oldUser, role, domain...)
126
+
if err != nil {
127
+
return err
128
+
}
129
+
}
130
+
131
+
_, err = e.e.AddRoleForUser(name, role, domain...)
132
+
return err
133
+
}
134
+
135
+
// validateAtUri enforeces AT-URI to have valid did as authority and match collection NSID.
136
+
func validateAtUri(uri syntax.ATURI, expected string) error {
137
+
if !uri.Authority().IsDID() {
138
+
return fmt.Errorf("expected at-uri with did")
139
+
}
140
+
if expected != "" && uri.Collection().String() != expected {
141
+
return fmt.Errorf("incorrect repo at-uri collection nsid '%s' (expected '%s')", uri.Collection(), expected)
142
+
}
143
+
return nil
144
+
}
+115
rbac2/rbac2_test.go
+115
rbac2/rbac2_test.go
···
···
1
+
package rbac2_test
2
+
3
+
import (
4
+
"testing"
5
+
6
+
"github.com/bluesky-social/indigo/atproto/syntax"
7
+
_ "github.com/mattn/go-sqlite3"
8
+
"github.com/stretchr/testify/assert"
9
+
"tangled.org/core/rbac2"
10
+
)
11
+
12
+
func setup(t *testing.T) *rbac2.Enforcer {
13
+
enforcer, err := rbac2.NewEnforcer(":memory:")
14
+
assert.NoError(t, err)
15
+
16
+
return enforcer
17
+
}
18
+
19
+
func TestRepoOwnerPermissions(t *testing.T) {
20
+
var (
21
+
e = setup(t)
22
+
ok bool
23
+
err error
24
+
fooRepo = syntax.ATURI("at://did:plc:foo/sh.tangled.repo/reporkey")
25
+
fooUser = syntax.DID("did:plc:foo")
26
+
)
27
+
28
+
assert.NoError(t, e.AddRepo(fooRepo))
29
+
30
+
ok, err = e.IsRepoOwner(fooUser, fooRepo)
31
+
assert.NoError(t, err)
32
+
assert.True(t, ok, "repo author should be repo owner")
33
+
34
+
ok, err = e.IsRepoWriteAllowed(fooUser, fooRepo)
35
+
assert.NoError(t, err)
36
+
assert.True(t, ok, "repo owner should be able to modify the repo itself")
37
+
38
+
ok, err = e.IsRepoCollaborator(fooUser, fooRepo)
39
+
assert.NoError(t, err)
40
+
assert.True(t, ok, "repo owner should inherit role role:collaborator")
41
+
42
+
ok, err = e.IsRepoSettingsWriteAllowed(fooUser, fooRepo)
43
+
assert.NoError(t, err)
44
+
assert.True(t, ok, "repo owner should inherit collaborator permissions")
45
+
}
46
+
47
+
func TestRepoCollaboratorPermissions(t *testing.T) {
48
+
var (
49
+
e = setup(t)
50
+
ok bool
51
+
err error
52
+
fooRepo = syntax.ATURI("at://did:plc:foo/sh.tangled.repo/reporkey")
53
+
barUser = syntax.DID("did:plc:bar")
54
+
)
55
+
56
+
assert.NoError(t, e.AddRepo(fooRepo))
57
+
assert.NoError(t, e.AddRepoCollaborator(barUser, fooRepo))
58
+
59
+
ok, err = e.IsRepoCollaborator(barUser, fooRepo)
60
+
assert.NoError(t, err)
61
+
assert.True(t, ok, "should set repo collaborator")
62
+
63
+
ok, err = e.IsRepoSettingsWriteAllowed(barUser, fooRepo)
64
+
assert.NoError(t, err)
65
+
assert.True(t, ok, "repo collaborator should be able to edit repo settings")
66
+
67
+
ok, err = e.IsRepoWriteAllowed(barUser, fooRepo)
68
+
assert.NoError(t, err)
69
+
assert.False(t, ok, "repo collaborator shouldn't be able to modify the repo itself")
70
+
}
71
+
72
+
func TestGetByRole(t *testing.T) {
73
+
var (
74
+
e = setup(t)
75
+
err error
76
+
fooRepo = syntax.ATURI("at://did:plc:foo/sh.tangled.repo/reporkey")
77
+
owner = syntax.DID("did:plc:foo")
78
+
collaborator1 = syntax.DID("did:plc:bar")
79
+
collaborator2 = syntax.DID("did:plc:baz")
80
+
)
81
+
82
+
assert.NoError(t, e.AddRepo(fooRepo))
83
+
assert.NoError(t, e.AddRepoCollaborator(collaborator1, fooRepo))
84
+
assert.NoError(t, e.AddRepoCollaborator(collaborator2, fooRepo))
85
+
86
+
collaborators, err := e.GetRepoCollaborators(fooRepo)
87
+
assert.NoError(t, err)
88
+
assert.ElementsMatch(t, []syntax.DID{
89
+
owner,
90
+
collaborator1,
91
+
collaborator2,
92
+
}, collaborators)
93
+
}
94
+
95
+
func TestSpindleOwnerPermissions(t *testing.T) {
96
+
var (
97
+
e = setup(t)
98
+
ok bool
99
+
err error
100
+
spindle = syntax.DID("did:web:spindle.example.com")
101
+
owner = syntax.DID("did:plc:foo")
102
+
member = syntax.DID("did:plc:bar")
103
+
)
104
+
105
+
assert.NoError(t, e.SetSpindleOwner(owner, spindle))
106
+
assert.NoError(t, e.AddSpindleMember(member, spindle))
107
+
108
+
ok, err = e.IsSpindleMemberInviteAllowed(owner, spindle)
109
+
assert.NoError(t, err)
110
+
assert.True(t, ok, "spindle owner can invite members")
111
+
112
+
ok, err = e.IsSpindleMemberInviteAllowed(member, spindle)
113
+
assert.NoError(t, err)
114
+
assert.False(t, ok, "spindle member cannot invite members")
115
+
}
+91
rbac2/repo.go
+91
rbac2/repo.go
···
···
1
+
package rbac2
2
+
3
+
import (
4
+
"slices"
5
+
"strings"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
"tangled.org/core/api/tangled"
9
+
)
10
+
11
+
// AddRepo adds new repo with its owner to rbac enforcer
12
+
func (e *Enforcer) AddRepo(repo syntax.ATURI) error {
13
+
if err := validateAtUri(repo, tangled.RepoNSID); err != nil {
14
+
return err
15
+
}
16
+
user := repo.Authority()
17
+
18
+
return e.setRoleForUser(user.String(), "repo:owner", repo.String())
19
+
}
20
+
21
+
// DeleteRepo deletes all policies related to the repo
22
+
func (e *Enforcer) DeleteRepo(repo syntax.ATURI) error {
23
+
if err := validateAtUri(repo, tangled.RepoNSID); err != nil {
24
+
return err
25
+
}
26
+
27
+
_, err := e.e.DeleteDomains(repo.String())
28
+
return err
29
+
}
30
+
31
+
// AddRepoCollaborator adds new collaborator to the repo
32
+
func (e *Enforcer) AddRepoCollaborator(user syntax.DID, repo syntax.ATURI) error {
33
+
if err := validateAtUri(repo, tangled.RepoNSID); err != nil {
34
+
return err
35
+
}
36
+
37
+
_, err := e.e.AddRoleForUser(user.String(), "repo:collaborator", repo.String())
38
+
return err
39
+
}
40
+
41
+
// RemoveRepoCollaborator removes the collaborator from the repo.
42
+
// This won't remove inherited roles like repository owner.
43
+
func (e *Enforcer) RemoveRepoCollaborator(user syntax.DID, repo syntax.ATURI) error {
44
+
if err := validateAtUri(repo, tangled.RepoNSID); err != nil {
45
+
return err
46
+
}
47
+
48
+
_, err := e.e.DeleteRoleForUser(user.String(), "repo:collaborator", repo.String())
49
+
return err
50
+
}
51
+
52
+
func (e *Enforcer) GetRepoCollaborators(repo syntax.ATURI) ([]syntax.DID, error) {
53
+
var collaborators []syntax.DID
54
+
members, err := e.e.GetImplicitUsersForRole("repo:collaborator", repo.String())
55
+
if err != nil {
56
+
return nil, err
57
+
}
58
+
for _, m := range members {
59
+
if !strings.HasPrefix(m, "did:") { // skip non-user subjects like 'repo:owner'
60
+
continue
61
+
}
62
+
collaborators = append(collaborators, syntax.DID(m))
63
+
}
64
+
65
+
slices.Sort(collaborators)
66
+
return slices.Compact(collaborators), nil
67
+
}
68
+
69
+
func (e *Enforcer) IsRepoOwner(user syntax.DID, repo syntax.ATURI) (bool, error) {
70
+
return e.e.HasRoleForUser(user.String(), "repo:owner", repo.String())
71
+
}
72
+
73
+
func (e *Enforcer) IsRepoCollaborator(user syntax.DID, repo syntax.ATURI) (bool, error) {
74
+
return e.hasImplicitRoleForUser(user.String(), "repo:collaborator", repo.String())
75
+
}
76
+
77
+
func (e *Enforcer) IsRepoWriteAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) {
78
+
return e.e.Enforce(user.String(), repo.String(), "#/", "write")
79
+
}
80
+
81
+
func (e *Enforcer) IsRepoSettingsWriteAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) {
82
+
return e.e.Enforce(user.String(), repo.String(), "#/settings", "write")
83
+
}
84
+
85
+
func (e *Enforcer) IsRepoCollaboratorInviteAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) {
86
+
return e.e.Enforce(user.String(), repo.String(), "#/collaborator", "write")
87
+
}
88
+
89
+
func (e *Enforcer) IsRepoGitPushAllowed(user syntax.DID, repo syntax.ATURI) (bool, error) {
90
+
return e.e.Enforce(user.String(), repo.String(), "#/git", "write")
91
+
}
+29
rbac2/spindle.go
+29
rbac2/spindle.go
···
···
1
+
package rbac2
2
+
3
+
import "github.com/bluesky-social/indigo/atproto/syntax"
4
+
5
+
func (e *Enforcer) SetSpindleOwner(user syntax.DID, spindle syntax.DID) error {
6
+
return e.setRoleForUser(user.String(), "server:owner", intoSpindle(spindle))
7
+
}
8
+
9
+
func (e *Enforcer) IsSpindleMember(user syntax.DID, spindle syntax.DID) (bool, error) {
10
+
return e.e.HasRoleForUser(user.String(), "server:member", spindle.String())
11
+
}
12
+
13
+
func (e *Enforcer) AddSpindleMember(user syntax.DID, spindle syntax.DID) error {
14
+
_, err := e.e.AddRoleForUser(user.String(), "server:member", intoSpindle(spindle))
15
+
return err
16
+
}
17
+
18
+
func (e *Enforcer) RemoveSpindleMember(user syntax.DID, spindle syntax.DID) error {
19
+
_, err := e.e.DeleteRoleForUser(user.String(), "server:member", intoSpindle(spindle))
20
+
return err
21
+
}
22
+
23
+
func (e *Enforcer) IsSpindleMemberInviteAllowed(user syntax.DID, spindle syntax.DID) (bool, error) {
24
+
return e.e.Enforce(user.String(), intoSpindle(spindle), "#/member", "write")
25
+
}
26
+
27
+
func intoSpindle(did syntax.DID) string {
28
+
return "/spindle/" + did.String()
29
+
}
+31
sets/gen.go
+31
sets/gen.go
···
···
1
+
package sets
2
+
3
+
import (
4
+
"math/rand"
5
+
"reflect"
6
+
"testing/quick"
7
+
)
8
+
9
+
func (_ Set[T]) Generate(rand *rand.Rand, size int) reflect.Value {
10
+
s := New[T]()
11
+
12
+
var zero T
13
+
itemType := reflect.TypeOf(zero)
14
+
15
+
for {
16
+
if s.Len() >= size {
17
+
break
18
+
}
19
+
20
+
item, ok := quick.Value(itemType, rand)
21
+
if !ok {
22
+
continue
23
+
}
24
+
25
+
if val, ok := item.Interface().(T); ok {
26
+
s.Insert(val)
27
+
}
28
+
}
29
+
30
+
return reflect.ValueOf(s)
31
+
}
+35
sets/readme.txt
+35
sets/readme.txt
···
···
1
+
sets
2
+
----
3
+
set datastructure for go with generics and iterators. the
4
+
api is supposed to mimic rust's std::collections::HashSet api.
5
+
6
+
s1 := sets.Collect(slices.Values([]int{1, 2, 3, 4}))
7
+
s2 := sets.Collect(slices.Values([]int{1, 2, 3, 4, 5, 6}))
8
+
9
+
union := sets.Collect(s1.Union(s2))
10
+
intersect := sets.Collect(s1.Intersection(s2))
11
+
diff := sets.Collect(s1.Difference(s2))
12
+
symdiff := sets.Collect(s1.SymmetricDifference(s2))
13
+
14
+
s1.Len() // 4
15
+
s1.Contains(1) // true
16
+
s1.IsEmpty() // false
17
+
s1.IsSubset(s2) // true
18
+
s1.IsSuperset(s2) // false
19
+
s1.IsDisjoint(s2) // false
20
+
21
+
if exists := s1.Insert(1); exists {
22
+
// already existed in set
23
+
}
24
+
25
+
if existed := s1.Remove(1); existed {
26
+
// existed in set, now removed
27
+
}
28
+
29
+
30
+
testing
31
+
-------
32
+
includes property-based tests using the wonderful
33
+
testing/quick module!
34
+
35
+
go test -v
+174
sets/set.go
+174
sets/set.go
···
···
1
+
package sets
2
+
3
+
import (
4
+
"iter"
5
+
"maps"
6
+
)
7
+
8
+
type Set[T comparable] struct {
9
+
data map[T]struct{}
10
+
}
11
+
12
+
func New[T comparable]() Set[T] {
13
+
return Set[T]{
14
+
data: make(map[T]struct{}),
15
+
}
16
+
}
17
+
18
+
func (s *Set[T]) Insert(item T) bool {
19
+
_, exists := s.data[item]
20
+
s.data[item] = struct{}{}
21
+
return !exists
22
+
}
23
+
24
+
func Singleton[T comparable](item T) Set[T] {
25
+
n := New[T]()
26
+
_ = n.Insert(item)
27
+
return n
28
+
}
29
+
30
+
func (s *Set[T]) Remove(item T) bool {
31
+
_, exists := s.data[item]
32
+
if exists {
33
+
delete(s.data, item)
34
+
}
35
+
return exists
36
+
}
37
+
38
+
func (s Set[T]) Contains(item T) bool {
39
+
_, exists := s.data[item]
40
+
return exists
41
+
}
42
+
43
+
func (s Set[T]) Len() int {
44
+
return len(s.data)
45
+
}
46
+
47
+
func (s Set[T]) IsEmpty() bool {
48
+
return len(s.data) == 0
49
+
}
50
+
51
+
func (s *Set[T]) Clear() {
52
+
s.data = make(map[T]struct{})
53
+
}
54
+
55
+
func (s Set[T]) All() iter.Seq[T] {
56
+
return func(yield func(T) bool) {
57
+
for item := range s.data {
58
+
if !yield(item) {
59
+
return
60
+
}
61
+
}
62
+
}
63
+
}
64
+
65
+
func (s Set[T]) Clone() Set[T] {
66
+
return Set[T]{
67
+
data: maps.Clone(s.data),
68
+
}
69
+
}
70
+
71
+
func (s Set[T]) Union(other Set[T]) iter.Seq[T] {
72
+
if s.Len() >= other.Len() {
73
+
return chain(s.All(), other.Difference(s))
74
+
} else {
75
+
return chain(other.All(), s.Difference(other))
76
+
}
77
+
}
78
+
79
+
func chain[T any](seqs ...iter.Seq[T]) iter.Seq[T] {
80
+
return func(yield func(T) bool) {
81
+
for _, seq := range seqs {
82
+
for item := range seq {
83
+
if !yield(item) {
84
+
return
85
+
}
86
+
}
87
+
}
88
+
}
89
+
}
90
+
91
+
func (s Set[T]) Intersection(other Set[T]) iter.Seq[T] {
92
+
return func(yield func(T) bool) {
93
+
for item := range s.data {
94
+
if other.Contains(item) {
95
+
if !yield(item) {
96
+
return
97
+
}
98
+
}
99
+
}
100
+
}
101
+
}
102
+
103
+
func (s Set[T]) Difference(other Set[T]) iter.Seq[T] {
104
+
return func(yield func(T) bool) {
105
+
for item := range s.data {
106
+
if !other.Contains(item) {
107
+
if !yield(item) {
108
+
return
109
+
}
110
+
}
111
+
}
112
+
}
113
+
}
114
+
115
+
func (s Set[T]) SymmetricDifference(other Set[T]) iter.Seq[T] {
116
+
return func(yield func(T) bool) {
117
+
for item := range s.data {
118
+
if !other.Contains(item) {
119
+
if !yield(item) {
120
+
return
121
+
}
122
+
}
123
+
}
124
+
for item := range other.data {
125
+
if !s.Contains(item) {
126
+
if !yield(item) {
127
+
return
128
+
}
129
+
}
130
+
}
131
+
}
132
+
}
133
+
134
+
func (s Set[T]) IsSubset(other Set[T]) bool {
135
+
for item := range s.data {
136
+
if !other.Contains(item) {
137
+
return false
138
+
}
139
+
}
140
+
return true
141
+
}
142
+
143
+
func (s Set[T]) IsSuperset(other Set[T]) bool {
144
+
return other.IsSubset(s)
145
+
}
146
+
147
+
func (s Set[T]) IsDisjoint(other Set[T]) bool {
148
+
for item := range s.data {
149
+
if other.Contains(item) {
150
+
return false
151
+
}
152
+
}
153
+
return true
154
+
}
155
+
156
+
func (s Set[T]) Equal(other Set[T]) bool {
157
+
if s.Len() != other.Len() {
158
+
return false
159
+
}
160
+
for item := range s.data {
161
+
if !other.Contains(item) {
162
+
return false
163
+
}
164
+
}
165
+
return true
166
+
}
167
+
168
+
func Collect[T comparable](seq iter.Seq[T]) Set[T] {
169
+
result := New[T]()
170
+
for item := range seq {
171
+
result.Insert(item)
172
+
}
173
+
return result
174
+
}
+411
sets/set_test.go
+411
sets/set_test.go
···
···
1
+
package sets
2
+
3
+
import (
4
+
"slices"
5
+
"testing"
6
+
"testing/quick"
7
+
)
8
+
9
+
func TestNew(t *testing.T) {
10
+
s := New[int]()
11
+
if s.Len() != 0 {
12
+
t.Errorf("New set should be empty, got length %d", s.Len())
13
+
}
14
+
if !s.IsEmpty() {
15
+
t.Error("New set should be empty")
16
+
}
17
+
}
18
+
19
+
func TestFromSlice(t *testing.T) {
20
+
s := Collect(slices.Values([]int{1, 2, 3, 2, 1}))
21
+
if s.Len() != 3 {
22
+
t.Errorf("Expected length 3, got %d", s.Len())
23
+
}
24
+
if !s.Contains(1) || !s.Contains(2) || !s.Contains(3) {
25
+
t.Error("Set should contain all unique elements from slice")
26
+
}
27
+
}
28
+
29
+
func TestInsert(t *testing.T) {
30
+
s := New[string]()
31
+
32
+
if !s.Insert("hello") {
33
+
t.Error("First insert should return true")
34
+
}
35
+
if s.Insert("hello") {
36
+
t.Error("Duplicate insert should return false")
37
+
}
38
+
if s.Len() != 1 {
39
+
t.Errorf("Expected length 1, got %d", s.Len())
40
+
}
41
+
}
42
+
43
+
func TestRemove(t *testing.T) {
44
+
s := Collect(slices.Values([]int{1, 2, 3}))
45
+
46
+
if !s.Remove(2) {
47
+
t.Error("Remove existing element should return true")
48
+
}
49
+
if s.Remove(2) {
50
+
t.Error("Remove non-existing element should return false")
51
+
}
52
+
if s.Contains(2) {
53
+
t.Error("Element should be removed")
54
+
}
55
+
if s.Len() != 2 {
56
+
t.Errorf("Expected length 2, got %d", s.Len())
57
+
}
58
+
}
59
+
60
+
func TestContains(t *testing.T) {
61
+
s := Collect(slices.Values([]int{1, 2, 3}))
62
+
63
+
if !s.Contains(1) {
64
+
t.Error("Should contain 1")
65
+
}
66
+
if s.Contains(4) {
67
+
t.Error("Should not contain 4")
68
+
}
69
+
}
70
+
71
+
func TestClear(t *testing.T) {
72
+
s := Collect(slices.Values([]int{1, 2, 3}))
73
+
s.Clear()
74
+
75
+
if !s.IsEmpty() {
76
+
t.Error("Set should be empty after clear")
77
+
}
78
+
if s.Len() != 0 {
79
+
t.Errorf("Expected length 0, got %d", s.Len())
80
+
}
81
+
}
82
+
83
+
func TestIterator(t *testing.T) {
84
+
s := Collect(slices.Values([]int{1, 2, 3}))
85
+
var items []int
86
+
87
+
for item := range s.All() {
88
+
items = append(items, item)
89
+
}
90
+
91
+
slices.Sort(items)
92
+
expected := []int{1, 2, 3}
93
+
if !slices.Equal(items, expected) {
94
+
t.Errorf("Expected %v, got %v", expected, items)
95
+
}
96
+
}
97
+
98
+
func TestClone(t *testing.T) {
99
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
100
+
s2 := s1.Clone()
101
+
102
+
if !s1.Equal(s2) {
103
+
t.Error("Cloned set should be equal to original")
104
+
}
105
+
106
+
s2.Insert(4)
107
+
if s1.Contains(4) {
108
+
t.Error("Modifying clone should not affect original")
109
+
}
110
+
}
111
+
112
+
func TestUnion(t *testing.T) {
113
+
s1 := Collect(slices.Values([]int{1, 2}))
114
+
s2 := Collect(slices.Values([]int{2, 3}))
115
+
116
+
result := Collect(s1.Union(s2))
117
+
expected := Collect(slices.Values([]int{1, 2, 3}))
118
+
119
+
if !result.Equal(expected) {
120
+
t.Errorf("Expected %v, got %v", expected, result)
121
+
}
122
+
}
123
+
124
+
func TestIntersection(t *testing.T) {
125
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
126
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
127
+
128
+
expected := Collect(slices.Values([]int{2, 3}))
129
+
result := Collect(s1.Intersection(s2))
130
+
131
+
if !result.Equal(expected) {
132
+
t.Errorf("Expected %v, got %v", expected, result)
133
+
}
134
+
}
135
+
136
+
func TestDifference(t *testing.T) {
137
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
138
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
139
+
140
+
expected := Collect(slices.Values([]int{1}))
141
+
result := Collect(s1.Difference(s2))
142
+
143
+
if !result.Equal(expected) {
144
+
t.Errorf("Expected %v, got %v", expected, result)
145
+
}
146
+
}
147
+
148
+
func TestSymmetricDifference(t *testing.T) {
149
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
150
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
151
+
152
+
expected := Collect(slices.Values([]int{1, 4}))
153
+
result := Collect(s1.SymmetricDifference(s2))
154
+
155
+
if !result.Equal(expected) {
156
+
t.Errorf("Expected %v, got %v", expected, result)
157
+
}
158
+
}
159
+
160
+
func TestSymmetricDifferenceCommutativeProperty(t *testing.T) {
161
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
162
+
s2 := Collect(slices.Values([]int{2, 3, 4}))
163
+
164
+
result1 := Collect(s1.SymmetricDifference(s2))
165
+
result2 := Collect(s2.SymmetricDifference(s1))
166
+
167
+
if !result1.Equal(result2) {
168
+
t.Errorf("Expected %v, got %v", result1, result2)
169
+
}
170
+
}
171
+
172
+
func TestIsSubset(t *testing.T) {
173
+
s1 := Collect(slices.Values([]int{1, 2}))
174
+
s2 := Collect(slices.Values([]int{1, 2, 3}))
175
+
176
+
if !s1.IsSubset(s2) {
177
+
t.Error("s1 should be subset of s2")
178
+
}
179
+
if s2.IsSubset(s1) {
180
+
t.Error("s2 should not be subset of s1")
181
+
}
182
+
}
183
+
184
+
func TestIsSuperset(t *testing.T) {
185
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
186
+
s2 := Collect(slices.Values([]int{1, 2}))
187
+
188
+
if !s1.IsSuperset(s2) {
189
+
t.Error("s1 should be superset of s2")
190
+
}
191
+
if s2.IsSuperset(s1) {
192
+
t.Error("s2 should not be superset of s1")
193
+
}
194
+
}
195
+
196
+
func TestIsDisjoint(t *testing.T) {
197
+
s1 := Collect(slices.Values([]int{1, 2}))
198
+
s2 := Collect(slices.Values([]int{3, 4}))
199
+
s3 := Collect(slices.Values([]int{2, 3}))
200
+
201
+
if !s1.IsDisjoint(s2) {
202
+
t.Error("s1 and s2 should be disjoint")
203
+
}
204
+
if s1.IsDisjoint(s3) {
205
+
t.Error("s1 and s3 should not be disjoint")
206
+
}
207
+
}
208
+
209
+
func TestEqual(t *testing.T) {
210
+
s1 := Collect(slices.Values([]int{1, 2, 3}))
211
+
s2 := Collect(slices.Values([]int{3, 2, 1}))
212
+
s3 := Collect(slices.Values([]int{1, 2}))
213
+
214
+
if !s1.Equal(s2) {
215
+
t.Error("s1 and s2 should be equal")
216
+
}
217
+
if s1.Equal(s3) {
218
+
t.Error("s1 and s3 should not be equal")
219
+
}
220
+
}
221
+
222
+
func TestCollect(t *testing.T) {
223
+
s1 := Collect(slices.Values([]int{1, 2}))
224
+
s2 := Collect(slices.Values([]int{2, 3}))
225
+
226
+
unionSet := Collect(s1.Union(s2))
227
+
if unionSet.Len() != 3 {
228
+
t.Errorf("Expected union set length 3, got %d", unionSet.Len())
229
+
}
230
+
if !unionSet.Contains(1) || !unionSet.Contains(2) || !unionSet.Contains(3) {
231
+
t.Error("Union set should contain 1, 2, and 3")
232
+
}
233
+
234
+
diffSet := Collect(s1.Difference(s2))
235
+
if diffSet.Len() != 1 {
236
+
t.Errorf("Expected difference set length 1, got %d", diffSet.Len())
237
+
}
238
+
if !diffSet.Contains(1) {
239
+
t.Error("Difference set should contain 1")
240
+
}
241
+
}
242
+
243
+
func TestPropertySingleonLen(t *testing.T) {
244
+
f := func(item int) bool {
245
+
single := Singleton(item)
246
+
return single.Len() == 1
247
+
}
248
+
249
+
if err := quick.Check(f, nil); err != nil {
250
+
t.Error(err)
251
+
}
252
+
}
253
+
254
+
func TestPropertyInsertIdempotent(t *testing.T) {
255
+
f := func(s Set[int], item int) bool {
256
+
clone := s.Clone()
257
+
258
+
clone.Insert(item)
259
+
firstLen := clone.Len()
260
+
261
+
clone.Insert(item)
262
+
secondLen := clone.Len()
263
+
264
+
return firstLen == secondLen
265
+
}
266
+
267
+
if err := quick.Check(f, nil); err != nil {
268
+
t.Error(err)
269
+
}
270
+
}
271
+
272
+
func TestPropertyUnionCommutative(t *testing.T) {
273
+
f := func(s1 Set[int], s2 Set[int]) bool {
274
+
union1 := Collect(s1.Union(s2))
275
+
union2 := Collect(s2.Union(s1))
276
+
return union1.Equal(union2)
277
+
}
278
+
279
+
if err := quick.Check(f, nil); err != nil {
280
+
t.Error(err)
281
+
}
282
+
}
283
+
284
+
func TestPropertyIntersectionCommutative(t *testing.T) {
285
+
f := func(s1 Set[int], s2 Set[int]) bool {
286
+
inter1 := Collect(s1.Intersection(s2))
287
+
inter2 := Collect(s2.Intersection(s1))
288
+
return inter1.Equal(inter2)
289
+
}
290
+
291
+
if err := quick.Check(f, nil); err != nil {
292
+
t.Error(err)
293
+
}
294
+
}
295
+
296
+
func TestPropertyCloneEquals(t *testing.T) {
297
+
f := func(s Set[int]) bool {
298
+
clone := s.Clone()
299
+
return s.Equal(clone)
300
+
}
301
+
302
+
if err := quick.Check(f, nil); err != nil {
303
+
t.Error(err)
304
+
}
305
+
}
306
+
307
+
func TestPropertyIntersectionIsSubset(t *testing.T) {
308
+
f := func(s1 Set[int], s2 Set[int]) bool {
309
+
inter := Collect(s1.Intersection(s2))
310
+
return inter.IsSubset(s1) && inter.IsSubset(s2)
311
+
}
312
+
313
+
if err := quick.Check(f, nil); err != nil {
314
+
t.Error(err)
315
+
}
316
+
}
317
+
318
+
func TestPropertyUnionIsSuperset(t *testing.T) {
319
+
f := func(s1 Set[int], s2 Set[int]) bool {
320
+
union := Collect(s1.Union(s2))
321
+
return union.IsSuperset(s1) && union.IsSuperset(s2)
322
+
}
323
+
324
+
if err := quick.Check(f, nil); err != nil {
325
+
t.Error(err)
326
+
}
327
+
}
328
+
329
+
func TestPropertyDifferenceDisjoint(t *testing.T) {
330
+
f := func(s1 Set[int], s2 Set[int]) bool {
331
+
diff := Collect(s1.Difference(s2))
332
+
return diff.IsDisjoint(s2)
333
+
}
334
+
335
+
if err := quick.Check(f, nil); err != nil {
336
+
t.Error(err)
337
+
}
338
+
}
339
+
340
+
func TestPropertySymmetricDifferenceCommutative(t *testing.T) {
341
+
f := func(s1 Set[int], s2 Set[int]) bool {
342
+
symDiff1 := Collect(s1.SymmetricDifference(s2))
343
+
symDiff2 := Collect(s2.SymmetricDifference(s1))
344
+
return symDiff1.Equal(symDiff2)
345
+
}
346
+
347
+
if err := quick.Check(f, nil); err != nil {
348
+
t.Error(err)
349
+
}
350
+
}
351
+
352
+
func TestPropertyRemoveWorks(t *testing.T) {
353
+
f := func(s Set[int], item int) bool {
354
+
clone := s.Clone()
355
+
clone.Insert(item)
356
+
clone.Remove(item)
357
+
return !clone.Contains(item)
358
+
}
359
+
360
+
if err := quick.Check(f, nil); err != nil {
361
+
t.Error(err)
362
+
}
363
+
}
364
+
365
+
func TestPropertyClearEmpty(t *testing.T) {
366
+
f := func(s Set[int]) bool {
367
+
s.Clear()
368
+
return s.IsEmpty() && s.Len() == 0
369
+
}
370
+
371
+
if err := quick.Check(f, nil); err != nil {
372
+
t.Error(err)
373
+
}
374
+
}
375
+
376
+
func TestPropertyIsSubsetReflexive(t *testing.T) {
377
+
f := func(s Set[int]) bool {
378
+
return s.IsSubset(s)
379
+
}
380
+
381
+
if err := quick.Check(f, nil); err != nil {
382
+
t.Error(err)
383
+
}
384
+
}
385
+
386
+
func TestPropertyDeMorganUnion(t *testing.T) {
387
+
f := func(s1 Set[int], s2 Set[int], universe Set[int]) bool {
388
+
// create a universe that contains both sets
389
+
u := universe.Clone()
390
+
for item := range s1.All() {
391
+
u.Insert(item)
392
+
}
393
+
for item := range s2.All() {
394
+
u.Insert(item)
395
+
}
396
+
397
+
// (A u B)' = A' n B'
398
+
union := Collect(s1.Union(s2))
399
+
complementUnion := Collect(u.Difference(union))
400
+
401
+
complementS1 := Collect(u.Difference(s1))
402
+
complementS2 := Collect(u.Difference(s2))
403
+
intersectionComplements := Collect(complementS1.Intersection(complementS2))
404
+
405
+
return complementUnion.Equal(intersectionComplements)
406
+
}
407
+
408
+
if err := quick.Check(f, nil); err != nil {
409
+
t.Error(err)
410
+
}
411
+
}
+18
-11
spindle/config/config.go
+18
-11
spindle/config/config.go
···
3
import (
4
"context"
5
"fmt"
6
7
"github.com/bluesky-social/indigo/atproto/syntax"
8
"github.com/sethvargo/go-envconfig"
9
)
10
11
type Server struct {
12
-
ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"`
13
-
DBPath string `env:"DB_PATH, default=spindle.db"`
14
-
Hostname string `env:"HOSTNAME, required"`
15
-
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
16
-
PlcUrl string `env:"PLC_URL, default=https://plc.directory"`
17
-
Dev bool `env:"DEV, default=false"`
18
-
Owner string `env:"OWNER, required"`
19
-
Secrets Secrets `env:",prefix=SECRETS_"`
20
-
LogDir string `env:"LOG_DIR, default=/var/log/spindle"`
21
-
QueueSize int `env:"QUEUE_SIZE, default=100"`
22
-
MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time
23
}
24
25
func (s Server) Did() syntax.DID {
26
return syntax.DID(fmt.Sprintf("did:web:%s", s.Hostname))
27
}
28
29
type Secrets struct {
···
3
import (
4
"context"
5
"fmt"
6
+
"path"
7
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
"github.com/sethvargo/go-envconfig"
10
)
11
12
type Server struct {
13
+
ListenAddr string `env:"LISTEN_ADDR, default=0.0.0.0:6555"`
14
+
DBPath string `env:"DB_PATH, default=spindle.db"`
15
+
Hostname string `env:"HOSTNAME, required"`
16
+
JetstreamEndpoint string `env:"JETSTREAM_ENDPOINT, default=wss://jetstream1.us-west.bsky.network/subscribe"`
17
+
TapUrl string `env:"TAP_URL, required"`
18
+
PlcUrl string `env:"PLC_URL, default=https://plc.directory"`
19
+
Dev bool `env:"DEV, default=false"`
20
+
Owner syntax.DID `env:"OWNER, required"`
21
+
Secrets Secrets `env:",prefix=SECRETS_"`
22
+
LogDir string `env:"LOG_DIR, default=/var/log/spindle"`
23
+
DataDir string `env:"DATA_DIR, default=/var/lib/spindle"`
24
+
QueueSize int `env:"QUEUE_SIZE, default=100"`
25
+
MaxJobCount int `env:"MAX_JOB_COUNT, default=2"` // max number of jobs that run at a time
26
}
27
28
func (s Server) Did() syntax.DID {
29
return syntax.DID(fmt.Sprintf("did:web:%s", s.Hostname))
30
+
}
31
+
32
+
func (s Server) RepoDir() string {
33
+
return path.Join(s.DataDir, "repos")
34
}
35
36
type Secrets struct {
+59
-18
spindle/db/db.go
+59
-18
spindle/db/db.go
···
1
package db
2
3
import (
4
"database/sql"
5
"strings"
6
7
_ "github.com/mattn/go-sqlite3"
8
)
9
10
type DB struct {
11
*sql.DB
12
}
13
14
-
func Make(dbPath string) (*DB, error) {
15
// https://github.com/mattn/go-sqlite3#connection-string
16
opts := []string{
17
"_foreign_keys=1",
···
19
"_synchronous=NORMAL",
20
"_auto_vacuum=incremental",
21
}
22
23
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
24
if err != nil {
25
return nil, err
26
}
27
28
-
// NOTE: If any other migration is added here, you MUST
29
-
// copy the pattern in appview: use a single sql.Conn
30
-
// for every migration.
31
32
_, err = db.Exec(`
33
create table if not exists _jetstream (
···
76
return nil, err
77
}
78
79
-
return &DB{db}, nil
80
-
}
81
82
-
func (d *DB) SaveLastTimeUs(lastTimeUs int64) error {
83
-
_, err := d.Exec(`
84
-
insert into _jetstream (id, last_time_us)
85
-
values (1, ?)
86
-
on conflict(id) do update set last_time_us = excluded.last_time_us
87
-
`, lastTimeUs)
88
-
return err
89
}
90
91
-
func (d *DB) GetLastTimeUs() (int64, error) {
92
-
var lastTimeUs int64
93
-
row := d.QueryRow(`select last_time_us from _jetstream where id = 1;`)
94
-
err := row.Scan(&lastTimeUs)
95
-
return lastTimeUs, err
96
}
···
1
package db
2
3
import (
4
+
"context"
5
"database/sql"
6
"strings"
7
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
_ "github.com/mattn/go-sqlite3"
10
+
"tangled.org/core/log"
11
+
"tangled.org/core/orm"
12
)
13
14
type DB struct {
15
*sql.DB
16
}
17
18
+
func Make(ctx context.Context, dbPath string) (*DB, error) {
19
// https://github.com/mattn/go-sqlite3#connection-string
20
opts := []string{
21
"_foreign_keys=1",
···
23
"_synchronous=NORMAL",
24
"_auto_vacuum=incremental",
25
}
26
+
27
+
logger := log.FromContext(ctx)
28
+
logger = log.SubLogger(logger, "db")
29
30
db, err := sql.Open("sqlite3", dbPath+"?"+strings.Join(opts, "&"))
31
if err != nil {
32
return nil, err
33
}
34
35
+
conn, err := db.Conn(ctx)
36
+
if err != nil {
37
+
return nil, err
38
+
}
39
+
defer conn.Close()
40
41
_, err = db.Exec(`
42
create table if not exists _jetstream (
···
85
return nil, err
86
}
87
88
+
// run migrations
89
+
90
+
// NOTE: this won't migrate existing records
91
+
// they will be fetched again with tap instead
92
+
orm.RunMigration(conn, logger, "add-rkey-to-repos", func(tx *sql.Tx) error {
93
+
// archive legacy repos (just in case)
94
+
_, err = tx.Exec(`alter table repos rename to repos_old`)
95
+
if err != nil {
96
+
return err
97
+
}
98
+
99
+
_, err := tx.Exec(`
100
+
create table repos_new (
101
+
-- identifiers
102
+
id integer primary key autoincrement,
103
+
did text not null,
104
+
rkey text not null,
105
+
at_uri text generated always as ('at://' || did || '/' || 'sh.tangled.repo' || '/' || rkey) stored,
106
+
107
+
name text not null,
108
+
knot text not null,
109
+
110
+
addedAt text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),
111
+
unique(did, rkey)
112
+
);
113
+
`)
114
+
if err != nil {
115
+
return err
116
+
}
117
+
118
+
return nil
119
+
})
120
121
+
return &DB{db}, nil
122
}
123
124
+
func (d *DB) IsKnownDid(did syntax.DID) (bool, error) {
125
+
// is spindle member / repo collaborator
126
+
var exists bool
127
+
err := d.QueryRow(
128
+
`select exists (
129
+
select 1 from repo_collaborators where did = ?
130
+
union all
131
+
select 1 from spindle_members where did = ?
132
+
)`,
133
+
did,
134
+
did,
135
+
).Scan(&exists)
136
+
return exists, err
137
}
+6
-18
spindle/db/events.go
+6
-18
spindle/db/events.go
···
18
EventJson string `json:"event"`
19
}
20
21
-
func (d *DB) InsertEvent(event Event, notifier *notifier.Notifier) error {
22
_, err := d.Exec(
23
`insert into events (rkey, nsid, event, created) values (?, ?, ?, ?)`,
24
event.Rkey,
···
70
return evts, nil
71
}
72
73
-
func (d *DB) CreateStatusEvent(rkey string, s tangled.PipelineStatus, n *notifier.Notifier) error {
74
-
eventJson, err := json.Marshal(s)
75
-
if err != nil {
76
-
return err
77
-
}
78
-
79
-
event := Event{
80
-
Rkey: rkey,
81
-
Nsid: tangled.PipelineStatusNSID,
82
-
Created: time.Now().UnixNano(),
83
-
EventJson: string(eventJson),
84
-
}
85
-
86
-
return d.InsertEvent(event, n)
87
-
}
88
-
89
func (d *DB) createStatusEvent(
90
workflowId models.WorkflowId,
91
statusKind models.StatusKind,
···
116
EventJson: string(eventJson),
117
}
118
119
-
return d.InsertEvent(event, n)
120
121
}
122
···
164
165
func (d *DB) StatusFailed(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error {
166
return d.createStatusEvent(workflowId, models.StatusKindFailed, &workflowError, &exitCode, n)
167
}
168
169
func (d *DB) StatusSuccess(workflowId models.WorkflowId, n *notifier.Notifier) error {
···
18
EventJson string `json:"event"`
19
}
20
21
+
func (d *DB) insertEvent(event Event, notifier *notifier.Notifier) error {
22
_, err := d.Exec(
23
`insert into events (rkey, nsid, event, created) values (?, ?, ?, ?)`,
24
event.Rkey,
···
70
return evts, nil
71
}
72
73
func (d *DB) createStatusEvent(
74
workflowId models.WorkflowId,
75
statusKind models.StatusKind,
···
100
EventJson: string(eventJson),
101
}
102
103
+
return d.insertEvent(event, n)
104
105
}
106
···
148
149
func (d *DB) StatusFailed(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error {
150
return d.createStatusEvent(workflowId, models.StatusKindFailed, &workflowError, &exitCode, n)
151
+
}
152
+
153
+
func (d *DB) StatusCancelled(workflowId models.WorkflowId, workflowError string, exitCode int64, n *notifier.Notifier) error {
154
+
return d.createStatusEvent(workflowId, models.StatusKindCancelled, &workflowError, &exitCode, n)
155
}
156
157
func (d *DB) StatusSuccess(workflowId models.WorkflowId, n *notifier.Notifier) error {
-44
spindle/db/known_dids.go
-44
spindle/db/known_dids.go
···
1
-
package db
2
-
3
-
func (d *DB) AddDid(did string) error {
4
-
_, err := d.Exec(`insert or ignore into known_dids (did) values (?)`, did)
5
-
return err
6
-
}
7
-
8
-
func (d *DB) RemoveDid(did string) error {
9
-
_, err := d.Exec(`delete from known_dids where did = ?`, did)
10
-
return err
11
-
}
12
-
13
-
func (d *DB) GetAllDids() ([]string, error) {
14
-
var dids []string
15
-
16
-
rows, err := d.Query(`select did from known_dids`)
17
-
if err != nil {
18
-
return nil, err
19
-
}
20
-
defer rows.Close()
21
-
22
-
for rows.Next() {
23
-
var did string
24
-
if err := rows.Scan(&did); err != nil {
25
-
return nil, err
26
-
}
27
-
dids = append(dids, did)
28
-
}
29
-
30
-
if err := rows.Err(); err != nil {
31
-
return nil, err
32
-
}
33
-
34
-
return dids, nil
35
-
}
36
-
37
-
func (d *DB) HasKnownDids() bool {
38
-
var count int
39
-
err := d.QueryRow(`select count(*) from known_dids`).Scan(&count)
40
-
if err != nil {
41
-
return false
42
-
}
43
-
return count > 0
44
-
}
···
+121
-11
spindle/db/repos.go
+121
-11
spindle/db/repos.go
···
1
package db
2
3
type Repo struct {
4
-
Knot string
5
-
Owner string
6
-
Name string
7
}
8
9
-
func (d *DB) AddRepo(knot, owner, name string) error {
10
-
_, err := d.Exec(`insert or ignore into repos (knot, owner, name) values (?, ?, ?)`, knot, owner, name)
11
return err
12
}
13
···
16
if err != nil {
17
return nil, err
18
}
19
20
var knots []string
21
for rows.Next() {
···
33
return knots, nil
34
}
35
36
-
func (d *DB) GetRepo(knot, owner, name string) (*Repo, error) {
37
var repo Repo
38
-
39
-
query := "select knot, owner, name from repos where knot = ? and owner = ? and name = ?"
40
-
err := d.DB.QueryRow(query, knot, owner, name).
41
-
Scan(&repo.Knot, &repo.Owner, &repo.Name)
42
-
43
if err != nil {
44
return nil, err
45
}
46
47
return &repo, nil
48
}
···
1
package db
2
3
+
import "github.com/bluesky-social/indigo/atproto/syntax"
4
+
5
type Repo struct {
6
+
Did syntax.DID
7
+
Rkey syntax.RecordKey
8
+
Name string
9
+
Knot string
10
+
}
11
+
12
+
type RepoCollaborator struct {
13
+
Did syntax.DID
14
+
Rkey syntax.RecordKey
15
+
Repo syntax.ATURI
16
+
Subject syntax.DID
17
}
18
19
+
func (d *DB) PutRepo(repo *Repo) error {
20
+
_, err := d.Exec(
21
+
`insert or ignore into repos (did, rkey, name, knot)
22
+
values (?, ?, ?, ?)
23
+
on conflict(did, rkey) do update set
24
+
name = excluded.name
25
+
knot = excluded.knot`,
26
+
repo.Did,
27
+
repo.Rkey,
28
+
repo.Name,
29
+
repo.Knot,
30
+
)
31
+
return err
32
+
}
33
+
34
+
func (d *DB) DeleteRepo(did syntax.DID, rkey syntax.RecordKey) error {
35
+
_, err := d.Exec(
36
+
`delete from repos where did = ? and rkey = ?`,
37
+
did,
38
+
rkey,
39
+
)
40
return err
41
}
42
···
45
if err != nil {
46
return nil, err
47
}
48
+
defer rows.Close()
49
50
var knots []string
51
for rows.Next() {
···
63
return knots, nil
64
}
65
66
+
func (d *DB) GetRepo(did syntax.DID, rkey syntax.RecordKey) (*Repo, error) {
67
var repo Repo
68
+
err := d.DB.QueryRow(
69
+
`select
70
+
did,
71
+
rkey,
72
+
name,
73
+
knot
74
+
from repos where did = ? and rkey = ?`,
75
+
did,
76
+
rkey,
77
+
).Scan(
78
+
&repo.Did,
79
+
&repo.Rkey,
80
+
&repo.Name,
81
+
&repo.Knot,
82
+
)
83
if err != nil {
84
return nil, err
85
}
86
+
return &repo, nil
87
+
}
88
89
+
func (d *DB) GetRepoWithName(did syntax.DID, name string) (*Repo, error) {
90
+
var repo Repo
91
+
err := d.DB.QueryRow(
92
+
`select
93
+
did,
94
+
rkey,
95
+
name,
96
+
knot
97
+
from repos where did = ? and name = ?`,
98
+
did,
99
+
name,
100
+
).Scan(
101
+
&repo.Did,
102
+
&repo.Rkey,
103
+
&repo.Name,
104
+
&repo.Knot,
105
+
)
106
+
if err != nil {
107
+
return nil, err
108
+
}
109
return &repo, nil
110
}
111
+
112
+
func (d *DB) PutRepoCollaborator(collaborator *RepoCollaborator) error {
113
+
_, err := d.Exec(
114
+
`insert into repo_collaborators (did, rkey, repo, subject)
115
+
values (?, ?, ?, ?)
116
+
on conflict(did, rkey) do update set
117
+
repo = excluded.repo
118
+
subject = excluded.subject`,
119
+
collaborator.Did,
120
+
collaborator.Rkey,
121
+
collaborator.Repo,
122
+
collaborator.Subject,
123
+
)
124
+
return err
125
+
}
126
+
127
+
func (d *DB) RemoveRepoCollaborator(did syntax.DID, rkey syntax.RecordKey) error {
128
+
_, err := d.Exec(
129
+
`delete from repo_collaborators where did = ? and rkey = ?`,
130
+
did,
131
+
rkey,
132
+
)
133
+
return err
134
+
}
135
+
136
+
func (d *DB) GetRepoCollaborator(did syntax.DID, rkey syntax.RecordKey) (*RepoCollaborator, error) {
137
+
var collaborator RepoCollaborator
138
+
err := d.DB.QueryRow(
139
+
`select
140
+
did,
141
+
rkey,
142
+
repo,
143
+
subject
144
+
from repo_collaborators
145
+
where did = ? and rkey = ?`,
146
+
did,
147
+
rkey,
148
+
).Scan(
149
+
&collaborator.Did,
150
+
&collaborator.Rkey,
151
+
&collaborator.Repo,
152
+
&collaborator.Subject,
153
+
)
154
+
if err != nil {
155
+
return nil, err
156
+
}
157
+
return &collaborator, nil
158
+
}
+22
-21
spindle/engine/engine.go
+22
-21
spindle/engine/engine.go
···
3
import (
4
"context"
5
"errors"
6
-
"fmt"
7
"log/slog"
8
9
securejoin "github.com/cyphar/filepath-securejoin"
10
-
"golang.org/x/sync/errgroup"
11
"tangled.org/core/notifier"
12
"tangled.org/core/spindle/config"
13
"tangled.org/core/spindle/db"
···
31
}
32
}
33
34
-
eg, ctx := errgroup.WithContext(ctx)
35
for eng, wfs := range pipeline.Workflows {
36
workflowTimeout := eng.WorkflowTimeout()
37
l.Info("using workflow timeout", "timeout", workflowTimeout)
38
39
for _, w := range wfs {
40
-
eg.Go(func() error {
41
wid := models.WorkflowId{
42
PipelineId: pipelineId,
43
Name: w.Name,
···
45
46
err := db.StatusRunning(wid, n)
47
if err != nil {
48
-
return err
49
}
50
51
err = eng.SetupWorkflow(ctx, wid, &w)
···
61
62
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
63
if dbErr != nil {
64
-
return dbErr
65
}
66
-
return err
67
}
68
defer eng.DestroyWorkflow(ctx, wid)
69
70
-
wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid)
71
if err != nil {
72
l.Warn("failed to setup step logger; logs will not be persisted", "error", err)
73
wfLogger = nil
···
99
if errors.Is(err, ErrTimedOut) {
100
dbErr := db.StatusTimeout(wid, n)
101
if dbErr != nil {
102
-
return dbErr
103
}
104
} else {
105
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
106
if dbErr != nil {
107
-
return dbErr
108
}
109
}
110
-
111
-
return fmt.Errorf("starting steps image: %w", err)
112
}
113
}
114
115
err = db.StatusSuccess(wid, n)
116
if err != nil {
117
-
return err
118
}
119
-
120
-
return nil
121
-
})
122
}
123
}
124
125
-
if err := eg.Wait(); err != nil {
126
-
l.Error("failed to run one or more workflows", "err", err)
127
-
} else {
128
-
l.Info("successfully ran full pipeline")
129
-
}
130
}
···
3
import (
4
"context"
5
"errors"
6
"log/slog"
7
+
"sync"
8
9
securejoin "github.com/cyphar/filepath-securejoin"
10
"tangled.org/core/notifier"
11
"tangled.org/core/spindle/config"
12
"tangled.org/core/spindle/db"
···
30
}
31
}
32
33
+
var wg sync.WaitGroup
34
for eng, wfs := range pipeline.Workflows {
35
workflowTimeout := eng.WorkflowTimeout()
36
l.Info("using workflow timeout", "timeout", workflowTimeout)
37
38
for _, w := range wfs {
39
+
wg.Add(1)
40
+
go func() {
41
+
defer wg.Done()
42
+
43
wid := models.WorkflowId{
44
PipelineId: pipelineId,
45
Name: w.Name,
···
47
48
err := db.StatusRunning(wid, n)
49
if err != nil {
50
+
l.Error("failed to set workflow status to running", "wid", wid, "err", err)
51
+
return
52
}
53
54
err = eng.SetupWorkflow(ctx, wid, &w)
···
64
65
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
66
if dbErr != nil {
67
+
l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr)
68
}
69
+
return
70
}
71
defer eng.DestroyWorkflow(ctx, wid)
72
73
+
secretValues := make([]string, len(allSecrets))
74
+
for i, s := range allSecrets {
75
+
secretValues[i] = s.Value
76
+
}
77
+
wfLogger, err := models.NewWorkflowLogger(cfg.Server.LogDir, wid, secretValues)
78
if err != nil {
79
l.Warn("failed to setup step logger; logs will not be persisted", "error", err)
80
wfLogger = nil
···
106
if errors.Is(err, ErrTimedOut) {
107
dbErr := db.StatusTimeout(wid, n)
108
if dbErr != nil {
109
+
l.Error("failed to set workflow status to timeout", "wid", wid, "err", dbErr)
110
}
111
} else {
112
dbErr := db.StatusFailed(wid, err.Error(), -1, n)
113
if dbErr != nil {
114
+
l.Error("failed to set workflow status to failed", "wid", wid, "err", dbErr)
115
}
116
}
117
+
return
118
}
119
}
120
121
err = db.StatusSuccess(wid, n)
122
if err != nil {
123
+
l.Error("failed to set workflow status to success", "wid", wid, "err", err)
124
}
125
+
}()
126
}
127
}
128
129
+
wg.Wait()
130
+
l.Info("all workflows completed")
131
}
+29
-13
spindle/engines/nixery/engine.go
+29
-13
spindle/engines/nixery/engine.go
···
179
return err
180
}
181
e.registerCleanup(wid, func(ctx context.Context) error {
182
-
return e.docker.NetworkRemove(ctx, networkName(wid))
183
})
184
185
addl := wf.Data.(addlFields)
···
229
return fmt.Errorf("creating container: %w", err)
230
}
231
e.registerCleanup(wid, func(ctx context.Context) error {
232
-
err = e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{})
233
if err != nil {
234
-
return err
235
}
236
237
-
return e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{
238
RemoveVolumes: true,
239
RemoveLinks: false,
240
Force: false,
241
})
242
})
243
244
err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{})
···
294
workflowEnvs.AddEnv(s.Key, s.Value)
295
}
296
297
-
step := w.Steps[idx].(Step)
298
299
select {
300
case <-ctx.Done():
···
303
}
304
305
envs := append(EnvVars(nil), workflowEnvs...)
306
-
for k, v := range step.environment {
307
-
envs.AddEnv(k, v)
308
}
309
envs.AddEnv("HOME", homeDir)
310
···
392
}
393
394
func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error {
395
-
e.cleanupMu.Lock()
396
-
key := wid.String()
397
-
398
-
fns := e.cleanup[key]
399
-
delete(e.cleanup, key)
400
-
e.cleanupMu.Unlock()
401
402
for _, fn := range fns {
403
if err := fn(ctx); err != nil {
···
413
414
key := wid.String()
415
e.cleanup[key] = append(e.cleanup[key], fn)
416
}
417
418
func networkName(wid models.WorkflowId) string {
···
179
return err
180
}
181
e.registerCleanup(wid, func(ctx context.Context) error {
182
+
err := e.docker.NetworkRemove(ctx, networkName(wid))
183
+
if err != nil {
184
+
return fmt.Errorf("removing network: %w", err)
185
+
}
186
+
return nil
187
})
188
189
addl := wf.Data.(addlFields)
···
233
return fmt.Errorf("creating container: %w", err)
234
}
235
e.registerCleanup(wid, func(ctx context.Context) error {
236
+
err := e.docker.ContainerStop(ctx, resp.ID, container.StopOptions{})
237
if err != nil {
238
+
return fmt.Errorf("stopping container: %w", err)
239
}
240
241
+
err = e.docker.ContainerRemove(ctx, resp.ID, container.RemoveOptions{
242
RemoveVolumes: true,
243
RemoveLinks: false,
244
Force: false,
245
})
246
+
if err != nil {
247
+
return fmt.Errorf("removing container: %w", err)
248
+
}
249
+
return nil
250
})
251
252
err = e.docker.ContainerStart(ctx, resp.ID, container.StartOptions{})
···
302
workflowEnvs.AddEnv(s.Key, s.Value)
303
}
304
305
+
step := w.Steps[idx]
306
307
select {
308
case <-ctx.Done():
···
311
}
312
313
envs := append(EnvVars(nil), workflowEnvs...)
314
+
if nixStep, ok := step.(Step); ok {
315
+
for k, v := range nixStep.environment {
316
+
envs.AddEnv(k, v)
317
+
}
318
}
319
envs.AddEnv("HOME", homeDir)
320
···
402
}
403
404
func (e *Engine) DestroyWorkflow(ctx context.Context, wid models.WorkflowId) error {
405
+
fns := e.drainCleanups(wid)
406
407
for _, fn := range fns {
408
if err := fn(ctx); err != nil {
···
418
419
key := wid.String()
420
e.cleanup[key] = append(e.cleanup[key], fn)
421
+
}
422
+
423
+
func (e *Engine) drainCleanups(wid models.WorkflowId) []cleanupFunc {
424
+
e.cleanupMu.Lock()
425
+
key := wid.String()
426
+
427
+
fns := e.cleanup[key]
428
+
delete(e.cleanup, key)
429
+
e.cleanupMu.Unlock()
430
+
431
+
return fns
432
}
433
434
func networkName(wid models.WorkflowId) string {
-300
spindle/ingester.go
-300
spindle/ingester.go
···
1
-
package spindle
2
-
3
-
import (
4
-
"context"
5
-
"encoding/json"
6
-
"errors"
7
-
"fmt"
8
-
"time"
9
-
10
-
"tangled.org/core/api/tangled"
11
-
"tangled.org/core/eventconsumer"
12
-
"tangled.org/core/rbac"
13
-
"tangled.org/core/spindle/db"
14
-
15
-
comatproto "github.com/bluesky-social/indigo/api/atproto"
16
-
"github.com/bluesky-social/indigo/atproto/identity"
17
-
"github.com/bluesky-social/indigo/atproto/syntax"
18
-
"github.com/bluesky-social/indigo/xrpc"
19
-
"github.com/bluesky-social/jetstream/pkg/models"
20
-
securejoin "github.com/cyphar/filepath-securejoin"
21
-
)
22
-
23
-
type Ingester func(ctx context.Context, e *models.Event) error
24
-
25
-
func (s *Spindle) ingest() Ingester {
26
-
return func(ctx context.Context, e *models.Event) error {
27
-
var err error
28
-
defer func() {
29
-
eventTime := e.TimeUS
30
-
lastTimeUs := eventTime + 1
31
-
if err := s.db.SaveLastTimeUs(lastTimeUs); err != nil {
32
-
err = fmt.Errorf("(deferred) failed to save last time us: %w", err)
33
-
}
34
-
}()
35
-
36
-
if e.Kind != models.EventKindCommit {
37
-
return nil
38
-
}
39
-
40
-
switch e.Commit.Collection {
41
-
case tangled.SpindleMemberNSID:
42
-
err = s.ingestMember(ctx, e)
43
-
case tangled.RepoNSID:
44
-
err = s.ingestRepo(ctx, e)
45
-
case tangled.RepoCollaboratorNSID:
46
-
err = s.ingestCollaborator(ctx, e)
47
-
}
48
-
49
-
if err != nil {
50
-
s.l.Debug("failed to process message", "nsid", e.Commit.Collection, "err", err)
51
-
}
52
-
53
-
return nil
54
-
}
55
-
}
56
-
57
-
func (s *Spindle) ingestMember(_ context.Context, e *models.Event) error {
58
-
var err error
59
-
did := e.Did
60
-
rkey := e.Commit.RKey
61
-
62
-
l := s.l.With("component", "ingester", "record", tangled.SpindleMemberNSID)
63
-
64
-
switch e.Commit.Operation {
65
-
case models.CommitOperationCreate, models.CommitOperationUpdate:
66
-
raw := e.Commit.Record
67
-
record := tangled.SpindleMember{}
68
-
err = json.Unmarshal(raw, &record)
69
-
if err != nil {
70
-
l.Error("invalid record", "error", err)
71
-
return err
72
-
}
73
-
74
-
domain := s.cfg.Server.Hostname
75
-
recordInstance := record.Instance
76
-
77
-
if recordInstance != domain {
78
-
l.Error("domain mismatch", "domain", recordInstance, "expected", domain)
79
-
return fmt.Errorf("domain mismatch: %s != %s", record.Instance, domain)
80
-
}
81
-
82
-
ok, err := s.e.IsSpindleInviteAllowed(did, rbacDomain)
83
-
if err != nil || !ok {
84
-
l.Error("failed to add member", "did", did, "error", err)
85
-
return fmt.Errorf("failed to enforce permissions: %w", err)
86
-
}
87
-
88
-
if err := db.AddSpindleMember(s.db, db.SpindleMember{
89
-
Did: syntax.DID(did),
90
-
Rkey: rkey,
91
-
Instance: recordInstance,
92
-
Subject: syntax.DID(record.Subject),
93
-
Created: time.Now(),
94
-
}); err != nil {
95
-
l.Error("failed to add member", "error", err)
96
-
return fmt.Errorf("failed to add member: %w", err)
97
-
}
98
-
99
-
if err := s.e.AddSpindleMember(rbacDomain, record.Subject); err != nil {
100
-
l.Error("failed to add member", "error", err)
101
-
return fmt.Errorf("failed to add member: %w", err)
102
-
}
103
-
l.Info("added member from firehose", "member", record.Subject)
104
-
105
-
if err := s.db.AddDid(record.Subject); err != nil {
106
-
l.Error("failed to add did", "error", err)
107
-
return fmt.Errorf("failed to add did: %w", err)
108
-
}
109
-
s.jc.AddDid(record.Subject)
110
-
111
-
return nil
112
-
113
-
case models.CommitOperationDelete:
114
-
record, err := db.GetSpindleMember(s.db, did, rkey)
115
-
if err != nil {
116
-
l.Error("failed to find member", "error", err)
117
-
return fmt.Errorf("failed to find member: %w", err)
118
-
}
119
-
120
-
if err := db.RemoveSpindleMember(s.db, did, rkey); err != nil {
121
-
l.Error("failed to remove member", "error", err)
122
-
return fmt.Errorf("failed to remove member: %w", err)
123
-
}
124
-
125
-
if err := s.e.RemoveSpindleMember(rbacDomain, record.Subject.String()); err != nil {
126
-
l.Error("failed to add member", "error", err)
127
-
return fmt.Errorf("failed to add member: %w", err)
128
-
}
129
-
l.Info("added member from firehose", "member", record.Subject)
130
-
131
-
if err := s.db.RemoveDid(record.Subject.String()); err != nil {
132
-
l.Error("failed to add did", "error", err)
133
-
return fmt.Errorf("failed to add did: %w", err)
134
-
}
135
-
s.jc.RemoveDid(record.Subject.String())
136
-
137
-
}
138
-
return nil
139
-
}
140
-
141
-
func (s *Spindle) ingestRepo(ctx context.Context, e *models.Event) error {
142
-
var err error
143
-
did := e.Did
144
-
145
-
l := s.l.With("component", "ingester", "record", tangled.RepoNSID)
146
-
147
-
l.Info("ingesting repo record", "did", did)
148
-
149
-
switch e.Commit.Operation {
150
-
case models.CommitOperationCreate, models.CommitOperationUpdate:
151
-
raw := e.Commit.Record
152
-
record := tangled.Repo{}
153
-
err = json.Unmarshal(raw, &record)
154
-
if err != nil {
155
-
l.Error("invalid record", "error", err)
156
-
return err
157
-
}
158
-
159
-
domain := s.cfg.Server.Hostname
160
-
161
-
// no spindle configured for this repo
162
-
if record.Spindle == nil {
163
-
l.Info("no spindle configured", "name", record.Name)
164
-
return nil
165
-
}
166
-
167
-
// this repo did not want this spindle
168
-
if *record.Spindle != domain {
169
-
l.Info("different spindle configured", "name", record.Name, "spindle", *record.Spindle, "domain", domain)
170
-
return nil
171
-
}
172
-
173
-
// add this repo to the watch list
174
-
if err := s.db.AddRepo(record.Knot, did, record.Name); err != nil {
175
-
l.Error("failed to add repo", "error", err)
176
-
return fmt.Errorf("failed to add repo: %w", err)
177
-
}
178
-
179
-
didSlashRepo, err := securejoin.SecureJoin(did, record.Name)
180
-
if err != nil {
181
-
return err
182
-
}
183
-
184
-
// add repo to rbac
185
-
if err := s.e.AddRepo(did, rbac.ThisServer, didSlashRepo); err != nil {
186
-
l.Error("failed to add repo to enforcer", "error", err)
187
-
return fmt.Errorf("failed to add repo: %w", err)
188
-
}
189
-
190
-
// add collaborators to rbac
191
-
owner, err := s.res.ResolveIdent(ctx, did)
192
-
if err != nil || owner.Handle.IsInvalidHandle() {
193
-
return err
194
-
}
195
-
if err := s.fetchAndAddCollaborators(ctx, owner, didSlashRepo); err != nil {
196
-
return err
197
-
}
198
-
199
-
// add this knot to the event consumer
200
-
src := eventconsumer.NewKnotSource(record.Knot)
201
-
s.ks.AddSource(context.Background(), src)
202
-
203
-
return nil
204
-
205
-
}
206
-
return nil
207
-
}
208
-
209
-
func (s *Spindle) ingestCollaborator(ctx context.Context, e *models.Event) error {
210
-
var err error
211
-
212
-
l := s.l.With("component", "ingester", "record", tangled.RepoCollaboratorNSID, "did", e.Did)
213
-
214
-
l.Info("ingesting collaborator record")
215
-
216
-
switch e.Commit.Operation {
217
-
case models.CommitOperationCreate, models.CommitOperationUpdate:
218
-
raw := e.Commit.Record
219
-
record := tangled.RepoCollaborator{}
220
-
err = json.Unmarshal(raw, &record)
221
-
if err != nil {
222
-
l.Error("invalid record", "error", err)
223
-
return err
224
-
}
225
-
226
-
subjectId, err := s.res.ResolveIdent(ctx, record.Subject)
227
-
if err != nil || subjectId.Handle.IsInvalidHandle() {
228
-
return err
229
-
}
230
-
231
-
repoAt, err := syntax.ParseATURI(record.Repo)
232
-
if err != nil {
233
-
l.Info("rejecting record, invalid repoAt", "repoAt", record.Repo)
234
-
return nil
235
-
}
236
-
237
-
// TODO: get rid of this entirely
238
-
// resolve this aturi to extract the repo record
239
-
owner, err := s.res.ResolveIdent(ctx, repoAt.Authority().String())
240
-
if err != nil || owner.Handle.IsInvalidHandle() {
241
-
return fmt.Errorf("failed to resolve handle: %w", err)
242
-
}
243
-
244
-
xrpcc := xrpc.Client{
245
-
Host: owner.PDSEndpoint(),
246
-
}
247
-
248
-
resp, err := comatproto.RepoGetRecord(ctx, &xrpcc, "", tangled.RepoNSID, repoAt.Authority().String(), repoAt.RecordKey().String())
249
-
if err != nil {
250
-
return err
251
-
}
252
-
253
-
repo := resp.Value.Val.(*tangled.Repo)
254
-
didSlashRepo, _ := securejoin.SecureJoin(owner.DID.String(), repo.Name)
255
-
256
-
// check perms for this user
257
-
if ok, err := s.e.IsCollaboratorInviteAllowed(owner.DID.String(), rbac.ThisServer, didSlashRepo); !ok || err != nil {
258
-
return fmt.Errorf("insufficient permissions: %w", err)
259
-
}
260
-
261
-
// add collaborator to rbac
262
-
if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil {
263
-
l.Error("failed to add repo to enforcer", "error", err)
264
-
return fmt.Errorf("failed to add repo: %w", err)
265
-
}
266
-
267
-
return nil
268
-
}
269
-
return nil
270
-
}
271
-
272
-
func (s *Spindle) fetchAndAddCollaborators(ctx context.Context, owner *identity.Identity, didSlashRepo string) error {
273
-
l := s.l.With("component", "ingester", "handler", "fetchAndAddCollaborators")
274
-
275
-
l.Info("fetching and adding existing collaborators")
276
-
277
-
xrpcc := xrpc.Client{
278
-
Host: owner.PDSEndpoint(),
279
-
}
280
-
281
-
resp, err := comatproto.RepoListRecords(ctx, &xrpcc, tangled.RepoCollaboratorNSID, "", 50, owner.DID.String(), false)
282
-
if err != nil {
283
-
return err
284
-
}
285
-
286
-
var errs error
287
-
for _, r := range resp.Records {
288
-
if r == nil {
289
-
continue
290
-
}
291
-
record := r.Value.Val.(*tangled.RepoCollaborator)
292
-
293
-
if err := s.e.AddCollaborator(record.Subject, rbac.ThisServer, didSlashRepo); err != nil {
294
-
l.Error("failed to add repo to enforcer", "error", err)
295
-
errors.Join(errs, fmt.Errorf("failed to add repo: %w", err))
296
-
}
297
-
}
298
-
299
-
return errs
300
-
}
···
+6
-1
spindle/models/logger.go
+6
-1
spindle/models/logger.go
···
12
type WorkflowLogger struct {
13
file *os.File
14
encoder *json.Encoder
15
}
16
17
-
func NewWorkflowLogger(baseDir string, wid WorkflowId) (*WorkflowLogger, error) {
18
path := LogFilePath(baseDir, wid)
19
20
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
···
25
return &WorkflowLogger{
26
file: file,
27
encoder: json.NewEncoder(file),
28
}, nil
29
}
30
···
62
63
func (w *dataWriter) Write(p []byte) (int, error) {
64
line := strings.TrimRight(string(p), "\r\n")
65
entry := NewDataLogLine(w.idx, line, w.stream)
66
if err := w.logger.encoder.Encode(entry); err != nil {
67
return 0, err
···
12
type WorkflowLogger struct {
13
file *os.File
14
encoder *json.Encoder
15
+
mask *SecretMask
16
}
17
18
+
func NewWorkflowLogger(baseDir string, wid WorkflowId, secretValues []string) (*WorkflowLogger, error) {
19
path := LogFilePath(baseDir, wid)
20
21
file, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
···
26
return &WorkflowLogger{
27
file: file,
28
encoder: json.NewEncoder(file),
29
+
mask: NewSecretMask(secretValues),
30
}, nil
31
}
32
···
64
65
func (w *dataWriter) Write(p []byte) (int, error) {
66
line := strings.TrimRight(string(p), "\r\n")
67
+
if w.logger.mask != nil {
68
+
line = w.logger.mask.Mask(line)
69
+
}
70
entry := NewDataLogLine(w.idx, line, w.stream)
71
if err := w.logger.encoder.Encode(entry); err != nil {
72
return 0, err
+1
-1
spindle/models/pipeline_env.go
+1
-1
spindle/models/pipeline_env.go
+51
spindle/models/secret_mask.go
+51
spindle/models/secret_mask.go
···
···
1
+
package models
2
+
3
+
import (
4
+
"encoding/base64"
5
+
"strings"
6
+
)
7
+
8
+
// SecretMask replaces secret values in strings with "***".
9
+
type SecretMask struct {
10
+
replacer *strings.Replacer
11
+
}
12
+
13
+
// NewSecretMask creates a mask for the given secret values.
14
+
// Also registers base64-encoded variants of each secret.
15
+
func NewSecretMask(values []string) *SecretMask {
16
+
var pairs []string
17
+
18
+
for _, value := range values {
19
+
if value == "" {
20
+
continue
21
+
}
22
+
23
+
pairs = append(pairs, value, "***")
24
+
25
+
b64 := base64.StdEncoding.EncodeToString([]byte(value))
26
+
if b64 != value {
27
+
pairs = append(pairs, b64, "***")
28
+
}
29
+
30
+
b64NoPad := strings.TrimRight(b64, "=")
31
+
if b64NoPad != b64 && b64NoPad != value {
32
+
pairs = append(pairs, b64NoPad, "***")
33
+
}
34
+
}
35
+
36
+
if len(pairs) == 0 {
37
+
return nil
38
+
}
39
+
40
+
return &SecretMask{
41
+
replacer: strings.NewReplacer(pairs...),
42
+
}
43
+
}
44
+
45
+
// Mask replaces all registered secret values with "***".
46
+
func (m *SecretMask) Mask(input string) string {
47
+
if m == nil || m.replacer == nil {
48
+
return input
49
+
}
50
+
return m.replacer.Replace(input)
51
+
}
+135
spindle/models/secret_mask_test.go
+135
spindle/models/secret_mask_test.go
···
···
1
+
package models
2
+
3
+
import (
4
+
"encoding/base64"
5
+
"testing"
6
+
)
7
+
8
+
func TestSecretMask_BasicMasking(t *testing.T) {
9
+
mask := NewSecretMask([]string{"mysecret123"})
10
+
11
+
input := "The password is mysecret123 in this log"
12
+
expected := "The password is *** in this log"
13
+
14
+
result := mask.Mask(input)
15
+
if result != expected {
16
+
t.Errorf("expected %q, got %q", expected, result)
17
+
}
18
+
}
19
+
20
+
func TestSecretMask_Base64Encoded(t *testing.T) {
21
+
secret := "mysecret123"
22
+
mask := NewSecretMask([]string{secret})
23
+
24
+
b64 := base64.StdEncoding.EncodeToString([]byte(secret))
25
+
input := "Encoded: " + b64
26
+
expected := "Encoded: ***"
27
+
28
+
result := mask.Mask(input)
29
+
if result != expected {
30
+
t.Errorf("expected %q, got %q", expected, result)
31
+
}
32
+
}
33
+
34
+
func TestSecretMask_Base64NoPadding(t *testing.T) {
35
+
// "test" encodes to "dGVzdA==" with padding
36
+
secret := "test"
37
+
mask := NewSecretMask([]string{secret})
38
+
39
+
b64NoPad := "dGVzdA" // base64 without padding
40
+
input := "Token: " + b64NoPad
41
+
expected := "Token: ***"
42
+
43
+
result := mask.Mask(input)
44
+
if result != expected {
45
+
t.Errorf("expected %q, got %q", expected, result)
46
+
}
47
+
}
48
+
49
+
func TestSecretMask_MultipleSecrets(t *testing.T) {
50
+
mask := NewSecretMask([]string{"password1", "apikey123"})
51
+
52
+
input := "Using password1 and apikey123 for auth"
53
+
expected := "Using *** and *** for auth"
54
+
55
+
result := mask.Mask(input)
56
+
if result != expected {
57
+
t.Errorf("expected %q, got %q", expected, result)
58
+
}
59
+
}
60
+
61
+
func TestSecretMask_MultipleOccurrences(t *testing.T) {
62
+
mask := NewSecretMask([]string{"secret"})
63
+
64
+
input := "secret appears twice: secret"
65
+
expected := "*** appears twice: ***"
66
+
67
+
result := mask.Mask(input)
68
+
if result != expected {
69
+
t.Errorf("expected %q, got %q", expected, result)
70
+
}
71
+
}
72
+
73
+
func TestSecretMask_ShortValues(t *testing.T) {
74
+
mask := NewSecretMask([]string{"abc", "xy", ""})
75
+
76
+
if mask == nil {
77
+
t.Fatal("expected non-nil mask")
78
+
}
79
+
80
+
input := "abc xy test"
81
+
expected := "*** *** test"
82
+
result := mask.Mask(input)
83
+
if result != expected {
84
+
t.Errorf("expected %q, got %q", expected, result)
85
+
}
86
+
}
87
+
88
+
func TestSecretMask_NilMask(t *testing.T) {
89
+
var mask *SecretMask
90
+
91
+
input := "some input text"
92
+
result := mask.Mask(input)
93
+
if result != input {
94
+
t.Errorf("expected %q, got %q", input, result)
95
+
}
96
+
}
97
+
98
+
func TestSecretMask_EmptyInput(t *testing.T) {
99
+
mask := NewSecretMask([]string{"secret"})
100
+
101
+
result := mask.Mask("")
102
+
if result != "" {
103
+
t.Errorf("expected empty string, got %q", result)
104
+
}
105
+
}
106
+
107
+
func TestSecretMask_NoMatch(t *testing.T) {
108
+
mask := NewSecretMask([]string{"secretvalue"})
109
+
110
+
input := "nothing to mask here"
111
+
result := mask.Mask(input)
112
+
if result != input {
113
+
t.Errorf("expected %q, got %q", input, result)
114
+
}
115
+
}
116
+
117
+
func TestSecretMask_EmptySecretsList(t *testing.T) {
118
+
mask := NewSecretMask([]string{})
119
+
120
+
if mask != nil {
121
+
t.Error("expected nil mask for empty secrets list")
122
+
}
123
+
}
124
+
125
+
func TestSecretMask_EmptySecretsFiltered(t *testing.T) {
126
+
mask := NewSecretMask([]string{"ab", "validpassword", "", "xyz"})
127
+
128
+
input := "Using validpassword here"
129
+
expected := "Using *** here"
130
+
131
+
result := mask.Mask(input)
132
+
if result != expected {
133
+
t.Errorf("expected %q, got %q", expected, result)
134
+
}
135
+
}
+133
-70
spindle/server.go
+133
-70
spindle/server.go
···
1
package spindle
2
3
import (
4
"context"
5
_ "embed"
6
"encoding/json"
···
8
"log/slog"
9
"maps"
10
"net/http"
11
12
"github.com/go-chi/chi/v5"
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/eventconsumer"
15
"tangled.org/core/eventconsumer/cursor"
16
"tangled.org/core/idresolver"
17
-
"tangled.org/core/jetstream"
18
"tangled.org/core/log"
19
"tangled.org/core/notifier"
20
-
"tangled.org/core/rbac"
21
"tangled.org/core/spindle/config"
22
"tangled.org/core/spindle/db"
23
"tangled.org/core/spindle/engine"
···
26
"tangled.org/core/spindle/queue"
27
"tangled.org/core/spindle/secrets"
28
"tangled.org/core/spindle/xrpc"
29
"tangled.org/core/xrpc/serviceauth"
30
)
31
32
//go:embed motd
33
var motd []byte
34
35
-
const (
36
-
rbacDomain = "thisserver"
37
-
)
38
-
39
type Spindle struct {
40
-
jc *jetstream.JetstreamClient
41
db *db.DB
42
-
e *rbac.Enforcer
43
l *slog.Logger
44
n *notifier.Notifier
45
engs map[string]models.Engine
···
54
func New(ctx context.Context, cfg *config.Config, engines map[string]models.Engine) (*Spindle, error) {
55
logger := log.FromContext(ctx)
56
57
-
d, err := db.Make(cfg.Server.DBPath)
58
if err != nil {
59
return nil, fmt.Errorf("failed to setup db: %w", err)
60
}
61
62
-
e, err := rbac.NewEnforcer(cfg.Server.DBPath)
63
if err != nil {
64
return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err)
65
}
66
-
e.E.EnableAutoSave(true)
67
68
n := notifier.New()
69
···
95
jq := queue.NewQueue(cfg.Server.QueueSize, cfg.Server.MaxJobCount)
96
logger.Info("initialized queue", "queueSize", cfg.Server.QueueSize, "numWorkers", cfg.Server.MaxJobCount)
97
98
-
collections := []string{
99
-
tangled.SpindleMemberNSID,
100
-
tangled.RepoNSID,
101
-
tangled.RepoCollaboratorNSID,
102
-
}
103
-
jc, err := jetstream.NewJetstreamClient(cfg.Server.JetstreamEndpoint, "spindle", collections, nil, log.SubLogger(logger, "jetstream"), d, true, true)
104
-
if err != nil {
105
-
return nil, fmt.Errorf("failed to setup jetstream client: %w", err)
106
-
}
107
-
jc.AddDid(cfg.Server.Owner)
108
-
109
-
// Check if the spindle knows about any Dids;
110
-
dids, err := d.GetAllDids()
111
-
if err != nil {
112
-
return nil, fmt.Errorf("failed to get all dids: %w", err)
113
-
}
114
-
for _, d := range dids {
115
-
jc.AddDid(d)
116
-
}
117
118
resolver := idresolver.DefaultResolver(cfg.Server.PlcUrl)
119
120
spindle := &Spindle{
121
-
jc: jc,
122
e: e,
123
db: d,
124
l: logger,
···
130
vault: vault,
131
}
132
133
-
err = e.AddSpindle(rbacDomain)
134
-
if err != nil {
135
-
return nil, fmt.Errorf("failed to set rbac domain: %w", err)
136
-
}
137
-
err = spindle.configureOwner()
138
if err != nil {
139
return nil, err
140
}
···
143
cursorStore, err := cursor.NewSQLiteStore(cfg.Server.DBPath)
144
if err != nil {
145
return nil, fmt.Errorf("failed to setup sqlite3 cursor store: %w", err)
146
-
}
147
-
148
-
err = jc.StartJetstream(ctx, spindle.ingest())
149
-
if err != nil {
150
-
return nil, fmt.Errorf("failed to start jetstream consumer: %w", err)
151
}
152
153
// for each incoming sh.tangled.pipeline, we execute
···
197
}
198
199
// Enforcer returns the RBAC enforcer instance.
200
-
func (s *Spindle) Enforcer() *rbac.Enforcer {
201
return s.e
202
}
203
···
217
s.ks.Start(ctx)
218
}()
219
220
s.l.Info("starting spindle server", "address", s.cfg.Server.ListenAddr)
221
return http.ListenAndServe(s.cfg.Server.ListenAddr, s.Router())
222
}
···
268
Config: s.cfg,
269
Resolver: s.res,
270
Vault: s.vault,
271
ServiceAuth: serviceAuth,
272
}
273
···
275
}
276
277
func (s *Spindle) processPipeline(ctx context.Context, src eventconsumer.Source, msg eventconsumer.Message) error {
278
if msg.Nsid == tangled.PipelineNSID {
279
tpl := tangled.Pipeline{}
280
err := json.Unmarshal(msg.EventJson, &tpl)
281
if err != nil {
···
296
}
297
298
// filter by repos
299
-
_, err = s.db.GetRepo(
300
-
tpl.TriggerMetadata.Repo.Knot,
301
-
tpl.TriggerMetadata.Repo.Did,
302
tpl.TriggerMetadata.Repo.Repo,
303
)
304
if err != nil {
305
-
return err
306
}
307
308
pipelineId := models.PipelineId{
···
323
Name: w.Name,
324
}, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n)
325
if err != nil {
326
-
return err
327
}
328
329
continue
···
337
338
ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl)
339
if err != nil {
340
-
return err
341
}
342
343
// inject TANGLED_* env vars after InitWorkflow
···
354
Name: w.Name,
355
}, s.n)
356
if err != nil {
357
-
return err
358
}
359
}
360
}
···
377
} else {
378
s.l.Error("failed to enqueue pipeline: queue is full")
379
}
380
}
381
382
return nil
383
}
384
385
-
func (s *Spindle) configureOwner() error {
386
-
cfgOwner := s.cfg.Server.Owner
387
388
-
existing, err := s.e.GetSpindleUsersByRole("server:owner", rbacDomain)
389
if err != nil {
390
-
return err
391
}
392
393
-
switch len(existing) {
394
-
case 0:
395
-
// no owner configured, continue
396
-
case 1:
397
-
// find existing owner
398
-
existingOwner := existing[0]
399
400
-
// no ownership change, this is okay
401
-
if existingOwner == s.cfg.Server.Owner {
402
-
break
403
}
404
-
405
-
// remove existing owner
406
-
err = s.e.RemoveSpindleOwner(rbacDomain, existingOwner)
407
-
if err != nil {
408
-
return nil
409
}
410
-
default:
411
-
return fmt.Errorf("more than one owner in DB, try deleting %q and starting over", s.cfg.Server.DBPath)
412
}
413
414
-
return s.e.AddSpindleOwner(rbacDomain, cfgOwner)
415
}
···
1
package spindle
2
3
import (
4
+
"bytes"
5
"context"
6
_ "embed"
7
"encoding/json"
···
9
"log/slog"
10
"maps"
11
"net/http"
12
+
"os"
13
+
"os/exec"
14
+
"path"
15
+
"strings"
16
17
+
"github.com/bluesky-social/indigo/atproto/syntax"
18
"github.com/go-chi/chi/v5"
19
+
"github.com/hashicorp/go-version"
20
"tangled.org/core/api/tangled"
21
"tangled.org/core/eventconsumer"
22
"tangled.org/core/eventconsumer/cursor"
23
"tangled.org/core/idresolver"
24
"tangled.org/core/log"
25
"tangled.org/core/notifier"
26
+
"tangled.org/core/rbac2"
27
"tangled.org/core/spindle/config"
28
"tangled.org/core/spindle/db"
29
"tangled.org/core/spindle/engine"
···
32
"tangled.org/core/spindle/queue"
33
"tangled.org/core/spindle/secrets"
34
"tangled.org/core/spindle/xrpc"
35
+
"tangled.org/core/tap"
36
"tangled.org/core/xrpc/serviceauth"
37
)
38
39
//go:embed motd
40
var motd []byte
41
42
type Spindle struct {
43
+
tap *tap.Client
44
db *db.DB
45
+
e *rbac2.Enforcer
46
l *slog.Logger
47
n *notifier.Notifier
48
engs map[string]models.Engine
···
57
func New(ctx context.Context, cfg *config.Config, engines map[string]models.Engine) (*Spindle, error) {
58
logger := log.FromContext(ctx)
59
60
+
if err := ensureGitVersion(); err != nil {
61
+
return nil, fmt.Errorf("ensuring git version: %w", err)
62
+
}
63
+
64
+
d, err := db.Make(ctx, cfg.Server.DBPath)
65
if err != nil {
66
return nil, fmt.Errorf("failed to setup db: %w", err)
67
}
68
69
+
e, err := rbac2.NewEnforcer(cfg.Server.DBPath)
70
if err != nil {
71
return nil, fmt.Errorf("failed to setup rbac enforcer: %w", err)
72
}
73
74
n := notifier.New()
75
···
101
jq := queue.NewQueue(cfg.Server.QueueSize, cfg.Server.MaxJobCount)
102
logger.Info("initialized queue", "queueSize", cfg.Server.QueueSize, "numWorkers", cfg.Server.MaxJobCount)
103
104
+
tap := tap.NewClient(cfg.Server.TapUrl, "")
105
106
resolver := idresolver.DefaultResolver(cfg.Server.PlcUrl)
107
108
spindle := &Spindle{
109
+
tap: &tap,
110
e: e,
111
db: d,
112
l: logger,
···
118
vault: vault,
119
}
120
121
+
err = e.SetSpindleOwner(spindle.cfg.Server.Owner, spindle.cfg.Server.Did())
122
if err != nil {
123
return nil, err
124
}
···
127
cursorStore, err := cursor.NewSQLiteStore(cfg.Server.DBPath)
128
if err != nil {
129
return nil, fmt.Errorf("failed to setup sqlite3 cursor store: %w", err)
130
}
131
132
// for each incoming sh.tangled.pipeline, we execute
···
176
}
177
178
// Enforcer returns the RBAC enforcer instance.
179
+
func (s *Spindle) Enforcer() *rbac2.Enforcer {
180
return s.e
181
}
182
···
196
s.ks.Start(ctx)
197
}()
198
199
+
go func() {
200
+
s.l.Info("starting tap stream consumer")
201
+
s.tap.Connect(ctx, &tap.SimpleIndexer{
202
+
EventHandler: s.processEvent,
203
+
})
204
+
}()
205
+
206
s.l.Info("starting spindle server", "address", s.cfg.Server.ListenAddr)
207
return http.ListenAndServe(s.cfg.Server.ListenAddr, s.Router())
208
}
···
254
Config: s.cfg,
255
Resolver: s.res,
256
Vault: s.vault,
257
+
Notifier: s.Notifier(),
258
ServiceAuth: serviceAuth,
259
}
260
···
262
}
263
264
func (s *Spindle) processPipeline(ctx context.Context, src eventconsumer.Source, msg eventconsumer.Message) error {
265
+
l := log.FromContext(ctx).With("handler", "processKnotStream")
266
+
l = l.With("src", src.Key(), "msg.Nsid", msg.Nsid, "msg.Rkey", msg.Rkey)
267
if msg.Nsid == tangled.PipelineNSID {
268
+
return nil
269
tpl := tangled.Pipeline{}
270
err := json.Unmarshal(msg.EventJson, &tpl)
271
if err != nil {
···
286
}
287
288
// filter by repos
289
+
_, err = s.db.GetRepoWithName(
290
+
syntax.DID(tpl.TriggerMetadata.Repo.Did),
291
tpl.TriggerMetadata.Repo.Repo,
292
)
293
if err != nil {
294
+
return fmt.Errorf("failed to get repo: %w", err)
295
}
296
297
pipelineId := models.PipelineId{
···
312
Name: w.Name,
313
}, fmt.Sprintf("unknown engine %#v", w.Engine), -1, s.n)
314
if err != nil {
315
+
return fmt.Errorf("db.StatusFailed: %w", err)
316
}
317
318
continue
···
326
327
ewf, err := s.engs[w.Engine].InitWorkflow(*w, tpl)
328
if err != nil {
329
+
return fmt.Errorf("init workflow: %w", err)
330
}
331
332
// inject TANGLED_* env vars after InitWorkflow
···
343
Name: w.Name,
344
}, s.n)
345
if err != nil {
346
+
return fmt.Errorf("db.StatusPending: %w", err)
347
}
348
}
349
}
···
366
} else {
367
s.l.Error("failed to enqueue pipeline: queue is full")
368
}
369
+
} else if msg.Nsid == tangled.GitRefUpdateNSID {
370
+
event := tangled.GitRefUpdate{}
371
+
if err := json.Unmarshal(msg.EventJson, &event); err != nil {
372
+
l.Error("error unmarshalling", "err", err)
373
+
return err
374
+
}
375
+
l = l.With("repoDid", event.RepoDid, "repoName", event.RepoName)
376
+
377
+
// use event.RepoAt
378
+
// sync git repos in {data}/repos/{did}/sh.tangled.repo/{rkey}
379
+
// if it's nil, don't run pipeline. knot needs upgrade
380
+
// we will leave sh.tangled.pipeline.trigger for backward compatibility
381
+
382
+
// NOTE: we are blindly trusting the knot that it will return only repos it own
383
+
repoCloneUri := s.newRepoCloneUrl(src.Key(), event.RepoDid, event.RepoName)
384
+
repoPath := s.newRepoPath(event.RepoDid, event.RepoName)
385
+
err := sparseSyncGitRepo(ctx, repoCloneUri, repoPath, event.NewSha)
386
+
if err != nil {
387
+
l.Error("failed to sync git repo", "err", err)
388
+
return fmt.Errorf("sync git repo: %w", err)
389
+
}
390
+
l.Info("synced git repo")
391
+
392
+
// TODO: plan the pipeline
393
}
394
395
return nil
396
}
397
398
+
func (s *Spindle) newRepoPath(did, name string) string {
399
+
return path.Join(s.cfg.Server.RepoDir(), did, name)
400
+
}
401
+
402
+
func (s *Spindle) newRepoCloneUrl(knot, did, name string) string {
403
+
scheme := "https://"
404
+
if s.cfg.Server.Dev {
405
+
scheme = "http://"
406
+
}
407
+
return fmt.Sprintf("%s%s/%s/%s", scheme, knot, did, name)
408
+
}
409
+
410
+
const RequiredVersion = "2.49.0"
411
+
412
+
func ensureGitVersion() error {
413
+
v, err := gitVersion()
414
+
if err != nil {
415
+
return fmt.Errorf("fetching git version: %w", err)
416
+
}
417
+
if v.LessThan(version.Must(version.NewVersion(RequiredVersion))) {
418
+
return fmt.Errorf("installed git version %q is not supported, Spindle requires git version >= %q", v, RequiredVersion)
419
+
}
420
+
return nil
421
+
}
422
423
+
// TODO: move to "git" module shared between knot, appview & spindle
424
+
func gitVersion() (*version.Version, error) {
425
+
var buf bytes.Buffer
426
+
cmd := exec.Command("git", "version")
427
+
cmd.Stdout = &buf
428
+
cmd.Stderr = os.Stderr
429
+
err := cmd.Run()
430
if err != nil {
431
+
return nil, err
432
+
}
433
+
fields := strings.Fields(buf.String())
434
+
if len(fields) < 3 {
435
+
return nil, fmt.Errorf("invalid git version: %s", buf)
436
}
437
438
+
// version string is like: "git version 2.29.3" or "git version 2.29.3.windows.1"
439
+
versionString := fields[2]
440
+
if pos := strings.Index(versionString, "windows"); pos >= 1 {
441
+
versionString = versionString[:pos-1]
442
+
}
443
+
return version.NewVersion(versionString)
444
+
}
445
446
+
func sparseSyncGitRepo(ctx context.Context, cloneUri, path, rev string) error {
447
+
exist, err := isDir(path)
448
+
if err != nil {
449
+
return err
450
+
}
451
+
if !exist {
452
+
if err := exec.Command("git", "clone", "--no-checkout", "--depth=1", "--filter=tree:0", "--revision="+rev, cloneUri, path).Run(); err != nil {
453
+
return fmt.Errorf("git clone: %w", err)
454
}
455
+
if err := exec.Command("git", "-C", path, "sparse-checkout", "set", "--no-cone", `'/.tangled/workflows'`).Run(); err != nil {
456
+
return fmt.Errorf("git sparse-checkout set: %w", err)
457
+
}
458
+
if err := exec.Command("git", "-C", path, "checkout", rev).Run(); err != nil {
459
+
return fmt.Errorf("git checkout: %w", err)
460
+
}
461
+
} else {
462
+
if err := exec.Command("git", "-C", path, "pull", "origin", rev).Run(); err != nil {
463
+
return fmt.Errorf("git pull: %w", err)
464
}
465
}
466
+
return nil
467
+
}
468
469
+
func isDir(path string) (bool, error) {
470
+
info, err := os.Stat(path)
471
+
if err == nil && info.IsDir() {
472
+
return true, nil
473
+
}
474
+
if os.IsNotExist(err) {
475
+
return false, nil
476
+
}
477
+
return false, err
478
}
+281
spindle/tap.go
+281
spindle/tap.go
···
···
1
+
package spindle
2
+
3
+
import (
4
+
"context"
5
+
"encoding/json"
6
+
"fmt"
7
+
"time"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/eventconsumer"
12
+
"tangled.org/core/spindle/db"
13
+
"tangled.org/core/tap"
14
+
)
15
+
16
+
func (s *Spindle) processEvent(ctx context.Context, evt tap.Event) error {
17
+
l := s.l.With("component", "tapIndexer")
18
+
19
+
var err error
20
+
switch evt.Type {
21
+
case tap.EvtRecord:
22
+
switch evt.Record.Collection.String() {
23
+
case tangled.SpindleMemberNSID:
24
+
err = s.processMember(ctx, evt)
25
+
case tangled.RepoNSID:
26
+
err = s.processRepo(ctx, evt)
27
+
case tangled.RepoCollaboratorNSID:
28
+
err = s.processCollaborator(ctx, evt)
29
+
case tangled.RepoPullNSID:
30
+
err = s.processPull(ctx, evt)
31
+
}
32
+
case tap.EvtIdentity:
33
+
// no-op
34
+
}
35
+
36
+
if err != nil {
37
+
l.Error("failed to process message. will retry later", "event.ID", evt.ID, "err", err)
38
+
return err
39
+
}
40
+
return nil
41
+
}
42
+
43
+
// NOTE: make sure to return nil if we don't need to retry (e.g. forbidden, unrelated)
44
+
45
+
func (s *Spindle) processMember(ctx context.Context, evt tap.Event) error {
46
+
l := s.l.With("component", "tapIndexer", "record", evt.Record.AtUri())
47
+
48
+
l.Info("processing spindle.member record")
49
+
50
+
// check perms for this user
51
+
if ok, err := s.e.IsSpindleMemberInviteAllowed(evt.Record.Did, s.cfg.Server.Did()); !ok || err != nil {
52
+
l.Warn("forbidden request", "did", evt.Record.Did, "error", err)
53
+
return nil
54
+
}
55
+
56
+
switch evt.Record.Action {
57
+
case tap.RecordCreateAction, tap.RecordUpdateAction:
58
+
record := tangled.SpindleMember{}
59
+
if err := json.Unmarshal(evt.Record.Record, &record); err != nil {
60
+
return fmt.Errorf("parsing record: %w", err)
61
+
}
62
+
63
+
domain := s.cfg.Server.Hostname
64
+
if record.Instance != domain {
65
+
l.Info("domain mismatch", "domain", record.Instance, "expected", domain)
66
+
return nil
67
+
}
68
+
69
+
created, err := time.Parse(record.CreatedAt, time.RFC3339)
70
+
if err != nil {
71
+
created = time.Now()
72
+
}
73
+
if err := db.AddSpindleMember(s.db, db.SpindleMember{
74
+
Did: evt.Record.Did,
75
+
Rkey: evt.Record.Rkey.String(),
76
+
Instance: record.Instance,
77
+
Subject: syntax.DID(record.Subject),
78
+
Created: created,
79
+
}); err != nil {
80
+
l.Error("failed to add member", "error", err)
81
+
return fmt.Errorf("adding member to db: %w", err)
82
+
}
83
+
if err := s.e.AddSpindleMember(syntax.DID(record.Subject), s.cfg.Server.Did()); err != nil {
84
+
return fmt.Errorf("adding member to rbac: %w", err)
85
+
}
86
+
if err := s.tap.AddRepos(ctx, []syntax.DID{syntax.DID(record.Subject)}); err != nil {
87
+
return fmt.Errorf("adding did to tap", err)
88
+
}
89
+
90
+
l.Info("added member", "member", record.Subject)
91
+
return nil
92
+
93
+
case tap.RecordDeleteAction:
94
+
var (
95
+
did = evt.Record.Did.String()
96
+
rkey = evt.Record.Rkey.String()
97
+
)
98
+
member, err := db.GetSpindleMember(s.db, did, rkey)
99
+
if err != nil {
100
+
return fmt.Errorf("finding member: %w", err)
101
+
}
102
+
103
+
if err := db.RemoveSpindleMember(s.db, did, rkey); err != nil {
104
+
return fmt.Errorf("removing member from db: %w", err)
105
+
}
106
+
if err := s.e.RemoveSpindleMember(member.Subject, s.cfg.Server.Did()); err != nil {
107
+
return fmt.Errorf("removing member from rbac: %w", err)
108
+
}
109
+
if err := s.tapSafeRemoveDid(ctx, member.Subject); err != nil {
110
+
return fmt.Errorf("removing did from tap: %w", err)
111
+
}
112
+
113
+
l.Info("removed member", "member", member.Subject)
114
+
return nil
115
+
}
116
+
return nil
117
+
}
118
+
119
+
func (s *Spindle) processCollaborator(ctx context.Context, evt tap.Event) error {
120
+
l := s.l.With("component", "tapIndexer", "record", evt.Record.AtUri())
121
+
122
+
l.Info("processing collaborator record")
123
+
switch evt.Record.Action {
124
+
case tap.RecordCreateAction, tap.RecordUpdateAction:
125
+
record := tangled.RepoCollaborator{}
126
+
if err := json.Unmarshal(evt.Record.Record, &record); err != nil {
127
+
l.Error("invalid record", "err", err)
128
+
return fmt.Errorf("parsing record: %w", err)
129
+
}
130
+
131
+
// check perms for this user
132
+
if ok, err := s.e.IsRepoCollaboratorInviteAllowed(evt.Record.Did, syntax.ATURI(record.Repo)); !ok || err != nil {
133
+
l.Warn("forbidden request", "did", evt.Record.Did, "err", err)
134
+
return nil
135
+
}
136
+
137
+
if err := s.db.PutRepoCollaborator(&db.RepoCollaborator{
138
+
Did: evt.Record.Did,
139
+
Rkey: evt.Record.Rkey,
140
+
Repo: syntax.ATURI(record.Repo),
141
+
Subject: syntax.DID(record.Subject),
142
+
}); err != nil {
143
+
return fmt.Errorf("adding collaborator to db: %w", err)
144
+
}
145
+
if err := s.e.AddRepoCollaborator(syntax.DID(record.Subject), syntax.ATURI(record.Repo)); err != nil {
146
+
return fmt.Errorf("adding collaborator to rbac: %w", err)
147
+
}
148
+
if err := s.tap.AddRepos(ctx, []syntax.DID{syntax.DID(record.Subject)}); err != nil {
149
+
return fmt.Errorf("adding did to tap: %w", err)
150
+
}
151
+
152
+
l.Info("add repo collaborator", "subejct", record.Subject, "repo", record.Repo)
153
+
return nil
154
+
155
+
case tap.RecordDeleteAction:
156
+
// get existing collaborator
157
+
collaborator, err := s.db.GetRepoCollaborator(evt.Record.Did, evt.Record.Rkey)
158
+
if err != nil {
159
+
return fmt.Errorf("failed to get existing collaborator info: %w", err)
160
+
}
161
+
162
+
// check perms for this user
163
+
if ok, err := s.e.IsRepoCollaboratorInviteAllowed(evt.Record.Did, collaborator.Repo); !ok || err != nil {
164
+
l.Warn("forbidden request", "did", evt.Record.Did, "err", err)
165
+
return nil
166
+
}
167
+
168
+
if err := s.db.RemoveRepoCollaborator(collaborator.Subject, collaborator.Rkey); err != nil {
169
+
return fmt.Errorf("removing collaborator from db: %w", err)
170
+
}
171
+
if err := s.e.RemoveRepoCollaborator(collaborator.Subject, collaborator.Repo); err != nil {
172
+
return fmt.Errorf("removing collaborator from rbac: %w", err)
173
+
}
174
+
if err := s.tapSafeRemoveDid(ctx, collaborator.Subject); err != nil {
175
+
return fmt.Errorf("removing did from tap: %w", err)
176
+
}
177
+
178
+
l.Info("removed repo collaborator", "subejct", collaborator.Subject, "repo", collaborator.Repo)
179
+
return nil
180
+
}
181
+
return nil
182
+
}
183
+
184
+
func (s *Spindle) processRepo(ctx context.Context, evt tap.Event) error {
185
+
l := s.l.With("component", "tapIndexer", "record", evt.Record.AtUri())
186
+
187
+
l.Info("processing repo record")
188
+
189
+
// check perms for this user
190
+
if ok, err := s.e.IsSpindleMember(evt.Record.Did, s.cfg.Server.Did()); !ok || err != nil {
191
+
l.Warn("forbidden request", "did", evt.Record.Did, "err", err)
192
+
return nil
193
+
}
194
+
195
+
switch evt.Record.Action {
196
+
case tap.RecordCreateAction, tap.RecordUpdateAction:
197
+
record := tangled.Repo{}
198
+
if err := json.Unmarshal(evt.Record.Record, &record); err != nil {
199
+
return fmt.Errorf("parsing record: %w", err)
200
+
}
201
+
202
+
domain := s.cfg.Server.Hostname
203
+
if record.Spindle == nil || *record.Spindle != domain {
204
+
if record.Spindle == nil {
205
+
l.Info("spindle isn't configured", "name", record.Name)
206
+
} else {
207
+
l.Info("different spindle configured", "name", record.Name, "spindle", *record.Spindle, "domain", domain)
208
+
}
209
+
if err := s.db.DeleteRepo(evt.Record.Did, evt.Record.Rkey); err != nil {
210
+
return fmt.Errorf("deleting repo from db: %w", err)
211
+
}
212
+
return nil
213
+
}
214
+
215
+
if err := s.db.PutRepo(&db.Repo{
216
+
Did: evt.Record.Did,
217
+
Rkey: evt.Record.Rkey,
218
+
Name: record.Name,
219
+
Knot: record.Knot,
220
+
}); err != nil {
221
+
return fmt.Errorf("adding repo to db: %w", err)
222
+
}
223
+
224
+
if err := s.e.AddRepo(evt.Record.AtUri()); err != nil {
225
+
return fmt.Errorf("adding repo to rbac")
226
+
}
227
+
228
+
// add this knot to the event consumer
229
+
src := eventconsumer.NewKnotSource(record.Knot)
230
+
s.ks.AddSource(context.Background(), src)
231
+
232
+
l.Info("added repo", "repo", evt.Record.AtUri())
233
+
return nil
234
+
235
+
case tap.RecordDeleteAction:
236
+
// check perms for this user
237
+
if ok, err := s.e.IsRepoOwner(evt.Record.Did, evt.Record.AtUri()); !ok || err != nil {
238
+
l.Warn("forbidden request", "did", evt.Record.Did, "err", err)
239
+
return nil
240
+
}
241
+
242
+
if err := s.db.DeleteRepo(evt.Record.Did, evt.Record.Rkey); err != nil {
243
+
return fmt.Errorf("deleting repo from db: %w", err)
244
+
}
245
+
246
+
if err := s.e.DeleteRepo(evt.Record.AtUri()); err != nil {
247
+
return fmt.Errorf("deleting repo from rbac: %w", err)
248
+
}
249
+
250
+
l.Info("deleted repo", "repo", evt.Record.AtUri())
251
+
return nil
252
+
}
253
+
return nil
254
+
}
255
+
256
+
func (s *Spindle) processPull(ctx context.Context, evt tap.Event) error {
257
+
l := s.l.With("component", "tapIndexer", "record", evt.Record.AtUri())
258
+
259
+
l.Info("processing pull record")
260
+
261
+
switch evt.Record.Action {
262
+
case tap.RecordCreateAction, tap.RecordUpdateAction:
263
+
// TODO
264
+
case tap.RecordDeleteAction:
265
+
// TODO
266
+
}
267
+
return nil
268
+
}
269
+
270
+
func (s *Spindle) tapSafeRemoveDid(ctx context.Context, did syntax.DID) error {
271
+
known, err := s.db.IsKnownDid(syntax.DID(did))
272
+
if err != nil {
273
+
return fmt.Errorf("ensuring did known state: %w", err)
274
+
}
275
+
if !known {
276
+
if err := s.tap.RemoveRepos(ctx, []syntax.DID{did}); err != nil {
277
+
return fmt.Errorf("removing did from tap: %w", err)
278
+
}
279
+
}
280
+
return nil
281
+
}
+1
-2
spindle/xrpc/add_secret.go
+1
-2
spindle/xrpc/add_secret.go
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
"tangled.org/core/spindle/secrets"
16
xrpcerr "tangled.org/core/xrpc/errors"
17
)
···
68
return
69
}
70
71
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
72
l.Error("insufficent permissions", "did", actorDid.String())
73
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
74
return
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/spindle/secrets"
15
xrpcerr "tangled.org/core/xrpc/errors"
16
)
···
67
return
68
}
69
70
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
71
l.Error("insufficent permissions", "did", actorDid.String())
72
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
73
return
+1
-2
spindle/xrpc/list_secrets.go
+1
-2
spindle/xrpc/list_secrets.go
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
-
"tangled.org/core/rbac"
15
"tangled.org/core/spindle/secrets"
16
xrpcerr "tangled.org/core/xrpc/errors"
17
)
···
63
return
64
}
65
66
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
67
l.Error("insufficent permissions", "did", actorDid.String())
68
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
69
return
···
11
"github.com/bluesky-social/indigo/xrpc"
12
securejoin "github.com/cyphar/filepath-securejoin"
13
"tangled.org/core/api/tangled"
14
"tangled.org/core/spindle/secrets"
15
xrpcerr "tangled.org/core/xrpc/errors"
16
)
···
62
return
63
}
64
65
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
66
l.Error("insufficent permissions", "did", actorDid.String())
67
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
return
+1
-1
spindle/xrpc/owner.go
+1
-1
spindle/xrpc/owner.go
+72
spindle/xrpc/pipeline_cancelPipeline.go
+72
spindle/xrpc/pipeline_cancelPipeline.go
···
···
1
+
package xrpc
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
"net/http"
7
+
"strings"
8
+
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
+
"tangled.org/core/api/tangled"
11
+
"tangled.org/core/spindle/models"
12
+
xrpcerr "tangled.org/core/xrpc/errors"
13
+
)
14
+
15
+
func (x *Xrpc) CancelPipeline(w http.ResponseWriter, r *http.Request) {
16
+
l := x.Logger
17
+
fail := func(e xrpcerr.XrpcError) {
18
+
l.Error("failed", "kind", e.Tag, "error", e.Message)
19
+
writeError(w, e, http.StatusBadRequest)
20
+
}
21
+
l.Debug("cancel pipeline")
22
+
23
+
actorDid, ok := r.Context().Value(ActorDid).(syntax.DID)
24
+
if !ok {
25
+
fail(xrpcerr.MissingActorDidError)
26
+
return
27
+
}
28
+
29
+
var input tangled.PipelineCancelPipeline_Input
30
+
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
31
+
fail(xrpcerr.GenericError(err))
32
+
return
33
+
}
34
+
35
+
aturi := syntax.ATURI(input.Pipeline)
36
+
wid := models.WorkflowId{
37
+
PipelineId: models.PipelineId{
38
+
Knot: strings.TrimPrefix(aturi.Authority().String(), "did:web:"),
39
+
Rkey: aturi.RecordKey().String(),
40
+
},
41
+
Name: input.Workflow,
42
+
}
43
+
l.Debug("cancel pipeline", "wid", wid)
44
+
45
+
// unfortunately we have to resolve repo-at here
46
+
repoAt, err := syntax.ParseATURI(input.Repo)
47
+
if err != nil {
48
+
fail(xrpcerr.InvalidRepoError(input.Repo))
49
+
return
50
+
}
51
+
52
+
isRepoOwner, err := x.Enforcer.IsRepoOwner(actorDid, repoAt)
53
+
if err != nil || !isRepoOwner {
54
+
fail(xrpcerr.AccessControlError(actorDid.String()))
55
+
return
56
+
}
57
+
for _, engine := range x.Engines {
58
+
l.Debug("destorying workflow", "wid", wid)
59
+
err = engine.DestroyWorkflow(r.Context(), wid)
60
+
if err != nil {
61
+
fail(xrpcerr.GenericError(fmt.Errorf("dailed to destroy workflow: %w", err)))
62
+
return
63
+
}
64
+
err = x.Db.StatusCancelled(wid, "User canceled the workflow", -1, x.Notifier)
65
+
if err != nil {
66
+
fail(xrpcerr.GenericError(fmt.Errorf("dailed to emit status failed: %w", err)))
67
+
return
68
+
}
69
+
}
70
+
71
+
w.WriteHeader(http.StatusOK)
72
+
}
+1
-2
spindle/xrpc/remove_secret.go
+1
-2
spindle/xrpc/remove_secret.go
···
10
"github.com/bluesky-social/indigo/xrpc"
11
securejoin "github.com/cyphar/filepath-securejoin"
12
"tangled.org/core/api/tangled"
13
-
"tangled.org/core/rbac"
14
"tangled.org/core/spindle/secrets"
15
xrpcerr "tangled.org/core/xrpc/errors"
16
)
···
62
return
63
}
64
65
-
if ok, err := x.Enforcer.IsSettingsAllowed(actorDid.String(), rbac.ThisServer, didPath); !ok || err != nil {
66
l.Error("insufficent permissions", "did", actorDid.String())
67
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
68
return
···
10
"github.com/bluesky-social/indigo/xrpc"
11
securejoin "github.com/cyphar/filepath-securejoin"
12
"tangled.org/core/api/tangled"
13
"tangled.org/core/spindle/secrets"
14
xrpcerr "tangled.org/core/xrpc/errors"
15
)
···
61
return
62
}
63
64
+
if ok, err := x.Enforcer.IsRepoSettingsWriteAllowed(actorDid, repoAt); !ok || err != nil {
65
l.Error("insufficent permissions", "did", actorDid.String())
66
writeError(w, xrpcerr.AccessControlError(actorDid.String()), http.StatusUnauthorized)
67
return
+5
-2
spindle/xrpc/xrpc.go
+5
-2
spindle/xrpc/xrpc.go
···
10
11
"tangled.org/core/api/tangled"
12
"tangled.org/core/idresolver"
13
-
"tangled.org/core/rbac"
14
"tangled.org/core/spindle/config"
15
"tangled.org/core/spindle/db"
16
"tangled.org/core/spindle/models"
···
24
type Xrpc struct {
25
Logger *slog.Logger
26
Db *db.DB
27
-
Enforcer *rbac.Enforcer
28
Engines map[string]models.Engine
29
Config *config.Config
30
Resolver *idresolver.Resolver
31
Vault secrets.Manager
32
ServiceAuth *serviceauth.ServiceAuth
33
}
34
···
41
r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
42
r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
43
r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
44
})
45
46
// service query endpoints (no auth required)
···
10
11
"tangled.org/core/api/tangled"
12
"tangled.org/core/idresolver"
13
+
"tangled.org/core/notifier"
14
+
"tangled.org/core/rbac2"
15
"tangled.org/core/spindle/config"
16
"tangled.org/core/spindle/db"
17
"tangled.org/core/spindle/models"
···
25
type Xrpc struct {
26
Logger *slog.Logger
27
Db *db.DB
28
+
Enforcer *rbac2.Enforcer
29
Engines map[string]models.Engine
30
Config *config.Config
31
Resolver *idresolver.Resolver
32
Vault secrets.Manager
33
+
Notifier *notifier.Notifier
34
ServiceAuth *serviceauth.ServiceAuth
35
}
36
···
43
r.Post("/"+tangled.RepoAddSecretNSID, x.AddSecret)
44
r.Post("/"+tangled.RepoRemoveSecretNSID, x.RemoveSecret)
45
r.Get("/"+tangled.RepoListSecretsNSID, x.ListSecrets)
46
+
r.Post("/"+tangled.PipelineCancelPipelineNSID, x.CancelPipeline)
47
})
48
49
// service query endpoints (no auth required)
+18
tap/simpleIndexer.go
+18
tap/simpleIndexer.go
···
···
1
+
package tap
2
+
3
+
import "context"
4
+
5
+
type SimpleIndexer struct {
6
+
EventHandler func(ctx context.Context, evt Event) error
7
+
ErrorHandler func(ctx context.Context, err error)
8
+
}
9
+
10
+
var _ Handler = (*SimpleIndexer)(nil)
11
+
12
+
func (i *SimpleIndexer) OnEvent(ctx context.Context, evt Event) error {
13
+
return i.EventHandler(ctx, evt)
14
+
}
15
+
16
+
func (i *SimpleIndexer) OnError(ctx context.Context, err error) {
17
+
i.ErrorHandler(ctx, err)
18
+
}
+169
tap/tap.go
+169
tap/tap.go
···
···
1
+
/// heavily inspired by <https://github.com/bluesky-social/atproto/blob/c7f5a868837d3e9b3289f988fee2267789327b06/packages/tap/README.md>
2
+
3
+
package tap
4
+
5
+
import (
6
+
"bytes"
7
+
"context"
8
+
"encoding/json"
9
+
"fmt"
10
+
"net/http"
11
+
"net/url"
12
+
13
+
"github.com/bluesky-social/indigo/atproto/syntax"
14
+
"github.com/gorilla/websocket"
15
+
"tangled.org/core/log"
16
+
)
17
+
18
+
// type WebsocketOptions struct {
19
+
// maxReconnectSeconds int
20
+
// heartbeatIntervalMs int
21
+
// // onReconnectError
22
+
// }
23
+
24
+
type Handler interface {
25
+
OnEvent(ctx context.Context, evt Event) error
26
+
OnError(ctx context.Context, err error)
27
+
}
28
+
29
+
type Client struct {
30
+
Url string
31
+
AdminPassword string
32
+
HTTPClient *http.Client
33
+
}
34
+
35
+
func NewClient(url, adminPassword string) Client {
36
+
return Client{
37
+
Url: url,
38
+
AdminPassword: adminPassword,
39
+
HTTPClient: &http.Client{},
40
+
}
41
+
}
42
+
43
+
func (c *Client) AddRepos(ctx context.Context, dids []syntax.DID) error {
44
+
body, err := json.Marshal(map[string][]syntax.DID{"dids": dids})
45
+
if err != nil {
46
+
return err
47
+
}
48
+
req, err := http.NewRequestWithContext(ctx, "POST", c.Url+"/repos/add", bytes.NewReader(body))
49
+
if err != nil {
50
+
return err
51
+
}
52
+
req.SetBasicAuth("admin", c.AdminPassword)
53
+
req.Header.Set("Content-Type", "application/json")
54
+
55
+
resp, err := c.HTTPClient.Do(req)
56
+
if err != nil {
57
+
return err
58
+
}
59
+
defer resp.Body.Close()
60
+
if resp.StatusCode != http.StatusOK {
61
+
return fmt.Errorf("tap: /repos/add failed with status %d", resp.StatusCode)
62
+
}
63
+
return nil
64
+
}
65
+
66
+
func (c *Client) RemoveRepos(ctx context.Context, dids []syntax.DID) error {
67
+
body, err := json.Marshal(map[string][]syntax.DID{"dids": dids})
68
+
if err != nil {
69
+
return err
70
+
}
71
+
req, err := http.NewRequestWithContext(ctx, "POST", c.Url+"/repos/remove", bytes.NewReader(body))
72
+
if err != nil {
73
+
return err
74
+
}
75
+
req.SetBasicAuth("admin", c.AdminPassword)
76
+
req.Header.Set("Content-Type", "application/json")
77
+
78
+
resp, err := c.HTTPClient.Do(req)
79
+
if err != nil {
80
+
return err
81
+
}
82
+
defer resp.Body.Close()
83
+
if resp.StatusCode != http.StatusOK {
84
+
return fmt.Errorf("tap: /repos/remove failed with status %d", resp.StatusCode)
85
+
}
86
+
return nil
87
+
}
88
+
89
+
func (c *Client) Connect(ctx context.Context, handler Handler) error {
90
+
l := log.FromContext(ctx)
91
+
92
+
u, err := url.Parse(c.Url)
93
+
if err != nil {
94
+
return err
95
+
}
96
+
if u.Scheme == "https" {
97
+
u.Scheme = "wss"
98
+
} else {
99
+
u.Scheme = "ws"
100
+
}
101
+
u.Path = "/channel"
102
+
103
+
// TODO: set auth on dial
104
+
105
+
url := u.String()
106
+
107
+
// var backoff int
108
+
// for {
109
+
// select {
110
+
// case <-ctx.Done():
111
+
// return ctx.Err()
112
+
// default:
113
+
// }
114
+
//
115
+
// header := http.Header{
116
+
// "Authorization": []string{""},
117
+
// }
118
+
// conn, res, err := websocket.DefaultDialer.DialContext(ctx, url, header)
119
+
// if err != nil {
120
+
// l.Warn("dialing failed", "url", url, "err", err, "backoff", backoff)
121
+
// time.Sleep(time.Duration(5+backoff) * time.Second)
122
+
// backoff++
123
+
//
124
+
// continue
125
+
// } else {
126
+
// backoff = 0
127
+
// }
128
+
//
129
+
// l.Info("event subscription response", "code", res.StatusCode)
130
+
// }
131
+
132
+
// TODO: keep websocket connection alive
133
+
conn, _, err := websocket.DefaultDialer.DialContext(ctx, url, nil)
134
+
if err != nil {
135
+
return err
136
+
}
137
+
defer conn.Close()
138
+
139
+
for {
140
+
select {
141
+
case <-ctx.Done():
142
+
return ctx.Err()
143
+
default:
144
+
}
145
+
_, message, err := conn.ReadMessage()
146
+
if err != nil {
147
+
return err
148
+
}
149
+
150
+
var ev Event
151
+
if err := json.Unmarshal(message, &ev); err != nil {
152
+
handler.OnError(ctx, fmt.Errorf("failed to parse message: %w", err))
153
+
continue
154
+
}
155
+
if err := handler.OnEvent(ctx, ev); err != nil {
156
+
handler.OnError(ctx, fmt.Errorf("failed to process event %d: %w", ev.ID, err))
157
+
continue
158
+
}
159
+
160
+
ack := map[string]any{
161
+
"type": "ack",
162
+
"id": ev.ID,
163
+
}
164
+
if err := conn.WriteJSON(ack); err != nil {
165
+
l.Warn("failed to send ack", "err", err)
166
+
continue
167
+
}
168
+
}
169
+
}
+62
tap/types.go
+62
tap/types.go
···
···
1
+
package tap
2
+
3
+
import (
4
+
"encoding/json"
5
+
"fmt"
6
+
7
+
"github.com/bluesky-social/indigo/atproto/syntax"
8
+
)
9
+
10
+
type EventType string
11
+
12
+
const (
13
+
EvtRecord EventType = "record"
14
+
EvtIdentity EventType = "identity"
15
+
)
16
+
17
+
type Event struct {
18
+
ID int64 `json:"id"`
19
+
Type EventType `json:"type"`
20
+
Record *RecordEventData `json:"record,omitempty"`
21
+
Identity *IdentityEventData `json:"identity,omitempty"`
22
+
}
23
+
24
+
type RecordEventData struct {
25
+
Live bool `json:"live"`
26
+
Did syntax.DID `json:"did"`
27
+
Rev string `json:"rev"`
28
+
Collection syntax.NSID `json:"collection"`
29
+
Rkey syntax.RecordKey `json:"rkey"`
30
+
Action RecordAction `json:"action"`
31
+
Record json.RawMessage `json:"record,omitempty"`
32
+
CID *syntax.CID `json:"cid,omitempty"`
33
+
}
34
+
35
+
func (r *RecordEventData) AtUri() syntax.ATURI {
36
+
return syntax.ATURI(fmt.Sprintf("at://%s/%s/%s", r.Did, r.Collection, r.Rkey))
37
+
}
38
+
39
+
type RecordAction string
40
+
41
+
const (
42
+
RecordCreateAction RecordAction = "create"
43
+
RecordUpdateAction RecordAction = "update"
44
+
RecordDeleteAction RecordAction = "delete"
45
+
)
46
+
47
+
type IdentityEventData struct {
48
+
DID syntax.DID `json:"did"`
49
+
Handle string `json:"handle"`
50
+
IsActive bool `json:"is_active"`
51
+
Status RepoStatus `json:"status"`
52
+
}
53
+
54
+
type RepoStatus string
55
+
56
+
const (
57
+
RepoStatusActive RepoStatus = "active"
58
+
RepoStatusTakendown RepoStatus = "takendown"
59
+
RepoStatusSuspended RepoStatus = "suspended"
60
+
RepoStatusDeactivated RepoStatus = "deactivated"
61
+
RepoStatusDeleted RepoStatus = "deleted"
62
+
)
+6
-1
types/commit.go
+6
-1
types/commit.go
···
174
175
func (commit Commit) CoAuthors() []object.Signature {
176
var coAuthors []object.Signature
177
-
178
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
179
180
for _, match := range matches {
181
if len(match) >= 3 {
182
name := strings.TrimSpace(match[1])
183
email := strings.TrimSpace(match[2])
184
185
coAuthors = append(coAuthors, object.Signature{
186
Name: name,
···
174
175
func (commit Commit) CoAuthors() []object.Signature {
176
var coAuthors []object.Signature
177
+
seen := make(map[string]bool)
178
matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
179
180
for _, match := range matches {
181
if len(match) >= 3 {
182
name := strings.TrimSpace(match[1])
183
email := strings.TrimSpace(match[2])
184
+
185
+
if seen[email] {
186
+
continue
187
+
}
188
+
seen[email] = true
189
190
coAuthors = append(coAuthors, object.Signature{
191
Name: name,