diff --git a/backend/app/cmd/cleanup.go b/backend/app/cmd/cleanup.go index cb06dad9..0371b1bb 100644 --- a/backend/app/cmd/cleanup.go +++ b/backend/app/cmd/cleanup.go @@ -22,6 +22,7 @@ type CleanupCommand struct { BadWords []string `short:"w" long:"bword" description:"bad word(s)"` BadUsers []string `short:"u" long:"buser" description:"bad user(s)"` AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" required:"true" description:"admin basic auth password"` + SetTitle bool `long:"title" description:"title mode, will not remove comments, but reset titles to page's title'"` CommonOpts } @@ -47,24 +48,51 @@ func (cc *CleanupCommand) Execute(args []string) error { if e != nil { continue } - for _, comment := range comments { - totalComments++ - spam, score := cc.isSpam(comment) - if spam { - spamComments++ - if !cc.Dry { - if err = cc.deleteComment(comment); err != nil { - log.Printf("[WARN] can't remove comment, %v", err) - } - } - comment.Text = strings.Replace(comment.Text, "\n", " ", -1) - log.Printf("[SPAM] %+v [%.0f%%]", comment, score) + totalComments += len(comments) + if cc.SetTitle { + cc.procTitles(comments) + } else { + spamComments += cc.procSpam(comments) + + } + } + + msg := fmt.Sprintf("comments=%d, spam=%d", totalComments, spamComments) + if cc.SetTitle { + msg = fmt.Sprintf("comments=%d", totalComments) + } + + log.Printf("[INFO] completed, %s", msg) + return err +} + +func (cc *CleanupCommand) procSpam(comments []store.Comment) int { + spamComments := 0 + for _, comment := range comments { + spam, score := cc.isSpam(comment) + if spam { + spamComments++ + if !cc.Dry { + if err := cc.deleteComment(comment); err != nil { + log.Printf("[WARN] can't remove comment, %v", err) + } + } + comment.Text = strings.Replace(comment.Text, "\n", " ", -1) + log.Printf("[SPAM] %+v [%.0f%%]", comment, score) + } + } + return spamComments +} + +func (cc *CleanupCommand) procTitles(comments []store.Comment) { + for _, comment := range comments { + if !cc.Dry { + if err := cc.setTitle(comment); err != nil { + log.Printf("[WARN] can't set title for comment, %v", err) } } } - log.Printf("[INFO] comments=%d, spam=%d", totalComments, spamComments) - return err } // get list of posts in from/to represented as yyyymmdd. this is [from-to] inclusive @@ -180,6 +208,28 @@ func (cc *CleanupCommand) deleteComment(c store.Comment) error { return nil } +// setTitle with PUT /admin/title/{id}?site=siteID&url=post-url +func (cc *CleanupCommand) setTitle(c store.Comment) error { + + titleURL := fmt.Sprintf("%s/api/v1/admin/title/%s?site=%s&url=%s&format=plain", cc.RemarkURL, c.ID, cc.Site, c.Locator.URL) + req, err := http.NewRequest("PUT", titleURL, nil) + if err != nil { + return errors.Wrapf(err, "failed to make title request for comment %s, %s", c.ID, c.Locator.URL) + } + req.SetBasicAuth("admin", cc.AdminPasswd) + + client := http.Client{} + r, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "title request failed for comment %s, %s", c.ID, c.Locator.URL) + } + defer func() { _ = r.Body.Close() }() + if r.StatusCode != http.StatusOK { + return errors.Errorf("title request failed with status %s", r.Status) + } + return nil +} + // isSpam calculates spam's probability as a score func (cc *CleanupCommand) isSpam(comment store.Comment) (bool, float64) { diff --git a/backend/app/cmd/cleanup_test.go b/backend/app/cmd/cleanup_test.go index 1603ab40..3514abff 100644 --- a/backend/app/cmd/cleanup_test.go +++ b/backend/app/cmd/cleanup_test.go @@ -106,7 +106,7 @@ func TestCleanup_listComments(t *testing.T) { assert.Equal(t, 0, len(comments)) } -func TestCleanup_Execute(t *testing.T) { +func TestCleanup_ExecuteSpam(t *testing.T) { cleaned := cleanedComments{} r := chi.NewRouter() cleanupRoutes(t, r, &cleaned) @@ -125,6 +125,24 @@ func TestCleanup_Execute(t *testing.T) { assert.Equal(t, []string{"/api/v1/admin/comment/1", "/api/v1/admin/comment/3", "/api/v1/admin/comment/11"}, cleaned.ids) } +func TestCleanup_ExecuteTitle(t *testing.T) { + titledComments := cleanedComments{} + r := chi.NewRouter() + cleanupRoutes(t, r, &titledComments) + ts := httptest.NewServer(r) + defer ts.Close() + + cmd := CleanupCommand{} + cmd.SetCommon(CommonOpts{RemarkURL: ts.URL, SharedSecret: "123456"}) + p := flags.NewParser(&cmd, flags.Default) + _, err := p.ParseArgs([]string{"--site=remark", "--title", "--from=20181217", "--to=20181218", "--admin-passwd=secret"}) + require.Nil(t, err) + err = cmd.Execute(nil) + assert.NoError(t, err) + t.Logf("set titles for %+v", titledComments.ids) + assert.Equal(t, []string{"/api/v1/admin/title/1", "/api/v1/admin/title/2", "/api/v1/admin/title/3", "/api/v1/admin/title/11"}, titledComments.ids) +} + func cleanupRoutes(t *testing.T, r *chi.Mux, c *cleanedComments) { r.HandleFunc("/api/v1/list", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, "GET", r.Method) @@ -184,4 +202,13 @@ func cleanupRoutes(t *testing.T, r *chi.Mux, c *cleanedComments) { c.ids = append(c.ids, r.URL.Path) c.lock.Unlock() })) + + r.HandleFunc("/api/v1/admin/title/{id}", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, "PUT", r.Method) + t.Log("title for ", r.URL.Path) + c.lock.Lock() + c.ids = append(c.ids, r.URL.Path) + c.lock.Unlock() + })) + } diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index b4a60988..4cd0f420 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -40,6 +40,7 @@ func (a *admin) routes(middlewares ...func(http.Handler) http.Handler) chi.Route router.Put("/pin/{id}", a.setPinCtrl) router.Get("/blocked", a.blockedUsersCtrl) router.Put("/readonly", a.setReadOnlyCtrl) + router.Put("/title/{id}", a.setTitleCtrl) a.migrator.withRoutes(router) // set migrator routes, i.e. /export and /import @@ -191,6 +192,23 @@ func (a *admin) setReadOnlyCtrl(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, R.JSON{"locator": locator, "read-only": roStatus}) } +// PUT /title/{id}?site=siteID&url=post-url - set comment PostTitle to page's title +func (a *admin) setTitleCtrl(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "id") + locator := store.Locator{SiteID: r.URL.Query().Get("site"), URL: r.URL.Query().Get("url")} + + c, err := a.dataService.SetTitle(locator, id) + if err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "can't set title") + return + } + log.Printf("[INFO] set comment's title %s to %q", id, c.PostTitle) + + a.cache.Flush(cache.Flusher(locator.SiteID).Scopes(locator.URL, lastCommentsScope)) + render.Status(r, http.StatusOK) + render.JSON(w, r, R.JSON{"id": id, "locator": locator}) +} + // PUT /verify?site=siteID&url=post-url&ro=1 - set or reset read-only status for the post func (a *admin) setVerifyCtrl(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userid") diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index 6bbde081..1cc7ec45 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -7,6 +7,7 @@ import ( "fmt" "io/ioutil" "net/http" + "net/http/httptest" "os" "strings" "testing" @@ -15,6 +16,7 @@ import ( jwt "github.com/dgrijalva/jwt-go" "github.com/go-pkgz/auth/token" R "github.com/go-pkgz/rest" + "github.com/umputun/remark/backend/app/store/service" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -52,6 +54,49 @@ func TestAdmin_Delete(t *testing.T) { assert.True(t, cr.Deleted) } +func TestAdmin_Title(t *testing.T) { + ts, srv, teardown := startupT(t) + defer teardown() + + srv.DataService.TitleExtractor = service.NewTitleExtractor(http.Client{Timeout: time.Second}) + tss := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.String() == "/post1" { + w.Write([]byte("