@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -15,18 +15,31 @@ import (
|
||||
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
// admin provides router for all requests available for admin users only
|
||||
type admin struct {
|
||||
dataService *service.DataStore
|
||||
dataService adminStore
|
||||
cache cache.LoadingCache
|
||||
authenticator *auth.Service
|
||||
readOnlyAge int
|
||||
migrator *Migrator
|
||||
}
|
||||
|
||||
type adminStore interface {
|
||||
Delete(locator store.Locator, commentID string, mode store.DeleteMode) error
|
||||
DeleteUser(siteID string, userID string) error
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
IsBlocked(siteID string, userID string) bool
|
||||
SetBlock(siteID string, userID string, status bool, ttl time.Duration) error
|
||||
Blocked(siteID string) ([]store.BlockedUser, error)
|
||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||
SetTitle(locator store.Locator, commentID string) (comment store.Comment, err error)
|
||||
SetVerified(siteID string, userID string, status bool) error
|
||||
SetReadOnly(locator store.Locator, status bool) error
|
||||
SetPin(locator store.Locator, commentID string, status bool) error
|
||||
}
|
||||
|
||||
// DELETE /comment/{id}?site=siteID&url=post-url - removes comment
|
||||
func (a *admin) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -67,7 +80,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
|
||||
@@ -218,42 +231,3 @@ func (a *admin) setPinCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL))
|
||||
render.JSON(w, r, R.JSON{"id": commentID, "locator": locator, "pin": pinStatus})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ func TestAdmin_Block(t *testing.T) {
|
||||
assert.Equal(t, "", comments.Comments[0].Text)
|
||||
assert.True(t, comments.Comments[0].Deleted)
|
||||
|
||||
srv.Cache = &cache.Nop{} // TODO: with lru cache it won't be refreshed and invalidated for long time
|
||||
srv.pubRest.cache = &cache.Nop{} // TODO: with lru cache it won't be refreshed and invalidated for long time
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
res, code = get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah&sort=+time")
|
||||
assert.Equal(t, 200, code)
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
+110
-71
@@ -59,7 +59,9 @@ type Rest struct {
|
||||
httpServer *http.Server
|
||||
lock sync.Mutex
|
||||
|
||||
adminService admin
|
||||
pubRest public
|
||||
privRest private
|
||||
adminRest admin
|
||||
}
|
||||
|
||||
const hardBodyLimit = 1024 * 64 // limit size of body
|
||||
@@ -167,7 +169,28 @@ func (s *Rest) routes() chi.Router {
|
||||
router.Use(middleware.Throttle(1000), middleware.Timeout(60*time.Second))
|
||||
router.Use(R.AppInfo("remark42", "umputun", s.Version), R.Ping)
|
||||
|
||||
s.adminService = admin{
|
||||
s.pubRest = public{
|
||||
dataService: s.DataService,
|
||||
cache: s.Cache,
|
||||
imageService: s.ImageService,
|
||||
commentFormatter: s.CommentFormatter,
|
||||
readOnlyAge: s.ReadOnlyAge,
|
||||
confFn: s.config,
|
||||
webRoot: s.WebRoot,
|
||||
}
|
||||
|
||||
s.privRest = private{
|
||||
dataService: s.DataService,
|
||||
cache: s.Cache,
|
||||
imageService: s.ImageService,
|
||||
commentFormatter: s.CommentFormatter,
|
||||
readOnlyAge: s.ReadOnlyAge,
|
||||
authenticator: s.Authenticator,
|
||||
notifyService: s.NotifyService,
|
||||
remarkURL: s.RemarkURL,
|
||||
}
|
||||
|
||||
s.adminRest = admin{
|
||||
dataService: s.DataService,
|
||||
migrator: s.Migrator,
|
||||
cache: s.Cache,
|
||||
@@ -215,16 +238,16 @@ func (s *Rest) routes() chi.Router {
|
||||
rapi.Group(func(ropen chi.Router) {
|
||||
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
ropen.Use(authMiddleware.Trace, middleware.NoCache, logInfoWithBody)
|
||||
ropen.Get("/find", s.findCommentsCtrl)
|
||||
ropen.Get("/id/{id}", s.commentByIDCtrl)
|
||||
ropen.Get("/comments", s.findUserCommentsCtrl)
|
||||
ropen.Get("/last/{limit}", s.lastCommentsCtrl)
|
||||
ropen.Get("/count", s.countCtrl)
|
||||
ropen.Post("/counts", s.countMultiCtrl)
|
||||
ropen.Get("/list", s.listCtrl)
|
||||
ropen.Get("/config", s.configCtrl)
|
||||
ropen.Post("/preview", s.previewCommentCtrl)
|
||||
ropen.Get("/info", s.infoCtrl)
|
||||
ropen.Get("/find", s.pubRest.findCommentsCtrl)
|
||||
ropen.Get("/id/{id}", s.pubRest.commentByIDCtrl)
|
||||
ropen.Get("/comments", s.pubRest.findUserCommentsCtrl)
|
||||
ropen.Get("/last/{limit}", s.pubRest.lastCommentsCtrl)
|
||||
ropen.Get("/count", s.pubRest.countCtrl)
|
||||
ropen.Post("/counts", s.pubRest.countMultiCtrl)
|
||||
ropen.Get("/list", s.pubRest.listCtrl)
|
||||
ropen.Get("/config", s.pubRest.configCtrl)
|
||||
ropen.Post("/preview", s.pubRest.previewCommentCtrl)
|
||||
ropen.Get("/info", s.pubRest.infoCtrl)
|
||||
ropen.Get("/img", s.ImageProxy.Handler)
|
||||
|
||||
ropen.Route("/rss", func(rrss chi.Router) {
|
||||
@@ -238,15 +261,15 @@ func (s *Rest) routes() chi.Router {
|
||||
rapi.Group(func(ropen chi.Router) {
|
||||
ropen.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
ropen.Use(authMiddleware.Trace, logInfoWithBody)
|
||||
ropen.Get("/picture/{user}/{id}", s.loadPictureCtrl)
|
||||
ropen.Get("/picture/{user}/{id}", s.pubRest.loadPictureCtrl)
|
||||
})
|
||||
|
||||
// protected routes, require auth
|
||||
rapi.Group(func(rauth chi.Router) {
|
||||
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(10, nil)))
|
||||
rauth.Use(authMiddleware.Auth, middleware.NoCache, logInfoWithBody)
|
||||
rauth.Get("/user", s.userInfoCtrl)
|
||||
rauth.Get("/userdata", s.userAllDataCtrl)
|
||||
rauth.Get("/user", s.privRest.userInfoCtrl)
|
||||
rauth.Get("/userdata", s.privRest.userAllDataCtrl)
|
||||
})
|
||||
|
||||
// admin routes, require auth and admin users only
|
||||
@@ -255,22 +278,22 @@ func (s *Rest) routes() chi.Router {
|
||||
radmin.Use(authMiddleware.Auth, authMiddleware.AdminOnly)
|
||||
radmin.Use(middleware.NoCache, logInfoWithBody)
|
||||
|
||||
radmin.Delete("/comment/{id}", s.adminService.deleteCommentCtrl)
|
||||
radmin.Put("/user/{userid}", s.adminService.setBlockCtrl)
|
||||
radmin.Delete("/user/{userid}", s.adminService.deleteUserCtrl)
|
||||
radmin.Get("/user/{userid}", s.adminService.getUserInfoCtrl)
|
||||
radmin.Get("/deleteme", s.adminService.deleteMeRequestCtrl)
|
||||
radmin.Put("/verify/{userid}", s.adminService.setVerifyCtrl)
|
||||
radmin.Put("/pin/{id}", s.adminService.setPinCtrl)
|
||||
radmin.Get("/blocked", s.adminService.blockedUsersCtrl)
|
||||
radmin.Put("/readonly", s.adminService.setReadOnlyCtrl)
|
||||
radmin.Put("/title/{id}", s.adminService.setTitleCtrl)
|
||||
radmin.Delete("/comment/{id}", s.adminRest.deleteCommentCtrl)
|
||||
radmin.Put("/user/{userid}", s.adminRest.setBlockCtrl)
|
||||
radmin.Delete("/user/{userid}", s.adminRest.deleteUserCtrl)
|
||||
radmin.Get("/user/{userid}", s.adminRest.getUserInfoCtrl)
|
||||
radmin.Get("/deleteme", s.adminRest.deleteMeRequestCtrl)
|
||||
radmin.Put("/verify/{userid}", s.adminRest.setVerifyCtrl)
|
||||
radmin.Put("/pin/{id}", s.adminRest.setPinCtrl)
|
||||
radmin.Get("/blocked", s.adminRest.blockedUsersCtrl)
|
||||
radmin.Put("/readonly", s.adminRest.setReadOnlyCtrl)
|
||||
radmin.Put("/title/{id}", s.adminRest.setTitleCtrl)
|
||||
|
||||
// migrator
|
||||
radmin.Get("/export", s.adminService.migrator.exportCtrl)
|
||||
radmin.Post("/import", s.adminService.migrator.importCtrl)
|
||||
radmin.Post("/import/form", s.adminService.migrator.importFormCtrl)
|
||||
radmin.Get("/import/wait", s.adminService.migrator.importWaitCtrl)
|
||||
radmin.Get("/export", s.adminRest.migrator.exportCtrl)
|
||||
radmin.Post("/import", s.adminRest.migrator.importCtrl)
|
||||
radmin.Post("/import/form", s.adminRest.migrator.importFormCtrl)
|
||||
radmin.Get("/import/wait", s.adminRest.migrator.importWaitCtrl)
|
||||
})
|
||||
|
||||
// protected routes, throttled to 10/s by default, controlled by external UpdateLimiter param
|
||||
@@ -280,17 +303,17 @@ func (s *Rest) routes() chi.Router {
|
||||
rauth.Use(middleware.NoCache)
|
||||
rauth.Use(logger.New(logger.Log(log.Default()), logger.WithBody, logger.Prefix("[DEBUG]"), logger.IPfn(ipFn)).Handler)
|
||||
|
||||
rauth.Put("/comment/{id}", s.updateCommentCtrl)
|
||||
rauth.Post("/comment", s.createCommentCtrl)
|
||||
rauth.With(rejectAnonUser).Put("/vote/{id}", s.voteCtrl)
|
||||
rauth.With(rejectAnonUser).Post("/deleteme", s.deleteMeCtrl)
|
||||
rauth.Put("/comment/{id}", s.privRest.updateCommentCtrl)
|
||||
rauth.Post("/comment", s.privRest.createCommentCtrl)
|
||||
rauth.With(rejectAnonUser).Put("/vote/{id}", s.privRest.voteCtrl)
|
||||
rauth.With(rejectAnonUser).Post("/deleteme", s.privRest.deleteMeCtrl)
|
||||
})
|
||||
|
||||
rapi.Group(func(rauth chi.Router) {
|
||||
rauth.Use(tollbooth_chi.LimitHandler(tollbooth.NewLimiter(s.updateLimiter(), nil)))
|
||||
rauth.Use(authMiddleware.Auth, rejectAnonUser)
|
||||
rauth.Use(logger.New(logger.Log(log.Default()), logger.Prefix("[DEBUG]"), logger.IPfn(ipFn)).Handler)
|
||||
rauth.Post("/picture", s.savePictureCtrl)
|
||||
rauth.Post("/picture", s.privRest.savePictureCtrl)
|
||||
})
|
||||
|
||||
})
|
||||
@@ -298,8 +321,8 @@ func (s *Rest) routes() chi.Router {
|
||||
// open routes on root level
|
||||
router.Group(func(rroot chi.Router) {
|
||||
tollbooth_chi.LimitHandler(tollbooth.NewLimiter(50, nil))
|
||||
rroot.Get("/index.html", s.getStartedCtrl)
|
||||
rroot.Get("/robots.txt", s.getRobotsCtrl)
|
||||
rroot.Get("/index.html", s.pubRest.getStartedCtrl)
|
||||
rroot.Get("/robots.txt", s.pubRest.getRobotsCtrl)
|
||||
})
|
||||
|
||||
// file server for static content from /web
|
||||
@@ -307,41 +330,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
|
||||
@@ -448,3 +436,54 @@ func rejectAnonUser(next http.Handler) http.Handler {
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
func (s *Rest) config(siteID string) config {
|
||||
|
||||
cnf := config{
|
||||
Version: s.Version,
|
||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||
MaxCommentSize: s.DataService.MaxCommentSize,
|
||||
Admins: s.DataService.AdminStore.Admins(siteID),
|
||||
AdminEmail: s.DataService.AdminStore.Email(siteID),
|
||||
LowScore: s.ScoreThresholds.Low,
|
||||
CriticalScore: s.ScoreThresholds.Critical,
|
||||
PositiveScore: s.DataService.PositiveScore,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
MaxImageSize: s.ImageService.Store.SizeLimit(),
|
||||
}
|
||||
|
||||
cnf.Auth = []string{}
|
||||
for _, ap := range s.Authenticator.Providers() {
|
||||
cnf.Auth = append(cnf.Auth, ap.Name())
|
||||
}
|
||||
|
||||
if cnf.Admins == nil { // prevent json serialization to nil
|
||||
cnf.Admins = []string{}
|
||||
}
|
||||
return cnf
|
||||
}
|
||||
|
||||
func parseError(err error, defaultCode int) (code int) {
|
||||
code = defaultCode
|
||||
|
||||
switch {
|
||||
// voting errors
|
||||
case strings.Contains(err.Error(), "can not vote for his own comment"):
|
||||
code = rest.ErrVoteSelf
|
||||
case strings.Contains(err.Error(), "already voted for"):
|
||||
code = rest.ErrVoteDbl
|
||||
case strings.Contains(err.Error(), "maximum number of votes exceeded for comment"):
|
||||
code = rest.ErrVoteMax
|
||||
case strings.Contains(err.Error(), "minimal score reached for comment"):
|
||||
code = rest.ErrVoteMinScore
|
||||
|
||||
// edit errors
|
||||
case strings.HasPrefix(err.Error(), "too late to edit"):
|
||||
code = rest.ErrCommentEditExpired
|
||||
case strings.HasPrefix(err.Error(), "parent comment with reply can't be edited"):
|
||||
code = rest.ErrCommentEditChanged
|
||||
|
||||
}
|
||||
|
||||
return code
|
||||
}
|
||||
|
||||
@@ -12,19 +12,46 @@ import (
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-pkgz/auth"
|
||||
"github.com/go-pkgz/auth/token"
|
||||
log "github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/go-pkgz/rest/cache"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
|
||||
"github.com/umputun/remark/backend/app/notify"
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
type private struct {
|
||||
dataService privStore
|
||||
cache cache.LoadingCache
|
||||
readOnlyAge int
|
||||
commentFormatter *store.CommentFormatter
|
||||
imageService *image.Service
|
||||
notifyService *notify.Service
|
||||
authenticator *auth.Service
|
||||
remarkURL string
|
||||
}
|
||||
|
||||
type privStore interface {
|
||||
Create(comment store.Comment) (commentID string, err error)
|
||||
EditComment(locator store.Locator, commentID string, req service.EditRequest) (comment store.Comment, err error)
|
||||
Vote(locator store.Locator, commentID string, userID string, val bool) (comment store.Comment, err error)
|
||||
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
ValidateComment(c *store.Comment) error
|
||||
IsVerified(siteID string, userID string) bool
|
||||
IsReadOnly(locator store.Locator) bool
|
||||
IsBlocked(siteID string, userID string) bool
|
||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||
}
|
||||
|
||||
// POST /comment - adds comment, resets all immutable fields
|
||||
func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *private) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
comment := store.Comment{}
|
||||
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil {
|
||||
@@ -39,14 +66,14 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
comment.User.IP = strings.Split(r.RemoteAddr, ":")[0]
|
||||
|
||||
comment.Orig = comment.Text // original comment text, prior to md render
|
||||
if err := s.DataService.ValidateComment(&comment); err != nil {
|
||||
if err := s.dataService.ValidateComment(&comment); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
|
||||
return
|
||||
}
|
||||
comment = s.CommentFormatter.Format(comment)
|
||||
comment = s.commentFormatter.Format(comment)
|
||||
|
||||
// check if user blocked
|
||||
if s.adminService.checkBlocked(comment.Locator.SiteID, comment.User) {
|
||||
if s.dataService.IsBlocked(comment.Locator.SiteID, comment.User.ID) {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked", rest.ErrUserBlocked)
|
||||
return
|
||||
}
|
||||
@@ -56,7 +83,7 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := s.DataService.Create(comment)
|
||||
id, err := s.dataService.Create(comment)
|
||||
if err == service.ErrRestrictedWordsFound {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
|
||||
return
|
||||
@@ -66,17 +93,17 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// DataService modifies comment
|
||||
finalComment, err := s.DataService.Get(comment.Locator, id)
|
||||
// dataService modifies comment
|
||||
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
|
||||
}
|
||||
s.Cache.Flush(cache.Flusher(comment.Locator.SiteID).
|
||||
s.cache.Flush(cache.Flusher(comment.Locator.SiteID).
|
||||
Scopes(comment.Locator.URL, lastCommentsScope, comment.User.ID, comment.Locator.SiteID))
|
||||
|
||||
if s.NotifyService != nil {
|
||||
s.NotifyService.Submit(finalComment)
|
||||
if s.notifyService != nil {
|
||||
s.notifyService.Submit(finalComment)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] created commend %+v", finalComment)
|
||||
@@ -86,7 +113,7 @@ func (s *Rest) createCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// PUT /comment/{id}?site=siteID&url=post-url - update comment
|
||||
func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *private) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
edit := struct {
|
||||
Text string
|
||||
@@ -107,7 +134,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
|
||||
}
|
||||
@@ -119,40 +146,40 @@ func (s *Rest) updateCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
editReq := service.EditRequest{
|
||||
Text: s.CommentFormatter.FormatText(edit.Text),
|
||||
Text: s.commentFormatter.FormatText(edit.Text),
|
||||
Orig: edit.Text,
|
||||
Summary: edit.Summary,
|
||||
Delete: edit.Delete,
|
||||
}
|
||||
|
||||
res, err := s.DataService.EditComment(locator, id, editReq)
|
||||
res, err := s.dataService.EditComment(locator, id, editReq)
|
||||
if err == service.ErrRestrictedWordsFound {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
code := s.parseError(err, rest.ErrCommentRejected)
|
||||
code := parseError(err, rest.ErrCommentRejected)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't update comment", code)
|
||||
return
|
||||
}
|
||||
|
||||
s.Cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.SiteID, locator.URL, lastCommentsScope, user.ID))
|
||||
s.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.SiteID, locator.URL, lastCommentsScope, user.ID))
|
||||
render.JSON(w, r, res)
|
||||
}
|
||||
|
||||
// GET /user?site=siteID - returns user info
|
||||
func (s *Rest) userInfoCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *private) userInfoCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
user := rest.MustGetUserInfo(r)
|
||||
if siteID := r.URL.Query().Get("site"); siteID != "" {
|
||||
user.Verified = s.DataService.IsVerified(siteID, user.ID)
|
||||
user.Verified = s.dataService.IsVerified(siteID, user.ID)
|
||||
}
|
||||
|
||||
render.JSON(w, r, user)
|
||||
}
|
||||
|
||||
// PUT /vote/{id}?site=siteID&url=post-url&vote=1 - vote for/against comment
|
||||
func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *private) voteCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
user := rest.MustGetUserInfo(r)
|
||||
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
|
||||
id := chi.URLParam(r, "id")
|
||||
@@ -166,23 +193,23 @@ func (s *Rest) voteCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// check if user blocked
|
||||
if s.adminService.checkBlocked(locator.SiteID, user) {
|
||||
if s.dataService.IsBlocked(locator.SiteID, user.ID) {
|
||||
rest.SendErrorJSON(w, r, http.StatusForbidden, errors.New("rejected"), "user blocked", rest.ErrUserBlocked)
|
||||
return
|
||||
}
|
||||
|
||||
comment, err := s.DataService.Vote(locator, id, user.ID, vote)
|
||||
comment, err := s.dataService.Vote(locator, id, user.ID, vote)
|
||||
if err != nil {
|
||||
code := s.parseError(err, rest.ErrVoteRejected)
|
||||
code := parseError(err, rest.ErrVoteRejected)
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't vote for comment", code)
|
||||
return
|
||||
}
|
||||
s.Cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, comment.User.ID))
|
||||
s.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, comment.User.ID))
|
||||
render.JSON(w, r, R.JSON{"id": comment.ID, "score": comment.Score})
|
||||
}
|
||||
|
||||
// GET /userdata?site=siteID - exports all data about the user as a json with user info and list of all comments
|
||||
func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *private) userAllDataCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
siteID := r.URL.Query().Get("site")
|
||||
user := rest.MustGetUserInfo(r)
|
||||
userB, err := json.Marshal(&user)
|
||||
@@ -213,7 +240,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
|
||||
@@ -240,7 +267,7 @@ func (s *Rest) userAllDataCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// POST /deleteme?site_id=site - requesting delete of all user info
|
||||
// makes jwt with user info and sends it back as a part of json response
|
||||
func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *private) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
user := rest.MustGetUserInfo(r)
|
||||
siteID := r.URL.Query().Get("site")
|
||||
|
||||
@@ -260,18 +287,18 @@ func (s *Rest) deleteMeCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
},
|
||||
}
|
||||
|
||||
tokenStr, err := s.Authenticator.TokenService().Token(claims)
|
||||
tokenStr, err := s.authenticator.TokenService().Token(claims)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't make token", rest.ErrInternal)
|
||||
return
|
||||
}
|
||||
|
||||
link := fmt.Sprintf("%s/web/deleteme.html?token=%s", s.RemarkURL, tokenStr)
|
||||
link := fmt.Sprintf("%s/web/deleteme.html?token=%s", s.remarkURL, tokenStr)
|
||||
render.JSON(w, r, R.JSON{"site": siteID, "user_id": user.ID, "token": tokenStr, "link": link})
|
||||
}
|
||||
|
||||
// POST /image - save image with form request
|
||||
func (s *Rest) savePictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *private) savePictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
user := rest.MustGetUserInfo(r)
|
||||
|
||||
if err := r.ParseMultipartForm(5 * 1024 * 1024); err != nil { // 5M max memory, if bigger will make a file
|
||||
@@ -286,7 +313,7 @@ func (s *Rest) savePictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
id, err := s.ImageService.Save(header.Filename, user.ID, file)
|
||||
id, err := s.imageService.Save(header.Filename, user.ID, file)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't save image", rest.ErrInternal)
|
||||
return
|
||||
@@ -295,37 +322,12 @@ func (s *Rest) savePictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, R.JSON{"id": id})
|
||||
}
|
||||
|
||||
func (s *Rest) isReadOnly(locator store.Locator) bool {
|
||||
if s.ReadOnlyAge > 0 {
|
||||
func (s *private) isReadOnly(locator store.Locator) bool {
|
||||
if s.readOnlyAge > 0 {
|
||||
// check RO by age
|
||||
if info, e := s.DataService.Info(locator, s.ReadOnlyAge); e == nil && info.ReadOnly {
|
||||
if info, e := s.dataService.Info(locator, s.readOnlyAge); e == nil && info.ReadOnly {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return s.DataService.IsReadOnly(locator) // ro manually
|
||||
}
|
||||
|
||||
func (s *Rest) parseError(err error, defaultCode int) (code int) {
|
||||
code = defaultCode
|
||||
|
||||
switch {
|
||||
// voting errors
|
||||
case strings.Contains(err.Error(), "can not vote for his own comment"):
|
||||
code = rest.ErrVoteSelf
|
||||
case strings.Contains(err.Error(), "already voted for"):
|
||||
code = rest.ErrVoteDbl
|
||||
case strings.Contains(err.Error(), "maximum number of votes exceeded for comment"):
|
||||
code = rest.ErrVoteMax
|
||||
case strings.Contains(err.Error(), "minimal score reached for comment"):
|
||||
code = rest.ErrVoteMinScore
|
||||
|
||||
// edit errors
|
||||
case strings.HasPrefix(err.Error(), "too late to edit"):
|
||||
code = rest.ErrCommentEditExpired
|
||||
case strings.HasPrefix(err.Error(), "parent comment with reply can't be edited"):
|
||||
code = rest.ErrCommentEditChanged
|
||||
|
||||
}
|
||||
|
||||
return code
|
||||
return s.dataService.IsReadOnly(locator) // ro manually
|
||||
}
|
||||
|
||||
@@ -11,17 +11,14 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/go-pkgz/lgr"
|
||||
R "github.com/go-pkgz/rest"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
@@ -63,7 +60,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))
|
||||
|
||||
@@ -620,16 +617,21 @@ func TestRest_CreateWithPictures(t *testing.T) {
|
||||
}()
|
||||
lgr.Setup(lgr.Debug, lgr.CallerFile, lgr.CallerFunc)
|
||||
|
||||
svc.ImageService = &image.Service{
|
||||
Store: &image.FileSystem{
|
||||
Staging: "/tmp/remark42/images.staging",
|
||||
Location: "/tmp/remark42/images",
|
||||
MaxSize: 2000,
|
||||
},
|
||||
TTL: time.Millisecond * 100,
|
||||
imageService := svc.ImageService
|
||||
imageService.Store = &image.FileSystem{
|
||||
Staging: "/tmp/remark42/images.staging",
|
||||
Location: "/tmp/remark42/images",
|
||||
MaxSize: 2000,
|
||||
}
|
||||
svc.DataService.EditDuration = time.Millisecond * 100
|
||||
svc.DataService.ImageService = svc.ImageService
|
||||
imageService.TTL = 100 * time.Millisecond
|
||||
|
||||
svc.privRest.imageService = imageService
|
||||
svc.ImageService = imageService
|
||||
|
||||
dataService := svc.DataService
|
||||
dataService.EditDuration = time.Millisecond * 100
|
||||
dataService.ImageService = svc.ImageService
|
||||
svc.privRest.dataService = dataService
|
||||
|
||||
uploadPicture := func(file string) (id string) {
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
@@ -683,26 +685,3 @@ func TestRest_CreateWithPictures(t *testing.T) {
|
||||
_, err = os.Stat("/tmp/remark42/images/" + id3)
|
||||
assert.NoError(t, err, "moved from staging")
|
||||
}
|
||||
|
||||
func TestRest_parseError(t *testing.T) {
|
||||
tbl := []struct {
|
||||
err error
|
||||
res int
|
||||
}{
|
||||
{errors.New("can not vote for his own comment"), rest.ErrVoteSelf},
|
||||
{errors.New("already voted for"), rest.ErrVoteDbl},
|
||||
{errors.New("maximum number of votes exceeded for comment"), rest.ErrVoteMax},
|
||||
{errors.New("minimal score reached for comment"), rest.ErrVoteMinScore},
|
||||
{errors.New("too late to edit"), rest.ErrCommentEditExpired},
|
||||
{errors.New("parent comment with reply can't be edited"), rest.ErrCommentEditChanged},
|
||||
{errors.New("blah blah"), rest.ErrInternal},
|
||||
}
|
||||
|
||||
svc := Rest{}
|
||||
for n, tt := range tbl {
|
||||
t.Run(strconv.Itoa(n), func(t *testing.T) {
|
||||
res := svc.parseError(tt.err, rest.ErrInternal)
|
||||
assert.Equal(t, tt.res, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,12 +19,53 @@ import (
|
||||
|
||||
"github.com/umputun/remark/backend/app/rest"
|
||||
"github.com/umputun/remark/backend/app/store"
|
||||
"github.com/umputun/remark/backend/app/store/image"
|
||||
"github.com/umputun/remark/backend/app/store/service"
|
||||
)
|
||||
|
||||
type public struct {
|
||||
dataService pubStore
|
||||
cache cache.LoadingCache
|
||||
readOnlyAge int
|
||||
commentFormatter *store.CommentFormatter
|
||||
imageService *image.Service
|
||||
webRoot string
|
||||
confFn func(siteID string) config
|
||||
}
|
||||
|
||||
type pubStore interface {
|
||||
Create(comment store.Comment) (commentID string, err error)
|
||||
Get(locator store.Locator, commentID string, user store.User) (store.Comment, error)
|
||||
Find(locator store.Locator, sort string, user store.User) ([]store.Comment, error)
|
||||
Last(siteID string, limit int, since time.Time, user store.User) ([]store.Comment, error)
|
||||
User(siteID, userID string, limit, skip int, user store.User) ([]store.Comment, error)
|
||||
UserCount(siteID, userID string) (int, error)
|
||||
Count(locator store.Locator) (int, error)
|
||||
List(siteID string, limit int, skip int) ([]store.PostInfo, error)
|
||||
Info(locator store.Locator, readonlyAge int) (store.PostInfo, error)
|
||||
|
||||
ValidateComment(c *store.Comment) error
|
||||
IsReadOnly(locator store.Locator) bool
|
||||
Counts(siteID string, postIDs []string) ([]store.PostInfo, error)
|
||||
}
|
||||
|
||||
type config struct {
|
||||
Version string `json:"version"`
|
||||
EditDuration int `json:"edit_duration"`
|
||||
MaxCommentSize int `json:"max_comment_size"`
|
||||
Admins []string `json:"admins"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
Auth []string `json:"auth_providers"`
|
||||
LowScore int `json:"low_score"`
|
||||
CriticalScore int `json:"critical_score"`
|
||||
PositiveScore bool `json:"positive_score"`
|
||||
ReadOnlyAge int `json:"readonly_age"`
|
||||
MaxImageSize int `json:"max_image_size"`
|
||||
}
|
||||
|
||||
// GET /find?site=siteID&url=post-url&format=[tree|plain]&sort=[+/-time|+/-score|+/-controversy ]
|
||||
// find comments for given post. Returns in tree or plain formats, sorted
|
||||
func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
|
||||
sort := r.URL.Query().Get("sort")
|
||||
if strings.HasPrefix(sort, " ") { // restore + replaced by " "
|
||||
@@ -33,26 +74,25 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[DEBUG] get comments for %+v, sort %s, format %s", locator, sort, r.URL.Query().Get("format"))
|
||||
|
||||
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)
|
||||
data, err := s.cache.Get(key, func() ([]byte, error) {
|
||||
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{}
|
||||
}
|
||||
if s.DataService.IsReadOnly(locator) {
|
||||
if s.dataService.IsReadOnly(locator) {
|
||||
tree.Info.ReadOnly = true
|
||||
}
|
||||
b, e = encodeJSONWithHTML(tree)
|
||||
default:
|
||||
withInfo := commentsWithInfo{Comments: maskedComments}
|
||||
if info, ee := s.DataService.Info(locator, s.ReadOnlyAge); ee == nil {
|
||||
withInfo := commentsWithInfo{Comments: comments}
|
||||
if info, ee := s.dataService.Info(locator, s.readOnlyAge); ee == nil {
|
||||
withInfo.Info = info
|
||||
}
|
||||
b, e = encodeJSONWithHTML(withInfo)
|
||||
@@ -71,7 +111,7 @@ func (s *Rest) findCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// POST /preview, body is a comment, returns rendered html
|
||||
func (s *Rest) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
comment := store.Comment{}
|
||||
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &comment); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't bind comment", rest.ErrDecode)
|
||||
@@ -85,23 +125,23 @@ func (s *Rest) previewCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
comment.User = user
|
||||
comment.Orig = comment.Text
|
||||
if err = s.DataService.ValidateComment(&comment); err != nil {
|
||||
if err = s.dataService.ValidateComment(&comment); err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "invalid comment", rest.ErrCommentValidation)
|
||||
return
|
||||
}
|
||||
|
||||
comment = s.CommentFormatter.Format(comment)
|
||||
comment = s.commentFormatter.Format(comment)
|
||||
comment.Sanitize()
|
||||
render.HTML(w, r, comment.Text)
|
||||
}
|
||||
|
||||
// GET /info?site=siteID&url=post-url - get info about the post
|
||||
func (s *Rest) infoCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) infoCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
|
||||
|
||||
key := cache.NewKey(locator.SiteID).ID(URLKey(r)).Scopes(locator.SiteID, locator.URL)
|
||||
data, err := s.Cache.Get(key, func() ([]byte, error) {
|
||||
info, e := s.DataService.Info(locator, s.ReadOnlyAge)
|
||||
data, err := s.cache.Get(key, func() ([]byte, error) {
|
||||
info, e := s.dataService.Info(locator, s.readOnlyAge)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
@@ -120,7 +160,7 @@ func (s *Rest) infoCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// GET /last/{limit}?site=siteID&since=unix_ts_msec - last comments for the siteID, across all posts, sorted by time, optionally
|
||||
// limited with "since" param
|
||||
func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
siteID := r.URL.Query().Get("site")
|
||||
log.Printf("[DEBUG] get last comments for %s", siteID)
|
||||
|
||||
@@ -132,21 +172,20 @@ 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
|
||||
}
|
||||
|
||||
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)
|
||||
data, err := s.cache.Get(key, func() ([]byte, error) {
|
||||
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)
|
||||
@@ -163,7 +202,7 @@ func (s *Rest) lastCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// GET /id/{id}?site=siteID&url=post-url - gets a comment by id
|
||||
func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id := chi.URLParam(r, "id")
|
||||
siteID := r.URL.Query().Get("site")
|
||||
@@ -171,12 +210,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 {
|
||||
@@ -185,7 +223,7 @@ func (s *Rest) commentByIDCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// GET /comments?site=siteID&user=id - returns comments for given userID
|
||||
func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
userID := r.URL.Query().Get("user")
|
||||
siteID := r.URL.Query().Get("site")
|
||||
@@ -203,14 +241,13 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[DEBUG] get comments for userID %s, %s", userID, siteID)
|
||||
|
||||
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)
|
||||
data, err := s.cache.Get(key, func() ([]byte, error) {
|
||||
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)
|
||||
count, e := s.dataService.UserCount(siteID, userID)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
@@ -229,52 +266,17 @@ func (s *Rest) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// GET /config?site=siteID - returns configuration
|
||||
func (s *Rest) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) configCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
siteID := r.URL.Query().Get("site")
|
||||
|
||||
type config struct {
|
||||
Version string `json:"version"`
|
||||
EditDuration int `json:"edit_duration"`
|
||||
MaxCommentSize int `json:"max_comment_size"`
|
||||
Admins []string `json:"admins"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
Auth []string `json:"auth_providers"`
|
||||
LowScore int `json:"low_score"`
|
||||
CriticalScore int `json:"critical_score"`
|
||||
PositiveScore bool `json:"positive_score"`
|
||||
ReadOnlyAge int `json:"readonly_age"`
|
||||
MaxImageSize int `json:"max_image_size"`
|
||||
}
|
||||
|
||||
cnf := config{
|
||||
Version: s.Version,
|
||||
EditDuration: int(s.DataService.EditDuration.Seconds()),
|
||||
MaxCommentSize: s.DataService.MaxCommentSize,
|
||||
Admins: s.DataService.AdminStore.Admins(siteID),
|
||||
AdminEmail: s.DataService.AdminStore.Email(siteID),
|
||||
LowScore: s.ScoreThresholds.Low,
|
||||
CriticalScore: s.ScoreThresholds.Critical,
|
||||
PositiveScore: s.DataService.PositiveScore,
|
||||
ReadOnlyAge: s.ReadOnlyAge,
|
||||
MaxImageSize: s.ImageService.Store.SizeLimit(),
|
||||
}
|
||||
|
||||
cnf.Auth = []string{}
|
||||
for _, ap := range s.Authenticator.Providers() {
|
||||
cnf.Auth = append(cnf.Auth, ap.Name())
|
||||
}
|
||||
|
||||
if cnf.Admins == nil { // prevent json serialization to nil
|
||||
cnf.Admins = []string{}
|
||||
}
|
||||
cnf := s.confFn(siteID)
|
||||
render.Status(r, http.StatusOK)
|
||||
render.JSON(w, r, cnf)
|
||||
}
|
||||
|
||||
// GET /count?site=siteID&url=post-url - get number of comments for given post
|
||||
func (s *Rest) countCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) countCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")}
|
||||
count, err := s.DataService.Count(locator)
|
||||
count, err := s.dataService.Count(locator)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get count", rest.ErrPostNotFound)
|
||||
return
|
||||
@@ -283,7 +285,7 @@ func (s *Rest) countCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// POST /counts?site=siteID - get number of comments for posts from post body
|
||||
func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
siteID := r.URL.Query().Get("site")
|
||||
posts := []string{}
|
||||
if err := render.DecodeJSON(http.MaxBytesReader(w, r.Body, hardBodyLimit), &posts); err != nil {
|
||||
@@ -297,8 +299,8 @@ func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
sha := base64.URLEncoding.EncodeToString(h[:])
|
||||
|
||||
key := cache.NewKey(siteID).ID(sha).Scopes(siteID)
|
||||
data, err := s.Cache.Get(key, func() ([]byte, error) {
|
||||
counts, e := s.DataService.Counts(siteID, posts)
|
||||
data, err := s.cache.Get(key, func() ([]byte, error) {
|
||||
counts, e := s.dataService.Counts(siteID, posts)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
@@ -316,7 +318,7 @@ func (s *Rest) countMultiCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// GET /list?site=siteID&limit=50&skip=10 - list posts with comments
|
||||
func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) listCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
siteID := r.URL.Query().Get("site")
|
||||
limit, skip := 0, 0
|
||||
@@ -329,8 +331,8 @@ func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
key := cache.NewKey(siteID).ID(URLKey(r)).Scopes(siteID)
|
||||
data, err := s.Cache.Get(key, func() ([]byte, error) {
|
||||
posts, e := s.DataService.List(siteID, limit, skip)
|
||||
data, err := s.cache.Get(key, func() ([]byte, error) {
|
||||
posts, e := s.dataService.List(siteID, limit, skip)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
@@ -348,7 +350,7 @@ func (s *Rest) listCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// GET /picture/{user}/{id} - get picture
|
||||
func (s *Rest) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
imgContentType := func(img string) string {
|
||||
img = strings.ToLower(img)
|
||||
@@ -364,7 +366,7 @@ func (s *Rest) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
id := chi.URLParam(r, "user") + "/" + chi.URLParam(r, "id")
|
||||
imgRdr, size, err := s.ImageService.Load(id)
|
||||
imgRdr, size, err := s.imageService.Load(id)
|
||||
if err != nil {
|
||||
rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't get image "+id, rest.ErrAssetNotFound)
|
||||
return
|
||||
@@ -395,8 +397,8 @@ func (s *Rest) loadPictureCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// GET /index.html - respond to /index.html with the content of getstarted.html under /web root
|
||||
func (s *Rest) getStartedCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := ioutil.ReadFile(path.Join(s.WebRoot, "getstarted.html"))
|
||||
func (s *public) getStartedCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
data, err := ioutil.ReadFile(path.Join(s.webRoot, "getstarted.html"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
@@ -405,7 +407,7 @@ func (s *Rest) getStartedCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// GET /robots.txt
|
||||
func (s *Rest) getRobotsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *public) getRobotsCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
allowed := []string{"/find", "/last", "/id", "/count", "/counts", "/list", "/config",
|
||||
"/img", "/avatar", "/picture"}
|
||||
for i := range allowed {
|
||||
|
||||
@@ -453,7 +453,7 @@ func TestRest_Info(t *testing.T) {
|
||||
ts, srv, teardown := startupT(t)
|
||||
defer teardown()
|
||||
|
||||
srv.ReadOnlyAge = 10000000 // make sure we don't hit read-only
|
||||
srv.pubRest.readOnlyAge = 10000000 // make sure we don't hit read-only
|
||||
|
||||
user := store.User{ID: "user1", Name: "user name 1"}
|
||||
c1 := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
@@ -249,6 +250,29 @@ func Test_URLKeyWithUser(t *testing.T) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestRest_parseError(t *testing.T) {
|
||||
tbl := []struct {
|
||||
err error
|
||||
res int
|
||||
}{
|
||||
{errors.New("can not vote for his own comment"), rest.ErrVoteSelf},
|
||||
{errors.New("already voted for"), rest.ErrVoteDbl},
|
||||
{errors.New("maximum number of votes exceeded for comment"), rest.ErrVoteMax},
|
||||
{errors.New("minimal score reached for comment"), rest.ErrVoteMinScore},
|
||||
{errors.New("too late to edit"), rest.ErrCommentEditExpired},
|
||||
{errors.New("parent comment with reply can't be edited"), rest.ErrCommentEditChanged},
|
||||
{errors.New("blah blah"), rest.ErrInternal},
|
||||
}
|
||||
|
||||
for n, tt := range tbl {
|
||||
t.Run(strconv.Itoa(n), func(t *testing.T) {
|
||||
res := parseError(tt.err, rest.ErrInternal)
|
||||
assert.Equal(t, tt.res, res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) {
|
||||
|
||||
testDb := fmt.Sprintf("/tmp/test-remark-%d.db", rand.Int31())
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -10,7 +10,8 @@ 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"
|
||||
// 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 combines all store interfaces
|
||||
type Interface interface {
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
package engine
|
||||
|
||||
import mock "github.com/stretchr/testify/mock"
|
||||
import store "github.com/umputun/remark/backend/app/store"
|
||||
import time "time"
|
||||
|
||||
// MockInterface is an autogenerated mock type for the Interface type
|
||||
type MockInterface struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Blocked provides a mock function with given fields: siteID
|
||||
func (_m *MockInterface) Blocked(siteID string) ([]store.BlockedUser, error) {
|
||||
ret := _m.Called(siteID)
|
||||
|
||||
var r0 []store.BlockedUser
|
||||
if rf, ok := ret.Get(0).(func(string) []store.BlockedUser); ok {
|
||||
r0 = rf(siteID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]store.BlockedUser)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(siteID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *MockInterface) Close() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Count provides a mock function with given fields: locator
|
||||
func (_m *MockInterface) Count(locator store.Locator) (int, error) {
|
||||
ret := _m.Called(locator)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(store.Locator) int); ok {
|
||||
r0 = rf(locator)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(store.Locator) error); ok {
|
||||
r1 = rf(locator)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Create provides a mock function with given fields: comment
|
||||
func (_m *MockInterface) Create(comment store.Comment) (string, error) {
|
||||
ret := _m.Called(comment)
|
||||
|
||||
var r0 string
|
||||
if rf, ok := ret.Get(0).(func(store.Comment) string); ok {
|
||||
r0 = rf(comment)
|
||||
} else {
|
||||
r0 = ret.Get(0).(string)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(store.Comment) error); ok {
|
||||
r1 = rf(comment)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Delete provides a mock function with given fields: locator, commentID, mode
|
||||
func (_m *MockInterface) Delete(locator store.Locator, commentID string, mode store.DeleteMode) error {
|
||||
ret := _m.Called(locator, commentID, mode)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(store.Locator, string, store.DeleteMode) error); ok {
|
||||
r0 = rf(locator, commentID, mode)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteAll provides a mock function with given fields: siteID
|
||||
func (_m *MockInterface) DeleteAll(siteID string) error {
|
||||
ret := _m.Called(siteID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(siteID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteUser provides a mock function with given fields: siteID, userID
|
||||
func (_m *MockInterface) DeleteUser(siteID string, userID string) error {
|
||||
ret := _m.Called(siteID, userID)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(siteID, userID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Find provides a mock function with given fields: locator, sort
|
||||
func (_m *MockInterface) Find(locator store.Locator, sort string) ([]store.Comment, error) {
|
||||
ret := _m.Called(locator, sort)
|
||||
|
||||
var r0 []store.Comment
|
||||
if rf, ok := ret.Get(0).(func(store.Locator, string) []store.Comment); ok {
|
||||
r0 = rf(locator, sort)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]store.Comment)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(store.Locator, string) error); ok {
|
||||
r1 = rf(locator, sort)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Get provides a mock function with given fields: locator, commentID
|
||||
func (_m *MockInterface) Get(locator store.Locator, commentID string) (store.Comment, error) {
|
||||
ret := _m.Called(locator, commentID)
|
||||
|
||||
var r0 store.Comment
|
||||
if rf, ok := ret.Get(0).(func(store.Locator, string) store.Comment); ok {
|
||||
r0 = rf(locator, commentID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(store.Comment)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(store.Locator, string) error); ok {
|
||||
r1 = rf(locator, commentID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Info provides a mock function with given fields: locator, readonlyAge
|
||||
func (_m *MockInterface) Info(locator store.Locator, readonlyAge int) (store.PostInfo, error) {
|
||||
ret := _m.Called(locator, readonlyAge)
|
||||
|
||||
var r0 store.PostInfo
|
||||
if rf, ok := ret.Get(0).(func(store.Locator, int) store.PostInfo); ok {
|
||||
r0 = rf(locator, readonlyAge)
|
||||
} else {
|
||||
r0 = ret.Get(0).(store.PostInfo)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(store.Locator, int) error); ok {
|
||||
r1 = rf(locator, readonlyAge)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// IsBlocked provides a mock function with given fields: siteID, userID
|
||||
func (_m *MockInterface) IsBlocked(siteID string, userID string) bool {
|
||||
ret := _m.Called(siteID, userID)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
|
||||
r0 = rf(siteID, userID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IsReadOnly provides a mock function with given fields: locator
|
||||
func (_m *MockInterface) IsReadOnly(locator store.Locator) bool {
|
||||
ret := _m.Called(locator)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(store.Locator) bool); ok {
|
||||
r0 = rf(locator)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IsVerified provides a mock function with given fields: siteID, userID
|
||||
func (_m *MockInterface) IsVerified(siteID string, userID string) bool {
|
||||
ret := _m.Called(siteID, userID)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string) bool); ok {
|
||||
r0 = rf(siteID, userID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Last provides a mock function with given fields: siteID, limit, since
|
||||
func (_m *MockInterface) Last(siteID string, limit int, since time.Time) ([]store.Comment, error) {
|
||||
ret := _m.Called(siteID, limit, since)
|
||||
|
||||
var r0 []store.Comment
|
||||
if rf, ok := ret.Get(0).(func(string, int, time.Time) []store.Comment); ok {
|
||||
r0 = rf(siteID, limit, since)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]store.Comment)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, int, time.Time) error); ok {
|
||||
r1 = rf(siteID, limit, since)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// List provides a mock function with given fields: siteID, limit, skip
|
||||
func (_m *MockInterface) List(siteID string, limit int, skip int) ([]store.PostInfo, error) {
|
||||
ret := _m.Called(siteID, limit, skip)
|
||||
|
||||
var r0 []store.PostInfo
|
||||
if rf, ok := ret.Get(0).(func(string, int, int) []store.PostInfo); ok {
|
||||
r0 = rf(siteID, limit, skip)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]store.PostInfo)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, int, int) error); ok {
|
||||
r1 = rf(siteID, limit, skip)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Put provides a mock function with given fields: locator, comment
|
||||
func (_m *MockInterface) Put(locator store.Locator, comment store.Comment) error {
|
||||
ret := _m.Called(locator, comment)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(store.Locator, store.Comment) error); ok {
|
||||
r0 = rf(locator, comment)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetBlock provides a mock function with given fields: siteID, userID, status, ttl
|
||||
func (_m *MockInterface) SetBlock(siteID string, userID string, status bool, ttl time.Duration) error {
|
||||
ret := _m.Called(siteID, userID, status, ttl)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, bool, time.Duration) error); ok {
|
||||
r0 = rf(siteID, userID, status, ttl)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetReadOnly provides a mock function with given fields: locator, status
|
||||
func (_m *MockInterface) SetReadOnly(locator store.Locator, status bool) error {
|
||||
ret := _m.Called(locator, status)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(store.Locator, bool) error); ok {
|
||||
r0 = rf(locator, status)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// SetVerified provides a mock function with given fields: siteID, userID, status
|
||||
func (_m *MockInterface) SetVerified(siteID string, userID string, status bool) error {
|
||||
ret := _m.Called(siteID, userID, status)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string, bool) error); ok {
|
||||
r0 = rf(siteID, userID, status)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// User provides a mock function with given fields: siteID, userID, limit, skip
|
||||
func (_m *MockInterface) User(siteID string, userID string, limit int, skip int) ([]store.Comment, error) {
|
||||
ret := _m.Called(siteID, userID, limit, skip)
|
||||
|
||||
var r0 []store.Comment
|
||||
if rf, ok := ret.Get(0).(func(string, string, int, int) []store.Comment); ok {
|
||||
r0 = rf(siteID, userID, limit, skip)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]store.Comment)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, int, int) error); ok {
|
||||
r1 = rf(siteID, userID, limit, skip)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// UserCount provides a mock function with given fields: siteID, userID
|
||||
func (_m *MockInterface) UserCount(siteID string, userID string) (int, error) {
|
||||
ret := _m.Called(siteID, userID)
|
||||
|
||||
var r0 int
|
||||
if rf, ok := ret.Get(0).(func(string, string) int); ok {
|
||||
r0 = rf(siteID, userID)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string) error); ok {
|
||||
r1 = rf(siteID, userID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Verified provides a mock function with given fields: siteID
|
||||
func (_m *MockInterface) Verified(siteID string) ([]string, error) {
|
||||
ret := _m.Called(siteID)
|
||||
|
||||
var r0 []string
|
||||
if rf, ok := ret.Get(0).(func(string) []string); ok {
|
||||
r0 = rf(siteID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]string)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(siteID)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -18,11 +18,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 +40,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 +66,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 +94,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 +102,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 +145,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 +174,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 +195,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 +204,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 +250,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 +271,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 +290,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 +309,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 +324,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 +370,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 +404,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 +413,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 +428,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 +441,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 +455,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 +464,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 +474,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 +491,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 +534,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 +670,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 +688,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 +723,37 @@ func TestService_submitImages(t *testing.T) {
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestService_alterComment(t *testing.T) {
|
||||
defer teardown(t)
|
||||
|
||||
engineMock := engine.MockInterface{}
|
||||
engineMock.On("IsBlocked", mock.Anything, mock.Anything).Return(false)
|
||||
engineMock.On("IsVerified", mock.Anything, mock.Anything).Return(false)
|
||||
svc := DataStore{Interface: &engineMock}
|
||||
|
||||
r := svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1"}}, store.User{Name: "dev", Admin: false})
|
||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: ""}}, r, "ip cleaned")
|
||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1"}}, store.User{Name: "dev", Admin: true})
|
||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "127.0.0.1"}}, r, "ip not cleaned")
|
||||
|
||||
engineMock = engine.MockInterface{}
|
||||
engineMock.On("IsBlocked", mock.Anything, mock.Anything).Return(false)
|
||||
engineMock.On("IsVerified", mock.Anything, mock.Anything).Return(true)
|
||||
svc = DataStore{Interface: &engineMock}
|
||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", Verified: true}},
|
||||
store.User{Name: "dev", Admin: false})
|
||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", Verified: true}}, r, "verified set")
|
||||
|
||||
engineMock = engine.MockInterface{}
|
||||
engineMock.On("IsBlocked", mock.Anything, mock.Anything).Return(true)
|
||||
engineMock.On("IsVerified", mock.Anything, mock.Anything).Return(false)
|
||||
svc = DataStore{Interface: &engineMock}
|
||||
r = svc.alterComment(store.Comment{ID: "123", User: store.User{IP: "127.0.0.1", Verified: true}},
|
||||
store.User{Name: "dev", Admin: false})
|
||||
assert.Equal(t, store.Comment{ID: "123", User: store.User{IP: "", Verified: true, Blocked: true}, Deleted: true}, r,
|
||||
"blocked")
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
func prepStoreEngine(t *testing.T) engine.Interface {
|
||||
_ = os.Remove(testDb)
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -1 +1,3 @@
|
||||
module github.com/hashicorp/golang-lru
|
||||
|
||||
go 1.12
|
||||
|
||||
Reference in New Issue
Block a user