github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/comments/timestamp.go (about) 1 // Copyright (c) 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 comments 6 7 import ( 8 "encoding/json" 9 "strconv" 10 "strings" 11 12 "github.com/decred/politeia/politeiad/plugins/comments" 13 "github.com/decred/politeia/util" 14 "github.com/pkg/errors" 15 ) 16 17 const ( 18 // timestampKey is the key for a timestamp entry in the key-value store 19 // cache. 20 timestampKey = "timestamp-{shorttoken}-{commentID}" 21 ) 22 23 // cacheFinalTimestamps accepts a map of comment timestamps, it collects the 24 // final timestamps then stores them in the key-value store. 25 func (p *commentsPlugin) cacheFinalTimestamps(token []byte, cts map[uint32]comments.CommentTimestamp) error { 26 finalTimestamps, err := finalCommentTimestamps(cts, token) 27 if err != nil { 28 return err 29 } 30 31 err = p.saveTimestamps(token, finalTimestamps) 32 if err != nil { 33 return err 34 } 35 36 log.Debugf("Cached final comment timestamps of %v/%v", 37 len(finalTimestamps), len(cts)) 38 return nil 39 } 40 41 // finalCommentTimestamps accepts a map of comment timestamps, and it returns 42 // a new map with all final comment timestamps. A timestamp considered final 43 // if it was successfully timestamped on the DCR chain, meaning it's merkle 44 // root was included in a confirmed DCR transaction. 45 func finalCommentTimestamps(ts map[uint32]comments.CommentTimestamp, token []byte) (map[uint32]comments.CommentTimestamp, error) { 46 fts := make(map[uint32]comments.CommentTimestamp, len(ts)) 47 for cid, t := range ts { 48 // Search for final comment add timestamps 49 for _, at := range t.Adds { 50 if timestampIsFinal(at) { 51 // Add final comment add to the final timestamps map. 52 ct, exists := fts[cid] 53 if !exists { 54 ct = comments.CommentTimestamp{} 55 } 56 if len(ct.Adds) == 0 { 57 ct.Adds = make([]comments.Timestamp, 0, len(t.Adds)) 58 } 59 ct.Adds = append(ct.Adds, at) 60 fts[cid] = ct 61 } 62 } 63 64 // Search for final comment del timestamp 65 if t.Del != nil && timestampIsFinal(*t.Del) { 66 // Add final comment del to final timestamps map. 67 ct, exists := fts[cid] 68 if !exists { 69 ct = comments.CommentTimestamp{} 70 } 71 ct.Del = t.Del 72 fts[cid] = ct 73 } 74 75 // Search for final comment vote timestamps 76 for _, vt := range t.Votes { 77 if timestampIsFinal(vt) { 78 // Add final comment add to the final timestamps map. 79 ct, exists := fts[cid] 80 if !exists { 81 ct = comments.CommentTimestamp{} 82 } 83 if len(ct.Votes) == 0 { 84 ct.Votes = make([]comments.Timestamp, 0, len(t.Votes)) 85 } 86 ct.Votes = append(ct.Votes, vt) 87 fts[cid] = ct 88 } 89 } 90 } 91 92 return fts, nil 93 } 94 95 // saveTimestamps saves a list of timestamps to the key-value cache. 96 func (p *commentsPlugin) saveTimestamps(token []byte, ts map[uint32]comments.CommentTimestamp) error { 97 // Setup the blob entries 98 blobs := make(map[string][]byte, len(ts)) 99 keys := make([]string, 0, len(ts)) 100 for cid, v := range ts { 101 k, err := getTimestampKey(token, cid) 102 if err != nil { 103 return err 104 } 105 b, err := json.Marshal(v) 106 if err != nil { 107 return err 108 } 109 blobs[k] = b 110 keys = append(keys, k) 111 } 112 113 // Delete exisiting digests 114 err := p.tstore.CacheDel(keys) 115 if err != nil { 116 return err 117 } 118 119 // Save the blob entries 120 return p.tstore.CachePut(blobs, false) 121 } 122 123 // cachedTimestamps returns cached comment timestamps if they exist. An entry 124 // will not exist in the returned map if a timestamp was not found in the cache 125 // for a comment ID. 126 func (p *commentsPlugin) cachedTimestamps(token []byte, commentIDs []uint32) (map[uint32]*comments.CommentTimestamp, error) { 127 // Setup the timestamp keys 128 keys := make([]string, 0, len(commentIDs)) 129 for _, cid := range commentIDs { 130 k, err := getTimestampKey(token, cid) 131 if err != nil { 132 return nil, err 133 } 134 keys = append(keys, k) 135 } 136 137 // Get the timestamp blob entries 138 blobs, err := p.tstore.CacheGet(keys) 139 if err != nil { 140 return nil, err 141 } 142 143 // Decode the timestamps 144 ts := make(map[uint32]*comments.CommentTimestamp, len(blobs)) 145 cacheIDs := make([]uint32, 0, len(blobs)) 146 for k, v := range blobs { 147 var t comments.CommentTimestamp 148 err := json.Unmarshal(v, &t) 149 if err != nil { 150 return nil, err 151 } 152 cid, err := parseTimestampKey(k) 153 if err != nil { 154 return nil, err 155 } 156 ts[cid] = &t 157 cacheIDs = append(cacheIDs, cid) 158 } 159 160 log.Debugf("Retrieved cached final comment timestamps of %v/%v", 161 len(cacheIDs), len(commentIDs)) 162 return ts, nil 163 } 164 165 // getTimestampKey returns the key for a timestamp in the key-value store 166 // cache. 167 func getTimestampKey(token []byte, commentID uint32) (string, error) { 168 t, err := util.ShortTokenEncode(token) 169 if err != nil { 170 return "", err 171 } 172 cid := strconv.FormatUint(uint64(commentID), 10) 173 key := strings.Replace(timestampKey, "{shorttoken}", t, 1) 174 key = strings.Replace(key, "{commentID}", cid, 1) 175 return key, nil 176 } 177 178 // parseTimestampKey parses the comment ID from a timestamp key. 179 func parseTimestampKey(key string) (uint32, error) { 180 s := strings.Split(key, "-") 181 if len(s) != 3 { 182 return 0, errors.Errorf("invalid comment timestamp key") 183 } 184 cid, err := strconv.ParseUint(s[2], 10, 64) 185 if err != nil { 186 return 0, err 187 } 188 return uint32(cid), nil 189 } 190 191 // timestampIsFinal returns whether the timestamp is considered to be final and 192 // will not change in the future. Once the TxID is present then the timestamp 193 // is considered to be final since it has been included in a DCR transaction. 194 func timestampIsFinal(t comments.Timestamp) bool { 195 return t.TxID != "" 196 }