github.com/decred/politeia@v1.4.0/politeiawww/cmd/pictl/cmdseedproposals.go (about) 1 // Copyright (c) 2020-2021 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 main 6 7 import ( 8 "encoding/hex" 9 "fmt" 10 "math/rand" 11 "strconv" 12 "time" 13 14 pi "github.com/decred/politeia/politeiad/plugins/pi" 15 cmv1 "github.com/decred/politeia/politeiawww/api/comments/v1" 16 rcv1 "github.com/decred/politeia/politeiawww/api/records/v1" 17 "github.com/decred/politeia/util" 18 ) 19 20 // cmdSeedProposals seeds the backend with randomly generated users, proposals, 21 // comments, and comment votes. 22 type cmdSeedProposals struct { 23 Args struct { 24 AdminEmail string `positional-arg-name:"adminemail" required:"true"` 25 AdminPassword string `positional-arg-name:"adminpassword" required:"true"` 26 } `positional-args:"true"` 27 28 // Options to adjust the quantity being seeded. Default values are 29 // used when these flags are not provided. Pointers are used when 30 // a value of 0 is allowed. 31 Users uint32 `long:"users" optional:"true"` 32 Proposals uint32 `long:"proposals" optional:"true"` 33 Comments *uint32 `long:"comments" optional:"true"` 34 CommentVotes *uint32 `long:"commentvotes" optional:"true"` 35 ProposalStatus string `long:"proposalstatus" optional:"true"` 36 37 // IncludeImages is used to include image attachments in the 38 // proposal submissions. Each proposal will contain a random number 39 // of randomly generated images when this flag is used. 40 IncludeImages bool `long:"includeimages"` 41 } 42 43 // Execute executes the cmdSeedProposals command. 44 // 45 // This function satisfies the go-flags Commander interface. 46 func (c *cmdSeedProposals) Execute(args []string) error { 47 // Setup default parameters 48 var ( 49 userCount uint32 = 10 50 proposalCount uint32 = 25 51 commentsPerProposal uint32 = 10 52 commentSize uint32 = 32 // In characters 53 commentVotesPerProposal uint32 = 25 54 55 includeImages = c.IncludeImages 56 57 proposalStatus *pi.PropStatusT 58 ) 59 if c.Users != 0 { 60 userCount = c.Users 61 } 62 if c.Proposals != 0 { 63 proposalCount = c.Proposals 64 } 65 if c.Comments != nil { 66 commentsPerProposal = *c.Comments 67 } 68 if c.CommentVotes != nil { 69 commentVotesPerProposal = *c.CommentVotes 70 } 71 if c.ProposalStatus != "" { 72 s := parseProposalStatus(c.ProposalStatus) 73 if s == pi.PropStatusInvalid { 74 return fmt.Errorf("invalid proposal status '%v'", c.ProposalStatus) 75 } 76 proposalStatus = &s 77 } 78 79 // We don't want the output of individual commands printed. 80 cfg.Verbose = false 81 cfg.RawJSON = false 82 cfg.Silent = true 83 84 // User count must be at least 2. A user cannot upvote their own 85 // comments so we need at least 1 user to make comments and a 86 // second user to upvote the comments. 87 if userCount < 2 { 88 return fmt.Errorf("user count must be >= 2") 89 } 90 91 // Verify admin login credentials 92 admin := user{ 93 Email: c.Args.AdminEmail, 94 Password: c.Args.AdminPassword, 95 } 96 err := userLogin(admin) 97 if err != nil { 98 return fmt.Errorf("failed to login admin: %v", err) 99 } 100 lr, err := client.Me() 101 if err != nil { 102 return err 103 } 104 if !lr.IsAdmin { 105 return fmt.Errorf("provided user is not an admin") 106 } 107 admin.Username = lr.Username 108 109 // Verify paywall is disabled 110 policyWWW, err := client.Policy() 111 if err != nil { 112 return err 113 } 114 if policyWWW.PaywallEnabled { 115 return fmt.Errorf("paywall is not disabled") 116 } 117 118 // Log start time 119 fmt.Printf("Start time: %v\n", dateAndTimeFromUnix(time.Now().Unix())) 120 121 // Setup users 122 users := make([]user, 0, userCount) 123 for i := 0; i < int(userCount); i++ { 124 log := fmt.Sprintf("Creating user %v/%v", i+1, userCount) 125 printInPlace(log) 126 127 u, err := userNewRandom() 128 if err != nil { 129 return err 130 } 131 132 users = append(users, *u) 133 } 134 fmt.Printf("\n") 135 136 // Setup proposals 137 var ( 138 // statuses specifies the statuses that are rotated through when 139 // proposals are being submitted. We can increase the proption of 140 // proposals that are a specific status by increasing the number 141 // of times the status occurs in this array. 142 statuses = []pi.PropStatusT{ 143 pi.PropStatusUnderReview, 144 pi.PropStatusUnderReview, 145 pi.PropStatusUnderReview, 146 pi.PropStatusUnderReview, 147 pi.PropStatusUnvetted, 148 pi.PropStatusUnvettedCensored, 149 pi.PropStatusCensored, 150 pi.PropStatusAbandoned, 151 } 152 153 // These are used to track the number of proposals that are 154 // created for each status. 155 countUnvetted int 156 countUnvettedCensored int 157 countUnderReview int 158 countCensored int 159 countAbandoned int 160 161 // public is used to aggregate the tokens of public proposals. 162 // These will be used when we add comments to the proposals. 163 public = make([]string, 0, proposalCount) 164 ) 165 for i := 0; i < int(proposalCount); i++ { 166 // Select a random user 167 r := rand.Intn(len(users)) 168 u := users[r] 169 170 // Rotate through the statuses 171 s := statuses[i%len(statuses)] 172 173 // Override the default proposal status if one was provided 174 if proposalStatus != nil { 175 s = *proposalStatus 176 } 177 178 log := fmt.Sprintf("Submitting proposal %v/%v: %v", 179 i+1, proposalCount, s) 180 printInPlace(log) 181 182 // Create proposal 183 opts := &proposalOpts{ 184 Random: true, 185 RandomImages: includeImages, 186 } 187 switch s { 188 case pi.PropStatusUnvetted: 189 _, err = proposalUnreviewed(u, opts) 190 if err != nil { 191 return err 192 } 193 countUnvetted++ 194 case pi.PropStatusUnvettedCensored: 195 _, err = proposalUnvettedCensored(u, admin, opts) 196 if err != nil { 197 return err 198 } 199 countUnvettedCensored++ 200 case pi.PropStatusUnderReview: 201 r, err := proposalPublic(u, admin, opts) 202 if err != nil { 203 return err 204 } 205 countUnderReview++ 206 public = append(public, r.CensorshipRecord.Token) 207 case pi.PropStatusCensored: 208 _, err = proposalVettedCensored(u, admin, opts) 209 if err != nil { 210 return err 211 } 212 countCensored++ 213 case pi.PropStatusAbandoned: 214 _, err = proposalAbandoned(u, admin, opts) 215 if err != nil { 216 return err 217 } 218 countAbandoned++ 219 default: 220 return fmt.Errorf("this command does not "+ 221 "support the proposal status '%v'", s) 222 } 223 } 224 fmt.Printf("\n") 225 226 // Verify proposal inventory 227 var ( 228 statusesUnvetted = map[rcv1.RecordStatusT]int{ 229 rcv1.RecordStatusUnreviewed: countUnvetted, 230 rcv1.RecordStatusCensored: countUnvettedCensored, 231 } 232 233 statusesVetted = map[rcv1.RecordStatusT]int{ 234 rcv1.RecordStatusPublic: countUnderReview, 235 rcv1.RecordStatusCensored: countCensored, 236 rcv1.RecordStatusArchived: countAbandoned, 237 } 238 ) 239 for status, count := range statusesUnvetted { 240 // Tally up how many records are in the inventory for each 241 // status. 242 var tally int 243 var page uint32 = 1 244 for { 245 log := fmt.Sprintf("Verifying unvetted inv for status %v, page %v", 246 rcv1.RecordStatuses[status], page) 247 printInPlace(log) 248 249 tokens, err := invUnvetted(admin, status, page) 250 if err != nil { 251 return err 252 } 253 if len(tokens) == 0 { 254 // We've reached the end of the inventory 255 break 256 } 257 tally += len(tokens) 258 page++ 259 } 260 fmt.Printf("\n") 261 262 // The count might be more than the tally if there were already 263 // proposals in the inventory prior to running this command. The 264 // tally should never be less than the count. 265 if tally < count { 266 return fmt.Errorf("unexpected number of proposals in inventory "+ 267 "for status %v: got %v, want >=%v", rcv1.RecordStatuses[status], 268 tally, count) 269 } 270 } 271 for status, count := range statusesVetted { 272 // Tally up how many records are in the inventory for each 273 // status. 274 var tally int 275 var page uint32 = 1 276 for { 277 log := fmt.Sprintf("Verifying vetted inv for status %v, page %v", 278 rcv1.RecordStatuses[status], page) 279 printInPlace(log) 280 281 tokens, err := inv(rcv1.RecordStateVetted, status, page) 282 if err != nil { 283 return err 284 } 285 if len(tokens) == 0 { 286 // We've reached the end of the inventory 287 break 288 } 289 tally += len(tokens) 290 page++ 291 } 292 fmt.Printf("\n") 293 294 // The count might be more than the tally if there were already 295 // proposals in the inventory prior to running this command. The 296 // tally should never be less than the count. 297 if tally < count { 298 return fmt.Errorf("unexpected number of proposals in inventory "+ 299 "for status %v: got %v, want >=%v", rcv1.RecordStatuses[status], 300 tally, count) 301 } 302 } 303 304 // Users cannot vote on their own comment. Divide the user into two 305 // groups. Group 1 will create the comments. Group 2 will vote on 306 // the comments. 307 users1 := users[:len(users)/2] 308 users2 := users[len(users)/2:] 309 310 // Reverse the ordering of the public records so that comments are 311 // added to the most recent record first. 312 reverse := make([]string, 0, len(public)) 313 for i := len(public) - 1; i >= 0; i-- { 314 reverse = append(reverse, public[i]) 315 } 316 public = reverse 317 318 // Setup comments 319 for i, token := range public { 320 for j := 0; j < int(commentsPerProposal); j++ { 321 log := fmt.Sprintf("Submitting comments for proposal %v/%v, "+ 322 "comment %v/%v", i+1, len(public), j+1, commentsPerProposal) 323 printInPlace(log) 324 325 // Login a new, random user every 10 comments. Selecting a 326 // new user every comment is too slow. 327 if j%10 == 0 { 328 // Select a random user 329 r := rand.Intn(len(users1)) 330 u := users1[r] 331 332 // Login user 333 userLogin(u) 334 } 335 336 // Every 5th comment should be the start of a comment thread, not 337 // a reply. All other comments should be replies to a random 338 // existing comment. 339 var parentID uint32 340 switch { 341 case j%5 == 0: 342 // This should be a parent comment. Keep the parent ID as 0. 343 default: 344 // Reply to a random comment 345 parentID = uint32(rand.Intn(j + 1)) 346 } 347 348 // Create random comment 349 b, err := util.Random(int(commentSize) / 2) 350 if err != nil { 351 return err 352 } 353 comment := hex.EncodeToString(b) 354 355 // Submit comment 356 c := cmdCommentNew{} 357 c.Args.Token = token 358 c.Args.Comment = comment 359 c.Args.ParentID = parentID 360 err = c.Execute(nil) 361 if err != nil { 362 return fmt.Errorf("cmdCommentNew: %v", err) 363 } 364 } 365 } 366 fmt.Printf("\n") 367 368 // Setup comment votes 369 for i, token := range public { 370 // Get the number of comments this proposal has 371 count, err := commentCountForRecord(token) 372 if err != nil { 373 return err 374 } 375 376 // We iterate through the users and comments sequentially. Trying 377 // to vote on comments randomly can cause max vote changes 378 // exceeded errors. 379 var ( 380 userIdx int 381 needToLogin bool = true 382 commentID uint32 = 1 383 ) 384 for j := 0; j < int(commentVotesPerProposal); j++ { 385 log := fmt.Sprintf("Submitting comment votes for proposal %v/%v, "+ 386 "comment %v/%v", i+1, len(public), j+1, commentVotesPerProposal) 387 printInPlace(log) 388 389 // Setup the comment ID and the user 390 if commentID > count { 391 // We've reached the end of the comments. Start back over 392 // with a different user. 393 userIdx++ 394 commentID = 1 395 396 userLogout() 397 needToLogin = true 398 } 399 if userIdx == len(users2) { 400 // We've reached the end of the users. Start back over. 401 userIdx = 0 402 userLogout() 403 needToLogin = true 404 } 405 406 u := users2[userIdx] 407 if needToLogin { 408 userLogin(u) 409 needToLogin = false 410 } 411 412 // Select a random vote preference 413 var vote string 414 if rand.Intn(100)%2 == 0 { 415 vote = strconv.Itoa(int(cmv1.VoteUpvote)) 416 } else { 417 vote = strconv.Itoa(int(cmv1.VoteDownvote)) 418 } 419 420 // Cast comment vote 421 c := cmdCommentVote{} 422 c.Args.Token = token 423 c.Args.CommentID = commentID 424 c.Args.Vote = vote 425 err = c.Execute(nil) 426 if err != nil { 427 return err 428 } 429 430 // Increment comment ID 431 commentID++ 432 } 433 } 434 fmt.Printf("\n") 435 436 ts := dateAndTimeFromUnix(time.Now().Unix()) 437 fmt.Printf("Done!\n") 438 fmt.Printf("Stop time : %v\n", ts) 439 fmt.Printf("Users : %v\n", userCount) 440 fmt.Printf("Proposals : %v\n", proposalCount) 441 fmt.Printf("Comments per proposal : %v\n", commentsPerProposal) 442 fmt.Printf("Comment votes per proposal: %v\n", commentVotesPerProposal) 443 444 return nil 445 } 446 447 // inv returns a page of tokens for a record status. 448 func inv(state rcv1.RecordStateT, status rcv1.RecordStatusT, page uint32) ([]string, error) { 449 // Setup command 450 c := cmdProposalInv{} 451 c.Args.State = strconv.Itoa(int(state)) 452 c.Args.Status = strconv.Itoa(int(status)) 453 c.Args.Page = page 454 455 // Get inventory 456 ir, err := proposalInv(&c) 457 if err != nil { 458 return nil, fmt.Errorf("cmdProposalInv: %v", err) 459 } 460 461 // Unpack reply 462 s := rcv1.RecordStatuses[status] 463 var tokens []string 464 switch state { 465 case rcv1.RecordStateUnvetted: 466 tokens = ir.Unvetted[s] 467 case rcv1.RecordStateVetted: 468 tokens = ir.Vetted[s] 469 } 470 471 return tokens, nil 472 } 473 474 // invUnvetted returns a page of tokens for an unvetted record status. 475 // 476 // This function returns with the admin logged out. 477 func invUnvetted(admin user, status rcv1.RecordStatusT, page uint32) ([]string, error) { 478 // Login admin 479 err := userLogin(admin) 480 if err != nil { 481 return nil, err 482 } 483 484 // Get a page of tokens 485 tokens, err := inv(rcv1.RecordStateUnvetted, status, page) 486 if err != nil { 487 return nil, err 488 } 489 490 // Logout admin 491 err = userLogout() 492 if err != nil { 493 return nil, err 494 } 495 496 return tokens, nil 497 } 498 499 // commentCountForRecord returns the number of comments that have been made on 500 // a record. 501 func commentCountForRecord(token string) (uint32, error) { 502 c := cmdCommentCount{} 503 c.Args.Tokens = []string{token} 504 counts, err := commentCount(&c) 505 if err != nil { 506 return 0, fmt.Errorf("cmdCommentCount: %v", err) 507 } 508 count, ok := counts[token] 509 if !ok { 510 return 0, fmt.Errorf("cmdCommentCount: record not found %v", token) 511 } 512 return count, nil 513 } 514 515 // seedProposalsHelpMsg is the printed to stdout by the help command. 516 const seedProposalsHelpMsg = `seedproposals [flags] "adminemail" "adminpassword" 517 518 Seed the backend with randomly generated users, proposals, comments, and 519 comment votes. 520 521 Arguments: 522 1. adminemail (string, required) Email for admin account. 523 2. adminpassword (string, required) Password for admin account. 524 525 Flags: 526 --users (uint32) Number of users to seed the backend with. 527 (default: 10) 528 529 --proposals (uint32) Number of proposals to seed the backend with. 530 (default: 25) 531 532 --comments (uint32) Number of comments that will be made on each 533 proposal. (default: 10) 534 535 --commentvotes (uint32) Number of comment upvotes/downvotes that will be 536 cast on each proposal. (default: 25) 537 538 --proposalstatus (string) Proposal status that all of the seeded proposals 539 will be set to. 540 Valid options: unvetted, unvetted-censored, 541 under-review, censored, or abandoned. 542 By default, the seeded proposals will cycle through 543 all of these statuses. 544 545 --includeimages (bool) Include images in proposal submissions. This will 546 substantially increase the size of the proposal 547 payload. 548 `