add /vote support
This commit is contained in:
@@ -6,9 +6,9 @@ Comment engine
|
||||
|
||||
### Authorization
|
||||
|
||||
- `GET /login/{provider}?from=http://url` - login with one of supported providers and redirects to `url`
|
||||
- `GET /login/{provider}?from=http://url` - perform "social" login with one of supported providers and redirect to `url`
|
||||
- `GET /logout` - logout
|
||||
- `GET /user` - returns user info, auth required
|
||||
- `GET /user` - get user info, _auth required_
|
||||
|
||||
```
|
||||
type User struct {
|
||||
@@ -24,22 +24,28 @@ _currently supported providers are `google` and `github`_
|
||||
|
||||
### Commenting
|
||||
|
||||
- `POST /comment` - adds a comment. auth required
|
||||
- `POST /comment` - add a comment. _auth required_
|
||||
|
||||
```
|
||||
type Comment struct {
|
||||
ID int64 `json:"id"` // read only
|
||||
ParentID int64 `json:"pid"`
|
||||
Text string `json:"text"`
|
||||
User User `json:"user"` // read only
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"` // read only
|
||||
Timestamp time.Time `json:"time"` // read only
|
||||
ID int64 `json:"id"` // read only
|
||||
ParentID int64 `json:"pid"`
|
||||
Text string `json:"text"`
|
||||
User User `json:"user"` // read only
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"` // read only
|
||||
Votes map[string]bool `json:"votes"` // read only
|
||||
Timestamp time.Time `json:"time"` // read only
|
||||
}
|
||||
|
||||
type Locator struct {
|
||||
SiteID string `json:"site"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
```
|
||||
|
||||
- `GET /find?url=post-url` - find all comments for given post return list of `Comment`
|
||||
- `GET /find?url=post-url` - find all comments for given post returns list of `Comment`
|
||||
- `GET /last/{max}` - get last `{max}` comments
|
||||
- `GET /id/{id}` - get comment by `id`
|
||||
|
||||
- `DELETE /comment/{id}` - delete comment by `id`. auth and admin required
|
||||
- `PUT /vote/{id}?url=post-url&vote=1` - vote for comment. `vote`=1 will increase score, -1 decreases. _auth required_
|
||||
- `DELETE /comment/{id}` - delete comment by `id`. _auth and admin required_
|
||||
+34
-4
@@ -38,17 +38,17 @@ func (s *Server) Run() {
|
||||
router.Get("/login/google", s.AuthGoogle.LoginHandler)
|
||||
router.Get("/auth/google", s.AuthGoogle.AuthHandler)
|
||||
router.Get("/logout", s.AuthGithub.LogoutHandler) // can hit any provider
|
||||
|
||||
router.Get("/login/github", s.AuthGithub.LoginHandler)
|
||||
router.Get("/auth/github", s.AuthGithub.AuthHandler)
|
||||
|
||||
router.Post("/comment", s.createCommentCtrl)
|
||||
router.Get("/find", s.getURLComments)
|
||||
router.Get("/id/{id}", s.getByID)
|
||||
router.Get("/last/{max}", s.getLastComments)
|
||||
|
||||
router.With(Auth(s.SessionStore, s.Admins)).Group(func(r chi.Router) {
|
||||
r.Get("/last/{max}", s.getLastComments)
|
||||
r.Post("/comment", s.createCommentCtrl)
|
||||
r.Get("/user", s.getUserInfo)
|
||||
r.Put("/vote/{id}", s.voteCtrl)
|
||||
r.With(AdminOnly).Delete("/comment/{id}", s.deleteCommentCtrl)
|
||||
})
|
||||
|
||||
@@ -94,7 +94,7 @@ func (s *Server) deleteCommentCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("[INFO] delete comment %d", id)
|
||||
|
||||
url := r.URL.Query().Get("url")
|
||||
err = s.Store.Delete(url, id)
|
||||
err = s.Store.Delete(store.Locator{URL: url}, id)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] can't delete comment, %s", err)
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't delete comment")
|
||||
@@ -179,6 +179,36 @@ func (s *Server) getUserInfo(w http.ResponseWriter, r *http.Request) {
|
||||
render.JSON(w, r, user)
|
||||
}
|
||||
|
||||
// PUT /vote/{id}?url=post-url&vote=1
|
||||
func (s *Server) voteCtrl(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
user, err := GetUserInfo(r)
|
||||
if err != nil {
|
||||
httpError(w, r, http.StatusUnauthorized, err, "can't get user info")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] bad id %s", chi.URLParam(r, "id"))
|
||||
httpError(w, r, http.StatusBadRequest, err, "can't parse id")
|
||||
}
|
||||
|
||||
log.Printf("[INFO] vote for comment %d", id)
|
||||
|
||||
url := r.URL.Query().Get("url")
|
||||
vote := r.URL.Query().Get("vote") == "1"
|
||||
|
||||
comment, err := s.Store.Vote(store.Locator{URL: url}, id, user.ID, vote)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] vote rejected for %s - %d, %s", user.ID, id, err)
|
||||
httpError(w, r, http.StatusInternalServerError, err, "can't delete comment")
|
||||
return
|
||||
}
|
||||
|
||||
render.JSON(w, r, comment)
|
||||
}
|
||||
|
||||
func httpError(w http.ResponseWriter, r *http.Request, code int, err error, details string) {
|
||||
render.Status(r, code)
|
||||
render.JSON(w, r, JSON{"error": err.Error(), "details": details})
|
||||
|
||||
+51
-4
@@ -39,6 +39,7 @@ func (b *BoltDB) Create(comment Comment) (int64, error) {
|
||||
|
||||
comment.ID = time.Now().UnixNano()
|
||||
comment.Timestamp = time.Now()
|
||||
comment.Votes = map[string]bool{}
|
||||
|
||||
err := b.Update(func(tx *bolt.Tx) error {
|
||||
bucket, e := tx.CreateBucketIfNotExists([]byte(comment.Locator.URL))
|
||||
@@ -81,16 +82,16 @@ func (b *BoltDB) Create(comment Comment) (int64, error) {
|
||||
}
|
||||
|
||||
// Delete removed comment by url and id from the store
|
||||
func (b *BoltDB) Delete(url string, id int64) error {
|
||||
func (b *BoltDB) Delete(locator Locator, id int64) error {
|
||||
|
||||
return b.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(url))
|
||||
bucket := tx.Bucket([]byte(locator.URL))
|
||||
if bucket == nil {
|
||||
return errors.Errorf("no bucket %s in store", url)
|
||||
return errors.Errorf("no bucket %s in store", locator.URL)
|
||||
}
|
||||
key := b.keyFromValue(id)
|
||||
if err := bucket.Delete(key); err != nil {
|
||||
return errors.Wrapf(err, "can't delete key %s from bucket %s", key, url)
|
||||
return errors.Wrapf(err, "can't delete key %s from bucket %s", key, locator.URL)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -196,6 +197,52 @@ func (b *BoltDB) Last(locator Locator, max int) (result []Comment, err error) {
|
||||
return result, err
|
||||
}
|
||||
|
||||
// Get comment by id
|
||||
func (b *BoltDB) Vote(locator Locator, commentID int64, userID string, val bool) (comment Comment, err error) {
|
||||
|
||||
err = b.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(locator.URL))
|
||||
if bucket == nil {
|
||||
return errors.Errorf("no bucket %s in store", locator.URL)
|
||||
}
|
||||
|
||||
// get and unmarshal comment for the store
|
||||
commentVal := bucket.Get(b.keyFromValue(commentID))
|
||||
if commentVal == nil {
|
||||
return errors.Errorf("no comment for %d in store %s", commentID, locator.URL)
|
||||
}
|
||||
|
||||
if e := json.Unmarshal(commentVal, &comment); e != nil {
|
||||
return errors.Wrap(e, "failed to unmarshal")
|
||||
}
|
||||
|
||||
// check if user voted already
|
||||
for k := range comment.Votes {
|
||||
if k == userID {
|
||||
return errors.Errorf("user %s already voted for comment %d", userID, commentID)
|
||||
}
|
||||
}
|
||||
|
||||
// update votes and score
|
||||
comment.Votes[userID] = val
|
||||
if val {
|
||||
comment.Score++
|
||||
} else {
|
||||
comment.Score--
|
||||
}
|
||||
data, err := json.Marshal(&comment)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "can't marshal comment with updated votes")
|
||||
}
|
||||
if err = bucket.Put(b.keyFromValue(commentID), data); err != nil {
|
||||
return errors.Wrap(err, "failed to save comment with updated votes")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
return comment, err
|
||||
}
|
||||
|
||||
func (b *BoltDB) keyFromComment(comment Comment) []byte {
|
||||
return []byte(fmt.Sprintf("%22d", comment.ID))
|
||||
}
|
||||
|
||||
@@ -71,6 +71,30 @@ func TestBoltDB_Last(t *testing.T) {
|
||||
assert.Equal(t, "some text2", res[0].Text)
|
||||
}
|
||||
|
||||
func TestBoltDB_Vote(t *testing.T) {
|
||||
defer os.Remove(testDb)
|
||||
b := prep(t)
|
||||
|
||||
res, err := b.Last(Locator{URL: "https://radio-t.com"}, 0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, 0, res[0].Score)
|
||||
assert.Equal(t, map[string]bool{}, res[0].Votes)
|
||||
|
||||
c, err := b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 1, c.Score)
|
||||
assert.Equal(t, map[string]bool{"user1": true}, c.Votes)
|
||||
|
||||
_, err = b.Vote(Locator{URL: "https://radio-t.com"}, res[0].ID, "user1", true)
|
||||
assert.NotNil(t, err, "double-voting rejected")
|
||||
|
||||
res, err = b.Last(Locator{URL: "https://radio-t.com"}, 0)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, 2, len(res))
|
||||
assert.Equal(t, 1, res[0].Score)
|
||||
}
|
||||
|
||||
// makes new boltdb, put two records
|
||||
func prep(t *testing.T) *BoltDB {
|
||||
b, err := NewBoltDB(testDb)
|
||||
|
||||
+10
-8
@@ -4,13 +4,14 @@ import "time"
|
||||
|
||||
// Comment represents a single comment with reference to its parent
|
||||
type Comment struct {
|
||||
ID int64 `json:"id"`
|
||||
ParentID int64 `json:"pid"`
|
||||
Text string `json:"text"`
|
||||
User User `json:"user"`
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"`
|
||||
Timestamp time.Time `json:"time"`
|
||||
ID int64 `json:"id"`
|
||||
ParentID int64 `json:"pid"`
|
||||
Text string `json:"text"`
|
||||
User User `json:"user"`
|
||||
Locator Locator `json:"locator"`
|
||||
Score int `json:"score"`
|
||||
Votes map[string]bool `json:"votes"`
|
||||
Timestamp time.Time `json:"time"`
|
||||
}
|
||||
|
||||
// Locator keeps site and url of the post
|
||||
@@ -40,8 +41,9 @@ type Request struct {
|
||||
// Interface defines basic CRUD for comments
|
||||
type Interface interface {
|
||||
Create(comment Comment) (int64, error)
|
||||
Delete(url string, id int64) error
|
||||
Delete(locator Locator, id int64) error
|
||||
Find(request Request) ([]Comment, error)
|
||||
Last(locator Locator, max int) ([]Comment, error)
|
||||
Get(locator Locator, id int64) (Comment, error)
|
||||
Vote(locator Locator, commentID int64, userID string, val bool) (Comment, error)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user