Add good bit of functionality, some tests
Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
+223
@@ -0,0 +1,223 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CommentService handles communication with the comment
|
||||
// related methods of the Reddit API
|
||||
type CommentService interface {
|
||||
Submit(ctx context.Context, id string, text string) (*Comment, *Response, error)
|
||||
Edit(ctx context.Context, id string, text string) (*Comment, *Response, error)
|
||||
Delete(ctx context.Context, id string) (*Response, error)
|
||||
|
||||
Save(ctx context.Context, id string) (*Response, error)
|
||||
Unsave(ctx context.Context, id string) (*Response, error)
|
||||
}
|
||||
|
||||
// CommentServiceOp implements the CommentService interface
|
||||
type CommentServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ CommentService = &CommentServiceOp{}
|
||||
|
||||
type commentRoot struct {
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
Data *Comment `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type commentRootListing struct {
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
Data *struct {
|
||||
Dist int `json:"dist"`
|
||||
Roots []commentRoot `json:"children,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Comment is a comment posted by a user
|
||||
type Comment struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
ParentID string `json:"parent_id,omitempty"`
|
||||
Permalink string `json:"permalink,omitempty"`
|
||||
|
||||
Body string `json:"body,omitempty"`
|
||||
BodyHTML string `json:"body_html,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
AuthorID string `json:"author_fullname,omitempty"`
|
||||
AuthorFlairText string `json:"author_flair_text,omitempty"`
|
||||
|
||||
Subreddit string `json:"subreddit,omitempty"`
|
||||
SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"`
|
||||
SubredditID string `json:"subreddit_id,omitempty"`
|
||||
|
||||
Score int `json:"score"`
|
||||
Controversiality int `json:"controversiality"`
|
||||
|
||||
Created float64 `json:"created"`
|
||||
CreatedUTC float64 `json:"created_utc"`
|
||||
|
||||
LinkID string `json:"link_id,omitempty"`
|
||||
|
||||
// These don't appear when submitting a comment
|
||||
LinkTitle string `json:"link_title,omitempty"`
|
||||
LinkPermalink string `json:"link_permalink,omitempty"`
|
||||
LinkAuthor string `json:"link_author,omitempty"`
|
||||
LinkNumComments int `json:"num_comments"`
|
||||
|
||||
IsSubmitter bool `json:"is_submitter"`
|
||||
ScoreHidden bool `json:"score_hidden"`
|
||||
Saved bool `json:"saved"`
|
||||
Stickied bool `json:"stickied"`
|
||||
Locked bool `json:"locked"`
|
||||
CanGild bool `json:"can_gild"`
|
||||
NSFW bool `json:"over_18"`
|
||||
}
|
||||
|
||||
// CommentList holds information about a list of comments
|
||||
// The after and before fields help decide the anchor point for a subsequent
|
||||
// call that returns a list
|
||||
type CommentList struct {
|
||||
Comments []Comment `json:"comments,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
}
|
||||
|
||||
func (s *CommentServiceOp) isCommentID(id string) bool {
|
||||
return strings.HasPrefix(id, kindComment+"_")
|
||||
}
|
||||
|
||||
// Submit submits a comment as a reply to a link or to another comment
|
||||
func (s *CommentServiceOp) Submit(ctx context.Context, id string, text string) (*Comment, *Response, error) {
|
||||
path := "api/comment"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("parent", id)
|
||||
form.Set("text", text)
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Comment)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Edit edits the comment with the id provided
|
||||
// todo: don't forget to do this for links (i.e. posts)
|
||||
func (s *CommentServiceOp) Edit(ctx context.Context, id string, text string) (*Comment, *Response, error) {
|
||||
if !s.isCommentID(id) {
|
||||
return nil, nil, fmt.Errorf("must provide comment id (starting with t1_); id provided: %q", id)
|
||||
}
|
||||
|
||||
path := "api/editusertext"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("thing_id", id)
|
||||
form.Set("text", text)
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Comment)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Delete deletes a comment via the id
|
||||
// todo: don't forget to do this for links (i.e. posts)
|
||||
// Seems like this always returns {} as a response, no matter if an id is even provided
|
||||
func (s *CommentServiceOp) Delete(ctx context.Context, id string) (*Response, error) {
|
||||
if !s.isCommentID(id) {
|
||||
return nil, fmt.Errorf("must provide comment id (starting with t1_); id provided: %q", id)
|
||||
}
|
||||
|
||||
path := "api/del"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Save saves a comment
|
||||
// Seems like this just returns {} on success
|
||||
func (s *CommentServiceOp) Save(ctx context.Context, id string) (*Response, error) {
|
||||
if !s.isCommentID(id) {
|
||||
return nil, fmt.Errorf("must provide comment id (starting with t1_); id provided: %q", id)
|
||||
}
|
||||
|
||||
path := "api/save"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Unsave unsaves a comment
|
||||
// Seems like this just returns {} on success
|
||||
func (s *CommentServiceOp) Unsave(ctx context.Context, id string) (*Response, error) {
|
||||
if !s.isCommentID(id) {
|
||||
return nil, fmt.Errorf("must provide comment id (starting with t1_); id provided: %q", id)
|
||||
}
|
||||
|
||||
path := "api/unsave"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var expectedCommentSubmitOrEdit = &Comment{
|
||||
ID: "test2",
|
||||
FullID: "t1_test2",
|
||||
ParentID: "t1_test",
|
||||
Permalink: "/r/subreddit/comments/test1/some_thread/test2/",
|
||||
|
||||
Body: "test comment",
|
||||
BodyHTML: "<div class=\"md\"><p>test comment</p>\n</div>",
|
||||
Author: "reddit_username",
|
||||
AuthorID: "t2_user1",
|
||||
AuthorFlairText: "Flair",
|
||||
|
||||
Subreddit: "subreddit",
|
||||
SubredditNamePrefixed: "r/subreddit",
|
||||
SubredditID: "t5_test",
|
||||
|
||||
Score: 1,
|
||||
Controversiality: 0,
|
||||
|
||||
Created: 1588147787,
|
||||
CreatedUTC: 1588118987,
|
||||
|
||||
LinkID: "t3_link1",
|
||||
}
|
||||
|
||||
func TestCommentServiceOp_Submit(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
commentBlob := readFileContents(t, "testdata/comment-submit-edit.json")
|
||||
|
||||
mux.HandleFunc("/api/comment", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("parent", "t1_test")
|
||||
form.Set("text", "test comment")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, commentBlob)
|
||||
})
|
||||
|
||||
comment, _, err := client.Comment.Submit(ctx, "t1_test", "test comment")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := expectedCommentSubmitOrEdit, comment; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentServiceOp_Edit(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
commentBlob := readFileContents(t, "testdata/comment-submit-edit.json")
|
||||
|
||||
mux.HandleFunc("/api/editusertext", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("thing_id", "t1_test")
|
||||
form.Set("text", "test comment")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, commentBlob)
|
||||
})
|
||||
|
||||
_, _, err := client.Comment.Edit(ctx, "t3_test", "test comment")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nothing instead")
|
||||
}
|
||||
if expect, actual := `must provide comment id (starting with t1_); id provided: "t3_test"`, err.Error(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
comment, _, err := client.Comment.Edit(ctx, "t1_test", "test comment")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := expectedCommentSubmitOrEdit, comment; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentServiceOp_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/del", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Comment.Delete(ctx, "t3_test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nothing instead")
|
||||
}
|
||||
if expect, actual := `must provide comment id (starting with t1_); id provided: "t3_test"`, err.Error(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
res, err := client.Comment.Delete(ctx, "t1_test")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := http.StatusOK, res.StatusCode; expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentServiceOp_Save(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/save", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Comment.Save(ctx, "t3_test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nothing instead")
|
||||
}
|
||||
if expect, actual := `must provide comment id (starting with t1_); id provided: "t3_test"`, err.Error(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
res, err := client.Comment.Save(ctx, "t1_test")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := http.StatusOK, res.StatusCode; expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentServiceOp_Unsave(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/unsave", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Comment.Unsave(ctx, "t3_test")
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nothing instead")
|
||||
}
|
||||
if expect, actual := `must provide comment id (starting with t1_); id provided: "t3_test"`, err.Error(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
res, err := client.Comment.Unsave(ctx, "t1_test")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := http.StatusOK, res.StatusCode; expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FlairService handles communication with the user
|
||||
// related methods of the Reddit API
|
||||
type FlairService interface {
|
||||
GetFromSubreddit(ctx context.Context, name string) ([]Flair, *Response, error)
|
||||
GetFromSubredditV2(ctx context.Context, name string) ([]FlairV2, *Response, error)
|
||||
}
|
||||
|
||||
// FlairServiceOp implements the FlairService interface
|
||||
type FlairServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ FlairService = &FlairServiceOp{}
|
||||
|
||||
// Flair is a flair on Reddit
|
||||
type Flair struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CSS string `json:"css_class,omitempty"`
|
||||
}
|
||||
|
||||
// FlairV2 is a flair on Reddit
|
||||
type FlairV2 struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
CSS string `json:"css_class,omitempty"`
|
||||
ModOnly bool `json:"mod_only"`
|
||||
}
|
||||
|
||||
// GetFromSubreddit returns the flairs from the subreddit
|
||||
func (s *FlairServiceOp) GetFromSubreddit(ctx context.Context, name string) ([]Flair, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/user_flair", name)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var flairs []Flair
|
||||
resp, err := s.client.Do(ctx, req, &flairs)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return flairs, resp, nil
|
||||
}
|
||||
|
||||
// GetFromSubredditV2 returns the flairs from the subreddit
|
||||
func (s *FlairServiceOp) GetFromSubredditV2(ctx context.Context, name string) ([]FlairV2, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/user_flair_v2", name)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var flairs []FlairV2
|
||||
resp, err := s.client.Do(ctx, req, &flairs)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return flairs, resp, nil
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var expectedFlairs = []Flair{
|
||||
{
|
||||
ID: "b8a1c822-3feb-11e8-88e1-0e5f55d58ce0",
|
||||
Text: "Beginner",
|
||||
Type: "text",
|
||||
CSS: "Beginner1",
|
||||
},
|
||||
{
|
||||
ID: "b8ea0fce-3feb-11e8-af7a-0e263a127cf8",
|
||||
Text: "Beginner",
|
||||
Type: "text",
|
||||
CSS: "Beginner2",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedFlairsV2 = []FlairV2{
|
||||
{
|
||||
ID: "b8a1c822-3feb-11e8-88e1-0e5f55d58ce0",
|
||||
Text: "Beginner",
|
||||
Type: "text",
|
||||
CSS: "Beginner1",
|
||||
ModOnly: false,
|
||||
},
|
||||
{
|
||||
ID: "b8ea0fce-3feb-11e8-af7a-0e263a127cf8",
|
||||
Text: "Moderator",
|
||||
Type: "text",
|
||||
CSS: "Moderator1",
|
||||
ModOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestFlairServiceOp_GetFlairs(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
flairsBlob := readFileContents(t, "testdata/flairs.json")
|
||||
|
||||
mux.HandleFunc("/r/subreddit/api/user_flair", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, flairsBlob)
|
||||
})
|
||||
|
||||
flairs, _, err := client.Flair.GetFromSubreddit(ctx, "subreddit")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := expectedFlairs, flairs; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlairServiceOp_GetFlairsV2(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
flairsV2Blob := readFileContents(t, "testdata/flairs-v2.json")
|
||||
|
||||
mux.HandleFunc("/r/subreddit/api/user_flair_v2", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodGet)
|
||||
fmt.Fprint(w, flairsV2Blob)
|
||||
})
|
||||
|
||||
flairs, _, err := client.Flair.GetFromSubredditV2(ctx, "subreddit")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := expectedFlairsV2, flairs; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,67 @@ import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
libraryVersion = "0.0.1"
|
||||
defaultBaseURL = "https://reddit.com"
|
||||
defaultBaseURLOauth = "https://oauth.reddit.com"
|
||||
userAgent = "geddit/" + libraryVersion
|
||||
mediaType = "application/json"
|
||||
libraryName = "github.com/vartanbeno/geddit"
|
||||
libraryVersion = "0.0.1"
|
||||
|
||||
defaultBaseURL = "https://oauth.reddit.com"
|
||||
defaultTokenURL = "https://www.reddit.com/api/v1/access_token"
|
||||
|
||||
mediaTypeJSON = "application/json"
|
||||
mediaTypeForm = "application/x-www-form-urlencoded"
|
||||
|
||||
headerContentType = "Content-Type"
|
||||
headerAccept = "Accept"
|
||||
headerUserAgent = "User-Agent"
|
||||
)
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map,
|
||||
// since we'll only be modify the headers.
|
||||
// Per the specification of http.RoundTripper, we should not directly modify a request.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
return r2
|
||||
}
|
||||
|
||||
// sets the User-Agent header for requests
|
||||
type userAgentTransport struct {
|
||||
ua string
|
||||
Base http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *userAgentTransport) setUserAgent(req *http.Request) *http.Request {
|
||||
req2 := cloneRequest(req)
|
||||
req2.Header.Set(headerUserAgent, t.ua)
|
||||
return req2
|
||||
}
|
||||
|
||||
func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req2 := t.setUserAgent(req)
|
||||
return t.base().RoundTrip(req2)
|
||||
}
|
||||
|
||||
func (t *userAgentTransport) base() http.RoundTripper {
|
||||
if t.Base != nil {
|
||||
return t.Base
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
// RequestCompletionCallback defines the type of the request callback function
|
||||
type RequestCompletionCallback func(*http.Request, *http.Response)
|
||||
|
||||
@@ -31,12 +78,25 @@ type Client struct {
|
||||
// HTTP client used to communicate with the Reddit API
|
||||
client *http.Client
|
||||
|
||||
// Base URL for HTTP requests
|
||||
BaseURL *url.URL
|
||||
BaseURL *url.URL
|
||||
TokenURL *url.URL
|
||||
|
||||
UserAgent string
|
||||
userAgent string
|
||||
|
||||
ID string
|
||||
Secret string
|
||||
Username string
|
||||
Password string
|
||||
|
||||
Comment CommentService
|
||||
Flair FlairService
|
||||
Link LinkService
|
||||
Listings ListingsService
|
||||
Subreddit SubredditService
|
||||
User UserService
|
||||
Vote VoteService
|
||||
|
||||
oauth2Transport *oauth2.Transport
|
||||
|
||||
onRequestCompleted RequestCompletionCallback
|
||||
}
|
||||
@@ -52,21 +112,47 @@ func newClient(httpClient *http.Client) *Client {
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
tokenURL, _ := url.Parse(defaultTokenURL)
|
||||
|
||||
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
|
||||
c := &Client{client: httpClient, BaseURL: baseURL, TokenURL: tokenURL}
|
||||
|
||||
c.Comment = &CommentServiceOp{client: c}
|
||||
c.Flair = &FlairServiceOp{client: c}
|
||||
c.Link = &LinkServiceOp{client: c}
|
||||
c.Listings = &ListingsServiceOp{client: c}
|
||||
c.Subreddit = &SubredditServiceOp{client: c}
|
||||
c.User = &UserServiceOp{client: c}
|
||||
c.Vote = &VoteServiceOp{client: c}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// New returns a client that can make requests to the Reddit API
|
||||
func New(httpClient *http.Client, opts ...Opt) (c *Client, err error) {
|
||||
// NewClient returns a client that can make requests to the Reddit API
|
||||
func NewClient(httpClient *http.Client, opts ...Opt) (c *Client, err error) {
|
||||
c = newClient(httpClient)
|
||||
|
||||
for _, opt := range opts {
|
||||
if err = opt(c); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.userAgent = fmt.Sprintf("golang:%s:v%s (by /u/%s)", libraryName, libraryVersion, c.Username)
|
||||
userAgentTransport := &userAgentTransport{
|
||||
ua: c.userAgent,
|
||||
Base: c.client.Transport,
|
||||
}
|
||||
|
||||
oauth2Config := oauth2Config{
|
||||
id: c.ID,
|
||||
secret: c.Secret,
|
||||
username: c.Username,
|
||||
password: c.Password,
|
||||
tokenURL: c.TokenURL.String(),
|
||||
userAgentTransport: userAgentTransport,
|
||||
}
|
||||
c.client.Transport = oauth2Transport(oauth2Config)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -93,9 +179,28 @@ func (c *Client) NewRequest(method, path string, body interface{}) (*http.Reques
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add(headerContentType, mediaType)
|
||||
req.Header.Add(headerAccept, mediaType)
|
||||
req.Header.Add(headerUserAgent, c.UserAgent)
|
||||
req.Header.Add(headerContentType, mediaTypeJSON)
|
||||
req.Header.Add(headerAccept, mediaTypeJSON)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewPostForm creates an API request with a POST form
|
||||
// The path is the relative URL which will be resolves to the BaseURL of the Client
|
||||
// It should always be specified without a preceding slash
|
||||
func (c *Client) NewPostForm(path string, form url.Values) (*http.Request, error) {
|
||||
u, err := c.BaseURL.Parse(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add(headerContentType, mediaTypeForm)
|
||||
req.Header.Add(headerAccept, mediaTypeJSON)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
@@ -164,6 +269,29 @@ func DoRequestWithClient(ctx context.Context, client *http.Client, req *http.Req
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
// JSONErrorResponse is an error response that sometimes gets returned with a 200 code
|
||||
type JSONErrorResponse struct {
|
||||
// HTTP response that caused this error
|
||||
Response *http.Response
|
||||
|
||||
JSON *struct {
|
||||
Errors [][]string `json:"errors,omitempty"`
|
||||
} `json:"json,omitempty"`
|
||||
}
|
||||
|
||||
func (r *JSONErrorResponse) Error() string {
|
||||
var message string
|
||||
if r.JSON != nil && len(r.JSON.Errors) > 0 {
|
||||
for _, errList := range r.JSON.Errors {
|
||||
message += strings.Join(errList, ", ")
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"%v %v: %d %v",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, message,
|
||||
)
|
||||
}
|
||||
|
||||
// An ErrorResponse reports the error caused by an API request
|
||||
type ErrorResponse struct {
|
||||
// HTTP response that caused this error
|
||||
@@ -182,13 +310,27 @@ func (r *ErrorResponse) Error() string {
|
||||
|
||||
// CheckResponse checks the API response for errors, and returns them if present.
|
||||
// A response is considered an error if it has a status code outside the 200 range.
|
||||
// Reddit also sometimes sends errors with 200 codes; we check for those too.
|
||||
func CheckResponse(r *http.Response) error {
|
||||
jsonErrorResponse := &JSONErrorResponse{Response: r}
|
||||
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err == nil && len(data) > 0 {
|
||||
json.Unmarshal(data, jsonErrorResponse)
|
||||
if jsonErrorResponse.JSON != nil {
|
||||
return jsonErrorResponse
|
||||
}
|
||||
}
|
||||
|
||||
// reset response body
|
||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
||||
|
||||
if c := r.StatusCode; c >= 200 && c <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorResponse := &ErrorResponse{Response: r}
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
data, err = ioutil.ReadAll(r.Body)
|
||||
if err == nil && len(data) > 0 {
|
||||
err := json.Unmarshal(data, errorResponse)
|
||||
if err != nil {
|
||||
@@ -198,3 +340,60 @@ func CheckResponse(r *http.Response) error {
|
||||
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
// ListOptions are the optional parameters to the various endpoints that return lists
|
||||
type ListOptions struct {
|
||||
Sort string `url:"sort,omitempty"`
|
||||
Type string `url:"type,omitempty"` // links or comments
|
||||
|
||||
// For getting submissions
|
||||
// all, year, month, week, day, hour
|
||||
Timespan string `url:"t,omitempty"`
|
||||
|
||||
// Common for all listing endpoints
|
||||
After string `url:"after,omitempty"`
|
||||
Before string `url:"before,omitempty"`
|
||||
Limit int `url:"limit,omitempty"` // default: 25
|
||||
Count int `url:"count,omitempty"` // default: 0
|
||||
}
|
||||
|
||||
func addOptions(s string, opt interface{}) (string, error) {
|
||||
v := reflect.ValueOf(opt)
|
||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
origURL, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
origValues := origURL.Query()
|
||||
|
||||
newValues, err := query.Values(opt)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
for k, v := range newValues {
|
||||
origValues[k] = v
|
||||
}
|
||||
|
||||
origURL.RawQuery = origValues.Encode()
|
||||
return origURL.String(), nil
|
||||
}
|
||||
|
||||
type root struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
kindListing string = "Listing"
|
||||
kindComment string = "t1"
|
||||
kindAccount string = "t2"
|
||||
kindLink string = "t3"
|
||||
kindMessage string = "t4"
|
||||
kindSubreddit string = "t5"
|
||||
kindAward string = "t6"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
Docs:
|
||||
- https://www.reddit.com/dev/api/
|
||||
- https://github.com/reddit-archive/reddit/wiki/api
|
||||
- https://github.com/reddit-archive/reddit/wiki/OAuth2
|
||||
- https://github.com/reddit-archive/reddit/wiki/OAuth2-Quick-Start-Example
|
||||
|
||||
1. Go to https://www.reddit.com/prefs/apps and create an app. There are 3 types of apps:
|
||||
- Web app. Service is available over http or https, preferably the latter.
|
||||
- Installed app, such as a mobile app on a user's device which you can't control.
|
||||
Redirect the user to a URI after they grant your app permissions.
|
||||
- Script (the simplest type of app). Select this if you are the only person who will
|
||||
use the app. Only has access to your account.
|
||||
|
||||
Best option for a client like this is to use the script option.
|
||||
|
||||
2. After creating the app, you will get a client id and client secret.
|
||||
|
||||
3. Send a POST request (with the Content-Type header set to "application/x-www-form-urlencoded")
|
||||
to https://www.reddit.com/api/v1/access_token with the following form values:
|
||||
- grant_type=password
|
||||
- username={your Reddit username}
|
||||
- password={your Reddit password}
|
||||
|
||||
4. You should receive a response body like the following:
|
||||
{
|
||||
"access_token": "70743860-DRhHVNSEOMu1ldlI",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "*"
|
||||
}
|
||||
*/
|
||||
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
)
|
||||
|
||||
var endpoint = oauth2.Endpoint{
|
||||
TokenURL: "https://www.reddit.com/api/v1/access_token",
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
}
|
||||
|
||||
type oauth2Config struct {
|
||||
id string
|
||||
secret string
|
||||
username string
|
||||
password string
|
||||
tokenURL string
|
||||
|
||||
// We need to set a custom user agent, because using the one set by default by the
|
||||
// stdlib gives us 429 Too Many Request responses from the Reddit API
|
||||
userAgentTransport *userAgentTransport
|
||||
}
|
||||
|
||||
func oauth2Transport(c oauth2Config) *oauth2.Transport {
|
||||
params := url.Values{
|
||||
"grant_type": {"password"},
|
||||
"username": {c.username},
|
||||
"password": {c.password},
|
||||
}
|
||||
|
||||
cfg := clientcredentials.Config{
|
||||
ClientID: c.id,
|
||||
ClientSecret: c.secret,
|
||||
TokenURL: c.tokenURL,
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
EndpointParams: params,
|
||||
}
|
||||
|
||||
// We need to set a custom user agent, because using the one set by default by the
|
||||
// stdlib gives us 429 Too Many Request responses from the Reddit API
|
||||
httpClient := &http.Client{Transport: c.userAgentTransport}
|
||||
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
|
||||
|
||||
src := cfg.TokenSource(ctx)
|
||||
tr := &oauth2.Transport{
|
||||
Source: src,
|
||||
Base: c.userAgentTransport,
|
||||
}
|
||||
return tr
|
||||
}
|
||||
|
||||
// WithOAuth2 sets the necessary values for the client to authenticate via OAuth2
|
||||
func WithOAuth2(id, secret, username, password string) Opt {
|
||||
return func(c *Client) error {
|
||||
c.ID = id
|
||||
c.Secret = secret
|
||||
c.Username = username
|
||||
c.Password = password
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Opt is a configuration option to initialize a client
|
||||
type Opt func(*Client) error
|
||||
|
||||
// FromEnv configures the client with values from environment variables.
|
||||
//
|
||||
// Supported environment variables:
|
||||
// GEDDIT_CLIENT_ID to set the client's id.
|
||||
// GEDDIT_CLIENT_SECRET to set the client's secret.
|
||||
// GEDDIT_CLIENT_USERNAME to set the client's username.
|
||||
// GEDDIT_CLIENT_PASSWORD to set the client's password.
|
||||
func FromEnv(c *Client) error {
|
||||
if v, ok := os.LookupEnv("GEDDIT_CLIENT_ID"); ok {
|
||||
c.ID = v
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv("GEDDIT_CLIENT_SECRET"); ok {
|
||||
c.Secret = v
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv("GEDDIT_CLIENT_USERNAME"); ok {
|
||||
c.Username = v
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv("GEDDIT_CLIENT_PASSWORD"); ok {
|
||||
c.Password = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithBaseURL sets the base URL for the client to make requests to
|
||||
func WithBaseURL(u string) Opt {
|
||||
return func(c *Client) error {
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.BaseURL = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenURL sets the url used to get access tokens
|
||||
func WithTokenURL(u string) Opt {
|
||||
return func(c *Client) error {
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.TokenURL = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFromEnv(t *testing.T) {
|
||||
os.Setenv("GEDDIT_CLIENT_ID", "id1")
|
||||
defer os.Unsetenv("GEDDIT_CLIENT_ID")
|
||||
|
||||
os.Setenv("GEDDIT_CLIENT_SECRET", "secret1")
|
||||
defer os.Unsetenv("GEDDIT_CLIENT_SECRET")
|
||||
|
||||
os.Setenv("GEDDIT_CLIENT_USERNAME", "username1")
|
||||
defer os.Unsetenv("GEDDIT_CLIENT_USERNAME")
|
||||
|
||||
os.Setenv("GEDDIT_CLIENT_PASSWORD", "password1")
|
||||
defer os.Unsetenv("GEDDIT_CLIENT_PASSWORD")
|
||||
|
||||
c, err := NewClient(nil, FromEnv)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
type values struct {
|
||||
id, secret, username, password string
|
||||
}
|
||||
|
||||
expect := values{"id1", "secret1", "username1", "password1"}
|
||||
actual := values{c.ID, c.Secret, c.Username, c.Password}
|
||||
|
||||
if !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithBaseURL(t *testing.T) {
|
||||
baseURL := "http://localhost:8080"
|
||||
c, err := NewClient(nil, WithBaseURL(baseURL))
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := baseURL, c.BaseURL.String(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithTokenURL(t *testing.T) {
|
||||
tokenURL := "http://localhost:8080/api/v1/access_token"
|
||||
c, err := NewClient(nil, WithTokenURL(tokenURL))
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := tokenURL, c.TokenURL.String(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package geddit
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Opt is a configuration option to initialize a client
|
||||
type Opt func(*Client) error
|
||||
|
||||
// WithUserAgent sets the user agent for the client
|
||||
func WithUserAgent(ua string) Opt {
|
||||
return func(c *Client) error {
|
||||
c.UserAgent = fmt.Sprintf("%s %s", ua, c.UserAgent)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
mux *http.ServeMux
|
||||
ctx = context.Background()
|
||||
client *Client
|
||||
server *httptest.Server
|
||||
)
|
||||
|
||||
func setup() {
|
||||
mux = http.NewServeMux()
|
||||
server = httptest.NewServer(mux)
|
||||
|
||||
mux.HandleFunc("/api/v1/access_token", func(w http.ResponseWriter, r *http.Request) {
|
||||
response := `
|
||||
{
|
||||
"access_token": "token1",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "*"
|
||||
}
|
||||
`
|
||||
w.Header().Add(headerContentType, mediaTypeJSON)
|
||||
fmt.Fprint(w, response)
|
||||
})
|
||||
|
||||
client, _ = NewClient(nil,
|
||||
WithOAuth2("id1", "secret1", "user1", "password1"),
|
||||
WithBaseURL(server.URL),
|
||||
WithTokenURL(server.URL+"/api/v1/access_token"),
|
||||
)
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
server.Close()
|
||||
}
|
||||
|
||||
func testMethod(t *testing.T, r *http.Request, expected string) {
|
||||
if expected != r.Method {
|
||||
t.Fatalf("Request method = %v, expected %v", r.Method, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func readFileContents(t *testing.T, filepath string) string {
|
||||
file, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
return string(bytes)
|
||||
}
|
||||
|
||||
func testClientServices(t *testing.T, c *Client) {
|
||||
services := []string{
|
||||
"Comment",
|
||||
"Flair",
|
||||
"Link",
|
||||
"Listings",
|
||||
"Subreddit",
|
||||
"User",
|
||||
"Vote",
|
||||
}
|
||||
|
||||
cp := reflect.ValueOf(c)
|
||||
cv := reflect.Indirect(cp)
|
||||
|
||||
for _, s := range services {
|
||||
if cv.FieldByName(s).IsNil() {
|
||||
t.Fatalf("c.%s shouldn't be nil", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func testClientDefaultUserAgent(t *testing.T, c *Client) {
|
||||
if expect, actual := fmt.Sprintf("golang:%s:v%s (by /u/)", libraryName, libraryVersion), c.userAgent; expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %+v\nactual: %+v", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func testClientDefaults(t *testing.T, c *Client) {
|
||||
testClientDefaultUserAgent(t, c)
|
||||
testClientServices(t, c)
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
c, err := NewClient(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %+v", err)
|
||||
}
|
||||
testClientDefaults(t, c)
|
||||
}
|
||||
|
||||
func TestNewClient_Error(t *testing.T) {
|
||||
errorOpt := func(c *Client) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
|
||||
_, err := NewClient(nil, errorOpt)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nothing instead")
|
||||
}
|
||||
|
||||
if expect, actual := "foo", err.Error(); expect != actual {
|
||||
t.Fatalf("got unexpected error\nexpect: %+v\nactual: %+v", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// LinkService handles communication with the link (post)
|
||||
// related methods of the Reddit API
|
||||
type LinkService interface {
|
||||
Hide(ctx context.Context, ids ...string) (*Response, error)
|
||||
Unhide(ctx context.Context, ids ...string) (*Response, error)
|
||||
}
|
||||
|
||||
// LinkServiceOp implements the Vote interface
|
||||
type LinkServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ LinkService = &LinkServiceOp{}
|
||||
|
||||
// Hide hides links with the specified ids
|
||||
// On successful calls, it just returns {}
|
||||
func (s *LinkServiceOp) Hide(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/hide"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Unhide unhides links with the specified ids
|
||||
// On successful calls, it just returns {}
|
||||
func (s *LinkServiceOp) Unhide(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/unhide"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLinkServiceOp_Hide(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/hide", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "1,2,3")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Link.Hide(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nothing instead")
|
||||
}
|
||||
if expect, actual := `must provide at least 1 id`, err.Error(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
res, err := client.Link.Hide(ctx, "1", "2", "3")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := http.StatusOK, res.StatusCode; expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkServiceOp_Unhide(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/unhide", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "1,2,3")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Link.Unhide(ctx)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nothing instead")
|
||||
}
|
||||
if expect, actual := `must provide at least 1 id`, err.Error(); expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
res, err := client.Link.Unhide(ctx, "1", "2", "3")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if expect, actual := http.StatusOK, res.StatusCode; expect != actual {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
}
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ListingsService handles communication with the link (post)
|
||||
// related methods of the Reddit API
|
||||
type ListingsService interface {
|
||||
Get(ctx context.Context, ids ...string) (*Listing, *Response, error)
|
||||
}
|
||||
|
||||
// ListingsServiceOp implements the Vote interface
|
||||
type ListingsServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ ListingsService = &ListingsServiceOp{}
|
||||
|
||||
type listingRoot struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *struct {
|
||||
Dist int `json:"dist"`
|
||||
Children []map[string]interface{} `json:"children,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Listing holds various types of things that all come from the Reddit API
|
||||
type Listing struct {
|
||||
Links []*Submission `json:"links,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
Subreddits []*Subreddit `json:"subreddits,omitempty"`
|
||||
}
|
||||
|
||||
// Get gets a list of things based on their IDs
|
||||
// Only links, comments, and subreddits are allowed
|
||||
func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, *Response, error) {
|
||||
type query struct {
|
||||
IDs []string `url:"id,comma"`
|
||||
}
|
||||
|
||||
path := "api/info"
|
||||
path, err := addOptions(path, query{ids})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(listingRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if root.Data == nil {
|
||||
return nil, resp, nil
|
||||
}
|
||||
|
||||
l := new(Listing)
|
||||
|
||||
for _, result := range root.Data.Children {
|
||||
kind, ok1 := result["kind"].(string)
|
||||
data, ok2 := result["data"]
|
||||
|
||||
if ok1 && ok2 {
|
||||
byteValue, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
switch kind {
|
||||
case kindComment:
|
||||
v = new(Comment)
|
||||
l.Comments = append(l.Comments, v.(*Comment))
|
||||
case kindLink:
|
||||
v = new(Submission)
|
||||
l.Links = append(l.Links, v.(*Submission))
|
||||
case kindSubreddit:
|
||||
v = new(Subreddit)
|
||||
l.Subreddits = append(l.Subreddits, v.(*Subreddit))
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
err = json.Unmarshal(byteValue, v)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return l, resp, nil
|
||||
}
|
||||
|
||||
// todo: do by_id next
|
||||
+329
-11
@@ -5,12 +5,32 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SubredditService handles communication with the subreddit
|
||||
// related methods of the Reddit API
|
||||
type SubredditService interface {
|
||||
GetByName(ctx context.Context, name string) (*Subreddit, *Response, error)
|
||||
|
||||
GetPopular(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
GetNew(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
GetGold(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
GetDefault(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
|
||||
GetMineWhereSubscriber(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
GetMineWhereContributor(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
GetMineWhereModerator(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
GetMineWhereStreams(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
|
||||
|
||||
GetHotPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error)
|
||||
GetNewPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error)
|
||||
GetRisingPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error)
|
||||
GetControversialPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error)
|
||||
GetTopPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error)
|
||||
|
||||
GetSticky1(ctx context.Context, name string) (interface{}, *Response, error)
|
||||
GetSticky2(ctx context.Context, name string) (interface{}, *Response, error)
|
||||
}
|
||||
|
||||
// SubredditServiceOp implements the SubredditService interface
|
||||
@@ -25,20 +45,98 @@ type subredditRoot struct {
|
||||
Data *Subreddit `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type subredditRootListing struct {
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
Data *struct {
|
||||
Dist int `json:"dist"`
|
||||
Roots []subredditRoot `json:"children,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Subreddit holds information about a subreddit
|
||||
type Subreddit struct {
|
||||
ID *string `json:"id,omitempty"`
|
||||
FullID *string `json:"name,omitempty"`
|
||||
Created *float64 `json:"created_utc,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
Created float64 `json:"created"`
|
||||
CreatedUTC float64 `json:"created_utc"`
|
||||
|
||||
URL *string `json:"url,omitempty"`
|
||||
DisplayName *string `json:"display_name,omitempty"`
|
||||
DisplayNamePrefixed *string `json:"display_name_prefixed,omitempty"`
|
||||
Title *string `json:"title,omitempty"`
|
||||
PublicDescription *string `json:"public_description,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
DisplayNamePrefixed string `json:"display_name_prefixed,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
PublicDescription string `json:"public_description,omitempty"`
|
||||
Type string `json:"subreddit_type,omitempty"`
|
||||
SuggestedCommentSort string `json:"suggested_comment_sort,omitempty"`
|
||||
|
||||
Subscribers *int `json:"subscribers,omitempty"`
|
||||
Subscribers int `json:"subscribers"`
|
||||
ActiveUserCount *int `json:"active_user_count,omitempty"`
|
||||
NSFW bool `json:"over18"`
|
||||
UserIsMod bool `json:"user_is_moderator"`
|
||||
}
|
||||
|
||||
// SubredditList holds information about a list of subreddits
|
||||
// The after and before fields help decide the anchor point for a subsequent
|
||||
// call that returns a list
|
||||
type SubredditList struct {
|
||||
Subreddits []Subreddit `json:"subreddits,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
}
|
||||
|
||||
type submissionRoot struct {
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
Data *Submission `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type submissionRootListing struct {
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
Data *struct {
|
||||
Dist int `json:"dist"`
|
||||
Roots []submissionRoot `json:"children,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Submission is a submitted post on Reddit
|
||||
type Submission struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
Created float64 `json:"created"`
|
||||
CreatedUTC float64 `json:"created_utc"`
|
||||
|
||||
Permalink string `json:"permalink,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Body string `json:"selftext,omitempty"`
|
||||
Score int `json:"score"`
|
||||
NumberOfComments int `json:"num_comments"`
|
||||
|
||||
SubredditID string `json:"t5_2qo4s,omitempty"`
|
||||
SubredditName string `json:"subreddit,omitempty"`
|
||||
SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"`
|
||||
|
||||
AuthorID string `json:"author_fullname,omitempty"`
|
||||
AuthorName string `json:"author,omitempty"`
|
||||
|
||||
Spoiler bool `json:"spoiler"`
|
||||
Locked bool `json:"locked"`
|
||||
NSFW bool `json:"over_18"`
|
||||
IsSelfPost bool `json:"is_self"`
|
||||
Saved bool `json:"saved"`
|
||||
Stickied bool `json:"stickied"`
|
||||
}
|
||||
|
||||
// SubmissionList holds information about a list of subreddits
|
||||
// The after and before fields help decide the anchor point for a subsequent
|
||||
// call that returns a list
|
||||
type SubmissionList struct {
|
||||
Submissions []Submission `json:"submissions,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
}
|
||||
|
||||
// GetByName gets a subreddit by name
|
||||
@@ -47,8 +145,7 @@ func (s *SubredditServiceOp) GetByName(ctx context.Context, name string) (*Subre
|
||||
return nil, nil, errors.New("empty subreddit name provided")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/about.json", name)
|
||||
|
||||
path := fmt.Sprintf("r/%s/about", name)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -62,3 +159,224 @@ func (s *SubredditServiceOp) GetByName(ctx context.Context, name string) (*Subre
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// GetPopular returns popular subreddits
|
||||
func (s *SubredditServiceOp) GetPopular(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/popular", opts)
|
||||
}
|
||||
|
||||
// GetNew returns new subreddits
|
||||
func (s *SubredditServiceOp) GetNew(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/new", opts)
|
||||
}
|
||||
|
||||
// GetGold returns gold subreddits
|
||||
func (s *SubredditServiceOp) GetGold(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/gold", opts)
|
||||
}
|
||||
|
||||
// GetDefault returns default subreddits
|
||||
func (s *SubredditServiceOp) GetDefault(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/default", opts)
|
||||
}
|
||||
|
||||
// GetMineWhereSubscriber returns the list of subreddits the client is subscribed to
|
||||
func (s *SubredditServiceOp) GetMineWhereSubscriber(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/mine/subscriber", opts)
|
||||
}
|
||||
|
||||
// GetMineWhereContributor returns the list of subreddits the client is a contributor to
|
||||
func (s *SubredditServiceOp) GetMineWhereContributor(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/mine/contributor", opts)
|
||||
}
|
||||
|
||||
// GetMineWhereModerator returns the list of subreddits the client is a moderator in
|
||||
func (s *SubredditServiceOp) GetMineWhereModerator(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/mine/contributor", opts)
|
||||
}
|
||||
|
||||
// GetMineWhereStreams returns the list of subreddits the client is subscribed to and has hosted videos in
|
||||
func (s *SubredditServiceOp) GetMineWhereStreams(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/mine/contributor", opts)
|
||||
}
|
||||
|
||||
type sort int
|
||||
|
||||
const (
|
||||
sortHot sort = iota
|
||||
sortBest
|
||||
sortNew
|
||||
sortRising
|
||||
sortControversial
|
||||
sortTop
|
||||
)
|
||||
|
||||
var sorts = [...]string{
|
||||
"hot",
|
||||
"best",
|
||||
"new",
|
||||
"rising",
|
||||
"controversial",
|
||||
"top",
|
||||
}
|
||||
|
||||
// GetHotPosts returns the hot posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
// IMPORTANT: for subreddits, this will include the stickied posts (if any)
|
||||
// PLUS the number of posts from the limit parameter (which is 25 by default)
|
||||
func (s *SubredditServiceOp) GetHotPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
return s.getPosts(ctx, sortHot, opts, names...)
|
||||
}
|
||||
|
||||
// GetBestPosts returns the best posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
// IMPORTANT: for subreddits, this will include the stickied posts (if any)
|
||||
// PLUS the number of posts from the limit parameter (which is 25 by default)
|
||||
func (s *SubredditServiceOp) GetBestPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
return s.getPosts(ctx, sortBest, opts, names...)
|
||||
}
|
||||
|
||||
// GetNewPosts returns the new posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
func (s *SubredditServiceOp) GetNewPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
return s.getPosts(ctx, sortNew, opts, names...)
|
||||
}
|
||||
|
||||
// GetRisingPosts returns the rising posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
func (s *SubredditServiceOp) GetRisingPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
return s.getPosts(ctx, sortRising, opts, names...)
|
||||
}
|
||||
|
||||
// GetControversialPosts returns the controversial posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
func (s *SubredditServiceOp) GetControversialPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
return s.getPosts(ctx, sortControversial, opts, names...)
|
||||
}
|
||||
|
||||
// GetTopPosts returns the top posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
func (s *SubredditServiceOp) GetTopPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
return s.getPosts(ctx, sortTop, opts, names...)
|
||||
}
|
||||
|
||||
type sticky int
|
||||
|
||||
const (
|
||||
sticky1 sticky = iota + 1
|
||||
sticky2
|
||||
)
|
||||
|
||||
// GetSticky1 returns the first stickied post on a subreddit (if it exists)
|
||||
func (s *SubredditServiceOp) GetSticky1(ctx context.Context, name string) (interface{}, *Response, error) {
|
||||
return s.getSticky(ctx, name, sticky1)
|
||||
}
|
||||
|
||||
// GetSticky2 returns the second stickied post on a subreddit (if it exists)
|
||||
func (s *SubredditServiceOp) GetSticky2(ctx context.Context, name string) (interface{}, *Response, error) {
|
||||
return s.getSticky(ctx, name, sticky2)
|
||||
}
|
||||
|
||||
func (s *SubredditServiceOp) getSubreddits(ctx context.Context, path string, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(subredditRootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if root.Data == nil {
|
||||
return nil, resp, nil
|
||||
}
|
||||
|
||||
sl := new(SubredditList)
|
||||
var subreddits []Subreddit
|
||||
|
||||
for _, child := range root.Data.Roots {
|
||||
subreddits = append(subreddits, *child.Data)
|
||||
}
|
||||
sl.Subreddits = subreddits
|
||||
sl.After = root.Data.After
|
||||
sl.Before = root.Data.Before
|
||||
|
||||
return sl, resp, nil
|
||||
}
|
||||
|
||||
func (s *SubredditServiceOp) getPosts(ctx context.Context, sort sort, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
path := sorts[sort]
|
||||
if len(names) > 0 {
|
||||
path = fmt.Sprintf("r/%s/%s", strings.Join(names, "+"), sorts[sort])
|
||||
}
|
||||
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(submissionRootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if root.Data == nil {
|
||||
return nil, resp, nil
|
||||
}
|
||||
|
||||
sl := new(SubmissionList)
|
||||
var submissions []Submission
|
||||
|
||||
for _, child := range root.Data.Roots {
|
||||
submissions = append(submissions, *child.Data)
|
||||
}
|
||||
sl.Submissions = submissions
|
||||
sl.After = root.Data.After
|
||||
sl.Before = root.Data.Before
|
||||
|
||||
return sl, resp, nil
|
||||
}
|
||||
|
||||
// getSticky returns one of the 2 stickied posts of the subreddit
|
||||
// Num should be equal to 1 or 2, depending on which one you want
|
||||
// If it's <= 1, it's 1
|
||||
// If it's >= 2, it's 2
|
||||
// todo
|
||||
func (s *SubredditServiceOp) getSticky(ctx context.Context, name string, num sticky) (interface{}, *Response, error) {
|
||||
// type query struct {
|
||||
// Num sticky `url:"num"`
|
||||
// }
|
||||
|
||||
// path := fmt.Sprintf("r/%s/about/sticky", name)
|
||||
// path, err := addOptions(path, query{num})
|
||||
// if err != nil {
|
||||
// return nil, nil, err
|
||||
// }
|
||||
|
||||
// req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
// if err != nil {
|
||||
// return nil, nil, err
|
||||
// }
|
||||
|
||||
// root := new(submissionRootListing)
|
||||
// resp, err := s.client.Do(ctx, req, root)
|
||||
// if err != nil {
|
||||
// return nil, resp, err
|
||||
// }
|
||||
|
||||
// return nil, resp, nil
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"total_awards_received": 0,
|
||||
"approved_at_utc": null,
|
||||
"awarders": [],
|
||||
"mod_reason_by": null,
|
||||
"banned_by": null,
|
||||
"replies": "",
|
||||
"author_flair_type": "richtext",
|
||||
"removal_reason": null,
|
||||
"link_id": "t3_link1",
|
||||
"author_flair_template_id": "024b2b66-05ca-11e1-96f4-12313d096aae",
|
||||
"likes": true,
|
||||
"no_follow": false,
|
||||
"author_fullname": "t2_user1",
|
||||
"user_reports": [],
|
||||
"body_html": "<div class=\"md\"><p>test comment</p>\n</div>",
|
||||
"send_replies": true,
|
||||
"saved": false,
|
||||
"id": "test2",
|
||||
"banned_at_utc": null,
|
||||
"mod_reason_title": null,
|
||||
"gilded": 0,
|
||||
"archived": false,
|
||||
"report_reasons": null,
|
||||
"author": "reddit_username",
|
||||
"can_mod_post": false,
|
||||
"ups": 1,
|
||||
"parent_id": "t1_test",
|
||||
"score": 1,
|
||||
"approved_by": null,
|
||||
"author_premium": false,
|
||||
"all_awardings": [],
|
||||
"subreddit_id": "t5_test",
|
||||
"body": "test comment",
|
||||
"edited": false,
|
||||
"downs": 0,
|
||||
"author_flair_css_class": null,
|
||||
"is_submitter": false,
|
||||
"collapsed": false,
|
||||
"author_flair_richtext": [
|
||||
{
|
||||
"e": "text",
|
||||
"t": "Beginner - Strength"
|
||||
}
|
||||
],
|
||||
"author_patreon_flair": false,
|
||||
"collapsed_reason": null,
|
||||
"gildings": {},
|
||||
"associated_award": null,
|
||||
"stickied": false,
|
||||
"subreddit_type": "public",
|
||||
"can_gild": false,
|
||||
"subreddit": "subreddit",
|
||||
"author_flair_text_color": "dark",
|
||||
"score_hidden": false,
|
||||
"permalink": "/r/subreddit/comments/test1/some_thread/test2/",
|
||||
"num_reports": null,
|
||||
"locked": false,
|
||||
"name": "t1_test2",
|
||||
"created": 1588147787,
|
||||
"author_flair_text": "Flair",
|
||||
"treatment_tags": [],
|
||||
"rte_mode": "markdown",
|
||||
"created_utc": 1588118987,
|
||||
"subreddit_name_prefixed": "r/subreddit",
|
||||
"controversiality": 0,
|
||||
"author_flair_background_color": null,
|
||||
"collapsed_because_crowd_control": null,
|
||||
"mod_reports": [],
|
||||
"mod_note": null,
|
||||
"distinguished": null
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
[
|
||||
{
|
||||
"allowable_content": "all",
|
||||
"text": "Beginner",
|
||||
"text_color": "dark",
|
||||
"mod_only": false,
|
||||
"background_color": "",
|
||||
"id": "b8a1c822-3feb-11e8-88e1-0e5f55d58ce0",
|
||||
"css_class": "Beginner1",
|
||||
"max_emojis": 10,
|
||||
"richtext": [],
|
||||
"text_editable": false,
|
||||
"override_css": false,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"allowable_content": "all",
|
||||
"text": "Moderator",
|
||||
"text_color": "dark",
|
||||
"mod_only": true,
|
||||
"background_color": "",
|
||||
"id": "b8ea0fce-3feb-11e8-af7a-0e263a127cf8",
|
||||
"css_class": "Moderator1",
|
||||
"max_emojis": 10,
|
||||
"richtext": [],
|
||||
"text_editable": false,
|
||||
"override_css": false,
|
||||
"type": "text"
|
||||
}
|
||||
]
|
||||
Vendored
+20
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"text": "Beginner",
|
||||
"richtext": [],
|
||||
"text_editable": false,
|
||||
"override_css": false,
|
||||
"css_class": "Beginner1",
|
||||
"type": "text",
|
||||
"id": "b8a1c822-3feb-11e8-88e1-0e5f55d58ce0"
|
||||
},
|
||||
{
|
||||
"text": "Beginner",
|
||||
"richtext": [],
|
||||
"text_editable": false,
|
||||
"override_css": false,
|
||||
"css_class": "Beginner2",
|
||||
"type": "text",
|
||||
"id": "b8ea0fce-3feb-11e8-af7a-0e263a127cf8"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,210 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// UserService handles communication with the user
|
||||
// related methods of the Reddit API
|
||||
type UserService interface {
|
||||
Get(ctx context.Context, username string) (*User, *Response, error)
|
||||
GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserShort, *Response, error)
|
||||
UsernameAvailable(ctx context.Context, username string) (bool, *Response, error)
|
||||
|
||||
GetComments(ctx context.Context, sort sort, opts *ListOptions) (*CommentList, *Response, error)
|
||||
GetCommentsOf(ctx context.Context, username string, sort sort, opts *ListOptions) (*CommentList, *Response, error)
|
||||
}
|
||||
|
||||
// UserServiceOp implements the UserService interface
|
||||
type UserServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ UserService = &UserServiceOp{}
|
||||
|
||||
type userRoot struct {
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
Data *User `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a Reddit user
|
||||
type User struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Created float64 `json:"created"`
|
||||
CreatedUTC float64 `json:"created_utc"`
|
||||
|
||||
LinkKarma int `json:"link_karma"`
|
||||
CommentKarma int `json:"comment_karma"`
|
||||
|
||||
IsFriend bool `json:"is_friend"`
|
||||
IsEmployee bool `json:"is_employee"`
|
||||
HasVerifiedEmail bool `json:"has_verified_email"`
|
||||
NSFW bool `json:"over_18"`
|
||||
}
|
||||
|
||||
// UserShort represents a Reddit user, but contains fewer pieces of information
|
||||
// It is returned from the GET /api/user_data_by_account_ids endpoint
|
||||
type UserShort struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
CreatedUTC float64 `json:"created_utc"`
|
||||
|
||||
LinkKarma int `json:"link_karma"`
|
||||
CommentKarma int `json:"comment_karma"`
|
||||
|
||||
NSFW bool `json:"profile_over_18"`
|
||||
}
|
||||
|
||||
// Get returns information about the user
|
||||
func (s *UserServiceOp) Get(ctx context.Context, username string) (*User, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/about", username)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(userRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// GetMultipleByID returns multiple users from their full IDs
|
||||
// The response body is a map where the keys are the IDs (if they exist), and the value is the user
|
||||
func (s *UserServiceOp) GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserShort, *Response, error) {
|
||||
type query struct {
|
||||
IDs []string `url:"ids,omitempty,comma"`
|
||||
}
|
||||
|
||||
path := "api/user_data_by_account_ids"
|
||||
path, err := addOptions(path, query{ids})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(map[string]*UserShort)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return *root, resp, nil
|
||||
}
|
||||
|
||||
// GetComments returns a list of the client's comments
|
||||
func (s *UserServiceOp) GetComments(ctx context.Context, sort sort, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.GetCommentsOf(ctx, s.client.Username, sort, opts)
|
||||
}
|
||||
|
||||
// GetCommentsOf returns a list of the user's comments
|
||||
func (s *UserServiceOp) GetCommentsOf(ctx context.Context, username string, sort sort, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/comments?sort=%s", username, sorts[sort])
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(commentRootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
if root.Data == nil {
|
||||
return nil, resp, nil
|
||||
}
|
||||
|
||||
cl := new(CommentList)
|
||||
var comments []Comment
|
||||
|
||||
for _, child := range root.Data.Roots {
|
||||
comments = append(comments, *child.Data)
|
||||
}
|
||||
cl.Comments = comments
|
||||
cl.After = root.Data.After
|
||||
cl.Before = root.Data.Before
|
||||
|
||||
return cl, resp, nil
|
||||
}
|
||||
|
||||
// UsernameAvailable checks whether a username is available for registration
|
||||
// If a valid username is provided, this endpoint returns a body with just "true" or "false"
|
||||
// If an invalid username is provided, it returns the following:
|
||||
/*
|
||||
{
|
||||
"json": {
|
||||
"errors": [
|
||||
[
|
||||
"BAD_USERNAME",
|
||||
"invalid username",
|
||||
"user"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
*/
|
||||
func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) {
|
||||
type query struct {
|
||||
User string `url:"user,omitempty"`
|
||||
}
|
||||
|
||||
path := "api/username_available"
|
||||
path, err := addOptions(path, query{username})
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
root := new(bool)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if _, ok := err.(*json.UnmarshalTypeError); ok {
|
||||
return false, resp, fmt.Errorf("must provide username conforming to Reddit's criteria; username provided: %q", username)
|
||||
}
|
||||
if err != nil {
|
||||
return false, resp, err
|
||||
}
|
||||
|
||||
return *root, resp, nil
|
||||
}
|
||||
|
||||
// Friend creates or updates a "friend" relationship
|
||||
// Request body contains JSON data with:
|
||||
// name: existing Reddit username
|
||||
// note: a string no longer than 300 characters
|
||||
func (s *UserServiceOp) Friend(ctx context.Context, username string, note string) (interface{}, *Response, error) {
|
||||
type request struct {
|
||||
Name string `url:"name"`
|
||||
Note string `url:"note"`
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("api/v1/me/friends/%s", username)
|
||||
body := request{Name: username, Note: note}
|
||||
|
||||
_, err := s.client.NewRequest(http.MethodPut, path, body)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
// todo: requires gold
|
||||
return nil, nil, nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// String is a helper routine that allocates a new string value
|
||||
// to store v and returns a pointer to it.
|
||||
func String(v string) *string {
|
||||
p := new(string)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Int is a helper routine that allocates a new int32 value
|
||||
// to store v and returns a pointer to it, but unlike Int32
|
||||
// its argument value is an int.
|
||||
func Int(v int) *int {
|
||||
p := new(int)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Bool is a helper routine that allocates a new bool value
|
||||
// to store v and returns a pointer to it.
|
||||
func Bool(v bool) *bool {
|
||||
p := new(bool)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// StreamToString converts a reader to a string
|
||||
func StreamToString(stream io.Reader) string {
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(stream)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// var timestampType = reflect.TypeOf(Timestamp{})
|
||||
|
||||
// Stringify attempts to create a string representation of types
|
||||
func Stringify(message interface{}) string {
|
||||
var buf bytes.Buffer
|
||||
v := reflect.ValueOf(message)
|
||||
stringifyValue(&buf, v)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// stringifyValue was graciously cargoculted from the goprotubuf library
|
||||
func stringifyValue(w io.Writer, val reflect.Value) {
|
||||
if val.Kind() == reflect.Ptr && val.IsNil() {
|
||||
_, _ = w.Write([]byte("<nil>"))
|
||||
return
|
||||
}
|
||||
|
||||
v := reflect.Indirect(val)
|
||||
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
fmt.Fprintf(w, `"%s"`, v)
|
||||
case reflect.Slice:
|
||||
stringifySlice(w, v)
|
||||
return
|
||||
case reflect.Struct:
|
||||
stringifyStruct(w, v)
|
||||
default:
|
||||
if v.CanInterface() {
|
||||
fmt.Fprint(w, v.Interface())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stringifySlice(w io.Writer, v reflect.Value) {
|
||||
_, _ = w.Write([]byte{'['})
|
||||
for i := 0; i < v.Len(); i++ {
|
||||
if i > 0 {
|
||||
_, _ = w.Write([]byte{' '})
|
||||
}
|
||||
|
||||
stringifyValue(w, v.Index(i))
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte{']'})
|
||||
}
|
||||
|
||||
func stringifyStruct(w io.Writer, v reflect.Value) {
|
||||
if v.Type().Name() != "" {
|
||||
_, _ = w.Write([]byte(v.Type().String()))
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte{'{'})
|
||||
|
||||
var sep bool
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fv := v.Field(i)
|
||||
if fv.Kind() == reflect.Ptr && fv.IsNil() {
|
||||
continue
|
||||
}
|
||||
if fv.Kind() == reflect.Slice && fv.IsNil() {
|
||||
continue
|
||||
}
|
||||
|
||||
if sep {
|
||||
_, _ = w.Write([]byte(", "))
|
||||
} else {
|
||||
sep = true
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(v.Type().Field(i).Name))
|
||||
_, _ = w.Write([]byte{':'})
|
||||
stringifyValue(w, fv)
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte{'}'})
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// VoteService handles communication with the upvote/downvote
|
||||
// related methods of the Reddit API
|
||||
type VoteService interface {
|
||||
Up(ctx context.Context, id string) (*Response, error)
|
||||
Down(ctx context.Context, id string) (*Response, error)
|
||||
Remove(ctx context.Context, id string) (*Response, error)
|
||||
}
|
||||
|
||||
// VoteServiceOp implements the Vote interface
|
||||
type VoteServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ VoteService = &VoteServiceOp{}
|
||||
|
||||
type vote int
|
||||
|
||||
// Reddit interprets -1, 0, 1 as downvote, no vote, and upvote, respectively.
|
||||
const (
|
||||
downvote vote = iota - 1
|
||||
novote
|
||||
upvote
|
||||
)
|
||||
|
||||
func (s *VoteServiceOp) vote(ctx context.Context, id string, vote vote) (*Response, error) {
|
||||
path := "api/vote"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
form.Set("dir", fmt.Sprint(vote))
|
||||
form.Set("rank", "10")
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Up upvotes a link or a comment
|
||||
func (s *VoteServiceOp) Up(ctx context.Context, id string) (*Response, error) {
|
||||
return s.vote(ctx, id, upvote)
|
||||
}
|
||||
|
||||
// Down downvotes a link or a comment
|
||||
func (s *VoteServiceOp) Down(ctx context.Context, id string) (*Response, error) {
|
||||
return s.vote(ctx, id, downvote)
|
||||
}
|
||||
|
||||
// Remove removes the user's vote on a link or a comment
|
||||
func (s *VoteServiceOp) Remove(ctx context.Context, id string) (*Response, error) {
|
||||
return s.vote(ctx, id, novote)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package geddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVoteServiceOp_Up(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/vote", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t3_test")
|
||||
form.Set("dir", fmt.Sprint(upvote))
|
||||
form.Set("rank", "10")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Vote.Up(ctx, "t3_test")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVoteServiceOp_Down(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/vote", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t3_test")
|
||||
form.Set("dir", fmt.Sprint(downvote))
|
||||
form.Set("rank", "10")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Vote.Down(ctx, "t3_test")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVoteServiceOp_Remove(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/vote", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, http.MethodPost)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t3_test")
|
||||
form.Set("dir", fmt.Sprint(novote))
|
||||
form.Set("rank", "10")
|
||||
|
||||
_ = r.ParseForm()
|
||||
if expect, actual := form, r.PostForm; !reflect.DeepEqual(expect, actual) {
|
||||
t.Fatalf("got unexpected value\nexpect: %s\nactual: %s", Stringify(expect), Stringify(actual))
|
||||
}
|
||||
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
_, err := client.Vote.Remove(ctx, "t3_test")
|
||||
if err != nil {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user