github.com/decred/politeia@v1.4.0/politeiawww/cmd/pictl/cmdproposaledit.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/base64" 9 "encoding/hex" 10 "encoding/json" 11 "fmt" 12 "math/rand" 13 "time" 14 15 "github.com/decred/politeia/politeiad/api/v1/mime" 16 piv1 "github.com/decred/politeia/politeiawww/api/pi/v1" 17 rcv1 "github.com/decred/politeia/politeiawww/api/records/v1" 18 pclient "github.com/decred/politeia/politeiawww/client" 19 "github.com/decred/politeia/politeiawww/cmd/shared" 20 "github.com/decred/politeia/util" 21 ) 22 23 // cmdProposalEdit edits an existing proposal. 24 type cmdProposalEdit struct { 25 Args struct { 26 Token string `positional-arg-name:"token" required:"true"` 27 IndexFile string `positional-arg-name:"indexfile"` 28 Attachments []string `positional-arg-name:"attachmets"` 29 } `positional-args:"true" optional:"true"` 30 31 // UseMD is a flag that is intended to make editing proposal 32 // metadata easier by using exisiting proposal metadata values 33 // instead of having to pass in specific values. 34 UseMD bool `long:"usemd" optional:"true"` 35 36 // Metadata fields that can be set by the user 37 Name string `long:"name" optional:"true"` 38 LinkTo string `long:"linkto" optional:"true"` 39 LinkBy string `long:"linkby" optional:"true"` 40 Amount uint64 `long:"amount" optional:"true"` 41 StartDate string `long:"startdate" optional:"true"` 42 EndDate string `long:"enddate" optional:"true"` 43 Domain string `long:"domain" optional:"true"` 44 45 // RFP is a flag that is intended to make submitting an RFP easier 46 // by calculating and inserting a linkby timestamp automatically 47 // instead of having to pass in a timestamp using the --linkby 48 // flag. 49 RFP bool `long:"rfp" optional:"true"` 50 51 // Random generates a random index file. The IndexFile argument is 52 // not allowed when using this flag. 53 Random bool `long:"random" optional:"true"` 54 55 // RandomImages generates random image attachments. The Attachments 56 // argument is not allowed when using this flag. 57 RandomImages bool `long:"randomimages" optional:"true"` 58 } 59 60 // Execute executes the cmdProposalEdit command. 61 // 62 // This function satisfies the go-flags Commander interface. 63 func (c *cmdProposalEdit) Execute(args []string) error { 64 _, err := proposalEdit(c) 65 if err != nil { 66 return err 67 } 68 return nil 69 } 70 71 // proposalEdit edits a proposal. This function has been pulled out of the 72 // Execute method so that is can be used in the test commands. 73 func proposalEdit(c *cmdProposalEdit) (*rcv1.Record, error) { 74 // Unpack args 75 token := c.Args.Token 76 indexFile := c.Args.IndexFile 77 attachments := c.Args.Attachments 78 79 // Verify args and flags 80 switch { 81 case !c.Random && indexFile == "": 82 return nil, fmt.Errorf("index file not found; you must either " + 83 "provide an index.md file or use --random") 84 85 case c.RandomImages && len(attachments) > 0: 86 return nil, fmt.Errorf("you cannot provide attachment files and " + 87 "use the --randomimages flag at the same time") 88 89 case c.RFP && c.LinkBy != "": 90 return nil, fmt.Errorf("you cannot use both the --rfp and --linkby " + 91 "flags at the same time") 92 } 93 94 // Check for user identity. A user identity is required to sign 95 // the proposal files. 96 if cfg.Identity == nil { 97 return nil, shared.ErrUserIdentityNotFound 98 } 99 100 // Setup client 101 opts := pclient.Opts{ 102 HTTPSCert: cfg.HTTPSCert, 103 Cookies: cfg.Cookies, 104 HeaderCSRF: cfg.CSRF, 105 Verbose: cfg.Verbose, 106 RawJSON: cfg.RawJSON, 107 } 108 pc, err := pclient.New(cfg.Host, opts) 109 if err != nil { 110 return nil, err 111 } 112 113 // Get the pi policy. It contains the proposal requirements. 114 pr, err := pc.PiPolicy() 115 if err != nil { 116 return nil, err 117 } 118 119 // Setup proposal files 120 indexFileSize := 10000 // In bytes 121 var files []rcv1.File 122 switch { 123 case c.Random && c.RandomImages: 124 // Create a random index file and random attachments 125 files, err = proposalFilesRandom(indexFileSize, 126 int(pr.ImageFileCountMax)) 127 if err != nil { 128 return nil, err 129 } 130 case c.Random: 131 // Create a random index file 132 files, err = proposalFilesRandom(indexFileSize, 0) 133 if err != nil { 134 return nil, err 135 } 136 default: 137 // Read files from disk 138 files, err = proposalFilesFromDisk(indexFile, attachments) 139 if err != nil { 140 return nil, err 141 } 142 } 143 144 // Get current proposal if we are using the existing metadata 145 var curr *rcv1.Record 146 if c.UseMD { 147 d := rcv1.Details{ 148 Token: token, 149 } 150 curr, err = pc.RecordDetails(d) 151 if err != nil { 152 return nil, err 153 } 154 } 155 156 // Setup proposal metadata 157 switch { 158 // Use existing proposal metadata. 159 case c.UseMD: 160 pm, err := pclient.ProposalMetadataDecode(curr.Files) 161 if err != nil { 162 return nil, err 163 } 164 c.Name = pm.Name 165 c.Amount = pm.Amount 166 c.StartDate = dateAndTimeFromUnix(pm.StartDate) 167 c.EndDate = dateAndTimeFromUnix(pm.EndDate) 168 c.Domain = pm.Domain 169 // Set random metadata values in case no specific value is provided. 170 case c.Random: 171 // Set a random proposal name if not provided. 172 if c.Name == "" { 173 r, err := util.Random(int(pr.NameLengthMin)) 174 if err != nil { 175 return nil, err 176 } 177 c.Name = hex.EncodeToString(r) 178 } 179 // Set proposal domain if not provided 180 if c.Domain == "" { 181 // Pick random domain from the pi policy domains. 182 randomIndex := rand.Intn(len(pr.Domains)) 183 c.Domain = pr.Domains[randomIndex] 184 } 185 // In case of RFP no need to populate startdate, enddate & 186 // amount metadata fields. 187 if !c.RFP && c.LinkBy == "" { 188 // Set start date one month from now if not provided 189 if c.StartDate == "" { 190 c.StartDate = dateFromUnix(defaultStartDate) 191 } 192 // Set end date 4 months from now if not provided 193 if c.EndDate == "" { 194 c.EndDate = dateFromUnix(defaultEndDate) 195 } 196 if c.Amount == 0 { 197 c.Amount = defaultAmount 198 } 199 } 200 } 201 202 pm := piv1.ProposalMetadata{ 203 Name: c.Name, 204 Amount: c.Amount, 205 Domain: c.Domain, 206 } 207 if c.StartDate != "" { 208 // Parse start & end dates string timestamps. 209 pm.StartDate, err = unixFromDate(c.StartDate) 210 if err != nil { 211 return nil, err 212 } 213 } 214 if c.EndDate != "" { 215 pm.EndDate, err = unixFromDate(c.EndDate) 216 if err != nil { 217 return nil, err 218 } 219 } 220 221 pmb, err := json.Marshal(pm) 222 if err != nil { 223 return nil, err 224 } 225 files = append(files, rcv1.File{ 226 Name: piv1.FileNameProposalMetadata, 227 MIME: mime.DetectMimeType(pmb), 228 Digest: hex.EncodeToString(util.Digest(pmb)), 229 Payload: base64.StdEncoding.EncodeToString(pmb), 230 }) 231 232 // Setup vote metadata 233 var linkBy int64 234 switch { 235 case c.UseMD: 236 // Use existing vote metadata values 237 vm, err := pclient.VoteMetadataDecode(curr.Files) 238 if err != nil { 239 return nil, err 240 } 241 linkBy = vm.LinkBy 242 c.LinkTo = vm.LinkTo 243 case c.RFP: 244 // Set linkby to a month from now 245 linkBy = time.Now().Add(time.Hour * 24 * 30).Unix() 246 case c.LinkBy != "": 247 // Parse the provided linkby 248 d, err := time.ParseDuration(c.LinkBy) 249 if err != nil { 250 return nil, fmt.Errorf("unable to parse linkby: %v", err) 251 } 252 linkBy = time.Now().Add(d).Unix() 253 } 254 if linkBy != 0 || c.LinkTo != "" { 255 vm := piv1.VoteMetadata{ 256 LinkTo: c.LinkTo, 257 LinkBy: linkBy, 258 } 259 vmb, err := json.Marshal(vm) 260 if err != nil { 261 return nil, err 262 } 263 files = append(files, rcv1.File{ 264 Name: piv1.FileNameVoteMetadata, 265 MIME: mime.DetectMimeType(vmb), 266 Digest: hex.EncodeToString(util.Digest(vmb)), 267 Payload: base64.StdEncoding.EncodeToString(vmb), 268 }) 269 } 270 271 // Edit record 272 sig, err := signedMerkleRoot(files, cfg.Identity) 273 if err != nil { 274 return nil, err 275 } 276 e := rcv1.Edit{ 277 Token: token, 278 Files: files, 279 PublicKey: cfg.Identity.Public.String(), 280 Signature: sig, 281 } 282 er, err := pc.RecordEdit(e) 283 if err != nil { 284 return nil, err 285 } 286 287 // Verify record 288 vr, err := client.Version() 289 if err != nil { 290 return nil, err 291 } 292 err = pclient.RecordVerify(er.Record, vr.PubKey) 293 if err != nil { 294 return nil, fmt.Errorf("unable to verify record: %v", err) 295 } 296 297 // Print proposal to stdout 298 printf("Proposal editted\n") 299 err = printProposal(er.Record) 300 if err != nil { 301 return nil, err 302 } 303 304 return &er.Record, nil 305 } 306 307 // proposalEditHelpMsg is the printed to stdout by the help command. 308 const proposalEditHelpMsg = `editproposal [flags] "token" "indexfile" "attachments" 309 310 Edit an existing proposal. 311 312 A proposal can be submitted as an RFP (Request for Proposals) by using either 313 the --rfp flag or by manually specifying a link by deadline using the --linkby 314 flag. Only one of these flags can be used at a time. 315 316 A proposal can be submitted as an RFP submission by using the --linkto flag 317 to link to and an existing RFP proposal. 318 319 Arguments: 320 1. token (string, required) Proposal censorship token. 321 2. indexfile (string, optional) Index file. 322 3. attachments (string, optional) Attachment files. 323 324 Flags: 325 --usemd (bool) Use the existing proposal metadata. 326 327 --name (string) Name of the proposal. 328 329 --amount (int) Funding amount in cents. 330 331 --startdate (string) Start Date, Format: "01/02/2006" 332 333 --enddate (string) End Date, Format: "01/02/2006" 334 335 --domain (string) Default supported domains: ["development", 336 "research", "design", "marketing"] 337 338 --linkto (string) Token of an existing public proposal to link to. 339 340 --linkby (string) Make the proposal and RFP by setting the linkby 341 deadline. Other proposals must be entered as RFP 342 submissions by this linkby deadline. The provided 343 string should be a duration that will be added onto 344 the current time. Valid duration units are: 345 s (seconds), m (minutes), h (hours). 346 347 --rfp (bool) Make the proposal an RFP by setting the linkby to one 348 month from the current time. This is intended to be 349 used in place of --linkby. 350 351 --random (bool) Generate random proposal data, not including 352 attachments. The indexFile argument is not allowed 353 when using this flag. 354 355 --randomimages (bool) Generate random attachments. The attachments argument 356 is not allowed when using this flag. 357 `