Monorepo for Tangled
tangled.org
1package state
2
3import (
4 "fmt"
5 "log"
6 "net/http"
7 "time"
8
9 comatproto "github.com/bluesky-social/indigo/api/atproto"
10 "github.com/bluesky-social/indigo/atproto/syntax"
11 lexutil "github.com/bluesky-social/indigo/lex/util"
12 "tangled.org/core/api/tangled"
13 "tangled.org/core/appview/db"
14 "tangled.org/core/appview/models"
15 "tangled.org/core/appview/pages"
16 "tangled.org/core/tid"
17)
18
19func resolveStarSubject(d db.Execer, subjectUri syntax.ATURI) (models.StarSubjectType, string, *tangled.FeedStar_Subject, error) {
20 collection := subjectUri.Collection()
21
22 switch collection.String() {
23 case tangled.RepoNSID:
24 repo, err := db.GetRepoByAtUri(d, subjectUri.String())
25 if err != nil {
26 return "", "", nil, err
27 }
28 if repo.RepoDid == "" {
29 return "", "", nil, fmt.Errorf("repo has no DID: %s", subjectUri)
30 }
31 subject := &tangled.FeedStar_Subject{
32 FeedStar_Repo: &tangled.FeedStar_Repo{Did: repo.RepoDid},
33 }
34 return models.StarSubjectRepo, repo.RepoDid, subject, nil
35
36 case tangled.StringNSID:
37 uri := subjectUri.String()
38 subject := &tangled.FeedStar_Subject{
39 FeedStar_String: &tangled.FeedStar_String{Uri: uri},
40 }
41 return models.StarSubjectString, uri, subject, nil
42
43 default:
44 return "", "", nil, fmt.Errorf("unsupported star subject collection: %s", collection)
45 }
46}
47
48func (s *State) Star(w http.ResponseWriter, r *http.Request) {
49 l := s.logger.With("handler", "Star")
50 currentUser := s.oauth.GetMultiAccountUser(r)
51
52 subject := r.URL.Query().Get("subject")
53 if subject == "" {
54 l.Warn("invalid form")
55 return
56 }
57
58 subjectUri, err := syntax.ParseATURI(subject)
59 if err != nil {
60 l.Warn("invalid form", "subject", subject, "err", err)
61 return
62 }
63
64 subjectType, subjectKey, starSubject, err := resolveStarSubject(s.db, subjectUri)
65 if err != nil {
66 log.Println("failed to resolve star subject", err)
67 return
68 }
69
70 client, err := s.oauth.AuthorizedClient(r)
71 if err != nil {
72 l.Error("failed to authorize client", "err", err)
73 return
74 }
75
76 repoName := r.URL.Query().Get("repoName")
77
78 switch r.Method {
79 case http.MethodPost:
80 star := models.Star{
81 Did: currentUser.Did,
82 Rkey: tid.TID(),
83 SubjectType: subjectType,
84 Subject: subjectKey,
85 Created: time.Now(),
86 }
87
88 tx, err := s.db.BeginTx(r.Context(), nil)
89 if err != nil {
90 l.Error("failed to start transaction", "err", err)
91 return
92 }
93 defer tx.Rollback()
94
95 if err := db.UpsertStar(tx, star); err != nil {
96 l.Error("failed to star", "err", err)
97 return
98 }
99
100 resp, err := comatproto.RepoPutRecord(r.Context(), client, &comatproto.RepoPutRecord_Input{
101 Collection: tangled.FeedStarNSID,
102 Repo: currentUser.Did,
103 Rkey: star.Rkey,
104 Record: &lexutil.LexiconTypeDecoder{
105 Val: &tangled.FeedStar{
106 CreatedAt: star.Created.Format(time.RFC3339),
107 Subject: starSubject,
108 },
109 },
110 })
111 if err != nil {
112 l.Error("failed to create atproto record", "err", err)
113 return
114 }
115 l.Info("created atproto record", "uri", resp.Uri)
116
117 if err := tx.Commit(); err != nil {
118 l.Error("failed to commit transaction", "err", err)
119 // DB op failed but record is created in PDS. Ingester will backfill the missed operation
120 }
121
122 s.notifier.NewStar(r.Context(), &star)
123
124 starCount, err := db.GetStarCount(s.db, subjectType, subjectKey)
125 if err != nil {
126 l.Error("failed to get star count", "subject", subjectKey, "err", err)
127 }
128
129 s.pages.StarBtnFragment(w, pages.StarBtnFragmentParams{
130 IsStarred: true,
131 SubjectAt: subjectUri,
132 StarCount: starCount,
133 RepoName: repoName,
134 })
135
136 return
137 case http.MethodDelete:
138 tx, err := s.db.BeginTx(r.Context(), nil)
139 if err != nil {
140 l.Error("failed to start transaction", "err", err)
141 }
142 defer tx.Rollback()
143
144 stars, err := db.DeleteStars(tx, syntax.DID(currentUser.Did), subjectKey)
145 if err != nil {
146 l.Error("failed to delete stars from db", "err", err)
147 return
148 }
149
150 var writes []*comatproto.RepoApplyWrites_Input_Writes_Elem
151 for _, starAt := range stars {
152 writes = append(writes, &comatproto.RepoApplyWrites_Input_Writes_Elem{
153 RepoApplyWrites_Delete: &comatproto.RepoApplyWrites_Delete{
154 Collection: tangled.FeedStarNSID,
155 Rkey: starAt.RecordKey().String(),
156 },
157 })
158 }
159 _, err = comatproto.RepoApplyWrites(r.Context(), client, &comatproto.RepoApplyWrites_Input{
160 Repo: currentUser.Did,
161 Writes: writes,
162 })
163 if err != nil {
164 l.Error("failed to delete stars from PDS", "err", err)
165 return
166 }
167
168 if err := tx.Commit(); err != nil {
169 l.Error("failed to commit transaction", "err", err)
170 // DB op failed but record is created in PDS. Ingester will backfill the missed operation
171 }
172
173 s.notifier.DeleteStar(r.Context(), &models.Star{
174 Did: currentUser.Did,
175 SubjectType: subjectType,
176 Subject: subjectKey,
177 // Rkey
178 // Created
179 })
180
181 starCount, err := db.GetStarCount(s.db, subjectType, subjectKey)
182 if err != nil {
183 l.Error("failed to get star count", "subject", subjectKey, "err", err)
184 return
185 }
186
187 s.pages.StarBtnFragment(w, pages.StarBtnFragmentParams{
188 IsStarred: false,
189 SubjectAt: subjectUri,
190 StarCount: starCount,
191 RepoName: repoName,
192 })
193
194 return
195 }
196}