@@ -20,3 +20,4 @@ remark42
|
||||
/bin/
|
||||
/backend/var/
|
||||
compose-private-backend.yml
|
||||
/backend/_example/*/vendor
|
||||
|
||||
@@ -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"]
|
||||
@@ -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.
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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, <a href="http://radio-t.com">link</a>`, res[0].Text)
|
||||
assert.Equal(t, "user1", res[0].User.ID)
|
||||
t.Log(res[0].ID)
|
||||
|
||||
_, err = 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, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/ro", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
|
||||
flagReq := 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, <a href="http://radio-t.com">link</a>`, 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, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 24, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err := b.Create(comment)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := 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, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com/2", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err := b.Create(comment)
|
||||
assert.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, <a href="http://radio-t.com">link</a>`,
|
||||
Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local),
|
||||
Locator: store.Locator{URL: "https://radio-t.com", SiteID: "radio-t"},
|
||||
User: store.User{ID: "user1", Name: "user name"},
|
||||
}
|
||||
_, err := 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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}))
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
+3
-2
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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;
|
||||
|
||||
+4
-4
@@ -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.
|
||||
|
||||
+21
-11
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+12
@@ -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
|
||||
+70
@@ -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
|
||||
+21
@@ -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
|
||||
|
||||
+21
@@ -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.
|
||||
+99
@@ -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
|
||||
<details><summary>request details and an example:</summary>
|
||||
|
||||
```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
|
||||
}
|
||||
```
|
||||
</details>
|
||||
* 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`
|
||||
|
||||
<details><summary>response details:</summary>
|
||||
|
||||
```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"`
|
||||
}
|
||||
```
|
||||
</details>
|
||||
* 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.
|
||||
|
||||
+12
-9
@@ -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)
|
||||
+16
@@ -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
|
||||
)
|
||||
+32
@@ -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=
|
||||
+40
@@ -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}
|
||||
}
|
||||
+40
-42
@@ -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{}) {})
|
||||
Vendored
+4
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user