diff --git a/account.go b/account.go new file mode 100644 index 0000000..eb3ce26 --- /dev/null +++ b/account.go @@ -0,0 +1,172 @@ +package geddit + +import ( + "context" + "fmt" + "net/http" +) + +// AccountService handles communication with the account +// related methods of the Reddit API. +type AccountService interface { + Karma(ctx context.Context) ([]SubredditKarma, *Response, error) +} + +// AccountServiceOp implements the AccountService interface. +type AccountServiceOp struct { + client *Client +} + +var _ AccountService = &AccountServiceOp{} + +type rootSubredditKarma struct { + Kind string `json:"kind,omitempty"` + Data []SubredditKarma `json:"data,omitempty"` +} + +// SubredditKarma holds user karma data for the subreddit. +type SubredditKarma struct { + Subreddit string `json:"sr"` + PostKarma int `json:"link_karma"` + CommentKarma int `json:"comment_karma"` +} + +// Settings are the user's account settings. +// Some of the fields' descriptions are taken from: +// https://praw.readthedocs.io/en/latest/code_overview/other/preferences.html#praw.models.Preferences.update +// todo: these should probably be pointers with omitempty +type Settings struct { + AcceptPrivateMessages bool `json:"accept_pms"` + // Allow Reddit to use your activity on Reddit to show you more relevant advertisements. + ActivityRelevantAds bool `json:"activity_relevant_ads"` + // Allow reddit to log my outbound clicks for personalization. + AllowClickTracking bool `json:"allow_clicktracking"` + + // I would like to beta test features for reddit. By enabling, you will join r/beta immediately. + Beta bool `json:"beta"` + ClickGadget bool `json:"clickgadget"` + CollapseReadMessages bool `json:"collapse_read_messages"` + Compress bool `json:"compress"` + CredditAutorenew bool `json:"creddit_autorenew"` + DefaultCommentSort string `json:"default_comment_sort"` + ShowDomainDetails bool `json:"domain_details"` + SendEmailDigests bool `json:"email_digests"` + SendMessagesAsEmails bool `json:"email_messages"` + UnsubscribeFromAllEmails bool `json:"email_unsubscribe_all"` + DisableCustomThemes bool `json:"enable_default_themes"` + Location string `json:"g"` + HideAds bool `json:"hide_ads"` + + // Don't allow search engines to index my user profile. + HideFromSearchEngines bool `json:"hide_from_robots"` + + HideUpvotedPosts bool `json:"hide_ups"` + HideDownvotedPosts bool `json:"hide_downs"` + + HighlightControversialComments bool `json:"highlight_controversial"` + HighlightNewComments bool `json:"highlight_new_comments"` + IgnoreSuggestedSorts bool `json:"ignore_suggested_sort"` + // Use new Reddit as my default experience. + UseNewReddit bool `json:"in_redesign_beta"` + LabelNSFW bool `json:"label_nsfw"` + Language string `json:"lang"` + ShowOldSearchPage bool `json:"legacy_search"` + EnableNotifications bool `json:"live_orangereds"` + MarkMessagesAsRead bool `json:"mark_messages_read"` + + // Determine whether to show thumbnails next to posts in subreddits. + // - "on": show thumbnails next to posts + // - "off": do not show thumbnails next to posts + // - "subreddit": show thumbnails next to posts based on the subreddit's preferences + ShowThumbnails string `json:"media"` + + // Determine whether to auto-expand media in subreddits. + // - "on": auto-expand media previews + // - "off": do not auto-expand media previews + // - "subreddit": auto-expand media previews based on the subreddit's preferences + AutoExpandMedia string `json:"media_preview"` + MinimumCommentScore *int `json:"min_comment_score"` + MinimumPostScore *int `json:"min_link_score"` + EnableMentionNotifications bool `json:"monitor_mentions"` + OpenLinksInNewWindow bool `json:"newwindow"` + // todo: test this + DarkMode bool `json:"nightmode"` + DisableProfanity bool `json:"no_profanity"` + NumComments int `json:"num_comments,omitempty"` + NumPosts int `json:"numsites,omitempty"` + ShowSpotlightBox bool `json:"organic"` + // todo: test this + SubredditTheme string `json:"other_theme"` + ShowNSFW bool `json:"over_18"` + EnablePrivateRSSFeeds bool `json:"private_feeds"` + ProfileOptOut bool `json:"profile_opt_out"` + // Make my upvotes and downvotes public. + PublicizeVotes bool `json:"public_votes"` + + // Allow my data to be used for research purposes. + AllowResearch bool `json:"research"` + IncludeNSFWSearchResults bool `json:"search_include_over_18"` + ReceiveCrosspostMessages bool `json:"send_crosspost_messages"` + ReceiveWelcomeMessages bool `json:"send_welcome_messages"` + + // Show a user's flair (next to their name on a post or comment). + ShowUserFlair bool `json:"show_flair"` + // Show a post's flair. + ShowPostFlair bool `json:"show_link_flair"` + + ShowGoldExpiration bool `json:"show_gold_expiration"` + ShowLocationBasedRecommendations bool `json:"show_location_based_recommendations"` + ShowPromote bool `json:"show_promote"` + ShowCustomSubredditThemes bool `json:"show_stylesheets"` + ShowTrendingSubreddits bool `json:"show_trending"` + ShowTwitter bool `json:"show_twitter"` + StoreVisits bool `json:"store_visits"` + ThemeSelector string `json:"theme_selector"` + + // Allow Reddit to use data provided by third-parties to show you more relevant advertisements on Reddit.i + AllowThirdPartyDataAdPersonalization bool `json:"third_party_data_personalized_ads"` + // Allow personalization of advertisements using data from third-party websites. + AllowThirdPartySiteDataAdPersonalization bool `json:"third_party_site_data_personalized_ads"` + // Allow personalization of content using data from third-party websites. + AllowThirdPartySiteDataContentPersonalization bool `json:"third_party_site_data_personalized_content"` + + EnableThreadedMessages bool `json:"threaded_messages"` + EnableThreadedModmail bool `json:"threaded_modmail"` + TopKarmaSubreddits bool `json:"top_karma_subreddits"` + UseGlobalDefaults bool `json:"use_global_defaults"` + // todo: test this, no_video_autoplay + EnableVideoAutoplay bool `json:"video_autoplay"` +} + +// Karma returns a breakdown of your karma per subreddit. +func (s *AccountServiceOp) Karma(ctx context.Context) ([]SubredditKarma, *Response, error) { + path := "api/v1/me/karma" + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return nil, nil, err + } + + root := new(rootSubredditKarma) + resp, err := s.client.Do(ctx, req, root) + if err != nil { + return nil, resp, err + } + + return root.Data, resp, nil +} + +// Settings returns your account settings. +func (s *AccountServiceOp) Settings(ctx context.Context) { + path := "api/v1/me/prefs" + + req, err := s.client.NewRequest(http.MethodGet, path, nil) + if err != nil { + return + } + + fmt.Println(req) + + root := new(Settings) + fmt.Println(root.ShowThumbnails) +} diff --git a/account_test.go b/account_test.go new file mode 100644 index 0000000..99212d1 --- /dev/null +++ b/account_test.go @@ -0,0 +1,32 @@ +package geddit + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +var expectedKarma = []SubredditKarma{ + {Subreddit: "nba", PostKarma: 144, CommentKarma: 21999}, + {Subreddit: "redditdev", PostKarma: 19, CommentKarma: 4}, + {Subreddit: "test", PostKarma: 1, CommentKarma: 0}, + {Subreddit: "golang", PostKarma: 1, CommentKarma: 0}, +} + +func TestAccountServiceOp_Karma(t *testing.T) { + setup() + defer teardown() + + blob := readFileContents(t, "testdata/account/karma.json") + + mux.HandleFunc("/api/v1/me/karma", func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodGet, r.Method) + fmt.Fprint(w, blob) + }) + + karma, _, err := client.Account.Karma(ctx) + assert.NoError(t, err) + assert.Equal(t, expectedKarma, karma) +} diff --git a/geddit.go b/geddit.go index 1cfbb8f..b557e76 100644 --- a/geddit.go +++ b/geddit.go @@ -91,6 +91,7 @@ type Client struct { // This is the client's user ID in Reddit's database. redditID string + Account AccountService Comment CommentService Flair FlairService Listings ListingsService @@ -120,6 +121,7 @@ func newClient(httpClient *http.Client) *Client { c := &Client{client: httpClient, BaseURL: baseURL, TokenURL: tokenURL} + c.Account = &AccountServiceOp{client: c} c.Comment = &CommentServiceOp{client: c} c.Flair = &FlairServiceOp{client: c} c.Listings = &ListingsServiceOp{client: c} diff --git a/testdata/account/karma.json b/testdata/account/karma.json new file mode 100644 index 0000000..832b034 --- /dev/null +++ b/testdata/account/karma.json @@ -0,0 +1,25 @@ +{ + "kind": "KarmaList", + "data": [ + { + "sr": "nba", + "comment_karma": 21999, + "link_karma": 144 + }, + { + "sr": "redditdev", + "comment_karma": 4, + "link_karma": 19 + }, + { + "sr": "test", + "comment_karma": 0, + "link_karma": 1 + }, + { + "sr": "golang", + "comment_karma": 0, + "link_karma": 1 + } + ] +} diff --git a/testdata/account/settings.json b/testdata/account/settings.json new file mode 100644 index 0000000..fedf5a9 --- /dev/null +++ b/testdata/account/settings.json @@ -0,0 +1,69 @@ +{ + "default_theme_sr": null, + "threaded_messages": true, + "hide_downs": false, + "label_nsfw": true, + "activity_relevant_ads": false, + "email_messages": false, + "profile_opt_out": false, + "video_autoplay": true, + "third_party_site_data_personalized_content": false, + "show_link_flair": true, + "show_trending": true, + "private_feeds": true, + "monitor_mentions": true, + "public_server_seconds": false, + "research": false, + "ignore_suggested_sort": true, + "send_crosspost_messages": false, + "email_digests": false, + "layout": 0, + "num_comments": 200, + "clickgadget": true, + "use_global_defaults": false, + "show_snoovatar": false, + "over_18": true, + "show_stylesheets": true, + "live_orangereds": true, + "enable_default_themes": false, + "legacy_search": false, + "domain_details": false, + "collapse_left_bar": false, + "lang": "en-ca", + "hide_ups": false, + "third_party_data_personalized_ads": false, + "allow_clicktracking": false, + "hide_from_robots": false, + "show_twitter": false, + "compress": false, + "store_visits": false, + "threaded_modmail": false, + "design_beta": false, + "min_link_score": null, + "media_preview": "off", + "show_location_based_recommendations": false, + "nightmode": false, + "highlight_controversial": true, + "geopopular": "GLOBAL", + "third_party_site_data_personalized_ads": false, + "survey_last_seen_time": null, + "min_comment_score": null, + "public_votes": false, + "collapse_read_messages": false, + "show_flair": true, + "mark_messages_read": true, + "search_include_over_18": true, + "no_profanity": false, + "hide_ads": false, + "beta": false, + "top_karma_subreddits": false, + "newwindow": true, + "numsites": 25, + "media": "subreddit", + "send_welcome_messages": true, + "show_gold_expiration": false, + "highlight_new_comments": true, + "email_unsubscribe_all": false, + "default_comment_sort": "top", + "accept_pms": "everyone" +} diff --git a/things.go b/things.go index 47bf99a..baa24ca 100644 --- a/things.go +++ b/things.go @@ -13,6 +13,7 @@ const ( kindSubreddit = "t5" kindAward = "t6" kindListing = "Listing" + kingKarmaList = "KarmaList" kingTrophyList = "TrophyList" kindUserList = "UserList" kindMode = "more" diff --git a/user.go b/user.go index c2add79..ee291bc 100644 --- a/user.go +++ b/user.go @@ -26,6 +26,7 @@ type UserService interface { CommentsOf(ctx context.Context, username string, opts ...SearchOptionSetter) (*Comments, *Response, error) Saved(ctx context.Context, opts ...SearchOptionSetter) (*Posts, *Comments, *Response, error) + // todo: votes can be public (they're private by default) so make UpvotedOf and DownvotedOf as well Upvoted(ctx context.Context, opts ...SearchOptionSetter) (*Posts, *Response, error) Downvoted(ctx context.Context, opts ...SearchOptionSetter) (*Posts, *Response, error) Hidden(ctx context.Context, opts ...SearchOptionSetter) (*Posts, *Response, error) @@ -36,6 +37,7 @@ type UserService interface { Unfriend(ctx context.Context, username string) (*Response, error) Block(ctx context.Context, username string) (*Blocked, *Response, error) + // todo: decide if you wanna keep these // BlockByID(ctx context.Context, id string) (*Blocked, *Response, error) Unblock(ctx context.Context, username string) (*Response, error) // UnblockByID(ctx context.Context, id string) (*Response, error) @@ -108,6 +110,8 @@ type Trophy struct { // Trophies is a list of trophies. type Trophies []Trophy +// todo: we don't really need this. just make a rootTrophyListingData struct or something and use the default unmarshaler for that + // UnmarshalJSON implements the json.Unmarshaler interface. func (t *Trophies) UnmarshalJSON(b []byte) error { var data map[string]interface{}