github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/comments/cmds.go (about) 1 // Copyright (c) 2020-2022 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package comments 6 7 import ( 8 "bytes" 9 "encoding/base64" 10 "encoding/hex" 11 "encoding/json" 12 "fmt" 13 "sort" 14 "strconv" 15 "time" 16 17 backend "github.com/decred/politeia/politeiad/backendv2" 18 "github.com/decred/politeia/politeiad/backendv2/tstorebe/store" 19 "github.com/decred/politeia/politeiad/plugins/comments" 20 "github.com/decred/politeia/util" 21 "github.com/pkg/errors" 22 ) 23 24 const ( 25 pluginID = comments.PluginID 26 27 // Blob entry data descriptors 28 dataDescriptorCommentAdd = pluginID + "-add-v1" 29 dataDescriptorCommentDel = pluginID + "-del-v1" 30 dataDescriptorCommentVote = pluginID + "-vote-v1" 31 ) 32 33 // commentAddSave saves a CommentAdd to the backend. 34 func (p *commentsPlugin) commentAddSave(token []byte, ca comments.CommentAdd) ([]byte, error) { 35 be, err := convertBlobEntryFromCommentAdd(ca) 36 if err != nil { 37 return nil, err 38 } 39 d, err := hex.DecodeString(be.Digest) 40 if err != nil { 41 return nil, err 42 } 43 err = p.tstore.BlobSave(token, *be) 44 if err != nil { 45 return nil, err 46 } 47 return d, nil 48 } 49 50 // commentAdds returns a commentAdd for each of the provided digests. A digest 51 // refers to the blob entry digest, which is used as the key when retrieving 52 // the blob entry from tstore. 53 // 54 // This function will return the comment adds in the same order that they are 55 // requested in, i.e. the order of the digests slice. An error is returned 56 // if a blob entry is not found for one or more of the provided digests. 57 func (p *commentsPlugin) commentAdds(token []byte, digests [][]byte) ([]comments.CommentAdd, error) { 58 // Retrieve blobs 59 blobs, err := p.tstore.Blobs(token, digests) 60 if err != nil { 61 return nil, err 62 } 63 if len(blobs) != len(digests) { 64 notFound := make([]string, 0, len(blobs)) 65 for _, v := range digests { 66 m := hex.EncodeToString(v) 67 _, ok := blobs[m] 68 if !ok { 69 notFound = append(notFound, m) 70 } 71 } 72 return nil, fmt.Errorf("blobs not found: %v", notFound) 73 } 74 75 // Decode blobs 76 adds := make([]comments.CommentAdd, 0, len(blobs)) 77 for _, digest := range digests { 78 d := hex.EncodeToString(digest) 79 c, err := convertCommentAddFromBlobEntry(blobs[d]) 80 if err != nil { 81 return nil, err 82 } 83 adds = append(adds, *c) 84 } 85 86 return adds, nil 87 } 88 89 // commentDelSave saves a CommentDel to the backend. 90 func (p *commentsPlugin) commentDelSave(token []byte, cd comments.CommentDel) ([]byte, error) { 91 be, err := convertBlobEntryFromCommentDel(cd) 92 if err != nil { 93 return nil, err 94 } 95 d, err := hex.DecodeString(be.Digest) 96 if err != nil { 97 return nil, err 98 } 99 err = p.tstore.BlobSave(token, *be) 100 if err != nil { 101 return nil, err 102 } 103 return d, nil 104 } 105 106 // commentDels returns a commentDel for each of the provided digests. A digest 107 // refers to the blob entry digest, which is used as the key when retrieving 108 // the blob entry from tstore. 109 // 110 // This function will return the comment dels in the same order that they are 111 // requested in, i.e. the order of the digests slice. An error is returned 112 // if a blob entry is not found for one or more of the provided digests. 113 func (p *commentsPlugin) commentDels(token []byte, digests [][]byte) ([]comments.CommentDel, error) { 114 // Retrieve blobs 115 blobs, err := p.tstore.Blobs(token, digests) 116 if err != nil { 117 return nil, err 118 } 119 if len(blobs) != len(digests) { 120 notFound := make([]string, 0, len(blobs)) 121 for _, v := range digests { 122 m := hex.EncodeToString(v) 123 _, ok := blobs[m] 124 if !ok { 125 notFound = append(notFound, m) 126 } 127 } 128 return nil, fmt.Errorf("blobs not found: %v", notFound) 129 } 130 131 // Decode blobs 132 dels := make([]comments.CommentDel, 0, len(blobs)) 133 for _, digest := range digests { 134 d := hex.EncodeToString(digest) 135 del, err := convertCommentDelFromBlobEntry(blobs[d]) 136 if err != nil { 137 return nil, err 138 } 139 dels = append(dels, *del) 140 } 141 142 return dels, nil 143 } 144 145 // commentVoteSave saves a CommentVote to the backend. 146 func (p *commentsPlugin) commentVoteSave(token []byte, cv comments.CommentVote) ([]byte, error) { 147 be, err := convertBlobEntryFromCommentVote(cv) 148 if err != nil { 149 return nil, err 150 } 151 d, err := hex.DecodeString(be.Digest) 152 if err != nil { 153 return nil, err 154 } 155 err = p.tstore.BlobSave(token, *be) 156 if err != nil { 157 return nil, err 158 } 159 return d, nil 160 } 161 162 // commentVotes returns a commentVote for each of the provided digests. A 163 // digest refers to the blob entry digest, which is used as the key when 164 // retrieving the blob entry from tstore. 165 // 166 // This function will return the comment votes in the same order that they are 167 // requested in, i.e. the order of the digests slice. An error is returned 168 // if a blob entry is not found for one or more of the provided digests. 169 func (p *commentsPlugin) commentVotes(token []byte, digests [][]byte) ([]comments.CommentVote, error) { 170 // Retrieve blobs 171 blobs, err := p.tstore.Blobs(token, digests) 172 if err != nil { 173 return nil, err 174 } 175 if len(blobs) != len(digests) { 176 notFound := make([]string, 0, len(blobs)) 177 for _, v := range digests { 178 m := hex.EncodeToString(v) 179 _, ok := blobs[m] 180 if !ok { 181 notFound = append(notFound, m) 182 } 183 } 184 return nil, fmt.Errorf("blobs not found: %v", notFound) 185 } 186 187 // Decode blobs 188 votes := make([]comments.CommentVote, 0, len(blobs)) 189 for _, digest := range digests { 190 d := hex.EncodeToString(digest) 191 c, err := convertCommentVoteFromBlobEntry(blobs[d]) 192 if err != nil { 193 return nil, err 194 } 195 votes = append(votes, *c) 196 } 197 198 return votes, nil 199 } 200 201 // comments returns the most recent version of the specified comments. Deleted 202 // comments are returned with limited data. If a comment is not found for a 203 // provided comment IDs, the comment ID is excluded from the returned map. An 204 // error will not be returned. It is the responsibility of the caller to ensure 205 // a comment is returned for each of the provided comment IDs. 206 func (p *commentsPlugin) comments(token []byte, ridx recordIndex, commentIDs []uint32) (map[uint32]comments.Comment, error) { 207 // Aggregate the digests for all records that need to be looked up. 208 // If a comment has been deleted then the only record that will 209 // still exist is the comment del record. If the comment has not 210 // been deleted then the comment add record will need to be 211 // retrieved for the latest version of the comment. 212 var ( 213 digestAdds = make([][]byte, 0, len(commentIDs)) 214 digestDels = make([][]byte, 0, len(commentIDs)) 215 ) 216 for _, v := range commentIDs { 217 cidx, ok := ridx.Comments[v] 218 if !ok { 219 // Comment does not exist 220 continue 221 } 222 223 // Comment del record 224 if cidx.Del != nil { 225 digestDels = append(digestDels, cidx.Del) 226 continue 227 } 228 229 // Comment add record 230 version := commentVersionLatest(cidx) 231 digestAdds = append(digestAdds, cidx.Adds[version]) 232 } 233 234 // Get comment add records 235 adds, err := p.commentAdds(token, digestAdds) 236 if err != nil { 237 return nil, errors.Errorf("commentAdds: %v", err) 238 } 239 if len(adds) != len(digestAdds) { 240 return nil, errors.Errorf("wrong comment adds count; got %v, want %v", 241 len(adds), len(digestAdds)) 242 } 243 244 // Get comment del records 245 dels, err := p.commentDels(token, digestDels) 246 if err != nil { 247 return nil, errors.Errorf("commentDels: %v", err) 248 } 249 if len(dels) != len(digestDels) { 250 return nil, errors.Errorf("wrong comment dels count; got %v, want %v", 251 len(dels), len(digestDels)) 252 } 253 254 // Prepare comments 255 cs := make(map[uint32]comments.Comment, len(commentIDs)) 256 for _, v := range adds { 257 c := convertCommentFromCommentAdd(v) 258 cidx, ok := ridx.Comments[c.CommentID] 259 if !ok { 260 return nil, errors.Errorf("comment index not found %v", c.CommentID) 261 } 262 c.Downvotes, c.Upvotes = voteScore(cidx) 263 // Populate creation timestamp 264 c.CreatedAt, err = p.commentCreationTimestamp(c, cidx) 265 if err != nil { 266 return nil, err 267 } 268 269 cs[v.CommentID] = c 270 } 271 for _, v := range dels { 272 c := convertCommentFromCommentDel(v) 273 cs[v.CommentID] = c 274 } 275 276 return cs, nil 277 } 278 279 // commentCreationTimestamp accepts the latest version of a comment with the 280 // comment index , and it returns the comment's creation timestamp. 281 func (p *commentsPlugin) commentCreationTimestamp(c comments.Comment, cidx commentIndex) (int64, error) { 282 // If comment was not edited, then the comment creation timestamp is 283 // equal to the first version's timestamp. 284 if c.Version == 1 { 285 return c.Timestamp, nil 286 } 287 288 // If comment was edited, we need to get the first version of the 289 // comment in order to determine the creation timestamp. 290 b, err := tokenDecode(c.Token) 291 if err != nil { 292 return 0, err 293 } 294 cf, err := p.commentFirstVersion(b, c.CommentID, cidx) 295 if err != nil { 296 return 0, err 297 } 298 299 return cf.Timestamp, nil 300 } 301 302 // comment returns the latest version of a comment. 303 func (p *commentsPlugin) comment(token []byte, ridx recordIndex, commentID uint32) (*comments.Comment, error) { 304 cs, err := p.comments(token, ridx, []uint32{commentID}) 305 if err != nil { 306 return nil, fmt.Errorf("comments: %v", err) 307 } 308 c, ok := cs[commentID] 309 if !ok { 310 return nil, fmt.Errorf("comment not found") 311 } 312 return &c, nil 313 } 314 315 // timestamp returns the timestamp for a blob entry digest. 316 func (p *commentsPlugin) timestamp(token []byte, digest []byte) (*comments.Timestamp, error) { 317 // Get timestamp 318 t, err := p.tstore.Timestamp(token, digest) 319 if err != nil { 320 return nil, err 321 } 322 323 // Convert response 324 proofs := make([]comments.Proof, 0, len(t.Proofs)) 325 for _, v := range t.Proofs { 326 proofs = append(proofs, comments.Proof{ 327 Type: v.Type, 328 Digest: v.Digest, 329 MerkleRoot: v.MerkleRoot, 330 MerklePath: v.MerklePath, 331 ExtraData: v.ExtraData, 332 }) 333 } 334 return &comments.Timestamp{ 335 Data: t.Data, 336 Digest: t.Digest, 337 TxID: t.TxID, 338 MerkleRoot: t.MerkleRoot, 339 Proofs: proofs, 340 }, nil 341 } 342 343 // commentTimestamps returns the CommentTimestamp for each of the provided 344 // comment IDs. 345 func (p *commentsPlugin) commentTimestamps(token []byte, commentIDs []uint32, includeVotes bool) (*comments.TimestampsReply, error) { 346 // Verify there is work to do 347 if len(commentIDs) == 0 { 348 return &comments.TimestampsReply{ 349 Comments: map[uint32]comments.CommentTimestamp{}, 350 }, nil 351 } 352 353 // Look for final timestamps in the key-value store. Caching final timestamps 354 // is necessary to improve the performance which is proportional to the tree 355 // size. 356 cts, err := p.cachedTimestamps(token, commentIDs) 357 if err != nil { 358 return nil, err 359 } 360 361 // Get record state 362 state, err := p.tstore.RecordState(token) 363 if err != nil { 364 return nil, err 365 } 366 367 // Get record index 368 ridx, err := p.recordIndex(token, state) 369 if err != nil { 370 return nil, err 371 } 372 373 // Get timestamps for each comment ID 374 r := make(map[uint32]comments.CommentTimestamp, len(commentIDs)) 375 for _, cid := range commentIDs { 376 cidx, ok := ridx.Comments[cid] 377 if !ok { 378 // Comment ID does not exist. Skip it. 379 continue 380 } 381 382 // Get cached comment timestamp associated with current comment ID if one 383 // exists, nil otherwise. 384 ct := cts[cid] 385 386 // Get comment add timestamps 387 adds := make([]comments.Timestamp, 0, len(cidx.Adds)) 388 for _, v := range cidx.Adds { 389 // Check if the comment add digest timestamp is final and already exists 390 // in cache. 391 var t *comments.Timestamp 392 if ct != nil { 393 for _, at := range ct.Adds { 394 if at.Digest == hex.EncodeToString(v) { 395 t = &at 396 } 397 } 398 } 399 if t != nil { 400 // Cached timestamp found, collect it and continue to next comment 401 // add digest. 402 adds = append(adds, *t) 403 continue 404 } 405 406 // Comment add digest was not found in cache, get timestamp 407 ts, err := p.timestamp(token, v) 408 if err != nil { 409 return nil, err 410 } 411 adds = append(adds, *ts) 412 } 413 414 // Get comment del timestamps. This will only exist if the 415 // comment has been deleted. 416 var del *comments.Timestamp 417 if cidx.Del != nil { 418 // Check if the comment del digest timestamp is final and already exists 419 // in cache. 420 var t *comments.Timestamp 421 if ct != nil { 422 if ct.Del != nil && ct.Del.Digest == hex.EncodeToString(cidx.Del) { 423 t = ct.Del 424 } 425 } 426 427 switch { 428 case t != nil: 429 // Comment del timestamp found in cache, collect it 430 del = t 431 432 case t == nil: 433 // Comment del timestamp was not found in cache, get timestamp 434 ts, err := p.timestamp(token, cidx.Del) 435 if err != nil { 436 return nil, err 437 } 438 del = ts 439 } 440 } 441 442 // Get comment vote timestamps 443 var votes []comments.Timestamp 444 if includeVotes { 445 votes = make([]comments.Timestamp, 0, len(cidx.Votes)) 446 for _, voteIdxs := range cidx.Votes { 447 for _, v := range voteIdxs { 448 // Check if the comment vote digest timestamp is final and already 449 // exists in cache. 450 var t *comments.Timestamp 451 if ct != nil { 452 for _, vt := range ct.Votes { 453 if vt.Digest == hex.EncodeToString(v.Digest) { 454 t = &vt 455 } 456 } 457 } 458 if t != nil { 459 // Cached timestamp found, collect it and continue to next comment 460 // vote digest. 461 votes = append(votes, *t) 462 continue 463 } 464 465 // Comment vote digest was not found in cache, get timestamp 466 ts, err := p.timestamp(token, v.Digest) 467 if err != nil { 468 return nil, err 469 } 470 votes = append(votes, *ts) 471 } 472 } 473 } 474 475 // Save timestamp 476 r[cid] = comments.CommentTimestamp{ 477 Adds: adds, 478 Del: del, 479 Votes: votes, 480 } 481 } 482 483 // Cache final timestamps 484 err = p.cacheFinalTimestamps(token, r) 485 if err != nil { 486 return nil, err 487 } 488 489 return &comments.TimestampsReply{ 490 Comments: r, 491 }, nil 492 } 493 494 // voteScore returns the total number of downvotes and upvotes, respectively, 495 // for a comment. 496 func voteScore(cidx commentIndex) (uint64, uint64) { 497 // Find the vote score by replaying all existing votes from all 498 // users. The net effect of a new vote on a comment score depends 499 // on the previous vote from that uuid. Example, a user upvotes a 500 // comment that they have already upvoted, the resulting vote score 501 // is 0 due to the second upvote removing the original upvote. 502 var upvotes uint64 503 var downvotes uint64 504 for _, votes := range cidx.Votes { 505 // Calculate the vote score that this user is contributing. This 506 // can only ever be -1, 0, or 1. 507 var score int64 508 for _, v := range votes { 509 vote := int64(v.Vote) 510 switch { 511 case score == 0: 512 // No previous vote. New vote becomes the score. 513 score = vote 514 515 case score == vote: 516 // New vote is the same as the previous vote. The vote gets 517 // removed from the score, making the score 0. 518 score = 0 519 520 case score != vote: 521 // New vote is different than the previous vote. New vote 522 // becomes the score. 523 score = vote 524 } 525 } 526 527 // Add the net result of all votes from this user to the totals. 528 switch score { 529 case 0: 530 // Nothing to do 531 case -1: 532 downvotes++ 533 case 1: 534 upvotes++ 535 default: 536 // Should not be possible 537 panic(fmt.Errorf("unexpected vote score %v", score)) 538 } 539 } 540 541 return downvotes, upvotes 542 } 543 544 // cmdNew creates a new comment. 545 func (p *commentsPlugin) cmdNew(token []byte, payload string) (string, error) { 546 // Decode payload 547 var n comments.New 548 err := json.Unmarshal([]byte(payload), &n) 549 if err != nil { 550 return "", err 551 } 552 553 // Verify token 554 err = tokenVerify(token, n.Token) 555 if err != nil { 556 return "", err 557 } 558 559 // Ensure no extra data provided if not allowed 560 err = p.verifyExtraData(n.ExtraData, n.ExtraDataHint) 561 if err != nil { 562 return "", err 563 } 564 565 // Verify signature 566 msg := strconv.FormatUint(uint64(n.State), 10) + n.Token + 567 strconv.FormatUint(uint64(n.ParentID), 10) + n.Comment + 568 n.ExtraData + n.ExtraDataHint 569 err = util.VerifySignature(n.Signature, n.PublicKey, msg) 570 if err != nil { 571 return "", convertSignatureError(err) 572 } 573 574 // Verify comment 575 if len(n.Comment) == 0 { 576 return "", backend.PluginError{ 577 PluginID: comments.PluginID, 578 ErrorCode: uint32(comments.ErrorCodeEmptyComment), 579 } 580 } 581 if len(n.Comment) > int(p.commentLengthMax) { 582 return "", backend.PluginError{ 583 PluginID: comments.PluginID, 584 ErrorCode: uint32(comments.ErrorCodeMaxLengthExceeded), 585 ErrorContext: fmt.Sprintf("max length is %v characters", 586 p.commentLengthMax), 587 } 588 } 589 590 // Verify record state 591 state, err := p.tstore.RecordState(token) 592 if err != nil { 593 return "", err 594 } 595 if uint32(n.State) != uint32(state) { 596 return "", backend.PluginError{ 597 PluginID: comments.PluginID, 598 ErrorCode: uint32(comments.ErrorCodeRecordStateInvalid), 599 ErrorContext: fmt.Sprintf("got %v, want %v", n.State, state), 600 } 601 } 602 603 // Get record index 604 ridx, err := p.recordIndex(token, state) 605 if err != nil { 606 return "", err 607 } 608 609 // Verify parent comment exists if set. A parent ID of 0 means that 610 // this is a base level comment, not a reply to another comment. 611 if n.ParentID > 0 && !commentExists(*ridx, n.ParentID) { 612 return "", backend.PluginError{ 613 PluginID: comments.PluginID, 614 ErrorCode: uint32(comments.ErrorCodeParentIDInvalid), 615 ErrorContext: "parent ID comment not found", 616 } 617 } 618 619 // Setup comment 620 receipt := p.identity.SignMessage([]byte(n.Signature)) 621 ca := comments.CommentAdd{ 622 UserID: n.UserID, 623 State: n.State, 624 Token: n.Token, 625 ParentID: n.ParentID, 626 Comment: n.Comment, 627 PublicKey: n.PublicKey, 628 Signature: n.Signature, 629 CommentID: commentIDLatest(*ridx) + 1, 630 Version: 1, 631 Timestamp: time.Now().Unix(), 632 Receipt: hex.EncodeToString(receipt[:]), 633 ExtraData: n.ExtraData, 634 ExtraDataHint: n.ExtraDataHint, 635 } 636 637 // Save comment 638 digest, err := p.commentAddSave(token, ca) 639 if err != nil { 640 return "", err 641 } 642 643 // Update the index 644 ridx.Comments[ca.CommentID] = commentIndex{ 645 Adds: map[uint32][]byte{ 646 1: digest, 647 }, 648 Del: nil, 649 Votes: make(map[string][]voteIndex), 650 } 651 652 // Save the updated index 653 p.recordIndexSave(token, state, *ridx) 654 655 log.Debugf("Comment saved to record %v comment ID %v", 656 ca.Token, ca.CommentID) 657 658 // Return new comment 659 c, err := p.comment(token, *ridx, ca.CommentID) 660 if err != nil { 661 return "", fmt.Errorf("comment %x %v: %v", token, ca.CommentID, err) 662 } 663 664 // Prepare reply 665 nr := comments.NewReply{ 666 Comment: *c, 667 } 668 reply, err := json.Marshal(nr) 669 if err != nil { 670 return "", err 671 } 672 673 return string(reply), nil 674 } 675 676 // cmdEdit edits an existing comment. 677 func (p *commentsPlugin) cmdEdit(token []byte, payload string) (string, error) { 678 // Check if comment edits are allowed 679 if !p.allowEdits { 680 return "", backend.PluginError{ 681 PluginID: comments.PluginID, 682 ErrorCode: uint32(comments.ErrorCodeEditNotAllowed), 683 ErrorContext: "comments plugin setting 'allowedits' is off", 684 } 685 } 686 687 // Decode payload 688 var e comments.Edit 689 err := json.Unmarshal([]byte(payload), &e) 690 if err != nil { 691 return "", err 692 } 693 694 // Verify token 695 err = tokenVerify(token, e.Token) 696 if err != nil { 697 return "", err 698 } 699 700 // Ensure no extra data provided if not allowed 701 err = p.verifyExtraData(e.ExtraData, e.ExtraDataHint) 702 if err != nil { 703 return "", err 704 } 705 706 // Verify signature 707 msg := strconv.FormatUint(uint64(e.State), 10) + e.Token + 708 strconv.FormatUint(uint64(e.ParentID), 10) + 709 strconv.FormatUint(uint64(e.CommentID), 10) + 710 e.Comment + e.ExtraData + e.ExtraDataHint 711 err = util.VerifySignature(e.Signature, e.PublicKey, msg) 712 if err != nil { 713 return "", convertSignatureError(err) 714 } 715 716 // Verify comment 717 if len(e.Comment) == 0 { 718 return "", backend.PluginError{ 719 PluginID: comments.PluginID, 720 ErrorCode: uint32(comments.ErrorCodeEmptyComment), 721 } 722 } 723 if len(e.Comment) > int(p.commentLengthMax) { 724 return "", backend.PluginError{ 725 PluginID: comments.PluginID, 726 ErrorCode: uint32(comments.ErrorCodeMaxLengthExceeded), 727 ErrorContext: fmt.Sprintf("max length is %v characters", 728 p.commentLengthMax), 729 } 730 } 731 732 // Verify record state 733 state, err := p.tstore.RecordState(token) 734 if err != nil { 735 return "", err 736 } 737 if uint32(e.State) != uint32(state) { 738 return "", backend.PluginError{ 739 PluginID: comments.PluginID, 740 ErrorCode: uint32(comments.ErrorCodeRecordStateInvalid), 741 ErrorContext: fmt.Sprintf("got %v, want %v", e.State, state), 742 } 743 } 744 745 // Get record index 746 ridx, err := p.recordIndex(token, state) 747 if err != nil { 748 return "", err 749 } 750 751 cidx, ok := ridx.Comments[e.CommentID] 752 if !ok { 753 // Comment not found 754 return "", backend.PluginError{ 755 PluginID: comments.PluginID, 756 ErrorCode: uint32(comments.ErrorCodeCommentNotFound), 757 } 758 } 759 760 // Get first version of the comment 761 cf, err := p.commentFirstVersion(token, e.CommentID, cidx) 762 if err != nil { 763 return "", err 764 } 765 766 // Comment edits are allowed only during the timeframe 767 // set by the editPeriod plugin setting. 768 if time.Now().Unix() > cf.Timestamp+int64(p.editPeriod) { 769 return "", backend.PluginError{ 770 PluginID: comments.PluginID, 771 ErrorCode: uint32(comments.ErrorCodeEditNotAllowed), 772 ErrorContext: "comment edits timeframe has expired", 773 } 774 } 775 776 // Get the existing comment 777 cs, err := p.comments(token, *ridx, []uint32{e.CommentID}) 778 if err != nil { 779 return "", fmt.Errorf("comments %v: %v", e.CommentID, err) 780 } 781 existing, ok := cs[e.CommentID] 782 if !ok { 783 return "", backend.PluginError{ 784 PluginID: comments.PluginID, 785 ErrorCode: uint32(comments.ErrorCodeCommentNotFound), 786 } 787 } 788 789 // Verify the user ID 790 if e.UserID != existing.UserID { 791 return "", backend.PluginError{ 792 PluginID: comments.PluginID, 793 ErrorCode: uint32(comments.ErrorCodeUserUnauthorized), 794 } 795 } 796 797 // Verify the parent ID 798 if e.ParentID != existing.ParentID { 799 return "", backend.PluginError{ 800 PluginID: comments.PluginID, 801 ErrorCode: uint32(comments.ErrorCodeParentIDInvalid), 802 ErrorContext: fmt.Sprintf("parent id cannot change; got %v, want %v", 803 e.ParentID, existing.ParentID), 804 } 805 } 806 807 // Verify extra data hint. This doesn't really belong here, and should be 808 // left up to the application plugin (i.e. the pi plugin) to decide. It was 809 // put here to prevent application plugin from needing to pull the prior 810 // version of the comment, which is expensive since it causes a tlog tree 811 // retrieval. 812 if e.ExtraDataHint != existing.ExtraDataHint { 813 return "", backend.PluginError{ 814 PluginID: comments.PluginID, 815 ErrorCode: uint32(comments.ErrorCodeEditNotAllowed), 816 ErrorContext: "extra data hint edits are not allowed", 817 } 818 } 819 820 // Verify comment changes 821 if e.Comment == existing.Comment && 822 e.ExtraData == existing.ExtraData { 823 return "", backend.PluginError{ 824 PluginID: comments.PluginID, 825 ErrorCode: uint32(comments.ErrorCodeNoChanges), 826 } 827 } 828 829 // Create a new comment version 830 receipt := p.identity.SignMessage([]byte(e.Signature)) 831 ca := comments.CommentAdd{ 832 UserID: e.UserID, 833 State: e.State, 834 Token: e.Token, 835 ParentID: e.ParentID, 836 Comment: e.Comment, 837 PublicKey: e.PublicKey, 838 Signature: e.Signature, 839 CommentID: e.CommentID, 840 Version: existing.Version + 1, 841 Timestamp: time.Now().Unix(), 842 Receipt: hex.EncodeToString(receipt[:]), 843 ExtraData: e.ExtraData, 844 ExtraDataHint: e.ExtraDataHint, 845 } 846 847 // Save comment 848 digest, err := p.commentAddSave(token, ca) 849 if err != nil { 850 return "", err 851 } 852 853 // Update the index 854 ridx.Comments[ca.CommentID].Adds[ca.Version] = digest 855 856 // Save the updated index 857 p.recordIndexSave(token, state, *ridx) 858 859 log.Debugf("Comment edited on record %v comment ID %v", 860 ca.Token, ca.CommentID) 861 862 // Return updated comment 863 c, err := p.comment(token, *ridx, e.CommentID) 864 if err != nil { 865 return "", fmt.Errorf("comment %x %v: %v", token, e.CommentID, err) 866 } 867 868 // Prepare reply 869 er := comments.EditReply{ 870 Comment: *c, 871 } 872 reply, err := json.Marshal(er) 873 if err != nil { 874 return "", err 875 } 876 877 return string(reply), nil 878 } 879 880 // commentFirstVersion returns the first version of the specified comment. The 881 // returned comment does not include the vote score. 882 func (p *commentsPlugin) commentFirstVersion(token []byte, commentID uint32, cidx commentIndex) (*comments.Comment, error) { 883 // First version comment add digest 884 digest := cidx.Adds[1] 885 886 // Comment add record 887 adds, err := p.commentAdds(token, [][]byte{digest}) 888 if err != nil { 889 return nil, errors.Errorf("commentAdds: %v", err) 890 } 891 if len(adds) != 1 { 892 return nil, errors.Errorf("wrong comment adds count; got %v, want %v", 893 len(adds), 1) 894 } 895 896 // Convert comment add 897 c := convertCommentFromCommentAdd(adds[0]) 898 899 return &c, nil 900 } 901 902 // verifyExtraData ensures no extra data provided if it's not allowed. 903 func (p *commentsPlugin) verifyExtraData(extraData, extraDataHint string) error { 904 if !p.allowExtraData && (extraData != "" || extraDataHint != "") { 905 return backend.PluginError{ 906 PluginID: comments.PluginID, 907 ErrorCode: uint32(comments.ErrorCodeExtraDataNotAllowed), 908 } 909 } 910 return nil 911 } 912 913 // cmdDel deletes a comment. 914 func (p *commentsPlugin) cmdDel(token []byte, payload string) (string, error) { 915 // Decode payload 916 var d comments.Del 917 err := json.Unmarshal([]byte(payload), &d) 918 if err != nil { 919 return "", err 920 } 921 922 // Verify token 923 err = tokenVerify(token, d.Token) 924 if err != nil { 925 return "", err 926 } 927 928 // Verify signature 929 msg := strconv.FormatUint(uint64(d.State), 10) + d.Token + 930 strconv.FormatUint(uint64(d.CommentID), 10) + d.Reason 931 err = util.VerifySignature(d.Signature, d.PublicKey, msg) 932 if err != nil { 933 return "", convertSignatureError(err) 934 } 935 936 // Verify record state 937 state, err := p.tstore.RecordState(token) 938 if err != nil { 939 return "", err 940 } 941 if uint32(d.State) != uint32(state) { 942 return "", backend.PluginError{ 943 PluginID: comments.PluginID, 944 ErrorCode: uint32(comments.ErrorCodeRecordStateInvalid), 945 ErrorContext: fmt.Sprintf("got %v, want %v", d.State, state), 946 } 947 } 948 949 // Get record index 950 ridx, err := p.recordIndex(token, state) 951 if err != nil { 952 return "", err 953 } 954 955 // Get the existing comment 956 cs, err := p.comments(token, *ridx, []uint32{d.CommentID}) 957 if err != nil { 958 return "", fmt.Errorf("comments %v: %v", d.CommentID, err) 959 } 960 existing, ok := cs[d.CommentID] 961 if !ok { 962 return "", backend.PluginError{ 963 PluginID: comments.PluginID, 964 ErrorCode: uint32(comments.ErrorCodeCommentNotFound), 965 } 966 } 967 968 // Prepare comment delete 969 receipt := p.identity.SignMessage([]byte(d.Signature)) 970 cd := comments.CommentDel{ 971 Token: d.Token, 972 State: d.State, 973 CommentID: d.CommentID, 974 Reason: d.Reason, 975 PublicKey: d.PublicKey, 976 Signature: d.Signature, 977 ParentID: existing.ParentID, 978 UserID: existing.UserID, 979 Timestamp: time.Now().Unix(), 980 Receipt: hex.EncodeToString(receipt[:]), 981 } 982 983 // Save comment del 984 digest, err := p.commentDelSave(token, cd) 985 if err != nil { 986 return "", err 987 } 988 989 // Update the index 990 cidx, ok := ridx.Comments[d.CommentID] 991 if !ok { 992 // Should not be possible. The cache is not coherent. 993 panic(fmt.Sprintf("comment not found in index: %v", d.CommentID)) 994 } 995 cidx.Del = digest 996 ridx.Comments[d.CommentID] = cidx 997 998 // Svae the updated index 999 p.recordIndexSave(token, state, *ridx) 1000 1001 // Delete all comment versions. A comment is considered deleted 1002 // once the CommenDel record has been saved. If attempts to 1003 // actually delete the blobs fails, simply log the error and 1004 // continue command execution. The period fsck will clean this up 1005 // next time it is run. 1006 digests := make([][]byte, 0, len(cidx.Adds)) 1007 for _, v := range cidx.Adds { 1008 digests = append(digests, v) 1009 } 1010 err = p.tstore.BlobsDel(token, digests) 1011 if err != nil { 1012 log.Errorf("comments cmdDel %x: BlobsDel %x: %v ", 1013 token, digests, err) 1014 } 1015 1016 // Return updated comment 1017 c, err := p.comment(token, *ridx, d.CommentID) 1018 if err != nil { 1019 return "", fmt.Errorf("comment %v: %v", d.CommentID, err) 1020 } 1021 1022 // Prepare reply 1023 dr := comments.DelReply{ 1024 Comment: *c, 1025 } 1026 reply, err := json.Marshal(dr) 1027 if err != nil { 1028 return "", err 1029 } 1030 1031 return string(reply), nil 1032 } 1033 1034 // cmdVote casts a upvote/downvote for a comment. 1035 func (p *commentsPlugin) cmdVote(token []byte, payload string) (string, error) { 1036 // Decode payload 1037 var v comments.Vote 1038 err := json.Unmarshal([]byte(payload), &v) 1039 if err != nil { 1040 return "", err 1041 } 1042 1043 // Verify token 1044 err = tokenVerify(token, v.Token) 1045 if err != nil { 1046 return "", err 1047 } 1048 1049 // Verify vote 1050 switch v.Vote { 1051 case comments.VoteDownvote, comments.VoteUpvote: 1052 // These are allowed 1053 default: 1054 return "", backend.PluginError{ 1055 PluginID: comments.PluginID, 1056 ErrorCode: uint32(comments.ErrorCodeVoteInvalid), 1057 } 1058 } 1059 1060 // Verify signature 1061 msg := strconv.FormatUint(uint64(v.State), 10) + v.Token + 1062 strconv.FormatUint(uint64(v.CommentID), 10) + 1063 strconv.FormatInt(int64(v.Vote), 10) 1064 err = util.VerifySignature(v.Signature, v.PublicKey, msg) 1065 if err != nil { 1066 return "", convertSignatureError(err) 1067 } 1068 1069 // Verify record state 1070 state, err := p.tstore.RecordState(token) 1071 if err != nil { 1072 return "", err 1073 } 1074 if uint32(v.State) != uint32(state) { 1075 return "", backend.PluginError{ 1076 PluginID: comments.PluginID, 1077 ErrorCode: uint32(comments.ErrorCodeRecordStateInvalid), 1078 ErrorContext: fmt.Sprintf("got %v, want %v", v.State, state), 1079 } 1080 } 1081 1082 // Get record index 1083 ridx, err := p.recordIndex(token, state) 1084 if err != nil { 1085 return "", err 1086 } 1087 1088 // Verify comment exists 1089 cidx, ok := ridx.Comments[v.CommentID] 1090 if !ok { 1091 return "", backend.PluginError{ 1092 PluginID: comments.PluginID, 1093 ErrorCode: uint32(comments.ErrorCodeCommentNotFound), 1094 } 1095 } 1096 1097 // Verify user has not exceeded max allowed vote changes 1098 if len(cidx.Votes[v.UserID]) > int(p.voteChangesMax) { 1099 return "", backend.PluginError{ 1100 PluginID: comments.PluginID, 1101 ErrorCode: uint32(comments.ErrorCodeVoteChangesMaxExceeded), 1102 } 1103 } 1104 1105 // Verify user is not voting on their own comment 1106 cs, err := p.comments(token, *ridx, []uint32{v.CommentID}) 1107 if err != nil { 1108 return "", fmt.Errorf("comments %v: %v", v.CommentID, err) 1109 } 1110 c, ok := cs[v.CommentID] 1111 if !ok { 1112 return "", fmt.Errorf("comment not found %v", v.CommentID) 1113 } 1114 if v.UserID == c.UserID { 1115 return "", backend.PluginError{ 1116 PluginID: comments.PluginID, 1117 ErrorCode: uint32(comments.ErrorCodeVoteInvalid), 1118 ErrorContext: "user cannot vote on their own comment", 1119 } 1120 } 1121 1122 // Prepare comment vote 1123 receipt := p.identity.SignMessage([]byte(v.Signature)) 1124 cv := comments.CommentVote{ 1125 UserID: v.UserID, 1126 State: v.State, 1127 Token: v.Token, 1128 CommentID: v.CommentID, 1129 Vote: v.Vote, 1130 PublicKey: v.PublicKey, 1131 Signature: v.Signature, 1132 Timestamp: time.Now().Unix(), 1133 Receipt: hex.EncodeToString(receipt[:]), 1134 } 1135 1136 // Save comment vote 1137 digest, err := p.commentVoteSave(token, cv) 1138 if err != nil { 1139 return "", err 1140 } 1141 1142 // Add vote to the comment index 1143 votes, ok := cidx.Votes[cv.UserID] 1144 if !ok { 1145 votes = make([]voteIndex, 0, 1) 1146 } 1147 votes = append(votes, voteIndex{ 1148 Vote: cv.Vote, 1149 Digest: digest, 1150 }) 1151 cidx.Votes[cv.UserID] = votes 1152 ridx.Comments[cv.CommentID] = cidx 1153 1154 // Save the updated index 1155 p.recordIndexSave(token, state, *ridx) 1156 1157 // Calculate the new vote scores 1158 downvotes, upvotes := voteScore(cidx) 1159 1160 // Prepare reply 1161 vr := comments.VoteReply{ 1162 Downvotes: downvotes, 1163 Upvotes: upvotes, 1164 Timestamp: cv.Timestamp, 1165 Receipt: cv.Receipt, 1166 } 1167 reply, err := json.Marshal(vr) 1168 if err != nil { 1169 return "", err 1170 } 1171 1172 return string(reply), nil 1173 } 1174 1175 // cmdGet retrieves a batch of specified comments. The most recent version of 1176 // each comment is returned. 1177 func (p *commentsPlugin) cmdGet(token []byte, payload string) (string, error) { 1178 // Decode payload 1179 var g comments.Get 1180 err := json.Unmarshal([]byte(payload), &g) 1181 if err != nil { 1182 return "", err 1183 } 1184 1185 // Get record state 1186 state, err := p.tstore.RecordState(token) 1187 if err != nil { 1188 return "", err 1189 } 1190 1191 // Get record index 1192 ridx, err := p.recordIndex(token, state) 1193 if err != nil { 1194 return "", err 1195 } 1196 1197 // Get comments 1198 cs, err := p.comments(token, *ridx, g.CommentIDs) 1199 if err != nil { 1200 return "", fmt.Errorf("comments: %v", err) 1201 } 1202 1203 // Prepare reply 1204 gr := comments.GetReply{ 1205 Comments: cs, 1206 } 1207 reply, err := json.Marshal(gr) 1208 if err != nil { 1209 return "", err 1210 } 1211 1212 return string(reply), nil 1213 } 1214 1215 // cmdGetAll retrieves all comments for a record. The latest version of each 1216 // comment is returned. 1217 func (p *commentsPlugin) cmdGetAll(token []byte) (string, error) { 1218 // Get record state 1219 state, err := p.tstore.RecordState(token) 1220 if err != nil { 1221 return "", err 1222 } 1223 1224 // Compile comment IDs 1225 ridx, err := p.recordIndex(token, state) 1226 if err != nil { 1227 return "", err 1228 } 1229 commentIDs := make([]uint32, 0, len(ridx.Comments)) 1230 for k := range ridx.Comments { 1231 commentIDs = append(commentIDs, k) 1232 } 1233 1234 // Get comments 1235 c, err := p.comments(token, *ridx, commentIDs) 1236 if err != nil { 1237 return "", fmt.Errorf("comments: %v", err) 1238 } 1239 1240 // Convert comments from a map to a slice 1241 cs := make([]comments.Comment, 0, len(c)) 1242 for _, v := range c { 1243 cs = append(cs, v) 1244 } 1245 1246 // Order comments by comment ID 1247 sort.SliceStable(cs, func(i, j int) bool { 1248 return cs[i].CommentID < cs[j].CommentID 1249 }) 1250 1251 // Prepare reply 1252 gar := comments.GetAllReply{ 1253 Comments: cs, 1254 } 1255 reply, err := json.Marshal(gar) 1256 if err != nil { 1257 return "", err 1258 } 1259 1260 return string(reply), nil 1261 } 1262 1263 // cmdGetVersion retrieves the specified version of a comment. 1264 func (p *commentsPlugin) cmdGetVersion(token []byte, payload string) (string, error) { 1265 // Decode payload 1266 var gv comments.GetVersion 1267 err := json.Unmarshal([]byte(payload), &gv) 1268 if err != nil { 1269 return "", err 1270 } 1271 1272 // Get record state 1273 state, err := p.tstore.RecordState(token) 1274 if err != nil { 1275 return "", err 1276 } 1277 1278 // Get record index 1279 ridx, err := p.recordIndex(token, state) 1280 if err != nil { 1281 return "", err 1282 } 1283 1284 // Verify comment exists 1285 cidx, ok := ridx.Comments[gv.CommentID] 1286 if !ok { 1287 return "", backend.PluginError{ 1288 PluginID: comments.PluginID, 1289 ErrorCode: uint32(comments.ErrorCodeCommentNotFound), 1290 } 1291 } 1292 if cidx.Del != nil { 1293 return "", backend.PluginError{ 1294 PluginID: comments.PluginID, 1295 ErrorCode: uint32(comments.ErrorCodeCommentNotFound), 1296 ErrorContext: "comment has been deleted", 1297 } 1298 } 1299 digest, ok := cidx.Adds[gv.Version] 1300 if !ok { 1301 return "", backend.PluginError{ 1302 PluginID: comments.PluginID, 1303 ErrorCode: uint32(comments.ErrorCodeCommentNotFound), 1304 ErrorContext: fmt.Sprintf("comment %v does not have version %v", 1305 gv.CommentID, gv.Version), 1306 } 1307 } 1308 1309 // Get comment add record 1310 adds, err := p.commentAdds(token, [][]byte{digest}) 1311 if err != nil { 1312 return "", fmt.Errorf("commentAdds: %v", err) 1313 } 1314 if len(adds) != 1 { 1315 return "", fmt.Errorf("wrong comment adds count; got %v, want 1", 1316 len(adds)) 1317 } 1318 1319 // Convert to a comment 1320 c := convertCommentFromCommentAdd(adds[0]) 1321 c.Downvotes, c.Upvotes = voteScore(cidx) 1322 1323 // Prepare reply 1324 gvr := comments.GetVersionReply{ 1325 Comment: c, 1326 } 1327 reply, err := json.Marshal(gvr) 1328 if err != nil { 1329 return "", err 1330 } 1331 1332 return string(reply), nil 1333 } 1334 1335 // cmdCount retrieves the comments count for a record. The comments count is 1336 // the number of comments that have been made on a record. 1337 func (p *commentsPlugin) cmdCount(token []byte) (string, error) { 1338 // Get record state 1339 state, err := p.tstore.RecordState(token) 1340 if err != nil { 1341 return "", err 1342 } 1343 1344 // Get record index 1345 ridx, err := p.recordIndex(token, state) 1346 if err != nil { 1347 return "", err 1348 } 1349 1350 // Prepare reply 1351 cr := comments.CountReply{ 1352 Count: uint32(len(ridx.Comments)), 1353 } 1354 reply, err := json.Marshal(cr) 1355 if err != nil { 1356 return "", err 1357 } 1358 1359 return string(reply), nil 1360 } 1361 1362 // cmdVotes retrieves the comment votes that meet the provided filtering 1363 // criteria. 1364 func (p *commentsPlugin) cmdVotes(token []byte, payload string) (string, error) { 1365 // Decode payload 1366 var v comments.Votes 1367 err := json.Unmarshal([]byte(payload), &v) 1368 if err != nil { 1369 return "", err 1370 } 1371 1372 // Get record state 1373 state, err := p.tstore.RecordState(token) 1374 if err != nil { 1375 return "", err 1376 } 1377 1378 // Get record index 1379 ridx, err := p.recordIndex(token, state) 1380 if err != nil { 1381 return "", err 1382 } 1383 1384 // Collect the requested page of comment vote digests 1385 digests := collectVoteDigestsPage(ridx.Comments, v.UserID, v.Page, 1386 p.votesPageSize) 1387 1388 // Lookup votes 1389 votes, err := p.commentVotes(token, digests) 1390 if err != nil { 1391 return "", fmt.Errorf("commentVotes: %v", err) 1392 } 1393 1394 // Sort comment votes by timestamp from newest to oldest. 1395 sort.SliceStable(votes, func(i, j int) bool { 1396 return votes[i].Timestamp > votes[j].Timestamp 1397 }) 1398 1399 // Prepare reply 1400 vr := comments.VotesReply{ 1401 Votes: votes, 1402 } 1403 reply, err := json.Marshal(vr) 1404 if err != nil { 1405 return "", err 1406 } 1407 1408 return string(reply), nil 1409 } 1410 1411 // collectVoteDigestsPage accepts a map of all comment indexes with a 1412 // filtering criteria and it collects the requested page. 1413 func collectVoteDigestsPage(commentIdxes map[uint32]commentIndex, userID string, page, pageSize uint32) [][]byte { 1414 // Default to first page if page is not provided 1415 if page == 0 { 1416 page = 1 1417 } 1418 1419 digests := make([][]byte, 0, pageSize) 1420 var ( 1421 pageFirstIndex uint32 = (page - 1) * pageSize 1422 pageLastIndex uint32 = page * pageSize 1423 idx uint32 = 0 1424 filterByUserID = userID != "" 1425 ) 1426 1427 // Iterate over record index comments map deterministically; start from 1428 // comment id 1 upwards. 1429 for commentID := 1; commentID <= len(commentIdxes); commentID++ { 1430 cidx := commentIdxes[uint32(commentID)] 1431 switch { 1432 // User ID filtering criteria is applied. Collect the requested page of 1433 // the user's comment votes. 1434 case filterByUserID: 1435 voteIdxs, ok := cidx.Votes[userID] 1436 if !ok { 1437 // User has not cast any votes for this comment 1438 continue 1439 } 1440 for _, vidx := range voteIdxs { 1441 // Add digest if it's part of the requested page 1442 if isInPageRange(idx, pageFirstIndex, pageLastIndex) { 1443 digests = append(digests, vidx.Digest) 1444 1445 // If digests page is full, then we are done 1446 if len(digests) == int(pageSize) { 1447 return digests 1448 } 1449 } 1450 idx++ 1451 } 1452 1453 // No filtering criteria is applied. The votes are indexed by user ID and 1454 // saved in a map. In order to return a page of votes in a deterministic 1455 // manner, the user IDs must first be sorted, then the pagination is 1456 // applied. 1457 default: 1458 userIDs := getSortedUserIDs(cidx.Votes) 1459 for _, userID := range userIDs { 1460 for _, vidx := range cidx.Votes[userID] { 1461 // Add digest if it's part of the requested page 1462 if isInPageRange(idx, pageFirstIndex, pageLastIndex) { 1463 digests = append(digests, vidx.Digest) 1464 1465 // If digests page is full, then we are done 1466 if len(digests) == int(pageSize) { 1467 return digests 1468 } 1469 } 1470 idx++ 1471 } 1472 } 1473 } 1474 } 1475 1476 return digests 1477 } 1478 1479 // getSortedUserIDs accepts a map of comment vote indexes indexed by user IDs, 1480 // it collects the keys, sorts them and finally returns them as sorted slice. 1481 func getSortedUserIDs(m map[string][]voteIndex) []string { 1482 userIDs := make([]string, 0, len(m)) 1483 for userID := range m { 1484 userIDs = append(userIDs, userID) 1485 } 1486 1487 // Sort keys 1488 sort.Strings(userIDs) 1489 1490 return userIDs 1491 } 1492 1493 // isInPageRange determines whether the given index is part of the given 1494 // page range. 1495 func isInPageRange(idx, pageFirstIndex, pageLastIndex uint32) bool { 1496 return idx >= pageFirstIndex && idx <= pageLastIndex 1497 } 1498 1499 // cmdTimestamps retrieves the timestamps for the comments of a record. 1500 func (p *commentsPlugin) cmdTimestamps(token []byte, payload string) (string, error) { 1501 // Decode payload 1502 var t comments.Timestamps 1503 err := json.Unmarshal([]byte(payload), &t) 1504 if err != nil { 1505 return "", err 1506 } 1507 1508 // Get timestamps 1509 ctr, err := p.commentTimestamps(token, t.CommentIDs, t.IncludeVotes) 1510 if err != nil { 1511 return "", err 1512 } 1513 1514 // Prepare reply 1515 reply, err := json.Marshal(*ctr) 1516 if err != nil { 1517 return "", err 1518 } 1519 1520 return string(reply), nil 1521 } 1522 1523 // tokenDecode decodes a tstore token. It only accepts full length tokens. 1524 func tokenDecode(token string) ([]byte, error) { 1525 return util.TokenDecode(util.TokenTypeTstore, token) 1526 } 1527 1528 // tokenVerify verifies that a token that is part of a plugin command payload 1529 // is valid. This is applicable when a plugin command payload contains a 1530 // signature that includes the record token. The token included in payload must 1531 // be a valid, full length record token and it must match the token that was 1532 // passed into the politeiad API for this plugin command, i.e. the token for 1533 // the record that this plugin command is being executed on. 1534 func tokenVerify(cmdToken []byte, payloadToken string) error { 1535 pt, err := tokenDecode(payloadToken) 1536 if err != nil { 1537 return backend.PluginError{ 1538 PluginID: comments.PluginID, 1539 ErrorCode: uint32(comments.ErrorCodeTokenInvalid), 1540 ErrorContext: util.TokenRegexp(), 1541 } 1542 } 1543 if !bytes.Equal(cmdToken, pt) { 1544 return backend.PluginError{ 1545 PluginID: comments.PluginID, 1546 ErrorCode: uint32(comments.ErrorCodeTokenInvalid), 1547 ErrorContext: fmt.Sprintf("payload token does not match "+ 1548 "command token: got %x, want %x", pt, cmdToken), 1549 } 1550 } 1551 return nil 1552 } 1553 1554 // commentVersionLatest returns the latest comment version. 1555 func commentVersionLatest(cidx commentIndex) uint32 { 1556 var maxVersion uint32 1557 for version := range cidx.Adds { 1558 if version > maxVersion { 1559 maxVersion = version 1560 } 1561 } 1562 return maxVersion 1563 } 1564 1565 // commentExists returns whether the provided comment ID exists. 1566 func commentExists(ridx recordIndex, commentID uint32) bool { 1567 _, ok := ridx.Comments[commentID] 1568 return ok 1569 } 1570 1571 // commentIDLatest returns the latest comment ID. 1572 func commentIDLatest(idx recordIndex) uint32 { 1573 var maxID uint32 1574 for id := range idx.Comments { 1575 if id > maxID { 1576 maxID = id 1577 } 1578 } 1579 return maxID 1580 } 1581 1582 func convertCommentFromCommentAdd(ca comments.CommentAdd) comments.Comment { 1583 return comments.Comment{ 1584 UserID: ca.UserID, 1585 State: ca.State, 1586 Token: ca.Token, 1587 ParentID: ca.ParentID, 1588 Comment: ca.Comment, 1589 PublicKey: ca.PublicKey, 1590 Signature: ca.Signature, 1591 CommentID: ca.CommentID, 1592 Version: ca.Version, 1593 Timestamp: ca.Timestamp, 1594 Receipt: ca.Receipt, 1595 Downvotes: 0, // Not part of commentAdd data 1596 Upvotes: 0, // Not part of commentAdd data 1597 Deleted: false, 1598 Reason: "", 1599 ExtraData: ca.ExtraData, 1600 ExtraDataHint: ca.ExtraDataHint, 1601 } 1602 } 1603 1604 func convertCommentFromCommentDel(cd comments.CommentDel) comments.Comment { 1605 // Score needs to be filled in separately 1606 return comments.Comment{ 1607 UserID: cd.UserID, 1608 State: cd.State, 1609 Token: cd.Token, 1610 ParentID: cd.ParentID, 1611 Comment: "", 1612 PublicKey: cd.PublicKey, 1613 Signature: cd.Signature, 1614 CommentID: cd.CommentID, 1615 Version: 0, 1616 Timestamp: cd.Timestamp, 1617 Receipt: cd.Receipt, 1618 Downvotes: 0, 1619 Upvotes: 0, 1620 Deleted: true, 1621 Reason: cd.Reason, 1622 } 1623 } 1624 1625 func convertSignatureError(err error) backend.PluginError { 1626 var e util.SignatureError 1627 var s comments.ErrorCodeT 1628 if errors.As(err, &e) { 1629 switch e.ErrorCode { 1630 case util.ErrorStatusPublicKeyInvalid: 1631 s = comments.ErrorCodePublicKeyInvalid 1632 case util.ErrorStatusSignatureInvalid: 1633 s = comments.ErrorCodeSignatureInvalid 1634 } 1635 } 1636 return backend.PluginError{ 1637 PluginID: comments.PluginID, 1638 ErrorCode: uint32(s), 1639 ErrorContext: e.ErrorContext, 1640 } 1641 } 1642 1643 func convertBlobEntryFromCommentAdd(c comments.CommentAdd) (*store.BlobEntry, error) { 1644 data, err := json.Marshal(c) 1645 if err != nil { 1646 return nil, err 1647 } 1648 hint, err := json.Marshal( 1649 store.DataDescriptor{ 1650 Type: store.DataTypeStructure, 1651 Descriptor: dataDescriptorCommentAdd, 1652 }) 1653 if err != nil { 1654 return nil, err 1655 } 1656 be := store.NewBlobEntry(hint, data) 1657 return &be, nil 1658 } 1659 1660 func convertBlobEntryFromCommentDel(c comments.CommentDel) (*store.BlobEntry, error) { 1661 data, err := json.Marshal(c) 1662 if err != nil { 1663 return nil, err 1664 } 1665 hint, err := json.Marshal( 1666 store.DataDescriptor{ 1667 Type: store.DataTypeStructure, 1668 Descriptor: dataDescriptorCommentDel, 1669 }) 1670 if err != nil { 1671 return nil, err 1672 } 1673 be := store.NewBlobEntry(hint, data) 1674 return &be, nil 1675 } 1676 1677 func convertBlobEntryFromCommentVote(c comments.CommentVote) (*store.BlobEntry, error) { 1678 data, err := json.Marshal(c) 1679 if err != nil { 1680 return nil, err 1681 } 1682 hint, err := json.Marshal( 1683 store.DataDescriptor{ 1684 Type: store.DataTypeStructure, 1685 Descriptor: dataDescriptorCommentVote, 1686 }) 1687 if err != nil { 1688 return nil, err 1689 } 1690 be := store.NewBlobEntry(hint, data) 1691 return &be, nil 1692 } 1693 1694 func convertCommentAddFromBlobEntry(be store.BlobEntry) (*comments.CommentAdd, error) { 1695 // Decode and validate data hint 1696 b, err := base64.StdEncoding.DecodeString(be.DataHint) 1697 if err != nil { 1698 return nil, fmt.Errorf("decode DataHint: %v", err) 1699 } 1700 var dd store.DataDescriptor 1701 err = json.Unmarshal(b, &dd) 1702 if err != nil { 1703 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 1704 } 1705 if dd.Descriptor != dataDescriptorCommentAdd { 1706 return nil, fmt.Errorf("unexpected data descriptor: got %v, want %v", 1707 dd.Descriptor, dataDescriptorCommentAdd) 1708 } 1709 1710 // Decode data 1711 b, err = base64.StdEncoding.DecodeString(be.Data) 1712 if err != nil { 1713 return nil, fmt.Errorf("decode Data: %v", err) 1714 } 1715 digest, err := hex.DecodeString(be.Digest) 1716 if err != nil { 1717 return nil, fmt.Errorf("decode digest: %v", err) 1718 } 1719 if !bytes.Equal(util.Digest(b), digest) { 1720 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 1721 util.Digest(b), digest) 1722 } 1723 var c comments.CommentAdd 1724 err = json.Unmarshal(b, &c) 1725 if err != nil { 1726 return nil, fmt.Errorf("unmarshal CommentAdd: %v", err) 1727 } 1728 1729 return &c, nil 1730 } 1731 1732 func convertCommentDelFromBlobEntry(be store.BlobEntry) (*comments.CommentDel, error) { 1733 // Decode and validate data hint 1734 b, err := base64.StdEncoding.DecodeString(be.DataHint) 1735 if err != nil { 1736 return nil, fmt.Errorf("decode DataHint: %v", err) 1737 } 1738 var dd store.DataDescriptor 1739 err = json.Unmarshal(b, &dd) 1740 if err != nil { 1741 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 1742 } 1743 if dd.Descriptor != dataDescriptorCommentDel { 1744 return nil, fmt.Errorf("unexpected data descriptor: got %v, want %v", 1745 dd.Descriptor, dataDescriptorCommentDel) 1746 } 1747 1748 // Decode data 1749 b, err = base64.StdEncoding.DecodeString(be.Data) 1750 if err != nil { 1751 return nil, fmt.Errorf("decode Data: %v", err) 1752 } 1753 digest, err := hex.DecodeString(be.Digest) 1754 if err != nil { 1755 return nil, fmt.Errorf("decode digest: %v", err) 1756 } 1757 if !bytes.Equal(util.Digest(b), digest) { 1758 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 1759 util.Digest(b), digest) 1760 } 1761 var c comments.CommentDel 1762 err = json.Unmarshal(b, &c) 1763 if err != nil { 1764 return nil, fmt.Errorf("unmarshal CommentDel: %v", err) 1765 } 1766 1767 return &c, nil 1768 } 1769 1770 func convertCommentVoteFromBlobEntry(be store.BlobEntry) (*comments.CommentVote, error) { 1771 // Decode and validate data hint 1772 b, err := base64.StdEncoding.DecodeString(be.DataHint) 1773 if err != nil { 1774 return nil, fmt.Errorf("decode DataHint: %v", err) 1775 } 1776 var dd store.DataDescriptor 1777 err = json.Unmarshal(b, &dd) 1778 if err != nil { 1779 return nil, fmt.Errorf("unmarshal DataHint: %v", err) 1780 } 1781 if dd.Descriptor != dataDescriptorCommentVote { 1782 return nil, fmt.Errorf("unexpected data descriptor: got %v, want %v", 1783 dd.Descriptor, dataDescriptorCommentVote) 1784 } 1785 1786 // Decode data 1787 b, err = base64.StdEncoding.DecodeString(be.Data) 1788 if err != nil { 1789 return nil, fmt.Errorf("decode Data: %v", err) 1790 } 1791 digest, err := hex.DecodeString(be.Digest) 1792 if err != nil { 1793 return nil, fmt.Errorf("decode digest: %v", err) 1794 } 1795 if !bytes.Equal(util.Digest(b), digest) { 1796 return nil, fmt.Errorf("data is not coherent; got %x, want %x", 1797 util.Digest(b), digest) 1798 } 1799 var cv comments.CommentVote 1800 err = json.Unmarshal(b, &cv) 1801 if err != nil { 1802 return nil, fmt.Errorf("unmarshal CommentVote: %v", err) 1803 } 1804 1805 return &cv, nil 1806 }