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 }