diff --git a/backend/_example/memory_store/accessor/admin.go b/backend/_example/memory_store/accessor/admin.go index 70581bfa..59a5f0d2 100644 --- a/backend/_example/memory_store/accessor/admin.go +++ b/backend/_example/memory_store/accessor/admin.go @@ -9,6 +9,7 @@ package accessor import ( log "github.com/go-pkgz/lgr" "github.com/pkg/errors" + "github.com/umputun/remark/backend/app/store/admin" ) // MemAdmin implements admin.Store with memory backend @@ -19,10 +20,11 @@ type MemAdmin struct { // AdminRec is a records per site with all admin info in type AdminRec struct { - SiteID string - IDs []string // admin ids - Email string // admin email - Enabled bool // site enabled + SiteID string + IDs []string // admin ids + Email string // admin email + Enabled bool // site enabled + CountCreated int64 // number of created posts } // NewMemAdminStore makes admin Store in memory @@ -65,6 +67,19 @@ func (m *MemAdmin) Enabled(siteID string) (ok bool, err error) { return resp.Enabled, nil } +// OnEvent reacts on events from updates, created, delete and vote +func (m *MemAdmin) OnEvent(siteID string, ev admin.EventType) error { + resp, ok := m.data[siteID] + if !ok { + return errors.Errorf("site %s not found", siteID) + } + if ev == admin.EvCreate { + resp.CountCreated += 1 // not a good idea, just for demo + m.data[siteID] = resp + } + return nil +} + // Set admin data for siteID func (m *MemAdmin) Set(siteID string, arec AdminRec) { m.data[siteID] = arec diff --git a/backend/_example/memory_store/accessor/admin_test.go b/backend/_example/memory_store/accessor/admin_test.go index e05bc956..f5eb3261 100644 --- a/backend/_example/memory_store/accessor/admin_test.go +++ b/backend/_example/memory_store/accessor/admin_test.go @@ -20,9 +20,9 @@ func TestMemAdmin_Get(t *testing.T) { var ms admin.Store = adm adm.data = map[string]AdminRec{ - "site1": {"site1", []string{"i11", "i12"}, "e1", true}, - "site2": {"site2", []string{"i21", "i22"}, "e2", true}, - "site3": {"site3", []string{"i21", "i22"}, "e3", false}, + "site1": {"site1", []string{"i11", "i12"}, "e1", true, 0}, + "site2": {"site2", []string{"i21", "i22"}, "e2", true, 0}, + "site3": {"site3", []string{"i21", "i22"}, "e3", false, 0}, } admins, err := ms.Admins("site1") diff --git a/backend/_example/memory_store/server/rpc.go b/backend/_example/memory_store/server/rpc.go index 20cf03a6..0253ba64 100644 --- a/backend/_example/memory_store/server/rpc.go +++ b/backend/_example/memory_store/server/rpc.go @@ -46,6 +46,7 @@ func (s *RPC) addHandlers() { "admins": s.admAdminsHndl, "email": s.admEmailHndl, "enabled": s.admEnabledHndl, + "event": s.admEventHndl, }) } @@ -196,3 +197,25 @@ func (s *RPC) admEnabledHndl(id uint64, params json.RawMessage) (rr jrpc.Respons } return jrpc.EncodeResponse(id, ok, err) } + +// onEvent returns nothing, callback to OnEvent +func (s *RPC) admEventHndl(id uint64, params json.RawMessage) (rr jrpc.Response) { + var siteID string + ps := []interface{}{} + if err := json.Unmarshal(params, &ps); err != nil { + return jrpc.Response{Error: err.Error()} + } + siteID, ok := ps[0].(string) + if !ok { + return jrpc.Response{Error: "wrong siteID type"} + } + evType, ok := ps[1].(float64) + if !ok { + return jrpc.Response{Error: "wrong event type"} + } + err := s.adm.OnEvent(siteID, admin.EventType(evType)) + if err != nil { + return jrpc.Response{Error: err.Error()} + } + return jrpc.EncodeResponse(id, nil, err) +} diff --git a/backend/_example/memory_store/server/rpc_test.go b/backend/_example/memory_store/server/rpc_test.go index f0c5194a..33b4ee91 100644 --- a/backend/_example/memory_store/server/rpc_test.go +++ b/backend/_example/memory_store/server/rpc_test.go @@ -311,6 +311,20 @@ func TestRPC_admEnabledHndl(t *testing.T) { assert.Equal(t, false, ok) } + +func TestRPC_admEventHndl(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.OnEvent("bad site", admin.EvCreate) + assert.EqualError(t, err, "site bad site not found") + + err = ra.OnEvent("test-site", admin.EvCreate) + assert.NoError(t, err) +} + func prepTestStore(t *testing.T) (s *RPC, port int, teardown func()) { port = 40000 + int(rand.Int31n(10000)) diff --git a/backend/app/store/admin/admin.go b/backend/app/store/admin/admin.go index c369325e..3e6052a5 100644 --- a/backend/app/store/admin/admin.go +++ b/backend/app/store/admin/admin.go @@ -14,8 +14,20 @@ type Store interface { Admins(siteID string) (ids []string, err error) Email(siteID string) (email string, err error) Enabled(siteID string) (ok bool, err error) + OnEvent(siteID string, et EventType) error } +// EventType indicates type of the event +type EventType int + +// enum of all event types +const ( + EvCreate EventType = iota + EvDelete + EvUpdate + EvVote +) + // StaticStore implements keys.Store with a single set of admins and email for all sites type StaticStore struct { admins []string @@ -65,3 +77,6 @@ func (s *StaticStore) Enabled(site string) (ok bool, err error) { } return false, nil } + +// OnEvent doesn nothing for StaticStore +func (s *StaticStore) OnEvent(siteID string, et EventType) error { return nil } diff --git a/backend/app/store/admin/remote.go b/backend/app/store/admin/remote.go index a5f4a83f..5797d18b 100644 --- a/backend/app/store/admin/remote.go +++ b/backend/app/store/admin/remote.go @@ -66,3 +66,12 @@ func (r *RPC) Enabled(siteID string) (ok bool, err error) { } return ok, nil } + +// OnEvent reacts (register) events about data modification +func (r *RPC) OnEvent(siteID string, et EventType) error { + _, err := r.Call("admin.event", siteID, et) + if err != nil { + return err + } + return nil +} diff --git a/backend/app/store/admin/remote_test.go b/backend/app/store/admin/remote_test.go index 88b7d645..f9d2722f 100644 --- a/backend/app/store/admin/remote_test.go +++ b/backend/app/store/admin/remote_test.go @@ -78,6 +78,18 @@ func TestRemote_Enables(t *testing.T) { t.Logf("%v %T", res, res) } +func TestRemote_OnEvent(t *testing.T) { + ts := testServer(t, `{"method":"admin.event","params":["site-1",2],"id":1}`, `{"id":1}`) + defer ts.Close() + c := RPC{Client: jrpc.Client{API: ts.URL, Client: http.Client{}}} + + var a Store = &c + _ = a + + err := c.OnEvent("site-1", EvUpdate) + assert.NoError(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) diff --git a/backend/app/store/service/service.go b/backend/app/store/service/service.go index cf1b11ef..97adcb57 100644 --- a/backend/app/store/service/service.go +++ b/backend/app/store/service/service.go @@ -102,6 +102,10 @@ func (s *DataStore) Create(comment store.Comment) (commentID string, err error) }() s.submitImages(comment) + if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvCreate); e != nil { + log.Printf("[WARN] failed to send create event, %s", e) + } + return s.Engine.Create(comment) } @@ -306,6 +310,10 @@ func (s *DataStore) Vote(req VoteReq) (comment store.Comment, err error) { } } + if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvVote); e != nil { + log.Printf("[WARN] failed to send vote event, %s", e) + } + comment.Controversy = s.controversy(s.upsAndDowns(comment)) comment.Locator = req.Locator return comment, s.Engine.Update(comment) @@ -386,6 +394,10 @@ func (s *DataStore) EditComment(locator store.Locator, commentID string, req Edi comment.Locator = locator comment.Sanitize() + if e := s.AdminStore.OnEvent(comment.Locator.SiteID, admin.EvUpdate); e != nil { + log.Printf("[WARN] failed to send update event, %s", e) + } + err = s.Engine.Update(comment) return comment, err } @@ -608,6 +620,9 @@ func (s *DataStore) Info(locator store.Locator, readonlyAge int) (store.PostInfo // Delete comment by id func (s *DataStore) Delete(locator store.Locator, commentID string, mode store.DeleteMode) error { + if e := s.AdminStore.OnEvent(locator.SiteID, admin.EvDelete); e != nil { + log.Printf("[WARN] failed to send delete event, %s", e) + } req := engine.DeleteRequest{Locator: locator, CommentID: commentID, DeleteMode: mode} return s.Engine.Delete(req) }