diff --git a/README.md b/README.md index 6ee5d419..d50ae1fa 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,7 @@ Sort can be `time` or `score`. Supported sort order with prefix -/+, i.e. `-time - `GET /api/v1/last/{max}` - get up to `{max}` last comments - `GET /api/v1/id/{id}` - get comment by `id` +- `GET /api/v1/comments?user=id` - get comment by `user id` - `GET /api/v1/count?url=post-url` - get comment's count for `{url}` - `PUT /api/v1/vote/{id}?url=post-url&vote=1` - vote for comment. `vote`=1 will increase score, -1 decreases. _auth required_ - `DELETE /api/v1/admin/comment/{id}?url=post-url` - delete comment by `id`. _auth and admin required_ diff --git a/app/rest/format/tree_test.go b/app/rest/format/tree_test.go index 323a5607..3340b8bb 100644 --- a/app/rest/format/tree_test.go +++ b/app/rest/format/tree_test.go @@ -35,7 +35,7 @@ func TestStore_MakeTree(t *testing.T) { enc.SetIndent("", " ") err := enc.Encode(res) assert.Nil(t, err) - assert.Equal(t, expJSON, string(buf.Bytes())) + assert.Equal(t, expJSON, buf.String()) // t.Log(string(buf.Bytes())) } diff --git a/app/rest/server.go b/app/rest/server.go index 06c2944d..df92a678 100644 --- a/app/rest/server.go +++ b/app/rest/server.go @@ -67,6 +67,7 @@ func (s *Server) Run() { router.Route("/api/v1", func(rapi chi.Router) { rapi.Get("/find", s.findCommentsCtrl) rapi.Get("/id/{id}", s.commentByIDCtrl) + rapi.Get("/comments", s.findUserCommentsCtrl) rapi.Get("/last/{max}", s.lastCommentsCtrl) rapi.Get("/count", s.countCtrl) @@ -219,6 +220,23 @@ func (s *Server) commentByIDCtrl(w http.ResponseWriter, r *http.Request) { renderJSONWithHTML(w, r, comment) } +// GET /comments?user=id +func (s *Server) findUserCommentsCtrl(w http.ResponseWriter, r *http.Request) { + + userID := chi.URLParam(r, "user") + + log.Printf("[DEBUG] get comments by userID %s", userID) + + comment, err := s.Store.GetForUser(store.Locator{}, userID) + if err != nil { + log.Printf("[WARN] can't get comment, %s", err) + httpError(w, r, http.StatusBadRequest, err, "can't get comment by user id") + return + } + render.Status(r, http.StatusOK) + renderJSONWithHTML(w, r, comment) +} + // GET /user func (s *Server) userInfoCtrl(w http.ResponseWriter, r *http.Request) { user, err := auth.GetUserInfo(r) diff --git a/app/store/bolt.go b/app/store/bolt.go index acbb4280..66079c8a 100644 --- a/app/store/bolt.go +++ b/app/store/bolt.go @@ -21,6 +21,7 @@ type BoltDB struct { const ( lastBucketName = "last" + userBucketName = "users" blocksBucketPrefix = "block-" lastLimit = 1000 ) @@ -76,13 +77,34 @@ func (b *BoltDB) Create(comment Comment) (string, error) { if e != nil { return errors.Wrapf(e, "can't make bucket %s", lastBucketName) } - rv := refFromComment(comment) e = bucket.Put([]byte(rv.key), []byte(rv.value)) if e != nil { return errors.Wrapf(e, "can't put reference %s to %s", rv.value, lastBucketName) } + // add reference to commentID to "users" bucket + bucket, e = tx.CreateBucketIfNotExists([]byte(userBucketName)) + if e != nil { + return errors.Wrapf(e, "can't make bucket %s", userBucketName) + } + + userRefs := []string{} // holds current comment refs for the user + if data := bucket.Get([]byte(comment.User.ID)); data != nil { + if err := json.Unmarshal(data, &userRefs); err != nil { + return errors.Wrapf(e, "can't unmarshal comments for %s", comment.User.ID) + } + } + userRefs = append(userRefs, rv.value) + // serialize to json []byte for bolt and save + jdata, jerr = json.Marshal(&userRefs) + if jerr != nil { + return errors.Wrapf(jerr, "can't marshal comment ids for user %s, comment %s", comment.User.ID, comment.ID) + } + + if err := bucket.Put([]byte(comment.User.ID), jdata); err != nil { + return errors.Wrapf(err, "failed to put user comment %s", comment.ID) + } return nil }) @@ -345,7 +367,7 @@ func (b BoltDB) List(locator Locator) (result []string, err error) { err = b.View(func(tx *bolt.Tx) error { return tx.ForEach(func(name []byte, _ *bolt.Bucket) error { - if string(name) != lastBucketName { + if string(name) != lastBucketName && string(name) != userBucketName { result = append(result, string(name)) } return nil @@ -383,6 +405,44 @@ func (b *BoltDB) SetPin(locator Locator, commentID string, status bool) error { }) } +// GetForUser extracts all comments for given site and given userID +// "users" bucket has pairs userID:[]commentID +func (b *BoltDB) GetForUser(locator Locator, userID string) (comments []Comment, err error) { + + comments = []Comment{} + commentRefs := []string{} + err = b.View(func(tx *bolt.Tx) error { + userBucket := tx.Bucket([]byte(userBucketName)) + if userBucket == nil { + return errors.Errorf("no bucket %s in store", userBucketName) + } + + uData := userBucket.Get([]byte(userID)) + if uData == nil { + return errors.Errorf("no comments for user %s in store", userID) + } + if e := json.Unmarshal(uData, &commentRefs); e != nil { + return errors.Wrap(e, "failed to unmarshal list of user's comments") + } + return nil + }) + + if err != nil { + return comments, err + } + + for _, v := range commentRefs { + url, commentID, e := ref{value: v}.parseValue() + if e != nil { + return comments, errors.Wrapf(e, "can't parse reference %s", v) + } + if c, e := b.Get(Locator{URL: url, SiteID: locator.SiteID}, commentID); e == nil { + comments = append(comments, c) + } + } + return comments, err +} + func (b *BoltDB) bucketForBlock(locator Locator, userID string) []byte { return []byte(fmt.Sprintf("%s%s", blocksBucketPrefix, locator.SiteID)) } diff --git a/app/store/bolt_test.go b/app/store/bolt_test.go index 5856fa4f..3977f767 100644 --- a/app/store/bolt_test.go +++ b/app/store/bolt_test.go @@ -165,6 +165,15 @@ func TestBoltDB_Pin(t *testing.T) { assert.Equal(t, false, c.Pin) } +func TestBoltDB_GetForUser(t *testing.T) { + defer os.Remove(testDb) + b := prep(t) + + res, err := b.GetForUser(Locator{SiteID: "radio-t"}, "user1") + assert.Nil(t, err) + assert.Equal(t, 2, len(res)) +} + // makes new boltdb, put two records func prep(t *testing.T) *BoltDB { os.Remove(testDb) @@ -172,13 +181,23 @@ func prep(t *testing.T) *BoltDB { b, err := NewBoltDB(testDb) assert.Nil(t, err) - comment := Comment{Text: `some text, link`, Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local), - Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}} + comment := Comment{ + ID: "id-1", + Text: `some text, link`, + Timestamp: time.Date(2017, 12, 20, 15, 18, 22, 0, time.Local), + Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: User{ID: "user1", Name: "user name"}, + } _, err = b.Create(comment) assert.Nil(t, err) - comment = Comment{Text: "some text2", Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local), - Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, User: User{ID: "user1", Name: "user name"}} + comment = Comment{ + ID: "id-2", + Text: "some text2", + Timestamp: time.Date(2017, 12, 20, 15, 18, 23, 0, time.Local), + Locator: Locator{URL: "https://radio-t.com", SiteID: "radio-t"}, + User: User{ID: "user1", Name: "user name"}, + } _, err = b.Create(comment) assert.Nil(t, err) diff --git a/app/store/store.go b/app/store/store.go index 2269eee1..e1bc7dcf 100644 --- a/app/store/store.go +++ b/app/store/store.go @@ -61,10 +61,10 @@ type Interface interface { Vote(locator Locator, commentID string, userID string, val bool) (Comment, error) Count(locator Locator) (int, error) List(locator Locator) ([]string, error) + GetForUser(locator Locator, userID string) ([]Comment, error) SetBlock(locator Locator, userID string, status bool) error IsBlocked(locator Locator, userID string) bool - SetPin(locator Locator, commentID string, status bool) error } diff --git a/remark.rest b/remark.rest index 113a25e6..6bcf8996 100644 --- a/remark.rest +++ b/remark.rest @@ -36,5 +36,8 @@ GET http://remark.umputun.com/api/v1/user ### get comment by id GET http://remark.umputun.com/api/v1/id/3665976683?url=https://radio-t.com/p/2017/12/16/podcast-576/ +### get comment by user id +GET http://remark.umputun.com/api/v1/api/v1/comments?user=umputun + ### get counts GET http://remark.umputun.com/api/v1/count?url=https://radio-t.com/p/2017/12/16/podcast-576/ \ No newline at end of file