378 lines
14 KiB
Go
378 lines
14 KiB
Go
package engine
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/globalsign/mgo"
|
|
"github.com/globalsign/mgo/bson"
|
|
"github.com/go-pkgz/mongo"
|
|
multierror "github.com/hashicorp/go-multierror"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/umputun/remark/backend/app/store"
|
|
)
|
|
|
|
// Mongo implements engine interface
|
|
type Mongo struct {
|
|
conn *mongo.Connection
|
|
postWriter mongo.BufferedWriter
|
|
}
|
|
|
|
const (
|
|
mongoPosts = "posts"
|
|
mongoMetaPosts = "meta_posts"
|
|
mongoMetaUsers = "meta_users"
|
|
)
|
|
|
|
type metaPost struct {
|
|
ID string `bson:"_id"` // url
|
|
SiteID string `bson:"site"`
|
|
ReadOnly bool `bson:"read_only"`
|
|
}
|
|
|
|
type metaUser struct {
|
|
ID string `bson:"_id"` // user_id
|
|
SiteID string `bson:"site"`
|
|
Verified bool `bson:"verified"`
|
|
Blocked bool `bson:"blocked"`
|
|
BlockedUntil time.Time `bson:"blocked_until"`
|
|
}
|
|
|
|
// NewMongo makes mongo engine. bufferSize denies how many records will be buffered, 0 turns buffering off.
|
|
// flushDuration triggers automatic flus (write from buffer), 0 disables it and will flush as buffer size reached.
|
|
// important! don't use flushDuration=0 for production use as it can leave records in-fly state for long or even unlimited time.
|
|
func NewMongo(conn *mongo.Connection, bufferSize int, flushDuration time.Duration) (*Mongo, error) {
|
|
writer := mongo.NewBufferedWriter(bufferSize, conn).WithCollection(mongoPosts).WithAutoFlush(flushDuration)
|
|
result := Mongo{conn: conn, postWriter: writer}
|
|
err := result.prepare()
|
|
return &result, errors.Wrap(err, "failed to prepare mongo")
|
|
}
|
|
|
|
// Create new comment, write can be buffered and delayed.
|
|
func (m *Mongo) Create(comment store.Comment) (commentID string, err error) {
|
|
// err = m.postWriter.Write(comment)
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
return coll.Insert(&comment)
|
|
})
|
|
return comment.ID, err
|
|
}
|
|
|
|
// Find returns all comments for post and sorts results
|
|
func (m *Mongo) Find(locator store.Locator, sortFld string) (comments []store.Comment, err error) {
|
|
comments = []store.Comment{}
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
query := bson.M{"locator.site": locator.SiteID, "locator.url": locator.URL}
|
|
return coll.Find(query).Sort(sortFld).All(&comments)
|
|
})
|
|
return comments, err
|
|
}
|
|
|
|
// Get returns comment for locator.URL and commentID string
|
|
func (m *Mongo) Get(locator store.Locator, commentID string) (comment store.Comment, err error) {
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
query := bson.M{"_id": commentID, "locator.site": locator.SiteID, "locator.url": locator.URL}
|
|
return coll.Find(query).One(&comment)
|
|
})
|
|
return comment, err
|
|
}
|
|
|
|
// Put updates comment for locator.URL with mutable part of comment
|
|
func (m *Mongo) Put(locator store.Locator, comment store.Comment) error {
|
|
return m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
return coll.Update(bson.M{"_id": comment.ID, "locator.site": locator.SiteID, "locator.url": locator.URL},
|
|
bson.M{"$set": bson.M{
|
|
"text": comment.Text,
|
|
"orig": comment.Orig,
|
|
"score": comment.Score,
|
|
"votes": comment.Votes,
|
|
"pin": comment.Pin,
|
|
"deleted": comment.Deleted,
|
|
}})
|
|
})
|
|
}
|
|
|
|
// Last returns up to max last comments for given siteID
|
|
func (m *Mongo) Last(siteID string, max int) (comments []store.Comment, err error) {
|
|
comments = []store.Comment{}
|
|
if max > lastLimit || max == 0 {
|
|
max = lastLimit
|
|
}
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
query := bson.M{"locator.site": siteID, "delete": false}
|
|
return coll.Find(query).Sort("-time").Limit(max).All(&comments)
|
|
})
|
|
return comments, err
|
|
}
|
|
|
|
// Count returns number of comments for locator
|
|
func (m *Mongo) Count(locator store.Locator) (count int, err error) {
|
|
|
|
e := m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
query := bson.M{"locator.site": locator.SiteID, "locator.url": locator.URL, "delete": false}
|
|
count, err = coll.Find(query).Count()
|
|
return err
|
|
})
|
|
return count, e
|
|
}
|
|
|
|
// List returns list of all commented posts with counters
|
|
func (m *Mongo) List(siteID string, limit, skip int) (list []store.PostInfo, err error) {
|
|
list = []store.PostInfo{}
|
|
|
|
if limit <= 0 {
|
|
limit = 1000
|
|
}
|
|
if skip < 0 {
|
|
skip = 0
|
|
}
|
|
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
pipeline := coll.Pipe([]bson.M{
|
|
{"$match": bson.M{"locator.site": siteID}},
|
|
{"$project": bson.M{"locator.site": 1, "locator.url": 1, "time": 1}},
|
|
{"$group": bson.M{"_id": "$locator.url", "url": bson.M{"$first": "$locator.url"}, "count": bson.M{"$sum": 1},
|
|
"first_time": bson.M{"$min": "$time"}, "last_time": bson.M{"$max": "$time"}}},
|
|
{"$skip": skip},
|
|
{"$limit": limit},
|
|
})
|
|
return errors.Wrap(pipeline.AllowDiskUse().All(&list), "list pipeline failed")
|
|
})
|
|
return list, errors.Wrap(err, "can't get list")
|
|
}
|
|
|
|
// Info returns time range and count for locator
|
|
func (m *Mongo) Info(locator store.Locator, readOnlyAge int) (info store.PostInfo, err error) {
|
|
list := []store.PostInfo{}
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
pipeline := coll.Pipe([]bson.M{
|
|
{"$match": bson.M{"locator.site": locator.SiteID, "locator.url": locator.URL}},
|
|
{"$project": bson.M{"locator.site": 1, "locator.url": 1, "time": 1}},
|
|
{"$group": bson.M{"_id": "$locator.url", "url": bson.M{"$first": "$locator.url"}, "count": bson.M{"$sum": 1},
|
|
"first_time": bson.M{"$min": "$time"}, "last_time": bson.M{"$max": "$time"}}},
|
|
})
|
|
return errors.Wrap(pipeline.AllowDiskUse().All(&list), "list pipeline failed")
|
|
})
|
|
if err != nil {
|
|
return info, err
|
|
}
|
|
if len(list) == 0 {
|
|
return info, errors.Errorf("can't load info for %s", locator.URL)
|
|
}
|
|
info = list[0]
|
|
// set read-only from age and manual bucket
|
|
info.ReadOnly = readOnlyAge > 0 && !info.FirstTS.IsZero() && info.FirstTS.AddDate(0, 0, readOnlyAge).Before(time.Now())
|
|
if m.IsReadOnly(locator) {
|
|
info.ReadOnly = true
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
// User extracts all comments for given site and given userID
|
|
func (m *Mongo) User(siteID, userID string, limit, skip int) (comments []store.Comment, err error) {
|
|
comments = []store.Comment{}
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
query := bson.M{"locator.site": siteID, "user.id": userID}
|
|
return m.setLimitAndSkip(coll.Find(query).Sort("-time"), limit, skip).All(&comments)
|
|
})
|
|
return comments, errors.Wrapf(err, "can't get comments for user %s", userID)
|
|
}
|
|
|
|
// UserCount returns number of comments for user
|
|
func (m *Mongo) UserCount(siteID, userID string) (count int, err error) {
|
|
err = m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
var e error
|
|
count, e = coll.Find(bson.M{"locator.site": siteID, "user.id": userID}).Count()
|
|
return e
|
|
})
|
|
return count, errors.Wrapf(err, "can't get comments count for user %s", userID)
|
|
}
|
|
|
|
// SetReadOnly makes post read-only or reset the ro flag
|
|
func (m *Mongo) SetReadOnly(locator store.Locator, status bool) (err error) {
|
|
return m.conn.WithCustomCollection(mongoMetaPosts, func(coll *mgo.Collection) error {
|
|
_, e := coll.Upsert(bson.M{"_id": locator.URL, "site": locator.SiteID}, bson.M{"$set": bson.M{"read_only": status}})
|
|
return e
|
|
})
|
|
}
|
|
|
|
// IsReadOnly checks if post in RO
|
|
func (m *Mongo) IsReadOnly(locator store.Locator) (ro bool) {
|
|
meta := metaPost{}
|
|
err := m.conn.WithCustomCollection(mongoMetaPosts, func(coll *mgo.Collection) error {
|
|
return coll.Find(bson.M{"_id": locator.URL, "site": locator.SiteID}).One(&meta)
|
|
})
|
|
return err == nil && meta.ReadOnly
|
|
}
|
|
|
|
// SetVerified makes user verified or reset the flag
|
|
func (m *Mongo) SetVerified(siteID string, userID string, status bool) error {
|
|
return m.conn.WithCustomCollection(mongoMetaUsers, func(coll *mgo.Collection) error {
|
|
_, e := coll.Upsert(bson.M{"_id": userID, "site": siteID}, bson.M{"$set": bson.M{"verified": status}})
|
|
return e
|
|
})
|
|
}
|
|
|
|
// IsVerified checks if user verified
|
|
func (m *Mongo) IsVerified(siteID string, userID string) (verified bool) {
|
|
meta := metaUser{}
|
|
err := m.conn.WithCustomCollection(mongoMetaUsers, func(coll *mgo.Collection) error {
|
|
return coll.Find(bson.M{"_id": userID, "site": siteID}).One(&meta)
|
|
})
|
|
return err == nil && meta.Verified
|
|
}
|
|
|
|
// Verified returns list of verified user IDs
|
|
func (m *Mongo) Verified(siteID string) (ids []string, err error) {
|
|
metas := []metaUser{}
|
|
err = m.conn.WithCustomCollection(mongoMetaUsers, func(coll *mgo.Collection) error {
|
|
return coll.Find(bson.M{"site": siteID, "verified": true}).All(&metas)
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, meta := range metas {
|
|
ids = append(ids, meta.ID)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// SetBlock blocks/unblocks user for given site. ttl defines for for how long, 0 - permanent
|
|
// block uses blocksBucketName with key=userID and val=TTL+now
|
|
func (m *Mongo) SetBlock(siteID string, userID string, status bool, ttl time.Duration) error {
|
|
|
|
until := time.Time{}
|
|
if status {
|
|
until = time.Now().AddDate(100, 0, 0) // permanent is 50year
|
|
if ttl > 0 {
|
|
until = time.Now().Add(ttl)
|
|
}
|
|
}
|
|
return m.conn.WithCustomCollection(mongoMetaUsers, func(coll *mgo.Collection) error {
|
|
_, e := coll.Upsert(bson.M{"_id": userID, "site": siteID},
|
|
bson.M{"$set": bson.M{"blocked": status, "blocked_until": until}})
|
|
return errors.Wrapf(e, "failed to set block for %s", userID)
|
|
})
|
|
}
|
|
|
|
// IsBlocked checks if user blocked
|
|
func (m *Mongo) IsBlocked(siteID string, userID string) (blocked bool) {
|
|
meta := metaUser{}
|
|
err := m.conn.WithCustomCollection(mongoMetaUsers, func(coll *mgo.Collection) error {
|
|
return coll.Find(bson.M{"_id": userID, "site": siteID}).One(&meta)
|
|
})
|
|
return err == nil && meta.Blocked && meta.BlockedUntil.After(time.Now())
|
|
}
|
|
|
|
// Blocked get lists of blocked users for given site
|
|
func (m *Mongo) Blocked(siteID string) (users []store.BlockedUser, err error) {
|
|
users = []store.BlockedUser{}
|
|
metas := []metaUser{}
|
|
err = m.conn.WithCustomCollection(mongoMetaUsers, func(coll *mgo.Collection) error {
|
|
return coll.Find(bson.M{"site": siteID,
|
|
"blocked": true, "blocked_until": bson.M{"$gt": time.Now()}}).All(&metas)
|
|
})
|
|
if err != nil {
|
|
return users, errors.Wrapf(err, "can't get blocked users for site for %s", siteID)
|
|
}
|
|
|
|
for _, mu := range metas {
|
|
blockedUser := store.BlockedUser{ID: mu.ID, Until: mu.BlockedUntil}
|
|
if ucc, e := m.User(siteID, mu.ID, 1, 0); e == nil && len(ucc) > 0 {
|
|
blockedUser.Name = ucc[0].User.Name
|
|
}
|
|
users = append(users, blockedUser)
|
|
}
|
|
return users, nil
|
|
}
|
|
|
|
// Delete removes comment, by locator from the store.
|
|
// Posts collection only sets status to deleted and clear fields in order to prevent breaking trees of replies.
|
|
func (m *Mongo) Delete(locator store.Locator, commentID string, mode store.DeleteMode) error {
|
|
comment := store.Comment{}
|
|
err := m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
e := coll.Find(bson.M{"locator.site": locator.SiteID, "locator.url": locator.URL, "_id": commentID}).One(&comment)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
comment.SetDeleted(mode)
|
|
return coll.Update(bson.M{"locator.site": locator.SiteID, "locator.url": locator.URL, "_id": commentID}, comment)
|
|
})
|
|
return errors.Wrapf(err, "can't delete %s", commentID)
|
|
}
|
|
|
|
// DeleteAll removes all info about siteID
|
|
func (m *Mongo) DeleteAll(siteID string) error {
|
|
err := m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
_, e := coll.RemoveAll(bson.M{"locator.site": siteID})
|
|
return e
|
|
})
|
|
return errors.Wrapf(err, "can't delete 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".
|
|
func (m *Mongo) DeleteUser(siteID string, userID string) error {
|
|
comments := []store.Comment{}
|
|
return m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
e := coll.Find(bson.M{"locator.site": siteID, "user.id": userID}).All(&comments)
|
|
if e != nil {
|
|
return e
|
|
}
|
|
for _, c := range comments {
|
|
if e = m.Delete(c.Locator, c.ID, store.HardDelete); e != nil {
|
|
return e
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Close boltdb store
|
|
func (m *Mongo) Close() error {
|
|
if m.postWriter != nil {
|
|
return m.postWriter.Close()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// prepare collections with all indexes
|
|
func (m *Mongo) prepare() error {
|
|
errs := new(multierror.Error)
|
|
e := m.conn.WithCustomCollection(mongoPosts, func(coll *mgo.Collection) error {
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("user.id", "locator.site", "time"))
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("locator.url", "locator.site", "time"))
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("locator.site", "time"))
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("locator.url", "locator.site", "score"))
|
|
return errors.Wrapf(errs.ErrorOrNil(), "can't create index for %s", mongoPosts)
|
|
})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
|
|
e = m.conn.WithCustomCollection(mongoMetaPosts, func(coll *mgo.Collection) error {
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("_id", "site"))
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("site", "read_only"))
|
|
return errors.Wrapf(errs.ErrorOrNil(), "can't create index for %s", mongoMetaPosts)
|
|
})
|
|
if e != nil {
|
|
return e
|
|
}
|
|
|
|
return m.conn.WithCustomCollection(mongoMetaUsers, func(coll *mgo.Collection) error {
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("_id", "site"))
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("site", "blocked"))
|
|
errs = multierror.Append(errs, coll.EnsureIndexKey("site", "verified"))
|
|
return errors.Wrapf(errs.ErrorOrNil(), "can't create index for %s", mongoMetaUsers)
|
|
})
|
|
}
|
|
|
|
func (m *Mongo) setLimitAndSkip(q *mgo.Query, limit, skip int) *mgo.Query {
|
|
if limit <= 0 {
|
|
limit = 1000
|
|
}
|
|
if skip < 0 {
|
|
skip = 0
|
|
}
|
|
return q.Skip(skip).Limit(limit)
|
|
}
|