diff --git a/README.md b/README.md index 80373b90..031da578 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,7 @@ Sort can be `time` or `score`. Supported sort order with prefix -/+, i.e. `-time } ``` - `PUT /api/v1/notify?site=site-id&url=post-url&action=1` - subscribe(action=1)/unsubscribe(0) comment notifications. _auth required_ +- `GET /api/v1/notify?site=site-id&url=post-url` - get subscription status. _auth required_ ### Admin diff --git a/app/main.go b/app/main.go index f99fe741..f17f89c3 100644 --- a/app/main.go +++ b/app/main.go @@ -116,7 +116,7 @@ func main() { AuthProviders: makeAuthProviders(sessionStore, avatarProxy), Cache: common.NewLoadingCache(4*time.Hour, 15*time.Minute, postFlushFn), AvatarProxy: avatarProxy, - Notifier: notifier.NoOperation{}, + Notifier: notifier.NewNoperation(), } if opts.DevMode { diff --git a/app/notifier/notifier.go b/app/notifier/notifier.go index e435682c..f49ca647 100644 --- a/app/notifier/notifier.go +++ b/app/notifier/notifier.go @@ -1,7 +1,9 @@ package notifier import ( + "fmt" "log" + "sync" "github.com/umputun/remark/app/store" ) @@ -11,25 +13,53 @@ type Interface interface { Subscribe(locator store.Locator, user store.User) error UnSubscribe(locator store.Locator, user store.User) error OnUpdate(locator store.Locator) error + Status(locator store.Locator, user store.User) (bool, error) } // NoOperation implements Interface doing nothing but logging -type NoOperation struct{} +type NoOperation struct { + sync.RWMutex + status map[string]struct{} +} + +// NewNoperation makes NoOperation fake notifier +func NewNoperation() *NoOperation { + res := NoOperation{status: map[string]struct{}{}} + return &res +} // Subscribe is a fake, just loging attempt -func (n NoOperation) Subscribe(locator store.Locator, user store.User) error { +func (n *NoOperation) Subscribe(locator store.Locator, user store.User) error { + n.Lock() + n.status[n.key(locator, user)] = struct{}{} + n.Unlock() log.Printf("[DEBUG] user %+v subscribed to %+v", user, locator) return nil } // UnSubscribe is a fake, just loging attempt -func (n NoOperation) UnSubscribe(locator store.Locator, user store.User) error { +func (n *NoOperation) UnSubscribe(locator store.Locator, user store.User) error { + n.Lock() + delete(n.status, n.key(locator, user)) + n.Unlock() log.Printf("[DEBUG] user %+v unsubscribed from %+v", user, locator) return nil } // OnUpdate is a fake, just loging event -func (n NoOperation) OnUpdate(locator store.Locator) error { +func (n *NoOperation) OnUpdate(locator store.Locator) error { log.Printf("[DEBUG] update for %+v", locator) return nil } + +// Status returns from in-memory +func (n *NoOperation) Status(locator store.Locator, user store.User) (bool, error) { + n.RLock() + defer n.RUnlock() + _, found := n.status[n.key(locator, user)] + return found, nil +} + +func (n *NoOperation) key(locator store.Locator, user store.User) string { + return fmt.Sprintf("%+v-%s", locator, user.ID) +} diff --git a/app/rest/server.go b/app/rest/server.go index f83b44bd..2818c324 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -107,7 +107,8 @@ func (s *Server) Run(port int) { rauth.Put("/comment/{id}", s.updateCommentCtrl) rauth.Get("/user", s.userInfoCtrl) rauth.Put("/vote/{id}", s.voteCtrl) - rauth.Put("/notify", s.notifyCtrl) + rauth.Put("/notify", s.notifyActionCtrl) + rauth.Get("/notify", s.notifyStatusCtrl) // admin routes, admin users only s.mod = admin{dataService: s.DataService, exporter: s.Exporter, cache: s.Cache} rauth.Mount("/admin", s.mod.routes()) @@ -417,24 +418,42 @@ func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) { } // PUT /notify?site=siteID&url=post-url&action=1 - subscribe/unsubscribe to notification -func (s *Server) notifyCtrl(w http.ResponseWriter, r *http.Request) { +func (s *Server) notifyActionCtrl(w http.ResponseWriter, r *http.Request) { user, err := common.GetUserInfo(r) if err != nil { common.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") return } locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} - action := r.URL.Query().Get("action") - switch action { + action := "unknown" + switch r.URL.Query().Get("action") { case "1": err = s.Notifier.Subscribe(locator, user) + action = "subscribe" case "0": err = s.Notifier.UnSubscribe(locator, user) + action = "unsubscribe" } if err != nil { common.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't subscribe/unsubscribe for notifications") return } + render.JSON(w, r, JSON{"locator": locator, "user": user.ID, "action": action}) +} + +// GET /notify?site=siteID&url=post-url - get notification status +func (s *Server) notifyStatusCtrl(w http.ResponseWriter, r *http.Request) { + user, err := common.GetUserInfo(r) + if err != nil { + common.SendErrorJSON(w, r, http.StatusUnauthorized, err, "can't get user info") + return + } + locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} + status := "not subscribed" + if st, err := s.Notifier.Status(locator, user); err == nil && st { + status = "subscribed" + } + render.JSON(w, r, JSON{"locator": locator, "user": user.ID, "status": status}) } // serves static files from /web diff --git a/remark.rest b/remark.rest index 425dcfcb..75423f97 100644 --- a/remark.rest +++ b/remark.rest @@ -67,3 +67,12 @@ GET {{host}}/api/v1/admin/blocked?site=remark ### delete comment by id DELETE {{host}}/api/v1/admin/comment/3665976683?site=remark&url=https://radio-t.com/p/2017/12/16/podcast-576/ + +### subscribe +PUT {{host}}/api/v1/notify?site=remark&url=https://radio-t.com/p/2017/12/16/podcast-576/&action=1 + +### unsubscribe +PUT {{host}}/api/v1/notify?site=remark&url=https://radio-t.com/p/2017/12/16/podcast-576/&action=0 + +### subscribtion status +GET {{host}}/api/v1/notify?site=remark&url=https://radio-t.com/p/2017/12/16/podcast-576/