diff --git a/api/tangled/cbor_gen.go b/api/tangled/cbor_gen.go index 63cdf665..21197704 100644 --- a/api/tangled/cbor_gen.go +++ b/api/tangled/cbor_gen.go @@ -438,3 +438,167 @@ func (t *KnotMember) UnmarshalCBOR(r io.Reader) (err error) { return nil } +func (t *GraphFollow) MarshalCBOR(w io.Writer) error { + if t == nil { + _, err := w.Write(cbg.CborNull) + return err + } + + cw := cbg.NewCborWriter(w) + + if _, err := cw.Write([]byte{163}); err != nil { + return err + } + + // t.LexiconTypeID (string) (string) + if len("$type") > 1000000 { + return xerrors.Errorf("Value in field \"$type\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("$type"))); err != nil { + return err + } + if _, err := cw.WriteString(string("$type")); err != nil { + return err + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("sh.tangled.graph.follow"))); err != nil { + return err + } + if _, err := cw.WriteString(string("sh.tangled.graph.follow")); err != nil { + return err + } + + // t.Subject (string) (string) + if len("subject") > 1000000 { + return xerrors.Errorf("Value in field \"subject\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("subject"))); err != nil { + return err + } + if _, err := cw.WriteString(string("subject")); err != nil { + return err + } + + if len(t.Subject) > 1000000 { + return xerrors.Errorf("Value in field t.Subject was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.Subject))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.Subject)); err != nil { + return err + } + + // t.CreatedAt (string) (string) + if len("createdAt") > 1000000 { + return xerrors.Errorf("Value in field \"createdAt\" was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len("createdAt"))); err != nil { + return err + } + if _, err := cw.WriteString(string("createdAt")); err != nil { + return err + } + + if len(t.CreatedAt) > 1000000 { + return xerrors.Errorf("Value in field t.CreatedAt was too long") + } + + if err := cw.WriteMajorTypeHeader(cbg.MajTextString, uint64(len(t.CreatedAt))); err != nil { + return err + } + if _, err := cw.WriteString(string(t.CreatedAt)); err != nil { + return err + } + return nil +} + +func (t *GraphFollow) UnmarshalCBOR(r io.Reader) (err error) { + *t = GraphFollow{} + + cr := cbg.NewCborReader(r) + + maj, extra, err := cr.ReadHeader() + if err != nil { + return err + } + defer func() { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + }() + + if maj != cbg.MajMap { + return fmt.Errorf("cbor input should be of type map") + } + + if extra > cbg.MaxLength { + return fmt.Errorf("GraphFollow: map struct too large (%d)", extra) + } + + n := extra + + nameBuf := make([]byte, 9) + for i := uint64(0); i < n; i++ { + nameLen, ok, err := cbg.ReadFullStringIntoBuf(cr, nameBuf, 1000000) + if err != nil { + return err + } + + if !ok { + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(cr, func(cid.Cid) {}); err != nil { + return err + } + continue + } + + switch string(nameBuf[:nameLen]) { + // t.LexiconTypeID (string) (string) + case "$type": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.LexiconTypeID = string(sval) + } + // t.Subject (string) (string) + case "subject": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.Subject = string(sval) + } + // t.CreatedAt (string) (string) + case "createdAt": + + { + sval, err := cbg.ReadStringWithMax(cr, 1000000) + if err != nil { + return err + } + + t.CreatedAt = string(sval) + } + + default: + // Field doesn't exist on this type, so ignore it + if err := cbg.ScanForLinks(r, func(cid.Cid) {}); err != nil { + return err + } + } + } + + return nil +} diff --git a/api/tangled/graphfollow.go b/api/tangled/graphfollow.go new file mode 100644 index 00000000..5039ba93 --- /dev/null +++ b/api/tangled/graphfollow.go @@ -0,0 +1,23 @@ +// Code generated by cmd/lexgen (see Makefile's lexgen); DO NOT EDIT. + +package tangled + +// schema: sh.tangled.graph.follow + +import ( + "github.com/bluesky-social/indigo/lex/util" +) + +const ( + GraphFollowNSID = "sh.tangled.graph.follow" +) + +func init() { + util.RegisterType("sh.tangled.graph.follow", &GraphFollow{}) +} // +// RECORDTYPE: GraphFollow +type GraphFollow struct { + LexiconTypeID string `json:"$type,const=sh.tangled.graph.follow" cborgen:"$type,const=sh.tangled.graph.follow"` + CreatedAt string `json:"createdAt" cborgen:"createdAt"` + Subject string `json:"subject" cborgen:"subject"` +} diff --git a/appview/db/db.go b/appview/db/db.go index e4312890..aa5bea6e 100644 --- a/appview/db/db.go +++ b/appview/db/db.go @@ -40,6 +40,13 @@ func Make(dbPath string) (*DB, error) { created text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), unique(did, name, knot) ); + create table if not exists follows ( + user_did text not null, + subject_did text not null, + followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), + primary key (user_did, subject_did), + check (user_did <> subject_did) + ); `) if err != nil { return nil, err diff --git a/appview/db/follow.go b/appview/db/follow.go new file mode 100644 index 00000000..d02e758d --- /dev/null +++ b/appview/db/follow.go @@ -0,0 +1,7 @@ +package db + +func (d *DB) AddFollow(userDid, subjectDid string) error { + query := `insert into follows (user_did, subject_did) values (?, ?)` + _, err := d.db.Exec(query, userDid, subjectDid) + return err +} diff --git a/appview/state/settings.go b/appview/state/settings.go new file mode 100644 index 00000000..201b61ca --- /dev/null +++ b/appview/state/settings.go @@ -0,0 +1,76 @@ +package state + +import ( + "log" + "net/http" + "strings" + "time" + + comatproto "github.com/bluesky-social/indigo/api/atproto" + lexutil "github.com/bluesky-social/indigo/lex/util" + "github.com/gliderlabs/ssh" + "github.com/sotangled/tangled/api/tangled" + "github.com/sotangled/tangled/appview/pages" +) + +func (s *State) Settings(w http.ResponseWriter, r *http.Request) { + // for now, this is just pubkeys + user := s.auth.GetUser(r) + pubKeys, err := s.db.GetPublicKeys(user.Did) + if err != nil { + log.Println(err) + } + + s.pages.Settings(w, pages.SettingsParams{ + LoggedInUser: user, + PubKeys: pubKeys, + }) +} + +func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + w.Write([]byte("unimplemented")) + log.Println("unimplemented") + return + case http.MethodPut: + did := s.auth.GetDid(r) + key := r.FormValue("key") + key = strings.TrimSpace(key) + name := r.FormValue("name") + client, _ := s.auth.AuthorizedClient(r) + + _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) + if err != nil { + log.Printf("parsing public key: %s", err) + return + } + + if err := s.db.AddPublicKey(did, name, key); err != nil { + log.Printf("adding public key: %s", err) + return + } + + // store in pds too + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: tangled.PublicKeyNSID, + Repo: did, + Rkey: s.TID(), + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.PublicKey{ + Created: time.Now().Format(time.RFC3339), + Key: key, + Name: name, + }}, + }) + // invalid record + if err != nil { + log.Printf("failed to create record: %s", err) + return + } + + log.Println("created atproto record: ", resp.Uri) + + return + } +} diff --git a/appview/state/state.go b/appview/state/state.go index 77980a22..64159e6f 100644 --- a/appview/state/state.go +++ b/appview/state/state.go @@ -14,7 +14,6 @@ import ( comatproto "github.com/bluesky-social/indigo/api/atproto" "github.com/bluesky-social/indigo/atproto/syntax" lexutil "github.com/bluesky-social/indigo/lex/util" - "github.com/gliderlabs/ssh" "github.com/go-chi/chi/v5" tangled "github.com/sotangled/tangled/api/tangled" "github.com/sotangled/tangled/appview" @@ -154,20 +153,6 @@ func (s *State) RegistrationKey(w http.ResponseWriter, r *http.Request) { } } -func (s *State) Settings(w http.ResponseWriter, r *http.Request) { - // for now, this is just pubkeys - user := s.auth.GetUser(r) - pubKeys, err := s.db.GetPublicKeys(user.Did) - if err != nil { - log.Println(err) - } - - s.pages.Settings(w, pages.SettingsParams{ - LoggedInUser: user, - PubKeys: pubKeys, - }) -} - func (s *State) Keys(w http.ResponseWriter, r *http.Request) { user := chi.URLParam(r, "user") user = strings.TrimPrefix(user, "@") @@ -200,54 +185,6 @@ func (s *State) Keys(w http.ResponseWriter, r *http.Request) { } } -func (s *State) SettingsKeys(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - w.Write([]byte("unimplemented")) - log.Println("unimplemented") - return - case http.MethodPut: - did := s.auth.GetDid(r) - key := r.FormValue("key") - key = strings.TrimSpace(key) - name := r.FormValue("name") - client, _ := s.auth.AuthorizedClient(r) - - _, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key)) - if err != nil { - log.Printf("parsing public key: %s", err) - return - } - - if err := s.db.AddPublicKey(did, name, key); err != nil { - log.Printf("adding public key: %s", err) - return - } - - // store in pds too - resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ - Collection: tangled.PublicKeyNSID, - Repo: did, - Rkey: s.TID(), - Record: &lexutil.LexiconTypeDecoder{ - Val: &tangled.PublicKey{ - Created: time.Now().Format(time.RFC3339), - Key: key, - Name: name, - }}, - }) - // invalid record - if err != nil { - log.Printf("failed to create record: %s", err) - return - } - - log.Println("created atproto record: ", resp.Uri) - - return - } -} - // create a signed request and check if a node responds to that func (s *State) InitKnotServer(w http.ResponseWriter, r *http.Request) { user := s.auth.GetUser(r) @@ -594,6 +531,46 @@ func (s *State) ProfilePage(w http.ResponseWriter, r *http.Request) { }) } +func (s *State) Follow(w http.ResponseWriter, r *http.Request) { + subject := r.FormValue("subject") + + if subject == "" { + log.Println("invalid form") + return + } + + subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject) + currentUser := s.auth.GetUser(r) + + client, _ := s.auth.AuthorizedClient(r) + createdAt := time.Now().Format(time.RFC3339) + resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{ + Collection: tangled.GraphFollowNSID, + Repo: currentUser.Did, + Rkey: s.TID(), + Record: &lexutil.LexiconTypeDecoder{ + Val: &tangled.GraphFollow{ + Subject: subjectIdent.DID.String(), + CreatedAt: createdAt, + }}, + }) + + err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String()) + if err != nil { + log.Println("failed to follow", err) + return + } + + // invalid record + if err != nil { + log.Printf("failed to create record: %s", err) + return + } + log.Println("created atproto record: ", resp.Uri) + + return +} + func (s *State) Router() http.Handler { router := chi.NewRouter() @@ -671,6 +648,8 @@ func (s *State) StandardRouter() http.Handler { // r.Post("/import", s.ImportRepo) }) + r.With(AuthMiddleware(s)).Put("/follow", s.Follow) + r.Route("/settings", func(r chi.Router) { r.Use(AuthMiddleware(s)) r.Get("/", s.Settings) diff --git a/cmd/gen.go b/cmd/gen.go index fab1b77b..5874b88b 100644 --- a/cmd/gen.go +++ b/cmd/gen.go @@ -16,6 +16,7 @@ func main() { "tangled", shtangled.PublicKey{}, shtangled.KnotMember{}, + shtangled.GraphFollow{}, ); err != nil { panic(err) } diff --git a/lexicons/follow.json b/lexicons/follow.json new file mode 100644 index 00000000..9417adb8 --- /dev/null +++ b/lexicons/follow.json @@ -0,0 +1,30 @@ +{ + "lexicon": 1, + "id": "sh.tangled.graph.follow", + "needsCbor": true, + "needsType": true, + "defs": { + "main": { + "type": "record", + "key": "tid", + "record": { + "type": "object", + "required": [ + "createdAt", + "subject" + ], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime" + }, + "subject": { + "type": "string", + "format": "did" + } + } + } + } + } +} +