A vibe coded tangled fork which supports pijul.
1package state
2
3import (
4 "log"
5 "net/http"
6 "time"
7
8 comatproto "github.com/bluesky-social/indigo/api/atproto"
9 "github.com/bluesky-social/indigo/atproto/syntax"
10 lexutil "github.com/bluesky-social/indigo/lex/util"
11 "tangled.org/core/api/tangled"
12 "tangled.org/core/appview/db"
13 "tangled.org/core/appview/models"
14 "tangled.org/core/appview/pages"
15 "tangled.org/core/tid"
16)
17
18func (s *State) Follow(w http.ResponseWriter, r *http.Request) {
19 currentUser := s.oauth.GetMultiAccountUser(r)
20
21 subject := r.URL.Query().Get("subject")
22 if subject == "" {
23 log.Println("invalid form")
24 return
25 }
26
27 subjectIdent, err := s.idResolver.ResolveIdent(r.Context(), subject)
28 if err != nil {
29 log.Println("failed to follow, invalid did")
30 return
31 }
32
33 if currentUser.Active.Did == subjectIdent.DID.String() {
34 log.Println("cant follow or unfollow yourself")
35 return
36 }
37
38 client, err := s.oauth.AuthorizedClient(r)
39 if err != nil {
40 log.Println("failed to authorize client")
41 return
42 }
43
44 switch r.Method {
45 case http.MethodPost:
46 follow := models.Follow{
47 UserDid: currentUser.Active.Did,
48 SubjectDid: subjectIdent.DID.String(),
49 Rkey: tid.TID(),
50 FollowedAt: time.Now(),
51 }
52
53 tx, err := s.db.BeginTx(r.Context(), nil)
54 if err != nil {
55 s.logger.Error("failed to start transaction", "err", err)
56 return
57 }
58 defer tx.Rollback()
59
60 if err := db.UpsertFollow(tx, follow); err != nil {
61 s.logger.Error("failed to follow", "err", err)
62 return
63 }
64
65 record := follow.AsRecord()
66 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
67 Collection: tangled.GraphFollowNSID,
68 Repo: currentUser.Active.Did,
69 Rkey: follow.Rkey,
70 Record: &lexutil.LexiconTypeDecoder{
71 Val: &record,
72 },
73 })
74 if err != nil {
75 log.Println("failed to create atproto record", err)
76 return
77 }
78 log.Println("created atproto record: ", resp.Uri)
79
80 if err := tx.Commit(); err != nil {
81 s.logger.Error("failed to commit transaction", "err", err)
82 // DB op failed but record is created in PDS. Ingester will backfill the missed operation
83 }
84
85 s.notifier.NewFollow(r.Context(), &follow)
86
87 followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
88 if err != nil {
89 log.Println("failed to get follow stats", err)
90 }
91
92 s.pages.FollowFragment(w, pages.FollowFragmentParams{
93 UserDid: subjectIdent.DID.String(),
94 FollowStatus: models.IsFollowing,
95 FollowersCount: followStats.Followers,
96 })
97
98 return
99 case http.MethodDelete:
100 tx, err := s.db.BeginTx(r.Context(), nil)
101 if err != nil {
102 s.logger.Error("failed to start transaction", "err", err)
103 }
104 defer tx.Rollback()
105
106 follows, err := db.DeleteFollow(tx, syntax.DID(currentUser.Active.Did), subjectIdent.DID)
107 if err != nil {
108 s.logger.Error("failed to delete follows from db", "err", err)
109 return
110 }
111
112 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
113 for _, followAt := range follows {
114 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
115 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{
116 Collection: tangled.GraphFollowNSID,
117 Rkey: followAt.RecordKey().String(),
118 },
119 })
120 }
121 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
122 Repo: currentUser.Active.Did,
123 Writes: writes,
124 })
125 if err != nil {
126 s.logger.Error("failed to delete follows from PDS", "err", err)
127 return
128 }
129
130 if err := tx.Commit(); err != nil {
131 s.logger.Error("failed to commit transaction", "err", err)
132 // DB op failed but record is created in PDS. Ingester will backfill the missed operation
133 }
134
135 s.notifier.DeleteFollow(r.Context(), &models.Follow{
136 UserDid: currentUser.Active.Did,
137 SubjectDid: subjectIdent.DID.String(),
138 // Rkey
139 // FollowedAt
140 })
141
142 followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
143 if err != nil {
144 log.Println("failed to get follow stats", err)
145 }
146
147 s.pages.FollowFragment(w, pages.FollowFragmentParams{
148 UserDid: subjectIdent.DID.String(),
149 FollowStatus: models.IsNotFollowing,
150 FollowersCount: followStats.Followers,
151 })
152
153 return
154 }
155
156}