github.com/decred/politeia@v1.4.0/politeiad/backend/gitbe/decred.go (about) 1 // Copyright (c) 2017-2019 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 gitbe 6 7 import ( 8 "bufio" 9 "bytes" 10 "encoding/base64" 11 "encoding/hex" 12 "encoding/json" 13 "errors" 14 "fmt" 15 "io" 16 "net/http" 17 "os" 18 "path/filepath" 19 "strconv" 20 "strings" 21 "time" 22 23 "github.com/decred/dcrd/chaincfg/chainhash" 24 "github.com/decred/dcrd/dcrec/secp256k1/v3/ecdsa" 25 "github.com/decred/dcrd/dcrutil/v3" 26 "github.com/decred/dcrd/wire" 27 dcrdataapi "github.com/decred/dcrdata/v6/api/types" 28 "github.com/decred/politeia/politeiad/api/v1/identity" 29 "github.com/decred/politeia/politeiad/backend" 30 "github.com/decred/politeia/politeiad/backend/gitbe/decredplugin" 31 "github.com/decred/politeia/util" 32 ) 33 34 // XXX plugins really need to become an interface. Run with this for now. 35 36 const ( 37 decredPluginIdentity = "fullidentity" 38 decredPluginJournals = "journals" 39 40 defaultCommentIDFilename = "commentid.txt" 41 defaultCommentFilename = "comments.journal" 42 defaultCommentsFlushed = "comments.flushed" 43 44 defaultBallotFilename = "ballot.journal" 45 defaultBallotFlushed = "ballot.flushed" 46 47 journalVersion = "1" // Version 1 of the comment journal 48 journalActionAdd = "add" // Add entry 49 journalActionDel = "del" // Delete entry 50 journalActionAddLike = "addlike" // Add comment like 51 52 flushRecordVersion = "1" // Version 1 of the flush journal 53 54 // Following are what should be well-known interface hooks 55 PluginPostHookEdit = "postedit" // Hook Post Edit 56 ) 57 58 var ( 59 // errDuplicateVote is emitted when a cast vote is a duplicate. 60 errDuplicateVote = errors.New("duplicate vote") 61 ) 62 63 // FlushRecord is a structure that is stored on disk when a journal has been 64 // flushed. 65 type FlushRecord struct { 66 Version string `json:"version"` // Version 67 Timestamp string `json:"timestamp"` // Timestamp 68 } 69 70 // JournalAction prefixes and determines what the next structure is in 71 // the JSON journal. 72 // Version is used to determine what version of the comment journal structure 73 // follows. 74 // journalActionAdd -> Add entry 75 // journalActionDel -> Delete entry 76 // journalActionAddLike -> Add comment like structure (comments only) 77 type JournalAction struct { 78 Version string `json:"version"` // Version 79 Action string `json:"action"` // Add/Del 80 } 81 82 var ( 83 decredPluginSettings map[string]string // [key]setting 84 decredPluginHooks map[string]func(string) error // [key]func(token) error 85 86 // Pregenerated journal actions 87 journalAdd []byte 88 journalDel []byte 89 journalAddLike []byte 90 91 // Plugin specific data that CANNOT be treated as metadata 92 pluginDataDir = filepath.Join("plugins", "decred") 93 94 decredPluginCommentsCache = make(map[string]map[string]decredplugin.Comment) // [token][commentid]comment 95 96 journalsReplayed bool = false 97 ) 98 99 // init is used to pregenerate the JSON journal actions. 100 func init() { 101 var err error 102 103 journalAdd, err = json.Marshal(JournalAction{ 104 Version: journalVersion, 105 Action: journalActionAdd, 106 }) 107 if err != nil { 108 panic(err.Error()) 109 } 110 journalDel, err = json.Marshal(JournalAction{ 111 Version: journalVersion, 112 Action: journalActionDel, 113 }) 114 if err != nil { 115 panic(err.Error()) 116 } 117 journalAddLike, err = json.Marshal(JournalAction{ 118 Version: journalVersion, 119 Action: journalActionAddLike, 120 }) 121 if err != nil { 122 panic(err.Error()) 123 } 124 } 125 126 func getDecredPlugin(dcrdataHost string) backend.Plugin { 127 decredPlugin := backend.Plugin{ 128 ID: decredplugin.ID, 129 Version: decredplugin.Version, 130 Settings: []backend.PluginSetting{}, 131 } 132 133 decredPlugin.Settings = append(decredPlugin.Settings, 134 backend.PluginSetting{ 135 Key: "dcrdata", 136 Value: dcrdataHost, 137 }, 138 ) 139 140 // Initialize hooks 141 decredPluginHooks = make(map[string]func(string) error) 142 143 // Initialize settings map 144 decredPluginSettings = make(map[string]string) 145 for _, v := range decredPlugin.Settings { 146 decredPluginSettings[v.Key] = v.Value 147 } 148 return decredPlugin 149 } 150 151 // initDecredPlugin is called externally to run initial procedures 152 // such as replaying journals 153 func (g *gitBackEnd) initDecredPluginJournals() error { 154 log.Infof("initDecredPlugin") 155 156 // check if backend journal is initialized 157 if g.journal == nil { 158 return fmt.Errorf("initDecredPlugin backend journal isn't initialized") 159 } 160 161 err := g.replayAllJournals() 162 if err != nil { 163 log.Infof("initDecredPlugin replay all journals %v", err) 164 } 165 return nil 166 } 167 168 // replayAllJournals replays ballot and comment journals for every stored proposal 169 // this function can be called without the lock held 170 func (g *gitBackEnd) replayAllJournals() error { 171 log.Infof("replayAllJournals") 172 files, err := os.ReadDir(g.journals) 173 if err != nil { 174 return fmt.Errorf("Read dir journals: %v", err) 175 } 176 for _, f := range files { 177 name := f.Name() 178 // replay comments for all props 179 _, err = g.replayComments(name) 180 if err != nil { 181 return fmt.Errorf("replayAllJournals replayComments %s %v", name, err) 182 } 183 } 184 journalsReplayed = true 185 return nil 186 } 187 188 // SetDecredPluginSetting removes a setting if the value is "" and adds a setting otherwise. 189 func setDecredPluginSetting(key, value string) { 190 if value == "" { 191 delete(decredPluginSettings, key) 192 return 193 } 194 195 decredPluginSettings[key] = value 196 } 197 198 func setDecredPluginHook(name string, f func(string) error) { 199 decredPluginHooks[name] = f 200 } 201 202 func (g *gitBackEnd) vettedPropExists(token string) bool { 203 tokenb, err := util.ConvertStringToken(token) 204 if err != nil { 205 return false 206 } 207 return g.VettedExists(tokenb) 208 } 209 210 func (g *gitBackEnd) getNewCid(token string) (string, error) { 211 dir := pijoin(g.journals, token) 212 err := os.MkdirAll(dir, 0774) 213 if err != nil { 214 return "", err 215 } 216 217 filename := pijoin(dir, defaultCommentIDFilename) 218 219 g.Lock() 220 defer g.Unlock() 221 222 fh, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0664) 223 if err != nil { 224 return "", err 225 } 226 defer fh.Close() 227 228 // Determine if file is empty 229 fi, err := fh.Stat() 230 if err != nil { 231 return "", err 232 } 233 if fi.Size() == 0 { 234 // First comment id 235 _, err := fmt.Fprintf(fh, "1\n") 236 if err != nil { 237 return "", err 238 } 239 return "1", nil 240 } 241 242 // Only allow one line 243 var cid string 244 s := bufio.NewScanner(fh) 245 for i := 0; s.Scan(); i++ { 246 if i != 0 { 247 return "", fmt.Errorf("comment id file corrupt") 248 } 249 250 c, err := strconv.ParseUint(s.Text(), 10, 64) 251 if err != nil { 252 return "", err 253 } 254 255 // Increment comment id 256 c++ 257 cid = strconv.FormatUint(c, 10) 258 259 // Write back new comment id 260 _, err = fh.Seek(0, io.SeekStart) 261 if err != nil { 262 return "", err 263 } 264 _, err = fmt.Fprintf(fh, "%v\n", c) 265 if err != nil { 266 return "", err 267 } 268 } 269 if err := s.Err(); err != nil { 270 return "", err 271 } 272 273 return cid, nil 274 } 275 276 // verifyMessage verifies a message is properly signed. 277 // Copied from https://github.com/decred/dcrd/blob/0fc55252f912756c23e641839b1001c21442c38a/rpcserver.go#L5605 278 func (g *gitBackEnd) verifyMessage(address, message, signature string) (bool, error) { 279 // Decode the provided address. 280 addr, err := dcrutil.DecodeAddress(address, g.activeNetParams) 281 if err != nil { 282 return false, fmt.Errorf("Could not decode address: %v", 283 err) 284 } 285 286 // Only P2PKH addresses are valid for signing. 287 if _, ok := addr.(*dcrutil.AddressPubKeyHash); !ok { 288 return false, fmt.Errorf("Address is not a pay-to-pubkey-hash "+ 289 "address: %v", address) 290 } 291 292 // Decode base64 signature. 293 sig, err := base64.StdEncoding.DecodeString(signature) 294 if err != nil { 295 return false, fmt.Errorf("Malformed base64 encoding: %v", err) 296 } 297 298 // Validate the signature - this just shows that it was valid at all. 299 // we will compare it with the key next. 300 var buf bytes.Buffer 301 wire.WriteVarString(&buf, 0, "Decred Signed Message:\n") 302 wire.WriteVarString(&buf, 0, message) 303 expectedMessageHash := chainhash.HashB(buf.Bytes()) 304 pk, wasCompressed, err := ecdsa.RecoverCompact(sig, 305 expectedMessageHash) 306 if err != nil { 307 // Mirror Bitcoin Core behavior, which treats error in 308 // RecoverCompact as invalid signature. 309 return false, nil 310 } 311 312 // Reconstruct the pubkey hash. 313 dcrPK := pk 314 var serializedPK []byte 315 if wasCompressed { 316 serializedPK = dcrPK.SerializeCompressed() 317 } else { 318 serializedPK = dcrPK.SerializeUncompressed() 319 } 320 a, err := dcrutil.NewAddressSecpPubKey(serializedPK, g.activeNetParams) 321 if err != nil { 322 // Again mirror Bitcoin Core behavior, which treats error in 323 // public key reconstruction as invalid signature. 324 return false, nil 325 } 326 327 // Return boolean if addresses match. 328 return a.Address() == address, nil 329 } 330 331 func bestBlock() (*dcrdataapi.BlockDataBasic, error) { 332 url := decredPluginSettings["dcrdata"] + "/api/block/best" 333 log.Debugf("connecting to %v", url) 334 // XXX this http command needs a reasonable timeout. 335 r, err := http.Get(url) 336 log.Debugf("http connecting to %v", url) 337 if err != nil { 338 return nil, err 339 } 340 defer r.Body.Close() 341 342 if r.StatusCode != http.StatusOK { 343 body, err := io.ReadAll(r.Body) 344 if err != nil { 345 return nil, fmt.Errorf("dcrdata error: %v %v %v", 346 r.StatusCode, url, err) 347 } 348 return nil, fmt.Errorf("dcrdata error: %v %v %s", 349 r.StatusCode, url, body) 350 } 351 352 var bdb dcrdataapi.BlockDataBasic 353 decoder := json.NewDecoder(r.Body) 354 if err := decoder.Decode(&bdb); err != nil { 355 return nil, err 356 } 357 358 return &bdb, nil 359 } 360 361 func block(block uint32) (*dcrdataapi.BlockDataBasic, error) { 362 h := strconv.FormatUint(uint64(block), 10) 363 url := decredPluginSettings["dcrdata"] + "/api/block/" + h 364 log.Debugf("connecting to %v", url) 365 r, err := http.Get(url) 366 if err != nil { 367 return nil, err 368 } 369 defer r.Body.Close() 370 371 if r.StatusCode != http.StatusOK { 372 body, err := io.ReadAll(r.Body) 373 if err != nil { 374 return nil, fmt.Errorf("dcrdata error: %v %v %v", 375 r.StatusCode, url, err) 376 } 377 return nil, fmt.Errorf("dcrdata error: %v %v %s", 378 r.StatusCode, url, body) 379 } 380 381 var bdb dcrdataapi.BlockDataBasic 382 decoder := json.NewDecoder(r.Body) 383 if err := decoder.Decode(&bdb); err != nil { 384 return nil, err 385 } 386 387 return &bdb, nil 388 } 389 390 // pluginBestBlock returns current best block height from wallet. 391 func (g *gitBackEnd) pluginBestBlock() (string, error) { 392 bb, err := bestBlock() 393 if err != nil { 394 return "", err 395 } 396 return strconv.FormatUint(uint64(bb.Height), 10), nil 397 } 398 399 // decredPluginPostEdit called after and edit is complete but before commit. 400 func (g *gitBackEnd) decredPluginPostEdit(token string) error { 401 log.Tracef("decredPluginPostEdit: %v", token) 402 403 // The post edit hook gets called on both unvetted and vetted 404 // proposals, but comments can only be made on vetted proposals. 405 var destination string 406 var err error 407 if g.vettedPropExists(token) { 408 destination, err = g.flushComments(token) 409 if err != nil { 410 return err 411 } 412 } 413 414 // When destination is empty there was nothing to do 415 if destination == "" { 416 log.Tracef("decredPluginPostEdit: nothing to do %v", token) 417 return nil 418 } 419 420 // Add comments to git 421 return g.gitAdd(g.unvetted, destination) 422 } 423 424 // createFlushFile creates a file that indicates that the a journal was flused. 425 // 426 // Must be called WITH the mutex held. 427 func createFlushFile(filename string) error { 428 // Mark directory as flushed 429 f, err := os.Create(filename) 430 if err != nil { 431 return err 432 } 433 434 defer f.Close() 435 436 // Stuff timestamp in flushfile 437 j := json.NewEncoder(f) 438 err = j.Encode(FlushRecord{ 439 Version: flushRecordVersion, 440 Timestamp: strconv.FormatInt(time.Now().Unix(), 10), 441 }) 442 443 return err 444 } 445 446 // flushJournalsUnwind unwinds all the flushing action if something goes wrong. 447 // 448 // Must be called WITH the mutex held. 449 func (g *gitBackEnd) flushJournalsUnwind(id string) error { 450 // git stash, can fail if there are no uncommitted failures 451 err := g.gitStash(g.unvetted) 452 if err == nil { 453 // git stash drop, allowed to fail 454 _ = g.gitStashDrop(g.unvetted) 455 } 456 457 // git checkout master 458 err = g.gitCheckout(g.unvetted, "master") 459 if err != nil { 460 return err 461 } 462 // delete branch 463 err = g.gitBranchDelete(g.unvetted, id) 464 if err != nil { 465 return err 466 } 467 // git clean -xdf 468 return g.gitClean(g.unvetted) 469 } 470 471 // flushCommentflushes comments journal to decred plugin directory in 472 // git. It returns the filename that was coppied into git repo. 473 // 474 // Must be called WITH the mutex held. 475 func (g *gitBackEnd) flushComments(token string) (string, error) { 476 if !g.vettedPropExists(token) { 477 return "", fmt.Errorf("unknown proposal: %v", token) 478 } 479 480 // Setup source filenames and verify they actually exist 481 srcDir := pijoin(g.journals, token) 482 srcComments := pijoin(srcDir, defaultCommentFilename) 483 if !util.FileExists(srcComments) { 484 return "", nil 485 } 486 487 // Setup destination filenames 488 version, err := getLatest(pijoin(g.unvetted, token)) 489 if err != nil { 490 return "", err 491 } 492 dir := pijoin(g.unvetted, token, version, pluginDataDir) 493 comments := pijoin(dir, defaultCommentFilename) 494 495 // Create the destination container dir 496 _ = os.MkdirAll(dir, 0764) 497 498 // Move journal and comment id into place 499 err = g.journal.Copy(srcComments, comments) 500 if err != nil { 501 return "", err 502 } 503 504 // Return filename that is relative to git dir. 505 return pijoin(token, version, pluginDataDir, defaultCommentFilename), 506 nil 507 } 508 509 // flushCommentJournal flushes an individual comment journal. 510 // 511 // Must be called WITH the mutex held. 512 func (g *gitBackEnd) flushCommentJournal(token string) (string, error) { 513 // We simply copy the journal into git 514 destination, err := g.flushComments(token) 515 if err != nil { 516 return "", fmt.Errorf("Could not flush %v: %v", token, err) 517 } 518 519 // Create flush record 520 filename := pijoin(g.journals, token, defaultCommentsFlushed) 521 err = createFlushFile(filename) 522 if err != nil { 523 return "", fmt.Errorf("Could not mark flushed %v: %v", token, 524 err) 525 } 526 527 return destination, nil 528 } 529 530 // _flushCommentJournals walks all comment journal directories and copies 531 // modified journals into the unvetted repo. It returns an array of filenames 532 // that need to be added to the git repo and subsequently rebased into the 533 // vetted repo . 534 // 535 // Must be called WITH the mutex held. 536 func (g *gitBackEnd) _flushCommentJournals() ([]string, error) { 537 dirs, err := os.ReadDir(g.journals) 538 if err != nil { 539 return nil, err 540 } 541 542 files := make([]string, 0, len(dirs)) 543 for _, v := range dirs { 544 filename := pijoin(g.journals, v.Name(), 545 defaultCommentsFlushed) 546 log.Tracef("Checking: %v", v.Name()) 547 if util.FileExists(filename) { 548 continue 549 } 550 551 log.Infof("Flushing comments: %v", v.Name()) 552 553 // Add filename to work 554 destination, err := g.flushCommentJournal(v.Name()) 555 if err != nil { 556 log.Error(err) 557 continue 558 } 559 560 files = append(files, destination) 561 } 562 563 return files, nil 564 } 565 566 // flushCommentJournals wraps _flushCommentJournals in git magic to revert 567 // flush in case of errors. 568 // 569 // Must be called WITHOUT the mutex held. 570 func (g *gitBackEnd) flushCommentJournals() error { 571 log.Tracef("flushCommentJournals") 572 573 // We may have to make this more granular 574 g.Lock() 575 defer g.Unlock() 576 577 // git checkout master 578 err := g.gitCheckout(g.unvetted, "master") 579 if err != nil { 580 return err 581 } 582 583 // git pull --ff-only --rebase 584 err = g.gitPull(g.unvetted, true) 585 if err != nil { 586 return err 587 } 588 589 // git checkout -b timestamp_flushcomments 590 branch := strconv.FormatInt(time.Now().Unix(), 10) + "_flushcomments" 591 _ = g.gitBranchDelete(g.unvetted, branch) // Just in case 592 err = g.gitNewBranch(g.unvetted, branch) 593 if err != nil { 594 return err 595 } 596 597 // closure to handle unwind if needed 598 var errUnwind error 599 defer func() { 600 if errUnwind == nil { 601 return 602 } 603 err := g.flushJournalsUnwind(branch) 604 if err != nil { 605 log.Errorf("flushJournalsUnwind: %v", err) 606 } 607 }() 608 609 // Flush journals 610 files, err := g._flushCommentJournals() 611 if err != nil { 612 errUnwind = err 613 return err 614 } 615 616 if len(files) == 0 { 617 log.Info("flushCommentJournals: nothing to do") 618 err = g.flushJournalsUnwind(branch) 619 if err != nil { 620 log.Errorf("flushJournalsUnwind: %v", err) 621 } 622 return nil 623 } 624 625 // git add journals 626 commitMessage := "Flush comment journals.\n\n" 627 for _, v := range files { 628 err = g.gitAdd(g.unvetted, v) 629 if err != nil { 630 errUnwind = err 631 return err 632 } 633 634 s := strings.Split(v, string(os.PathSeparator)) 635 if len(s) == 0 { 636 commitMessage += "ERROR: " + v + "\n" 637 } else { 638 commitMessage += s[0] + "\n" 639 } 640 } 641 642 // git commit 643 err = g.gitCommit(g.unvetted, commitMessage) 644 if err != nil { 645 errUnwind = err 646 return err 647 } 648 649 // git rebase master 650 err = g.rebasePR(branch) 651 if err != nil { 652 errUnwind = err 653 return err 654 } 655 656 return nil 657 } 658 659 // flushVotes flushes votes journal to decred plugin directory in git. It 660 // returns the filename that was coppied into git repo. 661 // 662 // Must be called WITH the mutex held. 663 func (g *gitBackEnd) flushVotes(token string) (string, error) { 664 if !g.vettedPropExists(token) { 665 return "", fmt.Errorf("unknown proposal: %v", token) 666 } 667 668 // Setup source filenames and verify they actually exist 669 srcDir := pijoin(g.journals, token) 670 srcVotes := pijoin(srcDir, defaultBallotFilename) 671 if !util.FileExists(srcVotes) { 672 return "", nil 673 } 674 675 // Setup destination filenames 676 version, err := getLatest(pijoin(g.unvetted, token)) 677 if err != nil { 678 return "", err 679 } 680 dir := pijoin(g.unvetted, token, version, pluginDataDir) 681 votes := pijoin(dir, defaultBallotFilename) 682 683 // Create the destination container dir 684 _ = os.MkdirAll(dir, 0764) 685 686 // Move journal into place 687 err = g.journal.Copy(srcVotes, votes) 688 if err != nil { 689 return "", err 690 } 691 692 // Return filename that is relative to git dir. 693 return pijoin(token, version, pluginDataDir, defaultBallotFilename), nil 694 } 695 696 // _flushVotesJournals walks all votes journal directories and copies 697 // modified journals into the unvetted repo. It returns an array of filenames 698 // that need to be added to the git repo and subsequently rebased into the 699 // vetted repo . 700 // 701 // Must be called WITH the mutex held. 702 func (g *gitBackEnd) _flushVotesJournals() ([]string, error) { 703 dirs, err := os.ReadDir(g.journals) 704 if err != nil { 705 return nil, err 706 } 707 708 files := make([]string, 0, len(dirs)) 709 for _, v := range dirs { 710 filename := pijoin(g.journals, v.Name(), 711 defaultBallotFlushed) 712 log.Tracef("Checking: %v", v.Name()) 713 if util.FileExists(filename) { 714 continue 715 } 716 717 log.Infof("Flushing votes: %v", v.Name()) 718 719 // We simply copy the journal into git 720 destination, err := g.flushVotes(v.Name()) 721 if err != nil { 722 log.Errorf("Could not flush %v: %v", v.Name(), err) 723 continue 724 } 725 726 // Create flush record 727 err = createFlushFile(filename) 728 if err != nil { 729 log.Errorf("Could not mark flushed %v: %v", v.Name(), 730 err) 731 continue 732 } 733 734 // Add filename to work 735 files = append(files, destination) 736 } 737 738 return files, nil 739 } 740 741 // flushVoteJournals wraps _flushVoteJournals in git magic to revert 742 // flush in case of errors. 743 // 744 // Must be called WITHOUT the mutex held. 745 func (g *gitBackEnd) flushVoteJournals() error { 746 log.Tracef("flushVoteJournals") 747 748 // We may have to make this more granular 749 g.Lock() 750 defer g.Unlock() 751 752 // git checkout master 753 err := g.gitCheckout(g.unvetted, "master") 754 if err != nil { 755 return err 756 } 757 758 // git pull --ff-only --rebase 759 err = g.gitPull(g.unvetted, true) 760 if err != nil { 761 return err 762 } 763 764 // git checkout -b timestamp_flushvotes 765 branch := strconv.FormatInt(time.Now().Unix(), 10) + "_flushvotes" 766 _ = g.gitBranchDelete(g.unvetted, branch) // Just in case 767 err = g.gitNewBranch(g.unvetted, branch) 768 if err != nil { 769 return err 770 } 771 772 // closure to handle unwind if needed 773 var errUnwind error 774 defer func() { 775 if errUnwind == nil { 776 return 777 } 778 err := g.flushJournalsUnwind(branch) 779 if err != nil { 780 log.Errorf("flushJournalsUnwind: %v", err) 781 } 782 }() 783 784 // Flush journals 785 files, err := g._flushVotesJournals() 786 if err != nil { 787 errUnwind = err 788 return err 789 } 790 791 if len(files) == 0 { 792 log.Info("flushVotesJournals: nothing to do") 793 err = g.flushJournalsUnwind(branch) 794 if err != nil { 795 log.Errorf("flushJournalsUnwind: %v", err) 796 } 797 return nil 798 } 799 800 // git add journals 801 commitMessage := "Flush vote journals.\n\n" 802 for _, v := range files { 803 err = g.gitAdd(g.unvetted, v) 804 if err != nil { 805 errUnwind = err 806 return err 807 } 808 809 s := strings.Split(v, string(os.PathSeparator)) 810 if len(s) == 0 { 811 commitMessage += "ERROR: " + v + "\n" 812 } else { 813 commitMessage += s[0] + "\n" 814 } 815 } 816 817 // git commit 818 err = g.gitCommit(g.unvetted, commitMessage) 819 if err != nil { 820 errUnwind = err 821 return err 822 } 823 824 // git rebase master 825 err = g.rebasePR(branch) 826 if err != nil { 827 errUnwind = err 828 return err 829 } 830 831 return nil 832 } 833 func (g *gitBackEnd) decredPluginJournalFlusher() { 834 // XXX make this a single PR instead of 2 to save some git time 835 err := g.flushCommentJournals() 836 if err != nil { 837 log.Errorf("decredPluginJournalFlusher: %v", err) 838 } 839 err = g.flushVoteJournals() 840 if err != nil { 841 log.Errorf("decredPluginVoteFlusher: %v", err) 842 } 843 } 844 845 func (g *gitBackEnd) pluginNewComment(payload string) (string, error) { 846 // XXX this should become part of some sort of context 847 fiJSON, ok := decredPluginSettings[decredPluginIdentity] 848 if !ok { 849 return "", fmt.Errorf("full identity not set") 850 } 851 fi, err := identity.UnmarshalFullIdentity([]byte(fiJSON)) 852 if err != nil { 853 return "", err 854 } 855 856 // Decode comment 857 comment, err := decredplugin.DecodeNewComment([]byte(payload)) 858 if err != nil { 859 return "", fmt.Errorf("DecodeNewComment: %v", err) 860 } 861 862 // Verify proposal exists, we can run this lockless 863 if !g.vettedPropExists(comment.Token) { 864 return "", fmt.Errorf("unknown proposal: %v", comment.Token) 865 } 866 867 // Do some cheap things before expensive calls 868 cfilename := pijoin(g.journals, comment.Token, 869 defaultCommentFilename) 870 if comment.ParentID == "" { 871 // Empty ParentID means comment 0 872 comment.ParentID = "0" 873 } 874 875 // Sign signature 876 r := fi.SignMessage([]byte(comment.Signature)) 877 receipt := hex.EncodeToString(r[:]) 878 879 // Create new comment id 880 cid, err := g.getNewCid(comment.Token) 881 if err != nil { 882 return "", fmt.Errorf("could not generate new comment id: %v", 883 err) 884 } 885 886 // Create Journal entry 887 c := decredplugin.Comment{ 888 Token: comment.Token, 889 ParentID: comment.ParentID, 890 Comment: comment.Comment, 891 Signature: comment.Signature, 892 PublicKey: comment.PublicKey, 893 CommentID: cid, 894 Receipt: receipt, 895 Timestamp: time.Now().Unix(), 896 } 897 blob, err := decredplugin.EncodeComment(c) 898 if err != nil { 899 return "", fmt.Errorf("EncodeComment: %v", err) 900 } 901 902 // Add comment to journal 903 err = g.journal.Journal(cfilename, string(journalAdd)+ 904 string(blob)) 905 if err != nil { 906 return "", fmt.Errorf("could not journal %v: %v", c.Token, err) 907 } 908 909 // Comment journal filename 910 flushFilename := pijoin(g.journals, comment.Token, 911 defaultCommentsFlushed) 912 913 // Cache comment 914 g.Lock() 915 916 // Mark comment journal dirty 917 _ = os.Remove(flushFilename) 918 919 // Remove from cash. 920 if _, ok := decredPluginCommentsCache[c.Token]; !ok { 921 decredPluginCommentsCache[c.Token] = 922 make(map[string]decredplugin.Comment) 923 } 924 _, ok = decredPluginCommentsCache[c.Token][c.CommentID] 925 if ok { 926 // Sanity 927 log.Errorf("comment should not have existed.") 928 } 929 decredPluginCommentsCache[c.Token][c.CommentID] = c 930 g.Unlock() 931 932 // Encode reply 933 ncr := decredplugin.NewCommentReply{ 934 CommentID: c.CommentID, 935 Receipt: c.Receipt, 936 Timestamp: c.Timestamp, 937 } 938 ncrb, err := decredplugin.EncodeNewCommentReply(ncr) 939 if err != nil { 940 return "", fmt.Errorf("EncodeNewCommentReply: %v", err) 941 } 942 943 // return success and encoded answer 944 return string(ncrb), nil 945 } 946 947 func (g *gitBackEnd) pluginCensorComment(payload string) (string, error) { 948 log.Tracef("pluginCensorComment") 949 950 // Check if journals were replayed 951 if !journalsReplayed { 952 return "", backend.ErrJournalsNotReplayed 953 } 954 955 // XXX this should become part of some sort of context 956 fiJSON, ok := decredPluginSettings[decredPluginIdentity] 957 if !ok { 958 return "", fmt.Errorf("full identity not set") 959 } 960 fi, err := identity.UnmarshalFullIdentity([]byte(fiJSON)) 961 if err != nil { 962 return "", fmt.Errorf("UnmarshalFullIdentity: %v", err) 963 } 964 965 // Decode censor comment 966 censor, err := decredplugin.DecodeCensorComment([]byte(payload)) 967 if err != nil { 968 return "", fmt.Errorf("DecodeCensorComment: %v", err) 969 } 970 971 // Verify proposal exists, we can run this lockless 972 if !g.vettedPropExists(censor.Token) { 973 return "", fmt.Errorf("unknown proposal: %v", censor.Token) 974 } 975 976 // Sign signature 977 r := fi.SignMessage([]byte(censor.Signature)) 978 receipt := hex.EncodeToString(r[:]) 979 980 // Comment journal filename 981 flushFilename := pijoin(g.journals, censor.Token, 982 defaultCommentsFlushed) 983 984 // Ensure proposal exists in comments cache 985 g.Lock() 986 987 // Mark comment journal dirty 988 _ = os.Remove(flushFilename) 989 990 // Verify cache 991 _, ok = decredPluginCommentsCache[censor.Token] 992 if !ok { 993 g.Unlock() 994 return "", fmt.Errorf("proposal not found %v", censor.Token) 995 } 996 997 // Ensure comment exists in comments cache and has not 998 // already been censored 999 c, ok := decredPluginCommentsCache[censor.Token][censor.CommentID] 1000 if !ok { 1001 g.Unlock() 1002 return "", fmt.Errorf("comment not found %v:%v", 1003 censor.Token, censor.CommentID) 1004 } 1005 if c.Censored { 1006 g.Unlock() 1007 return "", fmt.Errorf("comment already censored %v: %v", 1008 censor.Token, censor.CommentID) 1009 } 1010 1011 // Update comments cache 1012 oc := c 1013 c.Comment = "" 1014 c.Censored = true 1015 decredPluginCommentsCache[censor.Token][censor.CommentID] = c 1016 1017 g.Unlock() 1018 1019 // We create an unwind function that MUST be called from all error 1020 // paths. If everything works ok it is a no-op. 1021 unwind := func() { 1022 g.Lock() 1023 decredPluginCommentsCache[censor.Token][censor.CommentID] = oc 1024 g.Unlock() 1025 } 1026 1027 // Create Journal entry 1028 cc := decredplugin.CensorComment{ 1029 Token: censor.Token, 1030 CommentID: censor.CommentID, 1031 Reason: censor.Reason, 1032 Signature: censor.Signature, 1033 PublicKey: censor.PublicKey, 1034 Receipt: receipt, 1035 Timestamp: time.Now().Unix(), 1036 } 1037 blob, err := decredplugin.EncodeCensorComment(cc) 1038 if err != nil { 1039 unwind() 1040 return "", fmt.Errorf("EncodeCensorComment: %v", err) 1041 } 1042 1043 // Add censor comment to journal 1044 cfilename := pijoin(g.journals, censor.Token, 1045 defaultCommentFilename) 1046 err = g.journal.Journal(cfilename, string(journalDel)+string(blob)) 1047 if err != nil { 1048 unwind() 1049 return "", fmt.Errorf("could not journal %v: %v", cc.Token, err) 1050 } 1051 1052 // Encode reply 1053 ccr := decredplugin.CensorCommentReply{ 1054 Receipt: cc.Receipt, 1055 } 1056 ccrb, err := decredplugin.EncodeCensorCommentReply(ccr) 1057 if err != nil { 1058 unwind() 1059 return "", fmt.Errorf("EncodeCensorCommentReply: %v", err) 1060 } 1061 1062 return string(ccrb), nil 1063 } 1064 1065 // encodeGetCommentsReply converts a comment map into a JSON string that can be 1066 // returned as a decredplugin reply. If the comment map is nil it returns a 1067 // valid empty reply structure. 1068 func encodeGetCommentsReply(cm map[string]decredplugin.Comment) (string, error) { 1069 if cm == nil { 1070 cm = make(map[string]decredplugin.Comment) 1071 } 1072 1073 // Encode reply 1074 gcr := decredplugin.GetCommentsReply{ 1075 Comments: make([]decredplugin.Comment, 0, len(cm)), 1076 } 1077 for _, v := range cm { 1078 gcr.Comments = append(gcr.Comments, v) 1079 } 1080 1081 gcrb, err := decredplugin.EncodeGetCommentsReply(gcr) 1082 if err != nil { 1083 return "", fmt.Errorf("encodeGetCommentsReply: %v", err) 1084 } 1085 1086 return string(gcrb), nil 1087 } 1088 1089 // replayComments replay the comments for a given proposal 1090 // the proposal is matched by the provided token 1091 // this function can be called WITHOUT the lock held 1092 func (g *gitBackEnd) replayComments(token string) (map[string]decredplugin.Comment, error) { 1093 log.Debugf("replayComments %s", token) 1094 // Verify proposal exists, we can run this lockless 1095 if !g.vettedPropExists(token) { 1096 return nil, nil 1097 } 1098 1099 // Do some cheap things before expensive calls 1100 cfilename := pijoin(g.journals, token, 1101 defaultCommentFilename) 1102 1103 // Replay journal 1104 err := g.journal.Open(cfilename) 1105 if err != nil { 1106 if !os.IsNotExist(err) { 1107 return nil, fmt.Errorf("journal.Open: %v", err) 1108 } 1109 return nil, nil 1110 } 1111 defer func() { 1112 err = g.journal.Close(cfilename) 1113 if err != nil { 1114 log.Errorf("journal.Close: %v", err) 1115 } 1116 }() 1117 1118 comments := make(map[string]decredplugin.Comment) 1119 1120 for { 1121 err = g.journal.Replay(cfilename, func(s string) error { 1122 ss := bytes.NewReader([]byte(s)) 1123 d := json.NewDecoder(ss) 1124 1125 // Decode action 1126 var action JournalAction 1127 err = d.Decode(&action) 1128 if err != nil { 1129 return fmt.Errorf("journal action: %v", err) 1130 } 1131 1132 switch action.Action { 1133 case journalActionAdd: 1134 var c decredplugin.Comment 1135 err = d.Decode(&c) 1136 if err != nil { 1137 return fmt.Errorf("journal add: %v", 1138 err) 1139 } 1140 1141 // Sanity 1142 if _, ok := comments[c.CommentID]; ok { 1143 log.Errorf("duplicate comment id %v", 1144 c.CommentID) 1145 } 1146 comments[c.CommentID] = c 1147 1148 case journalActionDel: 1149 var cc decredplugin.CensorComment 1150 err = d.Decode(&cc) 1151 if err != nil { 1152 return fmt.Errorf("journal censor: %v", 1153 err) 1154 } 1155 1156 // Ensure comment has been added 1157 c, ok := comments[cc.CommentID] 1158 if !ok { 1159 // Complain but we can't do anything 1160 // about it. Can't return error or we'd 1161 // abort journal loop. 1162 log.Errorf("comment not found: %v", 1163 cc.CommentID) 1164 return nil 1165 } 1166 1167 // Delete comment 1168 c.Comment = "" 1169 c.Censored = true 1170 comments[cc.CommentID] = c 1171 1172 default: 1173 return fmt.Errorf("invalid action: %v", 1174 action.Action) 1175 } 1176 return nil 1177 }) 1178 if errors.Is(err, io.EOF) { 1179 break 1180 } else if err != nil { 1181 return nil, err 1182 } 1183 } 1184 1185 g.Lock() 1186 decredPluginCommentsCache[token] = comments 1187 g.Unlock() 1188 1189 return comments, nil 1190 } 1191 1192 func (g *gitBackEnd) pluginGetComments(payload string) (string, error) { 1193 log.Tracef("pluginGetComments") 1194 1195 // Check if journals were replayed 1196 if !journalsReplayed { 1197 return "", backend.ErrJournalsNotReplayed 1198 } 1199 1200 // Decode comment 1201 gc, err := decredplugin.DecodeGetComments([]byte(payload)) 1202 if err != nil { 1203 return "", fmt.Errorf("DecodeGetComments: %v", err) 1204 } 1205 1206 g.Lock() 1207 comments := decredPluginCommentsCache[gc.Token] 1208 g.Unlock() 1209 return encodeGetCommentsReply(comments) 1210 }