Move everything to new reddit/ folder
Signed-off-by: Vartan Benohanian <vartanbeno@gmail.com>
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// AccountService handles communication with the account
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_account
|
||||
type AccountService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
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
|
||||
type Settings struct {
|
||||
// Control whose private messages you see.
|
||||
// - "everyone": everyone except blocked users
|
||||
// - "whitelisted": only trusted users
|
||||
AcceptPrivateMessages *string `json:"accept_pms,omitempty"`
|
||||
// Allow Reddit to use your activity on Reddit to show you more relevant advertisements.
|
||||
ActivityRelevantAds *bool `json:"activity_relevant_ads,omitempty"`
|
||||
// Allow reddit to log my outbound clicks for personalization.
|
||||
AllowClickTracking *bool `json:"allow_clicktracking,omitempty"`
|
||||
|
||||
// Beta test features for reddit. By enabling, you will join r/beta immediately.
|
||||
Beta *bool `json:"beta,omitempty"`
|
||||
// Show me links I've recently viewed.
|
||||
ShowRecentlyViewedPosts *bool `json:"clickgadget,omitempty"`
|
||||
|
||||
CollapseReadMessages *bool `json:"collapse_read_messages,omitempty"`
|
||||
|
||||
// Compress the post display (make them look more compact).
|
||||
Compress *bool `json:"compress,omitempty"`
|
||||
|
||||
CredditAutorenew *bool `json:"creddit_autorenew,omitempty"`
|
||||
|
||||
// One of "confidence", "top", "new", "controversial", "old", "random", "qa", "live".
|
||||
DefaultCommentSort *string `json:"default_comment_sort,omitempty"`
|
||||
|
||||
// Show additional details in the domain text when available,
|
||||
// such as the source subreddit or the content author’s url/name.
|
||||
ShowDomainDetails *bool `json:"domain_details,omitempty"`
|
||||
|
||||
SendEmailDigests *bool `json:"email_digests,omitempty"`
|
||||
SendMessagesAsEmails *bool `json:"email_messages,omitempty"`
|
||||
UnsubscribeFromAllEmails *bool `json:"email_unsubscribe_all,omitempty"`
|
||||
|
||||
// Disable subreddits from displaying their custom themes.
|
||||
DisableCustomThemes *bool `json:"enable_default_themes,omitempty"`
|
||||
|
||||
// One of "GLOBAL", "AR", "AU", "BG", "CA", "CL", "CO", "CZ", "FI", "GB", "GR", "HR", "HU",
|
||||
// "IE", "IN", "IS", "JP", "MX", "MY", "NZ", "PH", "PL", "PR", "PT", "RO", "RS", "SE", "SG",
|
||||
// "TH", "TR", "TW", "US", "US_AK", "US_AL", "US_AR", "US_AZ", "US_CA", "US_CO", "US_CT",
|
||||
// "US_DC", "US_DE", "US_FL", "US_GA", "US_HI", "US_IA", "US_ID", "US_IL", "US_IN", "US_KS",
|
||||
// "US_KY", "US_LA", "US_MA", "US_MD", "US_ME", "US_MI", "US_MN", "US_MO", "US_MS", "US_MT",
|
||||
// "US_NC", "US_ND", "US_NE", "US_NH", "US_NJ", "US_NM", "US_NV", "US_NY", "US_OH", "US_OK",
|
||||
// "US_OR", "US_PA", "US_RI", "US_SC", "US_SD", "US_TN", "US_TX", "US_UT", "US_VA", "US_VT",
|
||||
// "US_WA", "US_WI", "US_WV", "US_WY".
|
||||
Location *string `json:"geopopular,omitempty"`
|
||||
|
||||
HideAds *bool `json:"hide_ads,omitempty"`
|
||||
|
||||
// Don't allow search engines to index my user profile.
|
||||
HideFromSearchEngines *bool `json:"hide_from_robots,omitempty"`
|
||||
|
||||
// Don’t show me posts after I’ve upvoted them, except my own.
|
||||
HideUpvotedPosts *bool `json:"hide_ups,omitempty"`
|
||||
// Don’t show me posts after I’ve downvoted them, except my own.
|
||||
HideDownvotedPosts *bool `json:"hide_downs,omitempty"`
|
||||
|
||||
// Show a dagger (†) on comments voted controversial (one that's been
|
||||
// upvoted and downvoted significantly).
|
||||
HighlightControversialComments *bool `json:"highlight_controversial,omitempty"`
|
||||
HighlightNewComments *bool `json:"highlight_new_comments,omitempty"`
|
||||
|
||||
// Ignore suggested sorts for specific threads/subreddits, like Q&As.
|
||||
IgnoreSuggestedSorts *bool `json:"ignore_suggested_sort,omitempty"`
|
||||
|
||||
// Use new Reddit as my default experience.
|
||||
// Use this to SET the setting.
|
||||
UseNewReddit *bool `json:"in_redesign_beta,omitempty"`
|
||||
|
||||
// Use new Reddit as my default experience.
|
||||
// Use this to GET the setting.
|
||||
UsesNewReddit *bool `json:"design_beta,omitempty"`
|
||||
|
||||
// Label posts that are not safe for work (NSFW).
|
||||
LabelNSFW *bool `json:"label_nsfw,omitempty"`
|
||||
|
||||
// A valid IETF language tag (underscore separated).
|
||||
Language *string `json:"lang,omitempty"`
|
||||
|
||||
ShowOldSearchPage *bool `json:"legacy_search,omitempty"`
|
||||
|
||||
// Send message notifications in my browser.
|
||||
EnableNotifications *bool `json:"live_orangereds,omitempty"`
|
||||
|
||||
MarkMessagesAsRead *bool `json:"mark_messages_read,omitempty"`
|
||||
|
||||
// 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,omitempty"`
|
||||
|
||||
// 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,omitempty"`
|
||||
|
||||
// Don't show me comments with a score less than this number.
|
||||
// Must be between -100 and 100 (inclusive).
|
||||
MinimumCommentScore *int `json:"min_comment_score,omitempty"`
|
||||
|
||||
// Don't show me posts with a score less than this number.
|
||||
// Must be between -100 and 100 (inclusive).
|
||||
MinimumPostScore *int `json:"min_link_score,omitempty"`
|
||||
|
||||
// Notify me when people say my username.
|
||||
EnableMentionNotifications *bool `json:"monitor_mentions,omitempty"`
|
||||
|
||||
// Opens link in a new window/tab.
|
||||
OpenLinksInNewWindow *bool `json:"newwindow,omitempty"`
|
||||
|
||||
DarkMode *bool `json:"nightmode,omitempty"`
|
||||
DisableProfanity *bool `json:"no_profanity,omitempty"`
|
||||
|
||||
// Display this many comments by default.
|
||||
// Must be between 1 and 500 (inclusive).
|
||||
NumberOfComments *int `json:"num_comments,omitempty,omitempty"`
|
||||
|
||||
// Display this many posts by default.
|
||||
// Must be between 1 and 100 (inclusive).
|
||||
NumberOfPosts *int `json:"numsites,omitempty,omitempty"`
|
||||
|
||||
// Show the spotlight box on the home feed.
|
||||
// Not sure what this is though...
|
||||
ShowSpotlightBox *bool `json:"organic,omitempty"`
|
||||
|
||||
SubredditTheme *string `json:"other_theme,omitempty"`
|
||||
|
||||
// Show content that is labeled not safe for work (NSFW).
|
||||
ShowNSFW *bool `json:"over_18,omitempty"`
|
||||
|
||||
EnablePrivateRSSFeeds *bool `json:"private_feeds,omitempty"`
|
||||
|
||||
// View user profiles on desktop using legacy mode.
|
||||
ProfileOptOut *bool `json:"profile_opt_out,omitempty"`
|
||||
// Make my upvotes and downvotes public.
|
||||
PublicizeVotes *bool `json:"public_votes,omitempty"`
|
||||
|
||||
// Allow my data to be used for research purposes.
|
||||
AllowResearch *bool `json:"research,omitempty"`
|
||||
|
||||
IncludeNSFWSearchResults *bool `json:"search_include_over_18,omitempty"`
|
||||
|
||||
// Receive a message when my post gets cross-posted.
|
||||
ReceiveCrosspostMessages *bool `json:"send_crosspost_messages,omitempty"`
|
||||
// Receive welcome messages from moderators when I join a community.
|
||||
ReceiveWelcomeMessages *bool `json:"send_welcome_messages,omitempty"`
|
||||
|
||||
// Show a user's flair (next to their name on a post or comment).
|
||||
ShowUserFlair *bool `json:"show_flair,omitempty"`
|
||||
// Show a post's flair.
|
||||
ShowPostFlair *bool `json:"show_link_flair,omitempty"`
|
||||
|
||||
// Show how much gold you have remaining on your profile.
|
||||
ShowGoldExpiration *bool `json:"show_gold_expiration,omitempty"`
|
||||
ShowLocationBasedRecommendations *bool `json:"show_location_based_recommendations,omitempty"`
|
||||
ShowPromote *bool `json:"show_promote,omitempty"`
|
||||
ShowCustomSubredditThemes *bool `json:"show_stylesheets,omitempty"`
|
||||
|
||||
// Show trending subreddits on the home feed.
|
||||
ShowTrendingSubreddits *bool `json:"show_trending,omitempty"`
|
||||
ShowTwitter *bool `json:"show_twitter,omitempty"`
|
||||
|
||||
// Store whether or not you want to track posts you've visited.
|
||||
StoreVisits *bool `json:"store_visits,omitempty"`
|
||||
ThemeSelector *string `json:"theme_selector,omitempty"`
|
||||
|
||||
// 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,omitempty"`
|
||||
// Allow personalization of advertisements using data from third-party websites.
|
||||
AllowThirdPartySiteDataAdPersonalization *bool `json:"third_party_site_data_personalized_ads,omitempty"`
|
||||
// Allow personalization of content using data from third-party websites.
|
||||
AllowThirdPartySiteDataContentPersonalization *bool `json:"third_party_site_data_personalized_content,omitempty"`
|
||||
|
||||
EnableThreadedMessages *bool `json:"threaded_messages,omitempty"`
|
||||
EnableThreadedModmail *bool `json:"threaded_modmail,omitempty"`
|
||||
|
||||
// Show the communities you are active in on your profile (mobile only).
|
||||
TopKarmaSubreddits *bool `json:"top_karma_subreddits,omitempty"`
|
||||
|
||||
UseGlobalDefaults *bool `json:"use_global_defaults,omitempty"`
|
||||
EnableVideoAutoplay *bool `json:"video_autoplay,omitempty"`
|
||||
}
|
||||
|
||||
type rootRelationshipList struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data struct {
|
||||
Relationships []Relationship `json:"children"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Info returns some general information about your account.
|
||||
func (s *AccountService) Info(ctx context.Context) (*User, *Response, error) {
|
||||
path := "api/v1/me"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(User)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Karma returns a breakdown of your karma per subreddit.
|
||||
func (s *AccountService) 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 *AccountService) Settings(ctx context.Context) (*Settings, *Response, error) {
|
||||
path := "api/v1/me/prefs"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Settings)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// UpdateSettings updates your account settings and returns the modified version.
|
||||
func (s *AccountService) UpdateSettings(ctx context.Context, settings *Settings) (*Settings, *Response, error) {
|
||||
path := "api/v1/me/prefs"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPatch, path, settings)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Settings)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Trophies returns a list of your trophies.
|
||||
func (s *AccountService) Trophies(ctx context.Context) ([]Trophy, *Response, error) {
|
||||
path := "api/v1/me/trophies"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootTrophyListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
var trophies []Trophy
|
||||
for _, trophy := range root.Data.Trophies {
|
||||
if trophy.Data != nil {
|
||||
trophies = append(trophies, *trophy.Data)
|
||||
}
|
||||
}
|
||||
|
||||
return trophies, resp, nil
|
||||
}
|
||||
|
||||
// Friends returns a list of your friends.
|
||||
func (s *AccountService) Friends(ctx context.Context) ([]Relationship, *Response, error) {
|
||||
path := "prefs/friends"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := make([]rootRelationshipList, 2)
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root[0].Data.Relationships, resp, nil
|
||||
}
|
||||
|
||||
// Blocked returns a list of your blocked users.
|
||||
func (s *AccountService) Blocked(ctx context.Context) ([]Relationship, *Response, error) {
|
||||
path := "prefs/blocked"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootRelationshipList)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data.Relationships, resp, nil
|
||||
}
|
||||
|
||||
// Messaging returns blocked users and trusted users, respectively.
|
||||
func (s *AccountService) Messaging(ctx context.Context) ([]Relationship, []Relationship, *Response, error) {
|
||||
path := "prefs/messaging"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
root := make([]rootRelationshipList, 2)
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
|
||||
blocked := root[0].Data.Relationships
|
||||
trusted := root[1].Data.Relationships
|
||||
|
||||
return blocked, trusted, resp, nil
|
||||
}
|
||||
|
||||
// Trusted returns a list of your trusted users.
|
||||
func (s *AccountService) Trusted(ctx context.Context) ([]Relationship, *Response, error) {
|
||||
path := "prefs/trusted"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootRelationshipList)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data.Relationships, resp, nil
|
||||
}
|
||||
|
||||
// AddTrusted adds a user to your trusted users.
|
||||
// This is not visible in the Reddit API docs.
|
||||
func (s *AccountService) AddTrusted(ctx context.Context, username string) (*Response, error) {
|
||||
path := "api/add_whitelisted"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
// todo: you can also do this with the user id. form.Set("id", id). should we? or is this enough?
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemoveTrusted removes a user from your trusted users.
|
||||
// This is not visible in the Reddit API docs.
|
||||
func (s *AccountService) RemoveTrusted(ctx context.Context, username string) (*Response, error) {
|
||||
path := "api/remove_whitelisted"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", username)
|
||||
// todo: you can also do this with the user id. form.Set("id", id). should we? or is this enough?
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedInfo = &User{
|
||||
ID: "164ab8",
|
||||
Name: "v_95",
|
||||
Created: &Timestamp{time.Date(2017, 3, 12, 4, 56, 47, 0, time.UTC)},
|
||||
PostKarma: 488,
|
||||
CommentKarma: 22223,
|
||||
HasVerifiedEmail: true,
|
||||
NSFW: true,
|
||||
}
|
||||
|
||||
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},
|
||||
}
|
||||
|
||||
var expectedSettings = &Settings{
|
||||
AcceptPrivateMessages: String("everyone"),
|
||||
ActivityRelevantAds: Bool(false),
|
||||
AllowClickTracking: Bool(false),
|
||||
Beta: Bool(false),
|
||||
ShowRecentlyViewedPosts: Bool(true),
|
||||
CollapseReadMessages: Bool(false),
|
||||
Compress: Bool(false),
|
||||
CredditAutorenew: nil,
|
||||
DefaultCommentSort: String("top"),
|
||||
ShowDomainDetails: Bool(false),
|
||||
SendEmailDigests: Bool(false),
|
||||
SendMessagesAsEmails: Bool(false),
|
||||
UnsubscribeFromAllEmails: Bool(true),
|
||||
DisableCustomThemes: Bool(false),
|
||||
Location: String("GLOBAL"),
|
||||
HideAds: Bool(false),
|
||||
HideFromSearchEngines: Bool(false),
|
||||
HideUpvotedPosts: Bool(false),
|
||||
HideDownvotedPosts: Bool(false),
|
||||
HighlightControversialComments: Bool(true),
|
||||
HighlightNewComments: Bool(true),
|
||||
IgnoreSuggestedSorts: Bool(true),
|
||||
UseNewReddit: nil,
|
||||
UsesNewReddit: Bool(false),
|
||||
LabelNSFW: Bool(true),
|
||||
Language: String("en-ca"),
|
||||
ShowOldSearchPage: Bool(false),
|
||||
EnableNotifications: Bool(true),
|
||||
MarkMessagesAsRead: Bool(true),
|
||||
ShowThumbnails: String("subreddit"),
|
||||
AutoExpandMedia: String("off"),
|
||||
MinimumCommentScore: nil,
|
||||
MinimumPostScore: nil,
|
||||
EnableMentionNotifications: Bool(true),
|
||||
OpenLinksInNewWindow: Bool(true),
|
||||
DarkMode: Bool(true),
|
||||
DisableProfanity: Bool(false),
|
||||
NumberOfComments: Int(200),
|
||||
NumberOfPosts: Int(25),
|
||||
ShowSpotlightBox: nil,
|
||||
SubredditTheme: nil,
|
||||
ShowNSFW: Bool(true),
|
||||
EnablePrivateRSSFeeds: Bool(true),
|
||||
ProfileOptOut: Bool(false),
|
||||
PublicizeVotes: Bool(false),
|
||||
AllowResearch: Bool(false),
|
||||
IncludeNSFWSearchResults: Bool(true),
|
||||
ReceiveCrosspostMessages: Bool(false),
|
||||
ReceiveWelcomeMessages: Bool(true),
|
||||
ShowUserFlair: Bool(true),
|
||||
ShowPostFlair: Bool(true),
|
||||
ShowGoldExpiration: Bool(false),
|
||||
ShowLocationBasedRecommendations: Bool(false),
|
||||
ShowPromote: nil,
|
||||
ShowCustomSubredditThemes: Bool(true),
|
||||
ShowTrendingSubreddits: Bool(true),
|
||||
ShowTwitter: Bool(false),
|
||||
StoreVisits: Bool(false),
|
||||
ThemeSelector: nil,
|
||||
AllowThirdPartyDataAdPersonalization: Bool(false),
|
||||
AllowThirdPartySiteDataAdPersonalization: Bool(false),
|
||||
AllowThirdPartySiteDataContentPersonalization: Bool(false),
|
||||
EnableThreadedMessages: Bool(true),
|
||||
EnableThreadedModmail: Bool(false),
|
||||
TopKarmaSubreddits: Bool(false),
|
||||
UseGlobalDefaults: Bool(false),
|
||||
EnableVideoAutoplay: Bool(true),
|
||||
}
|
||||
|
||||
var expectedRelationships = []Relationship{
|
||||
{
|
||||
ID: "r9_1r4879",
|
||||
User: "test1",
|
||||
UserID: "t2_test1",
|
||||
Created: &Timestamp{time.Date(2020, 6, 28, 16, 43, 55, 0, time.UTC)},
|
||||
},
|
||||
{
|
||||
ID: "r9_1re930",
|
||||
User: "test2",
|
||||
UserID: "t2_test2",
|
||||
Created: &Timestamp{time.Date(2020, 6, 28, 16, 44, 2, 0, time.UTC)},
|
||||
},
|
||||
}
|
||||
|
||||
var expectedRelationships2 = []Relationship{
|
||||
{
|
||||
ID: "r9_1re60i",
|
||||
User: "test3",
|
||||
UserID: "t2_test3",
|
||||
Created: &Timestamp{time.Date(2020, 3, 6, 2, 27, 0, 0, time.UTC)},
|
||||
},
|
||||
}
|
||||
|
||||
func TestAccountService_Info(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/info.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/me", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
info, _, err := client.Account.Info(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedInfo, info)
|
||||
}
|
||||
|
||||
func TestAccountService_Karma(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/karma.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/me/karma", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
karma, _, err := client.Account.Karma(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedKarma, karma)
|
||||
}
|
||||
|
||||
func TestAccountService_Settings(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/settings.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/me/prefs", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
settings, _, err := client.Account.Settings(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedSettings, settings)
|
||||
}
|
||||
|
||||
func TestAccountService_UpdateSettings(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/settings.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedSettingsBody := &Settings{NumberOfPosts: Int(10), MinimumCommentScore: Int(5), Compress: Bool(true)}
|
||||
|
||||
mux.HandleFunc("/api/v1/me/prefs", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPatch, r.Method)
|
||||
|
||||
settingsBody := new(Settings)
|
||||
err := json.NewDecoder(r.Body).Decode(settingsBody)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedSettingsBody, settingsBody)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
settings, _, err := client.Account.UpdateSettings(ctx, expectedSettingsBody)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedSettings, settings)
|
||||
}
|
||||
|
||||
func TestAccountService_Trophies(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/trophies.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/me/trophies", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
trophies, _, err := client.Account.Trophies(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedTrophies, trophies)
|
||||
}
|
||||
|
||||
func TestAccountService_Friends(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/friends.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/prefs/friends", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
relationships, _, err := client.Account.Friends(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRelationships, relationships)
|
||||
}
|
||||
|
||||
func TestAccountService_Blocked(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/blocked.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/prefs/blocked", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
relationships, _, err := client.Account.Blocked(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRelationships, relationships)
|
||||
}
|
||||
|
||||
func TestAccountService_Messaging(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/messaging.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/prefs/messaging", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
blocked, trusted, _, err := client.Account.Messaging(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRelationships, blocked)
|
||||
require.Equal(t, expectedRelationships2, trusted)
|
||||
}
|
||||
|
||||
func TestAccountService_Trusted(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/account/trusted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/prefs/trusted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
relationships, _, err := client.Account.Trusted(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRelationships, relationships)
|
||||
}
|
||||
|
||||
func TestAccountService_AddTrusted(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/add_whitelisted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", "test123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Account.AddTrusted(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAccountService_RemoveTrusted(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/remove_whitelisted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "test123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Account.RemoveTrusted(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// CollectionService handles communication with the collection
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_collections
|
||||
type CollectionService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Collection is a mod curated group of posts within a subreddit.
|
||||
type Collection struct {
|
||||
ID string `json:"collection_id,omitempty"`
|
||||
Created *Timestamp `json:"created_at_utc,omitempty"`
|
||||
Updated *Timestamp `json:"last_update_utc,omitempty"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Permalink string `json:"permalink,omitempty"`
|
||||
Layout string `json:"display_layout,omitempty"`
|
||||
|
||||
SubredditID string `json:"subreddit_id,omitempty"`
|
||||
Author string `json:"author_name,omitempty"`
|
||||
AuthorID string `json:"author_id,omitempty"`
|
||||
|
||||
// Post at the top of the collection.
|
||||
// This does not appear when getting a list of collections.
|
||||
PrimaryPostID string `json:"primary_link_id,omitempty"`
|
||||
PostIDs []string `json:"link_ids,omitempty"`
|
||||
}
|
||||
|
||||
// CollectionCreateRequest represents a request to create a collection.
|
||||
type CollectionCreateRequest struct {
|
||||
Title string `url:"title"`
|
||||
Description string `url:"description,omitempty"`
|
||||
SubredditID string `url:"sr_fullname"`
|
||||
// One of: TIMELINE, GALLERY.
|
||||
Layout string `url:"display_layout,omitempty"`
|
||||
}
|
||||
|
||||
// Get gets a collection by its ID.
|
||||
func (s *CollectionService) Get(ctx context.Context, id string) (*Collection, *Response, error) {
|
||||
path := "api/v1/collections/collection"
|
||||
|
||||
type params struct {
|
||||
ID string `url:"collection_id"`
|
||||
IncludePosts bool `url:"include_links"`
|
||||
}
|
||||
path, err := addOptions(path, params{id, false})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
collection := new(Collection)
|
||||
resp, err := s.client.Do(ctx, req, collection)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return collection, resp, nil
|
||||
}
|
||||
|
||||
// FromSubreddit gets all collections in the subreddit.
|
||||
func (s *CollectionService) FromSubreddit(ctx context.Context, id string) ([]*Collection, *Response, error) {
|
||||
path := "api/v1/collections/subreddit_collections"
|
||||
|
||||
type params struct {
|
||||
SubredditID string `url:"sr_fullname"`
|
||||
}
|
||||
path, err := addOptions(path, params{id})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var collections []*Collection
|
||||
resp, err := s.client.Do(ctx, req, &collections)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return collections, resp, nil
|
||||
}
|
||||
|
||||
// Create creates a collection.
|
||||
func (s *CollectionService) Create(ctx context.Context, createRequest *CollectionCreateRequest) (*Collection, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, errors.New("createRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := "api/v1/collections/create_collection"
|
||||
|
||||
form, err := query.Values(createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
collection := new(Collection)
|
||||
resp, err := s.client.Do(ctx, req, collection)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return collection, resp, nil
|
||||
}
|
||||
|
||||
// Delete deletes a collection via its id.
|
||||
func (s *CollectionService) Delete(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/v1/collections/delete_collection"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// AddPost adds a post (via its full ID) to a collection (via its id).
|
||||
func (s *CollectionService) AddPost(ctx context.Context, postID, collectionID string) (*Response, error) {
|
||||
path := "api/v1/collections/add_post_to_collection"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("link_fullname", postID)
|
||||
form.Set("collection_id", collectionID)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemovePost removes a post (via its full ID) from a collection (via its id).
|
||||
func (s *CollectionService) RemovePost(ctx context.Context, postID, collectionID string) (*Response, error) {
|
||||
path := "api/v1/collections/remove_post_in_collection"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("link_fullname", postID)
|
||||
form.Set("collection_id", collectionID)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// ReorderPosts reorders posts in a collection.
|
||||
func (s *CollectionService) ReorderPosts(ctx context.Context, collectionID string, postIDs ...string) (*Response, error) {
|
||||
path := "api/v1/collections/reorder_collection"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", collectionID)
|
||||
form.Set("link_ids", strings.Join(postIDs, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UpdateTitle updates a collection's title.
|
||||
func (s *CollectionService) UpdateTitle(ctx context.Context, id string, title string) (*Response, error) {
|
||||
path := "api/v1/collections/update_collection_title"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", id)
|
||||
form.Set("title", title)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UpdateDescription updates a collection's description.
|
||||
func (s *CollectionService) UpdateDescription(ctx context.Context, id string, description string) (*Response, error) {
|
||||
path := "api/v1/collections/update_collection_description"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", id)
|
||||
form.Set("description", description)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UpdateLayoutTimeline updates a collection's layout to the timeline format.
|
||||
func (s *CollectionService) UpdateLayoutTimeline(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/v1/collections/update_collection_display_layout"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", id)
|
||||
form.Set("display_layout", "TIMELINE")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UpdateLayoutGallery updates a collection's layout to the gallery format.
|
||||
func (s *CollectionService) UpdateLayoutGallery(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/v1/collections/update_collection_display_layout"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", id)
|
||||
form.Set("display_layout", "GALLERY")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Follow follows a collection.
|
||||
func (s *CollectionService) Follow(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/v1/collections/follow_collection"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", id)
|
||||
form.Set("follow", "true")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unfollow unfollows a collection.
|
||||
func (s *CollectionService) Unfollow(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/v1/collections/follow_collection"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", id)
|
||||
form.Set("follow", "false")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedCollection = &Collection{
|
||||
ID: "37f1e52d-7ec9-466b-b4cc-59e86e071ed7",
|
||||
Created: &Timestamp{time.Date(2020, 8, 6, 23, 25, 3, 0, time.UTC)},
|
||||
Updated: &Timestamp{time.Date(2020, 8, 7, 1, 59, 32, 0, time.UTC)},
|
||||
|
||||
Title: "Test Title",
|
||||
Permalink: "https://www.reddit.com/r/helloworldtestt/collection/37f1e52d-7ec9-466b-b4cc-59e86e071ed7",
|
||||
Layout: "TIMELINE",
|
||||
|
||||
SubredditID: "t5_2uquw1",
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
PrimaryPostID: "t3_hs0cyh",
|
||||
PostIDs: []string{"t3_hs0cyh", "t3_hqrg8s", "t3_hs03f3"},
|
||||
}
|
||||
|
||||
var expectedCollections = []*Collection{
|
||||
{
|
||||
ID: "37f1e52d-7ec9-466b-b4cc-59e86e071ed7",
|
||||
Created: &Timestamp{time.Date(2020, 8, 6, 23, 25, 3, 0, time.UTC)},
|
||||
Updated: &Timestamp{time.Date(2020, 8, 7, 1, 59, 32, 0, time.UTC)},
|
||||
|
||||
Title: "Test Title",
|
||||
Permalink: "https://www.reddit.com/r/helloworldtestt/collection/37f1e52d-7ec9-466b-b4cc-59e86e071ed7",
|
||||
Layout: "TIMELINE",
|
||||
|
||||
SubredditID: "t5_2uquw1",
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
PostIDs: []string{"t3_hs0cyh", "t3_hqrg8s", "t3_hs03f3"},
|
||||
},
|
||||
{
|
||||
ID: "8e94db00-6605-46c6-b0d2-44653d6f538c",
|
||||
Created: &Timestamp{time.Date(2020, 8, 7, 0, 56, 29, 0, time.UTC)},
|
||||
Updated: &Timestamp{time.Date(2020, 8, 7, 1, 59, 27, 0, time.UTC)},
|
||||
|
||||
Title: "Test Title 2",
|
||||
Description: "Test Description",
|
||||
Permalink: "https://www.reddit.com/r/helloworldtestt/collection/8e94db00-6605-46c6-b0d2-44653d6f538c",
|
||||
|
||||
SubredditID: "t5_2uquw1",
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
PostIDs: []string{},
|
||||
},
|
||||
{
|
||||
ID: "a1b3e088-f6b8-4d98-9e93-adaacef113cd",
|
||||
Created: &Timestamp{time.Date(2020, 8, 7, 0, 55, 24, 0, time.UTC)},
|
||||
Updated: &Timestamp{time.Date(2020, 8, 7, 0, 55, 24, 0, time.UTC)},
|
||||
|
||||
Title: "Test Title 3",
|
||||
Permalink: "https://www.reddit.com/r/helloworldtestt/collection/a1b3e088-f6b8-4d98-9e93-adaacef113cd",
|
||||
|
||||
SubredditID: "t5_2uquw1",
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
PostIDs: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
func TestCollectionService_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/collection/collection.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("include_links", "false")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
collection, _, err := client.Collection.Get(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCollection, collection)
|
||||
}
|
||||
|
||||
func TestCollectionService_FromSubreddit(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/collection/collections.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/subreddit_collections", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("sr_fullname", "t5_2uquw1")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
collections, _, err := client.Collection.FromSubreddit(ctx, "t5_2uquw1")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCollections, collections)
|
||||
}
|
||||
|
||||
func TestCollectionService_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/collection/collection.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/create_collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("title", "Test Title")
|
||||
form.Set("sr_fullname", "t5_2uquw1")
|
||||
form.Set("display_layout", "TIMELINE")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Collection.Create(ctx, nil)
|
||||
require.EqualError(t, err, "createRequest: cannot be nil")
|
||||
|
||||
collection, _, err := client.Collection.Create(ctx, &CollectionCreateRequest{
|
||||
Title: "Test Title",
|
||||
SubredditID: "t5_2uquw1",
|
||||
Layout: "TIMELINE",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCollection, collection)
|
||||
}
|
||||
|
||||
func TestCollectionService_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/delete_collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.Delete(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_AddPost(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/add_post_to_collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("link_fullname", "t3_hs03f3")
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.AddPost(ctx, "t3_hs03f3", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_RemovePost(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/remove_post_in_collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("link_fullname", "t3_hs03f3")
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.RemovePost(ctx, "t3_hs03f3", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_ReorderPosts(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/reorder_collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("link_ids", "t3_hs0cyh,t3_hqrg8s,t3_hs03f3")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.ReorderPosts(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7", "t3_hs0cyh", "t3_hqrg8s", "t3_hs03f3")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_UpdateTitle(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/update_collection_title", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("title", "Test Title")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateTitle(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7", "Test Title")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_UpdateDescription(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/update_collection_description", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("description", "Test Description")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateDescription(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7", "Test Description")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_UpdateLayoutTimeline(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/update_collection_display_layout", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("display_layout", "TIMELINE")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateLayoutTimeline(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_UpdateLayoutGallery(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/update_collection_display_layout", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("display_layout", "GALLERY")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateLayoutGallery(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_Follow(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/follow_collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("follow", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.Follow(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_Unfollow(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/collections/follow_collection", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("collection_id", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
form.Set("follow", "false")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Collection.Unfollow(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CommentService handles communication with the comment
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_links_and_comments
|
||||
type CommentService struct {
|
||||
*postAndCommentService
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Submit submits a comment as a reply to a post, comment, or message.
|
||||
// parentID is the full ID of the thing being replied to.
|
||||
func (s *CommentService) Submit(ctx context.Context, parentID string, text string) (*Comment, *Response, error) {
|
||||
path := "api/comment"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("parent", parentID)
|
||||
form.Set("text", text)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Comment)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Edit edits a comment.
|
||||
func (s *CommentService) Edit(ctx context.Context, id string, text string) (*Comment, *Response, error) {
|
||||
path := "api/editusertext"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("thing_id", id)
|
||||
form.Set("text", text)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Comment)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// LoadMoreReplies retrieves more replies that were left out when initially fetching the comment.
|
||||
func (s *CommentService) LoadMoreReplies(ctx context.Context, comment *Comment) (*Response, error) {
|
||||
if comment == nil {
|
||||
return nil, errors.New("comment: cannot be nil")
|
||||
}
|
||||
|
||||
if !comment.HasMore() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
postID := comment.PostID
|
||||
commentIDs := comment.Replies.More.Children
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link_id", postID)
|
||||
form.Set("children", strings.Join(commentIDs, ","))
|
||||
|
||||
path := "api/morechildren"
|
||||
|
||||
// This was originally a GET, but with POST you can send a bigger payload
|
||||
// since it's in the body and not the URI.
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
JSON struct {
|
||||
Data struct {
|
||||
Things things `json:"things"`
|
||||
} `json:"data"`
|
||||
} `json:"json"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
comments := root.JSON.Data.Things.Comments
|
||||
mores := root.JSON.Data.Things.Mores
|
||||
|
||||
for _, c := range comments {
|
||||
comment.addCommentToReplies(c)
|
||||
}
|
||||
|
||||
if len(mores) > 0 {
|
||||
comment.Replies.More = mores[0]
|
||||
} else {
|
||||
comment.Replies.More = nil
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedCommentSubmitOrEdit = &Comment{
|
||||
ID: "test2",
|
||||
FullID: "t1_test2",
|
||||
ParentID: "t1_test",
|
||||
Permalink: "/r/subreddit/comments/test1/some_thread/test2/",
|
||||
|
||||
Body: "test comment",
|
||||
Author: "reddit_username",
|
||||
AuthorID: "t2_user1",
|
||||
AuthorFlairText: "Flair",
|
||||
AuthorFlairID: "024b2b66-05ca-11e1-96f4-12313d096aae",
|
||||
|
||||
SubredditName: "subreddit",
|
||||
SubredditNamePrefixed: "r/subreddit",
|
||||
SubredditID: "t5_test",
|
||||
|
||||
Likes: Bool(true),
|
||||
|
||||
Score: 1,
|
||||
Controversiality: 0,
|
||||
|
||||
Created: &Timestamp{time.Date(2020, 4, 29, 0, 9, 47, 0, time.UTC)},
|
||||
// todo: this should just be nil
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
PostID: "t3_link1",
|
||||
}
|
||||
|
||||
func TestCommentService_Submit(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/comment/submit-or-edit.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/comment", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("parent", "t1_test")
|
||||
form.Set("text", "test comment")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
comment, _, err := client.Comment.Submit(ctx, "t1_test", "test comment")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCommentSubmitOrEdit, comment)
|
||||
}
|
||||
|
||||
func TestCommentService_Edit(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/comment/submit-or-edit.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/editusertext", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("thing_id", "t1_test")
|
||||
form.Set("text", "test comment")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
comment, _, err := client.Comment.Edit(ctx, "t1_test", "test comment")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCommentSubmitOrEdit, comment)
|
||||
}
|
||||
|
||||
func TestCommentService_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/del", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.Delete(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Save(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/save", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.Save(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Unsave(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/unsave", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.Unsave(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_EnableReplies(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/sendreplies", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
form.Set("state", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.EnableReplies(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_DisableReplies(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/sendreplies", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
form.Set("state", "false")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.DisableReplies(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Lock(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/lock", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.Lock(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Unlock(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/unlock", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.Unlock(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Upvote(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/vote", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
form.Set("dir", "1")
|
||||
form.Set("rank", "10")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.Upvote(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Downvote(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/vote", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
form.Set("dir", "-1")
|
||||
form.Set("rank", "10")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.Downvote(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_RemoveVote(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/vote", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t1_test")
|
||||
form.Set("dir", "0")
|
||||
form.Set("rank", "10")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
res, err := client.Comment.RemoveVote(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_LoadMoreReplies(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/comment/more.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/morechildren", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("link_id", "t3_123")
|
||||
form.Set("children", "def,ghi,jkl")
|
||||
form.Set("api_type", "json")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, err = client.Comment.LoadMoreReplies(ctx, nil)
|
||||
require.EqualError(t, err, "comment: cannot be nil")
|
||||
|
||||
resp, err := client.Comment.LoadMoreReplies(ctx, &Comment{})
|
||||
require.Nil(t, resp)
|
||||
require.Nil(t, err)
|
||||
|
||||
comment := &Comment{
|
||||
FullID: "t1_abc",
|
||||
PostID: "t3_123",
|
||||
Replies: Replies{
|
||||
More: &More{
|
||||
Children: []string{"def", "ghi", "jkl"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = client.Comment.LoadMoreReplies(ctx, comment)
|
||||
require.Nil(t, err)
|
||||
require.False(t, comment.HasMore())
|
||||
require.Len(t, comment.Replies.Comments, 2)
|
||||
require.Len(t, comment.Replies.Comments[0].Replies.Comments, 1)
|
||||
}
|
||||
+285
@@ -0,0 +1,285 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
)
|
||||
|
||||
// EmojiService handles communication with the emoji
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_emoji
|
||||
type EmojiService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Emoji is a graphic element you can include in a post flair or user flair.
|
||||
type Emoji struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
UserFlairAllowed bool `json:"user_flair_allowed,omitempty"`
|
||||
PostFlairAllowed bool `json:"post_flair_allowed,omitempty"`
|
||||
ModFlairOnly bool `json:"mod_flair_only,omitempty"`
|
||||
// ID of the user who created this emoji.
|
||||
CreatedBy string `json:"created_by,omitempty"`
|
||||
}
|
||||
|
||||
// EmojiCreateOrUpdateRequest represents a request to create/update an emoji.
|
||||
type EmojiCreateOrUpdateRequest struct {
|
||||
Name string `url:"name"`
|
||||
UserFlairAllowed *bool `url:"user_flair_allowed,omitempty"`
|
||||
PostFlairAllowed *bool `url:"post_flair_allowed,omitempty"`
|
||||
ModFlairOnly *bool `url:"mod_flair_only,omitempty"`
|
||||
}
|
||||
|
||||
func (r *EmojiCreateOrUpdateRequest) validate() error {
|
||||
if r.Name == "" {
|
||||
return errors.New("name: cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type emojis []*Emoji
|
||||
|
||||
func (e *emojis) UnmarshalJSON(data []byte) (err error) {
|
||||
emojiMap := make(map[string]json.RawMessage)
|
||||
err = json.Unmarshal(data, &emojiMap)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for emojiName, emojiValue := range emojiMap {
|
||||
emoji := new(Emoji)
|
||||
err = json.Unmarshal(emojiValue, emoji)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
emoji.Name = emojiName
|
||||
*e = append(*e, emoji)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Get returns the default set of Reddit emojis, and those of the subreddit, respectively.
|
||||
func (s *EmojiService) Get(ctx context.Context, subreddit string) ([]*Emoji, []*Emoji, *Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/emojis/all", subreddit)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
root := make(map[string]emojis)
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
|
||||
defaultEmojis := root["snoomojis"]
|
||||
var subredditEmojis []*Emoji
|
||||
|
||||
for k, v := range root {
|
||||
if strings.HasPrefix(k, kindSubreddit) {
|
||||
subredditEmojis = v
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return defaultEmojis, subredditEmojis, resp, nil
|
||||
}
|
||||
|
||||
// Delete deletes the emoji from the subreddit.
|
||||
func (s *EmojiService) Delete(ctx context.Context, subreddit string, emoji string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/emoji/%s", subreddit, emoji)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// SetSize sets the custom emoji size in the subreddit.
|
||||
// Both height and width must be between 1 and 40 (inclusive).
|
||||
func (s *EmojiService) SetSize(ctx context.Context, subreddit string, height, width int) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/emoji_custom_size", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("height", fmt.Sprint(height))
|
||||
form.Set("width", fmt.Sprint(width))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DisableCustomSize disables the custom emoji size in the subreddit.
|
||||
func (s *EmojiService) DisableCustomSize(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/emoji_custom_size", subreddit)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (s *EmojiService) lease(ctx context.Context, subreddit, imagePath string) (string, map[string]string, *Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/emoji_asset_upload_s3.json", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("filepath", imagePath)
|
||||
form.Set("mimetype", "image/jpeg")
|
||||
if strings.HasSuffix(strings.ToLower(imagePath), ".png") {
|
||||
form.Set("mimetype", "image/png")
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
|
||||
var response struct {
|
||||
S3UploadLease struct {
|
||||
Action string `json:"action"`
|
||||
Fields []struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
} `json:"fields"`
|
||||
} `json:"s3UploadLease"`
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, &response)
|
||||
if err != nil {
|
||||
return "", nil, resp, err
|
||||
}
|
||||
|
||||
uploadURL := fmt.Sprintf("http:%s", response.S3UploadLease.Action)
|
||||
|
||||
fields := make(map[string]string)
|
||||
for _, field := range response.S3UploadLease.Fields {
|
||||
fields[field.Name] = field.Value
|
||||
}
|
||||
|
||||
return uploadURL, fields, resp, nil
|
||||
}
|
||||
|
||||
func (s *EmojiService) upload(ctx context.Context, subreddit string, createRequest *EmojiCreateOrUpdateRequest, awsKey string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/emoji.json", subreddit)
|
||||
|
||||
form, err := query.Values(createRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form.Set("s3_key", awsKey)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Upload uploads an emoji to the subreddit.
|
||||
func (s *EmojiService) Upload(ctx context.Context, subreddit string, createRequest *EmojiCreateOrUpdateRequest, imagePath string) (*Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, errors.New("createRequest: cannot be nil")
|
||||
}
|
||||
|
||||
err := createRequest.validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uploadURL, fields, resp, err := s.lease(ctx, subreddit, imagePath)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
file, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
body := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(body)
|
||||
|
||||
// AWS ignores all fields in the request that come after the file field, so we need to set these before
|
||||
// https://stackoverflow.com/questions/15234496/upload-directly-to-amazon-s3-using-ajax-returning-error-bucket-post-must-contai/15235866#15235866
|
||||
for k, v := range fields {
|
||||
writer.WriteField(k, v)
|
||||
}
|
||||
|
||||
part, err := writer.CreateFormFile("file", file.Name())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, err = io.Copy(part, file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = writer.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpResponse, err := ctxhttp.Post(ctx, nil, uploadURL, writer.FormDataContentType(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = CheckResponse(httpResponse)
|
||||
if err != nil {
|
||||
return &Response{httpResponse}, err
|
||||
}
|
||||
|
||||
return s.upload(ctx, subreddit, createRequest, fields["key"])
|
||||
}
|
||||
|
||||
// Update updates an emoji on the subreddit.
|
||||
func (s *EmojiService) Update(ctx context.Context, subreddit string, updateRequest *EmojiCreateOrUpdateRequest) (*Response, error) {
|
||||
if updateRequest == nil {
|
||||
return nil, errors.New("updateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
err := updateRequest.validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("api/v1/%s/emoji_permissions", subreddit)
|
||||
|
||||
form, err := query.Values(updateRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedDefaultEmojis = []*Emoji{
|
||||
{
|
||||
Name: "cake",
|
||||
URL: "https://emoji.redditmedia.com/46kel8lf1guz_t5_3nqvj/cake",
|
||||
UserFlairAllowed: true,
|
||||
PostFlairAllowed: true,
|
||||
ModFlairOnly: false,
|
||||
CreatedBy: "t2_6zfp6ii",
|
||||
},
|
||||
{
|
||||
Name: "cat_blep",
|
||||
URL: "https://emoji.redditmedia.com/p9sxc1zh1guz_t5_3nqvj/cat_blep",
|
||||
UserFlairAllowed: true,
|
||||
PostFlairAllowed: true,
|
||||
ModFlairOnly: false,
|
||||
CreatedBy: "t2_6zfp6ii",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedSubredditEmojis = []*Emoji{
|
||||
{
|
||||
Name: "TestEmoji",
|
||||
URL: "https://emoji.redditmedia.com/fxe5a674hpf51_t5_2uquw1/TestEmoji",
|
||||
UserFlairAllowed: true,
|
||||
PostFlairAllowed: true,
|
||||
ModFlairOnly: false,
|
||||
CreatedBy: "t2_164ab8",
|
||||
},
|
||||
}
|
||||
|
||||
func TestEmojiService_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/emoji/emojis.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/test/emojis/all", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
defaultEmojis, subredditEmojis, _, err := client.Emoji.Get(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, defaultEmojis, 2)
|
||||
require.Contains(t, expectedDefaultEmojis, defaultEmojis[0])
|
||||
require.Contains(t, expectedDefaultEmojis, defaultEmojis[1])
|
||||
require.Equal(t, expectedSubredditEmojis, subredditEmojis)
|
||||
}
|
||||
|
||||
func TestEmojiService_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/emoji/testemoji", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodDelete, r.Method)
|
||||
})
|
||||
|
||||
_, err := client.Emoji.Delete(ctx, "testsubreddit", "testemoji")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmojiService_SetSize(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/emoji_custom_size", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("height", "20")
|
||||
form.Set("width", "20")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Emoji.SetSize(ctx, "testsubreddit", 20, 20)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmojiService_DisableCustomSize(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/emoji_custom_size", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Emoji.DisableCustomSize(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmojiService_Upload(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
uploadURL := u.Host + "/api/emoji_upload"
|
||||
|
||||
blob, err := readFileContents("../testdata/emoji/lease.json")
|
||||
require.NoError(t, err)
|
||||
blob = fmt.Sprintf(blob, uploadURL)
|
||||
|
||||
emojiFile, err := ioutil.TempFile("/tmp", "emoji*.png")
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
emojiFile.Close()
|
||||
os.Remove(emojiFile.Name())
|
||||
}()
|
||||
|
||||
_, err = emojiFile.WriteString("this is a test")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/emoji_asset_upload_s3.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("filepath", emojiFile.Name())
|
||||
form.Set("mimetype", "image/png")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/emoji_upload", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
_, file, err := r.FormFile("file")
|
||||
require.NoError(t, err)
|
||||
|
||||
rdr, err := file.Open()
|
||||
require.NoError(t, err)
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
_, err = io.Copy(buf, rdr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "this is a test", buf.String())
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("key", "t5_2uquw1/t2_164ab8/a94a8f45ccb199a61c4c0873d391e98c982fabd3")
|
||||
form.Set("test name", "test value")
|
||||
|
||||
// for some reason this has to come after the FormFile call
|
||||
err = r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/emoji.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "testemoji")
|
||||
form.Set("user_flair_allowed", "false")
|
||||
form.Set("post_flair_allowed", "true")
|
||||
form.Set("mod_flair_only", "true")
|
||||
form.Set("s3_key", "t5_2uquw1/t2_164ab8/a94a8f45ccb199a61c4c0873d391e98c982fabd3")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err = client.Emoji.Upload(ctx, "testsubreddit", nil, emojiFile.Name())
|
||||
require.EqualError(t, err, "createRequest: cannot be nil")
|
||||
|
||||
_, err = client.Emoji.Upload(ctx, "testsubreddit", &EmojiCreateOrUpdateRequest{Name: ""}, emojiFile.Name())
|
||||
require.EqualError(t, err, "name: cannot be empty")
|
||||
|
||||
_, err = client.Emoji.Upload(ctx, "testsubreddit", &EmojiCreateOrUpdateRequest{
|
||||
Name: "testemoji",
|
||||
UserFlairAllowed: Bool(false),
|
||||
PostFlairAllowed: Bool(true),
|
||||
ModFlairOnly: Bool(true),
|
||||
}, emojiFile.Name())
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmojiService_Update(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/emoji_permissions", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "testemoji")
|
||||
form.Set("post_flair_allowed", "false")
|
||||
form.Set("mod_flair_only", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Emoji.Update(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "updateRequest: cannot be nil")
|
||||
|
||||
_, err = client.Emoji.Update(ctx, "testsubreddit", &EmojiCreateOrUpdateRequest{Name: ""})
|
||||
require.EqualError(t, err, "name: cannot be empty")
|
||||
|
||||
_, err = client.Emoji.Update(ctx, "testsubreddit", &EmojiCreateOrUpdateRequest{
|
||||
Name: "testemoji",
|
||||
PostFlairAllowed: Bool(false),
|
||||
ModFlairOnly: Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// APIError is an error coming from Reddit.
|
||||
type APIError struct {
|
||||
Label string
|
||||
Reason string
|
||||
Field string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("field %q caused %s: %s", e.Field, e.Label, e.Reason)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (e *APIError) UnmarshalJSON(data []byte) error {
|
||||
var info [3]string
|
||||
|
||||
err := json.Unmarshal(data, &info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.Label = info[0]
|
||||
e.Reason = info[1]
|
||||
e.Field = info[2]
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// JSONErrorResponse is an error response that sometimes gets returned with a 200 code.
|
||||
type JSONErrorResponse struct {
|
||||
// HTTP response that caused this error.
|
||||
Response *http.Response `json:"-"`
|
||||
|
||||
JSON struct {
|
||||
Errors []APIError `json:"errors,omitempty"`
|
||||
} `json:"json"`
|
||||
}
|
||||
|
||||
func (r *JSONErrorResponse) Error() string {
|
||||
var message string
|
||||
if len(r.JSON.Errors) > 0 {
|
||||
message = r.JSON.Errors[0].Error()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s %s: %d %s",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, message,
|
||||
)
|
||||
}
|
||||
|
||||
// An ErrorResponse reports the error caused by an API request
|
||||
type ErrorResponse struct {
|
||||
// HTTP response that caused this error
|
||||
Response *http.Response `json:"-"`
|
||||
|
||||
// Error message
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (r *ErrorResponse) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"%s %s: %d %s",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, r.Message,
|
||||
)
|
||||
}
|
||||
|
||||
// todo: rate limit errors
|
||||
@@ -0,0 +1,92 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// FlairService handles communication with the flair
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_flair
|
||||
type FlairService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Flair is a tag that can be attached to a user or a post.
|
||||
type Flair struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
|
||||
Color string `json:"text_color,omitempty"`
|
||||
BackgroundColor string `json:"background_color,omitempty"`
|
||||
CSSClass string `json:"css_class,omitempty"`
|
||||
|
||||
Editable bool `json:"text_editable"`
|
||||
ModOnly bool `json:"mod_only"`
|
||||
}
|
||||
|
||||
// FlairSummary is a condensed version of Flair.
|
||||
type FlairSummary struct {
|
||||
User string `json:"user,omitempty"`
|
||||
Text string `json:"flair_text,omitempty"`
|
||||
CSSClass string `json:"flair_css_class,omitempty"`
|
||||
}
|
||||
|
||||
// GetUserFlairs returns the user flairs from the subreddit.
|
||||
func (s *FlairService) GetUserFlairs(ctx context.Context, subreddit string) ([]*Flair, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/user_flair_v2", subreddit)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var flairs []*Flair
|
||||
resp, err := s.client.Do(ctx, req, &flairs)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return flairs, resp, nil
|
||||
}
|
||||
|
||||
// GetPostFlairs returns the post flairs from the subreddit.
|
||||
func (s *FlairService) GetPostFlairs(ctx context.Context, subreddit string) ([]*Flair, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/link_flair_v2", subreddit)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var flairs []*Flair
|
||||
resp, err := s.client.Do(ctx, req, &flairs)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return flairs, resp, nil
|
||||
}
|
||||
|
||||
// ListUserFlairs returns all flairs of individual users in the subreddit.
|
||||
func (s *FlairService) ListUserFlairs(ctx context.Context, subreddit string) ([]*FlairSummary, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/flairlist", subreddit)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var root struct {
|
||||
UserFlairs []*FlairSummary `json:"users"`
|
||||
}
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.UserFlairs, resp, nil
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedUserFlairs = []*Flair{
|
||||
{
|
||||
ID: "b8a1c822-3feb-11e8-88e1-0e5f55d58ce0",
|
||||
Type: "text",
|
||||
Text: "Beginner",
|
||||
|
||||
Color: "dark",
|
||||
BackgroundColor: "",
|
||||
CSSClass: "Beginner1",
|
||||
|
||||
Editable: false,
|
||||
ModOnly: false,
|
||||
},
|
||||
{
|
||||
ID: "b8ea0fce-3feb-11e8-af7a-0e263a127cf8",
|
||||
Text: "Moderator",
|
||||
Type: "text",
|
||||
|
||||
Color: "dark",
|
||||
BackgroundColor: "",
|
||||
CSSClass: "Moderator1",
|
||||
|
||||
Editable: false,
|
||||
ModOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedPostFlairs = []*Flair{
|
||||
{
|
||||
ID: "305b503e-da60-11ea-9681-0e9f1d580d2d",
|
||||
Type: "richtext",
|
||||
Text: "test",
|
||||
|
||||
Color: "light",
|
||||
BackgroundColor: "#373c3f",
|
||||
CSSClass: "test",
|
||||
|
||||
Editable: false,
|
||||
ModOnly: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedListUserFlairs = []*FlairSummary{
|
||||
{
|
||||
User: "TestUser1",
|
||||
Text: "TestFlair1",
|
||||
},
|
||||
{
|
||||
User: "TestUser2",
|
||||
Text: "TestFlair2",
|
||||
},
|
||||
}
|
||||
|
||||
func TestFlairService_GetUserFlairs(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/user-flairs.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/user_flair_v2", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
userFlairs, _, err := client.Flair.GetUserFlairs(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUserFlairs, userFlairs)
|
||||
}
|
||||
|
||||
func TestFlairService_GetPostFlairs(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/post-flairs.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/link_flair_v2", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
postFlairs, _, err := client.Flair.GetPostFlairs(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedPostFlairs, postFlairs)
|
||||
}
|
||||
|
||||
func TestFlairService_ListUserFlairs(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/list-user-flairs.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flairlist", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
userFlairs, _, err := client.Flair.ListUserFlairs(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedListUserFlairs, userFlairs)
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ListingsService handles communication with the listing
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_listings
|
||||
type ListingsService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Get returns posts, comments, and subreddits from their full IDs.
|
||||
func (s *ListingsService) Get(ctx context.Context, ids ...string) ([]*Post, []*Comment, []*Subreddit, *Response, error) {
|
||||
path := fmt.Sprintf("api/info?id=%s", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, nil, nil, resp, err
|
||||
}
|
||||
|
||||
posts := root.getPosts().Posts
|
||||
comments := root.getComments().Comments
|
||||
subreddits := root.getSubreddits().Subreddits
|
||||
|
||||
return posts, comments, subreddits, resp, nil
|
||||
}
|
||||
|
||||
// GetPosts returns posts from their full IDs.
|
||||
func (s *ListingsService) GetPosts(ctx context.Context, ids ...string) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("by_id/%s", strings.Join(ids, ","))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
posts := root.getPosts().Posts
|
||||
return posts, resp, nil
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedListingPosts = []*Post{
|
||||
{
|
||||
ID: "i2gvg4",
|
||||
FullID: "t3_i2gvg4",
|
||||
Created: &Timestamp{time.Date(2020, 8, 2, 18, 23, 8, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
Permalink: "/r/test/comments/i2gvg4/this_is_a_title/",
|
||||
URL: "https://www.reddit.com/r/test/comments/i2gvg4/this_is_a_title/",
|
||||
|
||||
Title: "This is a title",
|
||||
Body: "This is some text",
|
||||
|
||||
Likes: Bool(true),
|
||||
Score: 1,
|
||||
UpvoteRatio: 1,
|
||||
NumberOfComments: 1,
|
||||
|
||||
SubredditName: "test",
|
||||
SubredditNamePrefixed: "r/test",
|
||||
SubredditID: "t5_2qh23",
|
||||
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
|
||||
IsSelfPost: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedListingComments = []*Comment{
|
||||
{
|
||||
ID: "g05v931",
|
||||
FullID: "t1_g05v931",
|
||||
Created: &Timestamp{time.Date(2020, 8, 3, 1, 15, 40, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
ParentID: "t3_i2gvg4",
|
||||
Permalink: "/r/test/comments/i2gvg4/this_is_a_title/g05v931/",
|
||||
|
||||
Body: "Test comment",
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
|
||||
SubredditName: "test",
|
||||
SubredditNamePrefixed: "r/test",
|
||||
SubredditID: "t5_2qh23",
|
||||
|
||||
Likes: Bool(true),
|
||||
|
||||
Score: 1,
|
||||
Controversiality: 0,
|
||||
|
||||
PostID: "t3_i2gvg4",
|
||||
|
||||
IsSubmitter: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedListingSubreddits = []*Subreddit{
|
||||
{
|
||||
ID: "2qh23",
|
||||
FullID: "t5_2qh23",
|
||||
Created: &Timestamp{time.Date(2008, 1, 25, 5, 11, 28, 0, time.UTC)},
|
||||
|
||||
URL: "/r/test/",
|
||||
Name: "test",
|
||||
NamePrefixed: "r/test",
|
||||
Title: "Testing",
|
||||
Type: "public",
|
||||
|
||||
Subscribers: 8202,
|
||||
Subscribed: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedListingPosts2 = []*Post{
|
||||
{
|
||||
ID: "i2gvg4",
|
||||
FullID: "t3_i2gvg4",
|
||||
Created: &Timestamp{time.Date(2020, 8, 2, 18, 23, 8, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
Permalink: "/r/test/comments/i2gvg4/this_is_a_title/",
|
||||
URL: "https://www.reddit.com/r/test/comments/i2gvg4/this_is_a_title/",
|
||||
|
||||
Title: "This is a title",
|
||||
Body: "This is some text",
|
||||
|
||||
Likes: Bool(true),
|
||||
Score: 1,
|
||||
UpvoteRatio: 1,
|
||||
NumberOfComments: 1,
|
||||
|
||||
SubredditName: "test",
|
||||
SubredditNamePrefixed: "r/test",
|
||||
SubredditID: "t5_2qh23",
|
||||
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
|
||||
IsSelfPost: true,
|
||||
},
|
||||
{
|
||||
ID: "i2gvs1",
|
||||
FullID: "t3_i2gvs1",
|
||||
Created: &Timestamp{time.Date(2020, 8, 2, 18, 23, 37, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
Permalink: "/r/test/comments/i2gvs1/this_is_a_title/",
|
||||
URL: "http://example.com",
|
||||
|
||||
Title: "This is a title",
|
||||
|
||||
Likes: Bool(true),
|
||||
Score: 1,
|
||||
UpvoteRatio: 1,
|
||||
NumberOfComments: 0,
|
||||
|
||||
SubredditName: "test",
|
||||
SubredditNamePrefixed: "r/test",
|
||||
SubredditID: "t5_2qh23",
|
||||
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
},
|
||||
}
|
||||
|
||||
func TestListingsService_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/listings/posts-comments-subreddits.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/info", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t5_2qh23,t3_i2gvg4,t1_g05v931")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, subreddits, _, err := client.Listings.Get(ctx, "t5_2qh23", "t3_i2gvg4", "t1_g05v931")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedListingPosts, posts)
|
||||
require.Equal(t, expectedListingComments, comments)
|
||||
require.Equal(t, expectedListingSubreddits, subreddits)
|
||||
}
|
||||
|
||||
func TestListingsService_GetPosts(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/listings/posts.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/by_id/t3_i2gvg4,t3_i2gwgz", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.Listings.GetPosts(ctx, "t3_i2gvg4", "t3_i2gwgz")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedListingPosts2, posts)
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// MessageService handles communication with the message
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_messages
|
||||
type MessageService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Message is a message.
|
||||
type Message struct {
|
||||
ID string `json:"id"`
|
||||
FullID string `json:"name"`
|
||||
Created *Timestamp `json:"created_utc"`
|
||||
|
||||
Subject string `json:"subject"`
|
||||
Text string `json:"body"`
|
||||
ParentID string `json:"parent_id"`
|
||||
|
||||
Author string `json:"author"`
|
||||
To string `json:"dest"`
|
||||
|
||||
IsComment bool `json:"was_comment"`
|
||||
}
|
||||
|
||||
// Messages is a list of messages.
|
||||
type Messages struct {
|
||||
Messages []*Message `json:"messages"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
type rootInboxListing struct {
|
||||
Kind string `json:"kind"`
|
||||
Data inboxListing `json:"data"`
|
||||
}
|
||||
|
||||
type inboxListing struct {
|
||||
Things inboxThings `json:"children"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// The returned JSON for comments is a bit different.
|
||||
// It looks for like the Message struct.
|
||||
type inboxThings struct {
|
||||
Comments []*Message
|
||||
Messages []*Message
|
||||
}
|
||||
|
||||
// init initializes or clears the inbox.
|
||||
func (t *inboxThings) init() {
|
||||
t.Comments = make([]*Message, 0)
|
||||
t.Messages = make([]*Message, 0)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (t *inboxThings) UnmarshalJSON(b []byte) error {
|
||||
t.init()
|
||||
|
||||
var things []thing
|
||||
if err := json.Unmarshal(b, &things); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, thing := range things {
|
||||
switch thing.Kind {
|
||||
case kindComment:
|
||||
v := new(Message)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.Comments = append(t.Comments, v)
|
||||
}
|
||||
case kindMessage:
|
||||
v := new(Message)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.Messages = append(t.Messages, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *rootInboxListing) getComments() *Messages {
|
||||
return &Messages{
|
||||
Messages: l.Data.Things.Comments,
|
||||
After: l.Data.After,
|
||||
Before: l.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *rootInboxListing) getMessages() *Messages {
|
||||
return &Messages{
|
||||
Messages: l.Data.Things.Messages,
|
||||
After: l.Data.After,
|
||||
Before: l.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
// SendMessageRequest represents a request to send a message.
|
||||
type SendMessageRequest struct {
|
||||
// Username, or /r/name for that subreddit's moderators.
|
||||
To string `url:"to"`
|
||||
Subject string `url:"subject"`
|
||||
Text string `url:"text"`
|
||||
// Optional. If specified, the message will look like it came from the subreddit.
|
||||
FromSubreddit string `url:"from_sr,omitempty"`
|
||||
}
|
||||
|
||||
// ReadAll marks all messages/comments as read. It queues up the task on Reddit's end.
|
||||
// A successful response returns 202 to acknowledge acceptance of the request.
|
||||
// This endpoint is heavily rate limited.
|
||||
func (s *MessageService) ReadAll(ctx context.Context) (*Response, error) {
|
||||
path := "api/read_all_messages"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Read marks a message/comment as read via its full ID.
|
||||
func (s *MessageService) Read(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/read_message"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unread marks a message/comment as unread via its full ID.
|
||||
func (s *MessageService) Unread(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/unread_message"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Block blocks the author of a thing via the thing's full ID.
|
||||
// The thing can be a post, comment or message.
|
||||
func (s *MessageService) Block(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/block"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Collapse collapses messages.
|
||||
func (s *MessageService) Collapse(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/collapse_message"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Uncollapse uncollapses messages.
|
||||
func (s *MessageService) Uncollapse(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/uncollapse_message"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Delete deletes a message.
|
||||
func (s *MessageService) Delete(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/del_msg"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Send sends a message.
|
||||
func (s *MessageService) Send(ctx context.Context, sendRequest *SendMessageRequest) (*Response, error) {
|
||||
if sendRequest == nil {
|
||||
return nil, errors.New("sendRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := "api/compose"
|
||||
|
||||
form, err := query.Values(sendRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Inbox returns comments and messages that appear in your inbox, respectively.
|
||||
func (s *MessageService) Inbox(ctx context.Context, opts *ListOptions) (*Messages, *Messages, *Response, error) {
|
||||
root, resp, err := s.inbox(ctx, "message/inbox", opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return root.getComments(), root.getMessages(), resp, nil
|
||||
}
|
||||
|
||||
// InboxUnread returns unread comments and messages that appear in your inbox, respectively.
|
||||
func (s *MessageService) InboxUnread(ctx context.Context, opts *ListOptions) (*Messages, *Messages, *Response, error) {
|
||||
root, resp, err := s.inbox(ctx, "message/unread", opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return root.getComments(), root.getMessages(), resp, nil
|
||||
}
|
||||
|
||||
// Sent returns messages that you've sent.
|
||||
func (s *MessageService) Sent(ctx context.Context, opts *ListOptions) (*Messages, *Response, error) {
|
||||
root, resp, err := s.inbox(ctx, "message/sent", opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.getMessages(), resp, nil
|
||||
}
|
||||
|
||||
func (s *MessageService) inbox(ctx context.Context, path string, opts *ListOptions) (*rootInboxListing, *Response, error) {
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootInboxListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedCommentMessages = &Messages{
|
||||
Messages: []*Message{
|
||||
{
|
||||
ID: "g1xi2m9",
|
||||
FullID: "t1_g1xi2m9",
|
||||
Created: &Timestamp{time.Date(2020, 8, 18, 0, 24, 13, 0, time.UTC)},
|
||||
|
||||
Subject: "post reply",
|
||||
Text: "u/testuser2 hello",
|
||||
ParentID: "t3_hs03f3",
|
||||
|
||||
Author: "testuser1",
|
||||
To: "testuser2",
|
||||
|
||||
IsComment: true,
|
||||
},
|
||||
},
|
||||
After: "",
|
||||
Before: "",
|
||||
}
|
||||
|
||||
var expectedMessages = &Messages{
|
||||
Messages: []*Message{
|
||||
{
|
||||
ID: "qwki97",
|
||||
FullID: "t4_qwki97",
|
||||
Created: &Timestamp{time.Date(2020, 8, 18, 0, 16, 53, 0, time.UTC)},
|
||||
|
||||
Subject: "re: test",
|
||||
Text: "test",
|
||||
ParentID: "t4_qwki4m",
|
||||
|
||||
Author: "testuser1",
|
||||
To: "testuser2",
|
||||
|
||||
IsComment: false,
|
||||
},
|
||||
},
|
||||
After: "",
|
||||
Before: "",
|
||||
}
|
||||
|
||||
func TestMessageService_ReadAll(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/read_all_messages", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
})
|
||||
|
||||
res, err := client.Message.ReadAll(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusAccepted, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestMessageService_Read(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/read_message", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "test1,test2,test3")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Message.Read(ctx)
|
||||
require.EqualError(t, err, "must provide at least 1 id")
|
||||
|
||||
_, err = client.Message.Read(ctx, "test1", "test2", "test3")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Unread(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/unread_message", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "test1,test2,test3")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Message.Unread(ctx)
|
||||
require.EqualError(t, err, "must provide at least 1 id")
|
||||
|
||||
_, err = client.Message.Unread(ctx, "test1", "test2", "test3")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Block(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/block", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Message.Block(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Collapse(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/collapse_message", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "test1,test2,test3")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Message.Collapse(ctx)
|
||||
require.EqualError(t, err, "must provide at least 1 id")
|
||||
|
||||
_, err = client.Message.Collapse(ctx, "test1", "test2", "test3")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Uncollapse(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/uncollapse_message", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "test1,test2,test3")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Message.Uncollapse(ctx)
|
||||
require.EqualError(t, err, "must provide at least 1 id")
|
||||
|
||||
_, err = client.Message.Uncollapse(ctx, "test1", "test2", "test3")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/del_msg", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Message.Delete(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Send(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/compose", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("to", "test")
|
||||
form.Set("subject", "test subject")
|
||||
form.Set("text", "test text")
|
||||
form.Set("from_sr", "hello world")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Message.Send(ctx, nil)
|
||||
require.EqualError(t, err, "sendRequest: cannot be nil")
|
||||
|
||||
_, err = client.Message.Send(ctx, &SendMessageRequest{
|
||||
To: "test",
|
||||
Subject: "test subject",
|
||||
Text: "test text",
|
||||
FromSubreddit: "hello world",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Inbox(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/message/inbox.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/message/inbox", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
comments, messages, _, err := client.Message.Inbox(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCommentMessages, comments)
|
||||
require.Equal(t, expectedMessages, messages)
|
||||
}
|
||||
|
||||
func TestMessageService_InboxUnread(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/message/inbox.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/message/unread", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
comments, messages, _, err := client.Message.InboxUnread(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCommentMessages, comments)
|
||||
require.Equal(t, expectedMessages, messages)
|
||||
}
|
||||
|
||||
func TestMessageService_Sent(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/message/inbox.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/message/sent", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
messages, _, err := client.Message.Sent(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedMessages, messages)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// ModerationService handles communication with the moderation
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_moderation
|
||||
type ModerationService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ModAction is an action executed by a moderator of a subreddit, such
|
||||
// as inviting another user to be a mod, or setting permissions.
|
||||
type ModAction struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Action string `json:"action,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
Moderator string `json:"mod,omitempty"`
|
||||
// Not the full ID, just the ID36.
|
||||
ModeratorID string `json:"mod_id36,omitempty"`
|
||||
|
||||
// The author of whatever the action was produced on, e.g. a user, post, comment, etc.
|
||||
TargetAuthor string `json:"target_author,omitempty"`
|
||||
// This is the full ID of whatever the target was.
|
||||
TargetID string `json:"target_fullname,omitempty"`
|
||||
TargetTitle string `json:"target_title,omitempty"`
|
||||
TargetPermalink string `json:"target_permalink,omitempty"`
|
||||
TargetBody string `json:"target_body,omitempty"`
|
||||
|
||||
Subreddit string `json:"subreddit,omitempty"`
|
||||
// Not the full ID, just the ID36.
|
||||
SubredditID string `json:"sr_id36,omitempty"`
|
||||
}
|
||||
|
||||
// GetActions gets a list of moderator actions on a subreddit.
|
||||
func (s *ModerationService) GetActions(ctx context.Context, subreddit string, opts *ListModActionOptions) (*ModActions, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/log", subreddit)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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.getModActions(), resp, nil
|
||||
}
|
||||
|
||||
// AcceptInvite accepts a pending invite to moderate the specified subreddit.
|
||||
func (s *ModerationService) AcceptInvite(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/accept_moderator_invite", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Approve approves a post or comment via its full ID.
|
||||
func (s *ModerationService) Approve(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/approve"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Remove removes a post, comment or modmail message via its full ID.
|
||||
func (s *ModerationService) Remove(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/remove"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
form.Set("spam", "false")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemoveSpam removes a post, comment or modmail message via its full ID and marks it as spam.
|
||||
func (s *ModerationService) RemoveSpam(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/remove"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
form.Set("spam", "true")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Leave abdicates your moderator status in a subreddit via its full ID.
|
||||
func (s *ModerationService) Leave(ctx context.Context, subredditID string) (*Response, error) {
|
||||
path := "api/leavemoderator"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", subredditID)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// LeaveContributor abdicates your approved user status in a subreddit via its full ID.
|
||||
func (s *ModerationService) LeaveContributor(ctx context.Context, subredditID string) (*Response, error) {
|
||||
path := "api/leavecontributor"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", subredditID)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Edited gets posts and comments that have been edited recently.
|
||||
func (s *ModerationService) Edited(ctx context.Context, subreddit string, opts *ListOptions) (*Posts, *Comments, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/edited", subreddit)
|
||||
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
return root.getPosts(), root.getComments(), resp, nil
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedModActions = &ModActions{
|
||||
ModActions: []*ModAction{
|
||||
{
|
||||
ID: "ModAction_b4e7979a-c4ad-11ea-8440-0ea1b7c2b8f9",
|
||||
Action: "spamcomment",
|
||||
Created: &Timestamp{time.Date(2020, 7, 13, 2, 8, 14, 0, time.UTC)},
|
||||
|
||||
Moderator: "v_95",
|
||||
ModeratorID: "164ab8",
|
||||
|
||||
TargetAuthor: "testuser",
|
||||
TargetID: "t1_fxw10aa",
|
||||
TargetPermalink: "/r/helloworldtestt/comments/hq6r3t/yo/fxw10aa/",
|
||||
TargetBody: "hi",
|
||||
|
||||
Subreddit: "helloworldtestt",
|
||||
SubredditID: "2uquw1",
|
||||
},
|
||||
{
|
||||
ID: "ModAction_a0408162-c4ad-11ea-8239-0e3b48262e8b",
|
||||
Action: "sticky",
|
||||
Created: &Timestamp{time.Date(2020, 7, 13, 2, 7, 38, 0, time.UTC)},
|
||||
|
||||
Moderator: "v_95",
|
||||
ModeratorID: "164ab8",
|
||||
|
||||
TargetAuthor: "testuser",
|
||||
TargetID: "t3_hq6r3t",
|
||||
TargetTitle: "yo",
|
||||
TargetPermalink: "/r/helloworldtestt/comments/hq6r3t/yo/",
|
||||
|
||||
Subreddit: "helloworldtestt",
|
||||
SubredditID: "2uquw1",
|
||||
},
|
||||
},
|
||||
After: "ModAction_a0408162-c4ad-11ea-8239-0e3b48262e8b",
|
||||
Before: "",
|
||||
}
|
||||
|
||||
func TestModerationService_GetActions(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/moderation/actions.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/about/log", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("type", "testtype")
|
||||
form.Set("mod", "testmod")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
modActions, _, err := client.Moderation.GetActions(ctx, "testsubreddit", &ListModActionOptions{Type: "testtype", Moderator: "testmod"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedModActions, modActions)
|
||||
}
|
||||
|
||||
func TestModerationService_AcceptInvite(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/moderation/actions.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/accept_moderator_invite", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, err = client.Moderation.AcceptInvite(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Approve(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/approve", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t3_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Approve(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Remove(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/remove", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t3_test")
|
||||
form.Set("spam", "false")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Remove(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_RemoveSpam(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/remove", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t3_test")
|
||||
form.Set("spam", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.RemoveSpam(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Leave(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/leavemoderator", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t5_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Leave(ctx, "t5_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_LeaveContributor(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/leavecontributor", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "t5_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.LeaveContributor(ctx, "t5_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Edited(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// contains posts and comments
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/about/edited", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, _, err := client.Moderation.Edited(ctx, "testsubreddit", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
|
||||
require.Len(t, comments.Comments, 1)
|
||||
require.Equal(t, expectedComment, comments.Comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", comments.After)
|
||||
require.Equal(t, "", comments.Before)
|
||||
}
|
||||
+334
@@ -0,0 +1,334 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// MultiService handles communication with the multireddit
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api#section_multis
|
||||
type MultiService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type multiRoot struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *Multi `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Multi is a multireddit, i.e. a customizable group of subreddits.
|
||||
// Users can create multis for custom navigation, instead of browsing
|
||||
// one subreddit or all subreddits at a time.
|
||||
type Multi struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
// Format: user/{username}/m/{multiname}
|
||||
Path string `json:"path,omitempty"`
|
||||
Description string `json:"description_md,omitempty"`
|
||||
Subreddits SubredditNames `json:"subreddits"`
|
||||
CopiedFrom *string `json:"copied_from"`
|
||||
|
||||
Owner string `json:"owner,omitempty"`
|
||||
OwnerID string `json:"owner_id,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
NumberOfSubscribers int `json:"num_subscribers"`
|
||||
Visibility string `json:"visibility,omitempty"`
|
||||
Subscribed bool `json:"is_subscriber"`
|
||||
Favorite bool `json:"is_favorited"`
|
||||
CanEdit bool `json:"can_edit"`
|
||||
NSFW bool `json:"over_18"`
|
||||
}
|
||||
|
||||
// SubredditNames is a list of subreddit names.
|
||||
type SubredditNames []string
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (n *SubredditNames) UnmarshalJSON(data []byte) error {
|
||||
type subreddit struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var subreddits []subreddit
|
||||
|
||||
err := json.Unmarshal(data, &subreddits)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, sr := range subreddits {
|
||||
*n = append(*n, sr.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (n *SubredditNames) MarshalJSON() ([]byte, error) {
|
||||
type subreddit struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
var subreddits []subreddit
|
||||
|
||||
for _, name := range *n {
|
||||
subreddits = append(subreddits, subreddit{name})
|
||||
}
|
||||
|
||||
return json.Marshal(subreddits)
|
||||
}
|
||||
|
||||
// MultiCopyRequest represents a request to copy a multireddit.
|
||||
type MultiCopyRequest struct {
|
||||
FromPath string `url:"from"`
|
||||
ToPath string `url:"to"`
|
||||
// Raw markdown text.
|
||||
Description string `url:"description_md,omitempty"`
|
||||
// No longer than 50 characters.
|
||||
DisplayName string `url:"display_name,omitempty"`
|
||||
}
|
||||
|
||||
// MultiCreateOrUpdateRequest represents a request to create/update a multireddit.
|
||||
type MultiCreateOrUpdateRequest struct {
|
||||
// For updates, this is the display name, i.e. the header of the multi.
|
||||
// Not part of the path necessarily.
|
||||
Name string `json:"display_name,omitempty"`
|
||||
Description string `json:"description_md,omitempty"`
|
||||
Subreddits SubredditNames `json:"subreddits,omitempty"`
|
||||
// One of: private, public, hidden.
|
||||
Visibility string `json:"visibility,omitempty"`
|
||||
}
|
||||
|
||||
// Form parameterizes the fields and returns the form.
|
||||
func (r *MultiCreateOrUpdateRequest) Form() url.Values {
|
||||
byteValue, _ := json.Marshal(r)
|
||||
form := url.Values{}
|
||||
form.Set("model", string(byteValue))
|
||||
return form
|
||||
}
|
||||
|
||||
type rootMultiDescription struct {
|
||||
Data struct {
|
||||
Body string `json:"body_md"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Get gets information about the multireddit from its url path.
|
||||
func (s *MultiService) Get(ctx context.Context, multiPath string) (*Multi, *Response, error) {
|
||||
path := fmt.Sprintf("api/multi/%s", multiPath)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(multiRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// Mine returns your multireddits.
|
||||
func (s *MultiService) Mine(ctx context.Context) ([]Multi, *Response, error) {
|
||||
path := "api/multi/mine"
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var root []multiRoot
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
multis := make([]Multi, 0)
|
||||
for _, multi := range root {
|
||||
multis = append(multis, *multi.Data)
|
||||
}
|
||||
|
||||
return multis, resp, nil
|
||||
}
|
||||
|
||||
// Of returns the user's public multireddits.
|
||||
// Or, if the user is you, all of your multireddits.
|
||||
func (s *MultiService) Of(ctx context.Context, username string) ([]Multi, *Response, error) {
|
||||
path := fmt.Sprintf("api/multi/user/%s", username)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var root []multiRoot
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
multis := make([]Multi, 0)
|
||||
for _, multi := range root {
|
||||
multis = append(multis, *multi.Data)
|
||||
}
|
||||
|
||||
return multis, resp, nil
|
||||
}
|
||||
|
||||
// Copy copies a multireddit.
|
||||
func (s *MultiService) Copy(ctx context.Context, copyRequest *MultiCopyRequest) (*Multi, *Response, error) {
|
||||
if copyRequest == nil {
|
||||
return nil, nil, errors.New("copyRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := "api/multi/copy"
|
||||
form, err := query.Values(copyRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(multiRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// Create creates a multireddit.
|
||||
func (s *MultiService) Create(ctx context.Context, createRequest *MultiCreateOrUpdateRequest) (*Multi, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, errors.New("createRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := "api/multi"
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, createRequest.Form())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(multiRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// Update updates a multireddit.
|
||||
// If the multireddit does not exist, it will be created.
|
||||
func (s *MultiService) Update(ctx context.Context, multiPath string, updateRequest *MultiCreateOrUpdateRequest) (*Multi, *Response, error) {
|
||||
if updateRequest == nil {
|
||||
return nil, nil, errors.New("updateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("api/multi/%s", multiPath)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPut, path, updateRequest.Form())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(multiRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// Delete deletes a multireddit.
|
||||
func (s *MultiService) Delete(ctx context.Context, multiPath string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/multi/%s", multiPath)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// GetDescription gets a multireddit's description.
|
||||
func (s *MultiService) GetDescription(ctx context.Context, multiPath string) (string, *Response, error) {
|
||||
path := fmt.Sprintf("api/multi/%s/description", multiPath)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
root := new(rootMultiDescription)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
|
||||
return root.Data.Body, resp, nil
|
||||
}
|
||||
|
||||
// UpdateDescription updates a multireddit's description.
|
||||
func (s *MultiService) UpdateDescription(ctx context.Context, multiPath string, description string) (string, *Response, error) {
|
||||
path := fmt.Sprintf("api/multi/%s/description", multiPath)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("model", fmt.Sprintf(`{"body_md":"%s"}`, description))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPut, path, form)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
root := new(rootMultiDescription)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
|
||||
return root.Data.Body, resp, nil
|
||||
}
|
||||
|
||||
// AddSubreddit adds a subreddit to a multireddit.
|
||||
func (s *MultiService) AddSubreddit(ctx context.Context, multiPath string, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/multi/%s/r/%s", multiPath, subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("model", fmt.Sprintf(`{"name":"%s"}`, subreddit))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPut, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DeleteSubreddit removes a subreddit from a multireddit.
|
||||
func (s *MultiService) DeleteSubreddit(ctx context.Context, multiPath string, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/multi/%s/r/%s", multiPath, subreddit)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedMulti = &Multi{
|
||||
Name: "test",
|
||||
DisplayName: "test",
|
||||
Path: "/user/v_95/m/test/",
|
||||
Subreddits: []string{"nba", "golang"},
|
||||
CopiedFrom: nil,
|
||||
|
||||
Owner: "v_95",
|
||||
OwnerID: "t2_164ab8",
|
||||
Created: &Timestamp{time.Date(2020, 7, 11, 4, 55, 12, 0, time.UTC)},
|
||||
|
||||
NumberOfSubscribers: 0,
|
||||
Visibility: "private",
|
||||
CanEdit: true,
|
||||
}
|
||||
|
||||
var expectedMulti2 = &Multi{
|
||||
Name: "test2",
|
||||
DisplayName: "test2",
|
||||
Path: "/user/v_95/m/test2/",
|
||||
Subreddits: []string{"redditdev", "test"},
|
||||
CopiedFrom: nil,
|
||||
|
||||
Owner: "v_95",
|
||||
OwnerID: "t2_164ab8",
|
||||
Created: &Timestamp{time.Date(2020, 7, 11, 4, 57, 3, 0, time.UTC)},
|
||||
|
||||
NumberOfSubscribers: 0,
|
||||
Visibility: "private",
|
||||
CanEdit: true,
|
||||
}
|
||||
|
||||
func TestMultiService_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/multi.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/multi/user/testuser/m/testmulti", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
multi, _, err := client.Multi.Get(ctx, "user/testuser/m/testmulti")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedMulti, multi)
|
||||
}
|
||||
|
||||
func TestMultiService_Mine(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/multis.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/multi/mine", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
multis, _, err := client.Multi.Mine(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []Multi{*expectedMulti, *expectedMulti2}, multis)
|
||||
}
|
||||
|
||||
func TestMultiService_Of(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/multis.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/multi/user/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
multis, _, err := client.Multi.Of(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []Multi{*expectedMulti, *expectedMulti2}, multis)
|
||||
}
|
||||
|
||||
func TestMultiService_Copy(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/multi.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/multi/copy", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("from", "user/testuser/m/testmulti")
|
||||
form.Set("to", "user/testuser2/m/testmulti2")
|
||||
form.Set("description_md", "this is a multireddit")
|
||||
form.Set("display_name", "hello")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Multi.Copy(ctx, nil)
|
||||
require.EqualError(t, err, "copyRequest: cannot be nil")
|
||||
|
||||
multi, _, err := client.Multi.Copy(ctx, &MultiCopyRequest{
|
||||
FromPath: "user/testuser/m/testmulti",
|
||||
ToPath: "user/testuser2/m/testmulti2",
|
||||
Description: "this is a multireddit",
|
||||
DisplayName: "hello",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedMulti, multi)
|
||||
}
|
||||
|
||||
func TestMultiService_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/multi.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
createRequest := &MultiCreateOrUpdateRequest{
|
||||
Name: "testmulti",
|
||||
Description: "this is a multireddit",
|
||||
Subreddits: []string{"golang"},
|
||||
Visibility: "public",
|
||||
}
|
||||
|
||||
mux.HandleFunc("/api/multi", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
|
||||
model := r.Form.Get("model")
|
||||
|
||||
expectedCreateRequest := new(MultiCreateOrUpdateRequest)
|
||||
err = json.Unmarshal([]byte(model), expectedCreateRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCreateRequest, createRequest)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Multi.Create(ctx, nil)
|
||||
require.EqualError(t, err, "createRequest: cannot be nil")
|
||||
|
||||
multi, _, err := client.Multi.Create(ctx, createRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedMulti, multi)
|
||||
}
|
||||
|
||||
func TestMultiService_Update(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/multi.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
updateRequest := &MultiCreateOrUpdateRequest{
|
||||
Name: "testmulti",
|
||||
Description: "this is a multireddit",
|
||||
Visibility: "public",
|
||||
}
|
||||
|
||||
mux.HandleFunc("/api/multi/user/testuser/m/testmulti", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPut, r.Method)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
|
||||
model := r.Form.Get("model")
|
||||
|
||||
expectedCreateRequest := new(MultiCreateOrUpdateRequest)
|
||||
err = json.Unmarshal([]byte(model), expectedCreateRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCreateRequest, updateRequest)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Multi.Update(ctx, "user/testuser/m/testmulti", nil)
|
||||
require.EqualError(t, err, "updateRequest: cannot be nil")
|
||||
|
||||
multi, _, err := client.Multi.Update(ctx, "user/testuser/m/testmulti", updateRequest)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedMulti, multi)
|
||||
}
|
||||
|
||||
func TestMultiService_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/multi/user/testuser/m/testmulti", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodDelete, r.Method)
|
||||
})
|
||||
|
||||
_, err := client.Multi.Delete(ctx, "user/testuser/m/testmulti")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMultiService_GetDescription(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/description.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/multi/user/testuser/m/testmulti/description", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
description, _, err := client.Multi.GetDescription(ctx, "user/testuser/m/testmulti")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", description)
|
||||
}
|
||||
|
||||
func TestMultiService_UpdateDescription(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/multi/description.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/multi/user/testuser/m/testmulti/description", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPut, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("model", `{"body_md":"hello world"}`)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
description, _, err := client.Multi.UpdateDescription(ctx, "user/testuser/m/testmulti", "hello world")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", description)
|
||||
}
|
||||
|
||||
func TestMultiService_AddSubreddit(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/multi/user/testuser/m/testmulti/r/golang", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPut, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("model", `{"name":"golang"}`)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.Multi.AddSubreddit(ctx, "user/testuser/m/testmulti", "golang")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMultiService_DeleteSubreddit(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/multi/user/testuser/m/testmulti/r/golang", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodDelete, r.Method)
|
||||
})
|
||||
|
||||
_, err := client.Multi.DeleteSubreddit(ctx, "user/testuser/m/testmulti", "golang")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// postAndCommentService handles communication with the post and comment
|
||||
// related methods of the Reddit API.
|
||||
// This service holds functionality common to both posts and comments.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_links_and_comments
|
||||
type postAndCommentService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type vote int
|
||||
|
||||
// Reddit interprets -1, 0, 1 as downvote, no vote, and upvote, respectively.
|
||||
const (
|
||||
downvote vote = iota - 1
|
||||
novote
|
||||
upvote
|
||||
)
|
||||
|
||||
// Delete deletes a post or comment via its full ID.
|
||||
func (s *postAndCommentService) Delete(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/del"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Save saves a post or comment.
|
||||
func (s *postAndCommentService) Save(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/save"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unsave unsaves a post or comment.
|
||||
func (s *postAndCommentService) Unsave(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/unsave"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// EnableReplies enables inbox replies for one of your posts or comments.
|
||||
func (s *postAndCommentService) EnableReplies(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/sendreplies"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
form.Set("state", "true")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DisableReplies dsables inbox replies for one of your posts or comments.
|
||||
func (s *postAndCommentService) DisableReplies(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/sendreplies"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
form.Set("state", "false")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Lock locks a post or comment, preventing it from receiving new comments.
|
||||
func (s *postAndCommentService) Lock(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/lock"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unlock unlocks a post or comment, allowing it to receive new comments.
|
||||
func (s *postAndCommentService) Unlock(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/unlock"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (s *postAndCommentService) vote(ctx context.Context, id string, vote vote) (*Response, error) {
|
||||
path := "api/vote"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
form.Set("dir", fmt.Sprint(vote))
|
||||
form.Set("rank", "10")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Upvote upvotes a post or a comment.
|
||||
func (s *postAndCommentService) Upvote(ctx context.Context, id string) (*Response, error) {
|
||||
return s.vote(ctx, id, upvote)
|
||||
}
|
||||
|
||||
// Downvote downvotes a post or a comment.
|
||||
func (s *postAndCommentService) Downvote(ctx context.Context, id string) (*Response, error) {
|
||||
return s.vote(ctx, id, downvote)
|
||||
}
|
||||
|
||||
// RemoveVote removes your vote on a post or a comment.
|
||||
func (s *postAndCommentService) RemoveVote(ctx context.Context, id string) (*Response, error) {
|
||||
return s.vote(ctx, id, novote)
|
||||
}
|
||||
+578
@@ -0,0 +1,578 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// PostService handles communication with the post
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_links_and_comments
|
||||
type PostService struct {
|
||||
*postAndCommentService
|
||||
client *Client
|
||||
}
|
||||
|
||||
type submittedLinkRoot struct {
|
||||
JSON struct {
|
||||
Data *Submitted `json:"data,omitempty"`
|
||||
} `json:"json"`
|
||||
}
|
||||
|
||||
// Submitted is a newly submitted post on Reddit.
|
||||
type Submitted struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// SubmitTextOptions are options used for text posts.
|
||||
type SubmitTextOptions struct {
|
||||
Subreddit string `url:"sr,omitempty"`
|
||||
Title string `url:"title,omitempty"`
|
||||
Text string `url:"text,omitempty"`
|
||||
|
||||
FlairID string `url:"flair_id,omitempty"`
|
||||
FlairText string `url:"flair_text,omitempty"`
|
||||
|
||||
SendReplies *bool `url:"sendreplies,omitempty"`
|
||||
NSFW bool `url:"nsfw,omitempty"`
|
||||
Spoiler bool `url:"spoiler,omitempty"`
|
||||
}
|
||||
|
||||
// SubmitLinkOptions are options used for link posts.
|
||||
type SubmitLinkOptions struct {
|
||||
Subreddit string `url:"sr,omitempty"`
|
||||
Title string `url:"title,omitempty"`
|
||||
URL string `url:"url,omitempty"`
|
||||
|
||||
FlairID string `url:"flair_id,omitempty"`
|
||||
FlairText string `url:"flair_text,omitempty"`
|
||||
|
||||
SendReplies *bool `url:"sendreplies,omitempty"`
|
||||
Resubmit bool `url:"resubmit,omitempty"`
|
||||
NSFW bool `url:"nsfw,omitempty"`
|
||||
Spoiler bool `url:"spoiler,omitempty"`
|
||||
}
|
||||
|
||||
// Get returns a post with its comments.
|
||||
// id is the ID36 of the post, not its full id.
|
||||
// Example: instead of t3_abc123, use abc123.
|
||||
func (s *PostService) Get(ctx context.Context, id string) (*PostAndComments, *Response, error) {
|
||||
path := fmt.Sprintf("comments/%s", id)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(PostAndComments)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Duplicates returns the post with the id, and a list of its duplicates.
|
||||
// id is the ID36 of the post, not its full id.
|
||||
// Example: instead of t3_abc123, use abc123.
|
||||
func (s *PostService) Duplicates(ctx context.Context, id string, opts *ListDuplicatePostOptions) (*Post, *Posts, *Response, error) {
|
||||
path := fmt.Sprintf("duplicates/%s", id)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
var root [2]rootListing
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
|
||||
post := root[0].Data.Things.Posts[0]
|
||||
duplicates := root[1].getPosts()
|
||||
|
||||
return post, duplicates, resp, nil
|
||||
}
|
||||
|
||||
func (s *PostService) submit(ctx context.Context, v interface{}) (*Submitted, *Response, error) {
|
||||
path := "api/submit"
|
||||
|
||||
form, err := query.Values(v)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(submittedLinkRoot)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.JSON.Data, resp, nil
|
||||
}
|
||||
|
||||
// SubmitText submits a text post.
|
||||
func (s *PostService) SubmitText(ctx context.Context, opts SubmitTextOptions) (*Submitted, *Response, error) {
|
||||
type submit struct {
|
||||
SubmitTextOptions
|
||||
Kind string `url:"kind,omitempty"`
|
||||
}
|
||||
return s.submit(ctx, &submit{opts, "self"})
|
||||
}
|
||||
|
||||
// SubmitLink submits a link post.
|
||||
func (s *PostService) SubmitLink(ctx context.Context, opts SubmitLinkOptions) (*Submitted, *Response, error) {
|
||||
type submit struct {
|
||||
SubmitLinkOptions
|
||||
Kind string `url:"kind,omitempty"`
|
||||
}
|
||||
return s.submit(ctx, &submit{opts, "link"})
|
||||
}
|
||||
|
||||
// Edit edits a post.
|
||||
func (s *PostService) Edit(ctx context.Context, id string, text string) (*Post, *Response, error) {
|
||||
path := "api/editusertext"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("return_rtjson", "true")
|
||||
form.Set("thing_id", id)
|
||||
form.Set("text", text)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Post)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Hide hides posts.
|
||||
func (s *PostService) Hide(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/hide"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unhide unhides posts.
|
||||
func (s *PostService) Unhide(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/unhide"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// MarkNSFW marks a post as NSFW.
|
||||
func (s *PostService) MarkNSFW(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/marknsfw"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UnmarkNSFW unmarks a post as NSFW.
|
||||
func (s *PostService) UnmarkNSFW(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/unmarknsfw"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Spoiler marks a post as a spoiler.
|
||||
func (s *PostService) Spoiler(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/spoiler"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unspoiler unmarks a post as a spoiler.
|
||||
func (s *PostService) Unspoiler(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/unspoiler"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Sticky stickies a post in its subreddit.
|
||||
// When bottom is true, the post will be set as the bottom sticky (the 2nd one).
|
||||
// If no top sticky exists, the post will become the top sticky regardless.
|
||||
// When attempting to sticky a post that's already stickied, it will return a 409 Conflict error.
|
||||
func (s *PostService) Sticky(ctx context.Context, id string, bottom bool) (*Response, error) {
|
||||
path := "api/set_subreddit_sticky"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", id)
|
||||
form.Set("state", "true")
|
||||
if !bottom {
|
||||
form.Set("num", "1")
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unsticky unstickies a post in its subreddit.
|
||||
func (s *PostService) Unsticky(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/set_subreddit_sticky"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", id)
|
||||
form.Set("state", "false")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// PinToProfile pins one of your posts to your profile.
|
||||
// TODO: very inconsistent behaviour, not sure I'm ready to include this parameter yet.
|
||||
// The pos parameter should be a number between 1-4 (inclusive), indicating the position at which
|
||||
// the post should appear on your profile.
|
||||
// Note: The position will be bumped upward if there's space. E.g. if you only have 1 pinned post,
|
||||
// and you try to pin another post to position 3, it will be pinned at 2.
|
||||
// When attempting to pin a post that's already pinned, it will return a 409 Conflict error.
|
||||
func (s *PostService) PinToProfile(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/set_subreddit_sticky"
|
||||
|
||||
// if pos < 1 {
|
||||
// pos = 1
|
||||
// }
|
||||
// if pos > 4 {
|
||||
// pos = 4
|
||||
// }
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", id)
|
||||
form.Set("state", "true")
|
||||
form.Set("to_profile", "true")
|
||||
// form.Set("num", fmt.Sprint(pos))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UnpinFromProfile unpins one of your posts from your profile.
|
||||
func (s *PostService) UnpinFromProfile(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/set_subreddit_sticky"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", id)
|
||||
form.Set("state", "false")
|
||||
form.Set("to_profile", "true")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// setSuggestedSort sets the suggested comment sort for the post.
|
||||
// sort must be one of: confidence (i.e. best), top, new, controversial, old, random, qa, live
|
||||
func (s *PostService) setSuggestedSort(ctx context.Context, id string, sort string) (*Response, error) {
|
||||
path := "api/set_suggested_sort"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", id)
|
||||
form.Set("sort", sort)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// SetSuggestedSortBest sets the suggested comment sort for the post to best.
|
||||
func (s *PostService) SetSuggestedSortBest(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "confidence")
|
||||
}
|
||||
|
||||
// SetSuggestedSortTop sets the suggested comment sort for the post to top.
|
||||
func (s *PostService) SetSuggestedSortTop(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "top")
|
||||
}
|
||||
|
||||
// SetSuggestedSortNew sets the suggested comment sort for the post to new.
|
||||
func (s *PostService) SetSuggestedSortNew(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "new")
|
||||
}
|
||||
|
||||
// SetSuggestedSortControversial sets the suggested comment sort for the post to controversial.
|
||||
func (s *PostService) SetSuggestedSortControversial(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "controversial")
|
||||
}
|
||||
|
||||
// SetSuggestedSortOld sorts the comments on the posts randomly.
|
||||
func (s *PostService) SetSuggestedSortOld(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "old")
|
||||
}
|
||||
|
||||
// SetSuggestedSortRandom sets the suggested comment sort for the post to random.
|
||||
func (s *PostService) SetSuggestedSortRandom(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "random")
|
||||
}
|
||||
|
||||
// SetSuggestedSortAMA sets the suggested comment sort for the post to a Q&A styled fashion.
|
||||
func (s *PostService) SetSuggestedSortAMA(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "qa")
|
||||
}
|
||||
|
||||
// SetSuggestedSortLive sets the suggested comment sort for the post to stream new comments as they're posted.
|
||||
// As of now, this is still in beta, so it's not a fully developed feature yet. It just sets the sort as "new" for now.
|
||||
func (s *PostService) SetSuggestedSortLive(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "live")
|
||||
}
|
||||
|
||||
// ClearSuggestedSort clears the suggested comment sort for the post.
|
||||
func (s *PostService) ClearSuggestedSort(ctx context.Context, id string) (*Response, error) {
|
||||
return s.setSuggestedSort(ctx, id, "")
|
||||
}
|
||||
|
||||
// EnableContestMode enables contest mode for the post.
|
||||
// Comments will be sorted randomly and regular users cannot see comment scores.
|
||||
func (s *PostService) EnableContestMode(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/set_contest_mode"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", id)
|
||||
form.Set("state", "true")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DisableContestMode disables contest mode for the post.
|
||||
func (s *PostService) DisableContestMode(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/set_contest_mode"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", id)
|
||||
form.Set("state", "false")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// LoadMoreComments retrieves more comments that were left out when initially fetching the post.
|
||||
func (s *PostService) LoadMoreComments(ctx context.Context, pc *PostAndComments) (*Response, error) {
|
||||
if pc == nil {
|
||||
return nil, errors.New("pc: cannot be nil")
|
||||
}
|
||||
|
||||
if !pc.HasMore() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
postID := pc.Post.FullID
|
||||
commentIDs := pc.More.Children
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link_id", postID)
|
||||
form.Set("children", strings.Join(commentIDs, ","))
|
||||
|
||||
path := "api/morechildren"
|
||||
|
||||
// This was originally a GET, but with POST you can send a bigger payload
|
||||
// since it's in the body and not the URI.
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
JSON struct {
|
||||
Data struct {
|
||||
Things things `json:"things"`
|
||||
} `json:"data"`
|
||||
} `json:"json"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
comments := root.JSON.Data.Things.Comments
|
||||
for _, c := range comments {
|
||||
pc.addCommentToTree(c)
|
||||
}
|
||||
|
||||
noMore := true
|
||||
|
||||
mores := root.JSON.Data.Things.Mores
|
||||
for _, m := range mores {
|
||||
if strings.HasPrefix(m.ParentID, kindPost+"_") {
|
||||
noMore = false
|
||||
}
|
||||
pc.addMoreToTree(m)
|
||||
}
|
||||
|
||||
if noMore {
|
||||
pc.More = nil
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *PostService) random(ctx context.Context, subreddits ...string) (*PostAndComments, *Response, error) {
|
||||
path := "random"
|
||||
if len(subreddits) > 0 {
|
||||
path = fmt.Sprintf("r/%s/random", strings.Join(subreddits, "+"))
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(PostAndComments)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// RandomFromSubreddits returns a random post and its comments from the subreddits.
|
||||
// If no subreddits are provided, Reddit runs the query against your subscriptions.
|
||||
func (s *PostService) RandomFromSubreddits(ctx context.Context, subreddits ...string) (*PostAndComments, *Response, error) {
|
||||
return s.random(ctx, subreddits...)
|
||||
}
|
||||
|
||||
// Random returns a random post and its comments from all of Reddit.
|
||||
func (s *PostService) Random(ctx context.Context) (*PostAndComments, *Response, error) {
|
||||
return s.random(ctx, "all")
|
||||
}
|
||||
|
||||
// RandomFromSubscriptions returns a random post and its comments from your subscriptions.
|
||||
func (s *PostService) RandomFromSubscriptions(ctx context.Context) (*PostAndComments, *Response, error) {
|
||||
return s.random(ctx)
|
||||
}
|
||||
|
||||
// MarkVisited marks the post(s) as visited.
|
||||
// This method requires a subscription to Reddit premium.
|
||||
func (s *PostService) MarkVisited(ctx context.Context, ids ...string) (*Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
|
||||
path := "api/store_visits"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("links", strings.Join(ids, ","))
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
+1154
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
Docs:
|
||||
- https://www.reddit.com/dev/api/
|
||||
- https://github.com/reddit-archive/reddit/wiki/api
|
||||
- https://github.com/reddit-archive/reddit/wiki/OAuth2
|
||||
- https://github.com/reddit-archive/reddit/wiki/OAuth2-Quick-Start-Example
|
||||
|
||||
1. Go to https://www.reddit.com/prefs/apps and create an app. There are 3 types of apps:
|
||||
- Web app. Service is available over http or https, preferably the latter.
|
||||
- Installed app, such as a mobile app on a user's device which you can't control.
|
||||
Redirect the user to a URI after they grant your app permissions.
|
||||
- Script (the simplest type of app). Select this if you are the only person who will
|
||||
use the app. Only has access to your account.
|
||||
|
||||
Best option for a client like this is to use the script option.
|
||||
|
||||
2. After creating the app, you will get a client id and client secret.
|
||||
|
||||
3. Send a POST request (with the Content-Type header set to "application/x-www-form-urlencoded")
|
||||
to https://www.reddit.com/api/v1/access_token with the following form values:
|
||||
- grant_type=password
|
||||
- username={your Reddit username}
|
||||
- password={your Reddit password}
|
||||
|
||||
4. You should receive a response body like the following:
|
||||
{
|
||||
"access_token": "70743860-DRhHVNSEOMu1ldlI",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "*"
|
||||
}
|
||||
*/
|
||||
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
type oauthTokenSource struct {
|
||||
ctx context.Context
|
||||
config *oauth2.Config
|
||||
username, password string
|
||||
}
|
||||
|
||||
func (s *oauthTokenSource) Token() (*oauth2.Token, error) {
|
||||
return s.config.PasswordCredentialsToken(s.ctx, s.username, s.password)
|
||||
}
|
||||
|
||||
func oauthTransport(client *Client) http.RoundTripper {
|
||||
// We need to set a custom user agent, because using the one set by default by the
|
||||
// stdlib gives us 429 Too Many Request responses from the Reddit API.
|
||||
userAgentTransport := &userAgentTransport{
|
||||
userAgent: client.UserAgent(),
|
||||
Base: client.client.Transport,
|
||||
}
|
||||
|
||||
httpClient := &http.Client{Transport: userAgentTransport}
|
||||
ctx := context.Background()
|
||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||
|
||||
config := &oauth2.Config{
|
||||
ClientID: client.ID,
|
||||
ClientSecret: client.Secret,
|
||||
Endpoint: oauth2.Endpoint{
|
||||
TokenURL: client.TokenURL.String(),
|
||||
AuthStyle: oauth2.AuthStyleInHeader,
|
||||
},
|
||||
}
|
||||
|
||||
tokenSource := oauth2.ReuseTokenSource(nil, &oauthTokenSource{
|
||||
ctx: ctx,
|
||||
config: config,
|
||||
username: client.Username,
|
||||
password: client.Password,
|
||||
})
|
||||
|
||||
return &oauth2.Transport{
|
||||
Source: tokenSource,
|
||||
Base: userAgentTransport,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Opt is a configuration option to initialize a client.
|
||||
type Opt func(*Client) error
|
||||
|
||||
// FromEnv configures the client with values from environment variables.
|
||||
//
|
||||
// Supported environment variables:
|
||||
// GO_REDDIT_CLIENT_ID to set the client's id.
|
||||
// GO_REDDIT_CLIENT_SECRET to set the client's secret.
|
||||
// GO_REDDIT_CLIENT_USERNAME to set the client's username.
|
||||
// GO_REDDIT_CLIENT_PASSWORD to set the client's password.
|
||||
func FromEnv(c *Client) error {
|
||||
if v, ok := os.LookupEnv("GO_REDDIT_CLIENT_ID"); ok {
|
||||
c.ID = v
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv("GO_REDDIT_CLIENT_SECRET"); ok {
|
||||
c.Secret = v
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv("GO_REDDIT_CLIENT_USERNAME"); ok {
|
||||
c.Username = v
|
||||
}
|
||||
|
||||
if v, ok := os.LookupEnv("GO_REDDIT_CLIENT_PASSWORD"); ok {
|
||||
c.Password = v
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithCredentials sets the necessary values for the client to authenticate via OAuth2.
|
||||
func WithCredentials(id, secret, username, password string) Opt {
|
||||
return func(c *Client) error {
|
||||
c.ID = id
|
||||
c.Secret = secret
|
||||
c.Username = username
|
||||
c.Password = password
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBaseURL sets the base URL for the client to make requests to.
|
||||
func WithBaseURL(u string) Opt {
|
||||
return func(c *Client) error {
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.BaseURL = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTokenURL sets the url used to get access tokens.
|
||||
func WithTokenURL(u string) Opt {
|
||||
return func(c *Client) error {
|
||||
url, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.TokenURL = url
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFromEnv(t *testing.T) {
|
||||
os.Setenv("GO_REDDIT_CLIENT_ID", "id1")
|
||||
defer os.Unsetenv("GO_REDDIT_CLIENT_ID")
|
||||
|
||||
os.Setenv("GO_REDDIT_CLIENT_SECRET", "secret1")
|
||||
defer os.Unsetenv("GO_REDDIT_CLIENT_SECRET")
|
||||
|
||||
os.Setenv("GO_REDDIT_CLIENT_USERNAME", "username1")
|
||||
defer os.Unsetenv("GO_REDDIT_CLIENT_USERNAME")
|
||||
|
||||
os.Setenv("GO_REDDIT_CLIENT_PASSWORD", "password1")
|
||||
defer os.Unsetenv("GO_REDDIT_CLIENT_PASSWORD")
|
||||
|
||||
c, err := NewClient(nil, FromEnv)
|
||||
require.NoError(t, err)
|
||||
|
||||
type values struct {
|
||||
id, secret, username, password string
|
||||
}
|
||||
|
||||
expect := values{"id1", "secret1", "username1", "password1"}
|
||||
actual := values{c.ID, c.Secret, c.Username, c.Password}
|
||||
require.Equal(t, expect, actual)
|
||||
}
|
||||
|
||||
func TestWithCredentials(t *testing.T) {
|
||||
withCredentials := WithCredentials("id1", "secret1", "username1", "password1")
|
||||
c, err := NewClient(nil, withCredentials)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "id1", c.ID)
|
||||
require.Equal(t, "secret1", c.Secret)
|
||||
require.Equal(t, "username1", c.Username)
|
||||
require.Equal(t, "password1", c.Password)
|
||||
}
|
||||
|
||||
func TestWithBaseURL(t *testing.T) {
|
||||
baseURL := "http://localhost:8080"
|
||||
c, err := NewClient(nil, WithBaseURL(baseURL))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, baseURL, c.BaseURL.String())
|
||||
}
|
||||
|
||||
func TestWithTokenURL(t *testing.T) {
|
||||
tokenURL := "http://localhost:8080/api/v1/access_token"
|
||||
c, err := NewClient(nil, WithTokenURL(tokenURL))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tokenURL, c.TokenURL.String())
|
||||
}
|
||||
@@ -0,0 +1,475 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
libraryName = "github.com/vartanbeno/go-reddit"
|
||||
libraryVersion = "0.0.1"
|
||||
|
||||
defaultBaseURL = "https://oauth.reddit.com"
|
||||
defaultTokenURL = "https://www.reddit.com/api/v1/access_token"
|
||||
|
||||
mediaTypeJSON = "application/json"
|
||||
mediaTypeForm = "application/x-www-form-urlencoded"
|
||||
|
||||
headerContentType = "Content-Type"
|
||||
headerAccept = "Accept"
|
||||
headerUserAgent = "User-Agent"
|
||||
)
|
||||
|
||||
// cloneRequest returns a clone of the provided *http.Request.
|
||||
// The clone is a shallow copy of the struct and its Header map,
|
||||
// since we'll only be modify the headers.
|
||||
// Per the specification of http.RoundTripper, we should not directly modify a request.
|
||||
func cloneRequest(r *http.Request) *http.Request {
|
||||
r2 := new(http.Request)
|
||||
*r2 = *r
|
||||
// deep copy of the Header
|
||||
r2.Header = make(http.Header, len(r.Header))
|
||||
for k, s := range r.Header {
|
||||
r2.Header[k] = append([]string(nil), s...)
|
||||
}
|
||||
return r2
|
||||
}
|
||||
|
||||
// Sets the User-Agent header for requests.
|
||||
type userAgentTransport struct {
|
||||
userAgent string
|
||||
Base http.RoundTripper
|
||||
}
|
||||
|
||||
func (t *userAgentTransport) setUserAgent(req *http.Request) *http.Request {
|
||||
req2 := cloneRequest(req)
|
||||
req2.Header.Set(headerUserAgent, t.userAgent)
|
||||
return req2
|
||||
}
|
||||
|
||||
func (t *userAgentTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
req2 := t.setUserAgent(req)
|
||||
return t.base().RoundTrip(req2)
|
||||
}
|
||||
|
||||
func (t *userAgentTransport) base() http.RoundTripper {
|
||||
if t.Base != nil {
|
||||
return t.Base
|
||||
}
|
||||
return http.DefaultTransport
|
||||
}
|
||||
|
||||
// RequestCompletionCallback defines the type of the request callback function.
|
||||
type RequestCompletionCallback func(*http.Request, *http.Response)
|
||||
|
||||
// Client manages communication with the Reddit API.
|
||||
type Client struct {
|
||||
// HTTP client used to communicate with the Reddit API.
|
||||
client *http.Client
|
||||
|
||||
BaseURL *url.URL
|
||||
TokenURL *url.URL
|
||||
|
||||
userAgent string
|
||||
|
||||
ID string
|
||||
Secret string
|
||||
Username string
|
||||
Password string
|
||||
|
||||
// This is the client's user ID in Reddit's database.
|
||||
redditID string
|
||||
|
||||
Account *AccountService
|
||||
Collection *CollectionService
|
||||
Comment *CommentService
|
||||
Emoji *EmojiService
|
||||
Flair *FlairService
|
||||
Listings *ListingsService
|
||||
Message *MessageService
|
||||
Moderation *ModerationService
|
||||
Multi *MultiService
|
||||
Post *PostService
|
||||
Subreddit *SubredditService
|
||||
User *UserService
|
||||
|
||||
oauth2Transport *oauth2.Transport
|
||||
|
||||
onRequestCompleted RequestCompletionCallback
|
||||
}
|
||||
|
||||
// OnRequestCompleted sets the client's request completion callback.
|
||||
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
|
||||
c.onRequestCompleted = rc
|
||||
}
|
||||
|
||||
func newClient(httpClient *http.Client) *Client {
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{}
|
||||
}
|
||||
|
||||
// todo...
|
||||
// Some endpoints (notably the ones to get random subreddits/posts) redirect to a
|
||||
// reddit.com url, which returns a 403 Forbidden for some reason, unless the url's
|
||||
// host is changed to oauth.reddit.com
|
||||
httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
redirectURL := req.URL.String()
|
||||
redirectURL = strings.Replace(redirectURL, "https://www.reddit.com", defaultBaseURL, 1)
|
||||
|
||||
reqURL, err := url.Parse(redirectURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.URL = reqURL
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
tokenURL, _ := url.Parse(defaultTokenURL)
|
||||
|
||||
c := &Client{client: httpClient, BaseURL: baseURL, TokenURL: tokenURL}
|
||||
|
||||
c.Account = &AccountService{client: c}
|
||||
c.Collection = &CollectionService{client: c}
|
||||
c.Emoji = &EmojiService{client: c}
|
||||
c.Flair = &FlairService{client: c}
|
||||
c.Listings = &ListingsService{client: c}
|
||||
c.Message = &MessageService{client: c}
|
||||
c.Moderation = &ModerationService{client: c}
|
||||
c.Multi = &MultiService{client: c}
|
||||
c.Subreddit = &SubredditService{client: c}
|
||||
c.User = &UserService{client: c}
|
||||
|
||||
postAndCommentService := &postAndCommentService{client: c}
|
||||
c.Comment = &CommentService{client: c, postAndCommentService: postAndCommentService}
|
||||
c.Post = &PostService{client: c, postAndCommentService: postAndCommentService}
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// NewClient returns a client that can make requests to the Reddit API.
|
||||
func NewClient(httpClient *http.Client, opts ...Opt) (c *Client, err error) {
|
||||
c = newClient(httpClient)
|
||||
|
||||
for _, opt := range opts {
|
||||
if err = opt(c); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
oauthTransport := oauthTransport(c)
|
||||
c.client.Transport = oauthTransport
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UserAgent returns the client's user agent.
|
||||
func (c *Client) UserAgent() string {
|
||||
if c.userAgent == "" {
|
||||
c.userAgent = fmt.Sprintf("golang:%s:v%s (by /u/%s)", libraryName, libraryVersion, c.Username)
|
||||
}
|
||||
return c.userAgent
|
||||
}
|
||||
|
||||
// NewRequest creates an API request.
|
||||
// The path is the relative URL which will be resolves to the BaseURL of the Client.
|
||||
// It should always be specified without a preceding slash.
|
||||
func (c *Client) NewRequest(method string, path string, body interface{}) (*http.Request, error) {
|
||||
u, err := c.BaseURL.Parse(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if body != nil {
|
||||
err = json.NewEncoder(buf).Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
reqBody := bytes.NewReader(buf.Bytes())
|
||||
req, err := http.NewRequest(method, u.String(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add(headerContentType, mediaTypeJSON)
|
||||
req.Header.Add(headerAccept, mediaTypeJSON)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewRequestWithForm creates an API request with form data.
|
||||
// The path is the relative URL which will be resolves to the BaseURL of the Client.
|
||||
// It should always be specified without a preceding slash.
|
||||
func (c *Client) NewRequestWithForm(method string, path string, form url.Values) (*http.Request, error) {
|
||||
u, err := c.BaseURL.Parse(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Add(headerContentType, mediaTypeForm)
|
||||
req.Header.Add(headerAccept, mediaTypeJSON)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Response is a PlayNetwork response. This wraps the standard http.Response returned from PlayNetwork.
|
||||
type Response struct {
|
||||
*http.Response
|
||||
}
|
||||
|
||||
// newResponse creates a new Response for the provided http.Response.
|
||||
func newResponse(r *http.Response) *Response {
|
||||
response := Response{Response: r}
|
||||
return &response
|
||||
}
|
||||
|
||||
// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
|
||||
// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
|
||||
// the raw response will be written to v, without attempting to decode it.
|
||||
func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
|
||||
resp, err := DoRequestWithClient(ctx, c.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.onRequestCompleted != nil {
|
||||
c.onRequestCompleted(req, resp)
|
||||
}
|
||||
|
||||
response := newResponse(resp)
|
||||
defer func() {
|
||||
if rerr := response.Body.Close(); err == nil {
|
||||
err = rerr
|
||||
}
|
||||
}()
|
||||
|
||||
err = CheckResponse(resp)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if v != nil {
|
||||
if w, ok := v.(io.Writer); ok {
|
||||
_, err = io.Copy(w, response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
err = json.NewDecoder(response.Body).Decode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
// id returns the client's Reddit ID.
|
||||
func (c *Client) id(ctx context.Context) (string, *Response, error) {
|
||||
if c.redditID != "" {
|
||||
return c.redditID, nil, nil
|
||||
}
|
||||
|
||||
self, resp, err := c.User.Get(ctx, c.Username)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
|
||||
c.redditID = fmt.Sprintf("%s_%s", kindAccount, self.ID)
|
||||
return c.redditID, resp, nil
|
||||
}
|
||||
|
||||
// DoRequest submits an HTTP request.
|
||||
func DoRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
|
||||
return DoRequestWithClient(ctx, http.DefaultClient, req)
|
||||
}
|
||||
|
||||
// DoRequestWithClient submits an HTTP request using the specified client.
|
||||
func DoRequestWithClient(ctx context.Context, client *http.Client, req *http.Request) (*http.Response, error) {
|
||||
req = req.WithContext(ctx)
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
// CheckResponse checks the API response for errors, and returns them if present.
|
||||
// A response is considered an error if it has a status code outside the 200 range.
|
||||
// Reddit also sometimes sends errors with 200 codes; we check for those too.
|
||||
func CheckResponse(r *http.Response) error {
|
||||
jsonErrorResponse := &JSONErrorResponse{Response: r}
|
||||
|
||||
data, err := ioutil.ReadAll(r.Body)
|
||||
if err == nil && len(data) > 0 {
|
||||
json.Unmarshal(data, jsonErrorResponse)
|
||||
if len(jsonErrorResponse.JSON.Errors) > 0 {
|
||||
return jsonErrorResponse
|
||||
}
|
||||
}
|
||||
|
||||
// reset response body
|
||||
r.Body = ioutil.NopCloser(bytes.NewBuffer(data))
|
||||
|
||||
if c := r.StatusCode; c >= 200 && c <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorResponse := &ErrorResponse{Response: r}
|
||||
data, err = ioutil.ReadAll(r.Body)
|
||||
if err == nil && len(data) > 0 {
|
||||
err := json.Unmarshal(data, errorResponse)
|
||||
if err != nil {
|
||||
errorResponse.Message = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
// ListOptions specifies the optional parameters to various API calls that return a listing.
|
||||
type ListOptions struct {
|
||||
// Maximum number of items to be returned.
|
||||
// Generally, the default is 25 and max is 100.
|
||||
Limit int `url:"limit,omitempty"`
|
||||
|
||||
// The full ID of an item in the listing to use
|
||||
// as the anchor point of the list. Only items
|
||||
// appearing after it will be returned.
|
||||
After string `url:"after,omitempty"`
|
||||
|
||||
// The full ID of an item in the listing to use
|
||||
// as the anchor point of the list. Only items
|
||||
// appearing before it will be returned.
|
||||
Before string `url:"before,omitempty"`
|
||||
}
|
||||
|
||||
// ListSubredditOptions defines possible options used when searching for subreddits.
|
||||
type ListSubredditOptions struct {
|
||||
ListOptions
|
||||
// One of: relevance, activity.
|
||||
Sort string `url:"sort,omitempty"`
|
||||
}
|
||||
|
||||
// ListPostOptions defines possible options used when getting posts from a subreddit.
|
||||
type ListPostOptions struct {
|
||||
ListOptions
|
||||
// One of: hour, day, week, month, year, all.
|
||||
Time string `url:"t,omitempty"`
|
||||
}
|
||||
|
||||
// ListPostSearchOptions defines possible options used when searching for posts within a subreddit.
|
||||
type ListPostSearchOptions struct {
|
||||
ListPostOptions
|
||||
// One of: relevance, hot, top, new, comments.
|
||||
Sort string `url:"sort,omitempty"`
|
||||
}
|
||||
|
||||
// ListUserOverviewOptions defines possible options used when getting a user's post and/or comments.
|
||||
type ListUserOverviewOptions struct {
|
||||
ListOptions
|
||||
// One of: hot, new, top, controversial.
|
||||
Sort string `url:"sort,omitempty"`
|
||||
// One of: hour, day, week, month, year, all.
|
||||
Time string `url:"t,omitempty"`
|
||||
}
|
||||
|
||||
// ListDuplicatePostOptions defines possible options used when getting duplicates of a post, i.e.
|
||||
// other submissions of the same URL.
|
||||
type ListDuplicatePostOptions struct {
|
||||
ListOptions
|
||||
// If empty, it'll search for duplicates in all subreddits.
|
||||
Subreddit string `url:"sr,omitempty"`
|
||||
// One of: num_comments, new.
|
||||
Sort string `url:"sort,omitempty"`
|
||||
// If true, the search will only return duplicates that are
|
||||
// crossposts of the original post.
|
||||
CrosspostsOnly bool `url:"crossposts_only,omitempty"`
|
||||
}
|
||||
|
||||
// ListModActionOptions defines possible options used when getting moderation actions in a subreddit.
|
||||
type ListModActionOptions struct {
|
||||
// The max for the limit parameter here is 500.
|
||||
ListOptions
|
||||
// If empty, the search will return all action types.
|
||||
// One of: banuser, unbanuser, spamlink, removelink, approvelink, spamcomment, removecomment,
|
||||
// approvecomment, addmoderator, showcomment, invitemoderator, uninvitemoderator, acceptmoderatorinvite,
|
||||
// removemoderator, addcontributor, removecontributor, editsettings, editflair, distinguish, marknsfw,
|
||||
// wikibanned, wikicontributor, wikiunbanned, wikipagelisted, removewikicontributor, wikirevise,
|
||||
// wikipermlevel, ignorereports, unignorereports, setpermissions, setsuggestedsort, sticky, unsticky,
|
||||
// setcontestmode, unsetcontestmode, lock, unlock, muteuser, unmuteuser, createrule, editrule,
|
||||
// reorderrules, deleterule, spoiler, unspoiler, modmail_enrollment, community_styling, community_widgets,
|
||||
// markoriginalcontent, collections, events, hidden_award, add_community_topics, remove_community_topics,
|
||||
// create_scheduled_post, edit_scheduled_post, delete_scheduled_post, submit_scheduled_post,
|
||||
// edit_post_requirements, invitesubscriber, submit_content_rating_survey.
|
||||
Type string `url:"type,omitempty"`
|
||||
// If provided, only return the actions of this moderator.
|
||||
Moderator string `url:"mod,omitempty"`
|
||||
}
|
||||
|
||||
func addOptions(s string, opt interface{}) (string, error) {
|
||||
v := reflect.ValueOf(opt)
|
||||
if v.Kind() == reflect.Ptr && v.IsNil() {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
origURL, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
origValues := origURL.Query()
|
||||
|
||||
newValues, err := query.Values(opt)
|
||||
if err != nil {
|
||||
return s, err
|
||||
}
|
||||
|
||||
for k, v := range newValues {
|
||||
origValues[k] = v
|
||||
}
|
||||
|
||||
origURL.RawQuery = origValues.Encode()
|
||||
return origURL.String(), nil
|
||||
}
|
||||
|
||||
// String is a helper routine that allocates a new string value
|
||||
// to store v and returns a pointer to it.
|
||||
func String(v string) *string {
|
||||
p := new(string)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Int is a helper routine that allocates a new int value
|
||||
// to store v and returns a pointer to it.
|
||||
func Int(v int) *int {
|
||||
p := new(int)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
// Bool is a helper routine that allocates a new bool value
|
||||
// to store v and returns a pointer to it.
|
||||
func Bool(v bool) *bool {
|
||||
p := new(bool)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
mux *http.ServeMux
|
||||
ctx = context.Background()
|
||||
client *Client
|
||||
server *httptest.Server
|
||||
)
|
||||
|
||||
func setup() {
|
||||
mux = http.NewServeMux()
|
||||
server = httptest.NewServer(mux)
|
||||
|
||||
mux.HandleFunc("/api/v1/access_token", func(w http.ResponseWriter, r *http.Request) {
|
||||
response := `
|
||||
{
|
||||
"access_token": "token1",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 3600,
|
||||
"scope": "*"
|
||||
}
|
||||
`
|
||||
w.Header().Add(headerContentType, mediaTypeJSON)
|
||||
fmt.Fprint(w, response)
|
||||
})
|
||||
|
||||
client, _ = NewClient(nil,
|
||||
WithCredentials("id1", "secret1", "user1", "password1"),
|
||||
WithBaseURL(server.URL),
|
||||
WithTokenURL(server.URL+"/api/v1/access_token"),
|
||||
)
|
||||
}
|
||||
|
||||
func teardown() {
|
||||
server.Close()
|
||||
}
|
||||
|
||||
func readFileContents(path string) (string, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
bytes, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func testClientServices(t *testing.T, c *Client) {
|
||||
services := []string{
|
||||
"Account",
|
||||
"Collection",
|
||||
"Comment",
|
||||
"Emoji",
|
||||
"Flair",
|
||||
"Listings",
|
||||
"Message",
|
||||
"Moderation",
|
||||
"Multi",
|
||||
"Post",
|
||||
"Subreddit",
|
||||
"User",
|
||||
}
|
||||
|
||||
cp := reflect.ValueOf(c)
|
||||
cv := reflect.Indirect(cp)
|
||||
|
||||
for _, s := range services {
|
||||
require.Falsef(t, cv.FieldByName(s).IsNil(), "c.%s should not be nil", s)
|
||||
}
|
||||
}
|
||||
|
||||
func testClientDefaultUserAgent(t *testing.T, c *Client) {
|
||||
expectedUserAgent := fmt.Sprintf("golang:%s:v%s (by /u/)", libraryName, libraryVersion)
|
||||
require.Equal(t, expectedUserAgent, c.userAgent)
|
||||
}
|
||||
|
||||
func testClientDefaults(t *testing.T, c *Client) {
|
||||
testClientDefaultUserAgent(t, c)
|
||||
testClientServices(t, c)
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
c, err := NewClient(nil)
|
||||
require.NoError(t, err)
|
||||
testClientDefaults(t, c)
|
||||
}
|
||||
|
||||
func TestNewClient_Error(t *testing.T) {
|
||||
errorOpt := func(c *Client) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
|
||||
_, err := NewClient(nil, errorOpt)
|
||||
require.EqualError(t, err, "foo")
|
||||
}
|
||||
|
||||
func TestClient_OnRequestComplemented(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
var i int
|
||||
cb := func(*http.Request, *http.Response) {
|
||||
i++
|
||||
}
|
||||
client.OnRequestCompleted(cb)
|
||||
|
||||
mux.HandleFunc("/api/v1/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
})
|
||||
|
||||
req, err := client.NewRequest(http.MethodGet, "api/v1/test", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _ = client.Do(ctx, req, nil)
|
||||
require.Equal(t, 1, i)
|
||||
|
||||
_, _ = client.Do(ctx, req, nil)
|
||||
_, _ = client.Do(ctx, req, nil)
|
||||
_, _ = client.Do(ctx, req, nil)
|
||||
_, _ = client.Do(ctx, req, nil)
|
||||
require.Equal(t, 5, i)
|
||||
|
||||
_, _ = client.Do(ctx, req, nil)
|
||||
require.Equal(t, 6, i)
|
||||
}
|
||||
|
||||
func TestClient_JSONErrorResponse(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, `{
|
||||
"json": {
|
||||
"errors": [
|
||||
[
|
||||
"TEST_ERROR",
|
||||
"this is a test error",
|
||||
"test field"
|
||||
]
|
||||
]
|
||||
}
|
||||
}`)
|
||||
})
|
||||
|
||||
req, err := client.NewRequest(http.MethodGet, "api/v1/test", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(ctx, req, nil)
|
||||
require.IsType(t, &JSONErrorResponse{}, err)
|
||||
require.EqualError(t, err, fmt.Sprintf(`GET %s/api/v1/test: 200 field "test field" caused TEST_ERROR: this is a test error`, server.URL))
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestClient_ErrorResponse(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprint(w, `{
|
||||
"message": "error message"
|
||||
}`)
|
||||
})
|
||||
|
||||
req, err := client.NewRequest(http.MethodGet, "api/v1/test", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
resp, err := client.Do(ctx, req, nil)
|
||||
require.IsType(t, &ErrorResponse{}, err)
|
||||
require.EqualError(t, err, fmt.Sprintf(`GET %s/api/v1/test: 403 error message`, server.URL))
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
}
|
||||
@@ -0,0 +1,679 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SubredditService handles communication with the subreddit
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_subreddits
|
||||
type SubredditService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type rootSubreddit struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *Subreddit `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type rootSubredditNames struct {
|
||||
Names []string `json:"names,omitempty"`
|
||||
}
|
||||
|
||||
// Relationship holds information about a relationship (friend/blocked).
|
||||
// todo: there's also banned, wikibanned, etc.
|
||||
type Relationship struct {
|
||||
ID string `json:"rel_id,omitempty"`
|
||||
User string `json:"name,omitempty"`
|
||||
UserID string `json:"id,omitempty"`
|
||||
Created *Timestamp `json:"date,omitempty"`
|
||||
}
|
||||
|
||||
// Relationships is a listing of relationships.
|
||||
type Relationships struct {
|
||||
Relationships []*Relationship `json:"relationships"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// Moderator is a user who moderates a subreddit.
|
||||
type Moderator struct {
|
||||
*Relationship
|
||||
Permissions []string `json:"mod_permissions"`
|
||||
}
|
||||
|
||||
// Ban represents a banned relationship.
|
||||
type Ban struct {
|
||||
*Relationship
|
||||
// nil means the ban is permanent
|
||||
DaysLeft *int `json:"days_left"`
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
// Bans is a listing of bans.
|
||||
type Bans struct {
|
||||
Bans []*Ban `json:"bans"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
func (s *SubredditService) getPosts(ctx context.Context, sort string, subreddit string, opts *ListPostOptions) (*Posts, *Response, error) {
|
||||
path := sort
|
||||
if subreddit != "" {
|
||||
path = fmt.Sprintf("r/%s/%s", subreddit, 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.getPosts(), resp, nil
|
||||
}
|
||||
|
||||
// HotPosts returns the hottest posts from the specified subreddit.
|
||||
// To search through multiple, separate the names with a plus (+), e.g. "golang+test".
|
||||
// If none are defined, it returns the ones from your subscribed subreddits.
|
||||
// Note: when looking for hot posts in a subreddit, it will include the stickied
|
||||
// posts (if any) PLUS posts from the limit parameter (25 by default).
|
||||
func (s *SubredditService) HotPosts(ctx context.Context, subreddit string, opts *ListPostOptions) (*Posts, *Response, error) {
|
||||
return s.getPosts(ctx, "hot", subreddit, opts)
|
||||
}
|
||||
|
||||
// NewPosts returns the newest posts from the specified subreddit.
|
||||
// To search through multiple, separate the names with a plus (+), e.g. "golang+test".
|
||||
// If none are defined, it returns the ones from your subscribed subreddits.
|
||||
func (s *SubredditService) NewPosts(ctx context.Context, subreddit string, opts *ListPostOptions) (*Posts, *Response, error) {
|
||||
return s.getPosts(ctx, "new", subreddit, opts)
|
||||
}
|
||||
|
||||
// RisingPosts returns the rising posts from the specified subreddit.
|
||||
// To search through multiple, separate the names with a plus (+), e.g. "golang+test".
|
||||
// If none are defined, it returns the ones from your subscribed subreddits.
|
||||
func (s *SubredditService) RisingPosts(ctx context.Context, subreddit string, opts *ListPostOptions) (*Posts, *Response, error) {
|
||||
return s.getPosts(ctx, "rising", subreddit, opts)
|
||||
}
|
||||
|
||||
// ControversialPosts returns the most controversial posts from the specified subreddit.
|
||||
// To search through multiple, separate the names with a plus (+), e.g. "golang+test".
|
||||
// If none are defined, it returns the ones from your subscribed subreddits.
|
||||
func (s *SubredditService) ControversialPosts(ctx context.Context, subreddit string, opts *ListPostOptions) (*Posts, *Response, error) {
|
||||
return s.getPosts(ctx, "controversial", subreddit, opts)
|
||||
}
|
||||
|
||||
// TopPosts returns the top posts from the specified subreddit.
|
||||
// To search through multiple, separate the names with a plus (+), e.g. "golang+test".
|
||||
// If none are defined, it returns the ones from your subscribed subreddits.
|
||||
func (s *SubredditService) TopPosts(ctx context.Context, subreddit string, opts *ListPostOptions) (*Posts, *Response, error) {
|
||||
return s.getPosts(ctx, "top", subreddit, opts)
|
||||
}
|
||||
|
||||
// Get gets a subreddit by name.
|
||||
func (s *SubredditService) Get(ctx context.Context, name string) (*Subreddit, *Response, error) {
|
||||
if name == "" {
|
||||
return nil, nil, errors.New("name: cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/about", name)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootSubreddit)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// Popular returns popular subreddits.
|
||||
func (s *SubredditService) Popular(ctx context.Context, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/popular", opts)
|
||||
}
|
||||
|
||||
// New returns new subreddits.
|
||||
func (s *SubredditService) New(ctx context.Context, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/new", opts)
|
||||
}
|
||||
|
||||
// Gold returns gold subreddits (i.e. only accessible to users with gold).
|
||||
// It seems like it returns an empty list if you don't have gold.
|
||||
func (s *SubredditService) Gold(ctx context.Context, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/gold", opts)
|
||||
}
|
||||
|
||||
// Default returns default subreddits.
|
||||
func (s *SubredditService) Default(ctx context.Context, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/default", opts)
|
||||
}
|
||||
|
||||
// Subscribed returns the list of subreddits you are subscribed to.
|
||||
func (s *SubredditService) Subscribed(ctx context.Context, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/mine/subscriber", opts)
|
||||
}
|
||||
|
||||
// Approved returns the list of subreddits you are an approved user in.
|
||||
func (s *SubredditService) Approved(ctx context.Context, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/mine/contributor", opts)
|
||||
}
|
||||
|
||||
// Moderated returns the list of subreddits you are a moderator of.
|
||||
func (s *SubredditService) Moderated(ctx context.Context, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
return s.getSubreddits(ctx, "subreddits/mine/moderator", opts)
|
||||
}
|
||||
|
||||
// GetSticky1 returns the first stickied post on a subreddit (if it exists).
|
||||
func (s *SubredditService) GetSticky1(ctx context.Context, subreddit string) (*PostAndComments, *Response, error) {
|
||||
return s.getSticky(ctx, subreddit, 1)
|
||||
}
|
||||
|
||||
// GetSticky2 returns the second stickied post on a subreddit (if it exists).
|
||||
func (s *SubredditService) GetSticky2(ctx context.Context, subreddit string) (*PostAndComments, *Response, error) {
|
||||
return s.getSticky(ctx, subreddit, 2)
|
||||
}
|
||||
|
||||
func (s *SubredditService) handleSubscription(ctx context.Context, form url.Values) (*Response, error) {
|
||||
path := "api/subscribe"
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Subscribe subscribes to subreddits based on their names.
|
||||
func (s *SubredditService) Subscribe(ctx context.Context, subreddits ...string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("action", "sub")
|
||||
form.Set("sr_name", strings.Join(subreddits, ","))
|
||||
return s.handleSubscription(ctx, form)
|
||||
}
|
||||
|
||||
// SubscribeByID subscribes to subreddits based on their id.
|
||||
func (s *SubredditService) 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 based on their names.
|
||||
func (s *SubredditService) Unsubscribe(ctx context.Context, subreddits ...string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("action", "unsub")
|
||||
form.Set("sr_name", strings.Join(subreddits, ","))
|
||||
return s.handleSubscription(ctx, form)
|
||||
}
|
||||
|
||||
// UnsubscribeByID unsubscribes from subreddits based on their id.
|
||||
func (s *SubredditService) 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)
|
||||
}
|
||||
|
||||
// Favorite favorites the subreddit.
|
||||
func (s *SubredditService) Favorite(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := "api/favorite"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("sr_name", subreddit)
|
||||
form.Set("make_favorite", "true")
|
||||
form.Set("api_type", "json")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unfavorite unfavorites the subreddit.
|
||||
func (s *SubredditService) Unfavorite(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := "api/favorite"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("sr_name", subreddit)
|
||||
form.Set("make_favorite", "false")
|
||||
form.Set("api_type", "json")
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Search searches for subreddits.
|
||||
func (s *SubredditService) Search(ctx context.Context, query string, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
path := "subreddits/search"
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
type params struct {
|
||||
Query string `url:"q"`
|
||||
}
|
||||
path, err = addOptions(path, params{query})
|
||||
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.getSubreddits(), resp, nil
|
||||
}
|
||||
|
||||
// SearchNames searches for subreddits with names beginning with the query provided.
|
||||
func (s *SubredditService) SearchNames(ctx context.Context, query string) ([]string, *Response, error) {
|
||||
path := fmt.Sprintf("api/search_reddit_names?query=%s", query)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootSubredditNames)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Names, resp, nil
|
||||
}
|
||||
|
||||
// SearchPosts searches for posts in the specified subreddit.
|
||||
// To search through multiple, separate the names with a plus (+), e.g. "golang+test".
|
||||
// If no subreddit is provided, the search is run against r/all.
|
||||
func (s *SubredditService) SearchPosts(ctx context.Context, query string, subreddit string, opts *ListPostSearchOptions) (*Posts, *Response, error) {
|
||||
if subreddit == "" {
|
||||
subreddit = "all"
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/search", subreddit)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
type params struct {
|
||||
Query string `url:"q"`
|
||||
RestrictSubreddits bool `url:"restrict_sr,omitempty"`
|
||||
}
|
||||
|
||||
notAll := !strings.EqualFold(subreddit, "all")
|
||||
path, err = addOptions(path, params{query, notAll})
|
||||
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.getPosts(), resp, nil
|
||||
}
|
||||
|
||||
func (s *SubredditService) getSubreddits(ctx context.Context, path string, opts *ListSubredditOptions) (*Subreddits, *Response, error) {
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.getSubreddits(), resp, nil
|
||||
}
|
||||
|
||||
// getSticky returns one of the 2 stickied posts of the subreddit (if they exist).
|
||||
// Num should be equal to 1 or 2, depending on which one you want.
|
||||
func (s *SubredditService) getSticky(ctx context.Context, subreddit string, num int) (*PostAndComments, *Response, error) {
|
||||
type params struct {
|
||||
Num int `url:"num"`
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/about/sticky", subreddit)
|
||||
path, err := addOptions(path, params{num})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(PostAndComments)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// todo: sr_detail's NSFW indicator is over_18 instead of over18
|
||||
func (s *SubredditService) random(ctx context.Context, nsfw bool) (*Subreddit, *Response, error) {
|
||||
path := "r/random"
|
||||
if nsfw {
|
||||
path = "r/randnsfw"
|
||||
}
|
||||
|
||||
type params struct {
|
||||
ExpandSubreddit bool `url:"sr_detail"`
|
||||
Limit int `url:"limit,omitempty"`
|
||||
}
|
||||
|
||||
path, err := addOptions(path, params{true, 1})
|
||||
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(struct {
|
||||
Data struct {
|
||||
Children []struct {
|
||||
Data struct {
|
||||
Subreddit *Subreddit `json:"sr_detail"`
|
||||
} `json:"data"`
|
||||
} `json:"children"`
|
||||
} `json:"data"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
var sr *Subreddit
|
||||
if len(root.Data.Children) > 0 {
|
||||
sr = root.Data.Children[0].Data.Subreddit
|
||||
}
|
||||
|
||||
return sr, resp, nil
|
||||
}
|
||||
|
||||
// Random returns a random SFW subreddit.
|
||||
func (s *SubredditService) Random(ctx context.Context) (*Subreddit, *Response, error) {
|
||||
return s.random(ctx, false)
|
||||
}
|
||||
|
||||
// RandomNSFW returns a random NSFW subreddit.
|
||||
func (s *SubredditService) RandomNSFW(ctx context.Context) (*Subreddit, *Response, error) {
|
||||
return s.random(ctx, true)
|
||||
}
|
||||
|
||||
// SubmissionText gets the submission text for the subreddit.
|
||||
// This text is set by the subreddit moderators and intended to be displayed on the submission form.
|
||||
func (s *SubredditService) SubmissionText(ctx context.Context, name string) (string, *Response, error) {
|
||||
if name == "" {
|
||||
return "", nil, errors.New("name: cannot be empty")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/submit_text", name)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
Text string `json:"submit_text"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
|
||||
return root.Text, resp, err
|
||||
}
|
||||
|
||||
// Banned gets banned users from the subreddit.
|
||||
func (s *SubredditService) Banned(ctx context.Context, subreddit string, opts *ListOptions) (*Bans, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/banned", subreddit)
|
||||
|
||||
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(struct {
|
||||
Data struct {
|
||||
Bans []*Ban `json:"children"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
} `json:"data"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
bans := &Bans{
|
||||
Bans: root.Data.Bans,
|
||||
After: root.Data.After,
|
||||
Before: root.Data.Before,
|
||||
}
|
||||
|
||||
return bans, resp, nil
|
||||
}
|
||||
|
||||
// Muted gets muted users from the subreddit.
|
||||
func (s *SubredditService) Muted(ctx context.Context, subreddit string, opts *ListOptions) (*Relationships, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/muted", subreddit)
|
||||
|
||||
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(struct {
|
||||
Data struct {
|
||||
Relationships []*Relationship `json:"children"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
} `json:"data"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
relationships := &Relationships{
|
||||
Relationships: root.Data.Relationships,
|
||||
After: root.Data.After,
|
||||
Before: root.Data.Before,
|
||||
}
|
||||
|
||||
return relationships, resp, nil
|
||||
}
|
||||
|
||||
// WikiBanned gets banned users from the subreddit.
|
||||
func (s *SubredditService) WikiBanned(ctx context.Context, subreddit string, opts *ListOptions) (*Bans, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/wikibanned", subreddit)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var root struct {
|
||||
Data struct {
|
||||
Bans []*Ban `json:"children"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
} `json:"data"`
|
||||
}
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
bans := &Bans{
|
||||
Bans: root.Data.Bans,
|
||||
After: root.Data.After,
|
||||
Before: root.Data.Before,
|
||||
}
|
||||
|
||||
return bans, resp, nil
|
||||
}
|
||||
|
||||
// Contributors gets contributors (also known as approved users) from the subreddit.
|
||||
func (s *SubredditService) Contributors(ctx context.Context, subreddit string, opts *ListOptions) (*Relationships, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/contributors", subreddit)
|
||||
|
||||
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(struct {
|
||||
Data struct {
|
||||
Relationships []*Relationship `json:"children"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
} `json:"data"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
relationships := &Relationships{
|
||||
Relationships: root.Data.Relationships,
|
||||
After: root.Data.After,
|
||||
Before: root.Data.Before,
|
||||
}
|
||||
|
||||
return relationships, resp, nil
|
||||
}
|
||||
|
||||
// WikiContributors gets contributors of the wiki from the subreddit.
|
||||
func (s *SubredditService) WikiContributors(ctx context.Context, subreddit string, opts *ListOptions) (*Relationships, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/wikicontributors", subreddit)
|
||||
|
||||
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(struct {
|
||||
Data struct {
|
||||
Relationships []*Relationship `json:"children"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
} `json:"data"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
relationships := &Relationships{
|
||||
Relationships: root.Data.Relationships,
|
||||
After: root.Data.After,
|
||||
Before: root.Data.Before,
|
||||
}
|
||||
|
||||
return relationships, resp, nil
|
||||
}
|
||||
|
||||
// Moderators gets the moderators of the subreddit.
|
||||
func (s *SubredditService) Moderators(ctx context.Context, subreddit string) ([]*Moderator, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/moderators", subreddit)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
Data struct {
|
||||
Moderators []*Moderator `json:"children"`
|
||||
} `json:"data"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data.Moderators, resp, nil
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,428 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
const (
|
||||
kindComment = "t1"
|
||||
kindAccount = "t2"
|
||||
kindPost = "t3"
|
||||
kindMessage = "t4"
|
||||
kindSubreddit = "t5"
|
||||
kindAward = "t6"
|
||||
kindListing = "Listing"
|
||||
kindKarmaList = "KarmaList"
|
||||
kindTrophyList = "TrophyList"
|
||||
kindUserList = "UserList"
|
||||
kindMore = "more"
|
||||
kindModAction = "modaction"
|
||||
)
|
||||
|
||||
// thing is an entity on Reddit.
|
||||
// Its kind reprsents what it is and what is stored in the Data field
|
||||
// e.g. t1 = comment, t2 = user, t3 = post, etc.
|
||||
type thing struct {
|
||||
Kind string `json:"kind"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
|
||||
type rootListing struct {
|
||||
Kind string `json:"kind"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type things struct {
|
||||
Comments []*Comment
|
||||
Mores []*More
|
||||
Users []*User
|
||||
Posts []*Post
|
||||
Subreddits []*Subreddit
|
||||
ModActions []*ModAction
|
||||
}
|
||||
|
||||
// init initializes or clears the listing.
|
||||
func (t *things) init() {
|
||||
t.Comments = make([]*Comment, 0)
|
||||
t.Mores = make([]*More, 0)
|
||||
t.Users = make([]*User, 0)
|
||||
t.Posts = make([]*Post, 0)
|
||||
t.Subreddits = make([]*Subreddit, 0)
|
||||
t.ModActions = make([]*ModAction, 0)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (t *things) UnmarshalJSON(b []byte) error {
|
||||
t.init()
|
||||
|
||||
var things []thing
|
||||
if err := json.Unmarshal(b, &things); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, thing := range things {
|
||||
switch thing.Kind {
|
||||
case kindComment:
|
||||
v := new(Comment)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.Comments = append(t.Comments, v)
|
||||
}
|
||||
case kindMore:
|
||||
v := new(More)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.Mores = append(t.Mores, v)
|
||||
}
|
||||
case kindAccount:
|
||||
v := new(User)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.Users = append(t.Users, v)
|
||||
}
|
||||
case kindPost:
|
||||
v := new(Post)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.Posts = append(t.Posts, v)
|
||||
}
|
||||
case kindSubreddit:
|
||||
v := new(Subreddit)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.Subreddits = append(t.Subreddits, v)
|
||||
}
|
||||
case kindModAction:
|
||||
v := new(ModAction)
|
||||
if err := json.Unmarshal(thing.Data, v); err == nil {
|
||||
t.ModActions = append(t.ModActions, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Comment is a comment posted by a user.
|
||||
type Comment struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
Edited *Timestamp `json:"edited,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"`
|
||||
|
||||
SubredditName string `json:"subreddit,omitempty"`
|
||||
SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"`
|
||||
SubredditID string `json:"subreddit_id,omitempty"`
|
||||
|
||||
// Indicates if you've upvote/downvoted (true/false).
|
||||
// If neither, it will be nil.
|
||||
Likes *bool `json:"likes"`
|
||||
|
||||
Score int `json:"score"`
|
||||
Controversiality int `json:"controversiality"`
|
||||
|
||||
PostID string `json:"link_id,omitempty"`
|
||||
// This doesn't appear consistently.
|
||||
PostTitle string `json:"link_title,omitempty"`
|
||||
// This doesn't appear consistently.
|
||||
PostPermalink string `json:"link_permalink,omitempty"`
|
||||
// This doesn't appear consistently.
|
||||
PostAuthor string `json:"link_author,omitempty"`
|
||||
// This doesn't appear consistently.
|
||||
PostNumComments *int `json:"num_comments,omitempty"`
|
||||
|
||||
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"`
|
||||
|
||||
Replies Replies `json:"replies"`
|
||||
}
|
||||
|
||||
// HasMore determines whether the comment has more replies to load in its reply tree.
|
||||
func (c *Comment) HasMore() bool {
|
||||
return c.Replies.More != nil && len(c.Replies.More.Children) > 0
|
||||
}
|
||||
|
||||
// addCommentToReplies traverses the comment tree to find the one
|
||||
// that the 2nd comment is replying to. It then adds it to its replies.
|
||||
func (c *Comment) addCommentToReplies(comment *Comment) {
|
||||
if c.FullID == comment.ParentID {
|
||||
c.Replies.Comments = append(c.Replies.Comments, comment)
|
||||
return
|
||||
}
|
||||
|
||||
for _, reply := range c.Replies.Comments {
|
||||
reply.addCommentToReplies(comment)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Comment) addMoreToReplies(more *More) {
|
||||
if c.FullID == more.ParentID {
|
||||
c.Replies.More = more
|
||||
return
|
||||
}
|
||||
|
||||
for _, reply := range c.Replies.Comments {
|
||||
reply.addMoreToReplies(more)
|
||||
}
|
||||
}
|
||||
|
||||
// Replies holds replies to a comment.
|
||||
// It contains both comments and "more" comments, which are entrypoints to other
|
||||
// comments that were left out.
|
||||
type Replies struct {
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
More *More `json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (r *Replies) UnmarshalJSON(data []byte) error {
|
||||
// if a comment has no replies, its "replies" field is set to ""
|
||||
if string(data) == `""` {
|
||||
r = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
root := new(rootListing)
|
||||
err := json.Unmarshal(data, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.Comments = root.Data.Things.Comments
|
||||
r.More = root.getFirstMore()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (r *Replies) MarshalJSON() ([]byte, error) {
|
||||
if r == nil || len(r.Comments) == 0 {
|
||||
return []byte(`null`), nil
|
||||
}
|
||||
return json.Marshal(r.Comments)
|
||||
}
|
||||
|
||||
// More holds information used to retrieve additional comments omitted from a base comment tree.
|
||||
type More struct {
|
||||
ID string `json:"id"`
|
||||
FullID string `json:"name"`
|
||||
ParentID string `json:"parent_id"`
|
||||
// Total number of replies to the parent + replies to those replies (recursively).
|
||||
Count int `json:"count"`
|
||||
// Number of comment nodes from the parent down to the furthest comment node.
|
||||
Depth int `json:"depth"`
|
||||
Children []string `json:"children"`
|
||||
}
|
||||
|
||||
// Post is a submitted post on Reddit.
|
||||
type Post 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"`
|
||||
|
||||
// Indicates if you've upvote/downvoted (true/false).
|
||||
// If neither, it will be nil.
|
||||
Likes *bool `json:"likes"`
|
||||
|
||||
Score int `json:"score"`
|
||||
UpvoteRatio float32 `json:"upvote_ratio"`
|
||||
NumberOfComments int `json:"num_comments"`
|
||||
|
||||
SubredditName string `json:"subreddit,omitempty"`
|
||||
SubredditNamePrefixed string `json:"subreddit_name_prefixed,omitempty"`
|
||||
SubredditID string `json:"subreddit_id,omitempty"`
|
||||
|
||||
Author string `json:"author,omitempty"`
|
||||
AuthorID string `json:"author_fullname,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"`
|
||||
Description 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"`
|
||||
Subscribed bool `json:"user_is_subscriber"`
|
||||
Favorite bool `json:"user_has_favorited"`
|
||||
}
|
||||
|
||||
func (l *rootListing) getComments() *Comments {
|
||||
return &Comments{
|
||||
Comments: l.Data.Things.Comments,
|
||||
After: l.Data.After,
|
||||
Before: l.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *rootListing) getFirstMore() *More {
|
||||
if len(l.Data.Things.Mores) == 0 {
|
||||
return nil
|
||||
}
|
||||
return l.Data.Things.Mores[0]
|
||||
}
|
||||
|
||||
func (l *rootListing) getUsers() *Users {
|
||||
return &Users{
|
||||
Users: l.Data.Things.Users,
|
||||
After: l.Data.After,
|
||||
Before: l.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *rootListing) getPosts() *Posts {
|
||||
return &Posts{
|
||||
Posts: l.Data.Things.Posts,
|
||||
After: l.Data.After,
|
||||
Before: l.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *rootListing) getSubreddits() *Subreddits {
|
||||
return &Subreddits{
|
||||
Subreddits: l.Data.Things.Subreddits,
|
||||
After: l.Data.After,
|
||||
Before: l.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *rootListing) getModActions() *ModActions {
|
||||
return &ModActions{
|
||||
ModActions: l.Data.Things.ModActions,
|
||||
After: l.Data.After,
|
||||
Before: l.Data.Before,
|
||||
}
|
||||
}
|
||||
|
||||
// Comments is a list of comments
|
||||
type Comments struct {
|
||||
Comments []*Comment `json:"comments"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// Users is a list of users
|
||||
type Users struct {
|
||||
Users []*User `json:"users"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// Subreddits is a list of subreddits
|
||||
type Subreddits struct {
|
||||
Subreddits []*Subreddit `json:"subreddits"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// Posts is a list of posts.
|
||||
type Posts struct {
|
||||
Posts []*Post `json:"posts"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// ModActions is a list of moderator actions.
|
||||
type ModActions struct {
|
||||
ModActions []*ModAction `json:"moderator_actions"`
|
||||
After string `json:"after"`
|
||||
Before string `json:"before"`
|
||||
}
|
||||
|
||||
// PostAndComments is a post and its comments.
|
||||
type PostAndComments struct {
|
||||
Post *Post `json:"post"`
|
||||
Comments []*Comment `json:"comments"`
|
||||
More *More `json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
// When getting a sticky post, you get an array of 2 Listings
|
||||
// The 1st one contains the single post in its children array
|
||||
// The 2nd one contains the comments to the post
|
||||
func (pc *PostAndComments) UnmarshalJSON(data []byte) error {
|
||||
var l [2]rootListing
|
||||
|
||||
err := json.Unmarshal(data, &l)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
post := l[0].getPosts().Posts[0]
|
||||
comments := l[1].getComments().Comments
|
||||
moreComments := l[1].getFirstMore()
|
||||
|
||||
pc.Post = post
|
||||
pc.Comments = comments
|
||||
pc.More = moreComments
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasMore determines whether the post has more replies to load in its reply tree.
|
||||
func (pc *PostAndComments) HasMore() bool {
|
||||
return pc.More != nil && len(pc.More.Children) > 0
|
||||
}
|
||||
|
||||
func (pc *PostAndComments) addCommentToTree(comment *Comment) {
|
||||
if pc.Post.FullID == comment.ParentID {
|
||||
pc.Comments = append(pc.Comments, comment)
|
||||
return
|
||||
}
|
||||
|
||||
for _, reply := range pc.Comments {
|
||||
reply.addCommentToReplies(comment)
|
||||
}
|
||||
}
|
||||
|
||||
func (pc *PostAndComments) addMoreToTree(more *More) {
|
||||
if pc.Post.FullID == more.ParentID {
|
||||
pc.More = more
|
||||
}
|
||||
|
||||
for _, reply := range pc.Comments {
|
||||
reply.addMoreToReplies(more)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package reddit
|
||||
|
||||
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 posts and comments 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)
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package reddit
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+592
@@ -0,0 +1,592 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// UserService handles communication with the user
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_users
|
||||
type UserService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
type rootUser struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *User `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// User represents a Reddit user.
|
||||
type User struct {
|
||||
// this is not the full ID, watch out.
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
PostKarma int `json:"link_karma"`
|
||||
CommentKarma int `json:"comment_karma"`
|
||||
|
||||
IsFriend bool `json:"is_friend"`
|
||||
IsEmployee bool `json:"is_employee"`
|
||||
HasVerifiedEmail bool `json:"has_verified_email"`
|
||||
NSFW bool `json:"over_18"`
|
||||
IsSuspended bool `json:"is_suspended"`
|
||||
}
|
||||
|
||||
// UserSummary represents a Reddit user, but
|
||||
// contains fewer pieces of information.
|
||||
type UserSummary struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
PostKarma int `json:"link_karma"`
|
||||
CommentKarma int `json:"comment_karma"`
|
||||
|
||||
NSFW bool `json:"profile_over_18"`
|
||||
}
|
||||
|
||||
// Blocked represents a blocked relationship.
|
||||
type Blocked struct {
|
||||
Blocked string `json:"name,omitempty"`
|
||||
BlockedID string `json:"id,omitempty"`
|
||||
Created *Timestamp `json:"date,omitempty"`
|
||||
}
|
||||
|
||||
type rootTrophyListing struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data struct {
|
||||
Trophies []rootTrophy `json:"trophies"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
type rootTrophy struct {
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Data *Trophy `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// Trophy is a Reddit award.
|
||||
type Trophy struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Get returns information about the user.
|
||||
func (s *UserService) Get(ctx context.Context, username string) (*User, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/about", username)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootUser)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// GetMultipleByID returns multiple users from their full IDs.
|
||||
// The response body is a map where the keys are the IDs (if they exist), and the value is the user.
|
||||
func (s *UserService) GetMultipleByID(ctx context.Context, ids ...string) (map[string]*UserSummary, *Response, error) {
|
||||
type params struct {
|
||||
IDs []string `url:"ids,omitempty,comma"`
|
||||
}
|
||||
|
||||
path := "api/user_data_by_account_ids"
|
||||
path, err := addOptions(path, params{ids})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := make(map[string]*UserSummary)
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// UsernameAvailable checks whether a username is available for registration.
|
||||
func (s *UserService) UsernameAvailable(ctx context.Context, username string) (bool, *Response, error) {
|
||||
type params struct {
|
||||
User string `url:"user"`
|
||||
}
|
||||
|
||||
path := "api/username_available"
|
||||
path, err := addOptions(path, params{username})
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
root := new(bool)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return false, resp, err
|
||||
}
|
||||
|
||||
return *root, resp, nil
|
||||
}
|
||||
|
||||
// Overview returns a list of your posts and comments.
|
||||
func (s *UserService) Overview(ctx context.Context, opts *ListUserOverviewOptions) (*Posts, *Comments, *Response, error) {
|
||||
return s.OverviewOf(ctx, s.client.Username, opts)
|
||||
}
|
||||
|
||||
// OverviewOf returns a list of the user's posts and comments.
|
||||
func (s *UserService) OverviewOf(ctx context.Context, username string, opts *ListUserOverviewOptions) (*Posts, *Comments, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/overview", username)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
|
||||
return root.getPosts(), root.getComments(), resp, nil
|
||||
}
|
||||
|
||||
// Posts returns a list of your posts.
|
||||
func (s *UserService) Posts(ctx context.Context, opts *ListUserOverviewOptions) (*Posts, *Response, error) {
|
||||
return s.PostsOf(ctx, s.client.Username, opts)
|
||||
}
|
||||
|
||||
// PostsOf returns a list of the user's posts.
|
||||
func (s *UserService) PostsOf(ctx context.Context, username string, opts *ListUserOverviewOptions) (*Posts, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/submitted", 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.getPosts(), resp, nil
|
||||
}
|
||||
|
||||
// Comments returns a list of your comments.
|
||||
func (s *UserService) Comments(ctx context.Context, opts *ListUserOverviewOptions) (*Comments, *Response, error) {
|
||||
return s.CommentsOf(ctx, s.client.Username, opts)
|
||||
}
|
||||
|
||||
// CommentsOf returns a list of the user's comments.
|
||||
func (s *UserService) CommentsOf(ctx context.Context, username string, opts *ListUserOverviewOptions) (*Comments, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/comments", 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.getComments(), resp, nil
|
||||
}
|
||||
|
||||
// Saved returns a list of the user's saved posts and comments.
|
||||
func (s *UserService) Saved(ctx context.Context, opts *ListUserOverviewOptions) (*Posts, *Comments, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/saved", s.client.Username)
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
|
||||
return root.getPosts(), root.getComments(), resp, nil
|
||||
}
|
||||
|
||||
// Upvoted returns a list of your upvoted posts.
|
||||
func (s *UserService) Upvoted(ctx context.Context, opts *ListUserOverviewOptions) (*Posts, *Response, error) {
|
||||
return s.UpvotedOf(ctx, s.client.Username, opts)
|
||||
}
|
||||
|
||||
// UpvotedOf returns a list of the user's upvoted posts.
|
||||
// The user's votes must be public for this to work (unless the user is you).
|
||||
func (s *UserService) UpvotedOf(ctx context.Context, username string, opts *ListUserOverviewOptions) (*Posts, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/upvoted", 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.getPosts(), resp, nil
|
||||
}
|
||||
|
||||
// Downvoted returns a list of your downvoted posts.
|
||||
func (s *UserService) Downvoted(ctx context.Context, opts *ListUserOverviewOptions) (*Posts, *Response, error) {
|
||||
return s.DownvotedOf(ctx, s.client.Username, opts)
|
||||
}
|
||||
|
||||
// DownvotedOf returns a list of the user's downvoted posts.
|
||||
// The user's votes must be public for this to work (unless the user is you).
|
||||
func (s *UserService) DownvotedOf(ctx context.Context, username string, opts *ListUserOverviewOptions) (*Posts, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/downvoted", 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.getPosts(), resp, nil
|
||||
}
|
||||
|
||||
// Hidden returns a list of the user's hidden posts.
|
||||
func (s *UserService) Hidden(ctx context.Context, opts *ListUserOverviewOptions) (*Posts, *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.getPosts(), resp, nil
|
||||
}
|
||||
|
||||
// Gilded returns a list of the user's gilded posts.
|
||||
func (s *UserService) Gilded(ctx context.Context, opts *ListUserOverviewOptions) (*Posts, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/gilded", 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.getPosts(), resp, nil
|
||||
}
|
||||
|
||||
// GetFriendship returns relationship details with the specified user.
|
||||
// If the user is not your friend, it will return an error.
|
||||
func (s *UserService) GetFriendship(ctx context.Context, username string) (*Relationship, *Response, error) {
|
||||
path := fmt.Sprintf("api/v1/me/friends/%s", username)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Relationship)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Friend friends a user.
|
||||
func (s *UserService) Friend(ctx context.Context, username string) (*Relationship, *Response, error) {
|
||||
type request struct {
|
||||
Username string `json:"name"`
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("api/v1/me/friends/%s", username)
|
||||
body := request{username}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPut, path, body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Relationship)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Unfriend unfriends a user.
|
||||
func (s *UserService) 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)
|
||||
}
|
||||
|
||||
// Block blocks a user.
|
||||
func (s *UserService) Block(ctx context.Context, username string) (*Blocked, *Response, error) {
|
||||
path := "api/block_user"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", username)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Blocked)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// BlockByID blocks a user via their full id.
|
||||
func (s *UserService) BlockByID(ctx context.Context, id string) (*Blocked, *Response, error) {
|
||||
path := "api/block_user"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("account_id", id)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(Blocked)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Unblock unblocks a user.
|
||||
func (s *UserService) Unblock(ctx context.Context, username string) (*Response, error) {
|
||||
selfID, resp, err := s.client.id(ctx)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
path := "api/unfriend"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", username)
|
||||
form.Set("type", "enemy")
|
||||
form.Set("container", selfID)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UnblockByID unblocks a user via their full id.
|
||||
func (s *UserService) UnblockByID(ctx context.Context, id string) (*Response, error) {
|
||||
selfID, resp, err := s.client.id(ctx)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
path := "api/unfriend"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
form.Set("type", "enemy")
|
||||
form.Set("container", selfID)
|
||||
|
||||
req, err := s.client.NewRequestWithForm(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Trophies returns a list of your trophies.
|
||||
func (s *UserService) Trophies(ctx context.Context) ([]Trophy, *Response, error) {
|
||||
return s.TrophiesOf(ctx, s.client.Username)
|
||||
}
|
||||
|
||||
// TrophiesOf returns a list of the specified user's trophies.
|
||||
func (s *UserService) TrophiesOf(ctx context.Context, username string) ([]Trophy, *Response, error) {
|
||||
path := fmt.Sprintf("api/v1/user/%s/trophies", username)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootTrophyListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
var trophies []Trophy
|
||||
for _, trophy := range root.Data.Trophies {
|
||||
if trophy.Data != nil {
|
||||
trophies = append(trophies, *trophy.Data)
|
||||
}
|
||||
}
|
||||
|
||||
return trophies, resp, nil
|
||||
}
|
||||
|
||||
// Popular gets the user subreddits with the most activity.
|
||||
func (s *UserService) Popular(ctx context.Context, opts *ListOptions) (*Subreddits, *Response, error) {
|
||||
path := "users/popular"
|
||||
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.getSubreddits(), resp, nil
|
||||
}
|
||||
|
||||
// New gets the most recently created user subreddits.
|
||||
func (s *UserService) New(ctx context.Context, opts *ListUserOverviewOptions) (*Subreddits, *Response, error) {
|
||||
path := "users/new"
|
||||
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.getSubreddits(), resp, nil
|
||||
}
|
||||
|
||||
// Search searches for users.
|
||||
// todo: maybe include the sort option? (relevance, activity)
|
||||
func (s *UserService) Search(ctx context.Context, query string, opts *ListOptions) (*Users, *Response, error) {
|
||||
path := "users/search"
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
type params struct {
|
||||
Query string `url:"q"`
|
||||
}
|
||||
path, err = addOptions(path, params{query})
|
||||
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.getUsers(), resp, nil
|
||||
}
|
||||
@@ -0,0 +1,978 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedUser = &User{
|
||||
ID: "test",
|
||||
Name: "Test_User",
|
||||
Created: &Timestamp{time.Date(2012, 10, 18, 10, 11, 11, 0, time.UTC)},
|
||||
|
||||
PostKarma: 8239,
|
||||
CommentKarma: 130514,
|
||||
|
||||
HasVerifiedEmail: true,
|
||||
}
|
||||
|
||||
var expectedUsers = map[string]*UserSummary{
|
||||
"t2_1": {
|
||||
Name: "test_user_1",
|
||||
Created: &Timestamp{time.Date(2017, 3, 12, 2, 1, 47, 0, time.UTC)},
|
||||
PostKarma: 488,
|
||||
CommentKarma: 22223,
|
||||
NSFW: false,
|
||||
},
|
||||
"t2_2": {
|
||||
Name: "test_user_2",
|
||||
Created: &Timestamp{time.Date(2015, 12, 20, 18, 12, 51, 0, time.UTC)},
|
||||
PostKarma: 8277,
|
||||
CommentKarma: 131948,
|
||||
NSFW: false,
|
||||
},
|
||||
"t2_3": {
|
||||
Name: "test_user_3",
|
||||
Created: &Timestamp{time.Date(2013, 3, 4, 15, 46, 31, 0, time.UTC)},
|
||||
PostKarma: 126887,
|
||||
CommentKarma: 81918,
|
||||
NSFW: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedPost = &Post{
|
||||
ID: "gczwql",
|
||||
FullID: "t3_gczwql",
|
||||
Created: &Timestamp{time.Date(2020, 5, 3, 22, 46, 25, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
Permalink: "/r/redditdev/comments/gczwql/get_userusernamegilded_does_it_return_other_users/",
|
||||
URL: "https://www.reddit.com/r/redditdev/comments/gczwql/get_userusernamegilded_does_it_return_other_users/",
|
||||
|
||||
Title: "GET /user/{username}/gilded: does it return other user's things you've gilded, or your things that have been gilded? Does it return both comments and posts?",
|
||||
Body: "Talking about [this](https://www.reddit.com/dev/api/#GET_user_{username}_{where}) endpoint specifically.\n\nI'm building a Reddit API client, but don't have gold.",
|
||||
|
||||
Likes: Bool(true),
|
||||
|
||||
Score: 9,
|
||||
UpvoteRatio: 0.86,
|
||||
NumberOfComments: 2,
|
||||
|
||||
SubredditName: "redditdev",
|
||||
SubredditNamePrefixed: "r/redditdev",
|
||||
SubredditID: "t5_2qizd",
|
||||
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
|
||||
IsSelfPost: true,
|
||||
}
|
||||
|
||||
var expectedComment = &Comment{
|
||||
ID: "f0zsa37",
|
||||
FullID: "t1_f0zsa37",
|
||||
Created: &Timestamp{time.Date(2019, 9, 21, 21, 38, 16, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
ParentID: "t3_d7ejpn",
|
||||
Permalink: "/r/apple/comments/d7ejpn/im_giving_away_an_iphone_11_pro_to_a_commenter_at/f0zsa37/",
|
||||
|
||||
Body: "Thank you!",
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
|
||||
SubredditName: "apple",
|
||||
SubredditNamePrefixed: "r/apple",
|
||||
SubredditID: "t5_2qh1f",
|
||||
|
||||
Likes: Bool(true),
|
||||
|
||||
Score: 1,
|
||||
Controversiality: 0,
|
||||
|
||||
PostID: "t3_d7ejpn",
|
||||
PostTitle: "I'm giving away an iPhone 11 Pro to a commenter at random to celebrate Apollo for Reddit's new iOS 13 update and as a thank you to the community! Just leave a comment on this post and the winner will be selected randomly and announced tomorrow at 8 PM GMT. Details inside, and good luck!",
|
||||
PostPermalink: "https://www.reddit.com/r/apple/comments/d7ejpn/im_giving_away_an_iphone_11_pro_to_a_commenter_at/",
|
||||
PostAuthor: "iamthatis",
|
||||
PostNumComments: Int(89751),
|
||||
}
|
||||
|
||||
var expectedRelationship = &Relationship{
|
||||
ID: "r9_tqfqp8",
|
||||
User: "test123",
|
||||
UserID: "t2_7b8q1eob",
|
||||
Created: &Timestamp{time.Date(2020, 6, 18, 20, 36, 34, 0, time.UTC)},
|
||||
}
|
||||
|
||||
var expectedBlocked = &Blocked{
|
||||
Blocked: "test123",
|
||||
BlockedID: "t2_3v9o1yoi",
|
||||
Created: &Timestamp{time.Date(2020, 6, 16, 16, 49, 50, 0, time.UTC)},
|
||||
}
|
||||
|
||||
var expectedTrophies = []Trophy{
|
||||
{
|
||||
ID: "",
|
||||
Name: "Three-Year Club",
|
||||
Description: "",
|
||||
},
|
||||
{
|
||||
ID: "1q1tez",
|
||||
Name: "Verified Email",
|
||||
Description: "",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedUserSubreddits = &Subreddits{
|
||||
Subreddits: []*Subreddit{
|
||||
{
|
||||
ID: "3kefx",
|
||||
FullID: "t5_3kefx",
|
||||
Created: &Timestamp{time.Date(2017, 5, 11, 16, 37, 16, 0, time.UTC)},
|
||||
|
||||
URL: "/user/nickofnight/",
|
||||
Name: "u_nickofnight",
|
||||
NamePrefixed: "u/nickofnight",
|
||||
Title: "nickofnight",
|
||||
Description: "Stories written for Writing Prompts, NoSleep, and originals. Current series: The Carnival of Night ",
|
||||
Type: "user",
|
||||
},
|
||||
{
|
||||
ID: "3knn1",
|
||||
FullID: "t5_3knn1",
|
||||
Created: &Timestamp{time.Date(2017, 5, 18, 2, 15, 55, 0, time.UTC)},
|
||||
|
||||
URL: "/user/shittymorph/",
|
||||
Name: "u_shittymorph",
|
||||
NamePrefixed: "u/shittymorph",
|
||||
Title: "shittymorph",
|
||||
Description: "In nineteen ninety eight the undertaker threw mankind off hеll in a cell, and plummeted sixteen feet through an announcer's table.",
|
||||
Type: "user",
|
||||
SuggestedCommentSort: "qa",
|
||||
},
|
||||
},
|
||||
After: "t5_3knn1",
|
||||
}
|
||||
|
||||
var expectedSearchUsers = &Users{
|
||||
Users: []*User{
|
||||
{
|
||||
ID: "179965",
|
||||
Name: "washingtonpost",
|
||||
Created: &Timestamp{time.Date(2017, 4, 20, 21, 23, 58, 0, time.UTC)},
|
||||
|
||||
PostKarma: 1075227,
|
||||
CommentKarma: 339569,
|
||||
|
||||
HasVerifiedEmail: true,
|
||||
},
|
||||
{
|
||||
ID: "11kowl2w",
|
||||
Name: "reuters",
|
||||
Created: &Timestamp{time.Date(2018, 3, 15, 1, 50, 4, 0, time.UTC)},
|
||||
|
||||
PostKarma: 76744,
|
||||
CommentKarma: 42717,
|
||||
|
||||
HasVerifiedEmail: true,
|
||||
},
|
||||
},
|
||||
After: "t2_11kowl2w",
|
||||
}
|
||||
|
||||
func TestUserService_Get(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/get.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/Test_User/about", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
user, _, err := client.User.Get(ctx, "Test_User")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUser, user)
|
||||
}
|
||||
|
||||
func TestUserService_GetMultipleByID(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/get-multiple-by-id.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/user_data_by_account_ids", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "t2_1,t2_2,t2_3", r.Form.Get("ids"))
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
users, _, err := client.User.GetMultipleByID(ctx, "t2_1", "t2_2", "t2_3")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUsers, users)
|
||||
}
|
||||
|
||||
func TestUserService_UsernameAvailable(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/username_available", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
|
||||
user := r.Form.Get("user")
|
||||
require.NotEmpty(t, user)
|
||||
|
||||
result := user == "test123"
|
||||
fmt.Fprint(w, result)
|
||||
})
|
||||
|
||||
ok, _, err := client.User.UsernameAvailable(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
ok, _, err = client.User.UsernameAvailable(ctx, "123test")
|
||||
require.NoError(t, err)
|
||||
require.False(t, ok)
|
||||
}
|
||||
|
||||
func TestUserService_Overview(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/overview", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, _, err := client.User.Overview(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
|
||||
require.Len(t, comments.Comments, 1)
|
||||
require.Equal(t, expectedComment, comments.Comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", comments.After)
|
||||
require.Equal(t, "", comments.Before)
|
||||
}
|
||||
|
||||
func TestUserService_OverviewOf(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user2/overview", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, _, err := client.User.OverviewOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
|
||||
require.Len(t, comments.Comments, 1)
|
||||
require.Equal(t, expectedComment, comments.Comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", comments.After)
|
||||
require.Equal(t, "", comments.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Overview_Options(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/overview", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("limit", "5")
|
||||
form.Set("after", "t3_after")
|
||||
form.Set("sort", "top")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, _, err = client.User.Overview(ctx, &ListUserOverviewOptions{
|
||||
ListOptions: ListOptions{
|
||||
Limit: 5,
|
||||
After: "t3_after",
|
||||
},
|
||||
Sort: "top",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_Posts(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/submitted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.Posts(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_PostsOf(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user2/submitted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.PostsOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Posts_Options(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/submitted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("limit", "10")
|
||||
form.Set("sort", "new")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.User.Posts(ctx, &ListUserOverviewOptions{
|
||||
ListOptions: ListOptions{
|
||||
Limit: 10,
|
||||
},
|
||||
Sort: "new",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_Comments(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/comments.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/comments", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
comments, _, err := client.User.Comments(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, comments.Comments, 1)
|
||||
require.Equal(t, expectedComment, comments.Comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", comments.After)
|
||||
require.Equal(t, "", comments.Before)
|
||||
}
|
||||
|
||||
func TestUserService_CommentsOf(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/comments.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user2/comments", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
comments, _, err := client.User.CommentsOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, comments.Comments, 1)
|
||||
require.Equal(t, expectedComment, comments.Comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", comments.After)
|
||||
require.Equal(t, "", comments.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Comments_Options(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/comments.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/comments", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("limit", "100")
|
||||
form.Set("before", "t1_before")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.User.Comments(ctx, &ListUserOverviewOptions{
|
||||
ListOptions: ListOptions{
|
||||
Limit: 100,
|
||||
Before: "t1_before",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_Saved(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/saved", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, _, err := client.User.Saved(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
|
||||
require.Len(t, comments.Comments, 1)
|
||||
require.Equal(t, expectedComment, comments.Comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", comments.After)
|
||||
require.Equal(t, "", comments.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Saved_Options(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/saved", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("limit", "50")
|
||||
form.Set("sort", "controversial")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, _, err = client.User.Saved(ctx, &ListUserOverviewOptions{
|
||||
ListOptions: ListOptions{
|
||||
Limit: 50,
|
||||
},
|
||||
Sort: "controversial",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
func TestUserService_Upvoted(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/upvoted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.Upvoted(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Upvoted_Options(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/upvoted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("limit", "30")
|
||||
form.Set("after", "t3_after")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.User.Upvoted(ctx, &ListUserOverviewOptions{
|
||||
ListOptions: ListOptions{
|
||||
Limit: 30,
|
||||
After: "t3_after",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_UpvotedOf(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user2/upvoted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.UpvotedOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Downvoted(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/downvoted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.Downvoted(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Downvoted_Options(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/downvoted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("limit", "20")
|
||||
form.Set("before", "t3_before")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.User.Downvoted(ctx, &ListUserOverviewOptions{
|
||||
ListOptions: ListOptions{
|
||||
Limit: 20,
|
||||
Before: "t3_before",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_DownvotedOf(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user2/downvoted", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.DownvotedOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Hidden(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/hidden", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.Hidden(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_Gilded(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
// we'll use this, similar payloads
|
||||
blob, err := readFileContents("../testdata/user/submitted.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/user/user1/gilded", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, _, err := client.User.Gilded(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts.Posts, 1)
|
||||
require.Equal(t, expectedPost, posts.Posts[0])
|
||||
require.Equal(t, "t3_gczwql", posts.After)
|
||||
require.Equal(t, "", posts.Before)
|
||||
}
|
||||
|
||||
func TestUserService_GetFriendship(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/friend.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/me/friends/test123", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
relationship, _, err := client.User.GetFriendship(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRelationship, relationship)
|
||||
}
|
||||
|
||||
func TestUserService_Friend(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/friend.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/me/friends/test123", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPut, r.Method)
|
||||
|
||||
type request struct {
|
||||
Username string `json:"name"`
|
||||
}
|
||||
|
||||
var req request
|
||||
err := json.NewDecoder(r.Body).Decode(&req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test123", req.Username)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
relationship, _, err := client.User.Friend(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedRelationship, relationship)
|
||||
}
|
||||
|
||||
func TestUserService_Unfriend(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
mux.HandleFunc("/api/v1/me/friends/test123", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodDelete, r.Method)
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
res, err := client.User.Unfriend(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, res.StatusCode)
|
||||
}
|
||||
|
||||
func TestUserService_Block(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/block.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/block_user", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "test123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
blocked, _, err := client.User.Block(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBlocked, blocked)
|
||||
}
|
||||
|
||||
func TestUserService_BlockByID(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/block.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/block_user", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("account_id", "abc123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
blocked, _, err := client.User.BlockByID(ctx, "abc123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedBlocked, blocked)
|
||||
}
|
||||
|
||||
func TestUserService_Unblock(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
client.redditID = "self123"
|
||||
|
||||
mux.HandleFunc("/api/unfriend", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "test123")
|
||||
form.Set("type", "enemy")
|
||||
form.Set("container", client.redditID)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.User.Unblock(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_UnblockByID(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
client.redditID = "self123"
|
||||
|
||||
mux.HandleFunc("/api/unfriend", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", "abc123")
|
||||
form.Set("type", "enemy")
|
||||
form.Set("container", client.redditID)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
})
|
||||
|
||||
_, err := client.User.UnblockByID(ctx, "abc123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_Trophies(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/trophies.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/user/user1/trophies", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
trophies, _, err := client.User.Trophies(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedTrophies, trophies)
|
||||
}
|
||||
|
||||
func TestUserService_TrophiesOf(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/trophies.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/v1/user/test123/trophies", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
trophies, _, err := client.User.TrophiesOf(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedTrophies, trophies)
|
||||
}
|
||||
|
||||
func TestUserService_Popular(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/user-subreddits.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/users/popular", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
userSubreddits, _, err := client.User.Popular(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUserSubreddits, userSubreddits)
|
||||
}
|
||||
|
||||
func TestUserService_New(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/user-subreddits.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/users/new", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
userSubreddits, _, err := client.User.New(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUserSubreddits, userSubreddits)
|
||||
}
|
||||
|
||||
func TestUserService_Search(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
blob, err := readFileContents("../testdata/user/list.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/users/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("q", "test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
users, _, err := client.User.Search(ctx, "test", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedSearchUsers, users)
|
||||
}
|
||||
Reference in New Issue
Block a user