github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/pi/pi.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 pi
     6  
     7  import (
     8  	"container/list"
     9  	"encoding/json"
    10  	"os"
    11  	"path/filepath"
    12  	"regexp"
    13  	"strconv"
    14  
    15  	"github.com/decred/politeia/politeiad/api/v1/identity"
    16  	backend "github.com/decred/politeia/politeiad/backendv2"
    17  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins"
    18  	"github.com/decred/politeia/politeiad/plugins/pi"
    19  	"github.com/decred/politeia/util"
    20  	"github.com/pkg/errors"
    21  )
    22  
    23  var (
    24  	_ plugins.PluginClient = (*piPlugin)(nil)
    25  )
    26  
    27  // piPlugin is the tstore backend implementation of the pi plugin. The pi
    28  // plugin extends a record with functionality specific to the decred proposal
    29  // system.
    30  //
    31  // piPlugin satisfies the plugins PluginClient interface.
    32  type piPlugin struct {
    33  	backend backend.Backend
    34  	tstore  plugins.TstoreClient
    35  
    36  	// statuses is a lazy loaded, memory cache that is used to improve the
    37  	// performance of determining the proposal statuses at runtime.
    38  	statuses proposalStatuses
    39  
    40  	// dataDir is the pi plugin data directory. The only data that is
    41  	// stored here is cached data that can be re-created at any time
    42  	// by walking the trillian trees.
    43  	dataDir string
    44  
    45  	// identity contains the full identity that the plugin uses to
    46  	// create receipts, i.e. signatures of user provided data that
    47  	// prove the backend received and processed a plugin command.
    48  	identity *identity.FullIdentity
    49  
    50  	// Plugin settings
    51  	textFileCountMax             uint32
    52  	textFileSizeMax              uint32 // In bytes
    53  	imageFileCountMax            uint32
    54  	imageFileSizeMax             uint32 // In bytes
    55  	titleSupportedChars          string // JSON encoded []string
    56  	titleLengthMin               uint32 // In characters
    57  	titleLengthMax               uint32 // In characters
    58  	titleRegexp                  *regexp.Regexp
    59  	proposalAmountMin            uint64 // In cents
    60  	proposalAmountMax            uint64 // In cents
    61  	proposalStartDateMin         int64  // Seconds from current time
    62  	proposalEndDateMax           int64  // Seconds from current time
    63  	proposalDomainsEncoded       string // JSON encoded []string
    64  	proposalDomains              map[string]struct{}
    65  	billingStatusChangesMax      uint32
    66  	summariesPageSize            uint32
    67  	billingStatusChangesPageSize uint32
    68  }
    69  
    70  // Setup performs any plugin setup that is required.
    71  //
    72  // This function satisfies the plugins PluginClient interface.
    73  func (p *piPlugin) Setup() error {
    74  	log.Tracef("pi Setup")
    75  
    76  	return nil
    77  }
    78  
    79  // Cmd executes a plugin command.
    80  //
    81  // This function satisfies the plugins PluginClient interface.
    82  func (p *piPlugin) Cmd(token []byte, cmd, payload string) (string, error) {
    83  	log.Tracef("pi Cmd: %x %v %v", token, cmd, payload)
    84  
    85  	switch cmd {
    86  	case pi.CmdSetBillingStatus:
    87  		return p.cmdSetBillingStatus(token, payload)
    88  	case pi.CmdSummary:
    89  		return p.cmdSummary(token)
    90  	case pi.CmdBillingStatusChanges:
    91  		return p.cmdBillingStatusChanges(token)
    92  	}
    93  
    94  	return "", backend.ErrPluginCmdInvalid
    95  }
    96  
    97  // Hook executes a plugin hook.
    98  //
    99  // This function satisfies the plugins PluginClient interface.
   100  func (p *piPlugin) Hook(h plugins.HookT, payload string) error {
   101  	log.Tracef("pi Hook: %v", plugins.Hooks[h])
   102  
   103  	switch h {
   104  	case plugins.HookTypeNewRecordPre:
   105  		return p.hookNewRecordPre(payload)
   106  	case plugins.HookTypeEditRecordPre:
   107  		return p.hookEditRecordPre(payload)
   108  	case plugins.HookTypePluginPre:
   109  		return p.hookPluginPre(payload)
   110  	}
   111  
   112  	return nil
   113  }
   114  
   115  // Fsck performs a plugin file system check. The plugin is provided with the
   116  // tokens for all records in the backend.
   117  //
   118  // This function satisfies the plugins PluginClient interface.
   119  func (p *piPlugin) Fsck(tokens [][]byte) error {
   120  	log.Tracef("pi Fsck")
   121  
   122  	return nil
   123  }
   124  
   125  // Settings returns the plugin's settings.
   126  //
   127  // This function satisfies the plugins PluginClient interface.
   128  func (p *piPlugin) Settings() []backend.PluginSetting {
   129  	log.Tracef("pi Settings")
   130  
   131  	return []backend.PluginSetting{
   132  		{
   133  			Key:   pi.SettingKeyTextFileSizeMax,
   134  			Value: strconv.FormatUint(uint64(p.textFileSizeMax), 10),
   135  		},
   136  		{
   137  			Key:   pi.SettingKeyImageFileCountMax,
   138  			Value: strconv.FormatUint(uint64(p.imageFileCountMax), 10),
   139  		},
   140  		{
   141  			Key:   pi.SettingKeyImageFileCountMax,
   142  			Value: strconv.FormatUint(uint64(p.imageFileCountMax), 10),
   143  		},
   144  		{
   145  			Key:   pi.SettingKeyImageFileSizeMax,
   146  			Value: strconv.FormatUint(uint64(p.imageFileSizeMax), 10),
   147  		},
   148  		{
   149  			Key:   pi.SettingKeyTitleLengthMin,
   150  			Value: strconv.FormatUint(uint64(p.titleLengthMin), 10),
   151  		},
   152  		{
   153  			Key:   pi.SettingKeyTitleLengthMax,
   154  			Value: strconv.FormatUint(uint64(p.titleLengthMax), 10),
   155  		},
   156  		{
   157  			Key:   pi.SettingKeyTitleSupportedChars,
   158  			Value: p.titleSupportedChars,
   159  		},
   160  		{
   161  			Key:   pi.SettingKeyProposalAmountMin,
   162  			Value: strconv.FormatUint(p.proposalAmountMin, 10),
   163  		},
   164  		{
   165  			Key:   pi.SettingKeyProposalAmountMax,
   166  			Value: strconv.FormatUint(p.proposalAmountMax, 10),
   167  		},
   168  		{
   169  			Key:   pi.SettingKeyProposalStartDateMin,
   170  			Value: strconv.FormatInt(p.proposalStartDateMin, 10),
   171  		},
   172  		{
   173  			Key:   pi.SettingKeyProposalEndDateMax,
   174  			Value: strconv.FormatInt(p.proposalEndDateMax, 10),
   175  		},
   176  		{
   177  			Key:   pi.SettingKeyProposalDomains,
   178  			Value: p.proposalDomainsEncoded,
   179  		},
   180  		{
   181  			Key:   pi.SettingKeyBillingStatusChangesMax,
   182  			Value: strconv.FormatUint(uint64(p.billingStatusChangesMax), 10),
   183  		},
   184  		{
   185  			Key:   pi.SettingKeySummariesPageSize,
   186  			Value: strconv.FormatUint(uint64(p.summariesPageSize), 10),
   187  		},
   188  		{
   189  			Key:   pi.SettingKeyBillingStatusChangesPageSize,
   190  			Value: strconv.FormatUint(uint64(p.billingStatusChangesPageSize), 10),
   191  		},
   192  	}
   193  }
   194  
   195  // New returns a new piPlugin.
   196  func New(backend backend.Backend, tstore plugins.TstoreClient, settings []backend.PluginSetting, dataDir string, id *identity.FullIdentity) (*piPlugin, error) {
   197  	// Create plugin data directory
   198  	dataDir = filepath.Join(dataDir, pi.PluginID)
   199  	err := os.MkdirAll(dataDir, 0700)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  
   204  	// Setup plugin setting default values
   205  	var (
   206  		textFileSizeMax              = pi.SettingTextFileSizeMax
   207  		imageFileCountMax            = pi.SettingImageFileCountMax
   208  		imageFileSizeMax             = pi.SettingImageFileSizeMax
   209  		titleLengthMin               = pi.SettingTitleLengthMin
   210  		titleLengthMax               = pi.SettingTitleLengthMax
   211  		titleSupportedChars          = pi.SettingTitleSupportedChars
   212  		amountMin                    = pi.SettingProposalAmountMin
   213  		amountMax                    = pi.SettingProposalAmountMax
   214  		startDateMin                 = pi.SettingProposalStartDateMin
   215  		endDateMax                   = pi.SettingProposalEndDateMax
   216  		domains                      = pi.SettingProposalDomains
   217  		billingStatusChangesMax      = pi.SettingBillingStatusChangesMax
   218  		summariesPageSize            = pi.SettingSummariesPageSize
   219  		billingStatusChangesPageSize = pi.SettingBillingStatusChangesPageSize
   220  	)
   221  
   222  	// Override defaults with any passed in settings
   223  	for _, v := range settings {
   224  		switch v.Key {
   225  		case pi.SettingKeyTextFileSizeMax:
   226  			u, err := strconv.ParseUint(v.Value, 10, 64)
   227  			if err != nil {
   228  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   229  					v.Key, v.Value, err)
   230  			}
   231  			textFileSizeMax = uint32(u)
   232  
   233  		case pi.SettingKeyImageFileCountMax:
   234  			u, err := strconv.ParseUint(v.Value, 10, 64)
   235  			if err != nil {
   236  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   237  					v.Key, v.Value, err)
   238  			}
   239  			imageFileCountMax = uint32(u)
   240  
   241  		case pi.SettingKeyImageFileSizeMax:
   242  			u, err := strconv.ParseUint(v.Value, 10, 64)
   243  			if err != nil {
   244  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   245  					v.Key, v.Value, err)
   246  			}
   247  			imageFileSizeMax = uint32(u)
   248  
   249  		case pi.SettingKeyTitleLengthMin:
   250  			u, err := strconv.ParseUint(v.Value, 10, 64)
   251  			if err != nil {
   252  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   253  					v.Key, v.Value, err)
   254  			}
   255  			titleLengthMin = uint32(u)
   256  
   257  		case pi.SettingKeyTitleLengthMax:
   258  			u, err := strconv.ParseUint(v.Value, 10, 64)
   259  			if err != nil {
   260  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   261  					v.Key, v.Value, err)
   262  			}
   263  			titleLengthMax = uint32(u)
   264  
   265  		case pi.SettingKeyTitleSupportedChars:
   266  			err := json.Unmarshal([]byte(v.Value), &titleSupportedChars)
   267  			if err != nil {
   268  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   269  					v.Key, v.Value, err)
   270  			}
   271  
   272  		case pi.SettingKeyProposalAmountMin:
   273  			u, err := strconv.ParseUint(v.Value, 10, 64)
   274  			if err != nil {
   275  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   276  					v.Key, v.Value, err)
   277  			}
   278  			amountMin = u
   279  
   280  		case pi.SettingKeyProposalAmountMax:
   281  			u, err := strconv.ParseUint(v.Value, 10, 64)
   282  			if err != nil {
   283  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   284  					v.Key, v.Value, err)
   285  			}
   286  			amountMax = u
   287  
   288  		case pi.SettingKeyProposalEndDateMax:
   289  			u, err := strconv.ParseInt(v.Value, 10, 64)
   290  			if err != nil {
   291  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   292  					v.Key, v.Value, err)
   293  			}
   294  			// Ensure provided max end date is not in the past
   295  			if u < 0 {
   296  				return nil, errors.Errorf("invalid plugin setting %v '%v': "+
   297  					"must be in the future", v.Key, v.Value)
   298  			}
   299  			endDateMax = u
   300  
   301  		case pi.SettingKeyProposalStartDateMin:
   302  			i, err := strconv.ParseInt(v.Value, 10, 64)
   303  			if err != nil {
   304  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   305  					v.Key, v.Value, err)
   306  			}
   307  			startDateMin = i
   308  
   309  		case pi.SettingKeyProposalDomains:
   310  			err := json.Unmarshal([]byte(v.Value), &domains)
   311  			if err != nil {
   312  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   313  					v.Key, v.Value, err)
   314  			}
   315  
   316  		case pi.SettingKeyBillingStatusChangesMax:
   317  			u, err := strconv.ParseUint(v.Value, 10, 64)
   318  			if err != nil {
   319  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   320  					v.Key, v.Value, err)
   321  			}
   322  			billingStatusChangesMax = uint32(u)
   323  
   324  		case pi.SettingKeySummariesPageSize:
   325  			u, err := strconv.ParseUint(v.Value, 10, 64)
   326  			if err != nil {
   327  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   328  					v.Key, v.Value, err)
   329  			}
   330  			summariesPageSize = uint32(u)
   331  
   332  		case pi.SettingKeyBillingStatusChangesPageSize:
   333  			u, err := strconv.ParseUint(v.Value, 10, 64)
   334  			if err != nil {
   335  				return nil, errors.Errorf("invalid plugin setting %v '%v': %v",
   336  					v.Key, v.Value, err)
   337  			}
   338  			billingStatusChangesPageSize = uint32(u)
   339  
   340  		default:
   341  			return nil, errors.Errorf("invalid plugin setting: %v", v.Key)
   342  		}
   343  	}
   344  
   345  	// Setup title regex
   346  	rexp, err := util.Regexp(titleSupportedChars, uint64(titleLengthMin),
   347  		uint64(titleLengthMax))
   348  	if err != nil {
   349  		return nil, errors.Errorf("proposal name regexp: %v", err)
   350  	}
   351  
   352  	// Encode the title supported chars so that they
   353  	// can be returned as a plugin setting string.
   354  	b, err := json.Marshal(titleSupportedChars)
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  	titleSupportedCharsString := string(b)
   359  
   360  	// Encode the proposal domains so that they can be returned as a
   361  	// plugin setting string.
   362  	b, err = json.Marshal(domains)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  	domainsString := string(b)
   367  
   368  	// Translate domains slice to a Map[string]string.
   369  	domainsMap := make(map[string]struct{}, len(domains))
   370  	for _, d := range domains {
   371  		domainsMap[d] = struct{}{}
   372  	}
   373  
   374  	return &piPlugin{
   375  		dataDir:                      dataDir,
   376  		identity:                     id,
   377  		backend:                      backend,
   378  		textFileSizeMax:              textFileSizeMax,
   379  		tstore:                       tstore,
   380  		imageFileCountMax:            imageFileCountMax,
   381  		imageFileSizeMax:             imageFileSizeMax,
   382  		titleLengthMin:               titleLengthMin,
   383  		titleLengthMax:               titleLengthMax,
   384  		titleSupportedChars:          titleSupportedCharsString,
   385  		titleRegexp:                  rexp,
   386  		proposalAmountMin:            amountMin,
   387  		proposalAmountMax:            amountMax,
   388  		proposalStartDateMin:         startDateMin,
   389  		proposalEndDateMax:           endDateMax,
   390  		proposalDomainsEncoded:       domainsString,
   391  		proposalDomains:              domainsMap,
   392  		billingStatusChangesMax:      billingStatusChangesMax,
   393  		summariesPageSize:            summariesPageSize,
   394  		billingStatusChangesPageSize: billingStatusChangesPageSize,
   395  		statuses: proposalStatuses{
   396  			data:    make(map[string]*statusEntry, statusesCacheLimit),
   397  			entries: list.New(),
   398  		},
   399  	}, nil
   400  }