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 }