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