Handle listings better by using custom unmarshaling

Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
Vartan Benohanian 2020-05-03 17:31:35 -04:00
parent 460554e19e
commit 7922711d51
9 changed files with 988 additions and 345 deletions

View file

@ -25,61 +25,6 @@ type CommentServiceOp struct {
var _ CommentService = &CommentServiceOp{} 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 // CommentList holds information about a list of comments
// The after and before fields help decide the anchor point for a subsequent // The after and before fields help decide the anchor point for a subsequent
// call that returns a list // call that returns a list

View file

@ -6,6 +6,7 @@ import (
"net/url" "net/url"
"reflect" "reflect"
"testing" "testing"
"time"
) )
var expectedCommentSubmitOrEdit = &Comment{ var expectedCommentSubmitOrEdit = &Comment{
@ -15,7 +16,6 @@ var expectedCommentSubmitOrEdit = &Comment{
Permalink: "/r/subreddit/comments/test1/some_thread/test2/", Permalink: "/r/subreddit/comments/test1/some_thread/test2/",
Body: "test comment", Body: "test comment",
BodyHTML: "<div class=\"md\"><p>test comment</p>\n</div>",
Author: "reddit_username", Author: "reddit_username",
AuthorID: "t2_user1", AuthorID: "t2_user1",
AuthorFlairText: "Flair", AuthorFlairText: "Flair",
@ -27,8 +27,7 @@ var expectedCommentSubmitOrEdit = &Comment{
Score: 1, Score: 1,
Controversiality: 0, Controversiality: 0,
Created: 1588147787, Created: &Timestamp{time.Date(2020, 4, 28, 20, 9, 47, 0, time.UTC)},
CreatedUTC: 1588118987,
LinkID: "t3_link1", LinkID: "t3_link1",
} }

192
geddit.go
View file

@ -343,7 +343,6 @@ func CheckResponse(r *http.Response) error {
// ListOptions are the optional parameters to the various endpoints that return lists // ListOptions are the optional parameters to the various endpoints that return lists
type ListOptions struct { type ListOptions struct {
Sort string `url:"sort,omitempty"`
Type string `url:"type,omitempty"` // links or comments Type string `url:"type,omitempty"` // links or comments
// For getting submissions // For getting submissions
@ -354,7 +353,6 @@ type ListOptions struct {
After string `url:"after,omitempty"` After string `url:"after,omitempty"`
Before string `url:"before,omitempty"` Before string `url:"before,omitempty"`
Limit int `url:"limit,omitempty"` // default: 25 Limit int `url:"limit,omitempty"` // default: 25
Count int `url:"count,omitempty"` // default: 0
} }
func addOptions(s string, opt interface{}) (string, error) { func addOptions(s string, opt interface{}) (string, error) {
@ -388,6 +386,61 @@ type root struct {
Data interface{} `json:"data,omitempty"` 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 ( const (
kindListing string = "Listing" kindListing string = "Listing"
kindComment string = "t1" kindComment string = "t1"
@ -397,3 +450,138 @@ const (
kindSubreddit string = "t5" kindSubreddit string = "t5"
kindAward string = "t6" 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,
}
}

View file

@ -2,7 +2,6 @@ package geddit
import ( import (
"context" "context"
"encoding/json"
"net/http" "net/http"
) )
@ -30,14 +29,15 @@ type listingRoot struct {
} }
// Listing holds various types of things that all come from the Reddit API // Listing holds various types of things that all come from the Reddit API
type Listing struct { // type Listing struct {
Links []*Submission `json:"links,omitempty"` // Links []*Submission `json:"links,omitempty"`
Comments []*Comment `json:"comments,omitempty"` // Comments []*Comment `json:"comments,omitempty"`
Subreddits []*Subreddit `json:"subreddits,omitempty"` // Subreddits []*Subreddit `json:"subreddits,omitempty"`
} // }
// Get gets a list of things based on their IDs // Get gets a list of things based on their IDs
// Only links, comments, and subreddits are allowed // Only links, comments, and subreddits are allowed
// todo: only links, comments, subreddits
func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, *Response, error) { func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, *Response, error) {
type query struct { type query struct {
IDs []string `url:"id,comma"` IDs []string `url:"id,comma"`
@ -54,51 +54,13 @@ func (s *ListingsServiceOp) Get(ctx context.Context, ids ...string) (*Listing, *
return nil, nil, err return nil, nil, err
} }
root := new(listingRoot) root := new(rootListing)
resp, err := s.client.Do(ctx, req, root) resp, err := s.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
if root.Data == nil { return root.Data, resp, 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 // todo: do by_id next

34
private-messages.go Normal file
View 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)
}

View file

@ -5,7 +5,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time"
) )
// SubredditService handles communication with the subreddit // SubredditService handles communication with the subreddit
@ -23,14 +25,22 @@ type SubredditService interface {
GetMineWhereModerator(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) GetMineWhereStreams(ctx context.Context, opts *ListOptions) (*SubredditList, *Response, error)
GetHotPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) GetHotLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
GetNewPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) GetBestLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
GetRisingPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) GetNewLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
GetControversialPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) GetRisingLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error)
GetTopPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *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) // GetSticky1(ctx context.Context, name string) (interface{}, *Response, error)
GetSticky2(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 // SubredditServiceOp implements the SubredditService interface
@ -40,42 +50,6 @@ type SubredditServiceOp struct {
var _ SubredditService = &SubredditServiceOp{} 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 // SubredditList holds information about a list of subreddits
// The after and before fields help decide the anchor point for a subsequent // The after and before fields help decide the anchor point for a subsequent
// call that returns a list // call that returns a list
@ -85,58 +59,14 @@ type SubredditList struct {
Before string `json:"before,omitempty"` Before string `json:"before,omitempty"`
} }
type submissionRoot struct { // LinkList holds information about a list of links
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 // The after and before fields help decide the anchor point for a subsequent
// call that returns a list // call that returns a list
type SubmissionList struct { // Note: not to be confused with linked lists
Submissions []Submission `json:"submissions,omitempty"` type LinkList struct {
After string `json:"after,omitempty"` Links []Link `json:"submissions,omitempty"`
Before string `json:"before,omitempty"` After string `json:"after,omitempty"`
Before string `json:"before,omitempty"`
} }
// GetByName gets a subreddit by name // GetByName gets a subreddit by name
@ -220,44 +150,44 @@ var sorts = [...]string{
"top", "top",
} }
// GetHotPosts returns the hot posts // GetHotLinks returns the hot links
// If no subreddit names are provided, then it runs the search against /r/all // 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) // 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) // 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) { func (s *SubredditServiceOp) GetHotLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
return s.getPosts(ctx, sortHot, opts, names...) return s.getLinks(ctx, sortHot, opts, names...)
} }
// GetBestPosts returns the best posts // GetBestLinks returns the best links
// If no subreddit names are provided, then it runs the search against /r/all // 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) // 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) // 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) { func (s *SubredditServiceOp) GetBestLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
return s.getPosts(ctx, sortBest, opts, names...) return s.getLinks(ctx, sortBest, opts, names...)
} }
// GetNewPosts returns the new posts // GetNewLinks returns the new links
// If no subreddit names are provided, then it runs the search against /r/all // If no subreddit names are provided, then it runs the search against all those the client is subscribed to
func (s *SubredditServiceOp) GetNewPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { func (s *SubredditServiceOp) GetNewLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
return s.getPosts(ctx, sortNew, opts, names...) return s.getLinks(ctx, sortNew, opts, names...)
} }
// GetRisingPosts returns the rising posts // GetRisingLinks returns the rising links
// If no subreddit names are provided, then it runs the search against /r/all // If no subreddit names are provided, then it runs the search against all those the client is subscribed to
func (s *SubredditServiceOp) GetRisingPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { func (s *SubredditServiceOp) GetRisingLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
return s.getPosts(ctx, sortRising, opts, names...) return s.getLinks(ctx, sortRising, opts, names...)
} }
// GetControversialPosts returns the controversial posts // GetControversialLinks returns the controversial links
// If no subreddit names are provided, then it runs the search against /r/all // If no subreddit names are provided, then it runs the search against all those the client is subscribed to
func (s *SubredditServiceOp) GetControversialPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { func (s *SubredditServiceOp) GetControversialLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
return s.getPosts(ctx, sortControversial, opts, names...) return s.getLinks(ctx, sortControversial, opts, names...)
} }
// GetTopPosts returns the top posts // GetTopLinks returns the top links
// If no subreddit names are provided, then it runs the search against /r/all // If no subreddit names are provided, then it runs the search against all those the client is subscribed to
func (s *SubredditServiceOp) GetTopPosts(ctx context.Context, opts *ListOptions, names ...string) (*SubmissionList, *Response, error) { func (s *SubredditServiceOp) GetTopLinks(ctx context.Context, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
return s.getPosts(ctx, sortTop, opts, names...) return s.getLinks(ctx, sortTop, opts, names...)
} }
type sticky int type sticky int
@ -267,14 +197,65 @@ const (
sticky2 sticky2
) )
// GetSticky1 returns the first stickied post on a subreddit (if it exists) // // GetSticky1 returns the first stickied post on a subreddit (if it exists)
func (s *SubredditServiceOp) GetSticky1(ctx context.Context, name string) (interface{}, *Response, error) { // func (s *SubredditServiceOp) GetSticky1(ctx context.Context, name string) (interface{}, *Response, error) {
return s.getSticky(ctx, name, sticky1) // 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) // SubscribeByID subscribes to subreddits based on their id
func (s *SubredditServiceOp) GetSticky2(ctx context.Context, name string) (interface{}, *Response, error) { // Returns {} on success
return s.getSticky(ctx, name, sticky2) 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) { 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 return nil, nil, err
} }
root := new(subredditRootListing) root := new(rootListing)
resp, err := s.client.Do(ctx, req, root) resp, err := s.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
if root.Data == nil { l := new(SubredditList)
return nil, resp, nil
if root.Data != nil {
l.Subreddits = root.Data.Things.Subreddits
l.After = root.Data.After
l.Before = root.Data.Before
} }
sl := new(SubredditList) return l, resp, nil
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) { func (s *SubredditServiceOp) getLinks(ctx context.Context, sort sort, opts *ListOptions, names ...string) (*LinkList, *Response, error) {
path := sorts[sort] path := sorts[sort]
if len(names) > 0 { if len(names) > 0 {
path = fmt.Sprintf("r/%s/%s", strings.Join(names, "+"), sorts[sort]) 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 return nil, nil, err
} }
root := new(submissionRootListing) root := new(rootListing)
resp, err := s.client.Do(ctx, req, root) resp, err := s.client.Do(ctx, req, root)
if err != nil { if err != nil {
return nil, resp, err return nil, resp, err
} }
if root.Data == nil { l := new(LinkList)
return nil, resp, nil
if root.Data != nil {
l.Links = root.Data.Things.Links
l.After = root.Data.After
l.Before = root.Data.Before
} }
sl := new(SubmissionList) return l, resp, nil
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 // 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 <= 1, it's 1
// If it's >= 2, it's 2 // If it's >= 2, it's 2
// todo // todo
func (s *SubredditServiceOp) getSticky(ctx context.Context, name string, num sticky) (interface{}, *Response, error) { // func (s *SubredditServiceOp) getSticky(ctx context.Context, name string, num sticky) (interface{}, *Response, error) {
// type query struct { // type query struct {
// Num sticky `url:"num"` // Num sticky `url:"num"`
// } // }
// path := fmt.Sprintf("r/%s/about/sticky", name) // path := fmt.Sprintf("r/%s/about/sticky", name)
// path, err := addOptions(path, query{num}) // path, err := addOptions(path, query{num})
// if err != nil { // if err != nil {
// return nil, nil, err // return nil, nil, err
// } // }
// req, err := s.client.NewRequest(http.MethodGet, path, nil) // req, err := s.client.NewRequest(http.MethodGet, path, nil)
// if err != nil { // if err != nil {
// return nil, nil, err // return nil, nil, err
// } // }
// root := new(submissionRootListing) // var root []rootListing
// resp, err := s.client.Do(ctx, req, root) // resp, err := s.client.Do(ctx, req, &root)
// if err != nil { // if err != nil {
// return nil, resp, err // return nil, resp, err
// } // }
// return nil, resp, nil // // test, _ := json.MarshalIndent(root, "", " ")
return nil, nil, nil // // 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
View 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
View 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
View file

@ -2,9 +2,9 @@ package geddit
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"net/url"
) )
// UserService handles communication with the user // UserService handles communication with the user
@ -14,8 +14,36 @@ type UserService interface {
GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserShort, *Response, error) GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserShort, *Response, error)
UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error)
GetComments(ctx context.Context, sort sort, opts *ListOptions) (*CommentList, *Response, error) // returns the client's links
GetCommentsOf(ctx context.Context, username string, sort sort, opts *ListOptions) (*CommentList, *Response, error) 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 // UserServiceOp implements the UserService interface
@ -32,10 +60,10 @@ type userRoot struct {
// User represents a Reddit user // User represents a Reddit user
type User struct { type User struct {
ID string `json:"id,omitempty"` // is not the full ID, watch out
Name string `json:"name,omitempty"` ID string `json:"id,omitempty"`
Created float64 `json:"created"` Name string `json:"name,omitempty"`
CreatedUTC float64 `json:"created_utc"` Created *Timestamp `json:"created_utc,omitempty"`
LinkKarma int `json:"link_karma"` LinkKarma int `json:"link_karma"`
CommentKarma int `json:"comment_karma"` CommentKarma int `json:"comment_karma"`
@ -49,8 +77,8 @@ type User struct {
// UserShort represents a Reddit user, but contains fewer pieces of information // UserShort represents a Reddit user, but contains fewer pieces of information
// It is returned from the GET /api/user_data_by_account_ids endpoint // It is returned from the GET /api/user_data_by_account_ids endpoint
type UserShort struct { type UserShort struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
CreatedUTC float64 `json:"created_utc"` Created *Timestamp `json:"created_utc,omitempty"`
LinkKarma int `json:"link_karma"` LinkKarma int `json:"link_karma"`
CommentKarma int `json:"comment_karma"` CommentKarma int `json:"comment_karma"`
@ -102,63 +130,8 @@ func (s *UserServiceOp) GetMultipleByID(ctx context.Context, ids ...string) (map
return *root, resp, nil 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 // 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 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) { func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) {
type query struct { type query struct {
User string `url:"user,omitempty"` User string `url:"user,omitempty"`
@ -177,9 +150,6 @@ func (s *UserServiceOp) UsernameAvailable(ctx context.Context, username string)
root := new(bool) root := new(bool)
resp, err := s.client.Do(ctx, req, root) 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 { if err != nil {
return false, resp, err return false, resp, err
} }
@ -208,3 +178,219 @@ func (s *UserServiceOp) Friend(ctx context.Context, username string, note string
// todo: requires gold // todo: requires gold
return nil, nil, nil 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
}