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