Monorepo for Tangled tangled.org

appview/issues: new fragments for replies

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li c643f6d5 a0079caa

verified
Changed files
+194 -106
appview
issues
+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