···4343 create table if not exists follows (4444 user_did text not null,4545 subject_did text not null,4646+ at_uri text not null,4647 followed_at text not null default (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')),4748 primary key (user_did, subject_did),4849 check (user_did <> subject_did)
+44-3
appview/db/follow.go
···11package db2233-func (d *DB) AddFollow(userDid, subjectDid string) error {44- query := `insert into follows (user_did, subject_did) values (?, ?)`55- _, err := d.db.Exec(query, userDid, subjectDid)33+import (44+ "log"55+ "time"66+)77+88+type Follow struct {99+ UserDid string1010+ SubjectDid string1111+ FollowedAt *time.Time1212+ AtUri string1313+}1414+1515+func (d *DB) AddFollow(userDid, subjectDid, atUri string) error {1616+ query := `insert into follows (user_did, subject_did, at_uri) values (?, ?, ?)`1717+ _, err := d.db.Exec(query, userDid, subjectDid, atUri)1818+ return err1919+}2020+2121+// Get a follow record2222+func (d *DB) GetFollow(userDid, subjectDid string) (*Follow, error) {2323+ query := `select user_did, subject_did, followed_at, at_uri from follows where user_did = ? and subject_did = ?`2424+ row := d.db.QueryRow(query, userDid, subjectDid)2525+2626+ var follow Follow2727+ var followedAt string2828+ err := row.Scan(&follow.UserDid, &follow.SubjectDid, &followedAt, &follow.AtUri)2929+ if err != nil {3030+ return nil, err3131+ }3232+3333+ followedAtTime, err := time.Parse(time.RFC3339, followedAt)3434+ if err != nil {3535+ log.Println("unable to determine followed at time")3636+ follow.FollowedAt = nil3737+ } else {3838+ follow.FollowedAt = &followedAtTime3939+ }4040+4141+ return &follow, nil4242+}4343+4444+// Get a follow record4545+func (d *DB) DeleteFollow(userDid, subjectDid string) error {4646+ _, err := d.db.Exec(`delete from follows where user_did = ? and subject_did = ?`, userDid, subjectDid)647 return err748}
+73-21
appview/state/state.go
···531531}532532533533func (s *State) Follow(w http.ResponseWriter, r *http.Request) {534534- subject := r.FormValue("subject")534534+ currentUser := s.auth.GetUser(r)535535536536+ subject := r.URL.Query().Get("subject")536537 if subject == "" {537538 log.Println("invalid form")538539 return539540 }540541541542 subjectIdent, err := s.resolver.ResolveIdent(r.Context(), subject)542542- currentUser := s.auth.GetUser(r)543543-544544- client, _ := s.auth.AuthorizedClient(r)545545- createdAt := time.Now().Format(time.RFC3339)546546- resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{547547- Collection: tangled.GraphFollowNSID,548548- Repo: currentUser.Did,549549- Rkey: s.TID(),550550- Record: &lexutil.LexiconTypeDecoder{551551- Val: &tangled.GraphFollow{552552- Subject: subjectIdent.DID.String(),553553- CreatedAt: createdAt,554554- }},555555- })556556-557557- err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String())558543 if err != nil {559559- log.Println("failed to follow", err)544544+ log.Println("failed to follow, invalid did")545545+ }546546+547547+ if currentUser.Did == subjectIdent.DID.String() {548548+ log.Println("cant follow or unfollow yourself")560549 return561550 }562551563563- log.Println("created atproto record: ", resp.Uri)552552+ client, _ := s.auth.AuthorizedClient(r)564553565565- return554554+ switch r.Method {555555+ case http.MethodPost:556556+ createdAt := time.Now().Format(time.RFC3339)557557+ resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{558558+ Collection: tangled.GraphFollowNSID,559559+ Repo: currentUser.Did,560560+ Rkey: s.TID(),561561+ Record: &lexutil.LexiconTypeDecoder{562562+ Val: &tangled.GraphFollow{563563+ Subject: subjectIdent.DID.String(),564564+ CreatedAt: createdAt,565565+ }},566566+ })567567+ if err != nil {568568+ log.Println("failed to create atproto record", err)569569+ return570570+ }571571+572572+ err = s.db.AddFollow(currentUser.Did, subjectIdent.DID.String(), resp.Uri)573573+ if err != nil {574574+ log.Println("failed to follow", err)575575+ return576576+ }577577+578578+ log.Println("created atproto record: ", resp.Uri)579579+580580+ return581581+ case http.MethodDelete:582582+ // find the record in the db583583+584584+ follow, err := s.db.GetFollow(currentUser.Did, subjectIdent.DID.String())585585+ if err != nil {586586+ log.Println("failed to get follow relationship")587587+ return588588+ }589589+590590+ existingRecordUri, _ := syntax.ParseATURI(follow.AtUri)591591+592592+ resp, err := comatproto.RepoDeleteRecord(r.Context(), client, &comatproto.RepoDeleteRecord_Input{593593+ Collection: tangled.GraphFollowNSID,594594+ Repo: currentUser.Did,595595+ Rkey: existingRecordUri.RecordKey().String(),596596+ })597597+598598+ log.Println(resp.Commit.Cid)599599+600600+ if err != nil {601601+ log.Println("failed to unfollow")602602+ return603603+ }604604+605605+ err = s.db.DeleteFollow(currentUser.Did, subjectIdent.DID.String())606606+ if err != nil {607607+ log.Println("failed to delete follow from DB")608608+ // this is not an issue, the firehose event might have already done this609609+ }610610+611611+ w.WriteHeader(http.StatusNoContent)612612+ return613613+ }614614+566615}567616568617func (s *State) Router() http.Handler {···704655 // r.Post("/import", s.ImportRepo)705656 })706657707707- r.With(AuthMiddleware(s)).Put("/follow", s.Follow)658658+ r.With(AuthMiddleware(s)).Route("/follow", func(r chi.Router) {659659+ r.Post("/", s.Follow)660660+ r.Delete("/", s.Follow)661661+ })708662709663 r.Route("/settings", func(r chi.Router) {710664 r.Use(AuthMiddleware(s))