github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/usermd/usermd.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 usermd
     6  
     7  import (
     8  	"encoding/hex"
     9  	"os"
    10  	"path/filepath"
    11  	"sort"
    12  	"sync"
    13  
    14  	backend "github.com/decred/politeia/politeiad/backendv2"
    15  	"github.com/decred/politeia/politeiad/backendv2/tstorebe/plugins"
    16  	"github.com/decred/politeia/politeiad/plugins/usermd"
    17  )
    18  
    19  var (
    20  	_ plugins.PluginClient = (*usermdPlugin)(nil)
    21  )
    22  
    23  // usermdPlugin is the tstore backend implementation of the usermd plugin. The
    24  // usermd plugin extends a record with user metadata.
    25  //
    26  // usermdPlugin satisfies the plugins PluginClient interface.
    27  type usermdPlugin struct {
    28  	sync.Mutex
    29  	tstore plugins.TstoreClient
    30  
    31  	// dataDir is the pi plugin data directory. The only data that is
    32  	// stored here is cached data that can be re-created at any time
    33  	// by walking the trillian trees.
    34  	dataDir string
    35  }
    36  
    37  // Setup performs any plugin setup that is required.
    38  //
    39  // This function satisfies the plugins PluginClient interface.
    40  func (p *usermdPlugin) Setup() error {
    41  	log.Tracef("usermd Setup")
    42  
    43  	return nil
    44  }
    45  
    46  // Cmd executes a plugin command.
    47  //
    48  // This function satisfies the plugins PluginClient interface.
    49  func (p *usermdPlugin) Cmd(token []byte, cmd, payload string) (string, error) {
    50  	log.Tracef("usermd Cmd: %x %v %v", token, cmd, payload)
    51  
    52  	switch cmd {
    53  	case usermd.CmdAuthor:
    54  		return p.cmdAuthor(token)
    55  	case usermd.CmdUserRecords:
    56  		return p.cmdUserRecords(payload)
    57  	}
    58  
    59  	return "", backend.ErrPluginCmdInvalid
    60  }
    61  
    62  // Hook executes a plugin hook.
    63  //
    64  // This function satisfies the plugins PluginClient interface.
    65  func (p *usermdPlugin) Hook(h plugins.HookT, payload string) error {
    66  	log.Tracef("usermd Hook: %v", plugins.Hooks[h])
    67  
    68  	switch h {
    69  	case plugins.HookTypeNewRecordPre:
    70  		return p.hookNewRecordPre(payload)
    71  	case plugins.HookTypeNewRecordPost:
    72  		return p.hookNewRecordPost(payload)
    73  	case plugins.HookTypeEditRecordPre:
    74  		return p.hookEditRecordPre(payload)
    75  	case plugins.HookTypeEditMetadataPre:
    76  		return p.hookEditMetadataPre(payload)
    77  	case plugins.HookTypeSetRecordStatusPre:
    78  		return p.hookSetRecordStatusPre(payload)
    79  	case plugins.HookTypeSetRecordStatusPost:
    80  		return p.hookSetRecordStatusPost(payload)
    81  	}
    82  
    83  	return nil
    84  }
    85  
    86  // addMissingRecord adds the given record's token to a list of tokens sorted
    87  // by the latest status change timestamp, from oldest to newest.
    88  func (p *usermdPlugin) addMissingRecord(tokens []string, missingRecord *backend.Record) ([]string, error) {
    89  	// Make list of records to be able to sort by latest status change
    90  	// timestamp.
    91  	records := make([]*backend.Record, 0, len(tokens)+1)
    92  	for _, t := range tokens {
    93  		// Decode string token
    94  		b, err := hex.DecodeString(t)
    95  		if err != nil {
    96  			return nil, err
    97  		}
    98  		r, err := p.tstore.RecordPartial(b, 0, nil, true)
    99  		if err != nil {
   100  			return nil, err
   101  		}
   102  		records = append(records, r)
   103  	}
   104  
   105  	// Append new record then sort records by latest status change timestamp
   106  	// from oldest to newest.
   107  	records = append(records, missingRecord)
   108  
   109  	// Sort records
   110  	sort.Slice(records, func(i, j int) bool {
   111  		return records[i].RecordMetadata.Timestamp <
   112  			records[j].RecordMetadata.Timestamp
   113  	})
   114  
   115  	// Return sorted tokens
   116  	newTokens := make([]string, 0, len(records))
   117  	for _, record := range records {
   118  		newTokens = append(newTokens, record.RecordMetadata.Token)
   119  	}
   120  
   121  	return newTokens, nil
   122  }
   123  
   124  // Fsck performs a plugin file system check. The plugin is provided with the
   125  // tokens for all records in the backend.
   126  //
   127  // It verifies the user cache using the following process:
   128  //
   129  //  1. For each record, get the user metadata file from the db.
   130  //  2. Get the user cache for the record's author.
   131  //  3. Verify that the record is listed in the user cache under the
   132  //     correct category.  If the record is not found in the user
   133  //     cache, add it.  The tokens listed in the user cache are
   134  //     ordered by the timestamp of their most recent status change
   135  //     from oldest to newest.
   136  //
   137  // This function satisfies the plugins PluginClient interface.
   138  func (p *usermdPlugin) Fsck(tokens [][]byte) error {
   139  	log.Tracef("usermd Fsck")
   140  
   141  	// Number of records which were added to the user cache.
   142  	var c int64
   143  
   144  	for _, token := range tokens {
   145  		r, err := p.tstore.RecordPartial(token, 0, nil, true)
   146  		if err != nil {
   147  			return err
   148  		}
   149  
   150  		// Decode user metadata
   151  		um, err := userMetadataDecode(r.Metadata)
   152  		if err != nil {
   153  			return err
   154  		}
   155  
   156  		// Get the user cache for the record's author
   157  		uc, err := p.userCache(um.UserID)
   158  		if err != nil {
   159  			return err
   160  		}
   161  
   162  		// Verify that the record is listed in the user cache under the
   163  		// correct category.
   164  		var found bool
   165  		tokenStr := hex.EncodeToString(token)
   166  		switch r.RecordMetadata.State {
   167  		case backend.StateUnvetted:
   168  			for _, t := range uc.Unvetted {
   169  				if t == tokenStr {
   170  					found = true
   171  				}
   172  			}
   173  			// Unvetted record is missing, add it
   174  			if !found {
   175  				uc.Unvetted, err = p.addMissingRecord(uc.Unvetted, r)
   176  				if err != nil {
   177  					return err
   178  				}
   179  			}
   180  
   181  		case backend.StateVetted:
   182  			for _, t := range uc.Vetted {
   183  				if t == tokenStr {
   184  					found = true
   185  				}
   186  			}
   187  			// Vetted record is missing, add it
   188  			if !found {
   189  				uc.Vetted, err = p.addMissingRecord(uc.Vetted, r)
   190  				if err != nil {
   191  					return err
   192  				}
   193  			}
   194  		}
   195  
   196  		// If a missing token was added to the user cache, save new user cache
   197  		// to disk.
   198  		if !found {
   199  			err = p.userCacheSave(um.UserID, *uc)
   200  			if err != nil {
   201  				return err
   202  			}
   203  			c++
   204  			log.Debugf("Missing %v record %v was added to %v user records cache",
   205  				backend.States[r.RecordMetadata.State], tokenStr, um.UserID)
   206  		}
   207  	}
   208  
   209  	log.Infof("%v missing records were added to the user records cache", c)
   210  
   211  	return nil
   212  }
   213  
   214  // Settings returns the plugin's settings.
   215  //
   216  // This function satisfies the plugins PluginClient interface.
   217  func (p *usermdPlugin) Settings() []backend.PluginSetting {
   218  	log.Tracef("usermd Settings")
   219  
   220  	return nil
   221  }
   222  
   223  // New returns a new usermdPlugin.
   224  func New(tstore plugins.TstoreClient, settings []backend.PluginSetting, dataDir string) (*usermdPlugin, error) {
   225  	// Create plugin data directory
   226  	dataDir = filepath.Join(dataDir, usermd.PluginID)
   227  	err := os.MkdirAll(dataDir, 0700)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	return &usermdPlugin{
   233  		tstore:  tstore,
   234  		dataDir: dataDir,
   235  	}, nil
   236  }