wip: engine2 passing test with updates service
This commit is contained in:
+13
-13
@@ -31,7 +31,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/rest/proxy"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
"github.com/umputun/remark/backend/app/store/engine2"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
@@ -249,7 +249,7 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) {
|
||||
log.Printf("[DEBUG] image service for url=%s, ttl=%v", imageService.ImageAPI, imageService.TTL)
|
||||
|
||||
dataService := &service.DataStore{
|
||||
Interface: storeEngine,
|
||||
Engine: storeEngine,
|
||||
EditDuration: s.EditDuration,
|
||||
AdminStore: adminStore,
|
||||
MaxCommentSize: s.MaxCommentSize,
|
||||
@@ -401,7 +401,7 @@ func (a *serverApp) activateBackup(ctx context.Context) {
|
||||
}
|
||||
|
||||
// makeDataStore creates store for all sites
|
||||
func (s *ServerCommand) makeDataStore() (result engine.Interface, err error) {
|
||||
func (s *ServerCommand) makeDataStore() (result engine2.Interface, err error) {
|
||||
log.Printf("[INFO] make data store, type=%s", s.Store.Type)
|
||||
|
||||
switch s.Store.Type {
|
||||
@@ -409,18 +409,18 @@ func (s *ServerCommand) makeDataStore() (result engine.Interface, err error) {
|
||||
if err = makeDirs(s.Store.Bolt.Path); err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create bolt store")
|
||||
}
|
||||
sites := []engine.BoltSite{}
|
||||
sites := []engine2.BoltSite{}
|
||||
for _, site := range s.Sites {
|
||||
sites = append(sites, engine.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", s.Store.Bolt.Path, site)})
|
||||
sites = append(sites, engine2.BoltSite{SiteID: site, FileName: fmt.Sprintf("%s/%s.db", s.Store.Bolt.Path, site)})
|
||||
}
|
||||
result, err = engine.NewBoltDB(bolt.Options{Timeout: s.Store.Bolt.Timeout}, sites...)
|
||||
case "mongo":
|
||||
mgServer, e := s.makeMongo()
|
||||
if e != nil {
|
||||
return result, errors.Wrap(e, "failed to create mongo server")
|
||||
}
|
||||
conn := mongo.NewConnection(mgServer, s.Mongo.DB, "")
|
||||
result, err = engine.NewMongo(conn, 500, 100*time.Millisecond)
|
||||
result, err = engine2.NewBoltDB(bolt.Options{Timeout: s.Store.Bolt.Timeout}, sites...)
|
||||
// case "mongo":
|
||||
// mgServer, e := s.makeMongo()
|
||||
// if e != nil {
|
||||
// return result, errors.Wrap(e, "failed to create mongo server")
|
||||
// }
|
||||
// conn := mongo.NewConnection(mgServer, s.Mongo.DB, "")
|
||||
// result, err = engine.NewMongo(conn, 500, 100*time.Millisecond)
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported store type %s", s.Store.Type)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import (
|
||||
"github.com/umputun/remark/backend/app/rest/proxy"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
adminstore "github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
"github.com/umputun/remark/backend/app/store/engine2"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
@@ -291,7 +291,7 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
os.RemoveAll("/tmp/ava-remark42")
|
||||
os.RemoveAll("/tmp/pics-remark42")
|
||||
|
||||
b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
b, err := engine2.NewBoltDB(bolt.Options{}, engine2.BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
require.Nil(t, err)
|
||||
|
||||
memCache, err := cache.NewMemoryCache()
|
||||
@@ -301,7 +301,7 @@ func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
restrictedWordsMatcher := service.NewRestrictedWordsMatcher(service.StaticRestrictedWordsLister{Words: []string{"duck"}})
|
||||
|
||||
dataStore := &service.DataStore{
|
||||
Interface: b,
|
||||
Engine: b,
|
||||
EditDuration: 5 * time.Minute,
|
||||
MaxCommentSize: 4000,
|
||||
AdminStore: adminStore,
|
||||
|
||||
@@ -105,7 +105,7 @@ func (b *BoltDB) Create(comment store.Comment) (commentID string, err error) {
|
||||
}
|
||||
|
||||
// serialize comment to json []byte for bolt and save
|
||||
if e = b.save(postBkt, []byte(comment.ID), comment); e != nil {
|
||||
if e = b.save(postBkt, comment.ID, comment); e != nil {
|
||||
return errors.Wrapf(e, "failed to put key %s to bucket %s", comment.ID, comment.Locator.URL)
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func (b *BoltDB) Last(siteID string, max int, since time.Time) (comments []store
|
||||
}
|
||||
|
||||
comment := store.Comment{}
|
||||
if e = b.load(postBkt, []byte(commentID), &comment); e != nil {
|
||||
if e = b.load(postBkt, commentID, &comment); e != nil {
|
||||
log.Printf("[WARN] can't load comment for %s from store %s", commentID, url)
|
||||
continue
|
||||
}
|
||||
@@ -263,7 +263,7 @@ func (b BoltDB) List(siteID string, limit, skip int) (list []store.PostInfo, err
|
||||
postURL := string(k)
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
info := store.PostInfo{}
|
||||
if e := b.load(infoBkt, []byte(postURL), &info); e != nil {
|
||||
if e := b.load(infoBkt, postURL, &info); e != nil {
|
||||
return errors.Wrapf(e, "can't load info for %s", postURL)
|
||||
}
|
||||
list = append(list, info)
|
||||
@@ -287,7 +287,7 @@ func (b *BoltDB) Info(locator store.Locator, readOnlyAge int) (store.PostInfo, e
|
||||
info := store.PostInfo{}
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
if e := b.load(infoBkt, []byte(locator.URL), &info); e != nil {
|
||||
if e := b.load(infoBkt, locator.URL, &info); e != nil {
|
||||
return errors.Wrapf(e, "can't load info for %s", locator.URL)
|
||||
}
|
||||
return nil
|
||||
@@ -391,7 +391,7 @@ func (b *BoltDB) Get(locator store.Locator, commentID string) (comment store.Com
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return b.load(bucket, []byte(commentID), &comment)
|
||||
return b.load(bucket, commentID, &comment)
|
||||
})
|
||||
return comment, err
|
||||
}
|
||||
@@ -417,7 +417,7 @@ func (b *BoltDB) Put(locator store.Locator, comment store.Comment) error {
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return b.save(bucket, []byte(comment.ID), comment)
|
||||
return b.save(bucket, comment.ID, comment)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -467,7 +467,7 @@ func (b *BoltDB) getUserBucket(tx *bolt.Tx, userID string) (*bolt.Bucket, error)
|
||||
}
|
||||
|
||||
// save marshaled value to key for bucket. Should run in update tx
|
||||
func (b *BoltDB) save(bkt *bolt.Bucket, key []byte, value interface{}) (err error) {
|
||||
func (b *BoltDB) save(bkt *bolt.Bucket, key string, value interface{}) (err error) {
|
||||
if value == nil {
|
||||
return errors.Errorf("can't save nil value for %s", key)
|
||||
}
|
||||
@@ -475,15 +475,15 @@ func (b *BoltDB) save(bkt *bolt.Bucket, key []byte, value interface{}) (err erro
|
||||
if jerr != nil {
|
||||
return errors.Wrap(jerr, "can't marshal comment")
|
||||
}
|
||||
if err = bkt.Put(key, jdata); err != nil {
|
||||
if err = bkt.Put([]byte(key), jdata); err != nil {
|
||||
return errors.Wrapf(err, "failed to save key %s", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// load and unmarshal json value by key from bucket. Should run in view tx
|
||||
func (b *BoltDB) load(bkt *bolt.Bucket, key []byte, res interface{}) error {
|
||||
value := bkt.Get(key)
|
||||
func (b *BoltDB) load(bkt *bolt.Bucket, key string, res interface{}) error {
|
||||
value := bkt.Get([]byte(key))
|
||||
if value == nil {
|
||||
return errors.Errorf("no value for %s", key)
|
||||
}
|
||||
@@ -501,7 +501,7 @@ func (b *BoltDB) count(tx *bolt.Tx, postURL string, val int) (int, error) {
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
|
||||
info := store.PostInfo{}
|
||||
if err := b.load(infoBkt, []byte(postURL), &info); err != nil {
|
||||
if err := b.load(infoBkt, postURL, &info); err != nil {
|
||||
info = store.PostInfo{}
|
||||
}
|
||||
if val == 0 { // get current count, don't update
|
||||
@@ -509,13 +509,13 @@ func (b *BoltDB) count(tx *bolt.Tx, postURL string, val int) (int, error) {
|
||||
}
|
||||
info.Count += val
|
||||
|
||||
return info.Count, b.save(infoBkt, []byte(postURL), &info)
|
||||
return info.Count, b.save(infoBkt, postURL, &info)
|
||||
}
|
||||
|
||||
func (b *BoltDB) setInfo(tx *bolt.Tx, comment store.Comment) (store.PostInfo, error) {
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
info := store.PostInfo{}
|
||||
if err := b.load(infoBkt, []byte(comment.Locator.URL), &info); err != nil {
|
||||
if err := b.load(infoBkt, comment.Locator.URL, &info); err != nil {
|
||||
info = store.PostInfo{
|
||||
Count: 0,
|
||||
URL: comment.Locator.URL,
|
||||
@@ -525,7 +525,7 @@ func (b *BoltDB) setInfo(tx *bolt.Tx, comment store.Comment) (store.PostInfo, er
|
||||
}
|
||||
info.Count++
|
||||
info.LastTS = comment.Timestamp
|
||||
return info, b.save(infoBkt, []byte(comment.Locator.URL), &info)
|
||||
return info, b.save(infoBkt, comment.Locator.URL, &info)
|
||||
}
|
||||
|
||||
func (b *BoltDB) db(siteID string) (*bolt.DB, error) {
|
||||
|
||||
@@ -29,13 +29,13 @@ func (b *BoltDB) Delete(locator store.Locator, commentID string, mode store.Dele
|
||||
}
|
||||
|
||||
comment := store.Comment{}
|
||||
if err = b.load(postBkt, []byte(commentID), &comment); err != nil {
|
||||
if err = b.load(postBkt, commentID, &comment); err != nil {
|
||||
return errors.Wrapf(err, "can't load key %s from bucket %s", commentID, locator.URL)
|
||||
}
|
||||
// set deleted status and clear fields
|
||||
comment.SetDeleted(mode)
|
||||
|
||||
if err = b.save(postBkt, []byte(commentID), comment); err != nil {
|
||||
if err = b.save(postBkt, commentID, comment); err != nil {
|
||||
return errors.Wrapf(err, "can't save deleted comment for key %s from bucket %s", commentID, locator.URL)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,14 +13,6 @@ import (
|
||||
// NOTE: mockery works from linked to go-path and with GOFLAGS='-mod=vendor' go generate
|
||||
//go:generate sh -c "mockery -inpkg -name Interface -print > /tmp/engine-mock.tmp && mv /tmp/engine-mock.tmp engine_mock.go"
|
||||
|
||||
// UserRequest is the request send to get comments by user
|
||||
type UserRequest struct {
|
||||
SiteID string
|
||||
UserID string
|
||||
Limit int
|
||||
Skip int
|
||||
}
|
||||
|
||||
// Interface defines methods provided by low-level storage engine
|
||||
type Interface interface {
|
||||
Create(comment store.Comment) (commentID string, err error) // create new comment, avoid dups by id
|
||||
|
||||
@@ -0,0 +1,863 @@
|
||||
package engine2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
bolt "github.com/coreos/bbolt"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
|
||||
// BoltDB implements store.Interface, represents multiple sites with multiplexing to different bolt dbs. Thread safe.
|
||||
// there are 5 types of top-level buckets:
|
||||
// - comments for post in "posts" top-level bucket. Each url (post) makes its own bucket and each k:v pair is commentID:comment
|
||||
// - history of all comments. They all in a single "last" bucket (per site) and key is defined by ref struct as ts+commentID
|
||||
// value is not full comment but a reference combined from post-url+commentID
|
||||
// - user to comment references in "users" bucket. It used to get comments for user. Key is userID and value
|
||||
// is a nested bucket named userID with kv as ts:reference
|
||||
// - blocking info sits in "block" bucket. Key is userID, value - ts
|
||||
// - counts per post to keep number of comments. Key is post url, value - count
|
||||
// - readonly per post to keep status of manually set RO posts. Key is post url, value - ts
|
||||
type BoltDB struct {
|
||||
dbs map[string]*bolt.DB
|
||||
}
|
||||
|
||||
const (
|
||||
// top level buckets
|
||||
postsBucketName = "posts"
|
||||
lastBucketName = "last"
|
||||
userBucketName = "users"
|
||||
blocksBucketName = "block"
|
||||
infoBucketName = "info"
|
||||
readonlyBucketName = "readonly"
|
||||
verifiedBucketName = "verified"
|
||||
|
||||
tsNano = "2006-01-02T15:04:05.000000000Z07:00"
|
||||
)
|
||||
|
||||
// BoltSite defines single site param
|
||||
type BoltSite struct {
|
||||
FileName string // full path to boltdb
|
||||
SiteID string // ID of given site
|
||||
}
|
||||
|
||||
// NewBoltDB makes persistent boltdb-based store. For each site new boltdb file created
|
||||
func NewBoltDB(options bolt.Options, sites ...BoltSite) (*BoltDB, error) {
|
||||
log.Printf("[INFO] bolt store for sites %+v, options %+v", sites, options)
|
||||
result := BoltDB{dbs: make(map[string]*bolt.DB)}
|
||||
for _, site := range sites {
|
||||
db, err := bolt.Open(site.FileName, 0600, &options)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to make boltdb for %s", site.FileName)
|
||||
}
|
||||
|
||||
// make top-level buckets
|
||||
topBuckets := []string{postsBucketName, lastBucketName, userBucketName, blocksBucketName, infoBucketName,
|
||||
readonlyBucketName, verifiedBucketName}
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
for _, bktName := range topBuckets {
|
||||
if _, e := tx.CreateBucketIfNotExists([]byte(bktName)); e != nil {
|
||||
return errors.Wrapf(e, "failed to create top level bucket %s", bktName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to create top level bucket)")
|
||||
}
|
||||
|
||||
result.dbs[site.SiteID] = db
|
||||
log.Printf("[DEBUG] bolt store created for %s", site.SiteID)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// Create saves new comment to store. Adds to posts bucket, reference to last and user bucket and increments count bucket
|
||||
func (b *BoltDB) Create(comment store.Comment) (commentID string, err error) {
|
||||
|
||||
bdb, err := b.db(comment.Locator.SiteID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if b.checkFlag(FlagRequest{Locator: comment.Locator, Flag: ReadOnly}) {
|
||||
return "", errors.Errorf("post %s is read-only", comment.Locator.URL)
|
||||
}
|
||||
|
||||
err = bdb.Update(func(tx *bolt.Tx) (err error) {
|
||||
var postBkt, lastBkt, userBkt *bolt.Bucket
|
||||
|
||||
if postBkt, err = b.makePostBucket(tx, comment.Locator.URL); err != nil {
|
||||
return err
|
||||
}
|
||||
// check if key already in store, reject doubles
|
||||
if postBkt.Get([]byte(comment.ID)) != nil {
|
||||
return errors.Errorf("key %s already in store", comment.ID)
|
||||
}
|
||||
|
||||
// serialize comment to json []byte for bolt and save
|
||||
if err = b.save(postBkt, comment.ID, comment); err != nil {
|
||||
return errors.Wrapf(err, "failed to put key %s to bucket %s", comment.ID, comment.Locator.URL)
|
||||
}
|
||||
|
||||
ref := b.makeRef(comment) // reference combines url and comment id
|
||||
|
||||
// add reference to comment to "last" bucket
|
||||
lastBkt = tx.Bucket([]byte(lastBucketName))
|
||||
commentTs := []byte(comment.Timestamp.Format(tsNano))
|
||||
if err = lastBkt.Put(commentTs, ref); err != nil {
|
||||
return errors.Wrapf(err, "can't put reference %s to %s", ref, lastBucketName)
|
||||
}
|
||||
|
||||
// add reference to commentID to "users" bucket
|
||||
if userBkt, err = b.getUserBucket(tx, comment.User.ID); err != nil {
|
||||
return errors.Wrapf(err, "can't get bucket %s", comment.User.ID)
|
||||
}
|
||||
// put into individual user's bucket with ts as a key
|
||||
if err = userBkt.Put(commentTs, ref); err != nil {
|
||||
return errors.Wrapf(err, "failed to put user comment %s for %s", comment.ID, comment.User.ID)
|
||||
}
|
||||
|
||||
// set info with the count for post url
|
||||
if _, err = b.setInfo(tx, comment); err != nil {
|
||||
return errors.Wrapf(err, "failed to set info for %s", comment.Locator)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return comment.ID, err
|
||||
}
|
||||
|
||||
// Get returns comment for locator.URL and commentID string
|
||||
func (b *BoltDB) Get(locator store.Locator, commentID string) (comment store.Comment, err error) {
|
||||
|
||||
bdb, err := b.db(locator.SiteID)
|
||||
if err != nil {
|
||||
return comment, err
|
||||
}
|
||||
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
bucket, e := b.getPostBucket(tx, locator.URL)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return b.load(bucket, commentID, &comment)
|
||||
})
|
||||
return comment, err
|
||||
}
|
||||
|
||||
// Find returns all comments for given request and sorts results
|
||||
func (b *BoltDB) Find(req FindRequest) (comments []store.Comment, err error) {
|
||||
comments = []store.Comment{}
|
||||
|
||||
bdb, err := b.db(req.Locator.SiteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch {
|
||||
case req.Locator.SiteID != "" && req.Locator.URL != "": // find comments for site and url
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
|
||||
bucket, e := b.getPostBucket(tx, req.Locator.URL)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
return bucket.ForEach(func(k, v []byte) error {
|
||||
comment := store.Comment{}
|
||||
if e = json.Unmarshal(v, &comment); e != nil {
|
||||
return errors.Wrap(e, "failed to unmarshal")
|
||||
}
|
||||
comments = append(comments, comment)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.UserID == "": // find last comments for site
|
||||
comments, err = b.lastComments(req.Locator.SiteID, req.Limit, req.Since)
|
||||
case req.Locator.SiteID != "" && req.UserID != "": // find comments for user
|
||||
comments, err = b.userComments(req.Locator.SiteID, req.UserID, req.Limit, req.Skip)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return SortComments(comments, req.Sort), nil
|
||||
}
|
||||
|
||||
// Flag sets and gets flag values
|
||||
func (b *BoltDB) Flag(req FlagRequest) (val bool, err error) {
|
||||
if req.Update == FlagNonSet { // read flag value, no update requested
|
||||
return b.checkFlag(req), nil
|
||||
}
|
||||
|
||||
// write flag value
|
||||
return b.setFlag(req)
|
||||
}
|
||||
|
||||
// Update for locator.URL with mutable part of comment
|
||||
func (b *BoltDB) Update(locator store.Locator, comment store.Comment) error {
|
||||
|
||||
if curComment, err := b.Get(locator, comment.ID); err == nil {
|
||||
// preserve immutable fields
|
||||
comment.ParentID = curComment.ParentID
|
||||
comment.Locator = curComment.Locator
|
||||
comment.Timestamp = curComment.Timestamp
|
||||
comment.User = curComment.User
|
||||
}
|
||||
|
||||
bdb, err := b.db(locator.SiteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return bdb.Update(func(tx *bolt.Tx) error {
|
||||
bucket, e := b.getPostBucket(tx, locator.URL)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
return b.save(bucket, comment.ID, comment)
|
||||
})
|
||||
}
|
||||
|
||||
// Count returns number of comments for post or user
|
||||
func (b *BoltDB) Count(req FindRequest) (count int, err error) {
|
||||
|
||||
bdb, err := b.db(req.Locator.SiteID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if req.Locator.URL != "" { // comment's count for post
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
var e error
|
||||
count, e = b.count(tx, req.Locator.URL, 0)
|
||||
return e
|
||||
})
|
||||
return count, err
|
||||
}
|
||||
|
||||
if req.UserID != "" { // comment's count for user
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
usersBkt := tx.Bucket([]byte(userBucketName))
|
||||
userIDBkt := usersBkt.Bucket([]byte(req.UserID))
|
||||
if userIDBkt == nil {
|
||||
return errors.Errorf("no comments for user %s in store for %s site", req.UserID, req.Locator.SiteID)
|
||||
}
|
||||
stats := userIDBkt.Stats()
|
||||
count = stats.KeyN
|
||||
return nil
|
||||
})
|
||||
return count, err
|
||||
}
|
||||
|
||||
return 0, errors.Errorf("invalid count request %+v", req)
|
||||
}
|
||||
|
||||
// Info get post(s) meta info
|
||||
func (b *BoltDB) Info(req InfoRequest) ([]store.PostInfo, error) {
|
||||
|
||||
bdb, err := b.db(req.Locator.SiteID)
|
||||
if err != nil {
|
||||
return []store.PostInfo{}, err
|
||||
}
|
||||
|
||||
if req.Locator.URL != "" { // post info
|
||||
info := store.PostInfo{}
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
if e := b.load(infoBkt, req.Locator.URL, &info); e != nil {
|
||||
return errors.Wrapf(e, "can't load info for %s", req.Locator.URL)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// set read-only from age and manual bucket
|
||||
readOnlyAge := req.ReadOnlyAge
|
||||
info.ReadOnly = readOnlyAge > 0 && !info.FirstTS.IsZero() && info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())
|
||||
if b.checkFlag(FlagRequest{Locator: req.Locator, Flag: ReadOnly}) {
|
||||
info.ReadOnly = true
|
||||
}
|
||||
return []store.PostInfo{info}, err
|
||||
}
|
||||
|
||||
if req.Locator.URL == "" && req.Locator.SiteID != "" { // site info (list)
|
||||
list := []store.PostInfo{}
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
postsBkt := tx.Bucket([]byte(postsBucketName))
|
||||
|
||||
c := postsBkt.Cursor()
|
||||
n := 0
|
||||
for k, _ := c.Last(); k != nil; k, _ = c.Prev() {
|
||||
n++
|
||||
if req.Skip > 0 && n <= req.Skip {
|
||||
continue
|
||||
}
|
||||
postURL := string(k)
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
info := store.PostInfo{}
|
||||
if e := b.load(infoBkt, postURL, &info); e != nil {
|
||||
return errors.Wrapf(e, "can't load info for %s", postURL)
|
||||
}
|
||||
list = append(list, info)
|
||||
if req.Limit > 0 && len(list) >= req.Limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return list, err
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("invalid info request %+v", req)
|
||||
}
|
||||
|
||||
// ListFlags get list of flagged keys, like blocked & verified user
|
||||
// works for full locator (post flags) or with userID
|
||||
func (b *BoltDB) ListFlags(siteID string, flag Flag) (res []interface{}, err error) {
|
||||
|
||||
bdb, e := b.db(siteID)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
|
||||
switch flag {
|
||||
case Verified:
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
usersBkt := tx.Bucket([]byte(verifiedBucketName))
|
||||
_ = usersBkt.ForEach(func(k, _ []byte) error {
|
||||
res = append(res, string(k))
|
||||
return nil
|
||||
})
|
||||
return nil
|
||||
})
|
||||
return res, err
|
||||
case Blocked:
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(blocksBucketName))
|
||||
return bucket.ForEach(func(k []byte, v []byte) error {
|
||||
ts, errParse := time.ParseInLocation(tsNano, string(v), time.Local)
|
||||
if errParse != nil {
|
||||
return errors.Wrap(errParse, "can't parse block ts")
|
||||
}
|
||||
if time.Now().Before(ts) {
|
||||
// get user name from comment user section
|
||||
userName := ""
|
||||
req := FindRequest{Locator: store.Locator{SiteID: siteID}, UserID: string(k), Limit: 1}
|
||||
userComments, errUser := b.Find(req)
|
||||
if errUser == nil && len(userComments) > 0 {
|
||||
userName = userComments[0].User.Name
|
||||
}
|
||||
res = append(res, store.BlockedUser{ID: string(k), Name: userName, Until: ts})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
})
|
||||
return res, err
|
||||
}
|
||||
return nil, errors.Errorf("flag %s not listable", flag)
|
||||
}
|
||||
|
||||
// Delete post(s) by id or by userID
|
||||
func (b *BoltDB) Delete(req DeleteRequest) error {
|
||||
|
||||
bdb, e := b.db(req.Locator.SiteID)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
switch {
|
||||
case req.Locator.URL != "" && req.CommentID != "":
|
||||
return b.deleteComment(bdb, req.Locator, req.CommentID, req.DeleteMode)
|
||||
case req.Locator.SiteID != "" && req.UserID != "" && req.CommentID == "":
|
||||
return b.deleteUser(bdb, req.Locator.SiteID, req.UserID)
|
||||
case req.Locator.SiteID != "" && req.Locator.URL == "" && req.CommentID == "" && req.UserID == "":
|
||||
return b.deleteAll(bdb, req.Locator.SiteID)
|
||||
}
|
||||
|
||||
return errors.Errorf("invalid delete request %+v", req)
|
||||
}
|
||||
|
||||
// Close boltdb store
|
||||
func (b *BoltDB) Close() error {
|
||||
errs := new(multierror.Error)
|
||||
for site, db := range b.dbs {
|
||||
err := errors.Wrapf(db.Close(), "can't close site %s", site)
|
||||
errs = multierror.Append(errs, err)
|
||||
}
|
||||
return errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
// Last returns up to max last comments for given siteID
|
||||
func (b *BoltDB) lastComments(siteID string, max int, since time.Time) (comments []store.Comment, err error) {
|
||||
|
||||
comments = []store.Comment{}
|
||||
|
||||
if max > lastLimit || max == 0 {
|
||||
max = lastLimit
|
||||
}
|
||||
|
||||
bdb, err := b.db(siteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
lastBkt := tx.Bucket([]byte(lastBucketName))
|
||||
c := lastBkt.Cursor()
|
||||
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
|
||||
if !since.IsZero() {
|
||||
// stop if reached "since" ts
|
||||
tsSince := []byte(since.Format(tsNano))
|
||||
if bytes.Compare(k, tsSince) <= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
url, commentID, e := b.parseRef(v)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
postBkt, e := b.getPostBucket(tx, url)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
comment := store.Comment{}
|
||||
if e = b.load(postBkt, commentID, &comment); e != nil {
|
||||
log.Printf("[WARN] can't load comment for %s from store %s", commentID, url)
|
||||
continue
|
||||
}
|
||||
if comment.Deleted {
|
||||
continue
|
||||
}
|
||||
comments = append(comments, comment)
|
||||
if len(comments) >= max {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return comments, err
|
||||
}
|
||||
|
||||
// userComments extracts all comments for given site and given userID
|
||||
// "users" bucket has sub-bucket for each userID, and keeps it as ts:ref
|
||||
func (b *BoltDB) userComments(siteID, userID string, limit, skip int) (comments []store.Comment, err error) {
|
||||
|
||||
comments = []store.Comment{}
|
||||
commentRefs := []string{}
|
||||
|
||||
bdb, err := b.db(siteID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if limit == 0 || limit > userLimit {
|
||||
limit = userLimit
|
||||
}
|
||||
|
||||
// get list of references to comments
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
usersBkt := tx.Bucket([]byte(userBucketName))
|
||||
userIDBkt := usersBkt.Bucket([]byte(userID))
|
||||
if userIDBkt == nil {
|
||||
return errors.Errorf("no comments for user %s in store", userID)
|
||||
}
|
||||
|
||||
c := userIDBkt.Cursor()
|
||||
skipComments := 0
|
||||
for k, v := c.Last(); k != nil; k, v = c.Prev() {
|
||||
if len(commentRefs) >= limit {
|
||||
break
|
||||
}
|
||||
if skip > 0 && skipComments < skip {
|
||||
skipComments++
|
||||
continue
|
||||
}
|
||||
commentRefs = append(commentRefs, string(v))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return comments, err
|
||||
}
|
||||
|
||||
// retrieve comments for refs
|
||||
for _, v := range commentRefs {
|
||||
url, commentID, errParse := b.parseRef([]byte(v))
|
||||
if errParse != nil {
|
||||
return comments, errors.Wrapf(errParse, "can't parse reference %s", v)
|
||||
}
|
||||
if c, errRef := b.Get(store.Locator{SiteID: siteID, URL: url}, commentID); errRef == nil {
|
||||
comments = append(comments, c)
|
||||
}
|
||||
}
|
||||
|
||||
return comments, err
|
||||
}
|
||||
|
||||
func (b *BoltDB) checkFlag(req FlagRequest) (val bool) {
|
||||
|
||||
bdb, err := b.db(req.Locator.SiteID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
key := req.Locator.URL
|
||||
if req.UserID != "" {
|
||||
key = req.UserID
|
||||
}
|
||||
|
||||
if req.Flag == Blocked {
|
||||
var blocked bool
|
||||
_ = bdb.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(blocksBucketName))
|
||||
v := bucket.Get([]byte(key))
|
||||
if v == nil {
|
||||
blocked = false
|
||||
return nil
|
||||
}
|
||||
|
||||
until, e := time.Parse(tsNano, string(v))
|
||||
if e != nil {
|
||||
blocked = false
|
||||
return nil
|
||||
}
|
||||
blocked = time.Now().Before(until)
|
||||
return nil
|
||||
})
|
||||
return blocked
|
||||
}
|
||||
|
||||
_ = bdb.View(func(tx *bolt.Tx) error {
|
||||
var bucket *bolt.Bucket
|
||||
if bucket, err = b.flagBucket(tx, req.Flag); err != nil {
|
||||
return err
|
||||
}
|
||||
val = bucket.Get([]byte(key)) != nil
|
||||
return nil
|
||||
})
|
||||
return val
|
||||
}
|
||||
|
||||
func (b *BoltDB) setFlag(req FlagRequest) (res bool, err error) {
|
||||
bdb, e := b.db(req.Locator.SiteID)
|
||||
if e != nil {
|
||||
return false, e
|
||||
}
|
||||
|
||||
key := req.Locator.URL
|
||||
if req.UserID != "" {
|
||||
key = req.UserID
|
||||
}
|
||||
|
||||
err = bdb.Update(func(tx *bolt.Tx) error {
|
||||
var bucket *bolt.Bucket
|
||||
if bucket, err = b.flagBucket(tx, req.Flag); err != nil {
|
||||
return err
|
||||
}
|
||||
switch req.Update {
|
||||
case FlagTrue:
|
||||
if req.Flag == Blocked {
|
||||
val := time.Now().AddDate(100, 0, 0).Format(tsNano) // permanent is 100 year
|
||||
if req.TTL > 0 {
|
||||
val = time.Now().Add(req.TTL).Format(tsNano)
|
||||
}
|
||||
if e = bucket.Put([]byte(key), []byte(val)); e != nil {
|
||||
return errors.Wrapf(e, "failed to put blocked to %s", key)
|
||||
}
|
||||
res = true
|
||||
return nil
|
||||
}
|
||||
|
||||
if e = bucket.Put([]byte(key), []byte(time.Now().Format(tsNano))); e != nil {
|
||||
return errors.Wrapf(e, "failed to set flag %s for %s", req.Flag, req.Locator.URL)
|
||||
}
|
||||
res = true
|
||||
return nil
|
||||
case FlagFalse:
|
||||
if e = bucket.Delete([]byte(key)); e != nil {
|
||||
return errors.Wrapf(e, "failed to clean flag %s for %s", req.Flag, req.Locator.URL)
|
||||
}
|
||||
res = false
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
func (b *BoltDB) flagBucket(tx *bolt.Tx, flag Flag) (bkt *bolt.Bucket, err error) {
|
||||
switch flag {
|
||||
case ReadOnly:
|
||||
bkt = tx.Bucket([]byte(readonlyBucketName))
|
||||
case Blocked:
|
||||
bkt = tx.Bucket([]byte(blocksBucketName))
|
||||
case Verified:
|
||||
bkt = tx.Bucket([]byte(verifiedBucketName))
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported flag %v", flag)
|
||||
}
|
||||
return bkt, nil
|
||||
}
|
||||
|
||||
func (b *BoltDB) deleteComment(bdb *bolt.DB, locator store.Locator, commentID string, mode store.DeleteMode) error {
|
||||
|
||||
return bdb.Update(func(tx *bolt.Tx) error {
|
||||
|
||||
postBkt, e := b.getPostBucket(tx, locator.URL)
|
||||
if e != nil {
|
||||
return e
|
||||
}
|
||||
|
||||
comment := store.Comment{}
|
||||
if e = b.load(postBkt, commentID, &comment); e != nil {
|
||||
return errors.Wrapf(e, "can't load key %s from bucket %s", commentID, locator.URL)
|
||||
}
|
||||
// set deleted status and clear fields
|
||||
comment.SetDeleted(mode)
|
||||
|
||||
if e = b.save(postBkt, commentID, comment); e != nil {
|
||||
return errors.Wrapf(e, "can't save deleted comment for key %s from bucket %s", commentID, locator.URL)
|
||||
}
|
||||
|
||||
// delete from "last" bucket
|
||||
lastBkt := tx.Bucket([]byte(lastBucketName))
|
||||
if e = lastBkt.Delete([]byte(commentID)); e != nil {
|
||||
return errors.Wrapf(e, "can't delete key %s from bucket %s", commentID, lastBucketName)
|
||||
}
|
||||
|
||||
// decrement comments count for post url
|
||||
if _, e = b.count(tx, comment.Locator.URL, -1); e != nil {
|
||||
return errors.Wrapf(e, "failed to decrement count for %s", comment.Locator)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// deleteAll removes all top-level buckets for given siteID
|
||||
func (b *BoltDB) deleteAll(bdb *bolt.DB, siteID string) error {
|
||||
|
||||
// delete all buckets except blocked users
|
||||
toDelete := []string{postsBucketName, lastBucketName, userBucketName, infoBucketName}
|
||||
|
||||
// delete top-level buckets
|
||||
err := bdb.Update(func(tx *bolt.Tx) error {
|
||||
for _, bktName := range toDelete {
|
||||
|
||||
if e := tx.DeleteBucket([]byte(bktName)); e != nil {
|
||||
return errors.Wrapf(e, "failed to delete top level bucket %s", bktName)
|
||||
}
|
||||
if _, e := tx.CreateBucketIfNotExists([]byte(bktName)); e != nil {
|
||||
return errors.Wrapf(e, "failed to create top level bucket %s", bktName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return errors.Wrapf(err, "failed to delete top level buckets from site %s", siteID)
|
||||
}
|
||||
|
||||
// deleteUser removes all comments for given user. Everything will be market as deleted
|
||||
// and user name and userID will be changed to "deleted". Also removes from last and from user buckets.
|
||||
func (b *BoltDB) deleteUser(bdb *bolt.DB, siteID string, userID string) error {
|
||||
bdb, err := b.db(siteID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// get list of all comments outside of transaction loop
|
||||
posts, err := b.Info(InfoRequest{Locator: store.Locator{SiteID: siteID}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type commentInfo struct {
|
||||
locator store.Locator
|
||||
commentID string
|
||||
}
|
||||
|
||||
// get list of commentID for all user's comment
|
||||
comments := []commentInfo{}
|
||||
for _, postInfo := range posts {
|
||||
err = bdb.View(func(tx *bolt.Tx) error {
|
||||
postsBkt := tx.Bucket([]byte(postsBucketName))
|
||||
postBkt := postsBkt.Bucket([]byte(postInfo.URL))
|
||||
err = postBkt.ForEach(func(postURL []byte, commentVal []byte) error {
|
||||
comment := store.Comment{}
|
||||
if err = json.Unmarshal(commentVal, &comment); err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal")
|
||||
}
|
||||
if comment.User.ID == userID {
|
||||
comments = append(comments, commentInfo{locator: comment.Locator, commentID: comment.ID})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return errors.Wrapf(err, "failed to collect list of comments for deletion from %s", postInfo.URL)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] comments for removal=%d", len(comments))
|
||||
|
||||
// delete collected comments
|
||||
for _, ci := range comments {
|
||||
if e := b.deleteComment(bdb, ci.locator, ci.commentID, store.HardDelete); e != nil {
|
||||
return errors.Wrapf(err, "failed to delete comment %+v", ci)
|
||||
}
|
||||
}
|
||||
|
||||
// delete user bucket
|
||||
err = bdb.Update(func(tx *bolt.Tx) error {
|
||||
usersBkt := tx.Bucket([]byte(userBucketName))
|
||||
if usersBkt != nil {
|
||||
if e := usersBkt.DeleteBucket([]byte(userID)); e != nil {
|
||||
return errors.Wrapf(err, "failed to delete user bucket for %s", userID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't delete user meta")
|
||||
}
|
||||
|
||||
if len(comments) == 0 {
|
||||
return errors.Errorf("unknown user %s", userID)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// getPostBucket return bucket with all comments for postURL
|
||||
func (b *BoltDB) getPostBucket(tx *bolt.Tx, postURL string) (*bolt.Bucket, error) {
|
||||
postsBkt := tx.Bucket([]byte(postsBucketName))
|
||||
if postsBkt == nil {
|
||||
return nil, errors.Errorf("no bucket %s", postsBucketName)
|
||||
}
|
||||
res := postsBkt.Bucket([]byte(postURL))
|
||||
if res == nil {
|
||||
return nil, errors.Errorf("no bucket %s in store", postURL)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// makePostBucket create new bucket for postURL as a key. This bucket holds all comments for the post.
|
||||
func (b *BoltDB) makePostBucket(tx *bolt.Tx, postURL string) (*bolt.Bucket, error) {
|
||||
postsBkt := tx.Bucket([]byte(postsBucketName))
|
||||
if postsBkt == nil {
|
||||
return nil, errors.Errorf("no bucket %s", postsBucketName)
|
||||
}
|
||||
res, err := postsBkt.CreateBucketIfNotExists([]byte(postURL))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "no bucket %s in store", postURL)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (b *BoltDB) getUserBucket(tx *bolt.Tx, userID string) (*bolt.Bucket, error) {
|
||||
usersBkt := tx.Bucket([]byte(userBucketName))
|
||||
userIDBkt, e := usersBkt.CreateBucketIfNotExists([]byte(userID)) // get bucket for userID
|
||||
if e != nil {
|
||||
return nil, errors.Wrapf(e, "can't get bucket %s", userID)
|
||||
}
|
||||
return userIDBkt, nil
|
||||
}
|
||||
|
||||
// save marshaled value to key for bucket. Should run in update tx
|
||||
func (b *BoltDB) save(bkt *bolt.Bucket, key string, value interface{}) (err error) {
|
||||
if value == nil {
|
||||
return errors.Errorf("can't save nil value for %s", key)
|
||||
}
|
||||
jdata, jerr := json.Marshal(value)
|
||||
if jerr != nil {
|
||||
return errors.Wrap(jerr, "can't marshal comment")
|
||||
}
|
||||
if err = bkt.Put([]byte(key), jdata); err != nil {
|
||||
return errors.Wrapf(err, "failed to save key %s", key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// load and unmarshal json value by key from bucket. Should run in view tx
|
||||
func (b *BoltDB) load(bkt *bolt.Bucket, key string, res interface{}) error {
|
||||
value := bkt.Get([]byte(key))
|
||||
if value == nil {
|
||||
return errors.Errorf("no value for %s", key)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(value, &res); err != nil {
|
||||
return errors.Wrap(err, "failed to unmarshal")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// count adds val to counts key postURL. val can be negative to subtract. if val 0 can be used as accessor
|
||||
// it uses separate counts bucket because boltdb Stat call is very slow
|
||||
func (b *BoltDB) count(tx *bolt.Tx, postURL string, val int) (int, error) {
|
||||
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
|
||||
info := store.PostInfo{}
|
||||
if err := b.load(infoBkt, postURL, &info); err != nil {
|
||||
info = store.PostInfo{}
|
||||
}
|
||||
if val == 0 { // get current count, don't update
|
||||
return info.Count, nil
|
||||
}
|
||||
info.Count += val
|
||||
|
||||
return info.Count, b.save(infoBkt, postURL, &info)
|
||||
}
|
||||
|
||||
func (b *BoltDB) setInfo(tx *bolt.Tx, comment store.Comment) (store.PostInfo, error) {
|
||||
infoBkt := tx.Bucket([]byte(infoBucketName))
|
||||
info := store.PostInfo{}
|
||||
if err := b.load(infoBkt, comment.Locator.URL, &info); err != nil {
|
||||
info = store.PostInfo{
|
||||
Count: 0,
|
||||
URL: comment.Locator.URL,
|
||||
FirstTS: comment.Timestamp,
|
||||
LastTS: comment.Timestamp,
|
||||
}
|
||||
}
|
||||
info.Count++
|
||||
info.LastTS = comment.Timestamp
|
||||
return info, b.save(infoBkt, comment.Locator.URL, &info)
|
||||
}
|
||||
|
||||
func (b *BoltDB) db(siteID string) (*bolt.DB, error) {
|
||||
if res, ok := b.dbs[siteID]; ok {
|
||||
return res, nil
|
||||
}
|
||||
return nil, errors.Errorf("site %q not found", siteID)
|
||||
}
|
||||
|
||||
// makeRef creates reference combining url and comment id
|
||||
func (b *BoltDB) makeRef(comment store.Comment) []byte {
|
||||
return []byte(fmt.Sprintf("%s!!%s", comment.Locator.URL, comment.ID))
|
||||
}
|
||||
|
||||
// parseRef gets parts of reference
|
||||
func (b *BoltDB) parseRef(val []byte) (url string, id string, err error) {
|
||||
elems := strings.Split(string(val), "!!")
|
||||
if len(elems) != 2 {
|
||||
return "", "", errors.Errorf("invalid reference value %s", string(val))
|
||||
}
|
||||
return elems[0], elems[1], nil
|
||||
}
|
||||
@@ -0,0 +1,774 @@
|
||||
package engine2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
bolt "github.com/coreos/bbolt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
|
||||
var testDb = "/tmp/test-remark.db"
|
||||
|
||||
func TestBoltDB_CreateAndFind(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
|
||||
res, err := b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text)
|
||||
assert.Equal(t, "user1", res[0].User.ID)
|
||||
t.Log(res[0].ID)
|
||||
|
||||
_, err = b.Create(store.Comment{ID: res[0].ID, Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}})
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "key id-1 already in store", err.Error())
|
||||
|
||||
req = FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-bad"}, Sort: "time"}
|
||||
_, err = b.Find(req)
|
||||
assert.EqualError(t, err, `site "radio-t-bad" not found`)
|
||||
|
||||
assert.NoError(t, b.Close())
|
||||
}
|
||||
|
||||
func TestBoltDB_CreateFailedReadOnly(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
comment := store.Comment{
|
||||
ID: "id-ro",
|
||||
Text: `some text, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/ro", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
|
||||
flagReq := FlagRequest{Locator: comment.Locator, Flag: ReadOnly, Update: FlagTrue}
|
||||
v, err := b.Flag(flagReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, v)
|
||||
|
||||
_, err = b.Create(comment)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, "post https://radio-t.com/ro is read-only", err.Error())
|
||||
|
||||
flagReq = FlagRequest{Locator: comment.Locator, Flag: ReadOnly, Update: FlagFalse}
|
||||
v, err = b.Flag(flagReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, false, v)
|
||||
|
||||
_, err = b.Create(comment)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBoltDB_Get(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
|
||||
res, err := b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res), "2 records initially")
|
||||
|
||||
comment, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "some text2", comment.Text)
|
||||
|
||||
comment, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567")
|
||||
assert.NotNil(t, err)
|
||||
|
||||
_, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, res[1].ID)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
}
|
||||
|
||||
func TestBoltDB_Update(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
|
||||
res, err := b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res), "2 records initially")
|
||||
|
||||
comment := res[0]
|
||||
comment.Text = "abc 123"
|
||||
comment.Score = 100
|
||||
err = b.Update(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, comment)
|
||||
assert.NoError(t, err)
|
||||
|
||||
comment, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "abc 123", comment.Text)
|
||||
assert.Equal(t, res[0].ID, comment.ID)
|
||||
assert.Equal(t, 100, comment.Score)
|
||||
|
||||
err = b.Update(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, comment)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
|
||||
err = b.Update(store.Locator{URL: "https://radio-t.com-bad", SiteID: "radio-t"}, comment)
|
||||
assert.EqualError(t, err, `no bucket https://radio-t.com-bad in store`)
|
||||
}
|
||||
|
||||
func TestBoltDB_FindLast(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time"}
|
||||
res, err := b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
|
||||
req.Limit = 1
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
|
||||
req.Locator.SiteID = "bad"
|
||||
_, err = b.Find(req)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
}
|
||||
|
||||
func TestBoltDB_FindLastSince(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
ts := time.Date(2017, 12, 20, 15, 18, 21, 0, time.Local)
|
||||
req := FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", Since: ts}
|
||||
res, err := b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
|
||||
req.Since = time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local)
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
|
||||
req.Since = time.Date(2017, 12, 20, 16, 18, 22, 0, time.Local)
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(res))
|
||||
}
|
||||
|
||||
func TestBoltDB_FindForUser(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 5}
|
||||
res, err := b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
|
||||
|
||||
req = FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1}
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(res), "allow 1 comment")
|
||||
assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
|
||||
|
||||
req = FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(res), "allow 1 comment")
|
||||
assert.Equal(t, `some text, <a href="http://radio-t.com">link</a>`, res[0].Text, "second comment")
|
||||
|
||||
req = FindRequest{Locator: store.Locator{SiteID: "bad"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
|
||||
_, err = b.Find(req)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
|
||||
req = FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "userZ", Limit: 1, Skip: 1}
|
||||
_, err = b.Find(req)
|
||||
assert.EqualError(t, err, `no comments for user userZ in store`)
|
||||
}
|
||||
|
||||
func TestBoltDB_FindForUserPagination(t *testing.T) {
|
||||
_ = os.Remove(testDb)
|
||||
b, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
require.Nil(t, err)
|
||||
|
||||
defer func() {
|
||||
require.NoError(t, b.Close())
|
||||
_ = os.Remove(testDb)
|
||||
}()
|
||||
|
||||
c := store.Comment{
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
|
||||
// write 200 comments
|
||||
for i := 0; i < 200; i++ {
|
||||
c.ID = fmt.Sprintf("id-%d", i)
|
||||
c.Text = fmt.Sprintf("text #%d", i)
|
||||
c.Timestamp = time.Date(2017, 12, 20, 15, 18, i, 0, time.Local)
|
||||
_, err = b.Create(c)
|
||||
require.Nil(t, err)
|
||||
}
|
||||
|
||||
// get all comments
|
||||
req := FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1"}
|
||||
res, err := b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 200, len(res))
|
||||
assert.Equal(t, "id-199", res[0].ID)
|
||||
|
||||
// seek 0, 5 comments
|
||||
req.Limit = 5
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, len(res))
|
||||
assert.Equal(t, "id-199", res[0].ID)
|
||||
assert.Equal(t, "id-195", res[4].ID)
|
||||
|
||||
// seek 10, 3 comments
|
||||
req.Skip, req.Limit = 10, 3
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(res))
|
||||
assert.Equal(t, "id-189", res[0].ID)
|
||||
assert.Equal(t, "id-187", res[2].ID)
|
||||
|
||||
// seek 195, ask 10 comments
|
||||
req.Skip, req.Limit = 195, 10
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 5, len(res))
|
||||
assert.Equal(t, "id-4", res[0].ID)
|
||||
assert.Equal(t, "id-0", res[4].ID)
|
||||
|
||||
// seek 255, ask 10 comments
|
||||
req.Skip, req.Limit = 255, 10
|
||||
res, err = b.Find(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(res))
|
||||
}
|
||||
|
||||
func TestBoltDB_CountPost(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}}
|
||||
c, err := b.Count(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, c)
|
||||
|
||||
req = FindRequest{Locator: store.Locator{URL: "https://radio-t.com-xxx", SiteID: "radio-t"}}
|
||||
c, err = b.Count(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, c)
|
||||
|
||||
req = FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "bad"}}
|
||||
_, err = b.Count(req)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
}
|
||||
|
||||
func TestBoltDB_CountUser(t *testing.T) {
|
||||
var b, teardown = prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
|
||||
c, err := b.Count(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, c)
|
||||
|
||||
req = FindRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1"}
|
||||
_, err = b.Count(req)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
|
||||
req = FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "userZ"}
|
||||
_, err = b.Count(req)
|
||||
assert.EqualError(t, err, `no comments for user userZ in store for radio-t site`)
|
||||
}
|
||||
|
||||
func TestBoltDB_InfoPost(t *testing.T) {
|
||||
b, teardown := prep(t) // two comments for https://radio-t.com
|
||||
defer teardown()
|
||||
|
||||
ts := func(min int) time.Time { return time.Date(2017, 12, 20, 15, 18, min, 0, time.Local) }
|
||||
|
||||
// add one more for https://radio-t.com/2
|
||||
comment := store.Comment{
|
||||
ID: "12345",
|
||||
Text: `some text, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 24, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err := b.Create(comment)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
|
||||
r, err := b.Info(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24)}}, r)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 10}
|
||||
r, err = b.Info(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24),
|
||||
ReadOnly: true}}, r)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, ReadOnlyAge: 0}
|
||||
r, err = b.Info(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, r)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/error", SiteID: "radio-t"}, ReadOnlyAge: 0}
|
||||
_, err = b.Info(req)
|
||||
require.NotNil(t, err)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-error"}, ReadOnlyAge: 0}
|
||||
_, err = b.Info(req)
|
||||
require.NotNil(t, err)
|
||||
|
||||
fr := FlagRequest{Flag: ReadOnly, Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, Update: FlagTrue}
|
||||
_, err = b.Flag(fr)
|
||||
require.NoError(t, err)
|
||||
req = InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
|
||||
r, err = b.Info(req)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24),
|
||||
ReadOnly: true}}, r)
|
||||
}
|
||||
|
||||
func TestBoltDB_InfoList(t *testing.T) {
|
||||
b, teardown := prep(t) // two comments for https://radio-t.com
|
||||
defer teardown()
|
||||
|
||||
// add one more for https://radio-t.com/2
|
||||
comment := store.Comment{
|
||||
ID: "12345",
|
||||
Text: `some text, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err := b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
|
||||
ts := func(sec int) time.Time { return time.Date(2017, 12, 20, 15, 18, sec, 0, time.Local) }
|
||||
|
||||
req := InfoRequest{Locator: store.Locator{SiteID: "radio-t"}}
|
||||
res, err := b.Info(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
|
||||
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: -1, Skip: -1}
|
||||
res, err = b.Info(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
|
||||
{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1}
|
||||
res, err = b.Info(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}}, res)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1, Skip: 1}
|
||||
res, err = b.Info(req)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
|
||||
|
||||
req = InfoRequest{Locator: store.Locator{SiteID: "bad"}, Limit: 1, Skip: 1}
|
||||
_, err = b.Info(req)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
}
|
||||
|
||||
func TestBolt_FlagBlockedUser(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FlagRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
|
||||
val, err := b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, val, "nothing blocked yet")
|
||||
|
||||
req = FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Update: FlagTrue}
|
||||
_, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
val, err = b.Flag(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, val, "user1 blocked")
|
||||
|
||||
req = FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Update: FlagTrue}
|
||||
_, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
val, err = b.Flag(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, val, "user1 still blocked")
|
||||
|
||||
req = FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Update: FlagFalse}
|
||||
_, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
val, err = b.Flag(FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, val, "user1 unblocked")
|
||||
|
||||
req = FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "bad"}, UserID: "user1", Update: FlagTrue}
|
||||
_, err = b.Flag(req)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
|
||||
req = FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "userX", Update: FlagTrue}
|
||||
_, err = b.Flag(req)
|
||||
assert.NoError(t, err, "non-existing user can't be blocked")
|
||||
|
||||
req = FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: "radio-t-bad"}, UserID: "user1"}
|
||||
val, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, val, "nothing blocked on wrong site")
|
||||
}
|
||||
|
||||
func TestBolt_FlagReadOnlyPost(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
req := FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: ReadOnly}
|
||||
val, err := b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, val, "nothing ro")
|
||||
|
||||
req = FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: ReadOnly, Update: FlagTrue}
|
||||
val, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
req = FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: ReadOnly}
|
||||
val, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, val, "url-1 ro")
|
||||
|
||||
req = FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-2"}, Flag: ReadOnly}
|
||||
val, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, val, "url-2 still writable")
|
||||
|
||||
req = FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: ReadOnly, Update: FlagFalse}
|
||||
_, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
req = FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: ReadOnly}
|
||||
val, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, val, "url-1 writable")
|
||||
|
||||
req = FlagRequest{Locator: store.Locator{SiteID: "bad", URL: "url-1"}, Flag: ReadOnly, Update: FlagFalse}
|
||||
_, err = b.Flag(req)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
|
||||
req = FlagRequest{Locator: store.Locator{SiteID: "radio-t-bad", URL: "url-1"}, Flag: ReadOnly}
|
||||
val, err = b.Flag(req)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, val, "nothing ro on wrong site")
|
||||
}
|
||||
|
||||
func TestBolt_FlagVerified(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
isVerified := func(site, user string) bool {
|
||||
req := FlagRequest{Flag: Verified, Locator: store.Locator{SiteID: site}, UserID: user}
|
||||
v, err := b.Flag(req)
|
||||
require.NoError(t, err)
|
||||
return v
|
||||
}
|
||||
|
||||
setVerified := func(site, user string, status FlagStatus) error {
|
||||
req := FlagRequest{Flag: Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
|
||||
_, err := b.Flag(req)
|
||||
return err
|
||||
}
|
||||
|
||||
assert.False(t, isVerified("radio-t", "u1"), "nothing verified")
|
||||
|
||||
assert.NoError(t, setVerified("radio-t", "u1", FlagTrue))
|
||||
assert.True(t, isVerified("radio-t", "u1"), "u1 verified")
|
||||
|
||||
assert.False(t, isVerified("radio-t", "u2"), "u2 still not verified")
|
||||
assert.NoError(t, setVerified("radio-t", "u1", FlagFalse))
|
||||
assert.False(t, isVerified("radio-t", "u1"), "u1 not verified anymore")
|
||||
|
||||
assert.EqualError(t, setVerified("bad", "u1", FlagTrue), `site "bad" not found`)
|
||||
assert.NoError(t, setVerified("radio-t", "u1xyz", FlagFalse))
|
||||
|
||||
assert.False(t, isVerified("radio-t-bad", "u1"), "nothing verified on wrong site")
|
||||
|
||||
assert.NoError(t, setVerified("radio-t", "u1", FlagTrue))
|
||||
assert.NoError(t, setVerified("radio-t", "u2", FlagTrue))
|
||||
assert.NoError(t, setVerified("radio-t", "u3", FlagFalse))
|
||||
}
|
||||
|
||||
func TestBolt_FlagListVerified(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
toIDs := func(inp []interface{}) (res []string) {
|
||||
res = make([]string, len(inp))
|
||||
for i, v := range inp {
|
||||
vv, ok := v.(string)
|
||||
require.True(t, ok)
|
||||
res[i] = vv
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
setVerified := func(site, user string, status FlagStatus) error {
|
||||
req := FlagRequest{Flag: Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
|
||||
_, err := b.Flag(req)
|
||||
return err
|
||||
}
|
||||
|
||||
ids, err := b.ListFlags("radio-t", Verified)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{}, toIDs(ids), "verified list empty")
|
||||
|
||||
assert.NoError(t, setVerified("radio-t", "u1", FlagTrue))
|
||||
assert.NoError(t, setVerified("radio-t", "u2", FlagTrue))
|
||||
ids, err = b.ListFlags("radio-t", Verified)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"u1", "u2"}, toIDs(ids), "verified 2 ids")
|
||||
|
||||
_, err = b.ListFlags("radio-t-bad", Verified)
|
||||
assert.Error(t, err, "site \"radio-t-bad\" not found", "fail on wrong site")
|
||||
}
|
||||
|
||||
func TestBolt_FlagListBlocked(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
setBlocked := func(site, user string, status FlagStatus, ttl time.Duration) error {
|
||||
req := FlagRequest{Flag: Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status, TTL: ttl}
|
||||
_, err := b.Flag(req)
|
||||
return err
|
||||
}
|
||||
|
||||
toBlocked := func(inp []interface{}) (res []store.BlockedUser) {
|
||||
res = make([]store.BlockedUser, len(inp))
|
||||
for i, v := range inp {
|
||||
vv, ok := v.(store.BlockedUser)
|
||||
require.True(t, ok)
|
||||
res[i] = vv
|
||||
}
|
||||
return res
|
||||
}
|
||||
assert.NoError(t, setBlocked("radio-t", "user1", FlagTrue, 0))
|
||||
assert.NoError(t, setBlocked("radio-t", "user2", FlagTrue, 50*time.Millisecond))
|
||||
assert.NoError(t, setBlocked("radio-t", "user3", FlagFalse, 0))
|
||||
|
||||
vv, err := b.ListFlags("radio-t", Blocked)
|
||||
assert.NoError(t, err)
|
||||
|
||||
blockedList := toBlocked(vv)
|
||||
assert.Equal(t, 2, len(blockedList))
|
||||
assert.Equal(t, "user1", blockedList[0].ID)
|
||||
assert.Equal(t, "user2", blockedList[1].ID)
|
||||
t.Logf("%+v", blockedList)
|
||||
|
||||
// check block expiration
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
vv, err = b.ListFlags("radio-t", Blocked)
|
||||
assert.NoError(t, err)
|
||||
blockedList = toBlocked(vv)
|
||||
assert.Equal(t, 1, len(blockedList))
|
||||
assert.Equal(t, "user1", blockedList[0].ID)
|
||||
|
||||
_, err = b.ListFlags("bad", Blocked)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
|
||||
}
|
||||
|
||||
func TestBolt_DeleteComment(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
reqReq := FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
|
||||
res, err := b.Find(reqReq)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res), "initially 2 comments")
|
||||
|
||||
count, err := b.Count(reqReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count, "count=2 initially")
|
||||
|
||||
delReq := DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
CommentID: res[0].ID, DeleteMode: store.SoftDelete}
|
||||
|
||||
err = b.Delete(delReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err = b.Find(reqReq)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, "", res[0].Text)
|
||||
assert.True(t, res[0].Deleted, "marked deleted")
|
||||
assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
|
||||
|
||||
assert.Equal(t, "some text2", res[1].Text)
|
||||
assert.False(t, res[1].Deleted)
|
||||
|
||||
comments, err := b.Find(FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 1, len(comments), "1 in last, 1 removed")
|
||||
|
||||
count, err = b.Count(reqReq)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, count)
|
||||
|
||||
delReq.CommentID = "123456"
|
||||
err = b.Delete(delReq)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
delReq.Locator.SiteID = "bad"
|
||||
delReq.CommentID = res[0].ID
|
||||
err = b.Delete(delReq)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
|
||||
delReq.Locator = store.Locator{URL: "https://radio-t.com/bad", SiteID: "radio-t"}
|
||||
err = b.Delete(delReq)
|
||||
assert.EqualError(t, err, `no bucket https://radio-t.com/bad in store`)
|
||||
}
|
||||
|
||||
func TestBolt_DeleteHard(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
reqReq := FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
|
||||
res, err := b.Find(reqReq)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res), "initially 2 comments")
|
||||
|
||||
delReq := DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
CommentID: res[0].ID, DeleteMode: store.HardDelete}
|
||||
err = b.Delete(delReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
res, err = b.Find(reqReq)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, "", res[0].Text)
|
||||
assert.True(t, res[0].Deleted, "marked deleted")
|
||||
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
|
||||
}
|
||||
|
||||
func TestBolt_DeleteAll(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
delReq := DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}}
|
||||
err := b.Delete(delReq)
|
||||
assert.NoError(t, err)
|
||||
|
||||
comments, err := b.Find(FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, len(comments), "nothing left")
|
||||
|
||||
delReq = DeleteRequest{Locator: store.Locator{SiteID: "bad"}}
|
||||
err = b.Delete(delReq)
|
||||
assert.EqualError(t, err, `site "bad" not found`)
|
||||
}
|
||||
|
||||
func TestBoltAdmin_DeleteUser(t *testing.T) {
|
||||
|
||||
b, teardown := prep(t)
|
||||
defer teardown()
|
||||
|
||||
err := b.Delete(DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
|
||||
require.NoError(t, err)
|
||||
|
||||
comments, err := b.Find(FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}, Sort: "time"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 2, len(comments), "2 comments with deleted info")
|
||||
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User)
|
||||
assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User)
|
||||
|
||||
c, err := b.Count(FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 0, c, "0 count")
|
||||
|
||||
_, err = b.Find(FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5})
|
||||
assert.EqualError(t, err, "no comments for user user1 in store")
|
||||
|
||||
comments, err = b.Find(FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"})
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 0, len(comments), "nothing left")
|
||||
|
||||
err = b.Delete(DeleteRequest{Locator: store.Locator{SiteID: "radio-t-bad"}, UserID: "user1"})
|
||||
assert.EqualError(t, err, `site "radio-t-bad" not found`)
|
||||
}
|
||||
|
||||
func TestBoltDB_ref(t *testing.T) {
|
||||
b := BoltDB{}
|
||||
comment := store.Comment{
|
||||
ID: "12345",
|
||||
Text: `some text, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
res := b.makeRef(comment)
|
||||
assert.Equal(t, "https://radio-t.com/2!!12345", string(res))
|
||||
|
||||
url, id, err := b.parseRef([]byte("https://radio-t.com/2!!12345"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "https://radio-t.com/2", url)
|
||||
assert.Equal(t, "12345", id)
|
||||
|
||||
_, _, err = b.parseRef([]byte("https://radio-t.com/2"))
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestBoltDB_NewFailed(t *testing.T) {
|
||||
_, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: "/tmp/no-such-place/tmp.db", SiteID: "radio-t"})
|
||||
assert.EqualError(t, err, "failed to make boltdb for /tmp/no-such-place/tmp.db: open /tmp/no-such-place/tmp.db: no such file or directory")
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
func prep(t *testing.T) (b *BoltDB, teardown func()) {
|
||||
_ = os.Remove(testDb)
|
||||
|
||||
boltStore, err := NewBoltDB(bolt.Options{}, BoltSite{FileName: testDb, SiteID: "radio-t"})
|
||||
assert.Nil(t, err)
|
||||
b = boltStore
|
||||
|
||||
comment := store.Comment{
|
||||
ID: "id-1",
|
||||
Text: `some text, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
|
||||
comment = store.Comment{
|
||||
ID: "id-2",
|
||||
Text: "some text2",
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err = b.Create(comment)
|
||||
assert.Nil(t, err)
|
||||
|
||||
teardown = func() {
|
||||
require.NoError(t, b.Close())
|
||||
_ = os.Remove(testDb)
|
||||
}
|
||||
return b, teardown
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package engine2
|
||||
|
||||
// Package engine defines interfaces each supported storage should implement.
|
||||
// Includes default implementation with boltdb
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
|
||||
// NOTE: mockery works from linked to go-path and with GOFLAGS='-mod=vendor' go generate
|
||||
//go:generate sh -c "mockery -inpkg -name Interface -print > /tmp/engine-mock.tmp && mv /tmp/engine-mock.tmp engine_mock.go"
|
||||
|
||||
// Interface defines methods provided by low-level storage engine
|
||||
type Interface interface {
|
||||
Create(comment store.Comment) (commentID string, err error) // create new comment, avoid dups by id
|
||||
Update(locator store.Locator, comment store.Comment) error // update comment, mutable parts only
|
||||
Get(locator store.Locator, commentID string) (store.Comment, error) // get comment by id
|
||||
Find(req FindRequest) ([]store.Comment, error) // find comments for locator or site
|
||||
Info(req InfoRequest) ([]store.PostInfo, error) // get post(s) meta info
|
||||
Count(req FindRequest) (int, error) // get count for post or user
|
||||
Delete(req DeleteRequest) error // delete post(s) by id or by userID
|
||||
Flag(req FlagRequest) (bool, error) // set and get flags
|
||||
ListFlags(siteID string, flag Flag) ([]interface{}, error) // get list of flagged keys, like blocked & verified user
|
||||
Close() error // close storage engine
|
||||
}
|
||||
|
||||
// FindRequest is the input for all find operations
|
||||
type FindRequest struct {
|
||||
Locator store.Locator // lack of URL means site operation
|
||||
UserID string // presence of UserID treated as user-related find
|
||||
Sort string // sort order with +/-field syntax
|
||||
Since time.Time // time limit for found results
|
||||
Limit, Skip int
|
||||
}
|
||||
|
||||
// InfoRequest is the input of Info operation used to get meta data about posts
|
||||
type InfoRequest struct {
|
||||
Locator store.Locator
|
||||
Limit, Skip int
|
||||
ReadOnlyAge int
|
||||
}
|
||||
|
||||
type DeleteRequest struct {
|
||||
Locator store.Locator // lack of URL means site operation
|
||||
CommentID string
|
||||
UserID string
|
||||
DeleteMode store.DeleteMode
|
||||
}
|
||||
|
||||
// Flag defines type of binary attribute
|
||||
type Flag string
|
||||
|
||||
// FlagStatus represents values of the flag update
|
||||
type FlagStatus int
|
||||
|
||||
// enum of update values
|
||||
const (
|
||||
FlagNonSet FlagStatus = 0
|
||||
FlagTrue FlagStatus = 1
|
||||
FlagFalse FlagStatus = -1
|
||||
)
|
||||
|
||||
// Enum of all flags
|
||||
const (
|
||||
ReadOnly = Flag("readonly")
|
||||
Verified = Flag("verified")
|
||||
Blocked = Flag("blocked")
|
||||
)
|
||||
|
||||
// FlagRequest is the input for both get/set for flags, like blocked, verified and so on
|
||||
type FlagRequest struct {
|
||||
Flag Flag // flag type
|
||||
Locator store.Locator // post locator
|
||||
UserID string // for flags setting user status
|
||||
Update FlagStatus // if FlagNonSet it will be get op, if set will set the value
|
||||
TTL time.Duration // ttl for time-sensitive flags only, like blocked for some period
|
||||
}
|
||||
|
||||
const (
|
||||
// limits
|
||||
lastLimit = 1000
|
||||
userLimit = 500
|
||||
)
|
||||
|
||||
// SortComments is for engines can't sort data internally
|
||||
func SortComments(comments []store.Comment, sortFld string) []store.Comment {
|
||||
sort.Slice(comments, func(i, j int) bool {
|
||||
switch sortFld {
|
||||
case "+time", "-time", "time", "+active", "-active", "active":
|
||||
if strings.HasPrefix(sortFld, "-") {
|
||||
return comments[i].Timestamp.After(comments[j].Timestamp)
|
||||
}
|
||||
return comments[i].Timestamp.Before(comments[j].Timestamp)
|
||||
|
||||
case "+score", "-score", "score":
|
||||
if strings.HasPrefix(sortFld, "-") {
|
||||
if comments[i].Score == comments[j].Score {
|
||||
return comments[i].Timestamp.Before(comments[j].Timestamp)
|
||||
}
|
||||
return comments[i].Score > comments[j].Score
|
||||
}
|
||||
if comments[i].Score == comments[j].Score {
|
||||
return comments[i].Timestamp.Before(comments[j].Timestamp)
|
||||
}
|
||||
return comments[i].Score < comments[j].Score
|
||||
|
||||
case "+controversy", "-controversy", "controversy":
|
||||
if strings.HasPrefix(sortFld, "-") {
|
||||
if comments[i].Controversy == comments[j].Controversy {
|
||||
return comments[i].Timestamp.Before(comments[j].Timestamp)
|
||||
}
|
||||
return comments[i].Controversy > comments[j].Controversy
|
||||
}
|
||||
if comments[i].Controversy == comments[j].Controversy {
|
||||
return comments[i].Timestamp.Before(comments[j].Timestamp)
|
||||
}
|
||||
return comments[i].Controversy < comments[j].Controversy
|
||||
|
||||
default:
|
||||
return comments[i].Timestamp.Before(comments[j].Timestamp)
|
||||
}
|
||||
})
|
||||
return comments
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package engine2
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
|
||||
func TestEngine_sortComments(t *testing.T) {
|
||||
cc := []store.Comment{
|
||||
{ID: "1", Score: 5, Controversy: 1, Timestamp: time.Date(2018, 2, 5, 10, 1, 0, 0, time.Local)},
|
||||
{ID: "2", Score: 4, Controversy: 2, Timestamp: time.Date(2018, 2, 5, 10, 2, 0, 0, time.Local)},
|
||||
{ID: "3", Score: 6, Controversy: 3, Timestamp: time.Date(2018, 2, 5, 10, 3, 0, 0, time.Local)},
|
||||
{ID: "4", Score: 6, Controversy: 1, Timestamp: time.Date(2018, 2, 5, 10, 4, 0, 0, time.Local)},
|
||||
}
|
||||
|
||||
SortComments(cc, "+time")
|
||||
assert.Equal(t, "1", cc[0].ID)
|
||||
assert.Equal(t, "2", cc[1].ID)
|
||||
assert.Equal(t, "3", cc[2].ID)
|
||||
assert.Equal(t, "4", cc[3].ID)
|
||||
|
||||
SortComments(cc, "-time")
|
||||
assert.Equal(t, "4", cc[0].ID)
|
||||
assert.Equal(t, "3", cc[1].ID)
|
||||
assert.Equal(t, "2", cc[2].ID)
|
||||
assert.Equal(t, "1", cc[3].ID)
|
||||
|
||||
SortComments(cc, "score")
|
||||
assert.Equal(t, "2", cc[0].ID)
|
||||
assert.Equal(t, "1", cc[1].ID)
|
||||
assert.Equal(t, "3", cc[2].ID)
|
||||
assert.Equal(t, "4", cc[3].ID)
|
||||
|
||||
SortComments(cc, "-score")
|
||||
assert.Equal(t, "3", cc[0].ID)
|
||||
assert.Equal(t, "4", cc[1].ID)
|
||||
assert.Equal(t, "1", cc[2].ID)
|
||||
assert.Equal(t, "2", cc[3].ID)
|
||||
|
||||
SortComments(cc, "controversy")
|
||||
assert.Equal(t, "1", cc[0].ID)
|
||||
assert.Equal(t, "4", cc[1].ID)
|
||||
assert.Equal(t, "2", cc[2].ID)
|
||||
assert.Equal(t, "3", cc[3].ID)
|
||||
|
||||
SortComments(cc, "-controversy")
|
||||
assert.Equal(t, "3", cc[0].ID)
|
||||
assert.Equal(t, "2", cc[1].ID)
|
||||
assert.Equal(t, "1", cc[2].ID)
|
||||
assert.Equal(t, "4", cc[3].ID)
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
|
||||
// Client implements remote engine and delegates all calls to remote http server
|
||||
type Client struct {
|
||||
API string
|
||||
Client http.Client
|
||||
AuthUser string
|
||||
AuthPasswd string
|
||||
}
|
||||
|
||||
// Request encloses method name and all params
|
||||
type Request struct {
|
||||
Method string `json:"method"`
|
||||
Params interface{} `json:"params"`
|
||||
}
|
||||
|
||||
// Response encloses result and error received from remote server
|
||||
type Response struct {
|
||||
Result *json.RawMessage `json:"result,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// Create comment and return ID
|
||||
func (r *Client) Create(comment store.Comment) (commentID string, err error) {
|
||||
|
||||
resp, err := r.call("create", comment)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(*resp.Result, &commentID)
|
||||
return commentID, err
|
||||
}
|
||||
|
||||
// Get comment by ID
|
||||
func (r *Client) Get(locator store.Locator, commentID string) (comment store.Comment, err error) {
|
||||
resp, err := r.call("get", locator, commentID)
|
||||
if err != nil {
|
||||
return store.Comment{}, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(*resp.Result, &comment)
|
||||
return comment, err
|
||||
}
|
||||
|
||||
// Put updates comment, mutable parts only
|
||||
func (r *Client) Put(locator store.Locator, comment store.Comment) error {
|
||||
_, err := r.call("put", locator, comment)
|
||||
return err
|
||||
}
|
||||
|
||||
// Find comments for locator
|
||||
func (r *Client) Find(locator store.Locator, sort string) (comments []store.Comment, err error) {
|
||||
resp, err := r.call("find", locator, sort)
|
||||
if err != nil {
|
||||
return []store.Comment{}, err
|
||||
}
|
||||
err = json.Unmarshal(*resp.Result, &comments)
|
||||
return comments, err
|
||||
}
|
||||
|
||||
// Last comments for given site, sorted by time
|
||||
func (r *Client) Last(siteID string, limit int, since time.Time) (comments []store.Comment, err error) {
|
||||
resp, err := r.call("last", siteID, limit, since)
|
||||
if err != nil {
|
||||
return []store.Comment{}, err
|
||||
}
|
||||
err = json.Unmarshal(*resp.Result, &comments)
|
||||
return comments, err
|
||||
}
|
||||
|
||||
// User get comments by user, sorted by time
|
||||
func (r *Client) User(siteID, userID string, limit, skip int) (comments []store.Comment, err error) {
|
||||
resp, err := r.call("user", siteID, userID, limit, skip)
|
||||
if err != nil {
|
||||
return []store.Comment{}, err
|
||||
}
|
||||
err = json.Unmarshal(*resp.Result, &comments)
|
||||
return comments, err
|
||||
}
|
||||
|
||||
// UserCount gets comments count by user
|
||||
func (r *Client) UserCount(siteID, userID string) (count int, err error) {
|
||||
resp, err := r.call("user_count", siteID, userID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
err = json.Unmarshal(*resp.Result, &count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *Client) call(method string, args ...interface{}) (*Response, error) {
|
||||
|
||||
b, err := json.Marshal(Request{Method: method, Params: args})
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "marshaling failed for %s", method)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", r.API, bytes.NewBuffer(b))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to make request for %s", method)
|
||||
}
|
||||
|
||||
req.SetBasicAuth(r.AuthUser, r.AuthPasswd)
|
||||
resp, err := r.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "remote call failed for %s", method)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, errors.Errorf("bad status %d for %s", resp.StatusCode, method)
|
||||
}
|
||||
|
||||
cr := Response{}
|
||||
if err = json.NewDecoder(resp.Body).Decode(&cr); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to decode response for %s", method)
|
||||
}
|
||||
|
||||
if cr.Error != "" {
|
||||
return nil, errors.New(cr.Error)
|
||||
}
|
||||
return &cr, nil
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package remote
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
)
|
||||
|
||||
func TestClient_Create(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"create","params":[{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"}]}`, `{"result":"12345"}`)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
res, err := c.Create(store.Comment{ID: "123", Locator: store.Locator{URL: "http://example.com/url", SiteID: "site"},
|
||||
Text: "msg"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "12345", res)
|
||||
t.Logf("%v %T", res, res)
|
||||
}
|
||||
|
||||
func TestClient_Get(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`,
|
||||
`{"result":{"id":"123","pid":"","text":"msg","delete":true}}`)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
res, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, store.Comment{ID: "123", Text: "msg", Deleted: true}, res)
|
||||
t.Logf("%v %T", res, res)
|
||||
}
|
||||
|
||||
func TestClient_GetWithErrorResult(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, `{"error":"failed"}`)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
_, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site")
|
||||
assert.EqualError(t, err, "failed")
|
||||
}
|
||||
|
||||
func TestClient_GetWithErrorDecode(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"get","params":[{"url":"http://example.com/url"},"site"]}`, ``)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
_, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site")
|
||||
assert.EqualError(t, err, "failed to decode response for get: EOF")
|
||||
}
|
||||
|
||||
func TestClient_GetWithErrorRemote(t *testing.T) {
|
||||
c := Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}
|
||||
|
||||
_, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site")
|
||||
assert.NotNil(t, err)
|
||||
assert.True(t, strings.Contains(err.Error(), "remote call failed for get:"))
|
||||
}
|
||||
|
||||
func TestClient_FailedStatus(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
t.Logf("req: %s", string(body))
|
||||
w.WriteHeader(400)
|
||||
}))
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
_, err := c.Get(store.Locator{URL: "http://example.com/url"}, "site")
|
||||
assert.EqualError(t, err, "bad status 400 for get")
|
||||
}
|
||||
|
||||
func TestClient_Put(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"put","params":[{"url":"http://example.com/url"},{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site123","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"}]}`, `{}`)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
err := c.Put(store.Locator{URL: "http://example.com/url"}, store.Comment{ID: "123",
|
||||
Locator: store.Locator{URL: "http://example.com/url", SiteID: "site123"}, Text: "msg"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
}
|
||||
|
||||
func TestClient_Find(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"find","params":[{"url":"http://example.com/url"},""]}`,
|
||||
`{"result":[{"text":"1"},{"text":"2"}]}`)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
res, err := c.Find(store.Locator{URL: "http://example.com/url"}, "")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.Comment{{Text: "1"}, {Text: "2"}}, res)
|
||||
}
|
||||
|
||||
func TestClient_Last(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"last","params":["site1",100,"2019-06-06T19:34:10Z"]}`,
|
||||
`{"result":[{"text":"1"},{"text":"2"}]}`)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
res, err := c.Last("site1", 100, time.Date(2019, 6, 6, 19, 34, 10, 0, time.UTC))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.Comment{{Text: "1"}, {Text: "2"}}, res)
|
||||
}
|
||||
|
||||
func TestClient_User(t *testing.T) {
|
||||
ts := testServer(t, `{"method":"user","params":["site1","u1",100,4]}`, `{"result":[{"text":"1"},{"text":"2"}]}`)
|
||||
defer ts.Close()
|
||||
c := Client{API: ts.URL, Client: http.Client{}}
|
||||
|
||||
res, err := c.User("site1", "u1", 100, 4)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []store.Comment{{Text: "1"}, {Text: "2"}}, res)
|
||||
}
|
||||
|
||||
func testServer(t *testing.T, req, resp string) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, req, string(body))
|
||||
t.Logf("req: %s", string(body))
|
||||
fmt.Fprintf(w, resp)
|
||||
}))
|
||||
}
|
||||
@@ -19,12 +19,13 @@ import (
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/admin"
|
||||
"github.com/umputun/remark/backend/app/store/engine"
|
||||
"github.com/umputun/remark/backend/app/store/engine2"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
)
|
||||
|
||||
// DataStore wraps store.Interface with additional methods
|
||||
type DataStore struct {
|
||||
engine.Interface
|
||||
Engine engine2.Interface
|
||||
EditDuration time.Duration
|
||||
AdminStore admin.Store
|
||||
MaxCommentSize int
|
||||
@@ -98,12 +99,13 @@ func (s *DataStore) Create(comment store.Comment) (commentID string, err error)
|
||||
}()
|
||||
|
||||
s.submitImages(comment)
|
||||
return s.Interface.Create(comment)
|
||||
return s.Engine.Create(comment)
|
||||
}
|
||||
|
||||
// Find wraps engine's Find call and alter results if needed
|
||||
func (s *DataStore) Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error) {
|
||||
comments, err := s.Interface.Find(locator, sort)
|
||||
req := engine2.FindRequest{Locator: locator, Sort: sort}
|
||||
comments, err := s.Engine.Find(req)
|
||||
if err != nil {
|
||||
return comments, err
|
||||
}
|
||||
@@ -130,19 +132,24 @@ func (s *DataStore) Find(locator store.Locator, sort string, user store.User) ([
|
||||
|
||||
// Get comment by ID
|
||||
func (s *DataStore) Get(locator store.Locator, commentID string, user store.User) (store.Comment, error) {
|
||||
c, err := s.Interface.Get(locator, commentID)
|
||||
c, err := s.Engine.Get(locator, commentID)
|
||||
if err != nil {
|
||||
return store.Comment{}, err
|
||||
}
|
||||
return s.alterComment(c, user), nil
|
||||
}
|
||||
|
||||
// Put updates comment, mutable parts only
|
||||
func (s *DataStore) Put(locator store.Locator, comment store.Comment) error {
|
||||
return s.Engine.Update(locator, comment)
|
||||
}
|
||||
|
||||
// submitImages initiated delayed commit of all images from the comment uploaded to remark42
|
||||
func (s *DataStore) submitImages(comment store.Comment) {
|
||||
|
||||
s.ImageService.Submit(func() []string {
|
||||
c := comment
|
||||
cc, err := s.Interface.Get(c.Locator, c.ID) // this can be called after last edit, we have to retrieve fresh comment
|
||||
cc, err := s.Engine.Get(c.Locator, c.ID) // this can be called after last edit, we have to retrieve fresh comment
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't get comment's %s text for image extraction, %v", c.ID, err)
|
||||
return nil
|
||||
@@ -182,14 +189,20 @@ func (s *DataStore) prepareNewComment(comment store.Comment) (store.Comment, err
|
||||
return comment, nil
|
||||
}
|
||||
|
||||
// DeleteAll removes all data from site
|
||||
func (s *DataStore) DeleteAll(siteID string) error {
|
||||
req := engine2.DeleteRequest{Locator: store.Locator{SiteID: siteID}}
|
||||
return s.Engine.Delete(req)
|
||||
}
|
||||
|
||||
// SetPin pin/un-pin comment as special
|
||||
func (s *DataStore) SetPin(locator store.Locator, commentID string, status bool) error {
|
||||
comment, err := s.Interface.Get(locator, commentID)
|
||||
comment, err := s.Engine.Get(locator, commentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
comment.Pin = status
|
||||
return s.Put(locator, comment)
|
||||
return s.Engine.Update(locator, comment)
|
||||
}
|
||||
|
||||
// Vote for comment by id and locator
|
||||
@@ -199,7 +212,7 @@ func (s *DataStore) Vote(locator store.Locator, commentID string, userID string,
|
||||
cLock.Lock() // prevents race on voting
|
||||
defer cLock.Unlock()
|
||||
|
||||
comment, err = s.Interface.Get(locator, commentID)
|
||||
comment, err = s.Engine.Get(locator, commentID)
|
||||
if err != nil {
|
||||
return comment, err
|
||||
}
|
||||
@@ -258,7 +271,7 @@ func (s *DataStore) Vote(locator store.Locator, commentID string, userID string,
|
||||
|
||||
comment.Controversy = s.controversy(s.upsAndDowns(comment))
|
||||
|
||||
return comment, s.Put(locator, comment)
|
||||
return comment, s.Engine.Update(locator, comment)
|
||||
}
|
||||
|
||||
// controversy calculates controversial index of votes
|
||||
@@ -287,7 +300,7 @@ type EditRequest struct {
|
||||
|
||||
// EditComment to edit text and update Edit info
|
||||
func (s *DataStore) EditComment(locator store.Locator, commentID string, req EditRequest) (comment store.Comment, err error) {
|
||||
comment, err = s.Interface.Get(locator, commentID)
|
||||
comment, err = s.Engine.Get(locator, commentID)
|
||||
if err != nil {
|
||||
return comment, err
|
||||
}
|
||||
@@ -303,7 +316,8 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi
|
||||
|
||||
if req.Delete { // delete request
|
||||
comment.Deleted = true
|
||||
return comment, s.Delete(locator, commentID, store.SoftDelete)
|
||||
delReq := engine2.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: store.SoftDelete}
|
||||
return comment, s.Engine.Delete(delReq)
|
||||
}
|
||||
|
||||
if s.RestrictedWordsMatcher != nil && s.RestrictedWordsMatcher.Match(comment.Locator.SiteID, req.Text) {
|
||||
@@ -318,7 +332,7 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi
|
||||
}
|
||||
|
||||
comment.Sanitize()
|
||||
err = s.Put(locator, comment)
|
||||
err = s.Engine.Update(locator, comment)
|
||||
return comment, err
|
||||
}
|
||||
|
||||
@@ -336,7 +350,8 @@ func (s *DataStore) HasReplies(comment store.Comment) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
comments, err := s.Interface.Last(comment.Locator.SiteID, maxLastCommentsReply, time.Time{})
|
||||
req := engine2.FindRequest{Locator: store.Locator{SiteID: comment.Locator.SiteID}, Limit: maxLastCommentsReply}
|
||||
comments, err := s.Engine.Find(req)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't get last comments for reply check, %v", err)
|
||||
return false
|
||||
@@ -395,7 +410,7 @@ func (s *DataStore) SetTitle(locator store.Locator, commentID string) (comment s
|
||||
return comment, errors.New("no title extractor")
|
||||
}
|
||||
|
||||
comment, err = s.Interface.Get(locator, commentID)
|
||||
comment, err = s.Engine.Get(locator, commentID)
|
||||
if err != nil {
|
||||
return comment, err
|
||||
}
|
||||
@@ -406,7 +421,7 @@ func (s *DataStore) SetTitle(locator store.Locator, commentID string) (comment s
|
||||
return comment, err
|
||||
}
|
||||
comment.PostTitle = title
|
||||
err = s.Put(locator, comment)
|
||||
err = s.Engine.Update(locator, comment)
|
||||
return comment, err
|
||||
}
|
||||
|
||||
@@ -414,7 +429,8 @@ func (s *DataStore) SetTitle(locator store.Locator, commentID string) (comment s
|
||||
func (s *DataStore) Counts(siteID string, postIDs []string) ([]store.PostInfo, error) {
|
||||
res := []store.PostInfo{}
|
||||
for _, p := range postIDs {
|
||||
if c, err := s.Count(store.Locator{SiteID: siteID, URL: p}); err == nil {
|
||||
req := engine2.FindRequest{Locator: store.Locator{SiteID: siteID, URL: p}}
|
||||
if c, err := s.Engine.Count(req); err == nil {
|
||||
res = append(res, store.PostInfo{URL: p, Count: c})
|
||||
}
|
||||
}
|
||||
@@ -449,20 +465,126 @@ func (s *DataStore) IsAdmin(siteID string, userID string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsReadOnly checks if post read-only
|
||||
func (s *DataStore) IsReadOnly(locator store.Locator) bool {
|
||||
req := engine2.FlagRequest{Locator: locator, Flag: engine2.ReadOnly}
|
||||
ro, err := s.Engine.Flag(req)
|
||||
return err == nil && ro
|
||||
}
|
||||
|
||||
// SetReadOnly set/reset read-only flag
|
||||
func (s *DataStore) SetReadOnly(locator store.Locator, status bool) error {
|
||||
roStatus := engine2.FlagFalse
|
||||
if status {
|
||||
roStatus = engine2.FlagTrue
|
||||
|
||||
}
|
||||
req := engine2.FlagRequest{Locator: locator, Flag: engine2.ReadOnly, Update: roStatus}
|
||||
_, err := s.Engine.Flag(req)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsVerified checks if user verified
|
||||
func (s *DataStore) IsVerified(siteID string, userID string) bool {
|
||||
req := engine2.FlagRequest{Locator: store.Locator{SiteID: siteID}, UserID: userID, Flag: engine2.Verified}
|
||||
ro, err := s.Engine.Flag(req)
|
||||
return err == nil && ro
|
||||
}
|
||||
|
||||
// SetVerified set/reset verified status for user
|
||||
func (s *DataStore) SetVerified(siteID string, userID string, status bool) error {
|
||||
roStatus := engine2.FlagFalse
|
||||
if status {
|
||||
roStatus = engine2.FlagTrue
|
||||
}
|
||||
req := engine2.FlagRequest{Locator: store.Locator{SiteID: siteID}, UserID: userID, Flag: engine2.Verified, Update: roStatus}
|
||||
_, err := s.Engine.Flag(req)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsBlocked checks if user blocked
|
||||
func (s *DataStore) IsBlocked(siteID string, userID string) bool {
|
||||
req := engine2.FlagRequest{Locator: store.Locator{SiteID: siteID}, UserID: userID, Flag: engine2.Blocked}
|
||||
ro, err := s.Engine.Flag(req)
|
||||
return err == nil && ro
|
||||
}
|
||||
|
||||
// SetBlock set/reset verified status for user
|
||||
func (s *DataStore) SetBlock(siteID string, userID string, status bool, ttl time.Duration) error {
|
||||
roStatus := engine2.FlagFalse
|
||||
if status {
|
||||
roStatus = engine2.FlagTrue
|
||||
}
|
||||
req := engine2.FlagRequest{Locator: store.Locator{SiteID: siteID}, UserID: userID,
|
||||
Flag: engine2.Blocked, Update: roStatus, TTL: ttl}
|
||||
_, err := s.Engine.Flag(req)
|
||||
return err
|
||||
}
|
||||
|
||||
// Blocked returns list with all blocked users
|
||||
func (s *DataStore) Blocked(siteID string) (res []store.BlockedUser, err error) {
|
||||
blocked, e := s.Engine.ListFlags(siteID, engine2.Blocked)
|
||||
if e != nil {
|
||||
return nil, errors.Wrapf(err, "can't get list of blocked users for %s", siteID)
|
||||
}
|
||||
for _, v := range blocked {
|
||||
res = append(res, v.(store.BlockedUser))
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
// Info get post info
|
||||
func (s *DataStore) Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) {
|
||||
req := engine2.InfoRequest{Locator: locator, ReadOnlyAge: readonlyAge}
|
||||
res, err := s.Engine.Info(req)
|
||||
if err != nil {
|
||||
return store.PostInfo{}, err
|
||||
}
|
||||
if len(res) == 0 {
|
||||
return store.PostInfo{}, errors.Errorf("post %+v not found", locator)
|
||||
}
|
||||
return res[0], nil
|
||||
}
|
||||
|
||||
// Delete comment by id
|
||||
func (s *DataStore) Delete(locator store.Locator, commentID string, mode store.DeleteMode) error {
|
||||
req := engine2.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: mode}
|
||||
return s.Engine.Delete(req)
|
||||
}
|
||||
|
||||
// DeleteUser removes all comments from user
|
||||
func (s *DataStore) DeleteUser(siteID string, userID string) error {
|
||||
req := engine2.DeleteRequest{Locator: store.Locator{SiteID: siteID}, UserID: userID, DeleteMode: store.HardDelete}
|
||||
return s.Engine.Delete(req)
|
||||
}
|
||||
|
||||
// List of commented posts
|
||||
func (s *DataStore) List(siteID string, limit int, skip int) ([]store.PostInfo, error) {
|
||||
req := engine2.InfoRequest{Locator: store.Locator{SiteID: siteID}, Limit: limit, Skip: skip}
|
||||
return s.Engine.Info(req)
|
||||
}
|
||||
|
||||
// Count gets number of comments for the post
|
||||
func (s *DataStore) Count(locator store.Locator) (int, error) {
|
||||
req := engine2.FindRequest{Locator: locator}
|
||||
return s.Engine.Count(req)
|
||||
}
|
||||
|
||||
// Metas returns metadata for users and posts
|
||||
func (s *DataStore) Metas(siteID string) (umetas []UserMetaData, pmetas []PostMetaData, err error) {
|
||||
umetas = []UserMetaData{}
|
||||
pmetas = []PostMetaData{}
|
||||
|
||||
// set posts meta
|
||||
posts, err := s.List(siteID, 0, 0)
|
||||
posts, err := s.Engine.Info(engine2.InfoRequest{Locator: store.Locator{SiteID: siteID}})
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "can't get list of posts for %s", siteID)
|
||||
}
|
||||
|
||||
for _, p := range posts {
|
||||
if s.IsReadOnly(store.Locator{SiteID: siteID, URL: p.URL}) {
|
||||
pmetas = append(pmetas, PostMetaData{URL: p.URL, ReadOnly: true})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// set users meta
|
||||
@@ -484,11 +606,12 @@ func (s *DataStore) Metas(siteID string) (umetas []UserMetaData, pmetas []PostMe
|
||||
}
|
||||
|
||||
// process verified users
|
||||
verified, err := s.Verified(siteID)
|
||||
verified, err := s.Engine.ListFlags(siteID, engine2.Verified)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "can't get list of verified users for %s", siteID)
|
||||
}
|
||||
for _, v := range verified {
|
||||
for _, vi := range verified {
|
||||
v := vi.(string)
|
||||
val, ok := m[v]
|
||||
if !ok {
|
||||
val = UserMetaData{ID: v}
|
||||
@@ -531,22 +654,35 @@ func (s *DataStore) SetMetas(siteID string, umetas []UserMetaData, pmetas []Post
|
||||
|
||||
// User gets comment for given userID on siteID
|
||||
func (s *DataStore) User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error) {
|
||||
comments, err := s.Interface.User(siteID, userID, limit, skip)
|
||||
req := engine2.FindRequest{Locator: store.Locator{SiteID: siteID}, UserID: userID, Limit: limit, Skip: skip}
|
||||
comments, err := s.Engine.Find(req)
|
||||
if err != nil {
|
||||
return comments, err
|
||||
}
|
||||
return s.alterComments(comments, user), nil
|
||||
}
|
||||
|
||||
// UserCount is comments count by user
|
||||
func (s *DataStore) UserCount(siteID, userID string) (int, error) {
|
||||
req := engine2.FindRequest{Locator: store.Locator{SiteID: siteID}, UserID: userID}
|
||||
return s.Engine.Count(req)
|
||||
}
|
||||
|
||||
// Last gets last comments for site, cross-post. Limited by count and optional since ts
|
||||
func (s *DataStore) Last(siteID string, limit int, since time.Time, user store.User) ([]store.Comment, error) {
|
||||
comments, err := s.Interface.Last(siteID, limit, since)
|
||||
req := engine2.FindRequest{Locator: store.Locator{SiteID: siteID}, Limit: limit, Since: since, Sort: "-time"}
|
||||
comments, err := s.Engine.Find(req)
|
||||
if err != nil {
|
||||
return comments, err
|
||||
}
|
||||
return s.alterComments(comments, user), nil
|
||||
}
|
||||
|
||||
// Close store service
|
||||
func (s *DataStore) Close() error {
|
||||
return s.Engine.Close()
|
||||
}
|
||||
|
||||
func (s *DataStore) upsAndDowns(c store.Comment) (ups, downs int) {
|
||||
for _, v := range c.Votes {
|
||||
if v {
|
||||
@@ -583,7 +719,9 @@ func (s *DataStore) alterComments(cc []store.Comment, user store.User) (res []st
|
||||
|
||||
func (s *DataStore) alterComment(c store.Comment, user store.User) (res store.Comment) {
|
||||
|
||||
blocked := s.IsBlocked(c.Locator.SiteID, c.User.ID)
|
||||
blocReq := engine2.FlagRequest{Flag: engine2.Blocked, Locator: store.Locator{SiteID: c.Locator.SiteID}, UserID: c.User.ID}
|
||||
blocked, _ := s.Engine.Flag(blocReq)
|
||||
|
||||
// process blocked users
|
||||
if blocked {
|
||||
if !user.Admin { // reset comment to deleted for non-admins
|
||||
@@ -595,7 +733,8 @@ func (s *DataStore) alterComment(c store.Comment, user store.User) (res store.Co
|
||||
|
||||
// set verified status retroactively
|
||||
if !blocked {
|
||||
c.User.Verified = s.IsVerified(c.Locator.SiteID, c.User.ID)
|
||||
verifReq := engine2.FlagRequest{Flag: engine2.Verified, Locator: store.Locator{SiteID: c.Locator.SiteID}, UserID: c.User.ID}
|
||||
c.User.Verified, _ = s.Engine.Flag(verifReq)
|
||||
}
|
||||
|
||||
// hide info from non-admins
|
||||
|
||||
Reference in New Issue
Block a user