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 createdAt := time.Now().Format(time.RFC3339)
47 rkey := tid.TID()
48 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
49 Collection: tangled.GraphFollowNSID,
50 Repo: currentUser.Active.Did,
51 Rkey: rkey,
52 Record: &lexutil.LexiconTypeDecoder{
53 Val: &tangled.GraphFollow{
54 Subject: subjectIdent.DID.String(),
55 CreatedAt: createdAt,
56 }},
57 })
58 if err != nil {
59 log.Println("failed to create atproto record", err)
60 return
61 }
62
63 log.Println("created atproto record: ", resp.Uri)
64
65 follow := &models.Follow{
66 UserDid: currentUser.Active.Did,
67 SubjectDid: subjectIdent.DID.String(),
68 Rkey: rkey,
69 }
70
71 err = db.AddFollow(s.db, follow)
72 if err != nil {
73 log.Println("failed to follow", err)
74 return
75 }
76
77 s.notifier.NewFollow(r.Context(), follow)
78
79 followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
80 if err != nil {
81 log.Println("failed to get follow stats", err)
82 }
83
84 s.pages.FollowFragment(w, pages.FollowFragmentParams{
85 UserDid: subjectIdent.DID.String(),
86 FollowStatus: models.IsFollowing,
87 FollowersCount: followStats.Followers,
88 })
89
90 return
91 case http.MethodDelete:
92 tx, err := s.db.BeginTx(r.Context(), nil)
93 if err != nil {
94 s.logger.Error("failed to start transaction", "err", err)
95 }
96 defer tx.Rollback()
97
98 follows, err := db.DeleteFollow(tx, syntax.DID(currentUser.Active.Did), subjectIdent.DID)
99 if err != nil {
100 s.logger.Error("failed to delete follows from db", "err", err)
101 return
102 }
103
104 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
105 for _, followAt := range follows {
106 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
107 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{
108 Collection: tangled.GraphFollowNSID,
109 Rkey: followAt.RecordKey().String(),
110 },
111 })
112 }
113 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
114 Repo: currentUser.Active.Did,
115 Writes: writes,
116 })
117 if err != nil {
118 s.logger.Error("failed to delete follows from PDS", "err", err)
119 return
120 }
121
122 if err := tx.Commit(); err != nil {
123 s.logger.Error("failed to commit transaction", "err", err)
124 // DB op failed but record is created in PDS. Ingester will backfill the missed operation
125 }
126
127 s.notifier.DeleteFollow(r.Context(), &models.Follow{
128 UserDid: currentUser.Active.Did,
129 SubjectDid: subjectIdent.DID.String(),
130 // Rkey
131 // FollowedAt
132 })
133
134 followStats, err := db.GetFollowerFollowingCount(s.db, subjectIdent.DID.String())
135 if err != nil {
136 log.Println("failed to get follow stats", err)
137 }
138
139 s.pages.FollowFragment(w, pages.FollowFragmentParams{
140 UserDid: subjectIdent.DID.String(),
141 FollowStatus: models.IsNotFollowing,
142 FollowersCount: followStats.Followers,
143 })
144
145 return
146 }
147
148}