Handle listings better by using custom unmarshaling
Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
parent
460554e19e
commit
7922711d51
9 changed files with 988 additions and 345 deletions
55
comment.go
55
comment.go
|
@ -25,61 +25,6 @@ type CommentServiceOp struct {
|
|||
|
||||
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
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var expectedCommentSubmitOrEdit = &Comment{
|
||||
|
@ -15,7 +16,6 @@ var expectedCommentSubmitOrEdit = &Comment{
|
|||
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",
|
||||
|
@ -27,8 +27,7 @@ var expectedCommentSubmitOrEdit = &Comment{
|
|||
Score: 1,
|
||||
Controversiality: 0,
|
||||
|
||||
Created: 1588147787,
|
||||
CreatedUTC: 1588118987,
|
||||
Created: &Timestamp{time.Date(2020, 4, 28, 20, 9, 47, 0, time.UTC)},
|
||||
|
||||
LinkID: "t3_link1",
|
||||
}
|
||||
|
|
192
geddit.go
192
geddit.go
|
@ -343,7 +343,6 @@ func CheckResponse(r *http.Response) error {
|
|||
|
||||
// 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
|
||||
|
@ -354,7 +353,6 @@ type ListOptions struct {
|
|||
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) {
|
||||
|
@ -388,6 +386,61 @@ type root struct {
|
|||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type rootListing struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *Listing `json:"data"`
|
||||
}
|
||||
|
||||
// Listing holds things coming from the Reddit API
|
||||
// It also contains the after/before anchors useful for subsequent requests
|
||||
type Listing struct {
|
||||
Things Things `json:"children"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// Things are stuff!
|
||||
type Things struct {
|
||||
Comments []Comment `json:"comments,omitempty"`
|
||||
Links []Link `json:"links,omitempty"`
|
||||
Subreddits []Subreddit `json:"subreddits,omitempty"`
|
||||
// todo: add the other kinds of things
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (l *Things) UnmarshalJSON(b []byte) error {
|
||||
var children []map[string]interface{}
|
||||
if err := json.Unmarshal(b, &children); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, child := range children {
|
||||
byteValue, _ := json.Marshal(child)
|
||||
switch child["kind"] {
|
||||
case kindComment:
|
||||
root := new(commentRoot)
|
||||
if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil {
|
||||
l.Comments = append(l.Comments, *root.Data)
|
||||
}
|
||||
case kindAccount:
|
||||
case kindLink:
|
||||
root := new(linkRoot)
|
||||
if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil {
|
||||
l.Links = append(l.Links, *root.Data)
|
||||
}
|
||||
case kindMessage:
|
||||
case kindSubreddit:
|
||||
root := new(subredditRoot)
|
||||
if err := json.Unmarshal(byteValue, root); err == nil && root.Data != nil {
|
||||
l.Subreddits = append(l.Subreddits, *root.Data)
|
||||
}
|
||||
case kindAward:
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
kindListing string = "Listing"
|
||||
kindComment string = "t1"
|
||||
|
@ -397,3 +450,138 @@ const (
|
|||
kindSubreddit string = "t5"
|
||||
kindAward string = "t6"
|
||||
)
|
||||
|
||||
type commentRoot struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *Comment `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type linkRoot struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *Link `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type subredditRoot struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *Subreddit `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"`
|
||||
Author string `json:"author,omitempty"`
|
||||
AuthorID string `json:"author_fullname,omitempty"`
|
||||
AuthorFlairText string `json:"author_flair_text,omitempty"`
|
||||
AuthorFlairID string `json:"author_flair_template_id,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 *Timestamp `json:"created_utc,omitempty"`
|
||||
Edited *Timestamp `json:"edited,omitempty"`
|
||||
|
||||
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"`
|
||||
|
||||
// If a comment has no replies, its "replies" value is "",
|
||||
// which the unmarshaler doesn't like
|
||||
// So we capture this varying field in RepliesRaw, and then
|
||||
// fill it in Replies
|
||||
RepliesRaw json.RawMessage `json:"replies,omitempty"`
|
||||
Replies []commentRoot `json:"-"`
|
||||
}
|
||||
|
||||
// Link is a submitted post on Reddit
|
||||
type Link struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
Edited *Timestamp `json:"edited,omitempty"`
|
||||
|
||||
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:"subreddit_id,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"`
|
||||
}
|
||||
|
||||
// Subreddit holds information about a subreddit
|
||||
type Subreddit struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
URL string `json:"url,omitempty"`
|
||||
Name string `json:"display_name,omitempty"`
|
||||
NamePrefixed 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"`
|
||||
ActiveUserCount *int `json:"active_user_count,omitempty"`
|
||||
NSFW bool `json:"over18"`
|
||||
UserIsMod bool `json:"user_is_moderator"`
|
||||
}
|
||||
|
||||
func (rl *rootListing) getLinks() *LinkList {
|
||||
if rl == nil || rl.Data == nil {
|
||||
return nil
|
||||
}
|
||||
return &LinkList{
|
||||
Links: rl.Data.Things.Links,
|
||||
After: rl.Data.After,
|
||||
Before: rl.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
func (rl *rootListing) getComments() *CommentList {
|
||||
if rl == nil || rl.Data == nil {
|
||||
return nil
|
||||
}
|
||||
return &CommentList{
|
||||
Comments: rl.Data.Things.Comments,
|
||||
After: rl.Data.After,
|
||||
Before: rl.Data.Before,
|
||||
}
|
||||
}
|
||||
|
|
54
listings.go
54
listings.go
|
@ -2,7 +2,6 @@ package geddit
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
|
@ -30,14 +29,15 @@ type listingRoot struct {
|
|||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
// 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
|
||||
// todo: only links, comments, subreddits
|
||||
func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, *Response, error) {
|
||||
type query struct {
|
||||
IDs []string `url:"id,comma"`
|
||||
|
@ -54,51 +54,13 @@ func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, *
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(listingRoot)
|
||||
root := new(rootListing)
|
||||
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
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// todo: do by_id next
|
||||
|
|
34
private-messages.go
Normal file
34
private-messages.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
package geddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// PrivateMessageService handles communication with the private message
|
||||
// related methods of the Reddit API
|
||||
type PrivateMessageService interface {
|
||||
BlockUser(ctx context.Context, messageID string) (*Response, error)
|
||||
}
|
||||
|
||||
// PrivateMessageServiceOp implements the PrivateMessageService interface
|
||||
type PrivateMessageServiceOp struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
var _ PrivateMessageService = &PrivateMessageServiceOp{}
|
||||
|
||||
// BlockUser blocks a user based on the ID of the private message
|
||||
func (s *PrivateMessageServiceOp) BlockUser(ctx context.Context, messageID string) (*Response, error) {
|
||||
path := "api/block"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", messageID)
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
439
subreddit.go
439
subreddit.go
|
@ -5,7 +5,9 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SubredditService handles communication with the subreddit
|
||||
|
@ -23,14 +25,22 @@ type SubredditService interface {
|
|||
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)
|
||||
GetHotLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
|
||||
GetBestLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
|
||||
GetNewLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
|
||||
GetRisingLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
|
||||
GetControversialLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
|
||||
GetTopLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
|
||||
|
||||
GetSticky1(ctx context.Context, name string) (interface{}, *Response, error)
|
||||
GetSticky2(ctx context.Context, name string) (interface{}, *Response, error)
|
||||
// GetSticky1(ctx context.Context, name string) (interface{}, *Response, error)
|
||||
// GetSticky2(ctx context.Context, name string) (interface{}, *Response, error)
|
||||
|
||||
Subscribe(ctx context.Context, names ...string) (*Response, error)
|
||||
SubscribeByID(ctx context.Context, ids ...string) (*Response, error)
|
||||
Unsubscribe(ctx context.Context, names ...string) (*Response, error)
|
||||
UnsubscribeByID(ctx context.Context, ids ...string) (*Response, error)
|
||||
|
||||
StreamLinks(ctx context.Context, names ...string) (<-chan Link, chan<- bool, error)
|
||||
}
|
||||
|
||||
// SubredditServiceOp implements the SubredditService interface
|
||||
|
@ -40,42 +50,6 @@ type SubredditServiceOp struct {
|
|||
|
||||
var _ SubredditService = &SubredditServiceOp{}
|
||||
|
||||
type subredditRoot struct {
|
||||
Kind *string `json:"kind,omitempty"`
|
||||
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"`
|
||||
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"`
|
||||
Type string `json:"subreddit_type,omitempty"`
|
||||
SuggestedCommentSort string `json:"suggested_comment_sort,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
|
||||
|
@ -85,58 +59,14 @@ type SubredditList struct {
|
|||
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
|
||||
// LinkList holds information about a list of links
|
||||
// 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"`
|
||||
// Note: not to be confused with linked lists
|
||||
type LinkList struct {
|
||||
Links []Link `json:"submissions,omitempty"`
|
||||
After string `json:"after,omitempty"`
|
||||
Before string `json:"before,omitempty"`
|
||||
}
|
||||
|
||||
// GetByName gets a subreddit by name
|
||||
|
@ -220,44 +150,44 @@ var sorts = [...]string{
|
|||
"top",
|
||||
}
|
||||
|
||||
// GetHotPosts returns the hot posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
// GetHotLinks returns the hot links
|
||||
// If no subreddit names are provided, then it runs the search against all those the client is subscribed to
|
||||
// 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...)
|
||||
func (s *SubredditServiceOp) GetHotLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, sortHot, opts, names...)
|
||||
}
|
||||
|
||||
// GetBestPosts returns the best posts
|
||||
// If no subreddit names are provided, then it runs the search against /r/all
|
||||
// GetBestLinks returns the best links
|
||||
// If no subreddit names are provided, then it runs the search against all those the client is subscribed to
|
||||
// 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...)
|
||||
func (s *SubredditServiceOp) GetBestLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
|
||||
return s.getLinks(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...)
|
||||
// GetNewLinks returns the new links
|
||||
// If no subreddit names are provided, then it runs the search against all those the client is subscribed to
|
||||
func (s *SubredditServiceOp) GetNewLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
|
||||
return s.getLinks(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...)
|
||||
// GetRisingLinks returns the rising links
|
||||
// If no subreddit names are provided, then it runs the search against all those the client is subscribed to
|
||||
func (s *SubredditServiceOp) GetRisingLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
|
||||
return s.getLinks(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...)
|
||||
// GetControversialLinks returns the controversial links
|
||||
// If no subreddit names are provided, then it runs the search against all those the client is subscribed to
|
||||
func (s *SubredditServiceOp) GetControversialLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
|
||||
return s.getLinks(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...)
|
||||
// GetTopLinks returns the top links
|
||||
// If no subreddit names are provided, then it runs the search against all those the client is subscribed to
|
||||
func (s *SubredditServiceOp) GetTopLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, sortTop, opts, names...)
|
||||
}
|
||||
|
||||
type sticky int
|
||||
|
@ -267,14 +197,65 @@ const (
|
|||
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)
|
||||
// // 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)
|
||||
// }
|
||||
|
||||
// Subscribe subscribes to subreddits based on their name
|
||||
// Returns {} on success
|
||||
func (s *SubredditServiceOp) Subscribe(ctx context.Context, names ...string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("action", "sub")
|
||||
form.Set("sr_name", strings.Join(names, ","))
|
||||
return s.handleSubscription(ctx, form)
|
||||
}
|
||||
|
||||
// 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)
|
||||
// SubscribeByID subscribes to subreddits based on their id
|
||||
// Returns {} on success
|
||||
func (s *SubredditServiceOp) SubscribeByID(ctx context.Context, ids ...string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("action", "sub")
|
||||
form.Set("sr", strings.Join(ids, ","))
|
||||
return s.handleSubscription(ctx, form)
|
||||
}
|
||||
|
||||
// Unsubscribe unsubscribes from subreddits
|
||||
// Returns {} on success
|
||||
func (s *SubredditServiceOp) Unsubscribe(ctx context.Context, names ...string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("action", "unsub")
|
||||
form.Set("sr_name", strings.Join(names, ","))
|
||||
return s.handleSubscription(ctx, form)
|
||||
}
|
||||
|
||||
// UnsubscribeByID unsubscribes from subreddits based on their id
|
||||
// Returns {} on success
|
||||
func (s *SubredditServiceOp) UnsubscribeByID(ctx context.Context, ids ...string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("action", "unsub")
|
||||
form.Set("sr", strings.Join(ids, ","))
|
||||
return s.handleSubscription(ctx, form)
|
||||
}
|
||||
|
||||
func (s *SubredditServiceOp) handleSubscription(ctx context.Context, form url.Values) (*Response, error) {
|
||||
path := "api/subscribe"
|
||||
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
|
||||
}
|
||||
|
||||
func (s *SubredditServiceOp) getSubreddits(ctx context.Context, path string, opts *ListOptions) (*SubredditList, *Response, error) {
|
||||
|
@ -288,30 +269,24 @@ func (s *SubredditServiceOp) getSubreddits(ctx context.Context, path string, opt
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(subredditRootListing)
|
||||
root := new(rootListing)
|
||||
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(SubredditList)
|
||||
|
||||
if root.Data != nil {
|
||||
l.Subreddits = root.Data.Things.Subreddits
|
||||
l.After = root.Data.After
|
||||
l.Before = root.Data.Before
|
||||
}
|
||||
|
||||
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
|
||||
return l, resp, nil
|
||||
}
|
||||
|
||||
func (s *SubredditServiceOp) getPosts(ctx context.Context, sort sort, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) {
|
||||
func (s *SubredditServiceOp) getLinks(ctx context.Context, sort sort, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
|
||||
path := sorts[sort]
|
||||
if len(names) > 0 {
|
||||
path = fmt.Sprintf("r/%s/%s", strings.Join(names, "+"), sorts[sort])
|
||||
|
@ -327,27 +302,21 @@ func (s *SubredditServiceOp) getPosts(ctx context.Context, sort sort, opts *List
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(submissionRootListing)
|
||||
root := new(rootListing)
|
||||
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(LinkList)
|
||||
|
||||
if root.Data != nil {
|
||||
l.Links = root.Data.Things.Links
|
||||
l.After = root.Data.After
|
||||
l.Before = root.Data.Before
|
||||
}
|
||||
|
||||
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
|
||||
return l, resp, nil
|
||||
}
|
||||
|
||||
// getSticky returns one of the 2 stickied posts of the subreddit
|
||||
|
@ -355,28 +324,154 @@ func (s *SubredditServiceOp) getPosts(ctx context.Context, sort sort, opts *List
|
|||
// 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"`
|
||||
// }
|
||||
// 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
|
||||
// }
|
||||
// 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
|
||||
// }
|
||||
// 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
|
||||
// }
|
||||
// var root []rootListing
|
||||
// resp, err := s.client.Do(ctx, req, &root)
|
||||
// if err != nil {
|
||||
// return nil, resp, err
|
||||
// }
|
||||
|
||||
// return nil, resp, nil
|
||||
return nil, nil, nil
|
||||
// // test, _ := json.MarshalIndent(root, "", " ")
|
||||
// // fmt.Println(string(test))
|
||||
|
||||
// linkRoot := new(linkRoot)
|
||||
|
||||
// link := root[0].Data.Children[0]
|
||||
// byteValue, err := json.Marshal(link)
|
||||
// if err != nil {
|
||||
// return nil, resp, err
|
||||
// }
|
||||
|
||||
// err = json.Unmarshal(byteValue, linkRoot)
|
||||
// if err != nil {
|
||||
// return nil, resp, err
|
||||
// }
|
||||
|
||||
// // these are all the comments in the post
|
||||
// comments := root[1].Data.Children
|
||||
|
||||
// var commentsRoot []commentRoot
|
||||
// byteValue, err = json.Marshal(comments)
|
||||
// if err != nil {
|
||||
// return nil, resp, err
|
||||
// }
|
||||
|
||||
// err = json.Unmarshal(byteValue, &commentsRoot)
|
||||
// if err != nil {
|
||||
// return nil, resp, err
|
||||
// }
|
||||
|
||||
// test, _ := json.MarshalIndent(commentsRoot, "", " ")
|
||||
// fmt.Println(string(test))
|
||||
|
||||
// for _, comment := range commentsRoot {
|
||||
// if string(comment.Data.RepliesRaw) == `""` {
|
||||
// comment.Data.Replies = nil
|
||||
// continue
|
||||
// }
|
||||
|
||||
// // var
|
||||
// }
|
||||
|
||||
// return commentsRoot, resp, nil
|
||||
// }
|
||||
|
||||
// func handleComments(comments []commentRoot) {
|
||||
// for _, comment := range comments {
|
||||
// if string(comment.Data.RepliesRaw) == `""` {
|
||||
// comment.Data.Replies = nil
|
||||
// continue
|
||||
// }
|
||||
|
||||
// }
|
||||
// }
|
||||
|
||||
// StreamLinks returns a channel that receives new submissions from the subreddits
|
||||
// To stop the stream, simply send a bool value to the stop channel
|
||||
func (s *SubredditServiceOp) StreamLinks(ctx context.Context, names ...string) (<-chan Link, chan<- bool, error) {
|
||||
if len(names) == 0 {
|
||||
return nil, nil, errors.New("must specify at least one subreddit")
|
||||
}
|
||||
|
||||
submissionCh := make(chan Link)
|
||||
stop := make(chan bool, 1)
|
||||
|
||||
go func() {
|
||||
// todo: if the post with the before gets deleted, you keep getting 0 posts
|
||||
var last *Timestamp
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
close(submissionCh)
|
||||
return
|
||||
default:
|
||||
sl, _, err := s.GetNewLinks(ctx, nil, names...)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var newest *Timestamp
|
||||
for i, submission := range sl.Links {
|
||||
if i == 0 {
|
||||
newest = submission.Created
|
||||
}
|
||||
if last == nil {
|
||||
submissionCh <- submission
|
||||
continue
|
||||
}
|
||||
if last.Before(*submission.Created) {
|
||||
submissionCh <- submission
|
||||
}
|
||||
}
|
||||
last = newest
|
||||
}
|
||||
<-time.After(time.Second * 3)
|
||||
fmt.Println()
|
||||
}
|
||||
}()
|
||||
|
||||
// go func() {
|
||||
// var before string
|
||||
// for {
|
||||
// select {
|
||||
// case <-stop:
|
||||
// close(submissionCh)
|
||||
// return
|
||||
// default:
|
||||
// sl, _, err := s.GetSubmissions(ctx, SortNew, &ListOptions{Before: before}, names...)
|
||||
// if err != nil {
|
||||
// continue
|
||||
// }
|
||||
// fmt.Printf("Received %d posts\n", len(sl.Submissions))
|
||||
|
||||
// if len(sl.Submissions) == 0 {
|
||||
// continue
|
||||
// }
|
||||
|
||||
// for _, submission := range sl.Submissions {
|
||||
// submissionCh <- submission
|
||||
// }
|
||||
// before = sl.Submissions[0].FullID
|
||||
// }
|
||||
// <-time.After(time.Second * 5)
|
||||
// fmt.Println()
|
||||
// }
|
||||
// }()
|
||||
|
||||
return submissionCh, stop, nil
|
||||
}
|
||||
|
|
52
timestamp.go
Normal file
52
timestamp.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package geddit
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Timestamp represents a time that can be unmarshalled from a JSON string
|
||||
// formatted as either an RFC3339 or Unix timestamp.
|
||||
type Timestamp struct {
|
||||
time.Time
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (t *Timestamp) MarshalJSON() ([]byte, error) {
|
||||
if t == nil || t.Time.IsZero() {
|
||||
return []byte(`false`), nil
|
||||
}
|
||||
|
||||
parsed := t.Time.Format(time.RFC3339)
|
||||
return []byte(`"` + parsed + `"`), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
// Time is expected in RFC3339 or Unix format.
|
||||
func (t *Timestamp) UnmarshalJSON(data []byte) (err error) {
|
||||
str := string(data)
|
||||
|
||||
// "edited" for comments and links is either false, or a timestamp
|
||||
if str == "false" {
|
||||
return
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(str, 64)
|
||||
if err == nil {
|
||||
t.Time = time.Unix(int64(f), 0).UTC()
|
||||
} else {
|
||||
t.Time, err = time.Parse(`"`+time.RFC3339+`"`, str)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Equal reports whether t and u are equal based on time.Equal
|
||||
func (t Timestamp) Equal(u Timestamp) bool {
|
||||
return t.Time.Equal(u.Time)
|
||||
}
|
||||
|
||||
// Before reports whether u is before t based on time.Before
|
||||
func (t Timestamp) Before(u Timestamp) bool {
|
||||
return t.Time.Before(u.Time)
|
||||
}
|
182
timestamp_test.go
Normal file
182
timestamp_test.go
Normal file
|
@ -0,0 +1,182 @@
|
|||
package geddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
emptyTimeStr = `"0001-01-01T00:00:00Z"`
|
||||
referenceTimeStr = `"2006-01-02T15:04:05Z"`
|
||||
referenceUnixTimeStr = `1136214245`
|
||||
)
|
||||
|
||||
var (
|
||||
referenceTime = time.Date(2006, time.January, 02, 15, 04, 05, 0, time.UTC)
|
||||
unixOrigin = time.Unix(0, 0).In(time.UTC)
|
||||
)
|
||||
|
||||
func TestTimestamp_Marshal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
data Timestamp
|
||||
want string
|
||||
wantErr bool
|
||||
equal bool
|
||||
}{
|
||||
{"Reference", Timestamp{referenceTime}, referenceTimeStr, false, true},
|
||||
{"Empty", Timestamp{}, emptyTimeStr, false, true},
|
||||
{"Mismatch", Timestamp{}, referenceTimeStr, false, false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
out, err := json.Marshal(tc.data)
|
||||
if gotErr := err != nil; gotErr != tc.wantErr {
|
||||
t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
|
||||
}
|
||||
got := string(out)
|
||||
equal := got == tc.want
|
||||
if (got == tc.want) != tc.equal {
|
||||
t.Fatalf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimestamp_Unmarshal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
data string
|
||||
want Timestamp
|
||||
wantErr bool
|
||||
equal bool
|
||||
}{
|
||||
{"Reference", referenceTimeStr, Timestamp{referenceTime}, false, true},
|
||||
{"ReferenceUnix", referenceUnixTimeStr, Timestamp{referenceTime}, false, true},
|
||||
{"Empty", emptyTimeStr, Timestamp{}, false, true},
|
||||
{"UnixStart", `0`, Timestamp{unixOrigin}, false, true},
|
||||
{"Mismatch", referenceTimeStr, Timestamp{}, false, false},
|
||||
{"MismatchUnix", `0`, Timestamp{}, false, false},
|
||||
{"Invalid", `"asdf"`, Timestamp{referenceTime}, true, false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
var got Timestamp
|
||||
err := json.Unmarshal([]byte(tc.data), &got)
|
||||
if gotErr := err != nil; gotErr != tc.wantErr {
|
||||
t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
|
||||
continue
|
||||
}
|
||||
equal := got.Equal(tc.want)
|
||||
if equal != tc.equal {
|
||||
t.Fatalf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimstamp_MarshalReflexivity(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
data Timestamp
|
||||
}{
|
||||
{"Reference", Timestamp{referenceTime}},
|
||||
{"Empty", Timestamp{}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
data, err := json.Marshal(tc.data)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Marshal err=%v", tc.desc, err)
|
||||
}
|
||||
var got Timestamp
|
||||
err = json.Unmarshal(data, &got)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unmarshal err=%v", tc.desc, err)
|
||||
}
|
||||
if !got.Equal(tc.data) {
|
||||
t.Fatalf("%s: %+v != %+v", tc.desc, got, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type WrappedTimestamp struct {
|
||||
A int
|
||||
Time Timestamp
|
||||
}
|
||||
|
||||
func TestWrappedTimstamp_Marshal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
data WrappedTimestamp
|
||||
want string
|
||||
wantErr bool
|
||||
equal bool
|
||||
}{
|
||||
{"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, true},
|
||||
{"Empty", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, emptyTimeStr), false, true},
|
||||
{"Mismatch", WrappedTimestamp{}, fmt.Sprintf(`{"A":0,"Time":%s}`, referenceTimeStr), false, false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
out, err := json.Marshal(tc.data)
|
||||
if gotErr := err != nil; gotErr != tc.wantErr {
|
||||
t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
|
||||
}
|
||||
got := string(out)
|
||||
equal := got == tc.want
|
||||
if equal != tc.equal {
|
||||
t.Fatalf("%s: got=%s, want=%s, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedTimstamp_Unmarshal(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
data string
|
||||
want WrappedTimestamp
|
||||
wantErr bool
|
||||
equal bool
|
||||
}{
|
||||
{"Reference", referenceTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
|
||||
{"ReferenceUnix", referenceUnixTimeStr, WrappedTimestamp{0, Timestamp{referenceTime}}, false, true},
|
||||
{"Empty", emptyTimeStr, WrappedTimestamp{0, Timestamp{}}, false, true},
|
||||
{"UnixStart", `0`, WrappedTimestamp{0, Timestamp{unixOrigin}}, false, true},
|
||||
{"Mismatch", referenceTimeStr, WrappedTimestamp{0, Timestamp{}}, false, false},
|
||||
{"MismatchUnix", `0`, WrappedTimestamp{0, Timestamp{}}, false, false},
|
||||
{"Invalid", `"asdf"`, WrappedTimestamp{0, Timestamp{referenceTime}}, true, false},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
var got Timestamp
|
||||
err := json.Unmarshal([]byte(tc.data), &got)
|
||||
if gotErr := err != nil; gotErr != tc.wantErr {
|
||||
t.Fatalf("%s: gotErr=%v, wantErr=%v, err=%v", tc.desc, gotErr, tc.wantErr, err)
|
||||
continue
|
||||
}
|
||||
equal := got.Time.Equal(tc.want.Time.Time)
|
||||
if equal != tc.equal {
|
||||
t.Fatalf("%s: got=%#v, want=%#v, equal=%v, want=%v", tc.desc, got, tc.want, equal, tc.equal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrappedTimstamp_MarshalReflexivity(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
data WrappedTimestamp
|
||||
}{
|
||||
{"Reference", WrappedTimestamp{0, Timestamp{referenceTime}}},
|
||||
{"Empty", WrappedTimestamp{0, Timestamp{}}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
bytes, err := json.Marshal(tc.data)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Marshal err=%v", tc.desc, err)
|
||||
}
|
||||
var got WrappedTimestamp
|
||||
err = json.Unmarshal(bytes, &got)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: Unmarshal err=%v", tc.desc, err)
|
||||
}
|
||||
if !got.Time.Equal(tc.data.Time) {
|
||||
t.Fatalf("%s: %+v != %+v", tc.desc, got, tc.data)
|
||||
}
|
||||
}
|
||||
}
|
320
user.go
320
user.go
|
@ -2,9 +2,9 @@ package geddit
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// UserService handles communication with the user
|
||||
|
@ -14,8 +14,36 @@ type UserService interface {
|
|||
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)
|
||||
// returns the client's links
|
||||
GetHotLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetNewLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetTopLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetControversialLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error)
|
||||
|
||||
// returns the links of the user with the username
|
||||
GetHotLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetNewLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetTopLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetControversialLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error)
|
||||
|
||||
GetUpvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetDownvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error)
|
||||
GetHidden(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error)
|
||||
|
||||
// returns the client's comments
|
||||
GetHotComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error)
|
||||
GetNewComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error)
|
||||
GetTopComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error)
|
||||
GetControversialComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error)
|
||||
|
||||
// returns the comments of the user with the username
|
||||
GetHotCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error)
|
||||
GetNewCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error)
|
||||
GetTopCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error)
|
||||
GetControversialCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error)
|
||||
|
||||
Unblock(ctx context.Context, username string) (*Response, error)
|
||||
Unfriend(ctx context.Context, username string) (*Response, error)
|
||||
}
|
||||
|
||||
// UserServiceOp implements the UserService interface
|
||||
|
@ -32,10 +60,10 @@ type userRoot struct {
|
|||
|
||||
// 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"`
|
||||
// is not the full ID, watch out
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
LinkKarma int `json:"link_karma"`
|
||||
CommentKarma int `json:"comment_karma"`
|
||||
|
@ -49,8 +77,8 @@ type User struct {
|
|||
// 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"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
LinkKarma int `json:"link_karma"`
|
||||
CommentKarma int `json:"comment_karma"`
|
||||
|
@ -102,63 +130,8 @@ func (s *UserServiceOp) GetMultipleByID(ctx context.Context, ids ...string) (map
|
|||
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"`
|
||||
|
@ -177,9 +150,6 @@ func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string)
|
|||
|
||||
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
|
||||
}
|
||||
|
@ -208,3 +178,219 @@ func (s *UserServiceOp) Friend(ctx context.Context, username string, note string
|
|||
// todo: requires gold
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Unblock unblocks a user
|
||||
func (s *UserServiceOp) Unblock(ctx context.Context, username string) (*Response, error) {
|
||||
path := "api/unfriend"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", username)
|
||||
form.Set("type", "enemy")
|
||||
form.Set("container", "todo: this should be the current user's full id")
|
||||
|
||||
req, err := s.client.NewPostForm(path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unfriend unfriends a user
|
||||
func (s *UserServiceOp) Unfriend(ctx context.Context, username string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/me/friends/%s", username)
|
||||
req, err := s.client.NewRequest(http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// GetUpvoted returns a list of the client's upvoted submissions
|
||||
func (s *UserServiceOp) GetUpvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/upvoted", s.client.Username)
|
||||
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(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.getLinks(), resp, nil
|
||||
}
|
||||
|
||||
// GetDownvoted returns a list of the client's downvoted submissions
|
||||
func (s *UserServiceOp) GetDownvoted(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/downvoted", s.client.Username)
|
||||
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(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.getLinks(), resp, nil
|
||||
}
|
||||
|
||||
// GetHidden returns a list of the client's hidden submissions
|
||||
func (s *UserServiceOp) GetHidden(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/hidden", s.client.Username)
|
||||
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(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.getLinks(), resp, nil
|
||||
}
|
||||
|
||||
// GetHotLinks returns a list of the client's hottest submissions
|
||||
func (s *UserServiceOp) GetHotLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, s.client.Username, sortHot, opts)
|
||||
}
|
||||
|
||||
// GetNewLinks returns a list of the client's newest submissions
|
||||
func (s *UserServiceOp) GetNewLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, s.client.Username, sortNew, opts)
|
||||
}
|
||||
|
||||
// GetTopLinks returns a list of the client's top submissions
|
||||
func (s *UserServiceOp) GetTopLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, s.client.Username, sortTop, opts)
|
||||
}
|
||||
|
||||
// GetControversialLinks returns a list of the client's most controversial submissions
|
||||
func (s *UserServiceOp) GetControversialLinks(ctx context.Context, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, s.client.Username, sortControversial, opts)
|
||||
}
|
||||
|
||||
// GetHotLinksOf returns a list of the user's hottest submissions
|
||||
func (s *UserServiceOp) GetHotLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, username, sortHot, opts)
|
||||
}
|
||||
|
||||
// GetNewLinksOf returns a list of the user's newest submissions
|
||||
func (s *UserServiceOp) GetNewLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, username, sortNew, opts)
|
||||
}
|
||||
|
||||
// GetTopLinksOf returns a list of the user's top submissions
|
||||
func (s *UserServiceOp) GetTopLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, username, sortTop, opts)
|
||||
}
|
||||
|
||||
// GetControversialLinksOf returns a list of the user's most controversial submissions
|
||||
func (s *UserServiceOp) GetControversialLinksOf(ctx context.Context, username string, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
return s.getLinks(ctx, username, sortControversial, opts)
|
||||
}
|
||||
|
||||
// GetHotComments returns a list of the client's hottest comments
|
||||
func (s *UserServiceOp) GetHotComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, s.client.Username, sortHot, opts)
|
||||
}
|
||||
|
||||
// GetNewComments returns a list of the client's newest comments
|
||||
func (s *UserServiceOp) GetNewComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, s.client.Username, sortNew, opts)
|
||||
}
|
||||
|
||||
// GetTopComments returns a list of the client's top comments
|
||||
func (s *UserServiceOp) GetTopComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, s.client.Username, sortTop, opts)
|
||||
}
|
||||
|
||||
// GetControversialComments returns a list of the client's most controversial comments
|
||||
func (s *UserServiceOp) GetControversialComments(ctx context.Context, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, s.client.Username, sortControversial, opts)
|
||||
}
|
||||
|
||||
// GetHotCommentsOf returns a list of the user's hottest comments
|
||||
func (s *UserServiceOp) GetHotCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, username, sortHot, opts)
|
||||
}
|
||||
|
||||
// GetNewCommentsOf returns a list of the user's newest comments
|
||||
func (s *UserServiceOp) GetNewCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, username, sortNew, opts)
|
||||
}
|
||||
|
||||
// GetTopCommentsOf returns a list of the user's top comments
|
||||
func (s *UserServiceOp) GetTopCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, username, sortTop, opts)
|
||||
}
|
||||
|
||||
// GetControversialCommentsOf returns a list of the user's most controversial comments
|
||||
func (s *UserServiceOp) GetControversialCommentsOf(ctx context.Context, username string, opts *ListOptions) (*CommentList, *Response, error) {
|
||||
return s.getComments(ctx, username, sortControversial, opts)
|
||||
}
|
||||
|
||||
func (s *UserServiceOp) getLinks(ctx context.Context, username string, sort sort, opts *ListOptions) (*LinkList, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/submitted?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(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.getLinks(), resp, nil
|
||||
}
|
||||
|
||||
func (s *UserServiceOp) getComments(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(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.getComments(), resp, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue