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  `