forked from tangled.org/core
Monorepo for Tangled — https://tangled.org

knotserver: rework knot publish mechanism

knots require owners to publish a record to their own PDS declaring the
knot domain

Signed-off-by: oppiliappan <me@oppi.li>

Changed files
+139 -33
knotserver
+1 -1
go.mod
···
github.com/posthog/posthog-go v1.5.5
github.com/resend/resend-go/v2 v2.15.0
github.com/sethvargo/go-envconfig v1.1.0
-
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e
+
github.com/whyrusleeping/cbor-gen v0.3.1
github.com/yuin/goldmark v1.4.13
golang.org/x/net v0.39.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
+2 -2
go.sum
···
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ=
github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw=
-
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
-
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
+
github.com/whyrusleeping/cbor-gen v0.3.1 h1:82ioxmhEYut7LBVGhGq8xoRkXPLElVuh5mV67AFfdv0=
+
github.com/whyrusleeping/cbor-gen v0.3.1/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+6
knotserver/db/init.go
···
pragma auto_vacuum = incremental;
pragma busy_timeout = 5000;
+
create table if not exists owner (
+
id integer primary key check (id = 0),
+
did text not null,
+
createdAt text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now'))
+
);
+
create table if not exists known_dids (
did text primary key
);
+18
knotserver/db/owner.go
···
+
package db
+
+
func (d *DB) SetOwner(did string) error {
+
query := `insert into owner (id, did) values (?, ?)`
+
_, err := d.db.Exec(query, 0, did)
+
return err
+
}
+
+
func (d *DB) Owner() (string, error) {
+
query := `select did from owner`
+
+
var did string
+
err := d.db.QueryRow(query).Scan(&did)
+
if err != nil {
+
return "", err
+
}
+
return did, nil
+
}
+100 -27
knotserver/handler.go
···
"log/slog"
"net/http"
"runtime/debug"
+
"time"
+
"github.com/bluesky-social/indigo/api/atproto"
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
lexutil "github.com/bluesky-social/indigo/lex/util"
+
"github.com/bluesky-social/indigo/xrpc"
"github.com/go-chi/chi/v5"
+
"tangled.sh/tangled.sh/core/api/tangled"
"tangled.sh/tangled.sh/core/jetstream"
"tangled.sh/tangled.sh/core/knotserver/config"
"tangled.sh/tangled.sh/core/knotserver/db"
"tangled.sh/tangled.sh/core/rbac"
+
"tangled.sh/tangled.sh/core/resolver"
)
const (
···
)
type Handle struct {
-
c *config.Config
-
db *db.DB
-
jc *jetstream.JetstreamClient
-
e *rbac.Enforcer
-
l *slog.Logger
-
-
// init is a channel that is closed when the knot has been initailized
-
// i.e. when the first user (knot owner) has been added.
-
init chan struct{}
-
knotInitialized bool
+
c *config.Config
+
db *db.DB
+
jc *jetstream.JetstreamClient
+
e *rbac.Enforcer
+
l *slog.Logger
+
clock syntax.TIDClock
}
func Setup(ctx context.Context, c *config.Config, db *db.DB, e *rbac.Enforcer, jc *jetstream.JetstreamClient, l *slog.Logger) (http.Handler, error) {
-
r := chi.NewRouter()
-
h := Handle{
-
c: c,
-
db: db,
-
e: e,
-
l: l,
-
jc: jc,
-
init: make(chan struct{}),
+
c: c,
+
db: db,
+
e: e,
+
l: l,
+
jc: jc,
}
err := e.AddDomain(ThisServer)
···
return nil, fmt.Errorf("failed to setup enforcer: %w", err)
}
+
// if this knot does not already have an owner, publish it
+
if _, err := h.db.Owner(); err != nil {
+
l.Info("publishing this knot ...", "owner", h.c.Owner.Did)
+
err = h.Publish()
+
if err != nil {
+
return nil, fmt.Errorf("failed to announce knot: %w", err)
+
}
+
}
+
+
l.Info("this knot has been published", "owner", h.c.Owner.Did)
+
err = h.jc.StartJetstream(ctx, h.processMessages)
if err != nil {
return nil, fmt.Errorf("failed to start jetstream: %w", err)
}
-
// Check if the knot knows about any Dids;
-
// if it does, it is already initialized and we can repopulate the
-
// Jetstream subscriptions.
dids, err := db.GetAllDids()
if err != nil {
return nil, fmt.Errorf("failed to get all Dids: %w", err)
}
-
if len(dids) > 0 {
-
h.knotInitialized = true
-
close(h.init)
-
for _, d := range dids {
-
h.jc.AddDid(d)
-
}
+
for _, d := range dids {
+
h.jc.AddDid(d)
}
+
+
r := chi.NewRouter()
r.Get("/", h.Index)
r.Get("/capabilities", h.Capabilities)
···
w.Header().Set("Content-Type", "text/plain")
fmt.Fprintf(w, "knotserver/%s", version)
}
+
+
func (h *Handle) Publish() error {
+
ownerDid := h.c.Owner.Did
+
appPassword := h.c.Owner.AppPassword
+
+
res := resolver.DefaultResolver()
+
ident, err := res.ResolveIdent(context.Background(), ownerDid)
+
if err != nil {
+
return err
+
}
+
+
client := xrpc.Client{
+
Host: ident.PDSEndpoint(),
+
}
+
+
resp, err := atproto.ServerCreateSession(context.Background(), &client, &atproto.ServerCreateSession_Input{
+
Identifier: ownerDid,
+
Password: appPassword,
+
})
+
if err != nil {
+
return err
+
}
+
+
authClient := xrpc.Client{
+
Host: ident.PDSEndpoint(),
+
Auth: &xrpc.AuthInfo{
+
AccessJwt: resp.AccessJwt,
+
RefreshJwt: resp.RefreshJwt,
+
Handle: resp.Handle,
+
Did: resp.Did,
+
},
+
}
+
+
rkey := h.TID()
+
+
// write a "knot" record to the owners's pds
+
_, err = atproto.RepoPutRecord(context.Background(), &authClient, &atproto.RepoPutRecord_Input{
+
Collection: tangled.KnotNSID,
+
Repo: ownerDid,
+
Rkey: rkey,
+
Record: &lexutil.LexiconTypeDecoder{
+
Val: &tangled.Knot{
+
CreatedAt: time.Now().Format(time.RFC3339),
+
Host: h.c.Server.Hostname,
+
},
+
},
+
})
+
if err != nil {
+
return err
+
}
+
+
err = h.db.SetOwner(ownerDid)
+
if err != nil {
+
return err
+
}
+
+
err = h.db.AddDid(ownerDid)
+
if err != nil {
+
return err
+
}
+
+
return nil
+
}
+
+
func (h *Handle) TID() string {
+
return h.clock.Next().String()
+
}
+1 -3
knotserver/routes.go
···
func (h *Handle) Init(w http.ResponseWriter, r *http.Request) {
l := h.l.With("handler", "Init")
-
if h.knotInitialized {
+
if _, err := h.db.Owner(); err == nil {
writeError(w, "knot already initialized", http.StatusConflict)
return
···
writeError(w, err.Error(), http.StatusInternalServerError)
return
-
-
close(h.init)
mac := hmac.New(sha256.New, []byte(h.c.Server.Secret))
mac.Write([]byte("ok"))
+11
knotserver/tid.go
···
+
package knotserver
+
+
import (
+
"github.com/bluesky-social/indigo/atproto/syntax"
+
)
+
+
var c syntax.TIDClock = syntax.NewTIDClock(0)
+
+
func TID() string {
+
return c.Next().String()
+
}