+194
-106
appview/issues/issues.go
+194
-106
appview/issues/issues.go
···
200
200
user := rp.oauth.GetUser(r)
201
201
f, err := rp.repoResolver.Resolve(r)
202
202
if err != nil {
203
-
log.Println("failed to get repo and knot", err)
203
+
l.Error("failed to get repo and knot", "err", err)
204
204
return
205
205
}
206
206
207
-
issueId := chi.URLParam(r, "issue")
208
-
issueIdInt, err := strconv.Atoi(issueId)
209
-
if err != nil {
210
-
http.Error(w, "bad issue id", http.StatusBadRequest)
211
-
log.Println("failed to parse issue id", err)
207
+
issue, ok := r.Context().Value("issue").(*db.Issue)
208
+
if !ok {
209
+
l.Error("failed to get issue")
210
+
rp.pages.Error404(w)
212
211
return
213
212
}
214
213
215
-
switch r.Method {
216
-
case http.MethodPost:
217
-
body := r.FormValue("body")
218
-
if body == "" {
219
-
rp.pages.Notice(w, "issue", "Body is required")
220
-
return
221
-
}
222
-
223
-
commentId := mathrand.IntN(1000000)
224
-
rkey := tid.TID()
214
+
body := r.FormValue("body")
215
+
if body == "" {
216
+
rp.pages.Notice(w, "issue", "Body is required")
217
+
return
218
+
}
225
219
226
-
err := db.NewIssueComment(rp.db, &db.Comment{
227
-
OwnerDid: user.Did,
228
-
RepoAt: f.RepoAt(),
229
-
Issue: issueIdInt,
230
-
CommentId: commentId,
231
-
Body: body,
232
-
Rkey: rkey,
233
-
})
220
+
replyToUri := r.FormValue("reply-to")
221
+
var replyTo *string
222
+
if replyToUri != "" {
223
+
uri, err := syntax.ParseATURI(replyToUri)
234
224
if err != nil {
235
-
log.Println("failed to create comment", err)
225
+
l.Error("failed to get parse replyTo", "err", err, "replyTo", replyToUri)
236
226
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
237
227
return
238
228
}
239
-
240
-
createdAt := time.Now().Format(time.RFC3339)
241
-
ownerDid := user.Did
242
-
issueAt, err := db.GetIssueAt(rp.db, f.RepoAt(), issueIdInt)
243
-
if err != nil {
244
-
log.Println("failed to get issue at", err)
245
-
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
246
-
return
247
-
}
248
-
249
-
atUri := f.RepoAt().String()
250
-
client, err := rp.oauth.AuthorizedClient(r)
251
-
if err != nil {
252
-
log.Println("failed to get authorized client", err)
253
-
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
254
-
return
255
-
}
256
-
_, err = client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
257
-
Collection: tangled.RepoIssueCommentNSID,
258
-
Repo: user.Did,
259
-
Rkey: rkey,
260
-
Record: &lexutil.LexiconTypeDecoder{
261
-
Val: &tangled.RepoIssueComment{
262
-
Repo: &atUri,
263
-
Issue: issueAt,
264
-
Owner: &ownerDid,
265
-
Body: body,
266
-
CreatedAt: createdAt,
267
-
},
268
-
},
269
-
})
270
-
if err != nil {
271
-
log.Println("failed to create comment", err)
229
+
if uri.Collection() != tangled.RepoIssueCommentNSID {
230
+
l.Error("invalid replyTo collection", "collection", uri.Collection())
272
231
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
273
232
return
274
233
}
234
+
u := uri.String()
235
+
replyTo = &u
236
+
}
275
237
276
-
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issueIdInt, commentId))
238
+
comment := db.IssueComment{
239
+
Did: user.Did,
240
+
Rkey: tid.TID(),
241
+
IssueAt: issue.AtUri().String(),
242
+
ReplyTo: replyTo,
243
+
Body: body,
244
+
Created: time.Now(),
245
+
}
246
+
if err = rp.validator.ValidateIssueComment(&comment); err != nil {
247
+
l.Error("failed to validate comment", "err", err)
248
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
277
249
return
278
250
}
279
-
}
251
+
record := comment.AsRecord()
280
252
281
-
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
282
-
user := rp.oauth.GetUser(r)
283
-
f, err := rp.repoResolver.Resolve(r)
253
+
client, err := rp.oauth.AuthorizedClient(r)
284
254
if err != nil {
285
-
log.Println("failed to get repo and knot", err)
255
+
l.Error("failed to get authorized client", "err", err)
256
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
286
257
return
287
258
}
288
259
289
-
issueId := chi.URLParam(r, "issue")
290
-
issueIdInt, err := strconv.Atoi(issueId)
260
+
// create a record first
261
+
resp, err := client.RepoPutRecord(r.Context(), &comatproto.RepoPutRecord_Input{
262
+
Collection: tangled.RepoIssueCommentNSID,
263
+
Repo: comment.Did,
264
+
Rkey: comment.Rkey,
265
+
Record: &lexutil.LexiconTypeDecoder{
266
+
Val: &record,
267
+
},
268
+
})
291
269
if err != nil {
292
-
http.Error(w, "bad issue id", http.StatusBadRequest)
293
-
log.Println("failed to parse issue id", err)
270
+
l.Error("failed to create comment", "err", err)
271
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
294
272
return
295
273
}
274
+
atUri := resp.Uri
275
+
defer func() {
276
+
if err := rollbackRecord(context.Background(), atUri, client); err != nil {
277
+
l.Error("rollback failed", "err", err)
278
+
}
279
+
}()
296
280
297
-
commentId := chi.URLParam(r, "comment_id")
298
-
commentIdInt, err := strconv.Atoi(commentId)
281
+
commentId, err := db.AddIssueComment(rp.db, comment)
299
282
if err != nil {
300
-
http.Error(w, "bad comment id", http.StatusBadRequest)
301
-
log.Println("failed to parse issue id", err)
283
+
l.Error("failed to create comment", "err", err)
284
+
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
302
285
return
303
286
}
304
287
305
-
issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt)
288
+
// reset atUri to make rollback a no-op
289
+
atUri = ""
290
+
rp.pages.HxLocation(w, fmt.Sprintf("/%s/issues/%d#comment-%d", f.OwnerSlashRepo(), issue.IssueId, commentId))
291
+
}
292
+
293
+
func (rp *Issues) IssueComment(w http.ResponseWriter, r *http.Request) {
294
+
l := rp.logger.With("handler", "IssueComment")
295
+
user := rp.oauth.GetUser(r)
296
+
f, err := rp.repoResolver.Resolve(r)
306
297
if err != nil {
307
-
log.Println("failed to get issue", err)
308
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
298
+
l.Error("failed to get repo and knot", "err", err)
309
299
return
310
300
}
311
301
312
-
comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt)
302
+
issue, ok := r.Context().Value("issue").(*db.Issue)
303
+
if !ok {
304
+
l.Error("failed to get issue")
305
+
rp.pages.Error404(w)
306
+
return
307
+
}
308
+
309
+
commentId := chi.URLParam(r, "commentId")
310
+
comments, err := db.GetIssueComments(
311
+
rp.db,
312
+
db.FilterEq("id", commentId),
313
+
)
313
314
if err != nil {
314
-
http.Error(w, "bad comment id", http.StatusBadRequest)
315
+
l.Error("failed to fetch comment", "id", commentId)
316
+
http.Error(w, "failed to fetch comment id", http.StatusBadRequest)
317
+
return
318
+
}
319
+
if len(comments) != 1 {
320
+
l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments))
321
+
http.Error(w, "invalid comment id", http.StatusBadRequest)
315
322
return
316
323
}
324
+
comment := comments[0]
317
325
318
-
rp.pages.SingleIssueCommentFragment(w, pages.SingleIssueCommentParams{
326
+
rp.pages.IssueCommentBodyFragment(w, pages.IssueCommentBodyParams{
319
327
LoggedInUser: user,
320
328
RepoInfo: f.RepoInfo(user),
321
329
Issue: issue,
322
-
Comment: comment,
330
+
Comment: &comment,
323
331
})
324
332
}
325
333
326
334
func (rp *Issues) EditIssueComment(w http.ResponseWriter, r *http.Request) {
335
+
l := rp.logger.With("handler", "EditIssueComment")
327
336
user := rp.oauth.GetUser(r)
328
337
f, err := rp.repoResolver.Resolve(r)
329
338
if err != nil {
330
-
log.Println("failed to get repo and knot", err)
339
+
l.Error("failed to get repo and knot", "err", err)
331
340
return
332
341
}
333
342
334
-
issueId := chi.URLParam(r, "issue")
335
-
issueIdInt, err := strconv.Atoi(issueId)
336
-
if err != nil {
337
-
http.Error(w, "bad issue id", http.StatusBadRequest)
338
-
log.Println("failed to parse issue id", err)
343
+
issue, ok := r.Context().Value("issue").(*db.Issue)
344
+
if !ok {
345
+
l.Error("failed to get issue")
346
+
rp.pages.Error404(w)
339
347
return
340
348
}
341
349
342
-
commentId := chi.URLParam(r, "comment_id")
343
-
commentIdInt, err := strconv.Atoi(commentId)
350
+
commentId := chi.URLParam(r, "commentId")
351
+
comments, err := db.GetIssueComments(
352
+
rp.db,
353
+
db.FilterEq("id", commentId),
354
+
)
344
355
if err != nil {
345
-
http.Error(w, "bad comment id", http.StatusBadRequest)
346
-
log.Println("failed to parse issue id", err)
356
+
l.Error("failed to fetch comment", "id", commentId)
357
+
http.Error(w, "failed to fetch comment id", http.StatusBadRequest)
347
358
return
348
359
}
349
-
350
-
issue, err := db.GetIssue(rp.db, f.RepoAt(), issueIdInt)
351
-
if err != nil {
352
-
log.Println("failed to get issue", err)
353
-
rp.pages.Notice(w, "issues", "Failed to load issue. Try again later.")
354
-
return
355
-
}
356
-
357
-
comment, err := db.GetComment(rp.db, f.RepoAt(), issueIdInt, commentIdInt)
358
-
if err != nil {
359
-
http.Error(w, "bad comment id", http.StatusBadRequest)
360
+
if len(comments) != 1 {
361
+
l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments))
362
+
http.Error(w, "invalid comment id", http.StatusBadRequest)
360
363
return
361
364
}
365
+
comment := comments[0]
362
366
363
-
if comment.OwnerDid != user.Did {
367
+
if comment.Did != user.Did {
368
+
l.Error("unauthorized comment edit", "expectedDid", comment.Did, "gotDid", user.Did)
364
369
http.Error(w, "you are not the author of this comment", http.StatusUnauthorized)
365
370
return
366
371
}
···
382
387
rp.pages.Notice(w, "issue-comment", "Failed to create comment.")
383
388
return
384
389
}
385
-
rkey := comment.Rkey
390
+
391
+
now := time.Now()
392
+
newComment := comment
393
+
newComment.Body = newBody
394
+
newComment.Edited = &now
395
+
record := newComment.AsRecord()
386
396
387
-
// optimistic update
388
-
edited := time.Now()
389
-
err = db.EditComment(rp.db, comment.RepoAt, comment.Issue, comment.CommentId, newBody)
397
+
_, err = db.AddIssueComment(rp.db, newComment)
390
398
if err != nil {
391
399
log.Println("failed to perferom update-description query", err)
392
400
rp.pages.Notice(w, "repo-notice", "Failed to update description, try again later.")
···
445
453
446
454
}
447
455
456
+
func (rp *Issues) ReplyIssueCommentPlaceholder(w http.ResponseWriter, r *http.Request) {
457
+
l := rp.logger.With("handler", "ReplyIssueCommentPlaceholder")
458
+
user := rp.oauth.GetUser(r)
459
+
f, err := rp.repoResolver.Resolve(r)
460
+
if err != nil {
461
+
l.Error("failed to get repo and knot", "err", err)
462
+
return
463
+
}
464
+
465
+
issue, ok := r.Context().Value("issue").(*db.Issue)
466
+
if !ok {
467
+
l.Error("failed to get issue")
468
+
rp.pages.Error404(w)
469
+
return
470
+
}
471
+
472
+
commentId := chi.URLParam(r, "commentId")
473
+
comments, err := db.GetIssueComments(
474
+
rp.db,
475
+
db.FilterEq("id", commentId),
476
+
)
477
+
if err != nil {
478
+
l.Error("failed to fetch comment", "id", commentId)
479
+
http.Error(w, "failed to fetch comment id", http.StatusBadRequest)
480
+
return
481
+
}
482
+
if len(comments) != 1 {
483
+
l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments))
484
+
http.Error(w, "invalid comment id", http.StatusBadRequest)
485
+
return
486
+
}
487
+
comment := comments[0]
488
+
489
+
rp.pages.ReplyIssueCommentPlaceholderFragment(w, pages.ReplyIssueCommentPlaceholderParams{
490
+
LoggedInUser: user,
491
+
RepoInfo: f.RepoInfo(user),
492
+
Issue: issue,
493
+
Comment: &comment,
494
+
})
495
+
}
496
+
497
+
func (rp *Issues) ReplyIssueComment(w http.ResponseWriter, r *http.Request) {
498
+
l := rp.logger.With("handler", "ReplyIssueComment")
499
+
user := rp.oauth.GetUser(r)
500
+
f, err := rp.repoResolver.Resolve(r)
501
+
if err != nil {
502
+
l.Error("failed to get repo and knot", "err", err)
503
+
return
504
+
}
505
+
506
+
issue, ok := r.Context().Value("issue").(*db.Issue)
507
+
if !ok {
508
+
l.Error("failed to get issue")
509
+
rp.pages.Error404(w)
510
+
return
511
+
}
512
+
513
+
commentId := chi.URLParam(r, "commentId")
514
+
comments, err := db.GetIssueComments(
515
+
rp.db,
516
+
db.FilterEq("id", commentId),
517
+
)
518
+
if err != nil {
519
+
l.Error("failed to fetch comment", "id", commentId)
520
+
http.Error(w, "failed to fetch comment id", http.StatusBadRequest)
521
+
return
522
+
}
523
+
if len(comments) != 1 {
524
+
l.Error("incorrect number of comments returned", "id", commentId, "len(comments)", len(comments))
525
+
http.Error(w, "invalid comment id", http.StatusBadRequest)
526
+
return
527
+
}
528
+
comment := comments[0]
529
+
530
+
rp.pages.ReplyIssueCommentFragment(w, pages.ReplyIssueCommentParams{
531
+
LoggedInUser: user,
532
+
RepoInfo: f.RepoInfo(user),
533
+
Issue: issue,
534
+
Comment: &comment,
535
+
})
448
536
}
449
537
450
538
func (rp *Issues) DeleteIssueComment(w http.ResponseWriter, r *http.Request) {
539
+
l := rp.logger.With("handler", "DeleteIssueComment")
451
540
user := rp.oauth.GetUser(r)
452
541
f, err := rp.repoResolver.Resolve(r)
453
542
if err != nil {
454
-
log.Println("failed to get repo and knot", err)
455
543
return
456
544
}
457
545