move comment alter functionality from rest and admin to store service

This commit is contained in:
Umputun
2019-05-16 20:49:07 -05:00
parent 5218f3f37c
commit 0590246112
23 changed files with 213 additions and 167 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ func TestDisqus_Import(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 4, size)
last, err := dataStore.Last("test", 10, time.Time{})
last, err := dataStore.Last("test", 10, time.Time{}, adminUser)
assert.Nil(t, err)
assert.Equal(t, 4, len(last), "4 comments imported")
+3 -1
View File
@@ -27,7 +27,7 @@ type Exporter interface {
// Store defines minimal interface needed to export and import comments
type Store interface {
Create(comment store.Comment) (commentID string, err error)
Find(locator store.Locator, sort string) ([]store.Comment, error)
Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error)
List(siteID string, limit int, skip int) ([]store.PostInfo, error)
DeleteAll(siteID string) error
Metas(siteID string) (umetas []service.UserMetaData, pmetas []service.PostMetaData, err error)
@@ -42,6 +42,8 @@ type ImportParams struct {
SiteID string
}
var adminUser = store.User{Admin: true}
// ImportComments imports from given provider format and saves to store
func ImportComments(p ImportParams) (int, error) {
log.Printf("[INFO] import from %s (%s) to %s", p.InputFile, p.Provider, p.SiteID)
+4 -3
View File
@@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"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/service"
@@ -36,7 +37,7 @@ func TestMigrator_ImportDisqus(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 4, size)
last, err := dataStore.Last("test", 10, time.Time{})
last, err := dataStore.Last("test", 10, time.Time{}, store.User{})
assert.Nil(t, err)
assert.Equal(t, 4, len(last), "4 comments imported")
}
@@ -62,7 +63,7 @@ func TestMigrator_ImportWordPress(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 3, size)
last, err := dataStore.Last("test", 10, time.Time{})
last, err := dataStore.Last("test", 10, time.Time{}, store.User{})
assert.Nil(t, err)
assert.Equal(t, 3, len(last), "3 comments imported")
}
@@ -92,7 +93,7 @@ func TestMigrator_ImportNative(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 2, size)
last, err := dataStore.Last("radio-t", 10, time.Time{})
last, err := dataStore.Last("radio-t", 10, time.Time{}, store.User{})
assert.Nil(t, err)
assert.Equal(t, 2, len(last), "2 comments imported")
}
+1 -1
View File
@@ -49,7 +49,7 @@ func (n *Native) Export(w io.Writer, siteID string) (size int, err error) {
commentsCount := 0
for i := len(topics) - 1; i >= 0; i-- { // topics from List sorted in opposite direction
topic := topics[i]
comments, e := n.DataStore.Find(store.Locator{SiteID: siteID, URL: topic.URL}, "time")
comments, e := n.DataStore.Find(store.Locator{SiteID: siteID, URL: topic.URL}, "time", adminUser)
if e != nil {
return commentsCount, e
}
+8 -5
View File
@@ -77,12 +77,13 @@ func TestNative_Import(t *testing.T) {
{"id":"f863bd79-fec6-4a75-b308-61fe5dd02aa1","pid":"1234","text":"some text2","user":{"name":"user name","id":"user2","picture":"","ip":"293ec5b0cf154855258824ec7fac5dc63d176915","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com/2"},"score":0,"votes":{},"time":"2017-12-20T15:18:23-06:00"}`
b := prep(t) // write some recs
r := Native{DataStore: &service.DataStore{Interface: b, AdminStore: admin.NewStaticStore("12345", []string{}, "")}}
b.AdminStore = admin.NewStaticStore("12345", []string{}, "")
r := Native{DataStore: b}
size, err := r.Import(strings.NewReader(inp), "radio-t")
assert.Nil(t, err)
assert.Equal(t, 2, size)
comments, err := b.Last("radio-t", 10, time.Time{})
comments, err := b.Last("radio-t", 10, time.Time{}, store.User{})
assert.Nil(t, err)
assert.Equal(t, 2, len(comments))
assert.Equal(t, "f863bd79-fec6-4a75-b308-61fe5dd02aa1", comments[0].ID)
@@ -106,7 +107,8 @@ func TestNative_ImportWrongVersion(t *testing.T) {
{"id":"f863bd79-fec6-4a75-b308-61fe5dd02aa1","pid":"1234","text":"some text2","user":{"name":"user name","id":"user2","picture":"","ip":"293ec5b0cf154855258824ec7fac5dc63d176915","admin":false},"locator":{"site":"radio-t","url":"https://radio-t.com/2"},"score":0,"votes":{},"time":"2017-12-20T15:18:23-06:00"}`
b := prep(t) // write some recs
r := Native{DataStore: &service.DataStore{Interface: b, AdminStore: admin.NewStaticStore("12345", []string{}, "")}}
b.AdminStore = admin.NewStaticStore("12345", []string{}, "")
r := Native{DataStore: b}
size, err := r.Import(strings.NewReader(inp), "radio-t")
assert.EqualError(t, err, "unexpected import file version 2")
assert.Equal(t, 0, size)
@@ -126,11 +128,12 @@ func TestNative_ImportManyWithError(t *testing.T) {
buf.WriteString("{}\n")
b := prep(t) // write some recs
r := Native{DataStore: &service.DataStore{Interface: b, AdminStore: admin.NewStaticStore("12345", []string{}, "")}}
b.AdminStore = admin.NewStaticStore("12345", []string{}, "")
r := Native{DataStore: b}
n, err := r.Import(buf, "radio-t")
assert.EqualError(t, err, "failed to save 2 comments")
assert.Equal(t, 1200, n)
comments, err := b.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}, "time")
comments, err := b.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}, "time", store.User{})
assert.Nil(t, err)
assert.Equal(t, 1200, len(comments))
}
+1 -1
View File
@@ -27,7 +27,7 @@ func TestWordPress_Import(t *testing.T) {
assert.Nil(t, err)
assert.Equal(t, 3, size)
last, err := dataStore.Last(siteID, 10, time.Time{})
last, err := dataStore.Last(siteID, 10, time.Time{}, adminUser)
assert.Nil(t, err)
assert.Equal(t, 3, len(last), "3 comments imported")
+2 -2
View File
@@ -31,7 +31,7 @@ type Destination interface {
// Store defines the minimal interface accessing stored comments used by notifier
type Store interface {
Get(locator store.Locator, id string) (store.Comment, error)
Get(locator store.Locator, id string, user store.User) (store.Comment, error)
}
type request struct {
@@ -69,7 +69,7 @@ func (s *Service) Submit(comment store.Comment) {
}
parentComment := store.Comment{}
if s.dataService != nil {
if p, err := s.dataService.Get(comment.Locator, comment.ParentID); err == nil {
if p, err := s.dataService.Get(comment.Locator, comment.ParentID, store.User{}); err == nil {
parentComment = p
}
}
+1 -1
View File
@@ -146,7 +146,7 @@ func (m *mockDest) String() string { return fmt.Sprintf("mock id=%d, closed=%v",
type mockStore struct{ data map[string]store.Comment }
func (m *mockStore) Get(_ store.Locator, id string) (store.Comment, error) {
func (m *mockStore) Get(_ store.Locator, id string, user store.User) (store.Comment, error) {
res, ok := m.data[id]
if !ok {
return store.Comment{}, errors.New("no such id")
+1 -36
View File
@@ -67,7 +67,7 @@ func (a *admin) getUserInfoCtrl(w http.ResponseWriter, r *http.Request) {
siteID := r.URL.Query().Get("site")
log.Printf("[INFO] get user info for %s, site %s", userID, siteID)
ucomments, err := a.dataService.User(siteID, userID, 1, 0)
ucomments, err := a.dataService.User(siteID, userID, 1, 0, rest.GetUserOrEmpty(r))
if err != nil || len(ucomments) == 0 {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get user info", rest.ErrInternal)
return
@@ -222,38 +222,3 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
func (a *admin) checkBlocked(siteID string, user store.User) bool {
return a.dataService.IsBlocked(siteID, user.ID)
}
// post-processes comments, hides text of all comments for blocked users,
// resets score and votes too. Also hides sensitive info for non-admin users
func (a *admin) alterComments(comments []store.Comment, r *http.Request) (res []store.Comment) {
res = make([]store.Comment, len(comments))
user, err := rest.GetUserInfo(r)
isAdmin := err == nil && user.Admin
for i, c := range comments {
blocked := a.dataService.IsBlocked(c.Locator.SiteID, c.User.ID)
// process blocked users
if blocked {
if !isAdmin { // reset comment to deleted for non-admins
c.SetDeleted(store.SoftDelete)
}
c.User.Blocked = true
c.Deleted = true
}
// set verified status retroactively
if !blocked {
c.User.Verified = a.dataService.IsVerified(c.Locator.SiteID, c.User.ID)
}
// hide info from non-admins
if !isAdmin {
c.User.IP = ""
}
res[i] = c
}
return res
}
+2 -2
View File
@@ -618,7 +618,7 @@ func TestAdmin_DeleteMeRequest(t *testing.T) {
_, err = srv.DataService.Create(c2)
assert.Nil(t, err)
comments, err := srv.DataService.User("radio-t", "user1", 0, 0)
comments, err := srv.DataService.User("radio-t", "user1", 0, 0, store.User{})
assert.Nil(t, err)
assert.Equal(t, 1, len(comments), "a comment for user1")
@@ -655,7 +655,7 @@ func TestAdmin_DeleteMeRequest(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
_, err = srv.DataService.User("radio-t", "user1", 0, 0)
_, err = srv.DataService.User("radio-t", "user1", 0, 0, store.User{})
assert.EqualError(t, err, "no comments for user user1 in store")
}
-35
View File
@@ -307,41 +307,6 @@ func (s *Rest) routes() chi.Router {
return router
}
func (s *Rest) alterComments(comments []store.Comment, r *http.Request) (res []store.Comment) {
res = s.adminService.alterComments(comments, r) // apply admin's alteration
// prepare vote info for client view
vote := func(c store.Comment, r *http.Request) store.Comment {
c.Vote = 0 // default is "none" (not voted)
user, err := rest.GetUserInfo(r)
if err != nil {
c.Votes = nil // hide voters list and don't set Vote for non-authed user
return c
}
if v, ok := c.Votes[user.ID]; ok {
if v {
c.Vote = 1
} else {
c.Vote = -1
}
}
c.Votes = nil // hide voters list
return c
}
for i, c := range res {
c = vote(c, r)
res[i] = c
}
return res
}
// updateLimiter returns UpdateLimiter if set, or 10 if not
func (s *Rest) updateLimiter() float64 {
lmt := 10.0
+3 -3
View File
@@ -67,7 +67,7 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
}
// DataService modifies comment
finalComment, err := s.DataService.Get(comment.Locator, id)
finalComment, err := s.DataService.Get(comment.Locator, id, rest.GetUserOrEmpty(r))
if err != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't load created comment", rest.ErrInternal)
return
@@ -107,7 +107,7 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
var currComment store.Comment
var err error
if currComment, err = s.DataService.Get(locator, id); err != nil {
if currComment, err = s.DataService.Get(locator, id, rest.GetUserOrEmpty(r)); err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't find comment", rest.ErrCommentNotFound)
return
}
@@ -213,7 +213,7 @@ func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) {
// get comments in 100 in each paginated request
for i := 0; i < 100; i++ {
comments, errUser := s.DataService.User(siteID, user.ID, 100, i*100)
comments, errUser := s.DataService.User(siteID, user.ID, 100, i*100, rest.GetUserOrEmpty(r))
if errUser != nil {
rest.SendErrorJSON(w, r, http.StatusInternalServerError, errUser, "can't get user comments", rest.ErrInternal)
return
+1 -1
View File
@@ -63,7 +63,7 @@ func TestRest_CreateOldPost(t *testing.T) {
_, err := srv.DataService.Create(old)
assert.Nil(t, err)
comments, err := srv.DataService.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, "time")
comments, err := srv.DataService.Find(store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, "time", store.User{})
assert.Nil(t, err)
assert.Equal(t, 1, len(comments))
+9 -13
View File
@@ -34,15 +34,14 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
key := cache.NewKey(locator.SiteID).ID(URLKeyWithUser(r)).Scopes(locator.SiteID, locator.URL)
data, err := s.Cache.Get(key, func() ([]byte, error) {
comments, e := s.DataService.Find(locator, sort)
comments, e := s.DataService.Find(locator, sort, rest.GetUserOrEmpty(r))
if e != nil {
comments = []store.Comment{} // error should clear comments and continue for post info
}
maskedComments := s.alterComments(comments, r)
var b []byte
switch r.URL.Query().Get("format") {
case "tree":
tree := service.MakeTree(maskedComments, sort, s.ReadOnlyAge)
tree := service.MakeTree(comments, sort, s.ReadOnlyAge)
if tree.Nodes == nil { // eliminate json nil serialization
tree.Nodes = []*service.Node{}
}
@@ -51,7 +50,7 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
}
b, e = encodeJSONWithHTML(tree)
default:
withInfo := commentsWithInfo{Comments: maskedComments}
withInfo := commentsWithInfo{Comments: comments}
if info, ee := s.DataService.Info(locator, s.ReadOnlyAge); ee == nil {
withInfo.Info = info
}
@@ -132,9 +131,9 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
sinceTime := time.Time{}
since := r.URL.Query().Get("since")
if since != "" {
unixTS, err := strconv.ParseInt(since, 10, 64)
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't translate since parameter", rest.ErrDecode)
unixTS, e := strconv.ParseInt(since, 10, 64)
if e != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, e, "can't translate since parameter", rest.ErrDecode)
return
}
sinceTime = time.Unix(unixTS/1000, 1000000*(unixTS%1000)) // since param in msec timestamp
@@ -142,11 +141,10 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(lastCommentsScope)
data, err := s.Cache.Get(key, func() ([]byte, error) {
comments, e := s.DataService.Last(siteID, limit, sinceTime)
comments, e := s.DataService.Last(siteID, limit, sinceTime, rest.GetUserOrEmpty(r))
if e != nil {
return nil, e
}
comments = s.alterComments(comments, r)
// filter deleted from last comments view. Blocked marked as deleted and will sneak in without
filterDeleted := filterComments(comments, func(c store.Comment) bool { return !c.Deleted })
return encodeJSONWithHTML(filterDeleted)
@@ -171,12 +169,11 @@ func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
log.Printf("[DEBUG] get comments by id %s, %s %s", id, siteID, url)
comment, err := s.DataService.Get(store.Locator{SiteID: siteID, URL: url}, id)
comment, err := s.DataService.Get(store.Locator{SiteID: siteID, URL: url}, id, rest.GetUserOrEmpty(r))
if err != nil {
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get comment by id", rest.ErrCommentNotFound)
return
}
comment = s.alterComments([]store.Comment{comment}, r)[0]
render.Status(r, http.StatusOK)
if err = R.RenderJSONWithHTML(w, r, comment); err != nil {
@@ -204,11 +201,10 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
key := cache.NewKey(siteID).ID(URLKeyWithUser(r)).Scopes(userID, siteID)
data, err := s.Cache.Get(key, func() ([]byte, error) {
comments, e := s.DataService.User(siteID, userID, limit, 0)
comments, e := s.DataService.User(siteID, userID, limit, 0, rest.GetUserOrEmpty(r))
if e != nil {
return nil, e
}
comments = s.alterComments(comments, r)
comments = filterComments(comments, func(c store.Comment) bool { return !c.Deleted })
count, e := s.DataService.UserCount(siteID, userID)
if e != nil {
+5 -8
View File
@@ -28,11 +28,10 @@ func (s *Rest) rssPostCommentsCtrl(w http.ResponseWriter, r *http.Request) {
key := cache.NewKey(locator.SiteID).ID(URLKey(r)).Scopes(locator.SiteID, locator.URL)
data, err := s.Cache.Get(key, func() ([]byte, error) {
comments, e := s.DataService.Find(locator, "-time")
comments, e := s.DataService.Find(locator, "-time", rest.GetUserOrEmpty(r))
if e != nil {
return nil, e
}
comments = s.alterComments(comments, r)
rss, e := s.toRssFeed(locator.URL, comments, "post comments for "+r.URL.Query().Get("url"))
if e != nil {
return nil, e
@@ -60,11 +59,10 @@ func (s *Rest) rssSiteCommentsCtrl(w http.ResponseWriter, r *http.Request) {
key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(siteID, lastCommentsScope)
data, err := s.Cache.Get(key, func() ([]byte, error) {
comments, e := s.DataService.Last(siteID, maxRssItems, time.Time{})
comments, e := s.DataService.Last(siteID, maxRssItems, time.Time{}, rest.GetUserOrEmpty(r))
if e != nil {
return nil, e
}
comments = s.alterComments(comments, r)
rss, e := s.toRssFeed(r.URL.Query().Get("site"), comments, "site comment for "+siteID)
if e != nil {
@@ -94,11 +92,10 @@ func (s *Rest) rssRepliesCtrl(w http.ResponseWriter, r *http.Request) {
userName := ""
key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(siteID, lastCommentsScope)
data, err := s.Cache.Get(key, func() (res []byte, e error) {
comments, e := s.DataService.Last(siteID, maxLastCommentsReply, time.Time{})
comments, e := s.DataService.Last(siteID, maxLastCommentsReply, time.Time{}, rest.GetUserOrEmpty(r))
if e != nil {
return nil, errors.Wrap(e, "can't get last comments")
}
comments = s.alterComments(comments, r)
replies := []store.Comment{}
for _, c := range comments {
if len(replies) > maxRssItems || c.Timestamp.Add(maxReplyDuration).Before(time.Now()) {
@@ -109,7 +106,7 @@ func (s *Rest) rssRepliesCtrl(w http.ResponseWriter, r *http.Request) {
}
if c.ParentID != "" && !c.Deleted && c.User.ID != userID { // not interested in replies to yourself
var pc store.Comment
if pc, e = s.DataService.Get(c.Locator, c.ParentID); e != nil {
if pc, e = s.DataService.Get(c.Locator, c.ParentID, rest.GetUserOrEmpty(r)); e != nil {
return nil, errors.Wrap(e, "can't get parent comment")
}
if pc.User.ID == userID {
@@ -166,7 +163,7 @@ func (s *Rest) toRssFeed(url string, comments []store.Comment, description strin
}
if c.ParentID != "" {
// add indication to parent comment
parentComment, err := s.DataService.Get(c.Locator, c.ParentID)
parentComment, err := s.DataService.Get(c.Locator, c.ParentID, store.User{})
if err == nil {
f.Title = fmt.Sprintf("%s > %s", c.User.Name, parentComment.User.Name)
} else {
+8
View File
@@ -36,7 +36,15 @@ func GetUserInfo(r *http.Request) (user store.User, err error) {
Verified: u.BoolAttr("verified"),
Blocked: u.BoolAttr("blocked"),
}, nil
}
// GetUserOrEmpty attempts to get user info from request and returns empty object if failed
func GetUserOrEmpty(r *http.Request) store.User {
user, err := GetUserInfo(r)
if err != nil {
return store.User{}
}
return user
}
// SetUserInfo sets user into request context
+1 -1
View File
@@ -10,7 +10,7 @@ import (
"github.com/umputun/remark/backend/app/store"
)
//go:generate sh -c "mockery -inpkg -name Interface -print > file.tmp && mv file.tmp engine_mock.go"
//go:generate sh -c "mockery -inpkg -name Interface -print > /tmp/engine-mock.tmp && mv /tmp/engine-mock.tmp engine_mock.go"
// Interface combines all store interfaces
type Interface interface {
+107 -25
View File
@@ -1,3 +1,6 @@
// Package service wraps engine interfaces with common logic unrelated to any particular engine implementation.
// All consumers should be using service.DataStore and not the naked engine!
package service
import (
@@ -96,12 +99,48 @@ func (s *DataStore) Create(comment store.Comment) (commentID string, err error)
return s.Interface.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)
if err != nil {
return comments, err
}
changedSort := false
// set votes controversy for comments added prior to #274
for i, c := range comments {
if c.Controversy == 0 && len(c.Votes) > 0 {
c.Controversy = s.controversy(s.upsAndDowns(c))
if !changedSort && strings.Contains(sort, "controversy") { // trigger sort change
changedSort = true
}
}
comments[i] = s.alterComment(c, user)
}
// resort commits if altered
if changedSort {
comments = engine.SortComments(comments, sort)
}
return comments, nil
}
// 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)
if err != nil {
return store.Comment{}, err
}
return s.alterComment(c, user), nil
}
// 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.Get(c.Locator, c.ID) // this can be called after last edit, we have to retrieve fresh comment
cc, err := s.Interface.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
@@ -143,7 +182,7 @@ func (s *DataStore) prepareNewComment(comment store.Comment) (store.Comment, err
// SetPin pin/un-pin comment as special
func (s *DataStore) SetPin(locator store.Locator, commentID string, status bool) error {
comment, err := s.Get(locator, commentID)
comment, err := s.Interface.Get(locator, commentID)
if err != nil {
return err
}
@@ -158,7 +197,7 @@ func (s *DataStore) Vote(locator store.Locator, commentID string, userID string,
cLock.Lock() // prevents race on voting
defer cLock.Unlock()
comment, err = s.Get(locator, commentID)
comment, err = s.Interface.Get(locator, commentID)
if err != nil {
return comment, err
}
@@ -246,7 +285,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.Get(locator, commentID)
comment, err = s.Interface.Get(locator, commentID)
if err != nil {
return comment, err
}
@@ -295,7 +334,7 @@ func (s *DataStore) HasReplies(comment store.Comment) bool {
return true
}
comments, err := s.Last(comment.Locator.SiteID, maxLastCommentsReply, time.Time{})
comments, err := s.Interface.Last(comment.Locator.SiteID, maxLastCommentsReply, time.Time{})
if err != nil {
log.Printf("[WARN] can't get last comments for reply check, %v", err)
return false
@@ -318,7 +357,7 @@ func (s *DataStore) SetTitle(locator store.Locator, commentID string) (comment s
return comment, errors.New("no title extractor")
}
comment, err = s.Get(locator, commentID)
comment, err = s.Interface.Get(locator, commentID)
if err != nil {
return comment, err
}
@@ -452,30 +491,22 @@ func (s *DataStore) SetMetas(siteID string, umetas []UserMetaData, pmetas []Post
return errs.ErrorOrNil()
}
// Find wraps engine's Find call and alter results if needed
func (s *DataStore) Find(locator store.Locator, sort string) ([]store.Comment, error) {
comments, err := s.Interface.Find(locator, sort)
// 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)
if err != nil {
return comments, err
}
return s.alterComments(comments, user), nil
}
changedSort := false
// set votes controversy for comments added prior to #274
for i, c := range comments {
if c.Controversy == 0 && len(c.Votes) > 0 {
comments[i].Controversy = s.controversy(s.upsAndDowns(c))
if !changedSort && strings.Contains(sort, "controversy") { // trigger sort change
changedSort = true
}
}
// 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)
if err != nil {
return comments, err
}
// resort commits if altered
if changedSort {
comments = engine.SortComments(comments, sort)
}
return comments, nil
return s.alterComments(comments, user), nil
}
func (s *DataStore) upsAndDowns(c store.Comment) (ups, downs int) {
@@ -503,3 +534,54 @@ func (s *DataStore) getScopedLocks(id string) (lock sync.Locker) {
return lock
}
func (s *DataStore) alterComments(cc []store.Comment, user store.User) (res []store.Comment) {
res = make([]store.Comment, len(cc))
for i, c := range cc {
res[i] = s.alterComment(c, user)
}
return res
}
func (s *DataStore) alterComment(c store.Comment, user store.User) (res store.Comment) {
blocked := s.IsBlocked(c.Locator.SiteID, c.User.ID)
// process blocked users
if blocked {
if !user.Admin { // reset comment to deleted for non-admins
c.SetDeleted(store.SoftDelete)
}
c.User.Blocked = true
c.Deleted = true
}
// set verified status retroactively
if !blocked {
c.User.Verified = s.IsVerified(c.Locator.SiteID, c.User.ID)
}
// hide info from non-admins
if !user.Admin {
c.User.IP = ""
}
c = s.prepVotes(c, user)
return c
}
// prepare vote info for client view
func (s *DataStore) prepVotes(c store.Comment, user store.User) store.Comment {
c.Vote = 0 // default is "none" (not voted)
if v, ok := c.Votes[user.ID]; ok {
if v {
c.Vote = 1
} else {
c.Vote = -1
}
}
c.Votes = nil // hide voters list
return c
}
+48 -26
View File
@@ -6,6 +6,7 @@ import (
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -18,11 +19,11 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/umputun/remark/backend/app/store/image"
"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/image"
)
var testDb = "/tmp/test-remark.db"
@@ -40,7 +41,7 @@ func TestService_CreateFromEmpty(t *testing.T) {
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
res, err := b.Interface.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
@@ -66,7 +67,7 @@ func TestService_CreateFromPartial(t *testing.T) {
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
res, err := b.Interface.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "text", res.Text)
@@ -94,7 +95,7 @@ func TestService_CreateFromPartialWithTitle(t *testing.T) {
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Get(store.Locator{URL: "https://radio-t.com/p/2018/12/29/podcast-630/", SiteID: "radio-t"}, id)
res, err := b.Interface.Get(store.Locator{URL: "https://radio-t.com/p/2018/12/29/podcast-630/", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "Радио-Т 630 — Радио-Т Подкаст", res.PostTitle)
@@ -102,7 +103,7 @@ func TestService_CreateFromPartialWithTitle(t *testing.T) {
comment.PostTitle = "post blah"
id, err = b.Create(comment)
assert.NoError(t, err)
res, err = b.Get(store.Locator{URL: "https://radio-t.com/p/2018/12/29/podcast-630/", SiteID: "radio-t"}, id)
res, err = b.Interface.Get(store.Locator{URL: "https://radio-t.com/p/2018/12/29/podcast-630/", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "post blah", res.PostTitle, "keep comment title")
@@ -145,7 +146,7 @@ func TestService_SetTitle(t *testing.T) {
assert.NoError(t, err)
assert.True(t, id != "", id)
res, err := b.Get(store.Locator{URL: tss.URL + "/post1", SiteID: "radio-t"}, id)
res, err := b.Interface.Get(store.Locator{URL: tss.URL + "/post1", SiteID: "radio-t"}, id)
assert.NoError(t, err)
t.Logf("%+v", res)
assert.Equal(t, "", res.PostTitle)
@@ -174,7 +175,7 @@ func TestService_Vote(t *testing.T) {
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 3, len(res))
@@ -195,7 +196,7 @@ func TestService_Vote(t *testing.T) {
assert.NotNil(t, err, "double-voting rejected")
assert.True(t, strings.HasPrefix(err.Error(), "user user1 already voted"))
res, err = b.Last("radio-t", 0, time.Time{})
res, err = b.Interface.Last("radio-t", 0, time.Time{})
assert.Nil(t, err)
assert.Equal(t, 3, len(res))
assert.Equal(t, 1, res[0].Score)
@@ -204,7 +205,7 @@ func TestService_Vote(t *testing.T) {
_, err = b.Vote(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, "user1", false)
assert.Nil(t, err, "vote reset")
res, err = b.Last("radio-t", 0, time.Time{})
res, err = b.Interface.Last("radio-t", 0, time.Time{})
assert.Nil(t, err)
assert.Equal(t, 3, len(res))
assert.Equal(t, 0, res[0].Score)
@@ -250,7 +251,7 @@ func TestService_VoteAggressive(t *testing.T) {
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
require.Nil(t, err)
t.Logf("%+v", res[0])
assert.Equal(t, 3, len(res))
@@ -271,7 +272,7 @@ func TestService_VoteAggressive(t *testing.T) {
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0, time.Time{})
res, err = b.Interface.Last("radio-t", 0, time.Time{})
require.NoError(t, err)
t.Logf("%+v", res[0])
@@ -290,7 +291,7 @@ func TestService_VoteAggressive(t *testing.T) {
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0, time.Time{})
res, err = b.Interface.Last("radio-t", 0, time.Time{})
require.NoError(t, err)
assert.Equal(t, 3, len(res))
t.Logf("%+v %d", res[0], res[0].Score)
@@ -309,7 +310,7 @@ func TestService_VoteConcurrent(t *testing.T) {
}
_, err := b.Create(comment)
assert.NoError(t, err)
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
require.Nil(t, err)
// concurrent vote +1 as multiple users for the same comment
@@ -324,7 +325,7 @@ func TestService_VoteConcurrent(t *testing.T) {
}()
}
wg.Wait()
res, err = b.Last("radio-t", 0, time.Time{})
res, err = b.Interface.Last("radio-t", 0, time.Time{})
require.NoError(t, err)
assert.Equal(t, 100, res[0].Score, "should have 100 score")
assert.Equal(t, 100, len(res[0].Votes), "should have 100 votes")
@@ -370,7 +371,7 @@ func TestService_VoteControversy(t *testing.T) {
assert.InDelta(t, 1.73, c.Controversy, 0.01)
// check if stored
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
require.NoError(t, err)
assert.Equal(t, 1, res[0].Score, "should have 1 score")
assert.InDelta(t, 1.73, res[0].Controversy, 0.01)
@@ -404,7 +405,7 @@ func TestService_Pin(t *testing.T) {
defer teardown(t)
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
@@ -413,13 +414,13 @@ func TestService_Pin(t *testing.T) {
err = b.SetPin(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, true)
assert.Nil(t, err)
c, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
c, err := b.Interface.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, true, c.Pin)
err = b.SetPin(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, false)
assert.Nil(t, err)
c, err = b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
c, err = b.Interface.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, false, c.Pin)
}
@@ -428,7 +429,7 @@ func TestService_EditComment(t *testing.T) {
defer teardown(t)
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
@@ -441,7 +442,7 @@ func TestService_EditComment(t *testing.T) {
assert.Equal(t, "xxx", comment.Text)
assert.Equal(t, "yyy", comment.Orig)
c, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
c, err := b.Interface.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.Equal(t, "my edit", c.Edit.Summary)
assert.Equal(t, "xxx", c.Text)
@@ -455,7 +456,7 @@ func TestService_DeleteComment(t *testing.T) {
defer teardown(t)
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
@@ -464,7 +465,7 @@ func TestService_DeleteComment(t *testing.T) {
_, err = b.EditComment(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID, EditRequest{Delete: true})
assert.Nil(t, err)
c, err := b.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
c, err := b.Interface.Get(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID)
assert.Nil(t, err)
assert.True(t, c.Deleted)
t.Logf("%+v", c)
@@ -474,7 +475,7 @@ func TestService_EditCommentDurationFailed(t *testing.T) {
defer teardown(t)
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond, AdminStore: admin.NewStaticKeyStore("secret 123")}
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
t.Logf("%+v", res[0])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
@@ -491,7 +492,7 @@ func TestService_EditCommentReplyFailed(t *testing.T) {
defer teardown(t)
b := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
res, err := b.Last("radio-t", 0, time.Time{})
res, err := b.Interface.Last("radio-t", 0, time.Time{})
t.Logf("%+v", res[1])
assert.Nil(t, err)
assert.Equal(t, 2, len(res))
@@ -534,6 +535,7 @@ func TestService_ValidateComment(t *testing.T) {
assert.Nil(t, e, "check #%d", n)
continue
}
require.NotNil(t, e)
assert.EqualError(t, tt.err, e.Error(), "check #%d", n)
}
}
@@ -669,7 +671,7 @@ func TestService_Find(t *testing.T) {
b := DataStore{Interface: prepStoreEngine(t), EditDuration: 100 * time.Millisecond,
AdminStore: admin.NewStaticStore("secret 123", []string{"user2"}, "user@email.com")}
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time")
res, err := b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "time", store.User{})
require.NoError(t, err)
assert.Equal(t, 2, len(res))
@@ -687,7 +689,7 @@ func TestService_Find(t *testing.T) {
assert.Nil(t, err)
// make sure Controversy altered
res, err = b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "-controversy")
res, err = b.Find(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "-controversy", store.User{})
require.NoError(t, err)
assert.Equal(t, 3, len(res))
assert.Equal(t, "123456", res[0].ID)
@@ -722,6 +724,26 @@ func TestService_submitImages(t *testing.T) {
time.Sleep(250 * time.Millisecond)
}
func TestService_alterComment(t *testing.T) {
defer teardown(t)
svc := DataStore{Interface: prepStoreEngine(t), AdminStore: admin.NewStaticKeyStore("secret 123")}
tbl := []struct {
c store.Comment
u store.User
r store.Comment
}{
{store.Comment{}, store.User{}, store.Comment{}},
{store.Comment{}, store.User{}, store.Comment{}},
}
for i, tt := range tbl {
t.Run(strconv.Itoa(i), func(t *testing.T) {
r := svc.alterComment(tt.c, tt.u)
assert.Equal(t, tt.r, r)
})
}
}
// makes new boltdb, put two records
func prepStoreEngine(t *testing.T) engine.Interface {
_ = os.Remove(testDb)
+1 -2
View File
@@ -24,8 +24,7 @@ require (
github.com/golang/protobuf v1.3.1 // indirect
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c
github.com/gorilla/feeds v1.1.0
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/golang-lru v0.5.1 // indirect
github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc
github.com/microcosm-cc/bluemonday v0.0.0-20171222152607-542fd4642604
+3
View File
@@ -71,6 +71,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874 h1:em+tTnzgU7N22woTBMcSJAOW7tRHAkK597W+MD/CpK8=
github.com/hashicorp/go-multierror v0.0.0-20171204182908-b7773ae21874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
@@ -107,6 +109,7 @@ github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/umputun/remark v1.3.2 h1:m0PvvY7GEWSIjfQeqddvglJVswxUoSBPi4RrsI3sJz8=
go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+1
View File
@@ -1,5 +1,6 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731 h1:y7wyeiA6T+TT+HGC9DYypvLkUeg99N4rqHMzn2MmjYk=
github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+2
View File
@@ -1 +1,3 @@
module github.com/hashicorp/golang-lru
go 1.12