+47
-87
appview/issues/issues.go
+47
-87
appview/issues/issues.go
···
1
1
package issues
2
2
3
3
import (
4
+
"context"
5
+
"database/sql"
6
+
"errors"
4
7
"fmt"
5
8
"log"
6
-
mathrand "math/rand/v2"
9
+
"log/slog"
7
10
"net/http"
8
11
"slices"
9
-
"strconv"
10
12
"strings"
11
13
"time"
12
14
13
15
comatproto "github.com/bluesky-social/indigo/api/atproto"
14
-
"github.com/bluesky-social/indigo/atproto/data"
16
+
"github.com/bluesky-social/indigo/atproto/syntax"
15
17
lexutil "github.com/bluesky-social/indigo/lex/util"
16
18
"github.com/go-chi/chi/v5"
17
19
···
24
26
"tangled.sh/tangled.sh/core/appview/pages/markup"
25
27
"tangled.sh/tangled.sh/core/appview/pagination"
26
28
"tangled.sh/tangled.sh/core/appview/reporesolver"
29
+
"tangled.sh/tangled.sh/core/appview/validator"
30
+
"tangled.sh/tangled.sh/core/appview/xrpcclient"
27
31
"tangled.sh/tangled.sh/core/idresolver"
32
+
tlog "tangled.sh/tangled.sh/core/log"
28
33
"tangled.sh/tangled.sh/core/tid"
29
34
)
30
35
···
36
41
db *db.DB
37
42
config *config.Config
38
43
notifier notify.Notifier
44
+
logger *slog.Logger
45
+
validator *validator.Validator
39
46
}
40
47
41
48
func New(
···
55
62
db: db,
56
63
config: config,
57
64
notifier: notifier,
65
+
logger: tlog.New("issues"),
66
+
validator: validator,
58
67
}
59
68
}
60
69
61
70
func (rp *Issues) RepoSingleIssue(w http.ResponseWriter, r *http.Request) {
71
+
l := rp.logger.With("handler", "RepoSingleIssue")
62
72
user := rp.oauth.GetUser(r)
63
73
f, err := rp.repoResolver.Resolve(r)
64
74
if err != nil {
···
66
76
return
67
77
}
68
78
69
-
issueId := chi.URLParam(r, "issue")
70
-
issueIdInt, err := strconv.Atoi(issueId)
71
-
if err != nil {
72
-
http.Error(w, "bad issue id", http.StatusBadRequest)
73
-
log.Println("failed to parse issue id", err)
74
-
return
75
-
}
76
-
77
-
issue, comments, err := db.GetIssueWithComments(rp.db, f.RepoAt(), issueIdInt)
78
-
if err != nil {
79
-
log.Println("failed to get issue and comments", err)
80
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
79
+
issue, ok := r.Context().Value("issue").(*db.Issue)
80
+
if !ok {
81
+
l.Error("failed to get issue")
82
+
rp.pages.Error404(w)
81
83
return
82
84
}
83
85
84
86
reactionCountMap, err := db.GetReactionCountMap(rp.db, issue.AtUri())
85
87
if err != nil {
86
-
log.Println("failed to get issue reactions")
87
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
88
+
l.Error("failed to get issue reactions", "err", err)
88
89
}
89
90
90
91
userReactions := map[db.ReactionKind]bool{}
···
92
93
userReactions = db.GetReactionStatusMap(rp.db, user.Did, issue.AtUri())
93
94
}
94
95
95
-
issueOwnerIdent, err := rp.idResolver.ResolveIdent(r.Context(), issue.OwnerDid)
96
-
if err != nil {
97
-
log.Println("failed to resolve issue owner", err)
98
-
}
99
-
100
96
rp.pages.RepoSingleIssue(w, pages.RepoSingleIssueParams{
101
-
LoggedInUser: user,
102
-
RepoInfo: f.RepoInfo(user),
103
-
Issue: issue,
104
-
Comments: comments,
105
-
106
-
IssueOwnerHandle: issueOwnerIdent.Handle.String(),
107
-
97
+
LoggedInUser: user,
98
+
RepoInfo: f.RepoInfo(user),
99
+
Issue: issue,
100
+
CommentList: issue.CommentList(),
108
101
OrderedReactionKinds: db.OrderedReactionKinds,
109
102
Reactions: reactionCountMap,
110
103
UserReacted: userReactions,
···
113
106
}
114
107
115
108
func (rp *Issues) CloseIssue(w http.ResponseWriter, r *http.Request) {
109
+
l := rp.logger.With("handler", "CloseIssue")
116
110
user := rp.oauth.GetUser(r)
117
111
f, err := rp.repoResolver.Resolve(r)
118
112
if err != nil {
119
-
log.Println("failed to get repo and knot", err)
120
-
return
121
-
}
122
-
123
-
issueId := chi.URLParam(r, "issue")
124
-
issueIdInt, err := strconv.Atoi(issueId)
125
-
if err != nil {
126
-
http.Error(w, "bad issue id", http.StatusBadRequest)
127
-
log.Println("failed to parse issue id", err)
113
+
l.Error("failed to get repo and knot", "err", err)
128
114
return
129
115
}
130
116
131
-
issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt)
132
-
if err != nil {
133
-
log.Println("failed to get issue", err)
134
-
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
117
+
issue, ok := r.Context().Value("issue").(*db.Issue)
118
+
if !ok {
119
+
l.Error("failed to get issue")
120
+
rp.pages.Error404(w)
135
121
return
136
122
}
137
123
···
142
128
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
143
129
return user.Did == collab.Did
144
130
})
145
-
isIssueOwner := user.Did == issue.OwnerDid
131
+
isIssueOwner := user.Did == issue.Did
146
132
147
133
// TODO: make this more granular
148
134
if isIssueOwner || isCollaborator {
149
-
150
-
closed := tangled.RepoIssueStateClosed
151
-
152
-
client, err := rp.oauth.AuthorizedClient(r)
153
-
if err != nil {
154
-
log.Println("failed to get authorized client", err)
155
-
return
156
-
}
157
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
158
-
Collection: tangled.RepoIssueStateNSID,
159
-
Repo: user.Did,
160
-
Rkey: tid.TID(),
161
-
Record: &lexutil.LexiconTypeDecoder{
162
-
Val: &tangled.RepoIssueState{
163
-
Issue: issue.AtUri().String(),
164
-
State: closed,
165
-
},
166
-
},
167
-
})
168
-
169
-
if err != nil {
170
-
log.Println("failed to update issue state", err)
171
-
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
172
-
return
173
-
}
174
-
175
-
err = db.CloseIssue(rp.db, f.RepoAt(), issueIdInt)
135
+
err = db.CloseIssues(
136
+
rp.db,
137
+
db.FilterEq("id", issue.Id),
138
+
)
176
139
if err != nil {
177
140
log.Println("failed to close issue", err)
178
141
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
179
142
return
180
143
}
181
144
182
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
145
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
183
146
return
184
147
} else {
185
148
log.Println("user is not permitted to close issue")
···
189
152
}
190
153
191
154
func (rp *Issues) ReopenIssue(w http.ResponseWriter, r *http.Request) {
155
+
l := rp.logger.With("handler", "ReopenIssue")
192
156
user := rp.oauth.GetUser(r)
193
157
f, err := rp.repoResolver.Resolve(r)
194
158
if err != nil {
···
196
160
return
197
161
}
198
162
199
-
issueId := chi.URLParam(r, "issue")
200
-
issueIdInt, err := strconv.Atoi(issueId)
201
-
if err != nil {
202
-
http.Error(w, "bad issue id", http.StatusBadRequest)
203
-
log.Println("failed to parse issue id", err)
204
-
return
205
-
}
206
-
207
-
issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt)
208
-
if err != nil {
209
-
log.Println("failed to get issue", err)
210
-
rp.pages.Notice(w, "issue-action", "Failed to close issue. Try again later.")
163
+
issue, ok := r.Context().Value("issue").(*db.Issue)
164
+
if !ok {
165
+
l.Error("failed to get issue")
166
+
rp.pages.Error404(w)
211
167
return
212
168
}
213
169
···
218
174
isCollaborator := slices.ContainsFunc(collaborators, func(collab pages.Collaborator) bool {
219
175
return user.Did == collab.Did
220
176
})
221
-
isIssueOwner := user.Did == issue.OwnerDid
177
+
isIssueOwner := user.Did == issue.Did
222
178
223
179
if isCollaborator || isIssueOwner {
224
-
err := db.ReopenIssue(rp.db, f.RepoAt(), issueIdInt)
180
+
err := db.ReopenIssues(
181
+
rp.db,
182
+
db.FilterEq("id", issue.Id),
183
+
)
225
184
if err != nil {
226
185
log.Println("failed to reopen issue", err)
227
186
rp.pages.Notice(w, "issue-action", "Failed to reopen issue. Try again later.")
228
187
return
229
188
}
230
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issueIdInt))
189
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d", f.OwnerSlashRepo(), issue.IssueId))
231
190
return
232
191
} else {
233
192
log.Println("user is not the owner of the repo")
···
237
196
}
238
197
239
198
func (rp *Issues) NewIssueComment(w http.ResponseWriter, r *http.Request) {
199
+
l := rp.logger.With("handler", "NewIssueComment")
240
200
user := rp.oauth.GetUser(r)
241
201
f, err := rp.repoResolver.Resolve(r)
242
202
if err != nil {
···
411
371
LoggedInUser: user,
412
372
RepoInfo: f.RepoInfo(user),
413
373
Issue: issue,
414
-
Comment: comment,
374
+
Comment: &comment,
415
375
})
416
376
case http.MethodPost:
417
377
// extract form value