diff --git a/.gitignore b/.gitignore
index f6821858..ed8c2e56 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,3 +20,4 @@ remark42
/bin/
/backend/var/
compose-private-backend.yml
+/backend/_example/*/vendor
diff --git a/backend/_example/memory_store/Dockerfile b/backend/_example/memory_store/Dockerfile
new file mode 100644
index 00000000..2eda2e85
--- /dev/null
+++ b/backend/_example/memory_store/Dockerfile
@@ -0,0 +1,22 @@
+FROM umputun/baseimage:buildgo-latest as build-backend
+
+#ADD . /build/memory_store
+#WORKDIR /build/memory_store
+
+ADD backend /build/backend
+WORKDIR /build/backend/_example/memory_store
+
+RUN go build -o /build/bin/memory_store -ldflags "-X main.revision=0.0.0 -s -w"
+
+
+FROM umputun/baseimage:app-latest
+
+WORKDIR /srv
+COPY --from=build-backend /build/bin/memory_store /srv/memory_store
+RUN chown -R app:app /srv
+
+EXPOSE 8080
+HEALTHCHECK --interval=30s --timeout=3s CMD curl --fail http://localhost:8080/ping || exit 1
+USER app
+
+CMD ["/srv/memory_store"]
diff --git a/backend/_example/memory_store/README.md b/backend/_example/memory_store/README.md
new file mode 100644
index 00000000..475703a4
--- /dev/null
+++ b/backend/_example/memory_store/README.md
@@ -0,0 +1,13 @@
+# sample store implementation
+
+`memory_store` illustrates how to make a custom storage plugin for remark42.
+
+In order to run remark42 with memory_store copy provided `compose-dev-memstore.yml` to the root directory and run:
+
+1. docker-compose -f compose-dev-memstore.yml build
+1. docker-compose -f compose-dev-memstore.yml up
+
+As usual, demo site will run on http://127.0.0.1:8080/web/
+
+note: in order to work with the latest (current) version of master `go.mod` uses replacement directive for the backend package
+. In real-life usage `replace github.com/umputun/remark/backend => ../../` should not be used.
\ No newline at end of file
diff --git a/backend/_example/memory_store/accessor/admin.go b/backend/_example/memory_store/accessor/admin.go
new file mode 100644
index 00000000..6e375dfd
--- /dev/null
+++ b/backend/_example/memory_store/accessor/admin.go
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2019 Umputun. All rights reserved.
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file.
+ */
+
+package accessor
+
+import (
+ log "github.com/go-pkgz/lgr"
+ "github.com/pkg/errors"
+)
+
+// MemAdmin implements admin.Store with memory backend
+type MemAdmin struct {
+ data map[string]AdminRec // admin info per site
+ key string
+}
+
+// AdminRec is a records per site with all admin info in
+type AdminRec struct {
+ SiteID string `bson:"site"`
+ IDs []string `bson:"ids"` // admin ids
+ Email string `bson:"email"` // admin email
+}
+
+// NewMemAdminStore makes admin Store in memory
+func NewMemAdminStore(key string) *MemAdmin {
+ log.Print("[DEBUG] make memory admin store")
+ return &MemAdmin{data: map[string]AdminRec{}, key: key}
+}
+
+// Key executes find by siteID and returns substructure with secret key
+func (m *MemAdmin) Key() (key string, err error) {
+ return m.key, nil
+}
+
+// Admins executes find by siteID and returns admins ids
+func (m *MemAdmin) Admins(siteID string) (ids []string, err error) {
+ resp, ok := m.data[siteID]
+ if !ok {
+ return nil, errors.Errorf("site %s not found", siteID)
+ }
+ log.Printf("[DEBUG] admins for %s, %+v", siteID, resp.IDs)
+ return resp.IDs, nil
+}
+
+// Email executes find by siteID and returns admin's email
+func (m *MemAdmin) Email(siteID string) (email string, err error) {
+ resp, ok := m.data[siteID]
+ if !ok {
+ return "", errors.Errorf("site %s not found", siteID)
+ }
+
+ return resp.Email, nil
+}
+
+// Set admin data for siteID
+func (m *MemAdmin) Set(siteID string, arec AdminRec) {
+ m.data[siteID] = arec
+}
diff --git a/backend/_example/memory_store/accessor/admin_test.go b/backend/_example/memory_store/accessor/admin_test.go
new file mode 100644
index 00000000..1fe18688
--- /dev/null
+++ b/backend/_example/memory_store/accessor/admin_test.go
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019 Umputun. All rights reserved.
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file.
+ */
+
+package accessor
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+
+ "github.com/umputun/remark/backend/app/store/admin"
+)
+
+func TestMemAdmin_Get(t *testing.T) {
+
+ adm := NewMemAdminStore("secret")
+ var ms admin.Store = adm
+
+ adm.data = map[string]AdminRec{
+ "site1": {"site1", []string{"i11", "i12"}, "e1"},
+ "site2": {"site2", []string{"i21", "i22"}, "e2"},
+ }
+
+ admins, err := ms.Admins("site1")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"i11", "i12"}, admins)
+ email, err := ms.Email("site1")
+ assert.NoError(t, err)
+ assert.Equal(t, "e1", email)
+ key, err := ms.Key()
+ assert.NoError(t, err)
+ assert.Equal(t, "secret", key)
+
+ admins, err = ms.Admins("site2")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"i21", "i22"}, admins)
+ email, err = ms.Email("site2")
+ assert.NoError(t, err)
+ assert.Equal(t, "e2", email)
+ key, err = ms.Key()
+ assert.NoError(t, err)
+ assert.Equal(t, "secret", key)
+
+ admins, err = ms.Admins("no-site-in-db")
+ assert.EqualError(t, err, "site no-site-in-db not found")
+
+ email, err = ms.Email("no-site-in-db")
+ assert.EqualError(t, err, "site no-site-in-db not found")
+}
diff --git a/backend/_example/memory_store/accessor/data.go b/backend/_example/memory_store/accessor/data.go
new file mode 100644
index 00000000..0773f09f
--- /dev/null
+++ b/backend/_example/memory_store/accessor/data.go
@@ -0,0 +1,441 @@
+/*
+ * Copyright 2019 Umputun. All rights reserved.
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file.
+ */
+
+package accessor
+
+import (
+ "log"
+ "sort"
+ "sync"
+ "time"
+
+ "github.com/pkg/errors"
+
+ "github.com/umputun/remark/backend/app/store/engine"
+
+ "github.com/umputun/remark/backend/app/store"
+)
+
+const lastLimit = 1000
+
+// MemData implements in-memory data store
+type MemData struct {
+ posts map[string][]store.Comment // key is siteID
+ metaUsers map[string]metaUser // key is userID
+ metaPosts map[store.Locator]metaPost // key is post's locator
+ sync.RWMutex
+}
+
+type metaPost struct {
+ PostURL string
+ SiteID string
+ ReadOnly bool
+}
+
+type metaUser struct {
+ UserID string
+ SiteID string
+ Verified bool
+ Blocked bool
+ BlockedUntil time.Time
+}
+
+// NewMemData makes in-memory engine.
+func NewMemData() *MemData {
+
+ result := &MemData{
+ posts: map[string][]store.Comment{},
+ metaUsers: map[string]metaUser{},
+ metaPosts: map[store.Locator]metaPost{},
+ }
+ return result
+}
+
+// Create new comment
+func (m *MemData) Create(comment store.Comment) (commentID string, err error) {
+
+ if ro, e := m.Flag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: comment.Locator}); e == nil && ro {
+ return "", errors.Errorf("post %s is read-only", comment.Locator.URL)
+ }
+
+ m.Lock()
+ defer m.Unlock()
+ comments := m.posts[comment.Locator.SiteID]
+ for _, c := range comments { // don't allow duplicated IDs
+ if c.ID == comment.ID {
+ return "", errors.New("dup key")
+ }
+ }
+ comments = append(comments, comment)
+ m.posts[comment.Locator.SiteID] = comments
+ return comment.ID, nil
+}
+
+// Find returns all comments for post and sorts results
+func (m *MemData) Find(req engine.FindRequest) (comments []store.Comment, err error) {
+ m.RLock()
+ defer m.RUnlock()
+
+ comments = []store.Comment{}
+
+ if req.Sort == "" {
+ req.Sort = "time"
+ }
+
+ switch {
+
+ case req.Locator.SiteID != "" && req.Locator.URL != "": // find comments for site and url
+ comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
+ return c.Locator == req.Locator && (req.Since.IsZero() || c.Timestamp.After(req.Since))
+ })
+
+ case req.Locator.SiteID != "" && req.Locator.URL == "" && req.UserID == "": // find last comments for site
+ if req.Limit > lastLimit || req.Limit == 0 {
+ req.Limit = lastLimit
+ }
+ if req.Since.IsZero() {
+ req.Since = time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
+ }
+
+ comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
+ return !c.Deleted && c.Timestamp.After(req.Since)
+ })
+ comments = engine.SortComments(comments, "-time")
+ if len(comments) > req.Limit {
+ comments = comments[:req.Limit]
+ }
+ return comments, nil
+
+ case req.Locator.SiteID != "" && req.UserID != "": // find comments for user
+ comments = m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
+ return c.User.ID == req.UserID
+ })
+ }
+
+ comments = engine.SortComments(comments, req.Sort)
+ if req.Skip > 0 && req.Skip > len(comments) {
+ return []store.Comment{}, nil
+ }
+ if req.Skip > 0 && req.Skip < len(comments) {
+ comments = comments[req.Skip:]
+ }
+
+ if req.Limit > 0 && req.Limit < len(comments) {
+ comments = comments[:req.Limit]
+ }
+
+ return comments, err
+}
+
+// Get returns comment for locator.URL and commentID string
+func (m *MemData) Get(req engine.GetRequest) (comment store.Comment, err error) {
+ m.RLock()
+ defer m.RUnlock()
+ return m.get(req.Locator, req.CommentID)
+}
+
+// Update updates comment for locator.URL with mutable part of comment
+func (m *MemData) Update(comment store.Comment) error {
+ m.Lock()
+ defer m.Unlock()
+ return m.updateComment(comment)
+}
+
+// Count returns number of comments for post or user
+func (m *MemData) Count(req engine.FindRequest) (count int, err error) {
+ m.RLock()
+ defer m.RUnlock()
+
+ switch {
+ case req.Locator.URL != "": // comment's count for post
+ comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
+ return c.Locator == req.Locator && !c.Deleted
+ })
+ return len(comments), nil
+ case req.UserID != "":
+ comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
+ return c.User.ID == req.UserID && !c.Deleted
+ })
+ return len(comments), nil
+ default:
+ return 0, errors.Errorf("invalid count request %+v", req)
+ }
+}
+
+// Info get post(s) meta info
+func (m *MemData) Info(req engine.InfoRequest) (res []store.PostInfo, err error) {
+ m.RLock()
+ defer m.RUnlock()
+ res = []store.PostInfo{}
+
+ if req.Locator.URL != "" { // post info
+ comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
+ return c.Locator == req.Locator
+ })
+ if len(comments) == 0 {
+ return nil, errors.New("not found")
+ }
+ info := store.PostInfo{
+ URL: req.Locator.URL,
+ Count: len(comments),
+ ReadOnly: false,
+ FirstTS: comments[0].Timestamp.UTC(),
+ LastTS: comments[len(comments)-1].Timestamp.UTC(),
+ }
+ // set read-only from age and manual bucket
+ info.ReadOnly = req.ReadOnlyAge > 0 && !info.FirstTS.IsZero() &&
+ info.FirstTS.AddDate(0, 0, req.ReadOnlyAge).Before(time.Now())
+ if !info.ReadOnly {
+ v := m.checkFlag(engine.FlagRequest{Flag: engine.ReadOnly, Locator: req.Locator})
+ info.ReadOnly = v
+ }
+ return []store.PostInfo{info}, nil
+ }
+
+ if req.Locator.URL == "" && req.Locator.SiteID != "" { // site info (list)
+ if req.Limit <= 0 {
+ req.Limit = 1000
+ }
+ if req.Skip < 0 {
+ req.Skip = 0
+ }
+
+ infoAll := map[store.Locator]store.PostInfo{}
+ for _, c := range m.posts[req.Locator.SiteID] {
+ var info store.PostInfo
+ var ok bool
+ if info, ok = infoAll[c.Locator]; !ok {
+ info = store.PostInfo{URL: c.Locator.URL, FirstTS: c.Timestamp.UTC()}
+ }
+ info.Count++
+ info.LastTS = c.Timestamp.UTC()
+ infoAll[c.Locator] = info
+ }
+
+ for _, v := range infoAll {
+ res = append(res, v)
+ }
+ sort.Slice(res, func(i, j int) bool {
+ return res[i].URL > res[j].URL
+ })
+
+ if req.Skip > 0 {
+ if req.Skip >= len(res) {
+ return []store.PostInfo{}, nil
+ }
+ res = res[req.Skip:]
+ }
+
+ if req.Limit > 0 && req.Limit < len(res) {
+ res = res[:req.Limit]
+ }
+ return res, nil
+ }
+
+ return nil, errors.Errorf("invalid info request %+v", req)
+}
+
+// Flag sets and gets flag values
+func (m *MemData) Flag(req engine.FlagRequest) (val bool, err error) {
+ m.Lock()
+ defer m.Unlock()
+
+ if req.Update == engine.FlagNonSet { // read flag value, no update requested
+ return m.checkFlag(req), nil
+ }
+ // write flag value
+ return m.setFlag(req)
+}
+
+// ListFlags get list of flagged keys, like blocked & verified user
+// works for full locator (post flags) or with userID
+func (m *MemData) ListFlags(req engine.FlagRequest) (res []interface{}, err error) {
+ m.RLock()
+ defer m.RUnlock()
+
+ res = []interface{}{}
+
+ switch req.Flag {
+ case engine.Verified:
+ for _, u := range m.metaUsers {
+ if u.SiteID == req.Locator.SiteID {
+ res = append(res, u.UserID)
+ }
+ }
+ return res, nil
+
+ case engine.Blocked:
+ log.Printf("%+v", m.metaUsers)
+ for _, u := range m.metaUsers {
+ if u.SiteID == req.Locator.SiteID && u.Blocked && u.BlockedUntil.After(time.Now()) {
+ res = append(res, store.BlockedUser{ID: u.UserID, Until: u.BlockedUntil})
+ }
+ }
+ return res, nil
+ }
+
+ return nil, errors.Errorf("flag %s not listable", req.Flag)
+}
+
+// Delete post(s) by id or by userID
+func (m *MemData) Delete(req engine.DeleteRequest) error {
+
+ m.Lock()
+ defer m.Unlock()
+
+ switch {
+ case req.Locator.URL != "" && req.CommentID != "": // delete comment
+ return m.deleteComment(req.Locator, req.CommentID, req.DeleteMode)
+
+ case req.Locator.SiteID != "" && req.UserID != "" && req.CommentID == "": // delete user
+ comments := m.match(m.posts[req.Locator.SiteID], func(c store.Comment) bool {
+ return c.User.ID == req.UserID && !c.Deleted
+ })
+ for _, c := range comments {
+ if e := m.deleteComment(c.Locator, c.ID, req.DeleteMode); e != nil {
+ return e
+ }
+ }
+ return nil
+
+ case req.Locator.SiteID != "" && req.Locator.URL == "" && req.CommentID == "" && req.UserID == "": // delete site
+ if _, ok := m.posts[req.Locator.SiteID]; !ok {
+ return errors.New("not found")
+ }
+ m.posts[req.Locator.SiteID] = []store.Comment{}
+ return nil
+ }
+
+ return errors.Errorf("invalid delete request %+v", req)
+}
+
+func (m *MemData) deleteComment(loc store.Locator, id string, mode store.DeleteMode) error {
+
+ comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool {
+ return c.Locator == loc && c.ID == id
+ })
+ if len(comments) == 0 {
+ return errors.New("not found")
+ }
+
+ comments[0].SetDeleted(mode)
+ return m.updateComment(comments[0])
+}
+
+// Close store
+func (m *MemData) Close() error {
+ return nil
+}
+
+func (m *MemData) checkFlag(req engine.FlagRequest) (val bool) {
+ switch req.Flag {
+ case engine.Blocked:
+ if meta, ok := m.metaUsers[req.UserID]; ok {
+ if meta.SiteID != req.Locator.SiteID {
+ return false
+ }
+ return meta.Blocked && meta.BlockedUntil.After(time.Now())
+ }
+ case engine.Verified:
+ if meta, ok := m.metaUsers[req.UserID]; ok {
+ if meta.SiteID != req.Locator.SiteID {
+ return false
+ }
+ return meta.Verified
+ }
+ case engine.ReadOnly:
+ if meta, ok := m.metaPosts[req.Locator]; ok {
+ return meta.ReadOnly
+ }
+ }
+ return false
+}
+
+func (m *MemData) setFlag(req engine.FlagRequest) (res bool, err error) {
+
+ status := false
+ if req.Update == engine.FlagTrue {
+ status = true
+ }
+
+ switch req.Flag {
+
+ case engine.Blocked:
+ until := time.Time{}
+ if status {
+ until = time.Now().AddDate(100, 0, 0) // permanent is 100years
+ if req.TTL > 0 {
+ until = time.Now().Add(req.TTL)
+ }
+ }
+ meta := m.metaUsers[req.UserID]
+ meta = metaUser{
+ UserID: req.UserID,
+ SiteID: req.Locator.SiteID,
+ Blocked: status,
+ BlockedUntil: until,
+ }
+ m.metaUsers[req.UserID] = meta
+
+ case engine.Verified:
+ meta := m.metaUsers[req.UserID]
+ meta = metaUser{
+ UserID: req.UserID,
+ SiteID: req.Locator.SiteID,
+ Verified: status,
+ }
+ m.metaUsers[req.UserID] = meta
+
+ case engine.ReadOnly:
+ info, ok := m.metaPosts[req.Locator]
+ if !ok {
+ info.SiteID = req.Locator.SiteID
+ info.PostURL = req.Locator.URL
+ }
+ info.ReadOnly = status
+ m.metaPosts[req.Locator] = info
+ }
+ return status, errors.Wrapf(err, "failed to set flag %+v", req)
+}
+
+func (m *MemData) get(loc store.Locator, commentID string) (store.Comment, error) {
+ comments := m.match(m.posts[loc.SiteID], func(c store.Comment) bool {
+ return c.Locator == loc && c.ID == commentID
+ })
+ if len(comments) == 0 {
+ return store.Comment{}, errors.New("not found")
+ }
+ return comments[0], nil
+}
+
+func (m *MemData) updateComment(comment store.Comment) error {
+ comments := m.posts[comment.Locator.SiteID]
+ for i, c := range comments {
+ if c.ID == comment.ID && c.Locator == comment.Locator {
+ c.Text = comment.Text
+ c.Orig = comment.Orig
+ c.Score = comment.Score
+ c.Votes = comment.Votes
+ c.Pin = comment.Pin
+ c.Deleted = comment.Deleted
+ c.User = comment.User
+ comments[i] = c
+ m.posts[comment.Locator.SiteID] = comments
+ return nil
+ }
+ }
+ return errors.New("not found")
+}
+
+func (m *MemData) match(comments []store.Comment, fn func(c store.Comment) bool) (res []store.Comment) {
+ res = []store.Comment{}
+ for _, c := range comments {
+ if fn(c) {
+ res = append(res, c)
+ }
+ }
+ return res
+}
diff --git a/backend/_example/memory_store/accessor/data_test.go b/backend/_example/memory_store/accessor/data_test.go
new file mode 100644
index 00000000..e95b15bf
--- /dev/null
+++ b/backend/_example/memory_store/accessor/data_test.go
@@ -0,0 +1,727 @@
+/*
+ * Copyright 2019 Umputun. All rights reserved.
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file.
+ */
+
+package accessor
+
+import (
+ "fmt"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+
+ "github.com/umputun/remark/backend/app/store"
+ "github.com/umputun/remark/backend/app/store/engine"
+)
+
+func TestMem_CreateAndFind(t *testing.T) {
+ m := prepMem(t) // adds two comments
+
+ req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
+ res, err := m.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res))
+ assert.Equal(t, `some text, link`, res[0].Text)
+ assert.Equal(t, "user1", res[0].User.ID)
+ t.Log(res[0].ID)
+
+ _, err = m.Create(store.Comment{ID: res[0].ID, Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}})
+ require.NotNil(t, err)
+ assert.True(t, strings.Contains(err.Error(), "dup key"), err.Error())
+
+ id, err := m.Create(store.Comment{ID: "id-3", Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}})
+ require.NoError(t, err)
+ assert.Equal(t, "id-3", id)
+ req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t2.com", SiteID: "radio-t2"}, Sort: "time"}
+ res, err = m.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 1, len(res))
+}
+
+func TestMem_CreateFailedReadOnly(t *testing.T) {
+ b := prepMem(t)
+ comment := store.Comment{
+ ID: "id-ro",
+ Text: `some text, link`,
+ Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
+ Locator: store.Locator{URL: "https://radio-t.com/ro", SiteID: "radio-t"},
+ User: store.User{ID: "user1", Name: "user name"},
+ }
+
+ flagReq := engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagTrue}
+ v, err := b.Flag(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, true, v)
+
+ _, err = b.Create(comment)
+ assert.NotNil(t, err)
+ assert.Equal(t, "post https://radio-t.com/ro is read-only", err.Error())
+
+ flagReq = engine.FlagRequest{Locator: comment.Locator, Flag: engine.ReadOnly, Update: engine.FlagFalse}
+ v, err = b.Flag(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, false, v)
+
+ _, err = b.Create(comment)
+ assert.NoError(t, err)
+}
+
+func TestMem_Get(t *testing.T) {
+ b := prepMem(t)
+ req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
+ res, err := b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res), "2 records initially")
+
+ comment, err := b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[1].ID))
+ assert.NoError(t, err)
+ assert.Equal(t, "some text2", comment.Text)
+
+ _, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, "1234567"))
+ assert.EqualError(t, err, `not found`)
+
+ _, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "bad"}, res[1].ID))
+ assert.EqualError(t, err, `not found`)
+}
+
+func TestMem_Update(t *testing.T) {
+ b := prepMem(t)
+ req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
+ res, err := b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res), "2 records initially")
+
+ comment := res[0]
+ comment.Text = "abc 123"
+ comment.Score = 100
+ err = b.Update(comment)
+ assert.NoError(t, err)
+
+ comment, err = b.Get(getReq(store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, res[0].ID))
+ assert.NoError(t, err)
+ assert.Equal(t, "abc 123", comment.Text)
+ assert.Equal(t, res[0].ID, comment.ID)
+ assert.Equal(t, 100, comment.Score)
+
+ comment.Locator.SiteID = "bad"
+ err = b.Update(comment)
+ assert.EqualError(t, err, `not found`)
+
+ comment.Locator.SiteID = "https://radio-t.com"
+ comment.Locator.URL = "https://radio-t.com-bad"
+ err = b.Update(comment)
+ assert.EqualError(t, err, `not found`)
+}
+
+func TestMem_FindLast(t *testing.T) {
+ b := prepMem(t)
+ req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time"}
+ res, err := b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res))
+ assert.Equal(t, "some text2", res[0].Text)
+
+ req.Limit = 1
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 1, len(res))
+ assert.Equal(t, "some text2", res[0].Text)
+
+ req.Locator.SiteID = "bad"
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(res))
+}
+
+func TestMem_FindLastSince(t *testing.T) {
+ b := prepMem(t)
+ ts := time.Date(2017, 12, 20, 15, 18, 21, 0, time.Local)
+ req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", Since: ts}
+ res, err := b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res))
+ assert.Equal(t, "some text2", res[0].Text)
+
+ req.Since = time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local)
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 1, len(res))
+ assert.Equal(t, "some text2", res[0].Text)
+
+ req.Since = time.Date(2017, 12, 20, 16, 18, 22, 0, time.Local)
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(res))
+}
+
+func TestMem_FindForUser(t *testing.T) {
+ b := prepMem(t)
+ req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 5}
+ res, err := b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res))
+ assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
+
+ req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1}
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 1, len(res), "allow 1 comment")
+ assert.Equal(t, "some text2", res[0].Text, "sorted by -time")
+
+ req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 1, len(res), "allow 1 comment")
+ assert.Equal(t, `some text, link`, res[0].Text, "second comment")
+
+ req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, Sort: "-time", UserID: "user1", Limit: 1, Skip: 1}
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(res), "no comments")
+
+ req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "userZ", Limit: 1, Skip: 1}
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(res), "no comments")
+}
+
+func TestMem_FindForUserPagination(t *testing.T) {
+ b := NewMemData()
+
+ c := store.Comment{
+ Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
+ User: store.User{ID: "user1", Name: "user name"},
+ }
+
+ // write 200 comments
+ for i := 0; i < 200; i++ {
+ c.ID = fmt.Sprintf("idd-%d", i)
+ c.Text = fmt.Sprintf("text #%d", i)
+ c.Timestamp = time.Date(2017, 12, 20, 15, 18, i, 0, time.Local)
+ _, err := b.Create(c)
+ require.Nil(t, err)
+ }
+
+ // get all comments
+ req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "-time", UserID: "user1"}
+ res, err := b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 200, len(res))
+ assert.Equal(t, "idd-199", res[0].ID)
+
+ // seek 0, 5 comments
+ req.Limit = 5
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 5, len(res))
+ assert.Equal(t, "idd-199", res[0].ID)
+ assert.Equal(t, "idd-195", res[4].ID)
+
+ // seek 10, 3 comments
+ req.Skip, req.Limit = 10, 3
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 3, len(res))
+ assert.Equal(t, "idd-189", res[0].ID)
+ assert.Equal(t, "idd-187", res[2].ID)
+
+ // seek 195, ask 10 comments
+ req.Skip, req.Limit = 195, 10
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ require.Equal(t, 5, len(res))
+ assert.Equal(t, "idd-4", res[0].ID)
+ assert.Equal(t, "idd-0", res[4].ID)
+
+ // seek 255, ask 10 comments
+ req.Skip, req.Limit = 255, 10
+ res, err = b.Find(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(res))
+}
+
+func TestMem_CountPost(t *testing.T) {
+ b := prepMem(t)
+ req := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}}
+ c, err := b.Count(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, c)
+
+ req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com-xxx", SiteID: "radio-t"}}
+ c, err = b.Count(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, c)
+
+ req = engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "bad"}}
+ c, err = b.Count(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, c)
+}
+
+func TestMem_CountUser(t *testing.T) {
+ b := prepMem(t)
+ req := engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
+ c, err := b.Count(req)
+ assert.NoError(t, err)
+ require.Equal(t, 2, c)
+
+ req = engine.FindRequest{Locator: store.Locator{SiteID: "bad"}, UserID: "user1"}
+ c, err = b.Count(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, c)
+
+ req = engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "userZ"}
+ c, err = b.Count(req)
+ assert.NoError(t, err)
+ assert.Equal(t, 0, c)
+}
+
+func TestMem_InfoPost(t *testing.T) {
+ b := prepMem(t)
+ ts := func(min int) time.Time { return time.Date(2017, 12, 20, 15, 18, min, 0, time.Local).In(time.UTC) }
+
+ // add one more for https://radio-t.com/2
+ comment := store.Comment{
+ ID: "12345",
+ Text: `some text, link`,
+ Timestamp: time.Date(2017, 12, 20, 15, 18, 24, 0, time.Local),
+ Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
+ User: store.User{ID: "user1", Name: "user name"},
+ }
+ _, err := b.Create(comment)
+ assert.NoError(t, err)
+
+ req := engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
+ r, err := b.Info(req)
+ require.NoError(t, err)
+ assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24)}}, r)
+
+ req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 10}
+ r, err = b.Info(req)
+ require.NoError(t, err)
+ assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1,
+ FirstTS: ts(24), LastTS: ts(24), ReadOnly: true}}, r)
+
+ req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, ReadOnlyAge: 0}
+ r, err = b.Info(req)
+ require.NoError(t, err)
+ assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, r)
+
+ req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/error", SiteID: "radio-t"}, ReadOnlyAge: 0}
+ _, err = b.Info(req)
+ require.NotNil(t, err)
+
+ req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t-error"}, ReadOnlyAge: 0}
+ _, err = b.Info(req)
+ require.NotNil(t, err)
+
+ fr := engine.FlagRequest{Flag: engine.ReadOnly,
+ Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, Update: engine.FlagTrue}
+ _, err = b.Flag(fr)
+ require.NoError(t, err)
+ req = engine.InfoRequest{Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"}, ReadOnlyAge: 0}
+ r, err = b.Info(req)
+ require.NoError(t, err)
+ assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(24), LastTS: ts(24),
+ ReadOnly: true}}, r)
+}
+
+func TestMem_InfoList(t *testing.T) {
+ b := prepMem(t)
+ // add one more for https://radio-t.com/2
+ comment := store.Comment{
+ ID: "12345",
+ Text: `some text, link`,
+ Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
+ Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
+ User: store.User{ID: "user1", Name: "user name"},
+ }
+ _, err := b.Create(comment)
+ assert.NoError(t, err)
+
+ ts := func(sec int) time.Time { return time.Date(2017, 12, 20, 15, 18, sec, 0, time.Local).In(time.UTC) }
+
+ req := engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}}
+ res, err := b.Info(req)
+ assert.NoError(t, err)
+ assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
+ {URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
+
+ req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: -1, Skip: -1}
+ res, err = b.Info(req)
+ assert.NoError(t, err)
+ assert.EqualValues(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)},
+ {URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
+
+ req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1}
+ res, err = b.Info(req)
+ assert.NoError(t, err)
+ assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com/2", Count: 1, FirstTS: ts(22), LastTS: ts(22)}}, res)
+
+ req = engine.InfoRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 1, Skip: 1}
+ res, err = b.Info(req)
+ assert.NoError(t, err)
+ assert.Equal(t, []store.PostInfo{{URL: "https://radio-t.com", Count: 2, FirstTS: ts(22), LastTS: ts(23)}}, res)
+
+ req = engine.InfoRequest{Locator: store.Locator{SiteID: "bad"}, Limit: 1, Skip: 1}
+ res, err = b.Info(req)
+ assert.NoError(t, err)
+ assert.Equal(t, []store.PostInfo{}, res)
+}
+
+func TestMem_FlagBlockedUser(t *testing.T) {
+
+ b := prepMem(t)
+ req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"}
+ val, err := b.Flag(req)
+ assert.NoError(t, err)
+ assert.False(t, val, "nothing blocked yet")
+
+ req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
+ Update: engine.FlagTrue}
+ _, err = b.Flag(req)
+ assert.NoError(t, err)
+ val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
+ assert.NoError(t, err)
+ assert.True(t, val, "user1 blocked")
+
+ req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
+ Update: engine.FlagTrue}
+ _, err = b.Flag(req)
+ assert.NoError(t, err)
+ val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
+ assert.NoError(t, err)
+ assert.True(t, val, "user1 still blocked")
+
+ req = engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
+ Update: engine.FlagFalse}
+ _, err = b.Flag(req)
+ assert.NoError(t, err)
+ val, err = b.Flag(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1"})
+ assert.NoError(t, err)
+ assert.False(t, val, "user1 unblocked")
+}
+
+func TestMem_FlagReadOnlyPost(t *testing.T) {
+
+ b := prepMem(t)
+ req := engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
+ val, err := b.Flag(req)
+ assert.NoError(t, err)
+ assert.False(t, val, "nothing ro")
+
+ req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly,
+ Update: engine.FlagTrue}
+ val, err = b.Flag(req)
+ assert.NoError(t, err)
+ req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
+ val, err = b.Flag(req)
+ assert.NoError(t, err)
+ assert.True(t, val, "url-1 ro")
+
+ req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-2"}, Flag: engine.ReadOnly}
+ val, err = b.Flag(req)
+ assert.NoError(t, err)
+ assert.False(t, val, "url-2 still writable")
+
+ req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly,
+ Update: engine.FlagFalse}
+ _, err = b.Flag(req)
+ assert.NoError(t, err)
+ req = engine.FlagRequest{Locator: store.Locator{SiteID: "radio-t", URL: "url-1"}, Flag: engine.ReadOnly}
+ val, err = b.Flag(req)
+ assert.NoError(t, err)
+ assert.False(t, val, "url-1 writable")
+}
+
+func TestMem_FlagVerified(t *testing.T) {
+
+ b := prepMem(t)
+ isVerified := func(site, user string) bool {
+ req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user}
+ v, err := b.Flag(req)
+ require.NoError(t, err)
+ return v
+ }
+
+ setVerified := func(site, user string, status engine.FlagStatus) error {
+ req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
+ _, err := b.Flag(req)
+ return err
+ }
+
+ assert.False(t, isVerified("radio-t", "u1"), "nothing verified")
+
+ assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
+ assert.True(t, isVerified("radio-t", "u1"), "u1 verified")
+
+ assert.False(t, isVerified("radio-t", "u2"), "u2 still not verified")
+ assert.NoError(t, setVerified("radio-t", "u1", engine.FlagFalse))
+ assert.False(t, isVerified("radio-t", "u1"), "u1 not verified anymore")
+
+ assert.NoError(t, setVerified("bad", "u1", engine.FlagTrue))
+ assert.NoError(t, setVerified("radio-t", "u1xyz", engine.FlagFalse))
+
+ assert.False(t, isVerified("radio-t-bad", "u1"), "nothing verified on wrong site")
+
+ assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
+ assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue))
+ assert.NoError(t, setVerified("radio-t", "u3", engine.FlagFalse))
+}
+
+func TestMem_FlagListVerified(t *testing.T) {
+
+ b := prepMem(t)
+ toIDs := func(inp []interface{}) (res []string) {
+ res = make([]string, len(inp))
+ for i, v := range inp {
+ vv, ok := v.(string)
+ require.True(t, ok)
+ res[i] = vv
+ }
+ sort.Strings(res)
+ return res
+ }
+
+ setVerified := func(site, user string, status engine.FlagStatus) error {
+ req := engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: site}, UserID: user, Update: status}
+ _, err := b.Flag(req)
+ return err
+ }
+
+ ids, err := b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}})
+ assert.NoError(t, err)
+ assert.Equal(t, []string{}, toIDs(ids), "verified list empty")
+
+ assert.NoError(t, setVerified("radio-t", "u1", engine.FlagTrue))
+ assert.NoError(t, setVerified("radio-t", "u2", engine.FlagTrue))
+ ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t"}})
+ assert.NoError(t, err)
+ assert.EqualValues(t, []string{"u1", "u2"}, toIDs(ids), "verified 2 ids")
+
+ ids, err = b.ListFlags(engine.FlagRequest{Flag: engine.Verified, Locator: store.Locator{SiteID: "radio-t-bad"}})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(ids))
+}
+
+func TestMem_FlagListBlocked(t *testing.T) {
+
+ b := prepMem(t)
+ setBlocked := func(site, user string, status engine.FlagStatus, ttl time.Duration) error {
+ req := engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: site}, UserID: user, Update: status,
+ TTL: ttl}
+ _, err := b.Flag(req)
+ return err
+ }
+
+ toBlocked := func(inp []interface{}) (res []store.BlockedUser) {
+ res = make([]store.BlockedUser, len(inp))
+ for i, v := range inp {
+ vv, ok := v.(store.BlockedUser)
+ require.True(t, ok)
+ res[i] = vv
+ }
+ return res
+ }
+ assert.NoError(t, setBlocked("radio-t", "user1", engine.FlagTrue, 0))
+ assert.NoError(t, setBlocked("radio-t", "user2", engine.FlagTrue, 50*time.Millisecond))
+ assert.NoError(t, setBlocked("radio-t", "user3", engine.FlagFalse, 0))
+
+ vv, err := b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}})
+ assert.NoError(t, err)
+
+ blockedList := toBlocked(vv)
+ require.Equal(t, 2, len(blockedList), b.metaUsers)
+ assert.Equal(t, "user1", blockedList[0].ID)
+ assert.Equal(t, "user2", blockedList[1].ID)
+ t.Logf("%+v", blockedList)
+
+ // check block expiration
+ time.Sleep(50 * time.Millisecond)
+ vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "radio-t"}})
+ assert.NoError(t, err)
+ blockedList = toBlocked(vv)
+ require.Equal(t, 1, len(blockedList))
+ assert.Equal(t, "user1", blockedList[0].ID)
+
+ vv, err = b.ListFlags(engine.FlagRequest{Flag: engine.Blocked, Locator: store.Locator{SiteID: "bad"}})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(vv))
+}
+
+func TestMem_DeleteComment(t *testing.T) {
+
+ b := prepMem(t)
+ reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
+ res, err := b.Find(reqReq)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res), "initially 2 comments")
+
+ count, err := b.Count(reqReq)
+ require.NoError(t, err)
+ require.Equal(t, 2, count, "count=2 initially")
+
+ delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
+ CommentID: res[0].ID, DeleteMode: store.SoftDelete}
+
+ err = b.Delete(delReq)
+ assert.NoError(t, err)
+
+ res, err = b.Find(reqReq)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res))
+ assert.Equal(t, "", res[0].Text)
+ assert.True(t, res[0].Deleted, "marked deleted")
+ assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
+
+ assert.Equal(t, "some text2", res[1].Text)
+ assert.False(t, res[1].Deleted)
+
+ comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
+ assert.NoError(t, err)
+ assert.Equal(t, 1, len(comments), "1 in last, 1 removed")
+
+ count, err = b.Count(reqReq)
+ require.NoError(t, err)
+ assert.Equal(t, 1, count)
+
+ delReq.CommentID = "123456"
+ err = b.Delete(delReq)
+ assert.NotNil(t, err)
+
+ delReq.Locator.SiteID = "bad"
+ delReq.CommentID = res[0].ID
+ err = b.Delete(delReq)
+ assert.EqualError(t, err, `not found`)
+
+ delReq.Locator = store.Locator{URL: "https://radio-t.com/bad", SiteID: "radio-t"}
+ err = b.Delete(delReq)
+ assert.EqualError(t, err, `not found`)
+}
+
+func TestMem_DeleteHard(t *testing.T) {
+
+ b := prepMem(t)
+ reqReq := engine.FindRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, Sort: "time"}
+ res, err := b.Find(reqReq)
+ assert.NoError(t, err)
+ assert.Equal(t, 2, len(res), "initially 2 comments")
+
+ delReq := engine.DeleteRequest{Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
+ CommentID: res[0].ID, DeleteMode: store.HardDelete}
+ err = b.Delete(delReq)
+ assert.NoError(t, err)
+
+ res, err = b.Find(reqReq)
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(res))
+ assert.Equal(t, "", res[0].Text)
+ assert.True(t, res[0].Deleted, "marked deleted")
+ assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, res[0].User)
+}
+
+func TestMem_DeleteAll(t *testing.T) {
+ b := prepMem(t)
+ delReq := engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}}
+ err := b.Delete(delReq)
+ assert.NoError(t, err)
+
+ comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Limit: 10})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(comments), "nothing left")
+}
+
+func TestBoltAdmin_DeleteUserHard(t *testing.T) {
+ b := prepMem(t)
+ err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
+ DeleteMode: store.HardDelete})
+ require.NoError(t, err)
+
+ comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"},
+ Sort: "time"})
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(comments), "2 comments with deleted info")
+ assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User)
+ assert.Equal(t, store.User{Name: "deleted", ID: "deleted", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User)
+
+ c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, c, "0 count")
+
+ _, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5})
+ assert.NoError(t, err, "no comments for user user1 in store")
+
+ comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(comments), "nothing left")
+}
+
+func TestBoltAdmin_DeleteUserSoft(t *testing.T) {
+
+ b := prepMem(t)
+ err := b.Delete(engine.DeleteRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1",
+ DeleteMode: store.SoftDelete})
+ require.NoError(t, err)
+
+ comments, err := b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"},
+ Sort: "time"})
+ assert.NoError(t, err)
+ require.Equal(t, 2, len(comments), "2 comments with deleted info")
+ assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[0].User)
+ assert.Equal(t, store.User{Name: "user name", ID: "user1", Picture: "", Admin: false, Blocked: false, IP: ""}, comments[1].User)
+
+ c, err := b.Count(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com"}})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, c, "0 count")
+
+ comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, UserID: "user1", Limit: 5})
+ assert.NoError(t, err, "no comments for user user1 in store")
+ require.Equal(t, 2, len(comments), "2 comments with deleted info")
+ assert.True(t, comments[0].Deleted)
+ assert.True(t, comments[1].Deleted)
+ assert.Equal(t, "", comments[0].Text)
+ assert.Equal(t, "", comments[1].Text)
+
+ comments, err = b.Find(engine.FindRequest{Locator: store.Locator{SiteID: "radio-t"}, Sort: "time"})
+ assert.NoError(t, err)
+ assert.Equal(t, 0, len(comments), "nothing left")
+}
+
+func prepMem(t *testing.T) *MemData {
+
+ m := NewMemData()
+
+ comment := store.Comment{
+ ID: "id-1",
+ Text: `some text, link`,
+ Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
+ Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
+ User: store.User{ID: "user1", Name: "user name"},
+ }
+ _, err := m.Create(comment)
+ require.NoError(t, err)
+
+ comment = store.Comment{
+ ID: "id-2",
+ Text: "some text2",
+ Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local),
+ Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
+ User: store.User{ID: "user1", Name: "user name"},
+ }
+ _, err = m.Create(comment)
+ require.NoError(t, err)
+ return m
+}
+
+func getReq(locator store.Locator, commentID string) engine.GetRequest {
+ return engine.GetRequest{
+ Locator: locator,
+ CommentID: commentID,
+ }
+}
diff --git a/backend/_example/memory_store/compose-dev-memstore.yml b/backend/_example/memory_store/compose-dev-memstore.yml
new file mode 100644
index 00000000..3392ecae
--- /dev/null
+++ b/backend/_example/memory_store/compose-dev-memstore.yml
@@ -0,0 +1,47 @@
+# compose file demonstrating custom storage use. The memory_store (see backend/_example/memory_store) starts
+# in a separate container and remark42 communicates to mem_store.r42 via STORE_RPC_API url
+
+version: '2'
+
+services:
+
+ remark42:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ args:
+ - SKIP_BACKEND_TEST=true
+ - SKIP_FRONTEND_TEST=true
+ image: umputun/remark42:dev
+ container_name: "remark42"
+ hostname: "remark42"
+ restart: always
+
+ ports:
+ - "8080:8080" # primary rest server
+ - "8084:8084" # local oauth2 server
+
+ environment:
+ - REMARK_URL=http://127.0.0.1:8080
+ - SECRET=123456
+ - BACKUP_PATH=/srv/var/backup
+ - DEBUG=true
+ - EMOJI=true
+ - AUTH_ANON=true
+ - AUTH_DEV=true
+ - ADMIN_TYPE=rpc
+ - ADMIN_RPC_API=http://mem_store.r42:8080/cmd
+ - STORE_TYPE=rpc
+ - STORE_RPC_API=http://mem_store.r42:8080/cmd
+
+ mem_store.r42:
+ image: umputun/mem_store.r42
+ build:
+ context: .
+ dockerfile: backend/_example/memory_store/Dockerfile
+ container_name: "mem_store.r42"
+ hostname: "mem_store.r42"
+ environment:
+ - API=/cmd
+ - SECRET=123456
+ - DEBUG=true
diff --git a/backend/_example/memory_store/go.mod b/backend/_example/memory_store/go.mod
new file mode 100644
index 00000000..ddabb0b5
--- /dev/null
+++ b/backend/_example/memory_store/go.mod
@@ -0,0 +1,17 @@
+module github.com/umputun/remark/memory_store
+
+go 1.12
+
+require (
+ github.com/go-pkgz/jrpc v0.1.0
+ github.com/go-pkgz/lgr v0.6.3
+ github.com/jessevdk/go-flags v1.4.0
+ github.com/pkg/errors v0.8.1
+ github.com/stretchr/testify v1.3.0
+ github.com/umputun/remark/backend v1.4.0
+ golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 // indirect
+)
+
+replace github.com/umputun/remark/backend => ../../
+
+replace gopkg.in/russross/blackfriday.v2 => github.com/russross/blackfriday/v2 v2.0.1
diff --git a/backend/_example/memory_store/go.sum b/backend/_example/memory_store/go.sum
new file mode 100644
index 00000000..ab5ff9e5
--- /dev/null
+++ b/backend/_example/memory_store/go.sum
@@ -0,0 +1,191 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.40.0/go.mod h1:Tk58MuI9rbLMKlAjeO/bDnteAx7tX2gJIXw4T5Jwlro=
+cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/PuerkitoBio/goquery v1.5.0 h1:uGvmFXOA73IKluu/F84Xd1tt/z07GYm8X49XKHP7EJk=
+github.com/PuerkitoBio/goquery v1.5.0/go.mod h1:qD2PgZ9lccMbQlc7eEOjaeRlFQON7xY8kdmcsrnKqMg=
+github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o=
+github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
+github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
+github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
+github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d h1:vs5Nf6IE0N/PwGJ8//zRed4gpCdcr99K2HzX7RuLOQ8=
+github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
+github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is=
+github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
+github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
+github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I=
+github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
+github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
+github.com/go-pkgz/auth v0.7.2/go.mod h1:ibOpZYISiaOvAHe2bsKj2s3v4AkMam2WxxIFn+zhulo=
+github.com/go-pkgz/jrpc v0.1.0 h1:hNg/IyfEqJcSWOKkuHw0ZwcuGc9TDp7QZREsD2ycmiM=
+github.com/go-pkgz/jrpc v0.1.0/go.mod h1:JxZsvoBklA50DNhELVJnJ567Rt+KrMH9rR3u515wvE8=
+github.com/go-pkgz/lcw v0.3.1/go.mod h1:k+PY1CkCMTLXILtFoJOyK65Qqi9rkoTYunFH1vE/C0I=
+github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
+github.com/go-pkgz/lgr v0.6.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
+github.com/go-pkgz/lgr v0.6.3 h1:n9pGk2paBV8w/Y/FVEq5MkwDmP33dnUPKbY4CyyygwM=
+github.com/go-pkgz/lgr v0.6.3/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
+github.com/go-pkgz/mongo v1.1.2 h1:2Vqn3CWQJkkx4gxxDiQUitAW2FN/CH26lKHkipmpKcc=
+github.com/go-pkgz/mongo v1.1.2/go.mod h1:0NkWnzpiUxoL5fYZuttCtJrpC67oNDidfYxcdPqHTf0=
+github.com/go-pkgz/repeater v1.1.2/go.mod h1:QfNR/a+xqjs+f9wSxWqOQlw9aQhmKlUaSwXCiZ+Ko2w=
+github.com/go-pkgz/rest v1.4.1 h1:DmaVLPH2O7yLehrWOW0uz01d2mVHz9fBR/iuTiPRzaw=
+github.com/go-pkgz/rest v1.4.1/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk=
+github.com/go-pkgz/syncs v1.1.1/go.mod h1:bt9lxWRRJ9vOCMGc8Big8ttjYHLKP88ofj1y38UlaHE=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
+github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+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/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/jessevdk/go-flags v0.0.0-20180331124232-1c38ed7ad0cc/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kyokomi/emoji v2.1.0+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2pxoszVseX1DNoGtU2tBA=
+github.com/microcosm-cc/bluemonday v1.0.2 h1:5lPfLTTAvAbtS0VqT+94yOtFnGfUWYyx0+iToC3Os3s=
+github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc=
+github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
+github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
+github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+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.4.0 h1:rJf4ndpvRDS7tOrtIpVkacItTxF5pVpXvDqtb1dX40M=
+go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
+go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3 h1:4y9KwBHBgBNwDbtu44R5o1fdOCQUEXhbk/P4A9WmJq0=
+golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.6.0/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
diff --git a/backend/_example/memory_store/main.go b/backend/_example/memory_store/main.go
new file mode 100644
index 00000000..07f2e85f
--- /dev/null
+++ b/backend/_example/memory_store/main.go
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2019 Umputun. All rights reserved.
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file.
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/go-pkgz/jrpc"
+ log "github.com/go-pkgz/lgr"
+ "github.com/jessevdk/go-flags"
+
+ "github.com/umputun/remark/memory_store/accessor"
+ "github.com/umputun/remark/memory_store/server"
+)
+
+// opts with all cli commands and flags
+var opts struct {
+ API string `long:"api" env:"API" default:"/" description:"api root url"`
+ Port int `long:"port" env:"PORT" default:"8080" description:"rpc server port"`
+ AuthUser string `long:"auth-user" env:"AUTH_USER" default:"" description:"rpc auth user name"`
+ AuthPasswd string `long:"auth-passwd" env:"AUTH_PASSWD" default:"" description:"rpc auth password"`
+
+ Secret string `long:"secret" env:"SECRET" required:"true" description:"secret key"`
+ Dbg bool `long:"dbg" env:"DEBUG" description:"debug mode"`
+}
+
+var revision = "unknown"
+
+func main() {
+ fmt.Printf("remark42-memory module %s\n", revision)
+
+ if _, err := flags.Parse(&opts); err != nil {
+ os.Exit(2)
+ }
+ setupLog(opts.Dbg)
+
+ dataStore := accessor.NewMemData()
+ adminStore := accessor.NewMemAdminStore(opts.Secret)
+
+ rpcServer := jrpc.Server{
+ API: opts.API,
+ AuthUser: opts.AuthUser,
+ AuthPasswd: opts.AuthPasswd,
+ Version: revision,
+ AppName: "remark42-memory",
+ Logger: log.Default(),
+ }
+
+ srv := server.NewRPC(dataStore, adminStore, &rpcServer)
+
+ admRec := accessor.AdminRec{
+ SiteID: "remark",
+ IDs: []string{"dev_user"},
+ Email: "admin@example.com",
+ }
+ adminStore.Set("remark", admRec)
+
+ err := srv.Run(opts.Port)
+ log.Printf("[ERROR] server failed or terminated, %+v", err)
+}
+
+func setupLog(dbg bool) {
+ if dbg {
+ log.Setup(log.Debug, log.CallerFile, log.CallerFunc, log.Msec, log.LevelBraces)
+ return
+ }
+ log.Setup(log.Msec, log.LevelBraces)
+}
diff --git a/backend/_example/memory_store/server/rpc.go b/backend/_example/memory_store/server/rpc.go
new file mode 100644
index 00000000..47bcc824
--- /dev/null
+++ b/backend/_example/memory_store/server/rpc.go
@@ -0,0 +1,183 @@
+package server
+
+import (
+ "encoding/json"
+
+ "github.com/go-pkgz/jrpc"
+
+ "github.com/umputun/remark/backend/app/store"
+ "github.com/umputun/remark/backend/app/store/admin"
+ "github.com/umputun/remark/backend/app/store/engine"
+)
+
+// RPC handler wraps both engine and remote server and implements all handlers for data store and admin store
+// Note: this file can be used as-is in any custom jrpc plugin
+type RPC struct {
+ *jrpc.Server
+ eng engine.Interface
+ adm admin.Store
+}
+
+// NewRPC makes RPC instance and register handlers
+func NewRPC(e engine.Interface, a admin.Store, r *jrpc.Server) *RPC {
+ res := &RPC{eng: e, adm: a, Server: r}
+ res.addHandlers()
+ return res
+}
+
+func (s *RPC) addHandlers() {
+ // data store handlers
+ s.Group("store", jrpc.HandlersGroup{
+ "create": s.createHndl,
+ "find": s.findHndl,
+ "get": s.getHndl,
+ "update": s.updateHndl,
+ "count": s.countHndl,
+ "info": s.infoHndl,
+ "flag": s.flagHndl,
+ "list_flags": s.listFlagsHndl,
+ "delete": s.deleteHndl,
+ "close": s.closeHndl,
+ })
+
+ // admin store handlers
+ s.Group("admin", jrpc.HandlersGroup{
+ "key": s.admKeyHndl,
+ "admins": s.admAdminsHndl,
+ "email": s.admEmailHndl,
+ })
+}
+
+func (s *RPC) createHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ comment := store.Comment{}
+ if err := json.Unmarshal(params, &comment); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ commentID, err := s.eng.Create(comment)
+ return jrpc.EncodeResponse(id, commentID, err)
+}
+
+// Find comments
+func (s *RPC) findHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ req := engine.FindRequest{}
+ if err := json.Unmarshal(params, &req); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ resp, err := s.eng.Find(req)
+ return jrpc.EncodeResponse(id, resp, err)
+}
+
+// Get comment
+func (s *RPC) getHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ req := engine.GetRequest{}
+ if err := json.Unmarshal(params, &req); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ comment, err := s.eng.Get(req)
+ return jrpc.EncodeResponse(id, comment, err)
+}
+
+// Update comment
+func (s *RPC) updateHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ comment := store.Comment{}
+ if err := json.Unmarshal(params, &comment); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ err := s.eng.Update(comment)
+ return jrpc.EncodeResponse(id, nil, err)
+}
+
+// counts for site and users
+func (s *RPC) countHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ req := engine.FindRequest{}
+ if err := json.Unmarshal(params, &req); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ count, err := s.eng.Count(req)
+ return jrpc.EncodeResponse(id, count, err)
+}
+
+// info get post meta info
+func (s *RPC) infoHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ req := engine.InfoRequest{}
+ if err := json.Unmarshal(params, &req); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ info, err := s.eng.Info(req)
+ return jrpc.EncodeResponse(id, info, err)
+}
+
+// flagHndl get and sets flag value
+func (s *RPC) flagHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ req := engine.FlagRequest{}
+ if err := json.Unmarshal(params, &req); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ status, err := s.eng.Flag(req)
+ return jrpc.EncodeResponse(id, status, err)
+}
+
+// listFlagsHndl list flags for given request
+func (s *RPC) listFlagsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ req := engine.FlagRequest{}
+ if err := json.Unmarshal(params, &req); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ flags, err := s.eng.ListFlags(req)
+ return jrpc.EncodeResponse(id, flags, err)
+}
+
+// deleteHndl remove comment(s)
+func (s *RPC) deleteHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ req := engine.DeleteRequest{}
+ if err := json.Unmarshal(params, &req); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ err := s.eng.Delete(req)
+ return jrpc.EncodeResponse(id, nil, err)
+}
+
+// close store
+func (s *RPC) closeHndl(id uint64, _ json.RawMessage) (rr jrpc.Response) {
+ if err := s.eng.Close(); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ return jrpc.Response{}
+}
+
+// get admin key
+func (s *RPC) admKeyHndl(id uint64, _ json.RawMessage) (rr jrpc.Response) {
+ key, err := s.adm.Key()
+ if err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ return jrpc.EncodeResponse(id, key, err)
+}
+
+// get admins list
+func (s *RPC) admAdminsHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ var siteID string
+ if err := json.Unmarshal(params, &siteID); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+
+ admins, err := s.adm.Admins(siteID)
+ if err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ return jrpc.EncodeResponse(id, admins, err)
+}
+
+// get admin email
+func (s *RPC) admEmailHndl(id uint64, params json.RawMessage) (rr jrpc.Response) {
+ var siteID string
+ if err := json.Unmarshal(params, &siteID); err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+
+ email, err := s.adm.Email(siteID)
+ if err != nil {
+ return jrpc.Response{Error: err.Error()}
+ }
+ return jrpc.EncodeResponse(id, email, err)
+}
diff --git a/backend/_example/memory_store/server/rpc_test.go b/backend/_example/memory_store/server/rpc_test.go
new file mode 100644
index 00000000..30f3942c
--- /dev/null
+++ b/backend/_example/memory_store/server/rpc_test.go
@@ -0,0 +1,318 @@
+/*
+ * Copyright 2019 Umputun. All rights reserved.
+ * Use of this source code is governed by a MIT-style
+ * license that can be found in the LICENSE file.
+ */
+
+package server
+
+import (
+ "fmt"
+ "math/rand"
+ "net/http"
+ "testing"
+ "time"
+
+ "github.com/go-pkgz/jrpc"
+ "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/memory_store/accessor"
+)
+
+func TestRPC_createHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ id, err := re.Create(store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}})
+ assert.NoError(t, err)
+ assert.Equal(t, "123456", id)
+}
+
+func TestRPC_findHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
+ comments, err := re.Find(findReq)
+ require.NoError(t, err)
+ assert.Equal(t, 0, len(comments))
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ id, err := re.Create(c)
+ assert.NoError(t, err)
+ assert.Equal(t, "123456", id)
+
+ comments, err = re.Find(findReq)
+ require.NoError(t, err)
+ assert.Equal(t, 1, len(comments))
+ assert.Equal(t, c, comments[0])
+}
+
+func TestRPC_getHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ req := engine.GetRequest{
+ Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ CommentID: "123456",
+ }
+
+ _, err := re.Get(req)
+ assert.EqualError(t, err, "not found")
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ _, err = re.Create(c)
+ assert.NoError(t, err)
+
+ comment, err := re.Get(req)
+ assert.NoError(t, err)
+ assert.Equal(t, c, comment)
+}
+
+func TestRPC_updateHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ err := re.Update(c)
+ assert.EqualError(t, err, "not found")
+
+ _, err = re.Create(c)
+ assert.NoError(t, err)
+
+ c.Text = "updates"
+ err = re.Update(c)
+ assert.NoError(t, err)
+
+ req := engine.GetRequest{
+ Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ CommentID: "123456",
+ }
+ comment, err := re.Get(req)
+ assert.NoError(t, err)
+ assert.Equal(t, c, comment)
+}
+
+func TestRPC_countHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ findReq := engine.FindRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
+ count, err := re.Count(findReq)
+ require.NoError(t, err)
+ assert.Equal(t, 0, count)
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ id, err := re.Create(c)
+ assert.NoError(t, err)
+ assert.Equal(t, "123456", id)
+
+ count, err = re.Count(findReq)
+ require.NoError(t, err)
+ assert.Equal(t, 1, count)
+}
+
+func TestRPC_infoHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ id, err := re.Create(c)
+ assert.NoError(t, err)
+ assert.Equal(t, "123456", id)
+
+ infoReq := engine.InfoRequest{Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"}}
+ info, err := re.Info(infoReq)
+ require.NoError(t, err)
+ assert.Equal(t, 1, len(info))
+ i := info[0]
+ assert.Equal(t, store.PostInfo{URL: "http://example.com/post1", Count: 1}, i)
+}
+
+func TestRPC_flagHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ id, err := re.Create(c)
+ assert.NoError(t, err)
+ assert.Equal(t, "123456", id)
+
+ flagReq := engine.FlagRequest{
+ Flag: engine.Verified,
+ Locator: store.Locator{
+ SiteID: "test-site",
+ },
+ UserID: "u1",
+ }
+ status, err := re.Flag(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, false, status)
+
+ flagReq.Update = engine.FlagTrue
+ status, err = re.Flag(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, true, status)
+
+ flagReq.Update = engine.FlagNonSet
+ status, err = re.Flag(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, true, status)
+}
+
+func TestRPC_listFlagsHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ id, err := re.Create(c)
+ assert.NoError(t, err)
+ assert.Equal(t, "123456", id)
+
+ flagReq := engine.FlagRequest{
+ Flag: engine.Verified,
+ UserID: "u1",
+ Locator: store.Locator{
+ SiteID: "test-site",
+ },
+ }
+ flags, err := re.ListFlags(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, []interface{}{}, flags)
+
+ flagReq.Update = engine.FlagTrue
+ status, err := re.Flag(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, true, status)
+
+ flags, err = re.ListFlags(flagReq)
+ require.NoError(t, err)
+ assert.Equal(t, []interface{}{"u1"}, flags)
+}
+
+func TestRPC_deleteHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ req := engine.DeleteRequest{
+ Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ CommentID: "123456",
+ }
+
+ err := re.Delete(req)
+ assert.EqualError(t, err, "not found")
+
+ c := store.Comment{ID: "123456", Locator: store.Locator{SiteID: "test-site", URL: "http://example.com/post1"},
+ Text: "text 123", User: store.User{ID: "u1", Name: "user1"}}
+ _, err = re.Create(c)
+ assert.NoError(t, err)
+
+ err = re.Delete(req)
+ assert.NoError(t, err)
+}
+
+func TestRPC_closeHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ re := engine.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ err := re.Close()
+ assert.NoError(t, err)
+}
+func TestRPC_admKeyHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ key, err := ra.Key()
+ assert.NoError(t, err)
+ assert.Equal(t, "secret", key)
+}
+
+func TestRPC_admAdminsHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ _, err := ra.Admins("bad site")
+ assert.EqualError(t, err, "site bad site not found")
+
+ admins, err := ra.Admins("test-site")
+ assert.NoError(t, err)
+ assert.Equal(t, []string{"id1", "id2"}, admins)
+}
+
+func TestRPC_admEmailHndl(t *testing.T) {
+ _, port, teardown := prepTestStore(t)
+ defer teardown()
+ api := fmt.Sprintf("http://localhost:%d/test", port)
+
+ ra := admin.RPC{Client: jrpc.Client{API: api, Client: http.Client{Timeout: 1 * time.Second}}}
+ _, err := ra.Admins("bad site")
+ assert.EqualError(t, err, "site bad site not found")
+
+ email, err := ra.Email("test-site")
+ assert.NoError(t, err)
+ assert.Equal(t, "admin@example.com", email)
+}
+
+func prepTestStore(t *testing.T) (s *RPC, port int, teardown func()) {
+ port = 40000 + int(rand.Int31n(10000))
+
+ mg := accessor.NewMemData()
+ adm := accessor.NewMemAdminStore("secret")
+ s = NewRPC(mg, adm, &jrpc.Server{API: "/test", Logger: jrpc.NoOpLogger})
+
+ admRec := accessor.AdminRec{
+ SiteID: "test-site",
+ IDs: []string{"id1", "id2"},
+ Email: "admin@example.com",
+ }
+ adm.Set("test-site", admRec)
+
+ go func() {
+ t.Log(s.Run(port))
+ }()
+ time.Sleep(time.Millisecond * 10)
+
+ return s, port, func() {
+ require.NoError(t, s.Shutdown())
+ }
+}
diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go
index 5ff65abe..ffdc5092 100644
--- a/backend/app/cmd/server.go
+++ b/backend/app/cmd/server.go
@@ -14,6 +14,7 @@ import (
"time"
bolt "github.com/coreos/bbolt"
+ "github.com/go-pkgz/jrpc"
log "github.com/go-pkgz/lgr"
"github.com/kyokomi/emoji"
authcache "github.com/patrickmn/go-cache"
@@ -30,7 +31,6 @@ import (
"github.com/umputun/remark/backend/app/notify"
"github.com/umputun/remark/backend/app/rest/api"
"github.com/umputun/remark/backend/app/rest/proxy"
- "github.com/umputun/remark/backend/app/rpc"
"github.com/umputun/remark/backend/app/store"
"github.com/umputun/remark/backend/app/store/admin"
"github.com/umputun/remark/backend/app/store/engine"
@@ -108,7 +108,7 @@ type StoreGroup struct {
Path string `long:"path" env:"PATH" default:"./var" description:"parent dir for bolt files"`
Timeout time.Duration `long:"timeout" env:"TIMEOUT" default:"30s" description:"bolt timeout"`
} `group:"bolt" namespace:"bolt" env-namespace:"BOLT"`
- RPC RPCGroup `group:"rpc" namespace:"rpc" env-namespace:"TPC"`
+ RPC RPCGroup `group:"rpc" namespace:"rpc" env-namespace:"RPC"`
}
// ImageGroup defines options group for store pictures
@@ -438,7 +438,7 @@ func (s *ServerCommand) makeDataStore() (result engine.Interface, err error) {
}
result, err = engine.NewBoltDB(bolt.Options{Timeout: s.Store.Bolt.Timeout}, sites...)
case "rpc":
- r := &engine.RPC{Client: rpc.Client{
+ r := &engine.RPC{Client: jrpc.Client{
API: s.Store.RPC.API,
Client: http.Client{Timeout: s.Store.RPC.TimeOut},
AuthUser: s.Store.RPC.AuthUser,
@@ -503,7 +503,7 @@ func (s *ServerCommand) makeAdminStore() (admin.Store, error) {
}
return admin.NewStaticStore(s.SharedSecret, s.Admin.Shared.Admins, s.Admin.Shared.Email), nil
case "rpc":
- r := &admin.RPC{Client: rpc.Client{
+ r := &admin.RPC{Client: jrpc.Client{
API: s.Admin.RPC.API,
Client: http.Client{Timeout: s.Admin.RPC.TimeOut},
AuthUser: s.Admin.RPC.AuthUser,
diff --git a/backend/app/rpc/client_test.go b/backend/app/rpc/client_test.go
deleted file mode 100644
index 12b718ce..00000000
--- a/backend/app/rpc/client_test.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package rpc
-
-import (
- "encoding/json"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestClient_Call(t *testing.T) {
- ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"result":"12345"}`)
- defer ts.Close()
- c := Client{API: ts.URL, Client: http.Client{}}
- resp, err := c.Call("test", 123, "abc")
- assert.NoError(t, err)
- res := ""
- err = json.Unmarshal(*resp.Result, &res)
- assert.NoError(t, err)
- assert.Equal(t, "12345", res)
- t.Logf("%v %T", res, res)
-}
-
-func TestClient_CallWithObject(t *testing.T) {
- ts := testServer(t, `{"method":"test","params":{"F1":123,"F2":"abc","F3":"2019-06-09T23:03:55Z"},"id":1}`, `{"result":"12345"}`)
- defer ts.Close()
- c := Client{API: ts.URL, Client: http.Client{}}
- obj := struct {
- F1 int
- F2 string
- F3 time.Time
- }{
- F1: 123,
- F2: "abc",
- F3: time.Date(2019, 6, 9, 23, 3, 55, 0, time.UTC),
- }
-
- resp, err := c.Call("test", obj)
- assert.NoError(t, err)
- res := ""
- err = json.Unmarshal(*resp.Result, &res)
- assert.NoError(t, err)
- assert.Equal(t, "12345", res)
- t.Logf("%v %T", res, res)
-}
-
-func TestClient_CallWithNoParams(t *testing.T) {
- ts := testServer(t, `{"method":"test","id":1}`, `{"result":"12345"}`)
- defer ts.Close()
- c := Client{API: ts.URL, Client: http.Client{}}
- resp, err := c.Call("test")
- assert.NoError(t, err)
- res := ""
- err = json.Unmarshal(*resp.Result, &res)
- assert.NoError(t, err)
- assert.Equal(t, "12345", res)
- t.Logf("%v %T", res, res)
-}
-
-func TestClient_CallError(t *testing.T) {
- ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"error":"some error"}`)
- defer ts.Close()
- c := Client{API: ts.URL, Client: http.Client{}}
- _, err := c.Call("test", 123, "abc")
- assert.EqualError(t, err, "some error")
-}
-
-func TestClient_CallBadResponse(t *testing.T) {
- ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"result":"12345 invalid}`)
- defer ts.Close()
- c := Client{API: ts.URL, Client: http.Client{}}
- _, err := c.Call("test", 123, "abc")
- assert.NotNil(t, err)
-}
-
-func TestClient_CallBadRemote(t *testing.T) {
- ts := testServer(t, `{"method":"test","params":[123,"abc"],"id":1}`, `{"result":"12345"}`)
- defer ts.Close()
- c := Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}
- _, err := c.Call("test", 123)
- assert.NotNil(t, err)
-}
-
-func testServer(t *testing.T, req, resp string) *httptest.Server {
- return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- body, err := ioutil.ReadAll(r.Body)
- require.NoError(t, err)
- assert.Equal(t, req, string(body))
- t.Logf("req: %s", string(body))
- fmt.Fprintf(w, resp)
- }))
-}
diff --git a/backend/app/rpc/remote.go b/backend/app/rpc/remote.go
deleted file mode 100644
index 48d8ac50..00000000
--- a/backend/app/rpc/remote.go
+++ /dev/null
@@ -1,23 +0,0 @@
-// Package rpc implements client ans server for RPC-like communication with remote storage.
-// The protocol is somewhat simplified version of json-rpc with a single POST call sending
-// Request json (method name and the list of parameters) and receiving back json Response with "result" json
-// and error string
-package rpc
-
-import (
- "encoding/json"
-)
-
-// Request encloses method name and all params
-type Request struct {
- Method string `json:"method"`
- Params interface{} `json:"params,omitempty"`
- ID uint64 `json:"id"`
-}
-
-// Response encloses result and error received from remote server
-type Response struct {
- Result *json.RawMessage `json:"result,omitempty"`
- Error string `json:"error,omitempty"`
- ID uint64 `json:"id"`
-}
diff --git a/backend/app/rpc/server_test.go b/backend/app/rpc/server_test.go
deleted file mode 100644
index c39fe51a..00000000
--- a/backend/app/rpc/server_test.go
+++ /dev/null
@@ -1,245 +0,0 @@
-package rpc
-
-import (
- "bytes"
- "encoding/json"
- "io/ioutil"
- "net/http"
- "net/http/httptest"
- "testing"
- "time"
-
- "github.com/pkg/errors"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-func TestServerPrimitiveTypes(t *testing.T) {
- s := Server{API: "/v1/cmd"}
-
- type respData struct {
- Res1 string
- Res2 bool
- }
-
- s.Add("test", func(id uint64, params json.RawMessage) Response {
- args := []interface{}{}
- if err := json.Unmarshal(params, &args); err != nil {
- return Response{Error: err.Error()}
- }
- t.Logf("%+v", args)
-
- assert.Equal(t, 3, len(args))
- assert.Equal(t, "blah", args[0].(string))
- assert.Equal(t, 42., args[1].(float64))
- assert.Equal(t, true, args[2].(bool))
-
- r, err := s.EncodeResponse(id, respData{"res blah", true}, nil)
- assert.NoError(t, err)
- return r
- })
-
- go func() { _ = s.Run(9091) }()
- defer func() { assert.NoError(t, s.Shutdown()) }()
- time.Sleep(10 * time.Millisecond)
-
- // check with direct http call
- clientReq := Request{Method: "test", Params: []interface{}{"blah", 42, true}, ID: 123}
- b := bytes.Buffer{}
- require.NoError(t, json.NewEncoder(&b).Encode(clientReq))
- resp, err := http.Post("http://127.0.0.1:9091/v1/cmd", "application/json", &b)
- require.NoError(t, err)
- defer resp.Body.Close()
- assert.Equal(t, 200, resp.StatusCode)
- data, err := ioutil.ReadAll(resp.Body)
- assert.NoError(t, err)
- assert.Equal(t, `{"result":{"Res1":"res blah","Res2":true},"id":123}`+"\n", string(data))
-
- // check with client call
- c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
- r, err := c.Call("test", "blah", 42, true)
- assert.NoError(t, err)
- assert.Equal(t, "", r.Error)
-
- res := respData{}
- err = json.Unmarshal(*r.Result, &res)
- assert.NoError(t, err)
- assert.Equal(t, respData{Res1: "res blah", Res2: true}, res)
- assert.Equal(t, uint64(1), r.ID)
-}
-
-func TestServerWithObject(t *testing.T) {
- s := Server{API: "/v1/cmd"}
-
- type respData struct {
- Res1 string
- Res2 bool
- }
-
- type reqData struct {
- Time time.Time
- F1 string
- F2 time.Duration
- }
-
- s.Add("test", func(id uint64, params json.RawMessage) Response {
- arg := reqData{}
- if err := json.Unmarshal(params, &arg); err != nil {
- return Response{Error: err.Error()}
- }
- t.Logf("%+v", arg)
-
- r, err := s.EncodeResponse(id, respData{"res blah", true}, nil)
- assert.NoError(t, err)
- return r
- })
-
- go func() { _ = s.Run(9091) }()
- defer func() { assert.NoError(t, s.Shutdown()) }()
- time.Sleep(10 * time.Millisecond)
-
- c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
- r, err := c.Call("test", reqData{Time: time.Now(), F1: "sawert", F2: time.Minute})
- assert.NoError(t, err)
- assert.Equal(t, "", r.Error)
-
- res := respData{}
- err = json.Unmarshal(*r.Result, &res)
- assert.NoError(t, err)
- assert.Equal(t, respData{Res1: "res blah", Res2: true}, res)
-}
-
-func TestServerMethodNotImplemented(t *testing.T) {
- s := Server{}
- ts := httptest.NewServer(http.HandlerFunc(s.handler))
- defer ts.Close()
- s.Add("test", func(id uint64, params json.RawMessage) Response {
- return Response{}
- })
-
- r := Request{Method: "blah"}
- buf := bytes.Buffer{}
- assert.NoError(t, json.NewEncoder(&buf).Encode(r))
- resp, err := http.Post(ts.URL, "application/json", &buf)
- require.NoError(t, err)
- assert.Equal(t, http.StatusNotImplemented, resp.StatusCode)
-
- assert.EqualError(t, s.Shutdown(), "http server is not running")
-}
-
-func TestServerWithAuth(t *testing.T) {
- s := Server{API: "/v1/cmd", AuthUser: "user", AuthPasswd: "passwd"}
-
- s.Add("test", func(id uint64, params json.RawMessage) Response {
- args := []interface{}{}
- if err := json.Unmarshal(params, &args); err != nil {
- return Response{Error: err.Error()}
- }
- t.Logf("%+v", args)
-
- assert.Equal(t, 3, len(args))
- assert.Equal(t, "blah", args[0].(string))
- assert.Equal(t, 42., args[1].(float64))
- assert.Equal(t, true, args[2].(bool))
-
- r, err := s.EncodeResponse(id, "res blah", nil)
- assert.NoError(t, err)
- return r
- })
-
- go func() { _ = s.Run(9091) }()
- time.Sleep(10 * time.Millisecond)
- defer func() { assert.NoError(t, s.Shutdown()) }()
-
- c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}, AuthUser: "user", AuthPasswd: "passwd"}
- r, err := c.Call("test", "blah", 42, true)
- assert.NoError(t, err)
- assert.Equal(t, "", r.Error)
- val := ""
- err = json.Unmarshal(*r.Result, &val)
- assert.NoError(t, err)
- assert.Equal(t, "res blah", val)
-
- c = Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
- _, err = c.Call("test", "blah", 42, true)
- assert.EqualError(t, err, "bad status 401 Unauthorized for test")
-}
-
-func TestServerErrReturn(t *testing.T) {
- s := Server{API: "/v1/cmd", AuthUser: "user", AuthPasswd: "passwd"}
-
- s.Add("test", func(id uint64, params json.RawMessage) Response {
- args := []interface{}{}
- if err := json.Unmarshal(params, &args); err != nil {
- return Response{Error: err.Error()}
- }
- t.Logf("%+v", args)
-
- assert.Equal(t, 3, len(args))
- assert.Equal(t, "blah", args[0].(string))
- assert.Equal(t, 42., args[1].(float64))
- assert.Equal(t, true, args[2].(bool))
-
- r, err := s.EncodeResponse(id, "res blah", errors.New("some error"))
- assert.NoError(t, err)
- return r
- })
-
- go func() { _ = s.Run(9091) }()
- defer func() { assert.NoError(t, s.Shutdown()) }()
- time.Sleep(10 * time.Millisecond)
-
- c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}, AuthUser: "user", AuthPasswd: "passwd"}
- _, err := c.Call("test", "blah", 42, true)
- assert.EqualError(t, err, "some error")
-}
-
-func TestServerGroup(t *testing.T) {
- s := Server{API: "/v1/cmd"}
- s.Group("pre", HandlersGroup{
- "fn1": func(id uint64, params json.RawMessage) Response {
- return Response{}
- },
- "fn2": func(id uint64, params json.RawMessage) Response {
- return Response{}
- },
- })
- go func() { _ = s.Run(9091) }()
- defer func() { assert.NoError(t, s.Shutdown()) }()
- time.Sleep(10 * time.Millisecond)
-
- c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
- _, err := c.Call("fn1")
- assert.EqualError(t, err, "bad status 501 Not Implemented for fn1")
-
- _, err = c.Call("pre.fn1")
- assert.NoError(t, err)
- _, err = c.Call("pre.fn2")
- assert.NoError(t, err)
-}
-
-func TestServerAddLate(t *testing.T) {
- s := Server{API: "/v1/cmd"}
- s.Add("fn1", func(id uint64, params json.RawMessage) Response {
- return Response{}
- })
- go func() { _ = s.Run(9091) }()
- defer func() { assert.NoError(t, s.Shutdown()) }()
- time.Sleep(10 * time.Millisecond)
-
- // too late, ignored after run
- s.Add("fn2", func(id uint64, params json.RawMessage) Response {
- return Response{}
- })
-
- c := Client{API: "http://127.0.0.1:9091/v1/cmd", Client: http.Client{}}
- _, err := c.Call("fn1")
- assert.NoError(t, err)
- _, err = c.Call("fn2")
- assert.EqualError(t, err, "bad status 501 Not Implemented for fn2")
-}
-
-func TestServerNoHandlers(t *testing.T) {
- s := Server{API: "/v1/cmd", AuthUser: "user", AuthPasswd: "passwd"}
- assert.EqualError(t, s.Run(9091), "nothing mapped for dispatch, Add has to be called prior to Run")
-}
diff --git a/backend/app/store/admin/remote.go b/backend/app/store/admin/remote.go
index c25b84ee..08a9fccd 100644
--- a/backend/app/store/admin/remote.go
+++ b/backend/app/store/admin/remote.go
@@ -9,12 +9,12 @@ package admin
import (
"encoding/json"
- "github.com/umputun/remark/backend/app/rpc"
+ "github.com/go-pkgz/jrpc"
)
// RPC implements remote engine and delegates all Calls to remote http server
type RPC struct {
- rpc.Client
+ jrpc.Client
}
// Key returns the key, same for all sites
diff --git a/backend/app/store/admin/remote_test.go b/backend/app/store/admin/remote_test.go
index b385c591..ebf4bb38 100644
--- a/backend/app/store/admin/remote_test.go
+++ b/backend/app/store/admin/remote_test.go
@@ -13,17 +13,16 @@ import (
"net/http/httptest"
"testing"
+ "github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
-
- "github.com/umputun/remark/backend/app/rpc"
)
func TestRemote_Key(t *testing.T) {
ts := testServer(t, `{"method":"admin.key","id":1}`,
`{"result":"12345","id":1}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
@@ -35,10 +34,10 @@ func TestRemote_Key(t *testing.T) {
}
func TestRemote_Admins(t *testing.T) {
- ts := testServer(t, `{"method":"admin.admins","params":["site-1"],"id":1}`,
+ ts := testServer(t, `{"method":"admin.admins","params":"site-1","id":1}`,
`{"result":["id1","id2"],"id":1}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
@@ -50,10 +49,10 @@ func TestRemote_Admins(t *testing.T) {
}
func TestRemote_Email(t *testing.T) {
- ts := testServer(t, `{"method":"admin.email","params":["site-1"],"id":1}`,
+ ts := testServer(t, `{"method":"admin.email","params":"site-1","id":1}`,
`{"result":"bbb@example.com","id":1}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var a Store = &c
_ = a
diff --git a/backend/app/store/engine/remote.go b/backend/app/store/engine/remote.go
index bb084295..186ff9f0 100644
--- a/backend/app/store/engine/remote.go
+++ b/backend/app/store/engine/remote.go
@@ -3,13 +3,14 @@ package engine
import (
"encoding/json"
- "github.com/umputun/remark/backend/app/rpc"
+ "github.com/go-pkgz/jrpc"
+
"github.com/umputun/remark/backend/app/store"
)
// RPC implements remote engine and delegates all Calls to remote http server
type RPC struct {
- rpc.Client
+ jrpc.Client
}
// Create comment and return ID
diff --git a/backend/app/store/engine/remote_test.go b/backend/app/store/engine/remote_test.go
index 160f6d29..b97b637c 100644
--- a/backend/app/store/engine/remote_test.go
+++ b/backend/app/store/engine/remote_test.go
@@ -9,10 +9,10 @@ import (
"testing"
"time"
+ "github.com/go-pkgz/jrpc"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
- "github.com/umputun/remark/backend/app/rpc"
"github.com/umputun/remark/backend/app/store"
)
@@ -20,7 +20,7 @@ func TestRemote_Create(t *testing.T) {
ts := testServer(t, `{"method":"store.create","params":{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"},"id":1}`,
`{"result":"12345","id":1}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
var eng Interface = &c
_ = eng
@@ -35,7 +35,7 @@ func TestRemote_Create(t *testing.T) {
func TestRemote_Get(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, `{"result":{"id":"123","pid":"","text":"msg","delete":true}}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
res, err := c.Get(req)
@@ -47,7 +47,7 @@ func TestRemote_Get(t *testing.T) {
func TestRemote_GetWithErrorResult(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, `{"error":"failed"}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -57,7 +57,7 @@ func TestRemote_GetWithErrorResult(t *testing.T) {
func TestRemote_GetWithErrorDecode(t *testing.T) {
ts := testServer(t, `{"method":"store.get","params":{"locator":{"url":"http://example.com/url"},"comment_id":"site"},"id":1}`, ``)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -65,7 +65,7 @@ func TestRemote_GetWithErrorDecode(t *testing.T) {
}
func TestRemote_GetWithErrorRemote(t *testing.T) {
- c := RPC{Client: rpc.Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}}
+ c := RPC{Client: jrpc.Client{API: "http://127.0.0.2", Client: http.Client{Timeout: 10 * time.Millisecond}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -81,7 +81,7 @@ func TestRemote_FailedStatus(t *testing.T) {
w.WriteHeader(400)
}))
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
req := GetRequest{Locator: store.Locator{URL: "http://example.com/url"}, CommentID: "site"}
_, err := c.Get(req)
@@ -91,7 +91,7 @@ func TestRemote_FailedStatus(t *testing.T) {
func TestRemote_Update(t *testing.T) {
ts := testServer(t, `{"method":"store.update","params":{"id":"123","pid":"","text":"msg","user":{"name":"","id":"","picture":"","admin":false},"locator":{"site":"site123","url":"http://example.com/url"},"score":0,"vote":0,"time":"0001-01-01T00:00:00Z"},"id":1}`, `{}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
err := c.Update(store.Comment{ID: "123", Locator: store.Locator{URL: "http://example.com/url", SiteID: "site123"},
Text: "msg"})
@@ -102,7 +102,7 @@ func TestRemote_Update(t *testing.T) {
func TestRemote_Find(t *testing.T) {
ts := testServer(t, `{"method":"store.find","params":{"locator":{"url":"http://example.com/url"},"sort":"-time","since":"0001-01-01T00:00:00Z","limit":10},"id":1}`, `{"result":[{"text":"1"},{"text":"2"}]}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Find(FindRequest{Locator: store.Locator{URL: "http://example.com/url"}, Sort: "-time", Limit: 10})
assert.NoError(t, err)
@@ -112,7 +112,7 @@ func TestRemote_Find(t *testing.T) {
func TestRemote_Info(t *testing.T) {
ts := testServer(t, `{"method":"store.info","params":{"locator":{"url":"http://example.com/url"},"limit":10,"skip":5,"ro_age":10},"id":1}`, `{"result":[{"url":"u1","count":22},{"url":"u2","count":33}]}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Info(InfoRequest{Locator: store.Locator{URL: "http://example.com/url"},
Limit: 10, Skip: 5, ReadOnlyAge: 10})
@@ -123,7 +123,7 @@ func TestRemote_Info(t *testing.T) {
func TestRemote_Flag(t *testing.T) {
ts := testServer(t, `{"method":"store.flag","params":{"flag":"verified","locator":{"url":"http://example.com/url"}},"id":1}`, `{"result":false}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Flag(FlagRequest{Locator: store.Locator{URL: "http://example.com/url"}, Flag: Verified})
assert.NoError(t, err)
@@ -133,7 +133,7 @@ func TestRemote_Flag(t *testing.T) {
func TestRemote_ListFlag(t *testing.T) {
ts := testServer(t, `{"method":"store.list_flags","params":{"flag":"blocked","locator":{"site":"site_id","url":""}},"id":1}`, `{"result":[{"ID":"id1"},{"ID":"id2"}]}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.ListFlags(FlagRequest{Locator: store.Locator{SiteID: "site_id"}, Flag: Blocked})
assert.NoError(t, err)
assert.Equal(t, []interface{}{map[string]interface{}{"ID": "id1"}, map[string]interface{}{"ID": "id2"}}, res)
@@ -142,7 +142,7 @@ func TestRemote_ListFlag(t *testing.T) {
func TestRemote_Count(t *testing.T) {
ts := testServer(t, `{"method":"store.count","params":{"locator":{"url":"http://example.com/url"},"since":"0001-01-01T00:00:00Z"},"id":1}`, `{"result":11}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
res, err := c.Count(FindRequest{Locator: store.Locator{URL: "http://example.com/url"}})
assert.NoError(t, err)
@@ -153,7 +153,7 @@ func TestRemote_Delete(t *testing.T) {
ts := testServer(t, `{"method":"store.delete","params":{"locator":{"url":"http://example.com/url"},"del_mode":0},"id":1}`,
`{}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
err := c.Delete(DeleteRequest{Locator: store.Locator{URL: "http://example.com/url"}})
assert.NoError(t, err)
@@ -162,7 +162,7 @@ func TestRemote_Delete(t *testing.T) {
func TestRemote_Close(t *testing.T) {
ts := testServer(t, `{"method":"store.close","id":1}`, `{}`)
defer ts.Close()
- c := RPC{Client: rpc.Client{API: ts.URL, Client: http.Client{}}}
+ c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}}
err := c.Close()
assert.NoError(t, err)
}
diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go
index 937cbd3c..820b37fe 100644
--- a/backend/app/store/service/service.go
+++ b/backend/app/store/service/service.go
@@ -469,6 +469,7 @@ func (s *DataStore) ValidateComment(c *store.Comment) error {
func (s *DataStore) IsAdmin(siteID string, userID string) bool {
admins, err := s.AdminStore.Admins(siteID)
if err != nil {
+ log.Printf("[WARN] can't get admins for %s, %v", siteID, err)
return false
}
for _, a := range admins {
diff --git a/backend/go.mod b/backend/go.mod
index f8fdac22..4b78ea1f 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -9,12 +9,13 @@ require (
github.com/PuerkitoBio/goquery v1.5.0
github.com/coreos/bbolt v1.3.3
github.com/dgrijalva/jwt-go v3.2.0+incompatible
- github.com/didip/tollbooth v4.0.0+incompatible
+ github.com/didip/tollbooth v4.0.2+incompatible
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/cors v1.0.0
github.com/go-chi/render v1.0.1
github.com/go-pkgz/auth v0.7.2
+ github.com/go-pkgz/jrpc v0.1.0
github.com/go-pkgz/lcw v0.3.1
github.com/go-pkgz/lgr v0.6.3
github.com/go-pkgz/repeater v1.1.2
@@ -35,7 +36,7 @@ require (
github.com/stretchr/testify v1.3.0
golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9
- golang.org/x/net v0.0.0-20190628185345-da137c7871d7
+ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
gopkg.in/russross/blackfriday.v2 v2.0.1
)
diff --git a/backend/go.sum b/backend/go.sum
index 56a7f941..2936f18e 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -21,6 +21,8 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/didip/tollbooth v4.0.0+incompatible h1:ayQZYuF5QOxx3NdYRNuRVFLv9/2b64JtSUlewb+0TMo=
github.com/didip/tollbooth v4.0.0+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
+github.com/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
+github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d h1:vs5Nf6IE0N/PwGJ8//zRed4gpCdcr99K2HzX7RuLOQ8=
github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
github.com/globalsign/mgo v0.0.0-20180615134936-113d3961e731/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
@@ -34,6 +36,8 @@ github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
github.com/go-pkgz/auth v0.7.2 h1:+LvAgqwQtYuWphpZE8qLtspVd65+VgreJkFLsKrtmmk=
github.com/go-pkgz/auth v0.7.2/go.mod h1:ibOpZYISiaOvAHe2bsKj2s3v4AkMam2WxxIFn+zhulo=
+github.com/go-pkgz/jrpc v0.1.0 h1:hNg/IyfEqJcSWOKkuHw0ZwcuGc9TDp7QZREsD2ycmiM=
+github.com/go-pkgz/jrpc v0.1.0/go.mod h1:JxZsvoBklA50DNhELVJnJ567Rt+KrMH9rR3u515wvE8=
github.com/go-pkgz/lcw v0.3.1 h1:PhfB0xNUawLMlx5rXvOTIc7d5LMrr1GM9vIzmG96aUI=
github.com/go-pkgz/lcw v0.3.1/go.mod h1:k+PY1CkCMTLXILtFoJOyK65Qqi9rkoTYunFH1vE/C0I=
github.com/go-pkgz/lgr v0.2.2/go.mod h1:hBM1NM/SoYdlrykgdgJWGrZ/TM/XaZIjRbJfx7NkMm8=
@@ -115,6 +119,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.4.0 h1:rJf4ndpvRDS7tOrtIpVkacItTxF5pVpXvDqtb1dX40M=
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -153,6 +158,8 @@ golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU=
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
diff --git a/backend/nginx-proxy-example.conf b/backend/nginx-proxy-example.conf
index fabd0ae7..32d96ec3 100644
--- a/backend/nginx-proxy-example.conf
+++ b/backend/nginx-proxy-example.conf
@@ -14,22 +14,6 @@ server {
limit_conn perip 10;
location / {
-
- #CORS
- add_header 'Access-Control-Allow-Origin' '*';
- add_header 'Access-Control-Allow-Methods' 'GET, PUT, POST, DELETE, OPTIONS';
- add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With";
- if ($request_method = 'OPTIONS') {
- add_header 'Access-Control-Allow-Origin' '*';
- add_header 'Access-Control-Allow-Methods' 'GET, PUT, POST, DELETE, OPTIONS';
- add_header 'Access-Control-Max-Age' 86400;
- add_header 'Access-Control-Allow-Credentials' 'true';
- add_header 'Access-Control-Allow-Headers' 'Authorization,DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
- add_header 'Content-Length' 0;
- add_header 'Content-Type' 'text/plain; charset=UTF-8';
- return 204;
- }
-
proxy_redirect off;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
diff --git a/backend/vendor/github.com/didip/tollbooth/README.md b/backend/vendor/github.com/didip/tollbooth/README.md
index d1971b84..f6145879 100644
--- a/backend/vendor/github.com/didip/tollbooth/README.md
+++ b/backend/vendor/github.com/didip/tollbooth/README.md
@@ -7,9 +7,7 @@ This is a generic middleware to rate-limit HTTP requests.
**NOTE 1:** This library is considered finished.
-**NOTE 2:** In the coming weeks, I will be removing thirdparty modules and moving them to their own dedicated repos.
-
-**NOTE 3:** Major version changes are backward-incompatible. `v2.0.0` streamlines the ugliness of the old API.
+**NOTE 2:** Major version changes are backward-incompatible. `v2.0.0` streamlines the ugliness of the old API.
## Versions
@@ -21,7 +19,7 @@ This is a generic middleware to rate-limit HTTP requests.
**v3.x.x:** Apparently we have been using golang.org/x/time/rate incorrectly. See issue #48. It always limit X number per 1 second. The time duration is not changeable, so it does not make sense to pass TTL to tollbooth.
-## Five Minutes Tutorial
+## Five Minute Tutorial
```go
package main
@@ -161,4 +159,6 @@ Sometimes, other frameworks require a little bit of shim to use Tollbooth. These
* [Stopwatch](https://github.com/didip/stopwatch): A small library to measure latency of things. Useful if you want to report latency data to Graphite.
+* [LaborUnion](https://github.com/didip/laborunion): A dynamic worker pool library.
+
* [Gomet](https://github.com/didip/gomet): Simple HTTP client & server long poll library for Go. Useful for receiving live updates without needing Websocket.
diff --git a/backend/vendor/github.com/didip/tollbooth/tollbooth.go b/backend/vendor/github.com/didip/tollbooth/tollbooth.go
index c023db26..b4f7b3e8 100644
--- a/backend/vendor/github.com/didip/tollbooth/tollbooth.go
+++ b/backend/vendor/github.com/didip/tollbooth/tollbooth.go
@@ -6,10 +6,11 @@ import (
"strings"
"fmt"
+ "math"
+
"github.com/didip/tollbooth/errors"
"github.com/didip/tollbooth/libstring"
"github.com/didip/tollbooth/limiter"
- "math"
)
// setResponseHeaders configures X-Rate-Limit-Limit and X-Rate-Limit-Duration
@@ -58,18 +59,21 @@ func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
if libstring.StringInSlice(lmtMethods, r.Method) {
for headerKey, headerValues := range lmtHeaders {
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
- // If header values are empty, rate-limit all request with headerKey.
+ // If header values are empty, rate-limit all request containing headerKey.
username, _, ok := r.BasicAuth()
if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
- sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, username})
+ sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, r.Header.Get(headerKey), username})
}
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
// If header values are not empty, rate-limit all request with headerKey and headerValues.
for _, headerValue := range headerValues {
- username, _, ok := r.BasicAuth()
- if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
- sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue, username})
+ if r.Header.Get(headerKey) == headerValue {
+ username, _, ok := r.BasicAuth()
+ if ok && libstring.StringInSlice(lmtBasicAuthUsers, username) {
+ sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue, username})
+ }
+ break
}
}
}
@@ -82,12 +86,15 @@ func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
for headerKey, headerValues := range lmtHeaders {
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
// If header values are empty, rate-limit all request with headerKey.
- sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey})
+ sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, r.Header.Get(headerKey)})
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
- // If header values are not empty, rate-limit all request with headerKey and headerValues.
+ // We are only limiting if request's header value is defined inside `headerValues`.
for _, headerValue := range headerValues {
- sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue})
+ if r.Header.Get(headerKey) == headerValue {
+ sliceKeys = append(sliceKeys, []string{remoteIP, path, r.Method, headerKey, headerValue})
+ break
+ }
}
}
}
@@ -113,12 +120,15 @@ func BuildKeys(lmt *limiter.Limiter, r *http.Request) [][]string {
for headerKey, headerValues := range lmtHeaders {
if (headerValues == nil || len(headerValues) <= 0) && r.Header.Get(headerKey) != "" {
// If header values are empty, rate-limit all request with headerKey.
- sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey})
+ sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey, r.Header.Get(headerKey)})
} else if len(headerValues) > 0 && r.Header.Get(headerKey) != "" {
// If header values are not empty, rate-limit all request with headerKey and headerValues.
for _, headerValue := range headerValues {
- sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey, headerValue})
+ if r.Header.Get(headerKey) == headerValue {
+ sliceKeys = append(sliceKeys, []string{remoteIP, path, headerKey, headerValue})
+ break
+ }
}
}
}
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/.gitignore b/backend/vendor/github.com/go-pkgz/jrpc/.gitignore
new file mode 100644
index 00000000..f1c181ec
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/.gitignore
@@ -0,0 +1,12 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, build with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/.golangci.yml b/backend/vendor/github.com/go-pkgz/jrpc/.golangci.yml
new file mode 100644
index 00000000..bb83e9ae
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/.golangci.yml
@@ -0,0 +1,70 @@
+linters-settings:
+ govet:
+ check-shadowing: true
+ golint:
+ min-confidence: 0
+ gocyclo:
+ min-complexity: 15
+ maligned:
+ suggest-new: true
+ dupl:
+ threshold: 100
+ goconst:
+ min-len: 2
+ min-occurrences: 2
+ misspell:
+ locale: US
+ lll:
+ line-length: 140
+ gocritic:
+ enabled-tags:
+ - performance
+ - style
+ - experimental
+ disabled-checks:
+ - wrapperFunc
+
+linters:
+ disable-all: true
+ enable:
+ - megacheck
+ - golint
+ - govet
+ - unconvert
+ - megacheck
+ - structcheck
+ - gas
+ - gocyclo
+ - dupl
+ - misspell
+ - unparam
+ - varcheck
+ - deadcode
+ - typecheck
+ - ineffassign
+ - varcheck
+ - stylecheck
+ - gochecknoinits
+ - scopelint
+ - gocritic
+ - golint
+ - nakedret
+ - gosimple
+ - prealloc
+ fast: false
+
+
+run:
+# modules-download-mode: vendor
+ skip-dirs:
+ - vendor
+ tests: true
+
+issues:
+ exclude-rules:
+ - text: "weak cryptographic primitive"
+ linters:
+ - gosec
+
+service:
+ golangci-lint-version: 1.17.x
\ No newline at end of file
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/.travis.yml b/backend/vendor/github.com/go-pkgz/jrpc/.travis.yml
new file mode 100644
index 00000000..0f0c5bdc
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/.travis.yml
@@ -0,0 +1,21 @@
+language: go
+
+go:
+ - "1.12.x"
+
+install: true
+
+before_install:
+ - export TZ=America/Chicago
+ - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.17.1
+ - go get github.com/mattn/goveralls
+ - export PATH=$(pwd)/bin:$PATH
+
+script:
+ - GO111MODULE=on go get ./...
+ - GO111MODULE=on go mod vendor
+ - GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
+ - GO111MODULE=on go test -v -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1;
+ - golangci-lint run -v || travis_terminate 1;
+ - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci
+
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/LICENSE b/backend/vendor/github.com/go-pkgz/jrpc/LICENSE
new file mode 100644
index 00000000..ac540250
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Umputun
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/README.md b/backend/vendor/github.com/go-pkgz/jrpc/README.md
new file mode 100644
index 00000000..a3e36c9b
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/README.md
@@ -0,0 +1,99 @@
+# jrpc - rpc with json [](https://travis-ci.org/go-pkgz/jrpc) [](https://goreportcard.com/report/github.com/go-pkgz/jrpc) [](https://coveralls.io/github/go-pkgz/jrpc?branch=master) [](https://godoc.org/github.com/go-pkgz/jrpc)
+
+jrpc library provides client and server for RPC-like communication over HTTP with json encoded messages.
+The protocol is a somewhat simplified version of json-rpc with a single POST call sending Request json
+(method name and the list of parameters) moreover, receiving json Response with result data and an error string.
+
+## Usage
+
+### Plugin (server)
+
+```go
+// Server wraps jrpc.Server and adds synced map to store data
+type Puglin struct {
+ *jrpc.Server
+}
+
+// create plugin (jrpc server)
+plugin := jrpcServer{
+ Server: &jrpc.Server{
+ API: "/command", // base url for rpc calls
+ AuthUser: "user", // basic auth user name
+ AuthPasswd: "password", // basic auth password
+ AppName: "jrpc-example", // plugin name for headers
+ Logger: logger,
+ },
+}
+
+plugin.Add("mycommand", func(id uint64, params json.RawMessage) Response {
+ return jrpc.EncodeResponse(id, "hello, it works", nil)
+})
+```
+
+### Application (client)
+
+```go
+// Client makes jrpc.Client and invoke remote call
+rpcClient := jrpc.Client{
+ API: "http://127.0.0.1:8080/command",
+ Client: http.Client{},
+ AuthUser: "user",
+ AuthPasswd: "password",
+}
+
+resp, err := rpcClient.Call("mycommand")
+var message string
+if err = json.Unmarshal(*resp.Result, &message); err != nil {
+ panic(err)
+}
+```
+
+*for functional examples for both plugin and application see [_example](https://github.com/go-pkgz/jrpc/tree/master/_example)*
+
+## Technical details
+
+ * `jrpc.Server` runs on user-defined port as a regular http server
+ * Server accepts a single POST request on user-defined url with [Request](https://github.com/go-pkgz/jrpc/blob/master/jrpc.go#L12) sent as json payload
+ request details and an example:
+
+ ```go
+ type Request struct {
+ Method string `json:"method"`
+ Params interface{} `json:"params,omitempty"`
+ ID uint64 `json:"id"`
+ }
+ ```
+ example:
+
+ ```json
+ {
+ "method":"test",
+ "params":[123,"abc"],
+ "id":1
+ }
+ ```
+
+* Params can be a struct, primitive type or slice of values, even with different types.
+* Server defines `ServerFn` handler function to react on a POST request. The handler provided by the user.
+* Communication between the server and the caller can be protected with basic auth.
+* [Client](https://github.com/go-pkgz/jrpc/blob/master/client.go) provides a single method `Call` and return `Response`
+
+ response details:
+
+ ```go
+ // Response encloses result and error received from remote server
+ type Response struct {
+ Result *json.RawMessage `json:"result,omitempty"`
+ Error string `json:"error,omitempty"`
+ ID uint64 `json:"id"`
+ }
+ ```
+
+* User should encode and decode json payloads on the application level, see provided [examples](https://github.com/go-pkgz/jrpc/tree/master/_example)
+* `jrpc.Server` doesn't support https internally (yet). If used on exposed or non-private networks, should be proxied with something providing https termination (nginx and others).
+
+## Status
+
+The code was extracted from [remark42](https://github.com/umputun/remark) and still under development. Until v1.x released the
+ API & protocol may change.
+
\ No newline at end of file
diff --git a/backend/app/rpc/client.go b/backend/vendor/github.com/go-pkgz/jrpc/client.go
similarity index 65%
rename from backend/app/rpc/client.go
rename to backend/vendor/github.com/go-pkgz/jrpc/client.go
index 5cef2a16..b9a40573 100644
--- a/backend/app/rpc/client.go
+++ b/backend/vendor/github.com/go-pkgz/jrpc/client.go
@@ -1,26 +1,29 @@
-package rpc
+package jrpc
import (
"bytes"
"encoding/json"
"net/http"
- "reflect"
"sync/atomic"
"github.com/pkg/errors"
)
// Client implements remote engine and delegates all calls to remote http server
+// if AuthUser and AuthPasswd defined will be used for basic auth in each call to server
type Client struct {
- API string
- Client http.Client
- AuthUser string
- AuthPasswd string
+ API string // URL to jrpc server with entrypoint, i.e. http://127.0.0.1:8080/command
+ Client http.Client // http client injected by user
+ AuthUser string // basic auth user name, should match Server.AuthUser, optional
+ AuthPasswd string // basic auth password, should match Server.AuthPasswd, optional
- id uint64
+ id uint64 // used with atomic to populate unique id to Request.ID
}
-// Call remote server with given method and arguments
+// Call remote server with given method and arguments.
+// Empty args will be ignored, single arg will be marshaled as-us and multiple args marshaled as []interface{}.
+// Returns Response and error. Note: Response has it's own Error field, but that onw controlled by server.
+// Returned error represent client-level errors, like failed http call, failed marshaling and so on.
func (r *Client) Call(method string, args ...interface{}) (*Response, error) {
var b []byte
@@ -32,7 +35,7 @@ func (r *Client) Call(method string, args ...interface{}) (*Response, error) {
if err != nil {
return nil, errors.Wrapf(err, "marshaling failed for %s", method)
}
- case len(args) == 1 && reflect.TypeOf(args[0]).Kind() == reflect.Struct:
+ case len(args) == 1:
b, err = json.Marshal(Request{Method: method, Params: args[0], ID: atomic.AddUint64(&r.id, 1)})
if err != nil {
return nil, errors.Wrapf(err, "marshaling failed for %s", method)
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/go.mod b/backend/vendor/github.com/go-pkgz/jrpc/go.mod
new file mode 100644
index 00000000..1e3fdb2a
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/go.mod
@@ -0,0 +1,16 @@
+module github.com/go-pkgz/jrpc
+
+go 1.12
+
+require (
+ github.com/didip/tollbooth v4.0.2+incompatible
+ github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d
+ github.com/go-chi/chi v4.0.2+incompatible
+ github.com/go-chi/render v1.0.1
+ github.com/go-pkgz/rest v1.4.1
+ github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
+ github.com/pkg/errors v0.8.1
+ github.com/stretchr/testify v1.3.0
+ golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 // indirect
+ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
+)
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/go.sum b/backend/vendor/github.com/go-pkgz/jrpc/go.sum
new file mode 100644
index 00000000..9a20171c
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/go.sum
@@ -0,0 +1,32 @@
+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/didip/tollbooth v4.0.2+incompatible h1:fVSa33JzSz0hoh2NxpwZtksAzAgd7zjmGO20HCZtF4M=
+github.com/didip/tollbooth v4.0.2+incompatible/go.mod h1:A9b0665CE6l1KmzpDws2++elm/CsuWBMa5Jv4WY0PEY=
+github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d h1:vs5Nf6IE0N/PwGJ8//zRed4gpCdcr99K2HzX7RuLOQ8=
+github.com/didip/tollbooth_chi v0.0.0-20170928041846-6ab5f3083f3d/go.mod h1:YWyIfq3y4ArRfWZ9XksmuusP+7Mad+T0iFZ0kv0XG/M=
+github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
+github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8=
+github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns=
+github.com/go-pkgz/rest v1.4.1 h1:DmaVLPH2O7yLehrWOW0uz01d2mVHz9fBR/iuTiPRzaw=
+github.com/go-pkgz/rest v1.4.1/go.mod h1:COazNj35u3RXAgQNBr6neR599tYP3URiOpsu9p0rOtk=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
+github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/backend/vendor/github.com/go-pkgz/jrpc/jrpc.go b/backend/vendor/github.com/go-pkgz/jrpc/jrpc.go
new file mode 100644
index 00000000..86063690
--- /dev/null
+++ b/backend/vendor/github.com/go-pkgz/jrpc/jrpc.go
@@ -0,0 +1,40 @@
+// Package jrpc implements client and server for RPC-like communication over HTTP with json encoded messages.
+// The protocol is somewhat simplified version of json-rpc with a single POST call sending Request json
+// (method name and the list of parameters) and receiving back json Response with "result" json
+// and error string
+package jrpc
+
+import (
+ "encoding/json"
+)
+
+// Request encloses method name and all params
+type Request struct {
+ Method string `json:"method"` // method (function) name
+ Params interface{} `json:"params,omitempty"` // function arguments
+ ID uint64 `json:"id"` // unique call id
+}
+
+// Response encloses result and error received from remote server
+type Response struct {
+ Result *json.RawMessage `json:"result,omitempty"` // response json
+ Error string `json:"error,omitempty"` // optional remote (server side / plugin side) error
+ ID uint64 `json:"id"` // unique call id, echoed Request.ID to allow calls tracing
+}
+
+// EncodeResponse convert anything (type interface{}) and incoming error (if any) to Response
+func EncodeResponse(id uint64, resp interface{}, e error) Response {
+ v, err := json.Marshal(&resp)
+ if err != nil {
+ return Response{Error: err.Error()}
+ }
+ if e != nil {
+ return Response{ID: id, Result: nil, Error: e.Error()} // pass input error
+ }
+ raw := json.RawMessage{}
+ if err := raw.UnmarshalJSON(v); err != nil {
+ return Response{Error: err.Error()}
+ }
+
+ return Response{ID: id, Result: &raw}
+}
diff --git a/backend/app/rpc/server.go b/backend/vendor/github.com/go-pkgz/jrpc/server.go
similarity index 58%
rename from backend/app/rpc/server.go
rename to backend/vendor/github.com/go-pkgz/jrpc/server.go
index 55d95866..a52e1434 100644
--- a/backend/app/rpc/server.go
+++ b/backend/vendor/github.com/go-pkgz/jrpc/server.go
@@ -1,4 +1,4 @@
-package rpc
+package jrpc
import (
"context"
@@ -13,21 +13,19 @@ import (
"github.com/go-chi/chi"
"github.com/go-chi/chi/middleware"
"github.com/go-chi/render"
- log "github.com/go-pkgz/lgr"
- R "github.com/go-pkgz/rest"
+ "github.com/go-pkgz/rest"
"github.com/go-pkgz/rest/logger"
"github.com/pkg/errors"
-
- "github.com/umputun/remark/backend/app/rest"
)
// Server is json-rpc server with an optional basic auth
type Server struct {
- API string
- AuthUser string
- AuthPasswd string
- Version string
- AppName string
+ API string // url path, i.e. "/command" or "/rpc" etc.
+ AuthUser string // basic auth user name, should match Client.AuthUser, optional
+ AuthPasswd string // basic auth password, should match Client.AuthPasswd, optional
+ Version string // server version, injected from main and used for informational headers only
+ AppName string // plugin name, injected from main and used for informational headers only
+ Logger L // logger, if nil will default to NoOpLogger
funcs struct {
m map[string]ServerFn
@@ -40,26 +38,26 @@ type Server struct {
}
}
-// Encoder is a function to encode call's result to Response
-type Encoder func(id uint64, resp interface{}, e error) (Response, error)
-
-// ServerFn handler registered for each method with Add
-// Implementations provided by consumer and define response logic.
+// ServerFn handler registered for each method with Add or Group.
+// Implementations provided by consumer and defines response logic.
type ServerFn func(id uint64, params json.RawMessage) Response
// Run http server on given port
func (s *Server) Run(port int) error {
+ if s.Logger == nil {
+ s.Logger = NoOpLogger
+ }
if s.AuthUser == "" || s.AuthPasswd == "" {
- log.Print("[WARN] extension server runs without auth")
+ s.Logger.Logf("[WARN] extension server runs without auth")
}
if s.funcs.m == nil && len(s.funcs.m) == 0 {
return errors.Errorf("nothing mapped for dispatch, Add has to be called prior to Run")
}
router := chi.NewRouter()
- router.Use(middleware.Throttle(1000), middleware.RealIP, R.Recoverer(log.Default()))
- router.Use(R.AppInfo(s.AppName, "umputun", s.Version), R.Ping)
- logInfoWithBody := logger.New(logger.Log(log.Default()), logger.WithBody, logger.Prefix("[INFO]")).Handler
+ router.Use(middleware.Throttle(1000), middleware.RealIP, rest.Recoverer(s.Logger))
+ router.Use(rest.AppInfo(s.AppName, "umputun", s.Version), rest.Ping)
+ logInfoWithBody := logger.New(logger.Log(s.Logger), logger.WithBody, logger.Prefix("[INFO]")).Handler
router.Use(middleware.Timeout(5 * time.Second))
router.Use(logInfoWithBody, tollbooth_chi.LimitHandler(tollbooth.NewLimiter(1000, nil)), middleware.NoCache)
router.Use(s.basicAuth)
@@ -76,26 +74,10 @@ func (s *Server) Run(port int) error {
}
s.httpServer.Unlock()
- log.Printf("[INFO] listen on %d", port)
+ s.Logger.Logf("[INFO] listen on %d", port)
return s.httpServer.ListenAndServe()
}
-// EncodeResponse convert anything to Response
-func (s *Server) EncodeResponse(id uint64, resp interface{}, e error) (Response, error) {
- v, err := json.Marshal(&resp)
- if err != nil {
- return Response{}, err
- }
- if e != nil {
- return Response{ID: id, Result: nil, Error: e.Error()}, nil
- }
- raw := json.RawMessage{}
- if err = raw.UnmarshalJSON(v); err != nil {
- return Response{}, err
- }
- return Response{ID: id, Result: &raw}, nil
-}
-
// Shutdown http server
func (s *Server) Shutdown() error {
s.httpServer.Lock()
@@ -108,12 +90,12 @@ func (s *Server) Shutdown() error {
return s.httpServer.Shutdown(ctx)
}
-// Add method handler
+// Add method handler. Handler will be called on matching method (Request.Method)
func (s *Server) Add(method string, fn ServerFn) {
s.httpServer.Lock()
defer s.httpServer.Unlock()
if s.httpServer.Server != nil {
- log.Printf("[WARN] ignored method %s, can't be added to activated server", method)
+ s.Logger.Logf("[WARN] ignored method %s, can't be added to activated server", method)
return
}
@@ -122,19 +104,20 @@ func (s *Server) Add(method string, fn ServerFn) {
})
s.funcs.m[method] = fn
- log.Printf("[INFO] add handler for %s", method)
+ s.Logger.Logf("[INFO] add handler for %s", method)
}
// HandlersGroup alias for map of handlers
type HandlersGroup map[string]ServerFn
-// Group of handlers with common prefix
+// Group of handlers with common prefix, match on group.method
func (s *Server) Group(prefix string, m HandlersGroup) {
for k, v := range m {
s.Add(prefix+"."+k, v)
}
}
+// handler is http handler multiplexing calls by req.Method
func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
req := struct {
ID uint64 `json:"id"`
@@ -143,12 +126,12 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
}{}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
- rest.SendErrorJSON(w, r, http.StatusBadRequest, err, req.Method, 0)
+ rest.SendErrorJSON(w, r, s.Logger, http.StatusBadRequest, err, req.Method)
return
}
fn, ok := s.funcs.m[req.Method]
if !ok {
- rest.SendErrorJSON(w, r, http.StatusNotImplemented, errors.New("unsupported method"), req.Method, 0)
+ rest.SendErrorJSON(w, r, s.Logger, http.StatusNotImplemented, errors.New("unsupported method"), req.Method)
return
}
@@ -160,6 +143,7 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, fn(req.ID, params))
}
+// basicAuth middleware. enabled only if both AuthUser and AuthPasswd defined.
func (s *Server) basicAuth(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -177,3 +161,17 @@ func (s *Server) basicAuth(h http.Handler) http.Handler {
h.ServeHTTP(w, r)
})
}
+
+// L defined logger interface used for an optional rest logging
+type L interface {
+ Logf(format string, args ...interface{})
+}
+
+// LoggerFunc type is an adapter to allow the use of ordinary functions as Logger.
+type LoggerFunc func(format string, args ...interface{})
+
+// Logf calls f(id)
+func (f LoggerFunc) Logf(format string, args ...interface{}) { f(format, args...) }
+
+// NoOpLogger logger does nothing
+var NoOpLogger = LoggerFunc(func(format string, args ...interface{}) {})
diff --git a/backend/vendor/modules.txt b/backend/vendor/modules.txt
index 582f708d..f501d4fb 100644
--- a/backend/vendor/modules.txt
+++ b/backend/vendor/modules.txt
@@ -10,7 +10,7 @@ github.com/coreos/bbolt
github.com/davecgh/go-spew/spew
# github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/dgrijalva/jwt-go
-# github.com/didip/tollbooth v4.0.0+incompatible
+# github.com/didip/tollbooth v4.0.2+incompatible
github.com/didip/tollbooth
github.com/didip/tollbooth/errors
github.com/didip/tollbooth/libstring
@@ -38,6 +38,8 @@ github.com/go-pkgz/auth/provider/sender
github.com/go-pkgz/auth/token
github.com/go-pkgz/auth/logger
github.com/go-pkgz/auth/middleware
+# github.com/go-pkgz/jrpc v0.1.0
+github.com/go-pkgz/jrpc
# github.com/go-pkgz/lcw v0.3.1
github.com/go-pkgz/lcw
# github.com/go-pkgz/lgr v0.6.3
@@ -98,7 +100,7 @@ golang.org/x/crypto/acme
# golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9
golang.org/x/image/draw
golang.org/x/image/math/f64
-# golang.org/x/net v0.0.0-20190628185345-da137c7871d7
+# golang.org/x/net v0.0.0-20190724013045-ca1201d0de80
golang.org/x/net/html
golang.org/x/net/idna
golang.org/x/net/html/atom