added v2 package
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
FROM golang:1.26-alpine
|
||||
|
||||
RUN apk add git
|
||||
|
||||
WORKDIR /project
|
||||
RUN go mod init project
|
||||
@@ -0,0 +1,34 @@
|
||||
module git.the-red-comet.org/guy176251/go-reddit/v2
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0
|
||||
github.com/enetx/surf v1.0.173
|
||||
github.com/google/go-querystring v1.0.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
golang.org/x/net v0.49.0
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/enetx/g v1.0.202 // indirect
|
||||
github.com/enetx/http v1.0.24 // indirect
|
||||
github.com/enetx/http2 v1.0.24 // indirect
|
||||
github.com/enetx/http3 v1.0.6 // indirect
|
||||
github.com/enetx/iter v0.0.0-20250912135656-f1583323588f // indirect
|
||||
github.com/enetx/utls v0.0.0-20260115181616-c525a7d559c8 // indirect
|
||||
github.com/golang/protobuf v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.3 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/wzshiming/socks5 v0.7.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
google.golang.org/appengine v1.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,69 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/enetx/g v1.0.202 h1:E32/PWp542wHh7c5VYPGohK0Nu6CpiRm34vr0P1F6dU=
|
||||
github.com/enetx/g v1.0.202/go.mod h1:6/HQeRy+tIJVGY+oRPQVJ/vOSruAi0aldFggurT6jBY=
|
||||
github.com/enetx/http v1.0.24 h1:sMeJ0rN00HlUYLIhUTHx36KumAlkaZu7vsQe5+GMTNM=
|
||||
github.com/enetx/http v1.0.24/go.mod h1:1f4mytfF/SfjATEJnynpwGS6aa1ALjb8DtmYgFVblY0=
|
||||
github.com/enetx/http2 v1.0.24 h1:MpNGZxD8wz2zg2bR4RSyB/4YJukZ91wIsQE2GLwQjCI=
|
||||
github.com/enetx/http2 v1.0.24/go.mod h1:t54ex5HIS8V1+2j6cvEOv6umlrHsbUPFKQ54nYB58Nk=
|
||||
github.com/enetx/http3 v1.0.6 h1:zOmCIOHqImGz0U3QTjvxyWIcPq7d4iTQ3sKZYejax8g=
|
||||
github.com/enetx/http3 v1.0.6/go.mod h1:sqpVGZ9F1/wCiW6sjBUS2errKAh3SUYn6VlWE7LL6KM=
|
||||
github.com/enetx/iter v0.0.0-20250912135656-f1583323588f h1:GUW+4AWfECIEJ9oAxgEAVGCpaozMCjRiUYnuR6Q0bCQ=
|
||||
github.com/enetx/iter v0.0.0-20250912135656-f1583323588f/go.mod h1:oMZN8hGLUpi7QBlMEUqailocNy0NFAO/7Lu+Nwh9HMM=
|
||||
github.com/enetx/surf v1.0.173 h1:36U3d5bGzncQJE8iqNAahIt6SlUYBYPJ90uigf7hqCw=
|
||||
github.com/enetx/surf v1.0.173/go.mod h1:7kQlsgHH2OT69AB/9w9/9Nppx3wijRFOAPnH4IhwMBM=
|
||||
github.com/enetx/utls v0.0.0-20260115181616-c525a7d559c8 h1:jN2LdG4CG7cXOMAkQVDencj67Gdt7FFwDP1UaPcYyV8=
|
||||
github.com/enetx/utls v0.0.0-20260115181616-c525a7d559c8/go.mod h1:jsHaW4RX6DteSbAHT/pW7iJxv7YXLf2NTr3k9PsmXoc=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
|
||||
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
|
||||
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/wzshiming/socks5 v0.7.0 h1:euJ+U48WrvVngi+opC8vAnpZ5sK12y1C2hPvb1f48Rg=
|
||||
github.com/wzshiming/socks5 v0.7.0/go.mod h1:BvCAqlzocQN5xwLjBZDBbvWlrx8sCYSSbHEOf2wZgT0=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,400 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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"
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
karma, _ := t.Karma()
|
||||
return karma, 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.NewJSONRequest(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"
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
trophies, _ := t.TrophyList()
|
||||
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
|
||||
}
|
||||
|
||||
var root [2]rootRelationshipList
|
||||
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
|
||||
}
|
||||
|
||||
var root [2]rootRelationshipList
|
||||
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)
|
||||
|
||||
req, err := s.client.NewRequest(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)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Account.AddTrusted(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestAccountService_RemoveTrusted(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Account.RemoveTrusted(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
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"
|
||||
|
||||
params := struct {
|
||||
ID string `url:"collection_id"`
|
||||
IncludePosts bool `url:"include_links"`
|
||||
}{id, false}
|
||||
|
||||
path, err := addOptions(path, params)
|
||||
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"
|
||||
|
||||
params := struct {
|
||||
SubredditID string `url:"sr_fullname"`
|
||||
}{id}
|
||||
|
||||
path, err := addOptions(path, params)
|
||||
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 a collection.
|
||||
func (s *CollectionService) Create(ctx context.Context, createRequest *CollectionCreateRequest) (*Collection, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, errors.New("*CollectionCreateRequest: 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.NewRequest(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 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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Follow 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unfollow 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Collection.Create(ctx, nil)
|
||||
require.EqualError(t, err, "*CollectionCreateRequest: 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.Delete(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_AddPost(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.AddPost(ctx, "t3_hs03f3", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_RemovePost(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.RemovePost(ctx, "t3_hs03f3", "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_ReorderPosts(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateTitle(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7", "Test Title")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_UpdateDescription(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateDescription(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7", "Test Description")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_UpdateLayoutTimeline(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateLayoutTimeline(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_UpdateLayoutGallery(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.UpdateLayoutGallery(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_Follow(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Collection.Follow(ctx, "37f1e52d-7ec9-466b-b4cc-59e86e071ed7")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestCollectionService_Unfollow(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, 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 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.NewRequest(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 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.NewRequest(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.NewRequest(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,355 @@
|
||||
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)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
PostID: "t3_link1",
|
||||
}
|
||||
|
||||
func TestCommentService_Submit(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.Delete(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Save(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.Save(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Unsave(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.Unsave(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_EnableReplies(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.EnableReplies(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_DisableReplies(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.DisableReplies(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Lock(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.Lock(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Unlock(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.Unlock(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Upvote(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.Upvote(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_Downvote(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.Downvote(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_RemoveVote(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.Comment.RemoveVote(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestCommentService_LoadMoreReplies(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestCommentService_Report(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/report", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("thing_id", "t1_test")
|
||||
form.Set("reason", "test reason")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Comment.Report(ctx, "t1_test", "test reason")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"compress/zlib"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
)
|
||||
|
||||
func ResponseReader(resp *http.Response) (io.ReadCloser, error) {
|
||||
encoding := resp.Header.Get("Content-Encoding")
|
||||
switch strings.ToLower(encoding) {
|
||||
case "deflate":
|
||||
return zlib.NewReader(resp.Body)
|
||||
case "gzip":
|
||||
return gzip.NewReader(resp.Body)
|
||||
case "br":
|
||||
return io.NopCloser(brotli.NewReader(resp.Body)), nil
|
||||
case "zstd":
|
||||
return zlib.NewReader(resp.Body)
|
||||
}
|
||||
return resp.Body, nil
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"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 == nil {
|
||||
return errors.New("*EmojiCreateOrUpdateRequest: cannot be nil")
|
||||
}
|
||||
if r.Name == "" {
|
||||
return errors.New("(*EmojiCreateOrUpdateRequest).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
|
||||
}
|
||||
|
||||
*e = make(emojis, 0, len(emojiMap))
|
||||
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 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 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", strconv.Itoa(height))
|
||||
form.Set("width", strconv.Itoa(width))
|
||||
|
||||
req, err := s.client.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Upload an emoji to the subreddit.
|
||||
func (s *EmojiService) Upload(ctx context.Context, subreddit string, createRequest *EmojiCreateOrUpdateRequest, imagePath string) (*Response, error) {
|
||||
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 newResponse(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) {
|
||||
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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Emoji.SetSize(ctx, "testsubreddit", 20, 20)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmojiService_DisableCustomSize(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Emoji.DisableCustomSize(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestEmojiService_Upload(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
uploadURL := client.BaseURL.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.PostForm)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err = client.Emoji.Upload(ctx, "testsubreddit", nil, emojiFile.Name())
|
||||
require.EqualError(t, err, "*EmojiCreateOrUpdateRequest: cannot be nil")
|
||||
|
||||
_, err = client.Emoji.Upload(ctx, "testsubreddit", &EmojiCreateOrUpdateRequest{Name: ""}, emojiFile.Name())
|
||||
require.EqualError(t, err, "(*EmojiCreateOrUpdateRequest).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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Emoji.Update(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "*EmojiCreateOrUpdateRequest: cannot be nil")
|
||||
|
||||
_, err = client.Emoji.Update(ctx, "testsubreddit", &EmojiCreateOrUpdateRequest{Name: ""})
|
||||
require.EqualError(t, err, "(*EmojiCreateOrUpdateRequest).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,105 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
errorMessages := make([]string, len(r.JSON.Errors))
|
||||
for i, err := range r.JSON.Errors {
|
||||
errorMessages[i] = err.Error()
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"%s %s: %d %s",
|
||||
r.Response.Request.Method, r.Response.Request.URL, r.Response.StatusCode, strings.Join(errorMessages, ";"),
|
||||
)
|
||||
}
|
||||
|
||||
// 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,
|
||||
)
|
||||
}
|
||||
|
||||
// RateLimitError occurs when the client is sending too many requests to Reddit in a given time frame.
|
||||
type RateLimitError struct {
|
||||
// Rate specifies the last known rate limit for the client
|
||||
Rate Rate
|
||||
// HTTP response that caused this error
|
||||
Response *http.Response
|
||||
// Error message
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *RateLimitError) Error() string {
|
||||
return fmt.Sprintf(
|
||||
"%s %s: %d %s %s",
|
||||
e.Response.Request.Method, e.Response.Request.URL, e.Response.StatusCode, e.Message, e.formateRateReset(),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *RateLimitError) formateRateReset() string {
|
||||
d := time.Until(e.Rate.Reset).Round(time.Second)
|
||||
|
||||
isNegative := d < 0
|
||||
if isNegative {
|
||||
d *= -1
|
||||
}
|
||||
|
||||
if isNegative {
|
||||
return fmt.Sprintf("[rate limit was reset %s ago]", d)
|
||||
}
|
||||
return fmt.Sprintf("[rate limit will reset in %s]", d)
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// FlairChoice is a choice of flair when selecting one for yourself or for a post.
|
||||
type FlairChoice struct {
|
||||
TemplateID string `json:"flair_template_id"`
|
||||
Text string `json:"flair_text"`
|
||||
Editable bool `json:"flair_text_editable"`
|
||||
Position string `json:"flair_position"`
|
||||
CSSClass string `json:"flair_css_class"`
|
||||
}
|
||||
|
||||
// FlairConfigureRequest represents a request to configure a subreddit's flair settings.
|
||||
// Not setting an attribute can have unexpected side effects, so assign every one just in case.
|
||||
type FlairConfigureRequest struct {
|
||||
// Enable user flair in the subreddit.
|
||||
UserFlairEnabled *bool `url:"flair_enabled,omitempty"`
|
||||
// One of: left, right.
|
||||
UserFlairPosition string `url:"flair_position,omitempty"`
|
||||
// Allow users to assign their own flair.
|
||||
UserFlairSelfAssignEnabled *bool `url:"flair_self_assign_enabled,omitempty"`
|
||||
|
||||
// One of: none, left, right.
|
||||
PostFlairPosition string `url:"link_flair_position,omitempty"`
|
||||
// Allow submitters to assign their own post flair.
|
||||
PostFlairSelfAssignEnabled *bool `url:"link_flair_self_assign_enabled,omitempty"`
|
||||
}
|
||||
|
||||
// FlairTemplateCreateOrUpdateRequest represents a request to create/update a flair template.
|
||||
// Not setting an attribute can have unexpected side effects, so assign every one just in case.
|
||||
type FlairTemplateCreateOrUpdateRequest struct {
|
||||
// The id of the template. Only provide this if it's an update request.
|
||||
// If provided and it's not a valid id, the template will be created.
|
||||
ID string `url:"flair_template_id,omitempty"`
|
||||
|
||||
// One of: all, emoji, text.
|
||||
AllowableContent string `url:"allowable_content,omitempty"`
|
||||
// No longer than 64 characters.
|
||||
Text string `url:"text,omitempty"`
|
||||
// One of: light, dark.
|
||||
TextColor string `url:"text_color,omitempty"`
|
||||
// Allow user to edit the text of the flair.
|
||||
TextEditable *bool `url:"text_editable,omitempty"`
|
||||
|
||||
ModOnly *bool `url:"mod_only,omitempty"`
|
||||
|
||||
// Between 1 and 10 (inclusive). Default: 10.
|
||||
MaxEmojis *int `url:"max_emojis,omitempty"`
|
||||
|
||||
// One of: none, transparent, 6-digit rgb hex color, e.g. #AABBCC.
|
||||
BackgroundColor string `url:"background_color,omitempty"`
|
||||
CSSClass string `url:"css_class,omitempty"`
|
||||
}
|
||||
|
||||
// FlairTemplate is a generic flair structure that can users can use next to their username
|
||||
// or posts in a subreddit.
|
||||
type FlairTemplate struct {
|
||||
ID string `json:"id"`
|
||||
// USER_FLAIR (for users) or LINK_FLAIR (for posts).
|
||||
Type string `json:"flairType"`
|
||||
ModOnly bool `json:"modOnly"`
|
||||
|
||||
AllowableContent string `json:"allowableContent"`
|
||||
Text string `json:"text"`
|
||||
TextType string `json:"type"`
|
||||
TextColor string `json:"textColor"`
|
||||
TextEditable bool `json:"textEditable"`
|
||||
RichText []map[string]string `json:"richtext"`
|
||||
|
||||
OverrideCSS bool `json:"overrideCss"`
|
||||
MaxEmojis int `json:"maxEmojis"`
|
||||
BackgroundColor string `json:"backgroundColor"`
|
||||
CSSClass string `json:"cssClass"`
|
||||
}
|
||||
|
||||
// FlairSelectRequest represents a request to select a flair.
|
||||
type FlairSelectRequest struct {
|
||||
// The id of the template.
|
||||
ID string `url:"flair_template_id,omitempty"`
|
||||
// No longer than 64 characters.
|
||||
// Only use this if the flair is editable (it is by default if you're a mod of the subreddit).
|
||||
Text string `url:"text,omitempty"`
|
||||
}
|
||||
|
||||
// FlairChangeRequest represents a request to change a user's flair.
|
||||
// If Text and CSSClass are empty, the request will just clear the user's flair.
|
||||
type FlairChangeRequest struct {
|
||||
User string
|
||||
Text string
|
||||
CSSClass string
|
||||
}
|
||||
|
||||
// FlairChangeResponse represents a response to a FlairChangeRequest.
|
||||
type FlairChangeResponse struct {
|
||||
// Whether or not the request was successful.
|
||||
OK bool `json:"ok"`
|
||||
Status string `json:"status"`
|
||||
Warnings map[string]string `json:"warnings,omitempty"`
|
||||
Errors map[string]string `json:"errors,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
|
||||
}
|
||||
|
||||
root := new(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
|
||||
}
|
||||
|
||||
// Configure the subreddit's flair settings.
|
||||
func (s *FlairService) Configure(ctx context.Context, subreddit string, request *FlairConfigureRequest) (*Response, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("*FlairConfigureRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/flairconfig", subreddit)
|
||||
|
||||
form, err := query.Values(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Enable your flair in the subreddit.
|
||||
func (s *FlairService) Enable(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/setflairenabled", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_enabled", "true")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Disable your flair in the subreddit.
|
||||
func (s *FlairService) Disable(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/setflairenabled", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_enabled", "false")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UpsertUserTemplate creates a user flair template, or updates it if the request.ID is valid.
|
||||
// It returns the created/updated flair template.
|
||||
func (s *FlairService) UpsertUserTemplate(ctx context.Context, subreddit string, request *FlairTemplateCreateOrUpdateRequest) (*FlairTemplate, *Response, error) {
|
||||
if request == nil {
|
||||
return nil, nil, errors.New("*FlairTemplateCreateOrUpdateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/flairtemplate_v2", subreddit)
|
||||
|
||||
form, err := query.Values(request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "USER_FLAIR")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(FlairTemplate)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// UpsertPostTemplate creates a post flair template, or updates it if the request.ID is valid.
|
||||
// It returns the created/updated flair template.
|
||||
func (s *FlairService) UpsertPostTemplate(ctx context.Context, subreddit string, request *FlairTemplateCreateOrUpdateRequest) (*FlairTemplate, *Response, error) {
|
||||
if request == nil {
|
||||
return nil, nil, errors.New("*FlairTemplateCreateOrUpdateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/flairtemplate_v2", subreddit)
|
||||
|
||||
form, err := query.Values(request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "LINK_FLAIR")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(FlairTemplate)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Delete the flair of the user.
|
||||
func (s *FlairService) Delete(ctx context.Context, subreddit, username string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/deleteflair", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DeleteTemplate deletes the flair template via its id.
|
||||
func (s *FlairService) DeleteTemplate(ctx context.Context, subreddit, id string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/deleteflairtemplate", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_template_id", id)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DeleteAllUserTemplates deletes all user flair templates.
|
||||
func (s *FlairService) DeleteAllUserTemplates(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/clearflairtemplates", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "USER_FLAIR")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DeleteAllPostTemplates deletes all post flair templates.
|
||||
func (s *FlairService) DeleteAllPostTemplates(ctx context.Context, subreddit string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/clearflairtemplates", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "LINK_FLAIR")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// ReorderUserTemplates reorders the user flair templates in the order provided in the slice.
|
||||
// The order should contain every single flair id of this flair type; omitting any id will result in an error.
|
||||
func (s *FlairService) ReorderUserTemplates(ctx context.Context, subreddit string, ids []string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/flair_template_order/USER_FLAIR", subreddit)
|
||||
req, err := s.client.NewJSONRequest(http.MethodPatch, path, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// ReorderPostTemplates reorders the post flair templates in the order provided in the slice.
|
||||
// The order should contain every single flair id of this flair type; omitting any id will result in an error.
|
||||
func (s *FlairService) ReorderPostTemplates(ctx context.Context, subreddit string, ids []string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/%s/flair_template_order/LINK_FLAIR", subreddit)
|
||||
req, err := s.client.NewJSONRequest(http.MethodPatch, path, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Choices returns a list of flairs you can assign to yourself in the subreddit, and your current one.
|
||||
func (s *FlairService) Choices(ctx context.Context, subreddit string) ([]*FlairChoice, *FlairChoice, *Response, error) {
|
||||
return s.ChoicesOf(ctx, subreddit, s.client.Username)
|
||||
}
|
||||
|
||||
// ChoicesOf returns a list of flairs the user can assign to themself in the subreddit, and their current one.
|
||||
// Unless the user is you, this only works if you're a moderator of the subreddit.
|
||||
func (s *FlairService) ChoicesOf(ctx context.Context, subreddit, username string) ([]*FlairChoice, *FlairChoice, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/flairselector", subreddit)
|
||||
form := url.Values{}
|
||||
form.Set("name", username)
|
||||
return s.choices(ctx, path, form)
|
||||
}
|
||||
|
||||
// ChoicesForPost returns a list of flairs you can assign to an existing post, and the current one assigned to it.
|
||||
// If the post isn't yours, this only works if you're the moderator of the subreddit it's in.
|
||||
func (s *FlairService) ChoicesForPost(ctx context.Context, postID string) ([]*FlairChoice, *FlairChoice, *Response, error) {
|
||||
path := "api/flairselector"
|
||||
form := url.Values{}
|
||||
form.Set("link", postID)
|
||||
return s.choices(ctx, path, form)
|
||||
}
|
||||
|
||||
// ChoicesForNewPost returns a list of flairs you can assign to a new post in a subreddit.
|
||||
func (s *FlairService) ChoicesForNewPost(ctx context.Context, subreddit string) ([]*FlairChoice, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/flairselector", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("is_newlink", "true")
|
||||
|
||||
choices, _, resp, err := s.choices(ctx, path, form)
|
||||
return choices, resp, err
|
||||
}
|
||||
|
||||
func (s *FlairService) choices(ctx context.Context, path string, form url.Values) ([]*FlairChoice, *FlairChoice, *Response, error) {
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
Choices []*FlairChoice `json:"choices"`
|
||||
Current *FlairChoice `json:"current"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
|
||||
return root.Choices, root.Current, resp, nil
|
||||
}
|
||||
|
||||
// Select a flair to display next to your username in the subreddit.
|
||||
func (s *FlairService) Select(ctx context.Context, subreddit string, request *FlairSelectRequest) (*Response, error) {
|
||||
return s.Assign(ctx, subreddit, s.client.Username, request)
|
||||
}
|
||||
|
||||
// Assign a flair to another user in the subreddit.
|
||||
// This only works if you're a moderator of the subreddit, or if the user is you.
|
||||
func (s *FlairService) Assign(ctx context.Context, subreddit, user string, request *FlairSelectRequest) (*Response, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("*FlairSelectRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/selectflair", subreddit)
|
||||
|
||||
form, err := query.Values(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", user)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// SelectForPost assigns a flair to the post.
|
||||
// If the post isn't yours, you have to be a moderator of the post's subreddit for this to work.
|
||||
func (s *FlairService) SelectForPost(ctx context.Context, postID string, request *FlairSelectRequest) (*Response, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("*FlairSelectRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := "api/selectflair"
|
||||
|
||||
form, err := query.Values(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", postID)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// RemoveFromPost removes the flair from the post.
|
||||
// If the post isn't yours, you have to be a moderator of the post's subreddit for this to work.
|
||||
func (s *FlairService) RemoveFromPost(ctx context.Context, postID string) (*Response, error) {
|
||||
path := "api/selectflair"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", postID)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Change the flair of multiple users in the subreddit at once.
|
||||
// You have to be a moderator of the subreddit for this to work.
|
||||
func (s *FlairService) Change(ctx context.Context, subreddit string, requests []FlairChangeRequest) ([]*FlairChangeResponse, *Response, error) {
|
||||
if len(requests) == 0 || len(requests) > 100 {
|
||||
return nil, nil, errors.New("requests: must provide between 1 and 100")
|
||||
}
|
||||
|
||||
records := make([][]string, len(requests))
|
||||
for i, req := range requests {
|
||||
records[i] = []string{req.User, req.Text, req.CSSClass}
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
w := csv.NewWriter(buf)
|
||||
|
||||
err := w.WriteAll(records)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
err = w.Error()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/flaircsv", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("flair_csv", buf.String())
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var root []*FlairChangeResponse
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"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",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedFlairTemplate = &FlairTemplate{
|
||||
ID: "be0a6110-f23c-11ea-862f-0e08890d7323",
|
||||
Type: "LINK_FLAIR",
|
||||
ModOnly: false,
|
||||
|
||||
AllowableContent: "all",
|
||||
Text: "lol",
|
||||
TextType: "richtext",
|
||||
TextColor: "dark",
|
||||
TextEditable: false,
|
||||
RichText: []map[string]string{
|
||||
{"e": "text", "t": "lol"},
|
||||
},
|
||||
|
||||
OverrideCSS: false,
|
||||
MaxEmojis: 1,
|
||||
BackgroundColor: "#fafafa",
|
||||
CSSClass: "",
|
||||
}
|
||||
|
||||
var expectedFlairChoices = []*FlairChoice{
|
||||
{
|
||||
TemplateID: "c4edd5ce-40e8-11e7-b814-0ef91bd65558",
|
||||
Text: "Reddit API",
|
||||
Editable: false,
|
||||
Position: "left",
|
||||
CSSClass: "",
|
||||
},
|
||||
{
|
||||
TemplateID: "49bb3d06-0dad-11e7-b897-0e42c2400b7a",
|
||||
Text: "PRAW",
|
||||
Editable: false,
|
||||
Position: "left",
|
||||
CSSClass: "",
|
||||
},
|
||||
{
|
||||
TemplateID: "f1905376-40e9-11e7-a0dc-0e2f53ef3712",
|
||||
Text: "snoowrap",
|
||||
Editable: false,
|
||||
Position: "left",
|
||||
CSSClass: "",
|
||||
},
|
||||
{
|
||||
TemplateID: "03dc6ea8-40e9-11e7-8abb-0eb85aed0bce",
|
||||
Text: "Other API Wrapper",
|
||||
Editable: false,
|
||||
Position: "left",
|
||||
CSSClass: "",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedFlairChoice = &FlairChoice{
|
||||
TemplateID: "03dc6ea8-40e9-11e7-8abb-0eb85aed0bce",
|
||||
Text: "Other API Wrapper",
|
||||
Editable: false,
|
||||
Position: "left",
|
||||
CSSClass: "",
|
||||
}
|
||||
|
||||
var expectedFlairChanges = []*FlairChangeResponse{
|
||||
{
|
||||
OK: false,
|
||||
Status: "skipped",
|
||||
Warnings: map[string]string{},
|
||||
Errors: map[string]string{
|
||||
"user": "unable to resolve user `testuser1', ignoring",
|
||||
},
|
||||
},
|
||||
{
|
||||
OK: true,
|
||||
Status: "added flair for user testuser2",
|
||||
Warnings: map[string]string{},
|
||||
Errors: map[string]string{},
|
||||
},
|
||||
{
|
||||
OK: true,
|
||||
Status: "added flair for user testuser3",
|
||||
Warnings: map[string]string{},
|
||||
Errors: map[string]string{},
|
||||
},
|
||||
{
|
||||
OK: true,
|
||||
Status: "removed flair for user testuser4",
|
||||
Warnings: map[string]string{},
|
||||
Errors: map[string]string{},
|
||||
},
|
||||
}
|
||||
|
||||
func TestFlairService_GetUserFlairs(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func TestFlairService_Configure(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flairconfig", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_enabled", "true")
|
||||
form.Set("flair_position", "right")
|
||||
form.Set("flair_self_assign_enabled", "false")
|
||||
form.Set("link_flair_position", "left")
|
||||
form.Set("link_flair_self_assign_enabled", "false")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.Configure(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "*FlairConfigureRequest: cannot be nil")
|
||||
|
||||
_, err = client.Flair.Configure(ctx, "testsubreddit", &FlairConfigureRequest{
|
||||
UserFlairEnabled: Bool(true),
|
||||
UserFlairPosition: "right",
|
||||
UserFlairSelfAssignEnabled: Bool(false),
|
||||
PostFlairPosition: "left",
|
||||
PostFlairSelfAssignEnabled: Bool(false),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_Enable(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/setflairenabled", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_enabled", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.Enable(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_Disable(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/setflairenabled", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_enabled", "false")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.Disable(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_UpsertUserTemplate(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/flair-template.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flairtemplate_v2", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "USER_FLAIR")
|
||||
form.Set("allowable_content", "all")
|
||||
form.Set("text", "testtext")
|
||||
form.Set("text_color", "dark")
|
||||
form.Set("text_editable", "false")
|
||||
form.Set("mod_only", "true")
|
||||
form.Set("max_emojis", "5")
|
||||
form.Set("background_color", "transparent")
|
||||
form.Set("css_class", "testclass")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Flair.UpsertUserTemplate(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "*FlairTemplateCreateOrUpdateRequest: cannot be nil")
|
||||
|
||||
flairTemplate, _, err := client.Flair.UpsertUserTemplate(ctx, "testsubreddit", &FlairTemplateCreateOrUpdateRequest{
|
||||
AllowableContent: "all",
|
||||
ModOnly: Bool(true),
|
||||
Text: "testtext",
|
||||
TextColor: "dark",
|
||||
TextEditable: Bool(false),
|
||||
MaxEmojis: Int(5),
|
||||
BackgroundColor: "transparent",
|
||||
CSSClass: "testclass",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedFlairTemplate, flairTemplate)
|
||||
}
|
||||
|
||||
func TestFlairService_UpsertPostTemplate(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/flair-template.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flairtemplate_v2", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "LINK_FLAIR")
|
||||
form.Set("flair_template_id", "testid")
|
||||
form.Set("allowable_content", "text")
|
||||
form.Set("text", "testtext")
|
||||
form.Set("text_color", "light")
|
||||
form.Set("text_editable", "true")
|
||||
form.Set("mod_only", "false")
|
||||
form.Set("background_color", "#fafafa")
|
||||
form.Set("css_class", "testclass")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Flair.UpsertPostTemplate(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "*FlairTemplateCreateOrUpdateRequest: cannot be nil")
|
||||
|
||||
flairTemplate, _, err := client.Flair.UpsertPostTemplate(ctx, "testsubreddit", &FlairTemplateCreateOrUpdateRequest{
|
||||
ID: "testid",
|
||||
AllowableContent: "text",
|
||||
ModOnly: Bool(false),
|
||||
Text: "testtext",
|
||||
TextColor: "light",
|
||||
TextEditable: Bool(true),
|
||||
BackgroundColor: "#fafafa",
|
||||
CSSClass: "testclass",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedFlairTemplate, flairTemplate)
|
||||
}
|
||||
|
||||
func TestFlairService_Delete(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/deleteflair", 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", "testuser")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.Delete(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_DeleteTemplate(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/deleteflairtemplate", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_template_id", "testtemplate")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.DeleteTemplate(ctx, "testsubreddit", "testtemplate")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_DeleteAllUserTemplates(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/clearflairtemplates", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "USER_FLAIR")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.DeleteAllUserTemplates(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_DeleteAllPostTemplates(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/clearflairtemplates", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("flair_type", "LINK_FLAIR")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.DeleteAllPostTemplates(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_ReorderUserTemplates(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/flair_template_order/USER_FLAIR", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPatch, r.Method)
|
||||
|
||||
var ids []string
|
||||
err := json.NewDecoder(r.Body).Decode(&ids)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"test1", "test2", "test3", "test4"}, ids)
|
||||
})
|
||||
|
||||
_, err := client.Flair.ReorderUserTemplates(ctx, "testsubreddit", []string{"test1", "test2", "test3", "test4"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_ReorderPostTemplates(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/v1/testsubreddit/flair_template_order/LINK_FLAIR", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPatch, r.Method)
|
||||
|
||||
var ids []string
|
||||
err := json.NewDecoder(r.Body).Decode(&ids)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"test1", "test2", "test3", "test4"}, ids)
|
||||
})
|
||||
|
||||
_, err := client.Flair.ReorderPostTemplates(ctx, "testsubreddit", []string{"test1", "test2", "test3", "test4"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_Choices(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/choices.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flairselector", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "user1")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
choices, current, _, err := client.Flair.Choices(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedFlairChoices, choices)
|
||||
require.Equal(t, expectedFlairChoice, current)
|
||||
}
|
||||
|
||||
func TestFlairService_ChoicesOf(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/choices.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flairselector", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("name", "testuser")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
choices, current, _, err := client.Flair.ChoicesOf(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedFlairChoices, choices)
|
||||
require.Equal(t, expectedFlairChoice, current)
|
||||
}
|
||||
|
||||
func TestFlairService_ChoicesForPost(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/choices.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/flairselector", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("link", "t3_123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
choices, current, _, err := client.Flair.ChoicesForPost(ctx, "t3_123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedFlairChoices, choices)
|
||||
require.Equal(t, expectedFlairChoice, current)
|
||||
}
|
||||
|
||||
func TestFlairService_ChoicesForNewPost(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/choices.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flairselector", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("is_newlink", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
choices, _, err := client.Flair.ChoicesForNewPost(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedFlairChoices, choices)
|
||||
}
|
||||
|
||||
func TestFlairService_Select(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/selectflair", 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", "user1")
|
||||
form.Set("flair_template_id", "id123")
|
||||
form.Set("text", "text123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.Select(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "*FlairSelectRequest: cannot be nil")
|
||||
|
||||
_, err = client.Flair.Select(ctx, "testsubreddit", &FlairSelectRequest{
|
||||
ID: "id123",
|
||||
Text: "text123",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_Assign(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/selectflair", 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", "testuser")
|
||||
form.Set("flair_template_id", "id123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.Assign(ctx, "testsubreddit", "testuser", nil)
|
||||
require.EqualError(t, err, "*FlairSelectRequest: cannot be nil")
|
||||
|
||||
_, err = client.Flair.Assign(ctx, "testsubreddit", "testuser", &FlairSelectRequest{
|
||||
ID: "id123",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_SelectForPost(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/selectflair", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", "t3_123")
|
||||
form.Set("flair_template_id", "id123")
|
||||
form.Set("text", "text123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.SelectForPost(ctx, "t3_123", nil)
|
||||
require.EqualError(t, err, "*FlairSelectRequest: cannot be nil")
|
||||
|
||||
_, err = client.Flair.SelectForPost(ctx, "t3_123", &FlairSelectRequest{
|
||||
ID: "id123",
|
||||
Text: "text123",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_RemoveFromPost(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/selectflair", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", "t3_123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Flair.RemoveFromPost(ctx, "t3_123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestFlairService_Change(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/flair/csv-change.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/flaircsv", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("flair_csv", `testuser1,testtext1,testclass1
|
||||
testuser2,testtext2,testclass2
|
||||
testuser3,testtext3,testclass3
|
||||
testuser4,testtext4,testclass4
|
||||
`)
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Flair.Change(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "requests: must provide between 1 and 100")
|
||||
|
||||
changes, _, err := client.Flair.Change(ctx, "testsubreddit", []FlairChangeRequest{
|
||||
{"testuser1", "testtext1", "testclass1"},
|
||||
{"testuser2", "testtext2", "testclass2"},
|
||||
{"testuser3", "testtext3", "testclass3"},
|
||||
{"testuser4", "testtext4", "testclass4"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedFlairChanges, changes)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GoldService handles communication with the gold
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_gold
|
||||
type GoldService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Gild the post or comment via its full ID.
|
||||
// This requires you to own Reddit coins and will consume them.
|
||||
func (s *GoldService) Gild(ctx context.Context, id string) (*Response, error) {
|
||||
path := fmt.Sprintf("api/v1/gold/gild/%s", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Give the user between 1 and 36 (inclusive) months of gold.
|
||||
// This requires you to own Reddit coins and will consume them.
|
||||
func (s *GoldService) Give(ctx context.Context, username string, months int) (*Response, error) {
|
||||
if months < 1 || months > 36 {
|
||||
return nil, errors.New("months: must be between 1 and 36 (inclusive)")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("api/v1/gold/give/%s", username)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("months", strconv.Itoa(months))
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGoldService_Gild(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/v1/gold/gild/t1_test", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
})
|
||||
|
||||
_, err := client.Gold.Gild(ctx, "t1_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestGoldService_Give(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/v1/gold/give/testuser", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("months", "1")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Gold.Give(ctx, "testuser", 0)
|
||||
require.EqualError(t, err, "months: must be between 1 and 36 (inclusive)")
|
||||
|
||||
_, err = client.Gold.Give(ctx, "testuser", 37)
|
||||
require.EqualError(t, err, "months: must be between 1 and 36 (inclusive)")
|
||||
|
||||
_, err = client.Gold.Give(ctx, "testuser", 1)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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 posts, comments, and subreddits from their full IDs.
|
||||
func (s *ListingsService) Get(ctx context.Context, ids ...string) ([]*Post, []*Comment, []*Subreddit, *Response, error) {
|
||||
path := "api/info"
|
||||
params := struct {
|
||||
IDs []string `url:"id,omitempty,comma"`
|
||||
}{ids}
|
||||
|
||||
l, resp, err := s.client.getListing(ctx, path, params)
|
||||
if err != nil {
|
||||
return nil, nil, nil, resp, err
|
||||
}
|
||||
|
||||
return l.Posts(), l.Comments(), l.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, ","))
|
||||
l, resp, err := s.client.getListing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
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",
|
||||
SubredditSubscribers: 8202,
|
||||
|
||||
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",
|
||||
SubredditSubscribers: 8201,
|
||||
|
||||
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",
|
||||
SubredditSubscribers: 8201,
|
||||
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
},
|
||||
}
|
||||
|
||||
func TestListingsService_Get(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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,610 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// LiveThreadService handles communication with the live thread
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_live
|
||||
type LiveThreadService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// LiveThread is a thread on Reddit that provides real-time updates.
|
||||
type LiveThread struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Resources string `json:"resources,omitempty"`
|
||||
|
||||
State string `json:"state,omitempty"`
|
||||
ViewerCount int `json:"viewer_count"`
|
||||
ViewerCountFuzzed bool `json:"viewer_count_fuzzed"`
|
||||
|
||||
// Empty when a live thread has ended.
|
||||
WebSocketURL string `json:"websocket_url,omitempty"`
|
||||
|
||||
Announcement bool `json:"is_announcement"`
|
||||
NSFW bool `json:"nsfw"`
|
||||
}
|
||||
|
||||
// LiveThreadUpdate is an update in a live thread.
|
||||
type LiveThreadUpdate struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
FullID string `json:"name,omitempty"`
|
||||
Author string `json:"author,omitempty"`
|
||||
Created *Timestamp `json:"created_utc,omitempty"`
|
||||
|
||||
Body string `json:"body,omitempty"`
|
||||
EmbeddedURLs []string `json:"embeds,omitempty"`
|
||||
Stricken bool `json:"stricken"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (u *LiveThreadUpdate) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
ID string `json:"id"`
|
||||
FullID string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
Created *Timestamp `json:"created_utc"`
|
||||
|
||||
Body string `json:"body"`
|
||||
EmbeddedURLs []struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"embeds"`
|
||||
Stricken bool `json:"stricken"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
u.ID = root.ID
|
||||
u.FullID = root.FullID
|
||||
u.Author = root.Author
|
||||
u.Created = root.Created
|
||||
|
||||
u.Body = root.Body
|
||||
u.Stricken = root.Stricken
|
||||
|
||||
for _, eu := range root.EmbeddedURLs {
|
||||
u.EmbeddedURLs = append(u.EmbeddedURLs, eu.URL)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LiveThreadCreateOrUpdateRequest represents a request to create/update a live thread.
|
||||
type LiveThreadCreateOrUpdateRequest struct {
|
||||
// No longer than 120 characters.
|
||||
Title string `url:"title,omitempty"`
|
||||
Description string `url:"description,omitempty"`
|
||||
Resources string `url:"resources,omitempty"`
|
||||
NSFW *bool `url:"nsfw,omitempty"`
|
||||
}
|
||||
|
||||
// LiveThreadContributor is a user that can contribute to a live thread.
|
||||
type LiveThreadContributor struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Permissions []string `json:"permissions,omitempty"`
|
||||
}
|
||||
|
||||
// LiveThreadContributors is a list of users that can contribute to a live thread.
|
||||
type LiveThreadContributors struct {
|
||||
Current []*LiveThreadContributor `json:"current_contributors"`
|
||||
// This is only filled if you are a contributor in the live thread with the "manage" permission.
|
||||
Invited []*LiveThreadContributor `json:"invited_contributors,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (c *LiveThreadContributors) UnmarshalJSON(b []byte) error {
|
||||
if len(b) == 0 {
|
||||
return errors.New("no bytes to unmarshal")
|
||||
}
|
||||
|
||||
// neat trick taken from:
|
||||
// https://www.calhoun.io/how-to-parse-json-that-varies-between-an-array-or-a-single-item-with-go
|
||||
switch b[0] {
|
||||
case '{':
|
||||
return c.unmarshalSingle(b)
|
||||
case '[':
|
||||
return c.unmarshalMany(b)
|
||||
}
|
||||
|
||||
// This shouldn't really happen as the standard library seems to strip
|
||||
// whitespace from the bytes being passed in, but just in case let's guess at
|
||||
// multiple tags and fall back to a single one if that doesn't work.
|
||||
err := c.unmarshalSingle(b)
|
||||
if err != nil {
|
||||
return c.unmarshalMany(b)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LiveThreadContributors) unmarshalSingle(b []byte) error {
|
||||
root := new(struct {
|
||||
Data struct {
|
||||
Children []*LiveThreadContributor `json:"children"`
|
||||
} `json:"data"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, &root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Current = root.Data.Children
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *LiveThreadContributors) unmarshalMany(b []byte) error {
|
||||
var root [2]struct {
|
||||
Data struct {
|
||||
Children []*LiveThreadContributor `json:"children"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
err := json.Unmarshal(b, &root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Current = root[0].Data.Children
|
||||
c.Invited = root[1].Data.Children
|
||||
return nil
|
||||
}
|
||||
|
||||
// LiveThreadPermissions are the different permissions contributors have or don't have for a live thread.
|
||||
// Read about them here: https://mods.reddithelp.com/hc/en-us/articles/360009381491-User-Management-moderators-and-permissions
|
||||
type LiveThreadPermissions struct {
|
||||
All bool `permission:"all"`
|
||||
Close bool `permission:"close"`
|
||||
Discussions bool `permission:"discussions"`
|
||||
Edit bool `permission:"edit"`
|
||||
Manage bool `permission:"manage"`
|
||||
Settings bool `permission:"settings"`
|
||||
// Posting updates to the thread.
|
||||
Update bool `permission:"update"`
|
||||
}
|
||||
|
||||
func (p *LiveThreadPermissions) String() (s string) {
|
||||
if p == nil {
|
||||
return "+all"
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(*p)
|
||||
v := reflect.ValueOf(*p)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if v.Field(i).Kind() != reflect.Bool {
|
||||
continue
|
||||
}
|
||||
|
||||
permission := t.Field(i).Tag.Get("permission")
|
||||
permitted := v.Field(i).Bool()
|
||||
|
||||
if permitted {
|
||||
s += "+"
|
||||
} else {
|
||||
s += "-"
|
||||
}
|
||||
|
||||
s += permission
|
||||
|
||||
if i != t.NumField()-1 {
|
||||
s += ","
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Now gets information about the currently featured live thread.
|
||||
// This returns an empty 204 response if no thread is currently featured.
|
||||
func (s *LiveThreadService) Now(ctx context.Context) (*LiveThread, *Response, error) {
|
||||
path := "api/live/happening_now"
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
if err == io.EOF && resp != nil && resp.StatusCode == http.StatusNoContent {
|
||||
return nil, resp, nil
|
||||
}
|
||||
return nil, resp, err
|
||||
}
|
||||
liveThread, _ := t.LiveThread()
|
||||
return liveThread, resp, nil
|
||||
}
|
||||
|
||||
// Get information about a live thread.
|
||||
func (s *LiveThreadService) Get(ctx context.Context, id string) (*LiveThread, *Response, error) {
|
||||
path := fmt.Sprintf("live/%s/about", id)
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
liveThread, _ := t.LiveThread()
|
||||
return liveThread, resp, nil
|
||||
}
|
||||
|
||||
// GetMultiple gets information about multiple live threads.
|
||||
func (s *LiveThreadService) GetMultiple(ctx context.Context, ids ...string) ([]*LiveThread, *Response, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil, errors.New("must provide at least 1 id")
|
||||
}
|
||||
path := fmt.Sprintf("api/live/by_id/%s", strings.Join(ids, ","))
|
||||
l, resp, err := s.client.getListing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.LiveThreads(), resp, nil
|
||||
}
|
||||
|
||||
// Update the live thread by posting an update to it.
|
||||
// Requires the "update" permission.
|
||||
func (s *LiveThreadService) Update(ctx context.Context, id, text string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("body", text)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/update", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Updates gets a list of updates posted in the live thread.
|
||||
func (s *LiveThreadService) Updates(ctx context.Context, id string, opts *ListOptions) ([]*LiveThreadUpdate, *Response, error) {
|
||||
path := fmt.Sprintf("live/%s", id)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.LiveThreadUpdates(), resp, nil
|
||||
}
|
||||
|
||||
// UpdateByID gets a specific update in the live thread by its id.
|
||||
// The ID of the update is the "short" one, i.e. the one that doesn't start with "LiveUpdate_".
|
||||
func (s *LiveThreadService) UpdateByID(ctx context.Context, threadID, updateID string) (*LiveThreadUpdate, *Response, error) {
|
||||
path := fmt.Sprintf("live/%s/updates/%s", threadID, updateID)
|
||||
|
||||
// this endpoint returns a listing
|
||||
l, resp, err := s.client.getListing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
var update *LiveThreadUpdate
|
||||
updates := l.LiveThreadUpdates()
|
||||
if len(updates) > 0 {
|
||||
update = updates[0]
|
||||
}
|
||||
|
||||
return update, resp, nil
|
||||
}
|
||||
|
||||
// Discussions gets a list of discussions (posts) about the live thread.
|
||||
func (s *LiveThreadService) Discussions(ctx context.Context, id string, opts *ListOptions) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("live/%s/discussions", id)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
|
||||
// Strike (mark incorrect and cross out) the content of an update.
|
||||
// You must either be the author of the update or have the "edit" permission.
|
||||
func (s *LiveThreadService) Strike(ctx context.Context, threadID, updateID string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", updateID)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/strike_update", threadID)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Delete an update from the live thread.
|
||||
// You must either be the author of the update or have the "edit" permission.
|
||||
func (s *LiveThreadService) Delete(ctx context.Context, threadID, updateID string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", updateID)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/delete_update", threadID)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Create a live thread and get its id.
|
||||
func (s *LiveThreadService) Create(ctx context.Context, request *LiveThreadCreateOrUpdateRequest) (string, *Response, error) {
|
||||
if request == nil {
|
||||
return "", nil, errors.New("*LiveThreadCreateOrUpdateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
form, err := query.Values(request)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
path := "api/live/create"
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
JSON struct {
|
||||
Data struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"data"`
|
||||
} `json:"json"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
|
||||
return root.JSON.Data.ID, resp, nil
|
||||
}
|
||||
|
||||
// Close the thread permanently, disallowing future updates.
|
||||
func (s *LiveThreadService) Close(ctx context.Context, id string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/close_thread", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Configure the thread.
|
||||
// Requires the "settings" permission.
|
||||
func (s *LiveThreadService) Configure(ctx context.Context, id string, request *LiveThreadCreateOrUpdateRequest) (*Response, error) {
|
||||
if request == nil {
|
||||
return nil, errors.New("*LiveThreadCreateOrUpdateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
form, err := query.Values(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/edit", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(ctx, req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Contributors gets a list of users that are contributors to the live thread.
|
||||
// If you are a contributor and you have the "manage" permission (to manage contributors), you
|
||||
// also get a list of invited contributors that haven't yet accepted/refused their invitation.
|
||||
func (s *LiveThreadService) Contributors(ctx context.Context, id string) (*LiveThreadContributors, *Response, error) {
|
||||
path := fmt.Sprintf("live/%s/contributors", id)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(LiveThreadContributors)
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
|
||||
// Accept a pending invite to contribute to the live thread.
|
||||
func (s *LiveThreadService) Accept(ctx context.Context, id string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/accept_contributor_invite", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Leave the live thread by abdicating your status as contributor.
|
||||
func (s *LiveThreadService) Leave(ctx context.Context, id string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/leave_contributor", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Invite another user to contribute to the live thread.
|
||||
// If permissions is nil, all permissions will be granted.
|
||||
// Requires the "manage" permission.
|
||||
func (s *LiveThreadService) Invite(ctx context.Context, id, username string, permissions *LiveThreadPermissions) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", "liveupdate_contributor_invite")
|
||||
form.Set("permissions", permissions.String())
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/invite_contributor", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Uninvite a user that's been invited to contribute to a live thread via their full ID.
|
||||
// Requires the "manage" permission.
|
||||
func (s *LiveThreadService) Uninvite(ctx context.Context, threadID, userID string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", userID)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/rm_contributor_invite", threadID)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// SetPermissions sets the permissions for the contributor in the live thread.
|
||||
// If permissions is nil, all permissions will be granted.
|
||||
// Requires the "manage" permission.
|
||||
func (s *LiveThreadService) SetPermissions(ctx context.Context, id, username string, permissions *LiveThreadPermissions) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", "liveupdate_contributor")
|
||||
form.Set("permissions", permissions.String())
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/set_contributor_permissions", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// SetPermissionsForInvite sets the permissions for a contributor who's yet to accept/refuse their invite.
|
||||
// If permissions is nil, all permissions will be granted.
|
||||
// Requires the "manage" permission.
|
||||
func (s *LiveThreadService) SetPermissionsForInvite(ctx context.Context, id, username string, permissions *LiveThreadPermissions) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", "liveupdate_contributor_invite")
|
||||
form.Set("permissions", permissions.String())
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/set_contributor_permissions", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Revoke a user's contributorship via their full ID.
|
||||
// Requires the "manage" permission.
|
||||
func (s *LiveThreadService) Revoke(ctx context.Context, threadID, userID string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", userID)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/rm_contributor", threadID)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// HideDiscussion hides a linked post from the live thread's discussion sidebar.
|
||||
// The postID should be the base36 ID of the post, i.e. not its full id.
|
||||
func (s *LiveThreadService) HideDiscussion(ctx context.Context, threadID, postID string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", postID)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/hide_discussion", threadID)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UnhideDiscussion unhides a linked post from the live thread's discussion sidebar.
|
||||
// The postID should be the base36 ID of the post, i.e. not its full id.
|
||||
func (s *LiveThreadService) UnhideDiscussion(ctx context.Context, threadID, postID string) (*Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", postID)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/unhide_discussion", threadID)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Report the live thread.
|
||||
// The reason should be one of:
|
||||
// spam, vote-manipulation, personal-information, sexualizing-minors, site-breaking
|
||||
func (s *LiveThreadService) Report(ctx context.Context, id, reason string) (*Response, error) {
|
||||
switch reason {
|
||||
case "spam", "vote-manipulation", "personal-information", "sexualizing-minors", "site-breaking":
|
||||
default:
|
||||
return nil, errors.New("invalid reason for reporting live thread: " + reason)
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("type", reason)
|
||||
|
||||
path := fmt.Sprintf("api/live/%s/report", id)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedLiveThread = &LiveThread{
|
||||
ID: "15nevtv8e54dh",
|
||||
FullID: "LiveUpdateEvent_15nevtv8e54dh",
|
||||
Created: &Timestamp{time.Date(2020, 9, 16, 1, 20, 27, 0, time.UTC)},
|
||||
|
||||
Title: "test",
|
||||
Description: "test",
|
||||
Resources: "",
|
||||
|
||||
State: "live",
|
||||
ViewerCount: 6,
|
||||
ViewerCountFuzzed: true,
|
||||
|
||||
WebSocketURL: "wss://ws-078adc7cb2099a9df.wss.redditmedia.com/live/15nevtv8e54dh?m=AQAA7rxiX6EpLYFCFZ0KJD4lVAPaMt0A1z2-xJ1b2dWCmxNIfMwL",
|
||||
|
||||
Announcement: false,
|
||||
NSFW: false,
|
||||
}
|
||||
|
||||
var expectedLiveThreads = []*LiveThread{
|
||||
{
|
||||
ID: "15nevtv8e54dh",
|
||||
FullID: "LiveUpdateEvent_15nevtv8e54dh",
|
||||
Created: &Timestamp{time.Date(2020, 9, 16, 1, 20, 27, 0, time.UTC)},
|
||||
|
||||
Title: "test",
|
||||
Description: "test",
|
||||
Resources: "",
|
||||
|
||||
State: "live",
|
||||
ViewerCount: 6,
|
||||
ViewerCountFuzzed: true,
|
||||
|
||||
WebSocketURL: "wss://ws-078adc7cb2099a9df.wss.redditmedia.com/live/15nevtv8e54dh?m=AQAA7rxiX6EpLYFCFZ0KJD4lVAPaMt0A1z2-xJ1b2dWCmxNIfMwL",
|
||||
|
||||
Announcement: false,
|
||||
NSFW: false,
|
||||
},
|
||||
{
|
||||
ID: "15ndkho8e54dh",
|
||||
FullID: "LiveUpdateEvent_15ndkho8e54dh",
|
||||
Created: &Timestamp{time.Date(2020, 9, 16, 1, 20, 37, 0, time.UTC)},
|
||||
|
||||
Title: "test 2",
|
||||
Description: "test 2",
|
||||
Resources: "",
|
||||
|
||||
State: "live",
|
||||
ViewerCount: 6,
|
||||
ViewerCountFuzzed: true,
|
||||
|
||||
WebSocketURL: "wss://ws-078adc7cb2099a9df.wss.redditmedia.com/live/15ndkho8e54dh?m=AQAA7rxiX6EpLYFCFZ0KJD4lVAPaMt0A1z2-xJ1b2dWCmxNIfMwL",
|
||||
|
||||
Announcement: false,
|
||||
NSFW: false,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedLiveThreadUpdates = []*LiveThreadUpdate{
|
||||
{
|
||||
ID: "5e46cd94-f968-11ea-9a6a-0e1933241e7d",
|
||||
FullID: "LiveUpdate_5e46cd94-f968-11ea-9a6a-0e1933241e7d",
|
||||
Author: "testuser1",
|
||||
Created: &Timestamp{time.Date(2020, 9, 18, 4, 35, 24, 0, time.UTC)},
|
||||
|
||||
Body: "test 2",
|
||||
EmbeddedURLs: []string{"https://example.com", "https://reddit.com"},
|
||||
|
||||
Stricken: true,
|
||||
},
|
||||
{
|
||||
ID: "fc44f204-f964-11ea-b148-0e2e56a0425f",
|
||||
FullID: "LiveUpdate_fc44f204-f964-11ea-b148-0e2e56a0425f",
|
||||
Author: "testuser1",
|
||||
Created: &Timestamp{time.Date(2020, 9, 18, 4, 11, 11, 0, time.UTC)},
|
||||
|
||||
Body: "test 1",
|
||||
|
||||
Stricken: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedLiveThreadUpdate = &LiveThreadUpdate{
|
||||
ID: "fc44f204-f964-11ea-b148-0e2e56a0425f",
|
||||
FullID: "LiveUpdate_fc44f204-f964-11ea-b148-0e2e56a0425f",
|
||||
Author: "testuser1",
|
||||
Created: &Timestamp{time.Date(2020, 9, 18, 4, 11, 11, 0, time.UTC)},
|
||||
|
||||
Body: "test 1",
|
||||
|
||||
Stricken: true,
|
||||
}
|
||||
|
||||
var expectedLiveThreadDiscussions = []*Post{
|
||||
{
|
||||
ID: "test1",
|
||||
FullID: "t3_test1",
|
||||
Created: &Timestamp{time.Date(2020, 9, 16, 12, 37, 31, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
Permalink: "/r/live/comments/test1/test_title/",
|
||||
URL: "https://www.reddit.com/live/15nfp4mtfbo14/",
|
||||
|
||||
Title: "test title",
|
||||
|
||||
Score: 22,
|
||||
UpvoteRatio: 0.9,
|
||||
NumberOfComments: 1,
|
||||
|
||||
SubredditName: "live",
|
||||
SubredditNamePrefixed: "r/live",
|
||||
SubredditID: "t5_32o7w",
|
||||
SubredditSubscribers: 24501,
|
||||
|
||||
Author: "TestUser",
|
||||
AuthorID: "t2_test1",
|
||||
},
|
||||
{
|
||||
ID: "test2",
|
||||
FullID: "t3_test2",
|
||||
Created: &Timestamp{time.Date(2020, 9, 16, 12, 37, 1, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
Permalink: "/r/live/comments/test2/test_title/",
|
||||
URL: "https://www.reddit.com/live/15nfp4mtfbo14/",
|
||||
|
||||
Title: "test title",
|
||||
|
||||
Score: 71,
|
||||
UpvoteRatio: 0.97,
|
||||
NumberOfComments: 34,
|
||||
|
||||
SubredditName: "live",
|
||||
SubredditNamePrefixed: "r/live",
|
||||
SubredditID: "t5_32o7w",
|
||||
SubredditSubscribers: 24501,
|
||||
|
||||
Author: "TestUser",
|
||||
AuthorID: "t2_test1",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedLiveThreadContributors = &LiveThreadContributors{
|
||||
Current: []*LiveThreadContributor{
|
||||
{ID: "t2_test1", Name: "test1", Permissions: []string{"all"}},
|
||||
{ID: "t2_test2", Name: "test2", Permissions: []string{"all"}},
|
||||
},
|
||||
Invited: nil,
|
||||
}
|
||||
|
||||
var expectedLiveThreadContributorsAndInvited = &LiveThreadContributors{
|
||||
Current: []*LiveThreadContributor{
|
||||
{ID: "t2_test1", Name: "test1", Permissions: []string{"all"}},
|
||||
{ID: "t2_test2", Name: "test2", Permissions: []string{"all"}},
|
||||
},
|
||||
Invited: []*LiveThreadContributor{
|
||||
{ID: "t2_test3", Name: "test3", Permissions: []string{"manage", "discussions"}},
|
||||
},
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Get(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/live-thread.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/live/id123/about", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
liveThread, _, err := client.LiveThread.Get(ctx, "id123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThread, liveThread)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Now(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/live-thread.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/live/happening_now", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
liveThread, _, err := client.LiveThread.Now(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThread, liveThread)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Now_NoContent(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/happening_now", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
w.WriteHeader(204)
|
||||
})
|
||||
|
||||
liveThread, _, err := client.LiveThread.Now(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, liveThread)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_GetMultiple(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/live-threads.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/api/live/by_id/id1,id2", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.LiveThread.GetMultiple(ctx)
|
||||
require.EqualError(t, err, "must provide at least 1 id")
|
||||
|
||||
liveThreads, _, err := client.LiveThread.GetMultiple(ctx, "id1", "id2")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThreads, liveThreads)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Update(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/update", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("body", "test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Update(ctx, "id123", "test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Updates(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/updates.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/live/id123", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
liveThreadUpdates, _, err := client.LiveThread.Updates(ctx, "id123", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThreadUpdates, liveThreadUpdates)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_UpdateByID(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/update.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/live/id123/updates/update123", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
liveThreadUpdate, _, err := client.LiveThread.UpdateByID(ctx, "id123", "update123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThreadUpdate, liveThreadUpdate)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Discussions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/discussions.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/live/id123/discussions", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
liveThreadDiscussions, _, err := client.LiveThread.Discussions(ctx, "id123", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThreadDiscussions, liveThreadDiscussions)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Strike(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/strike_update", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", "update123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Strike(ctx, "id123", "update123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Delete(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/delete_update", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", "update123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Delete(ctx, "id123", "update123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Create(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/create", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("title", "testtitle")
|
||||
form.Set("description", "testdescription")
|
||||
form.Set("resources", "testresources")
|
||||
form.Set("nsfw", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, `{
|
||||
"json": {
|
||||
"data": {
|
||||
"id": "id123"
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
}`)
|
||||
})
|
||||
|
||||
_, _, err := client.LiveThread.Create(ctx, nil)
|
||||
require.EqualError(t, err, "*LiveThreadCreateOrUpdateRequest: cannot be nil")
|
||||
|
||||
id, _, err := client.LiveThread.Create(ctx, &LiveThreadCreateOrUpdateRequest{
|
||||
Title: "testtitle",
|
||||
Description: "testdescription",
|
||||
Resources: "testresources",
|
||||
NSFW: Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "id123", id)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Close(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/close_thread", 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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Close(ctx, "id123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Configure(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/edit", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("title", "testtitle")
|
||||
form.Set("description", "testdescription")
|
||||
form.Set("resources", "testresources")
|
||||
form.Set("nsfw", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, `{
|
||||
"json": {
|
||||
"data": {
|
||||
"id": "id123"
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
}`)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Configure(ctx, "id123", nil)
|
||||
require.EqualError(t, err, "*LiveThreadCreateOrUpdateRequest: cannot be nil")
|
||||
|
||||
_, err = client.LiveThread.Configure(ctx, "id123", &LiveThreadCreateOrUpdateRequest{
|
||||
Title: "testtitle",
|
||||
Description: "testdescription",
|
||||
Resources: "testresources",
|
||||
NSFW: Bool(true),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Contributors(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/contributors.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/live/id123/contributors", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
contributors, _, err := client.LiveThread.Contributors(ctx, "id123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThreadContributors, contributors)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_ContributorsAndInvited(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/live-thread/contributors-and-invited.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/live/id123/contributors", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
contributors, _, err := client.LiveThread.Contributors(ctx, "id123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedLiveThreadContributorsAndInvited, contributors)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Accept(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/accept_contributor_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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Accept(ctx, "id123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Leave(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/leave_contributor", 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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Leave(ctx, "id123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Invite(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/invite_contributor", 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", "testuser")
|
||||
form.Set("type", "liveupdate_contributor_invite")
|
||||
form.Set("permissions", "+all")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Invite(ctx, "id123", "testuser", nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Invite_Permissions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/invite_contributor", 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", "testuser")
|
||||
form.Set("type", "liveupdate_contributor_invite")
|
||||
form.Set("permissions", "-all,+close,-discussions,-edit,+manage,-settings,+update")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Invite(ctx, "id123", "testuser", &LiveThreadPermissions{Close: true, Manage: true, Update: true})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Uninvite(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/rm_contributor_invite", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", "t2_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Uninvite(ctx, "id123", "t2_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_SetPermissions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/set_contributor_permissions", 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", "testuser")
|
||||
form.Set("type", "liveupdate_contributor")
|
||||
form.Set("permissions", "-all,-close,+discussions,+edit,-manage,+settings,-update")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.SetPermissions(ctx, "id123", "testuser", &LiveThreadPermissions{Discussions: true, Edit: true, Settings: true})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_SetPermissionsForInvite(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/set_contributor_permissions", 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", "testuser")
|
||||
form.Set("type", "liveupdate_contributor_invite")
|
||||
form.Set("permissions", "-all,-close,+discussions,+edit,-manage,+settings,-update")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.SetPermissionsForInvite(ctx, "id123", "testuser", &LiveThreadPermissions{Discussions: true, Edit: true, Settings: true})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Revoke(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/rm_contributor", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("id", "t2_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Revoke(ctx, "id123", "t2_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_HideDiscussion(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/hide_discussion", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", "t3_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.HideDiscussion(ctx, "id123", "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_UnhideDiscussion(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/unhide_discussion", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("link", "t3_test")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.UnhideDiscussion(ctx, "id123", "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestLiveThreadService_Report(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/live/id123/report", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("type", "spam")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.LiveThread.Report(ctx, "id123", "invalidreason")
|
||||
require.EqualError(t, err, "invalid reason for reporting live thread: invalidreason")
|
||||
|
||||
_, err = client.LiveThread.Report(ctx, "id123", "spam")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
type inboxThing struct {
|
||||
Kind string `json:"kind"`
|
||||
Data *Message `json:"data"`
|
||||
}
|
||||
|
||||
type inboxListing struct {
|
||||
inboxThings
|
||||
after string
|
||||
}
|
||||
|
||||
func (l *inboxListing) After() string {
|
||||
return l.after
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (l *inboxListing) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
Data struct {
|
||||
Things inboxThings `json:"children"`
|
||||
After string `json:"after"`
|
||||
} `json:"data"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.inboxThings = root.Data.Things
|
||||
l.after = root.Data.After
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type inboxThings struct {
|
||||
Comments []*Message
|
||||
Messages []*Message
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (t *inboxThings) UnmarshalJSON(b []byte) error {
|
||||
var things []inboxThing
|
||||
if err := json.Unmarshal(b, &things); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.add(things...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *inboxThings) add(things ...inboxThing) {
|
||||
for _, thing := range things {
|
||||
switch thing.Kind {
|
||||
case kindComment:
|
||||
t.Comments = append(t.Comments, thing.Data)
|
||||
case kindMessage:
|
||||
t.Messages = append(t.Messages, thing.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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.NewRequest(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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Block the author of a post, comment or message via its full ID.
|
||||
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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Collapse 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Uncollapse 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Delete 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Send a message.
|
||||
func (s *MessageService) Send(ctx context.Context, sendRequest *SendMessageRequest) (*Response, error) {
|
||||
if sendRequest == nil {
|
||||
return nil, errors.New("*SendMessageRequest: 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.NewRequest(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) ([]*Message, []*Message, *Response, error) {
|
||||
root, resp, err := s.inbox(ctx, "message/inbox", opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return root.Comments, root.Messages, resp, nil
|
||||
}
|
||||
|
||||
// InboxUnread returns unread comments and messages that appear in your inbox, respectively.
|
||||
func (s *MessageService) InboxUnread(ctx context.Context, opts *ListOptions) ([]*Message, []*Message, *Response, error) {
|
||||
root, resp, err := s.inbox(ctx, "message/unread", opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return root.Comments, root.Messages, resp, nil
|
||||
}
|
||||
|
||||
// Sent returns messages that you've sent.
|
||||
func (s *MessageService) Sent(ctx context.Context, opts *ListOptions) ([]*Message, *Response, error) {
|
||||
root, resp, err := s.inbox(ctx, "message/sent", opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return root.Messages, resp, nil
|
||||
}
|
||||
|
||||
func (s *MessageService) inbox(ctx context.Context, path string, opts *ListOptions) (*inboxListing, *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(inboxListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return root, resp, nil
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedCommentMessages = []*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,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedMessages = []*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,
|
||||
},
|
||||
}
|
||||
|
||||
func TestMessageService_ReadAll(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/read_all_messages", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
})
|
||||
|
||||
resp, err := client.Message.ReadAll(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestMessageService_Read(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Message.Block(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Collapse(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Message.Delete(ctx, "test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMessageService_Send(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Message.Send(ctx, nil)
|
||||
require.EqualError(t, err, "*SendMessageRequest: 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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,494 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// ModPermissions are the different permissions moderators have or don't have on a subreddit.
|
||||
// Read about them here: https://mods.reddithelp.com/hc/en-us/articles/360009381491-User-Management-moderators-and-permissions
|
||||
type ModPermissions struct {
|
||||
All bool `permission:"all"`
|
||||
Access bool `permission:"access"`
|
||||
ChatConfig bool `permission:"chat_config"`
|
||||
ChatOperator bool `permission:"chat_operator"`
|
||||
Config bool `permission:"config"`
|
||||
Flair bool `permission:"flair"`
|
||||
Mail bool `permission:"mail"`
|
||||
Posts bool `permission:"posts"`
|
||||
Wiki bool `permission:"wiki"`
|
||||
}
|
||||
|
||||
func (p *ModPermissions) String() (s string) {
|
||||
if p == nil {
|
||||
return "+all"
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(*p)
|
||||
v := reflect.ValueOf(*p)
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
if v.Field(i).Kind() != reflect.Bool {
|
||||
continue
|
||||
}
|
||||
|
||||
permission := t.Field(i).Tag.Get("permission")
|
||||
permitted := v.Field(i).Bool()
|
||||
|
||||
if permitted {
|
||||
s += "+"
|
||||
} else {
|
||||
s += "-"
|
||||
}
|
||||
|
||||
s += permission
|
||||
|
||||
if i != t.NumField()-1 {
|
||||
s += ","
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// BanConfig configures the ban of the user being banned.
|
||||
type BanConfig struct {
|
||||
Reason string `url:"reason,omitempty"`
|
||||
// Not visible to the user being banned.
|
||||
ModNote string `url:"note,omitempty"`
|
||||
// How long the ban will last. 0-999. Leave nil for permanent.
|
||||
Days *int `url:"duration,omitempty"`
|
||||
// Note to include in the ban message to the user.
|
||||
Message string `url:"ban_message,omitempty"`
|
||||
}
|
||||
|
||||
// Actions gets a list of moderator actions on a subreddit.
|
||||
func (s *ModerationService) Actions(ctx context.Context, subreddit string, opts *ListModActionOptions) ([]*ModAction, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/log", subreddit)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.ModActions(), 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Approve 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Remove 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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Reported returns posts and comments that have been reported.
|
||||
func (s *ModerationService) Reported(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/reports", subreddit)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return l.Posts(), l.Comments(), resp, nil
|
||||
}
|
||||
|
||||
// Spam returns posts and comments marked as spam.
|
||||
func (s *ModerationService) Spam(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/spam", subreddit)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return l.Posts(), l.Comments(), resp, nil
|
||||
}
|
||||
|
||||
// Queue returns posts and comments requiring moderator reviews, such as one that have been
|
||||
// reported or caught in the spam filter.
|
||||
func (s *ModerationService) Queue(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/modqueue", subreddit)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return l.Posts(), l.Comments(), resp, nil
|
||||
}
|
||||
|
||||
// Unmoderated returns posts that have yet to be approved/removed by a mod.
|
||||
func (s *ModerationService) Unmoderated(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/unmoderated", subreddit)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
|
||||
// Edited gets posts and comments that have been edited recently.
|
||||
func (s *ModerationService) Edited(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, []*Comment, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/about/edited", subreddit)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return l.Posts(), l.Comments(), resp, nil
|
||||
}
|
||||
|
||||
// IgnoreReports prevents reports on a post or comment from causing notifications.
|
||||
func (s *ModerationService) IgnoreReports(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/ignore_reports"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UnignoreReports allows reports on a post or comment to cause notifications.
|
||||
func (s *ModerationService) UnignoreReports(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/unignore_reports"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Invite a user to become a moderator of the subreddit.
|
||||
// If permissions is nil, all permissions will be granted.
|
||||
func (s *ModerationService) Invite(ctx context.Context, subreddit string, username string, permissions *ModPermissions) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/friend", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", "moderator_invite")
|
||||
form.Set("permissions", permissions.String())
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Uninvite a user from becoming a moderator of the subreddit.
|
||||
func (s *ModerationService) Uninvite(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.deleteRelationship(ctx, subreddit, username, "moderator_invite")
|
||||
}
|
||||
|
||||
// SetPermissions sets the mod permissions for the user in the subreddit.
|
||||
// If permissions is nil, all permissions will be granted.
|
||||
func (s *ModerationService) SetPermissions(ctx context.Context, subreddit string, username string, permissions *ModPermissions) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/setpermissions", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", "moderator_invite")
|
||||
form.Set("permissions", permissions.String())
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Ban a user from the subreddit.
|
||||
func (s *ModerationService) Ban(ctx context.Context, subreddit string, username string, config *BanConfig) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/friend", subreddit)
|
||||
|
||||
form, err := query.Values(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", "banned")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unban a user from the subreddit.
|
||||
func (s *ModerationService) Unban(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.deleteRelationship(ctx, subreddit, username, "banned")
|
||||
}
|
||||
|
||||
// BanWiki bans a user from contributing to the subreddit wiki.
|
||||
func (s *ModerationService) BanWiki(ctx context.Context, subreddit string, username string, config *BanConfig) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/friend", subreddit)
|
||||
|
||||
form, err := query.Values(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", "wikibanned")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// UnbanWiki unbans a user from contributing to the subreddit wiki.
|
||||
func (s *ModerationService) UnbanWiki(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.deleteRelationship(ctx, subreddit, username, "wikibanned")
|
||||
}
|
||||
|
||||
// Mute a user in the subreddit.
|
||||
func (s *ModerationService) Mute(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.createRelationship(ctx, subreddit, username, "muted")
|
||||
}
|
||||
|
||||
// Unmute a user in the subreddit.
|
||||
func (s *ModerationService) Unmute(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.deleteRelationship(ctx, subreddit, username, "muted")
|
||||
}
|
||||
|
||||
// ApproveUser adds a user as an approved user to the subreddit.
|
||||
func (s *ModerationService) ApproveUser(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.createRelationship(ctx, subreddit, username, "contributor")
|
||||
}
|
||||
|
||||
// UnapproveUser removes a user as an approved user to the subreddit.
|
||||
func (s *ModerationService) UnapproveUser(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.deleteRelationship(ctx, subreddit, username, "contributor")
|
||||
}
|
||||
|
||||
// ApproveUserWiki adds a user as an approved wiki contributor in the subreddit.
|
||||
func (s *ModerationService) ApproveUserWiki(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.createRelationship(ctx, subreddit, username, "wikicontributor")
|
||||
}
|
||||
|
||||
// UnapproveUserWiki removes a user as an approved wiki contributor in the subreddit.
|
||||
func (s *ModerationService) UnapproveUserWiki(ctx context.Context, subreddit string, username string) (*Response, error) {
|
||||
return s.deleteRelationship(ctx, subreddit, username, "wikicontributor")
|
||||
}
|
||||
|
||||
func (s *ModerationService) createRelationship(ctx context.Context, subreddit, username, relationship string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/friend", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", relationship)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
func (s *ModerationService) deleteRelationship(ctx context.Context, subreddit, username, relationship string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/unfriend", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("name", username)
|
||||
form.Set("type", relationship)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Distinguish your post or comment via its full ID, adding a moderator tag to it.
|
||||
// todo: add how=admin and how=special? They require special privileges.
|
||||
func (s *ModerationService) Distinguish(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/distinguish"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("how", "yes")
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// DistinguishAndSticky your comment via its full ID, adding a moderator tag to it
|
||||
// and stickying the comment at the top of the thread.
|
||||
func (s *ModerationService) DistinguishAndSticky(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/distinguish"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("how", "yes")
|
||||
form.Set("sticky", "true")
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Undistinguish your post or comment via its full ID, removing the moderator tag from it.
|
||||
func (s *ModerationService) Undistinguish(ctx context.Context, id string) (*Response, error) {
|
||||
path := "api/distinguish"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("how", "no")
|
||||
form.Set("id", id)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,701 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedModActions = []*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",
|
||||
},
|
||||
}
|
||||
|
||||
func TestModerationService_Actions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.Moderation.Actions(ctx, "testsubreddit", &ListModActionOptions{Type: "testtype", Moderator: "testmod"})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedModActions, modActions)
|
||||
require.Equal(t, "ModAction_a0408162-c4ad-11ea-8239-0e3b48262e8b", resp.After)
|
||||
}
|
||||
|
||||
func TestModerationService_AcceptInvite(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, err = client.Moderation.AcceptInvite(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Approve(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Approve(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Remove(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Remove(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_RemoveSpam(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.RemoveSpam(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Leave(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Leave(ctx, "t5_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_LeaveContributor(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.LeaveContributor(ctx, "t5_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Reported(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// contains posts and comments
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/about/reports", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, resp, err := client.Moderation.Reported(ctx, "testsubreddit", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestModerationService_Spam(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// contains posts and comments
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/about/spam", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, resp, err := client.Moderation.Spam(ctx, "testsubreddit", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestModerationService_Queue(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// contains posts and comments
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/about/modqueue", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, comments, resp, err := client.Moderation.Queue(ctx, "testsubreddit", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestModerationService_Unmoderated(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// contains posts and comments
|
||||
blob, err := readFileContents("../testdata/user/overview.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/about/unmoderated", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
posts, resp, err := client.Moderation.Unmoderated(ctx, "testsubreddit", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestModerationService_Edited(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.Moderation.Edited(ctx, "testsubreddit", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestModerationService_IgnoreReports(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/ignore_reports", 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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.IgnoreReports(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_UnignoreReports(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/unignore_reports", 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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.UnignoreReports(ctx, "t3_test")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Invite(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/friend", 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", "testuser")
|
||||
form.Set("type", "moderator_invite")
|
||||
form.Set("permissions", "+all")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Invite(ctx, "testsubreddit", "testuser", nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Invite_Permissions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/friend", 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", "testuser")
|
||||
form.Set("type", "moderator_invite")
|
||||
form.Set("permissions", "-all,-access,-chat_config,+chat_operator,+config,-flair,-mail,-posts,+wiki")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Invite(ctx, "testsubreddit", "testuser", &ModPermissions{ChatOperator: true, Config: true, Wiki: true})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Uninvite(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/unfriend", 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", "testuser")
|
||||
form.Set("type", "moderator_invite")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Uninvite(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_SetPermissions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/setpermissions", 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", "testuser")
|
||||
form.Set("type", "moderator_invite")
|
||||
form.Set("permissions", "-all,+access,-chat_config,-chat_operator,-config,+flair,-mail,+posts,-wiki")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.SetPermissions(ctx, "testsubreddit", "testuser", &ModPermissions{Access: true, Flair: true, Posts: true})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Ban(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/friend", 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", "testuser")
|
||||
form.Set("type", "banned")
|
||||
form.Set("reason", "test reason")
|
||||
form.Set("note", "test mod note")
|
||||
form.Set("duration", "5")
|
||||
form.Set("ban_message", "test message")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Ban(ctx, "testsubreddit", "testuser", &BanConfig{
|
||||
Reason: "test reason",
|
||||
ModNote: "test mod note",
|
||||
Days: Int(5),
|
||||
Message: "test message",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Unban(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/unfriend", 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", "testuser")
|
||||
form.Set("type", "banned")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Unban(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_BanWiki(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/friend", 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", "testuser")
|
||||
form.Set("type", "wikibanned")
|
||||
form.Set("reason", "test reason")
|
||||
form.Set("note", "test mod note")
|
||||
form.Set("duration", "5")
|
||||
form.Set("ban_message", "test message")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.BanWiki(ctx, "testsubreddit", "testuser", &BanConfig{
|
||||
Reason: "test reason",
|
||||
ModNote: "test mod note",
|
||||
Days: Int(5),
|
||||
Message: "test message",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_UnbanWiki(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/unfriend", 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", "testuser")
|
||||
form.Set("type", "wikibanned")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.UnbanWiki(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Mute(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/friend", 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", "testuser")
|
||||
form.Set("type", "muted")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Mute(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Unmuted(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/unfriend", 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", "testuser")
|
||||
form.Set("type", "muted")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Unmute(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_ApproveUser(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/friend", 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", "testuser")
|
||||
form.Set("type", "contributor")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.ApproveUser(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_UnapproveUser(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/unfriend", 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", "testuser")
|
||||
form.Set("type", "contributor")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.UnapproveUser(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_ApproveUserWiki(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/friend", 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", "testuser")
|
||||
form.Set("type", "wikicontributor")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.ApproveUserWiki(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_UnapproveUserWiki(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/unfriend", 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", "testuser")
|
||||
form.Set("type", "wikicontributor")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.UnapproveUserWiki(ctx, "testsubreddit", "testuser")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Distinguish(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/distinguish", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("how", "yes")
|
||||
form.Set("id", "t1_123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Distinguish(ctx, "t1_123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_DistinguishAndSticky(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/distinguish", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("how", "yes")
|
||||
form.Set("sticky", "true")
|
||||
form.Set("id", "t1_123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.DistinguishAndSticky(ctx, "t1_123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestModerationService_Undistinguish(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/api/distinguish", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("how", "no")
|
||||
form.Set("id", "t1_123")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Moderation.Undistinguish(ctx, "t1_123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
*n = make(SubredditNames, len(subreddits))
|
||||
for i, sr := range subreddits {
|
||||
(*n)[i] = sr.Name
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (n *SubredditNames) MarshalJSON() ([]byte, error) {
|
||||
type subreddit struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
subreddits := make([]subreddit, 0)
|
||||
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"`
|
||||
}
|
||||
|
||||
type rootMultiDescription struct {
|
||||
Body string `json:"body_md"`
|
||||
}
|
||||
|
||||
// Get 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)
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
multi, _ := t.Multi()
|
||||
return multi, 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
|
||||
}
|
||||
|
||||
root := new(things)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.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
|
||||
}
|
||||
|
||||
root := new(things)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Multis, resp, nil
|
||||
}
|
||||
|
||||
// Copy a multireddit.
|
||||
func (s *MultiService) Copy(ctx context.Context, copyRequest *MultiCopyRequest) (*Multi, *Response, error) {
|
||||
if copyRequest == nil {
|
||||
return nil, nil, errors.New("*MultiCopyRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := "api/multi/copy"
|
||||
form, err := query.Values(copyRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(thing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
multi, _ := root.Multi()
|
||||
return multi, resp, nil
|
||||
}
|
||||
|
||||
// Create a multireddit.
|
||||
func (s *MultiService) Create(ctx context.Context, createRequest *MultiCreateOrUpdateRequest) (*Multi, *Response, error) {
|
||||
if createRequest == nil {
|
||||
return nil, nil, errors.New("*MultiCreateOrUpdateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
byteValue, err := json.Marshal(createRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("model", string(byteValue))
|
||||
|
||||
path := "api/multi"
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(thing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
multi, _ := root.Multi()
|
||||
return multi, resp, nil
|
||||
}
|
||||
|
||||
// Update 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("*MultiCreateOrUpdateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
byteValue, err := json.Marshal(updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("model", string(byteValue))
|
||||
|
||||
path := fmt.Sprintf("api/multi/%s", multiPath)
|
||||
req, err := s.client.NewRequest(http.MethodPut, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(thing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
multi, _ := root.Multi()
|
||||
return multi, resp, nil
|
||||
}
|
||||
|
||||
// Delete 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)
|
||||
}
|
||||
|
||||
// Description gets a multireddit's description.
|
||||
func (s *MultiService) Description(ctx context.Context, multiPath string) (string, *Response, error) {
|
||||
path := fmt.Sprintf("api/multi/%s/description", multiPath)
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
multiDescription, _ := t.MultiDescription()
|
||||
return multiDescription, resp, nil
|
||||
}
|
||||
|
||||
// UpdateDescription updates a multireddit's description.
|
||||
func (s *MultiService) UpdateDescription(ctx context.Context, multiPath string, description string) (string, *Response, error) {
|
||||
form := url.Values{}
|
||||
form.Set("model", fmt.Sprintf(`{"body_md":"%s"}`, description))
|
||||
|
||||
path := fmt.Sprintf("api/multi/%s/description", multiPath)
|
||||
req, err := s.client.NewRequest(http.MethodPut, path, form)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
root := new(thing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return "", resp, err
|
||||
}
|
||||
|
||||
multiDescription, _ := root.MultiDescription()
|
||||
return multiDescription, 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.NewRequest(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,280 @@
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Multi.Copy(ctx, nil)
|
||||
require.EqualError(t, err, "*MultiCopyRequest: 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, "*MultiCreateOrUpdateRequest: 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, "*MultiCreateOrUpdateRequest: 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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_Description(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.Description(ctx, "user/testuser/m/testmulti")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "hello world", description)
|
||||
}
|
||||
|
||||
func TestMultiService_UpdateDescription(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Multi.AddSubreddit(ctx, "user/testuser/m/testmulti", "golang")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMultiService_DeleteSubreddit(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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,182 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// 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 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Save 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unsave 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.NewRequest(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.NewRequest(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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Lock 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unlock 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.NewRequest(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", strconv.Itoa(int(vote)))
|
||||
form.Set("rank", "10")
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Upvote a post or a comment.
|
||||
func (s *postAndCommentService) Upvote(ctx context.Context, id string) (*Response, error) {
|
||||
return s.vote(ctx, id, upvote)
|
||||
}
|
||||
|
||||
// Downvote 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)
|
||||
}
|
||||
|
||||
// Report a post or comment.
|
||||
// The reason must not be longer than 100 characters.
|
||||
func (s *postAndCommentService) Report(ctx context.Context, id string, reason string) (*Response, error) {
|
||||
path := "api/report"
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("api_type", "json")
|
||||
form.Set("thing_id", id)
|
||||
form.Set("reason", reason)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,582 @@
|
||||
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 rootSubmittedPost 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"`
|
||||
}
|
||||
|
||||
// SubmitTextRequest are options used for text posts.
|
||||
type SubmitTextRequest 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"`
|
||||
}
|
||||
|
||||
// SubmitLinkRequest are options used for link posts.
|
||||
type SubmitLinkRequest 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 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, subreddit string) (*PostAndComments, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/comments/%s/", subreddit, 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, []*Post, *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]thing
|
||||
resp, err := s.client.Do(ctx, req, &root)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
|
||||
listing1, _ := root[0].Listing()
|
||||
listing2, _ := root[1].Listing()
|
||||
|
||||
post := listing1.Posts()[0]
|
||||
duplicates := listing2.Posts()
|
||||
|
||||
resp.After = listing2.After()
|
||||
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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootSubmittedPost)
|
||||
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 SubmitTextRequest) (*Submitted, *Response, error) {
|
||||
form := struct {
|
||||
SubmitTextRequest
|
||||
Kind string `url:"kind,omitempty"`
|
||||
}{opts, "self"}
|
||||
return s.submit(ctx, form)
|
||||
}
|
||||
|
||||
// SubmitLink submits a link post.
|
||||
func (s *PostService) SubmitLink(ctx context.Context, opts SubmitLinkRequest) (*Submitted, *Response, error) {
|
||||
form := struct {
|
||||
SubmitLinkRequest
|
||||
Kind string `url:"kind,omitempty"`
|
||||
}{opts, "link"}
|
||||
return s.submit(ctx, form)
|
||||
}
|
||||
|
||||
// Edit 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.NewRequest(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 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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Unhide 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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Sticky 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.NewRequest(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.NewRequest(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", strconv.Itoa(pos))
|
||||
|
||||
req, err := s.client.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(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.NewRequest(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("*PostAndComments: 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.NewRequest(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.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
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 {
|
||||
httpClient := &http.Client{Transport: client.client.Transport}
|
||||
ctx := context.WithValue(context.Background(), 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: client.client.Transport,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Opt is used to further configure a client upon initialization.
|
||||
type Opt func(*Client) error
|
||||
|
||||
// WithHTTPClient sets the HTTP client which will be used to make requests.
|
||||
func WithHTTPClient(httpClient *http.Client) Opt {
|
||||
return func(c *Client) error {
|
||||
if httpClient == nil {
|
||||
return errors.New("*http.Client: cannot be nil")
|
||||
}
|
||||
c.client = httpClient
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserAgent sets the User-Agent header for requests made with the client.
|
||||
// Reddit recommends the following format for the user agent:
|
||||
// <platform>:<app ID>:<version string> (by /u/<reddit username>)
|
||||
func WithUserAgent(ua string) Opt {
|
||||
return func(c *Client) error {
|
||||
c.userAgent = ua
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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 := os.Getenv("GO_REDDIT_CLIENT_ID"); v != "" {
|
||||
c.ID = v
|
||||
}
|
||||
if v := os.Getenv("GO_REDDIT_CLIENT_SECRET"); v != "" {
|
||||
c.Secret = v
|
||||
}
|
||||
if v := os.Getenv("GO_REDDIT_CLIENT_USERNAME"); v != "" {
|
||||
c.Username = v
|
||||
}
|
||||
if v := os.Getenv("GO_REDDIT_CLIENT_PASSWORD"); v != "" {
|
||||
c.Password = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWithHTTPClient(t *testing.T) {
|
||||
_, err := NewClient(Credentials{}, WithHTTPClient(nil))
|
||||
require.EqualError(t, err, "*http.Client: cannot be nil")
|
||||
|
||||
_, err = NewClient(Credentials{}, WithHTTPClient(&http.Client{}))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWithUserAgent(t *testing.T) {
|
||||
c, err := NewClient(Credentials{}, WithUserAgent("test"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test", c.UserAgent())
|
||||
|
||||
c, err = NewClient(Credentials{}, WithUserAgent(""))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, fmt.Sprintf("golang:%s:v%s", libraryName, libraryVersion), c.UserAgent())
|
||||
}
|
||||
|
||||
func TestWithBaseURL(t *testing.T) {
|
||||
c, err := NewClient(Credentials{}, WithBaseURL(":"))
|
||||
urlErr, ok := err.(*url.Error)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "parse", urlErr.Op)
|
||||
|
||||
baseURL := "http://localhost:8080"
|
||||
c, err = NewClient(Credentials{}, WithBaseURL(baseURL))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, baseURL, c.BaseURL.String())
|
||||
}
|
||||
|
||||
func TestWithTokenURL(t *testing.T) {
|
||||
c, err := NewClient(Credentials{}, WithTokenURL(":"))
|
||||
urlErr, ok := err.(*url.Error)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "parse", urlErr.Op)
|
||||
|
||||
tokenURL := "http://localhost:8080/api/v1/access_token"
|
||||
c, err = NewClient(Credentials{}, WithTokenURL(tokenURL))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tokenURL, c.TokenURL.String())
|
||||
}
|
||||
|
||||
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(Credentials{}, FromEnv)
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package reddit
|
||||
|
||||
import "net/http"
|
||||
|
||||
// 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.
|
||||
// We need to set a custom user agent because using the one set by the
|
||||
// stdlib gives us 429 Too Many Requests responses from the Reddit API.
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,671 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/enetx/surf"
|
||||
"github.com/google/go-querystring/query"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
|
||||
const (
|
||||
libraryName = "git.the-red-comet.org/guy176251/go-reddit"
|
||||
libraryVersion = "2.0.0"
|
||||
|
||||
defaultBaseURL = "https://oauth.reddit.com"
|
||||
defaultBaseURLReadonly = "https://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"
|
||||
|
||||
headerRateLimitRemaining = "X-Ratelimit-Remaining"
|
||||
headerRateLimitUsed = "X-Ratelimit-Used"
|
||||
headerRateLimitReset = "X-Ratelimit-Reset"
|
||||
)
|
||||
|
||||
var defaultClient, _ = NewReadonlyClient()
|
||||
|
||||
// DefaultClient returns a valid, read-only client with limited access to the Reddit API.
|
||||
func DefaultClient() *Client {
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
// RequestCompletionCallback defines the type of the request callback function.
|
||||
type RequestCompletionCallback func(*http.Request, *http.Response)
|
||||
|
||||
// Credentials are used to authenticate to make requests to the Reddit API.
|
||||
type Credentials struct {
|
||||
ID string
|
||||
Secret string
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
rateMu sync.Mutex
|
||||
rate Rate
|
||||
|
||||
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
|
||||
Gold *GoldService
|
||||
Listings *ListingsService
|
||||
LiveThread *LiveThreadService
|
||||
Message *MessageService
|
||||
Moderation *ModerationService
|
||||
Multi *MultiService
|
||||
Post *PostService
|
||||
Stream *StreamService
|
||||
Subreddit *SubredditService
|
||||
User *UserService
|
||||
Widget *WidgetService
|
||||
Wiki *WikiService
|
||||
|
||||
oauth2Transport *oauth2.Transport
|
||||
|
||||
onRequestCompleted RequestCompletionCallback
|
||||
}
|
||||
|
||||
// OnRequestCompleted sets the client's request completion callback.
|
||||
func (c *Client) OnRequestCompleted(rc RequestCompletionCallback) {
|
||||
c.onRequestCompleted = rc
|
||||
}
|
||||
|
||||
func newClient() *Client {
|
||||
baseURL, _ := url.Parse(defaultBaseURL)
|
||||
tokenURL, _ := url.Parse(defaultTokenURL)
|
||||
|
||||
surf_client := surf.NewClient().
|
||||
Builder().
|
||||
Impersonate().
|
||||
Windows().
|
||||
Firefox().
|
||||
Session().
|
||||
Build().
|
||||
Unwrap().
|
||||
Std()
|
||||
|
||||
client := &Client{client: surf_client, BaseURL: baseURL, TokenURL: tokenURL}
|
||||
|
||||
client.Account = &AccountService{client: client}
|
||||
client.Collection = &CollectionService{client: client}
|
||||
client.Emoji = &EmojiService{client: client}
|
||||
client.Flair = &FlairService{client: client}
|
||||
client.Gold = &GoldService{client: client}
|
||||
client.Listings = &ListingsService{client: client}
|
||||
client.LiveThread = &LiveThreadService{client: client}
|
||||
client.Message = &MessageService{client: client}
|
||||
client.Moderation = &ModerationService{client: client}
|
||||
client.Multi = &MultiService{client: client}
|
||||
client.Stream = &StreamService{client: client}
|
||||
client.Subreddit = &SubredditService{client: client}
|
||||
client.User = &UserService{client: client}
|
||||
client.Widget = &WidgetService{client: client}
|
||||
client.Wiki = &WikiService{client: client}
|
||||
|
||||
postAndCommentService := &postAndCommentService{client: client}
|
||||
client.Comment = &CommentService{client: client, postAndCommentService: postAndCommentService}
|
||||
client.Post = &PostService{client: client, postAndCommentService: postAndCommentService}
|
||||
|
||||
return client
|
||||
}
|
||||
|
||||
// NewClient returns a new Reddit API client.
|
||||
// Use an Opt to configure the client credentials, such as WithHTTPClient or WithUserAgent.
|
||||
// If the FromEnv option is used with the correct environment variables, an empty struct can
|
||||
// be passed in as the credentials, since they will be overridden.
|
||||
func NewClient(credentials Credentials, opts ...Opt) (*Client, error) {
|
||||
client := newClient()
|
||||
|
||||
client.ID = credentials.ID
|
||||
client.Secret = credentials.Secret
|
||||
client.Username = credentials.Username
|
||||
client.Password = credentials.Password
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
userAgentTransport := &userAgentTransport{
|
||||
userAgent: client.UserAgent(),
|
||||
Base: client.client.Transport,
|
||||
}
|
||||
client.client.Transport = userAgentTransport
|
||||
|
||||
if client.client.CheckRedirect == nil {
|
||||
client.client.CheckRedirect = client.redirect
|
||||
}
|
||||
|
||||
oauthTransport := oauthTransport(client)
|
||||
client.client.Transport = oauthTransport
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// NewReadonlyClient returns a new read-only Reddit API client.
|
||||
// The client will have limited access to the Reddit API.
|
||||
// Options that modify credentials (such as FromEnv) won't have any effect on this client.
|
||||
func NewReadonlyClient(opts ...Opt) (*Client, error) {
|
||||
client := newClient()
|
||||
client.BaseURL, _ = url.Parse(defaultBaseURLReadonly)
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(client); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if client.client == nil {
|
||||
client.client = &http.Client{}
|
||||
}
|
||||
|
||||
userAgentTransport := &userAgentTransport{
|
||||
userAgent: client.UserAgent(),
|
||||
Base: client.client.Transport,
|
||||
}
|
||||
client.client.Transport = userAgentTransport
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
func (c *Client) redirect(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
|
||||
}
|
||||
|
||||
// The readonly Reddit url needs .json at the end of its path to return responses in JSON instead of HTML.
|
||||
func (c *Client) appendJSONExtensionToRequestURLPath(req *http.Request) {
|
||||
readonlyURL, err := url.Parse(defaultBaseURLReadonly)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if req.URL.Host != readonlyURL.Host {
|
||||
return
|
||||
}
|
||||
|
||||
req.URL.Path += ".json"
|
||||
}
|
||||
|
||||
// UserAgent returns the client's user agent.
|
||||
func (c *Client) UserAgent() string {
|
||||
if c.userAgent == "" {
|
||||
userAgent := fmt.Sprintf("golang:%s:v%s", libraryName, libraryVersion)
|
||||
if c.Username != "" {
|
||||
userAgent += fmt.Sprintf(" (by /u/%s)", c.Username)
|
||||
}
|
||||
c.userAgent = userAgent
|
||||
}
|
||||
return c.userAgent
|
||||
}
|
||||
|
||||
// NewRequest creates an API request with form data as the body.
|
||||
// The path is the relative URL which will be resolved to the BaseURL of the Client.
|
||||
// It should always be specified without a preceding slash.
|
||||
func (c *Client) NewRequest(method string, path string, form url.Values) (*http.Request, error) {
|
||||
u, err := c.BaseURL.Parse(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if form != nil {
|
||||
body = strings.NewReader(form.Encode())
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, u.String(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.appendJSONExtensionToRequestURLPath(req)
|
||||
req.Header.Add(headerContentType, mediaTypeForm)
|
||||
req.Header.Add(headerAccept, mediaTypeJSON)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// NewJSONRequest creates an API request with a JSON body.
|
||||
// The path is the relative URL which will be resolved to the BaseURL of the Client.
|
||||
// It should always be specified without a preceding slash.
|
||||
func (c *Client) NewJSONRequest(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
|
||||
}
|
||||
|
||||
c.appendJSONExtensionToRequestURLPath(req)
|
||||
req.Header.Add(headerContentType, mediaTypeJSON)
|
||||
req.Header.Add(headerAccept, mediaTypeJSON)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// Response is a Reddit response. This wraps the standard http.Response returned from Reddit.
|
||||
type Response struct {
|
||||
*http.Response
|
||||
|
||||
// Pagination anchor indicating there are more results after this id.
|
||||
After string
|
||||
|
||||
// Rate limit information.
|
||||
Rate Rate
|
||||
}
|
||||
|
||||
// newResponse creates a new Response for the provided http.Response.
|
||||
func newResponse(r *http.Response) *Response {
|
||||
response := Response{Response: r}
|
||||
response.Rate = parseRate(r)
|
||||
return &response
|
||||
}
|
||||
|
||||
func (r *Response) populateAnchors(a anchor) {
|
||||
r.After = a.After()
|
||||
}
|
||||
|
||||
// parseRate parses the rate related headers.
|
||||
func parseRate(r *http.Response) Rate {
|
||||
var rate Rate
|
||||
if remaining := r.Header.Get(headerRateLimitRemaining); remaining != "" {
|
||||
v, _ := strconv.ParseFloat(remaining, 64)
|
||||
rate.Remaining = int(v)
|
||||
}
|
||||
if used := r.Header.Get(headerRateLimitUsed); used != "" {
|
||||
rate.Used, _ = strconv.Atoi(used)
|
||||
}
|
||||
if reset := r.Header.Get(headerRateLimitReset); reset != "" {
|
||||
if v, _ := strconv.ParseInt(reset, 10, 64); v != 0 {
|
||||
rate.Reset = time.Now().Truncate(time.Second).Add(time.Second * time.Duration(v))
|
||||
}
|
||||
}
|
||||
return rate
|
||||
}
|
||||
|
||||
// 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) {
|
||||
slog.Debug("Making API request to reddit", "url", req.URL.String(), "method", req.Method)
|
||||
if err := c.checkRateLimitBeforeDo(req); err != nil {
|
||||
return &Response{
|
||||
Response: err.Response,
|
||||
Rate: err.Rate,
|
||||
}, err
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithClient(ctx, c.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if c.onRequestCompleted != nil {
|
||||
c.onRequestCompleted(req, resp)
|
||||
}
|
||||
|
||||
response := newResponse(resp)
|
||||
|
||||
c.rateMu.Lock()
|
||||
c.rate = response.Rate
|
||||
c.rateMu.Unlock()
|
||||
|
||||
err = CheckResponse(resp)
|
||||
if err != nil || v == nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
body, err := ResponseReader(resp)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
w, ok := v.(io.Writer)
|
||||
if ok {
|
||||
_, err = io.Copy(w, body)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
} else {
|
||||
err = json.NewDecoder(body).Decode(v)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
|
||||
if anchor, ok := v.(anchor); ok {
|
||||
response.populateAnchors(anchor)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (c *Client) checkRateLimitBeforeDo(req *http.Request) *RateLimitError {
|
||||
c.rateMu.Lock()
|
||||
rate := c.rate
|
||||
c.rateMu.Unlock()
|
||||
|
||||
if !rate.Reset.IsZero() && rate.Remaining == 0 && time.Now().Before(rate.Reset) {
|
||||
// Create a fake 429 response.
|
||||
resp := &http.Response{
|
||||
Status: http.StatusText(http.StatusTooManyRequests),
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
Request: req,
|
||||
Header: make(http.Header),
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
}
|
||||
return &RateLimitError{
|
||||
Rate: rate,
|
||||
Response: resp,
|
||||
Message: fmt.Sprintf("API rate limit still exceeded until %s, not making remote request.", rate.Reset),
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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", kindUser, 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 {
|
||||
if r.Header.Get(headerRateLimitRemaining) == "0" {
|
||||
err := &RateLimitError{
|
||||
Rate: parseRate(r),
|
||||
Response: r,
|
||||
}
|
||||
err.Message = fmt.Sprintf("API rate limit has been exceeded until %s.", err.Rate.Reset)
|
||||
return err
|
||||
}
|
||||
|
||||
jsonErrorResponse := &JSONErrorResponse{Response: r}
|
||||
|
||||
data, err := io.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 = io.NopCloser(bytes.NewBuffer(data))
|
||||
|
||||
if c := r.StatusCode; c >= 200 && c <= 299 {
|
||||
return nil
|
||||
}
|
||||
|
||||
errorResponse := &ErrorResponse{Response: r}
|
||||
data, err = io.ReadAll(r.Body)
|
||||
if err == nil && len(data) > 0 {
|
||||
err := json.Unmarshal(data, errorResponse)
|
||||
if err != nil {
|
||||
errorResponse.Message = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
return errorResponse
|
||||
}
|
||||
|
||||
// Rate represents the rate limit for the client.
|
||||
type Rate struct {
|
||||
// The number of remaining requests the client can make in the current 10-minute window.
|
||||
Remaining int `json:"remaining"`
|
||||
// The number of requests the client has made in the current 10-minute window.
|
||||
Used int `json:"used"`
|
||||
// The time at which the current rate limit will reset.
|
||||
Reset time.Time `json:"reset"`
|
||||
}
|
||||
|
||||
// A lot of Reddit's responses return a "thing": { "kind": "...", "data": {...} }
|
||||
// So this is just a nice convenient method to have.
|
||||
func (c *Client) getThing(ctx context.Context, path string, opts interface{}) (*thing, *Response, error) {
|
||||
path, err := addOptions(path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
req, err := c.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
t := new(thing)
|
||||
resp, err := c.Do(ctx, req, t)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return t, resp, nil
|
||||
}
|
||||
|
||||
func (c *Client) getListing(ctx context.Context, path string, opts interface{}) (*listing, *Response, error) {
|
||||
t, resp, err := c.getThing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
l, _ := t.Listing()
|
||||
return l, resp, nil
|
||||
}
|
||||
|
||||
// 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"`
|
||||
|
||||
IncludeNsfw bool `url:"include_over_18,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"`
|
||||
|
||||
IncludeNsfw bool `url:"include_over_18,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,295 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var ctx = context.Background()
|
||||
|
||||
func setup(t testing.TB) (*Client, *http.ServeMux) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
server := httptest.NewServer(mux)
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
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(
|
||||
Credentials{"id1", "secret1", "user1", "password1"},
|
||||
WithBaseURL(server.URL),
|
||||
WithTokenURL(server.URL+"/api/v1/access_token"),
|
||||
)
|
||||
|
||||
return client, mux
|
||||
}
|
||||
|
||||
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",
|
||||
"Gold",
|
||||
"Listings",
|
||||
"LiveThread",
|
||||
"Message",
|
||||
"Moderation",
|
||||
"Multi",
|
||||
"Post",
|
||||
"Stream",
|
||||
"Subreddit",
|
||||
"User",
|
||||
"Widget",
|
||||
"Wiki",
|
||||
}
|
||||
|
||||
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", 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(Credentials{})
|
||||
require.NoError(t, err)
|
||||
testClientDefaults(t, c)
|
||||
}
|
||||
|
||||
func TestNewClient_Error(t *testing.T) {
|
||||
_, err := NewClient(Credentials{})
|
||||
require.NoError(t, err)
|
||||
|
||||
errorOpt := func(c *Client) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
|
||||
_, err = NewClient(Credentials{}, errorOpt)
|
||||
require.EqualError(t, err, "foo")
|
||||
}
|
||||
|
||||
func TestNewReadonlyClient(t *testing.T) {
|
||||
c, err := NewReadonlyClient()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.BaseURL.String(), defaultBaseURLReadonly)
|
||||
}
|
||||
|
||||
func TestNewReadonlyClient_Error(t *testing.T) {
|
||||
_, err := NewReadonlyClient()
|
||||
require.NoError(t, err)
|
||||
|
||||
errorOpt := func(c *Client) error {
|
||||
return errors.New("foo")
|
||||
}
|
||||
|
||||
_, err = NewReadonlyClient(errorOpt)
|
||||
require.EqualError(t, err, "foo")
|
||||
}
|
||||
|
||||
func TestDefaultClient(t *testing.T) {
|
||||
require.NotNil(t, DefaultClient())
|
||||
}
|
||||
|
||||
func TestClient_Readonly_NewRequest(t *testing.T) {
|
||||
c, err := NewReadonlyClient()
|
||||
require.NoError(t, err)
|
||||
|
||||
req, err := c.NewRequest(http.MethodGet, "r/golang", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, defaultBaseURLReadonly+"/r/golang.json", req.URL.String())
|
||||
}
|
||||
|
||||
func TestClient_OnRequestComplemented(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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`, client.BaseURL))
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestClient_ErrorResponse(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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`, client.BaseURL))
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestClient_Do_RateLimitError(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
var counter int
|
||||
mux.HandleFunc("/api/v1/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
defer func() { counter++ }()
|
||||
|
||||
switch counter {
|
||||
case 0:
|
||||
w.Header().Set(headerRateLimitRemaining, "500")
|
||||
w.Header().Set(headerRateLimitUsed, "100")
|
||||
w.Header().Set(headerRateLimitReset, "120")
|
||||
case 1:
|
||||
w.Header().Set(headerRateLimitRemaining, "0")
|
||||
w.Header().Set(headerRateLimitUsed, "600")
|
||||
w.Header().Set(headerRateLimitReset, "240")
|
||||
}
|
||||
})
|
||||
|
||||
req, err := client.NewRequest(http.MethodGet, "api/v1/test", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
client.rate.Remaining = 0
|
||||
client.rate.Reset = time.Now().Add(time.Minute)
|
||||
|
||||
resp, err := client.Do(ctx, req, nil)
|
||||
require.Equal(t, 0, counter)
|
||||
require.IsType(t, &RateLimitError{}, err)
|
||||
require.Equal(t, http.StatusTooManyRequests, resp.StatusCode)
|
||||
|
||||
client.rate = Rate{}
|
||||
|
||||
resp, err = client.Do(ctx, req, nil)
|
||||
require.Equal(t, 1, counter)
|
||||
require.Nil(t, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, 500, resp.Rate.Remaining)
|
||||
require.Equal(t, 100, resp.Rate.Used)
|
||||
require.Equal(t, time.Now().Truncate(time.Second).Add(time.Minute*2), resp.Rate.Reset)
|
||||
|
||||
resp, err = client.Do(ctx, req, nil)
|
||||
require.Equal(t, 2, counter)
|
||||
require.IsType(t, &RateLimitError{}, err)
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, 0, resp.Rate.Remaining)
|
||||
require.Equal(t, 600, resp.Rate.Used)
|
||||
require.Equal(t, time.Now().Truncate(time.Second).Add(time.Minute*4), resp.Rate.Reset)
|
||||
}
|
||||
|
||||
func TestClient_Do_Gzip(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
expected := map[string]string{
|
||||
"message": "Hello!",
|
||||
}
|
||||
mux.HandleFunc("/api/v1/test", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Encoding", "gzip")
|
||||
b, _ := json.Marshal(&expected)
|
||||
zw := gzip.NewWriter(w)
|
||||
zw.Write(b)
|
||||
zw.Close()
|
||||
})
|
||||
req, err := client.NewRequest(http.MethodGet, "api/v1/test", nil)
|
||||
require.NoError(t, err)
|
||||
result := map[string]string{}
|
||||
_, err = client.Do(ctx, req, &result)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, result, expected)
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StreamService allows streaming new content from Reddit as it appears.
|
||||
type StreamService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Posts streams posts from the specified subreddit.
|
||||
// It returns 2 channels and a function:
|
||||
// - a channel into which new posts will be sent
|
||||
// - a channel into which any errors will be sent
|
||||
// - a function that the client can call once to stop the streaming and close the channels
|
||||
//
|
||||
// Because of the 100 post limit imposed by Reddit when fetching posts, some high-traffic
|
||||
// streams might drop submissions between API requests, such as when streaming r/all.
|
||||
func (s *StreamService) Posts(subreddit string, opts ...StreamOpt) (<-chan *Post, <-chan error, func()) {
|
||||
streamConfig := &streamConfig{
|
||||
Interval: defaultStreamInterval,
|
||||
DiscardInitial: false,
|
||||
MaxRequests: 0,
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(streamConfig)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(streamConfig.Interval)
|
||||
postsCh := make(chan *Post)
|
||||
errsCh := make(chan error)
|
||||
|
||||
var once sync.Once
|
||||
stop := func() {
|
||||
once.Do(func() {
|
||||
ticker.Stop()
|
||||
close(postsCh)
|
||||
close(errsCh)
|
||||
})
|
||||
}
|
||||
|
||||
// originally used the "before" parameter, but if that post gets deleted, subsequent requests
|
||||
// would just return empty listings; easier to just keep track of all post ids encountered
|
||||
ids := set{}
|
||||
|
||||
go func() {
|
||||
defer stop()
|
||||
|
||||
var n int
|
||||
infinite := streamConfig.MaxRequests == 0
|
||||
|
||||
for ; ; <-ticker.C {
|
||||
n++
|
||||
|
||||
posts, err := s.getPosts(subreddit)
|
||||
if err != nil {
|
||||
errsCh <- err
|
||||
if !infinite && n >= streamConfig.MaxRequests {
|
||||
break
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
for _, post := range posts {
|
||||
id := post.FullID
|
||||
|
||||
// if this post id is already part of the set, it means that it and the ones
|
||||
// after it in the list have already been streamed, so break out of the loop
|
||||
if ids.Exists(id) {
|
||||
break
|
||||
}
|
||||
ids.Add(id)
|
||||
|
||||
if streamConfig.DiscardInitial {
|
||||
streamConfig.DiscardInitial = false
|
||||
break
|
||||
}
|
||||
|
||||
postsCh <- post
|
||||
}
|
||||
|
||||
if !infinite && n >= streamConfig.MaxRequests {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return postsCh, errsCh, stop
|
||||
}
|
||||
|
||||
func (s *StreamService) getPosts(subreddit string) ([]*Post, error) {
|
||||
posts, _, err := s.client.Subreddit.NewPosts(context.Background(), subreddit, &ListOptions{Limit: 100})
|
||||
return posts, err
|
||||
}
|
||||
|
||||
type set map[string]struct{}
|
||||
|
||||
func (s set) Add(v string) {
|
||||
s[v] = struct{}{}
|
||||
}
|
||||
|
||||
func (s set) Delete(v string) {
|
||||
delete(s, v)
|
||||
}
|
||||
|
||||
func (s set) Len() int {
|
||||
return len(s)
|
||||
}
|
||||
|
||||
func (s set) Exists(v string) bool {
|
||||
_, ok := s[v]
|
||||
return ok
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestStreamService_Posts(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
var counter int
|
||||
mux.HandleFunc("/r/testsubreddit/new", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
defer func() { counter++ }()
|
||||
|
||||
switch counter {
|
||||
case 0:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
case 1:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
case 2:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post6"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
case 3:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post11"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post12"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
default:
|
||||
fmt.Fprint(w, `{}`)
|
||||
}
|
||||
})
|
||||
|
||||
posts, errs, stop := client.Stream.Posts("testsubreddit", StreamInterval(time.Millisecond*10), StreamMaxRequests(4))
|
||||
defer stop()
|
||||
|
||||
expectedPostIDs := []string{"t3_post1", "t3_post2", "t3_post3", "t3_post4", "t3_post5", "t3_post6", "t3_post7", "t3_post8", "t3_post9", "t3_post10", "t3_post11", "t3_post12"}
|
||||
var i int
|
||||
|
||||
loop:
|
||||
for i != len(expectedPostIDs) {
|
||||
select {
|
||||
case post, ok := <-posts:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
require.Equal(t, expectedPostIDs[i], post.FullID)
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
require.NoError(t, err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
require.Len(t, expectedPostIDs, i)
|
||||
}
|
||||
|
||||
func TestStreamService_Posts_DiscardInitial(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
var counter int
|
||||
mux.HandleFunc("/r/testsubreddit/new", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
defer func() { counter++ }()
|
||||
|
||||
switch counter {
|
||||
case 0:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
case 1:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post1"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
case 2:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post4"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post5"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post6"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
case 3:
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "Listing",
|
||||
"data": {
|
||||
"children": [
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post7"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post8"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post9"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post10"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post11"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "t3",
|
||||
"data": {
|
||||
"name": "t3_post12"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}`)
|
||||
default:
|
||||
fmt.Fprint(w, `{}`)
|
||||
}
|
||||
})
|
||||
|
||||
posts, errs, stop := client.Stream.Posts("testsubreddit", StreamInterval(time.Millisecond*10), StreamMaxRequests(4), StreamDiscardInitial)
|
||||
defer stop()
|
||||
|
||||
expectedPostIDs := []string{"t3_post3", "t3_post4", "t3_post5", "t3_post6", "t3_post7", "t3_post8", "t3_post9", "t3_post10", "t3_post11", "t3_post12"}
|
||||
var i int
|
||||
|
||||
loop:
|
||||
for i != len(expectedPostIDs) {
|
||||
select {
|
||||
case post, ok := <-posts:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
require.Equal(t, expectedPostIDs[i], post.FullID)
|
||||
case err, ok := <-errs:
|
||||
if !ok {
|
||||
break loop
|
||||
}
|
||||
require.NoError(t, err)
|
||||
}
|
||||
i++
|
||||
}
|
||||
|
||||
require.Len(t, expectedPostIDs, i)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package reddit
|
||||
|
||||
import "time"
|
||||
|
||||
const defaultStreamInterval = time.Second * 5
|
||||
|
||||
type streamConfig struct {
|
||||
Interval time.Duration
|
||||
DiscardInitial bool
|
||||
MaxRequests int
|
||||
}
|
||||
|
||||
// StreamOpt is a configuration option to configure a stream.
|
||||
type StreamOpt func(*streamConfig)
|
||||
|
||||
// StreamInterval sets the frequency at which data will be fetched for the stream.
|
||||
// If the duration is 0 or less, it will not be set and the default will be used.
|
||||
func StreamInterval(v time.Duration) StreamOpt {
|
||||
return func(c *streamConfig) {
|
||||
if v > 0 {
|
||||
c.Interval = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StreamDiscardInitial will discard data from the first fetch for the stream.
|
||||
func StreamDiscardInitial(c *streamConfig) {
|
||||
c.DiscardInitial = true
|
||||
}
|
||||
|
||||
// StreamMaxRequests sets a limit on the number of times data is fetched for a stream.
|
||||
// If less than or equal to 0, it is assumed to be infinite.
|
||||
func StreamMaxRequests(v int) StreamOpt {
|
||||
return func(c *streamConfig) {
|
||||
if v > 0 {
|
||||
c.MaxRequests = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Streamer streams data to the client.
|
||||
// type Streamer interface {
|
||||
// Stream() (<-chan *rootListing, <-chan error, func())
|
||||
// }
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,791 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
kindComment = "t1"
|
||||
kindUser = "t2"
|
||||
kindPost = "t3"
|
||||
kindMessage = "t4"
|
||||
kindSubreddit = "t5"
|
||||
kindTrophy = "t6"
|
||||
kindListing = "Listing"
|
||||
kindSubredditSettings = "subreddit_settings"
|
||||
kindKarmaList = "KarmaList"
|
||||
kindTrophyList = "TrophyList"
|
||||
kindUserList = "UserList"
|
||||
kindMore = "more"
|
||||
kindLiveThread = "LiveUpdateEvent"
|
||||
kindLiveThreadUpdate = "LiveUpdate"
|
||||
kindModAction = "modaction"
|
||||
kindMulti = "LabeledMulti"
|
||||
kindMultiDescription = "LabeledMultiDescription"
|
||||
kindWikiPage = "wikipage"
|
||||
kindWikiPageListing = "wikipagelisting"
|
||||
kindWikiPageSettings = "wikipagesettings"
|
||||
kindStyleSheet = "stylesheet"
|
||||
)
|
||||
|
||||
type anchor interface {
|
||||
After() string
|
||||
}
|
||||
|
||||
// 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 interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func (t *thing) After() string {
|
||||
if t == nil {
|
||||
return ""
|
||||
}
|
||||
a, ok := t.Data.(anchor)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return a.After()
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (t *thing) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
Kind string `json:"kind"`
|
||||
Data json.RawMessage `json:"data"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Kind = root.Kind
|
||||
var v interface{}
|
||||
|
||||
switch t.Kind {
|
||||
case kindListing:
|
||||
v = new(listing)
|
||||
case kindComment:
|
||||
v = new(Comment)
|
||||
case kindMore:
|
||||
v = new(More)
|
||||
case kindUser:
|
||||
v = new(User)
|
||||
case kindPost:
|
||||
v = new(Post)
|
||||
case kindSubreddit:
|
||||
v = new(Subreddit)
|
||||
case kindSubredditSettings:
|
||||
v = new(SubredditSettings)
|
||||
case kindLiveThread:
|
||||
v = new(LiveThread)
|
||||
case kindLiveThreadUpdate:
|
||||
v = new(LiveThreadUpdate)
|
||||
case kindModAction:
|
||||
v = new(ModAction)
|
||||
case kindMulti:
|
||||
v = new(Multi)
|
||||
case kindMultiDescription:
|
||||
v = new(rootMultiDescription)
|
||||
case kindTrophy:
|
||||
v = new(Trophy)
|
||||
case kindTrophyList:
|
||||
v = new(trophyList)
|
||||
case kindKarmaList:
|
||||
v = new([]*SubredditKarma)
|
||||
case kindWikiPage:
|
||||
v = new(WikiPage)
|
||||
case kindWikiPageListing:
|
||||
v = new([]string)
|
||||
case kindWikiPageSettings:
|
||||
v = new(WikiPageSettings)
|
||||
case kindStyleSheet:
|
||||
v = new(SubredditStyleSheet)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized kind: %q", t.Kind)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(root.Data, v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Data = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *thing) Listing() (v *listing, ok bool) {
|
||||
v, ok = t.Data.(*listing)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) Comment() (v *Comment, ok bool) {
|
||||
v, ok = t.Data.(*Comment)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) More() (v *More, ok bool) {
|
||||
v, ok = t.Data.(*More)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) User() (v *User, ok bool) {
|
||||
v, ok = t.Data.(*User)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) Post() (v *Post, ok bool) {
|
||||
v, ok = t.Data.(*Post)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) Subreddit() (v *Subreddit, ok bool) {
|
||||
v, ok = t.Data.(*Subreddit)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) SubredditSettings() (v *SubredditSettings, ok bool) {
|
||||
v, ok = t.Data.(*SubredditSettings)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) LiveThread() (v *LiveThread, ok bool) {
|
||||
v, ok = t.Data.(*LiveThread)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) LiveThreadUpdate() (v *LiveThreadUpdate, ok bool) {
|
||||
v, ok = t.Data.(*LiveThreadUpdate)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) ModAction() (v *ModAction, ok bool) {
|
||||
v, ok = t.Data.(*ModAction)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) Multi() (v *Multi, ok bool) {
|
||||
v, ok = t.Data.(*Multi)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) MultiDescription() (s string, ok bool) {
|
||||
v, ok := t.Data.(*rootMultiDescription)
|
||||
if ok {
|
||||
s = v.Body
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) Trophy() (v *Trophy, ok bool) {
|
||||
v, ok = t.Data.(*Trophy)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) TrophyList() ([]*Trophy, bool) {
|
||||
v, ok := t.Data.(*trophyList)
|
||||
if !ok {
|
||||
return nil, ok
|
||||
}
|
||||
return *v, ok
|
||||
}
|
||||
|
||||
func (t *thing) Karma() ([]*SubredditKarma, bool) {
|
||||
v, ok := t.Data.(*[]*SubredditKarma)
|
||||
if !ok {
|
||||
return nil, ok
|
||||
}
|
||||
return *v, ok
|
||||
}
|
||||
|
||||
func (t *thing) WikiPage() (v *WikiPage, ok bool) {
|
||||
v, ok = t.Data.(*WikiPage)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) WikiPages() ([]string, bool) {
|
||||
v, ok := t.Data.(*[]string)
|
||||
if !ok {
|
||||
return nil, ok
|
||||
}
|
||||
return *v, ok
|
||||
}
|
||||
|
||||
func (t *thing) WikiPageSettings() (v *WikiPageSettings, ok bool) {
|
||||
v, ok = t.Data.(*WikiPageSettings)
|
||||
return
|
||||
}
|
||||
|
||||
func (t *thing) StyleSheet() (v *SubredditStyleSheet, ok bool) {
|
||||
v, ok = t.Data.(*SubredditStyleSheet)
|
||||
return
|
||||
}
|
||||
|
||||
// listing is a list of things coming from the Reddit API.
|
||||
// It also contains the after anchor useful to get the next results via subsequent requests.
|
||||
type listing struct {
|
||||
things things
|
||||
after string
|
||||
}
|
||||
|
||||
func (l *listing) After() string {
|
||||
return l.after
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (l *listing) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
Things things `json:"children"`
|
||||
After string `json:"after"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l.things = root.Things
|
||||
l.after = root.After
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *listing) Comments() []*Comment {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.Comments
|
||||
}
|
||||
|
||||
func (l *listing) Mores() []*More {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.Mores
|
||||
}
|
||||
|
||||
func (l *listing) Users() []*User {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.Users
|
||||
}
|
||||
|
||||
func (l *listing) Posts() []*Post {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.Posts
|
||||
}
|
||||
|
||||
func (l *listing) Subreddits() []*Subreddit {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.Subreddits
|
||||
}
|
||||
|
||||
func (l *listing) ModActions() []*ModAction {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.ModActions
|
||||
}
|
||||
|
||||
func (l *listing) Multis() []*Multi {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.Multis
|
||||
}
|
||||
|
||||
func (l *listing) LiveThreads() []*LiveThread {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.LiveThreads
|
||||
}
|
||||
|
||||
func (l *listing) LiveThreadUpdates() []*LiveThreadUpdate {
|
||||
if l == nil {
|
||||
return nil
|
||||
}
|
||||
return l.things.LiveThreadUpdates
|
||||
}
|
||||
|
||||
type things struct {
|
||||
Comments []*Comment
|
||||
Mores []*More
|
||||
Users []*User
|
||||
Posts []*Post
|
||||
Subreddits []*Subreddit
|
||||
ModActions []*ModAction
|
||||
Multis []*Multi
|
||||
LiveThreads []*LiveThread
|
||||
LiveThreadUpdates []*LiveThreadUpdate
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (t *things) UnmarshalJSON(b []byte) error {
|
||||
var things []thing
|
||||
if err := json.Unmarshal(b, &things); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.add(things...)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *things) add(things ...thing) {
|
||||
for _, thing := range things {
|
||||
switch v := thing.Data.(type) {
|
||||
case *Comment:
|
||||
t.Comments = append(t.Comments, v)
|
||||
case *More:
|
||||
t.Mores = append(t.Mores, v)
|
||||
case *User:
|
||||
t.Users = append(t.Users, v)
|
||||
case *Post:
|
||||
t.Posts = append(t.Posts, v)
|
||||
case *Subreddit:
|
||||
t.Subreddits = append(t.Subreddits, v)
|
||||
case *ModAction:
|
||||
t.ModActions = append(t.ModActions, v)
|
||||
case *Multi:
|
||||
t.Multis = append(t.Multis, v)
|
||||
case *LiveThread:
|
||||
t.LiveThreads = append(t.LiveThreads, v)
|
||||
case *LiveThreadUpdate:
|
||||
t.LiveThreadUpdates = append(t.LiveThreadUpdates, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type trophyList []*Trophy
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (l *trophyList) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
Trophies []thing `json:"trophies"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = make(trophyList, 0, len(root.Trophies))
|
||||
for _, thing := range root.Trophies {
|
||||
if trophy, ok := thing.Trophy(); ok {
|
||||
*l = append(*l, trophy)
|
||||
}
|
||||
}
|
||||
|
||||
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"`
|
||||
|
||||
Distinguished string `json:"distinguished"`
|
||||
Depth int `json:"depth"`
|
||||
BodyHtml string `json:"body_html"`
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// Denotes whether the comment was written by a moderator
|
||||
func (c *Comment) IsModerator() bool {
|
||||
return c.Distinguished == "moderator"
|
||||
}
|
||||
|
||||
// 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(thing)
|
||||
err := json.Unmarshal(data, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listing, _ := root.Listing()
|
||||
|
||||
r.Comments = listing.Comments()
|
||||
if len(listing.Mores()) > 0 {
|
||||
r.More = listing.Mores()[0]
|
||||
}
|
||||
|
||||
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"`
|
||||
URLOverriddenByDest *string `json:"url_overridden_by_dest"`
|
||||
RemovedByCategory *string `json:"removed_by_category"`
|
||||
CrosspostParentList []Post `json:"crosspost_parent_list"`
|
||||
|
||||
Title string `json:"title,omitempty"`
|
||||
Body string `json:"selftext,omitempty"`
|
||||
SelftextHtml *string `json:"selftext_html"`
|
||||
|
||||
// Indicates if you've upvoted/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"`
|
||||
SubredditSubscribers int `json:"subreddit_subscribers"`
|
||||
|
||||
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"`
|
||||
|
||||
Thumbnail string `json:"thumbnail"`
|
||||
|
||||
MediaMetadata *MediaMetadata `json:"media_metadata,omitempty"`
|
||||
GalleryData *GalleryData `json:"gallery_data,omitempty"`
|
||||
|
||||
Preview *Preview `json:"preview,omitempty"`
|
||||
SecureMedia *SecureMedia `json:"secure_media,omitempty"`
|
||||
|
||||
PollData *PollData `json:"poll_data"`
|
||||
}
|
||||
|
||||
type PollDataInner struct {
|
||||
Timestamp int `json:"voting_end_timestamp"`
|
||||
Options []PollOption `json:"options"`
|
||||
IsPrediction bool `json:"is_prediction"`
|
||||
TotalVoteCount int `json:"total_vote_count"`
|
||||
}
|
||||
|
||||
type PollData struct {
|
||||
PollDataInner
|
||||
}
|
||||
|
||||
func (d *PollData) UnmarshalJSON(data []byte) error {
|
||||
var junk bool
|
||||
err := json.Unmarshal(data, &junk)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
inner := new(PollDataInner)
|
||||
err = json.Unmarshal(data, inner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.PollDataInner = *inner
|
||||
return nil
|
||||
}
|
||||
|
||||
type PollOption struct {
|
||||
Text string `json:"text"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type SecureMedia struct {
|
||||
RedditVideo RedditVideo `json:"reddit_video"`
|
||||
OEmbed OEmbed `json:"oembed"`
|
||||
}
|
||||
|
||||
type OEmbed struct {
|
||||
ProviderUrl string `json:"provider_url"`
|
||||
Version string `json:"version"`
|
||||
Title string `json:"title"`
|
||||
ThumbnailWidth int `json:"thumbnail_width"`
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
Html string `json:"html"`
|
||||
ProviderName string `json:"provider_name"`
|
||||
ThumbnailUrl string `json:"thumbnail_url"`
|
||||
Type string `json:"type"`
|
||||
ThumbnailHeight int `json:"thumbnail_height"`
|
||||
}
|
||||
|
||||
type RedditVideo struct {
|
||||
BitrateKbps int `json:"bitrate_kbps"`
|
||||
DashUrl string `json:"dash_url"`
|
||||
Duration int `json:"duration"`
|
||||
FallbackUrl string `json:"fallback_url"`
|
||||
HasAudio bool `json:"has_audio"`
|
||||
Height int `json:"height"`
|
||||
HlsUrl string `json:"hls_url"`
|
||||
IsGif bool `json:"is_gif"`
|
||||
ScrubberMediaUrl string `json:"scrubber_media_url"`
|
||||
TranscodingStatus string `json:"transcoding_status"`
|
||||
Width int `json:"width"`
|
||||
}
|
||||
|
||||
type Preview struct {
|
||||
Images []Image `json:"images"`
|
||||
Enabled bool `json:"enabled"`
|
||||
RedditVideoPreview *VideoPreview `json:"reddit_video_preview"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Source Resolution `json:"source"`
|
||||
Resolutions []Resolution `json:"resolutions"`
|
||||
Variants Variants `json:"variants"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type Resolution struct {
|
||||
Url string `json:"url"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
type Variants struct {
|
||||
Obfuscated *ImageVariant `json:"obfuscated"`
|
||||
Nsfw *ImageVariant `json:"nsfw"`
|
||||
Mp4 *ImageVariant `json:"mp4"`
|
||||
}
|
||||
|
||||
type ImageVariant struct {
|
||||
Source Resolution `json:"source"`
|
||||
Resolutions []Resolution `json:"resolutions"`
|
||||
}
|
||||
|
||||
type VideoPreview struct {
|
||||
BitrateKbps int `json:"bitrate_kbps"`
|
||||
FallbackUrl string `json:"fallback_url"`
|
||||
Height int `json:"height"`
|
||||
Width int `json:"width"`
|
||||
ScrubberMediaUrl string `json:"scrubber_media_url"`
|
||||
DashUrl string `json:"dash_url"`
|
||||
Duration int `json:"duration"`
|
||||
HlsUrl string `json:"hls_url"`
|
||||
IsGif bool `json:"is_gif"`
|
||||
TranscodingStatus string `json:"transcoding_status"`
|
||||
}
|
||||
|
||||
type GalleryData struct {
|
||||
Items []GalleryItem `json:"items"`
|
||||
}
|
||||
|
||||
type GalleryItem struct {
|
||||
MediaId string `json:"media_id"`
|
||||
Id int `json:"id"`
|
||||
}
|
||||
|
||||
type MediaMetadata map[string]Metadata
|
||||
|
||||
type Metadata struct {
|
||||
Status string `json:"status"`
|
||||
E string `json:"e"`
|
||||
M string `json:"m"`
|
||||
P []Picture `json:"p"`
|
||||
S Picture `json:"s"`
|
||||
Id string `json:"id"`
|
||||
}
|
||||
|
||||
type Picture struct {
|
||||
Y int `json:"y"`
|
||||
X int `json:"x"`
|
||||
U string `json:"u"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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 root [2]thing
|
||||
|
||||
err := json.Unmarshal(data, &root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
listing1, _ := root[0].Listing()
|
||||
listing2, _ := root[1].Listing()
|
||||
|
||||
pc.Post = listing1.Posts()[0]
|
||||
pc.Comments = listing2.Comments()
|
||||
if len(listing2.Mores()) > 0 {
|
||||
pc.More = listing2.Mores()[0]
|
||||
}
|
||||
|
||||
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,47 @@
|
||||
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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// 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)
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
user, _ := t.User()
|
||||
return user, 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) {
|
||||
params := struct {
|
||||
IDs []string `url:"ids,omitempty,comma"`
|
||||
}{ids}
|
||||
|
||||
path := "api/user_data_by_account_ids"
|
||||
path, err := addOptions(path, params)
|
||||
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) {
|
||||
params := struct {
|
||||
User string `url:"user"`
|
||||
}{username}
|
||||
|
||||
path := "api/username_available"
|
||||
path, err := addOptions(path, params)
|
||||
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) ([]*Post, []*Comment, *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) ([]*Post, []*Comment, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/overview", username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return l.Posts(), l.Comments(), resp, nil
|
||||
}
|
||||
|
||||
// Posts returns a list of your posts.
|
||||
func (s *UserService) Posts(ctx context.Context, opts *ListUserOverviewOptions) ([]*Post, *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) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/submitted", username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
|
||||
// Comments returns a list of your comments.
|
||||
func (s *UserService) Comments(ctx context.Context, opts *ListUserOverviewOptions) ([]*Comment, *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) ([]*Comment, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/comments", username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Comments(), resp, nil
|
||||
}
|
||||
|
||||
// Saved returns a list of the user's saved posts and comments.
|
||||
func (s *UserService) Saved(ctx context.Context, opts *ListUserOverviewOptions) ([]*Post, []*Comment, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/saved", s.client.Username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, nil, resp, err
|
||||
}
|
||||
return l.Posts(), l.Comments(), resp, nil
|
||||
}
|
||||
|
||||
// Upvoted returns a list of your upvoted posts.
|
||||
func (s *UserService) Upvoted(ctx context.Context, opts *ListUserOverviewOptions) ([]*Post, *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) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/upvoted", username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
|
||||
// Downvoted returns a list of your downvoted posts.
|
||||
func (s *UserService) Downvoted(ctx context.Context, opts *ListUserOverviewOptions) ([]*Post, *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) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/downvoted", username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
|
||||
// Hidden returns a list of the user's hidden posts.
|
||||
func (s *UserService) Hidden(ctx context.Context, opts *ListUserOverviewOptions) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/hidden", s.client.Username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
|
||||
// Gilded returns a list of the user's gilded posts.
|
||||
func (s *UserService) Gilded(ctx context.Context, opts *ListUserOverviewOptions) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("user/%s/gilded", s.client.Username)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), 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 a user.
|
||||
func (s *UserService) Friend(ctx context.Context, username string) (*Relationship, *Response, error) {
|
||||
body := struct {
|
||||
Username string `json:"name"`
|
||||
}{username}
|
||||
|
||||
path := fmt.Sprintf("api/v1/me/friends/%s", username)
|
||||
req, err := s.client.NewJSONRequest(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 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 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.NewRequest(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.NewRequest(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 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.NewRequest(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.NewRequest(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)
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
trophies, _ := t.TrophyList()
|
||||
return trophies, resp, nil
|
||||
}
|
||||
|
||||
// Popular gets the user subreddits with the most activity.
|
||||
func (s *UserService) Popular(ctx context.Context, opts *ListOptions) ([]*Subreddit, *Response, error) {
|
||||
path := "users/popular"
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Subreddits(), resp, nil
|
||||
}
|
||||
|
||||
// New gets the most recently created user subreddits.
|
||||
func (s *UserService) New(ctx context.Context, opts *ListUserOverviewOptions) ([]*Subreddit, *Response, error) {
|
||||
path := "users/new"
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Subreddits(), resp, nil
|
||||
}
|
||||
|
||||
// Search for users.
|
||||
// todo: maybe include the sort option? (relevance, activity)
|
||||
func (s *UserService) Search(ctx context.Context, query string, opts *ListOptions) ([]*User, *Response, error) {
|
||||
path := fmt.Sprintf("users/search?q=%s", query)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Users(), resp, nil
|
||||
}
|
||||
@@ -0,0 +1,925 @@
|
||||
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",
|
||||
SubredditSubscribers: 37829,
|
||||
|
||||
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 = []*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",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedSearchUsers = []*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,
|
||||
},
|
||||
}
|
||||
|
||||
func TestUserService_Get(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.Overview(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_OverviewOf(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.OverviewOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Overview_Options(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.Posts(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_PostsOf(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.PostsOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Posts_Options(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.Comments(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_CommentsOf(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.CommentsOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Comments_Options(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.User.Saved(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
|
||||
require.Len(t, comments, 1)
|
||||
require.Equal(t, expectedComment, comments[0])
|
||||
|
||||
require.Equal(t, "t1_f0zsa37", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Saved_Options(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.User.Upvoted(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Upvoted_Options(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.User.UpvotedOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Downvoted(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.User.Downvoted(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Downvoted_Options(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.User.DownvotedOf(ctx, "user2", nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Hidden(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.User.Hidden(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Gilded(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
// 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, resp, err := client.User.Gilded(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, posts, 1)
|
||||
require.Equal(t, expectedPost, posts[0])
|
||||
require.Equal(t, "t3_gczwql", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_GetFriendship(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
|
||||
var request struct {
|
||||
Username string `json:"name"`
|
||||
}
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(&request)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test123", request.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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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)
|
||||
})
|
||||
|
||||
resp, err := client.User.Unfriend(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, http.StatusNoContent, resp.StatusCode)
|
||||
}
|
||||
|
||||
func TestUserService_Block(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.User.Unblock(ctx, "test123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_UnblockByID(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.User.UnblockByID(ctx, "abc123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUserService_Trophies(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.Popular(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUserSubreddits, userSubreddits)
|
||||
require.Equal(t, "t5_3knn1", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_New(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.New(ctx, nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedUserSubreddits, userSubreddits)
|
||||
require.Equal(t, "t5_3knn1", resp.After)
|
||||
}
|
||||
|
||||
func TestUserService_Search(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
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, resp, err := client.User.Search(ctx, "test", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedSearchUsers, users)
|
||||
require.Equal(t, "t2_11kowl2w", resp.After)
|
||||
}
|
||||
@@ -0,0 +1,471 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// WidgetService handles communication with the widget
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_widgets
|
||||
type WidgetService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// Widget is a section of useful content on a subreddit.
|
||||
// They can feature information such as rules, links, the origins of the subreddit, etc.
|
||||
// Read about them here: https://mods.reddithelp.com/hc/en-us/articles/360010364372-Sidebar-Widgets
|
||||
type Widget interface {
|
||||
// kind returns the widget kind.
|
||||
// having un unexported method on an exported interface means it cannot be implemented by a client.
|
||||
kind() string
|
||||
// GetID returns the widget's id.
|
||||
GetID() string
|
||||
}
|
||||
|
||||
const (
|
||||
widgetKindTextArea = "textarea"
|
||||
widgetKindButton = "button"
|
||||
widgetKindImage = "image"
|
||||
widgetKindCommunityList = "community-list"
|
||||
widgetKindMenu = "menu"
|
||||
widgetKindCommunityDetails = "id-card"
|
||||
widgetKindModerators = "moderators"
|
||||
widgetKindSubredditRules = "subreddit-rules"
|
||||
widgetKindCustom = "custom"
|
||||
)
|
||||
|
||||
type rootWidget struct {
|
||||
Data Widget
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (w *rootWidget) UnmarshalJSON(data []byte) error {
|
||||
root := new(struct {
|
||||
Kind string `json:"kind"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(data, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch root.Kind {
|
||||
case widgetKindTextArea:
|
||||
w.Data = new(TextAreaWidget)
|
||||
case widgetKindButton:
|
||||
w.Data = new(ButtonWidget)
|
||||
case widgetKindImage:
|
||||
w.Data = new(ImageWidget)
|
||||
case widgetKindCommunityList:
|
||||
w.Data = new(CommunityListWidget)
|
||||
case widgetKindMenu:
|
||||
w.Data = new(MenuWidget)
|
||||
case widgetKindCommunityDetails:
|
||||
w.Data = new(CommunityDetailsWidget)
|
||||
case widgetKindModerators:
|
||||
w.Data = new(ModeratorsWidget)
|
||||
case widgetKindSubredditRules:
|
||||
w.Data = new(SubredditRulesWidget)
|
||||
case widgetKindCustom:
|
||||
w.Data = new(CustomWidget)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized widget kind: %q", root.Kind)
|
||||
}
|
||||
|
||||
return json.Unmarshal(data, w.Data)
|
||||
}
|
||||
|
||||
// WidgetList is a list of widgets.
|
||||
type WidgetList []Widget
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (l *WidgetList) UnmarshalJSON(data []byte) error {
|
||||
var widgetMap map[string]json.RawMessage
|
||||
err := json.Unmarshal(data, &widgetMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = make(WidgetList, 0, len(widgetMap))
|
||||
for _, w := range widgetMap {
|
||||
root := new(rootWidget)
|
||||
err = json.Unmarshal(w, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = append(*l, root.Data)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// common widget fields
|
||||
type widget struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Kind string `json:"kind,omitempty"`
|
||||
Style *WidgetStyle `json:"styles,omitempty"`
|
||||
}
|
||||
|
||||
func (w *widget) kind() string { return w.Kind }
|
||||
func (w *widget) GetID() string { return w.ID }
|
||||
|
||||
// TextAreaWidget displays a box of text in the subreddit.
|
||||
type TextAreaWidget struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
// ButtonWidget displays up to 10 button style links with customizable font colors for each button.
|
||||
type ButtonWidget struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Buttons []*WidgetButton `json:"buttons,omitempty"`
|
||||
}
|
||||
|
||||
// ImageWidget display a random image from up to 10 selected images.
|
||||
// The image can be clickable links.
|
||||
type ImageWidget struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Images []*WidgetImageLink `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// CommunityListWidget display a list of up to 10 other communities (subreddits).
|
||||
type CommunityListWidget struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Communities []*WidgetCommunity `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// MenuWidget displays tabs for your community's menu. These can be direct links or submenus that
|
||||
// create a drop-down menu to multiple links.
|
||||
type MenuWidget struct {
|
||||
widget
|
||||
|
||||
ShowWiki bool `json:"showWiki"`
|
||||
Links WidgetLinkList `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// CommunityDetailsWidget displays your subscriber count, users online, and community description,
|
||||
// as defined in your subreddit settings. You can customize the displayed text for subscribers and
|
||||
// users currently viewing the community.
|
||||
type CommunityDetailsWidget struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
|
||||
Subscribers int `json:"subscribersCount"`
|
||||
CurrentlyViewing int `json:"currentlyViewingCount"`
|
||||
|
||||
SubscribersText string `json:"subscribersText,omitempty"`
|
||||
CurrentlyViewingText string `json:"currentlyViewingText,omitempty"`
|
||||
}
|
||||
|
||||
// ModeratorsWidget displays the list of moderators of the subreddit.
|
||||
type ModeratorsWidget struct {
|
||||
widget
|
||||
|
||||
Mods []string `json:"mods"`
|
||||
Total int `json:"totalMods"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (w *ModeratorsWidget) UnmarshalJSON(data []byte) error {
|
||||
root := new(struct {
|
||||
widget
|
||||
|
||||
Mods []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"mods"`
|
||||
Total int `json:"totalMods"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(data, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.widget = root.widget
|
||||
w.Total = root.Total
|
||||
for _, mod := range root.Mods {
|
||||
w.Mods = append(w.Mods, mod.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SubredditRulesWidget displays your community rules.
|
||||
type SubredditRulesWidget struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName,omitempty"`
|
||||
// One of: full (includes description), compact (rule is collapsed).
|
||||
Display string `json:"display,omitempty"`
|
||||
Rules []string `json:"rules,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (w *SubredditRulesWidget) UnmarshalJSON(data []byte) error {
|
||||
root := new(struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName"`
|
||||
Display string `json:"display"`
|
||||
Rules []struct {
|
||||
Description string `json:"description"`
|
||||
} `json:"data"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(data, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w.widget = root.widget
|
||||
w.Name = root.Name
|
||||
w.Display = root.Display
|
||||
for _, r := range root.Rules {
|
||||
w.Rules = append(w.Rules, r.Description)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CustomWidget is a custom widget.
|
||||
type CustomWidget struct {
|
||||
widget
|
||||
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
|
||||
StyleSheet string `json:"css,omitempty"`
|
||||
StyleSheetURL string `json:"stylesheetUrl,omitempty"`
|
||||
Images []*WidgetImage `json:"imageData,omitempty"`
|
||||
}
|
||||
|
||||
// WidgetStyle contains style information for the widget.
|
||||
type WidgetStyle struct {
|
||||
HeaderColor string `json:"headerColor,omitempty"`
|
||||
BackgroundColor string `json:"backgroundColor,omitempty"`
|
||||
}
|
||||
|
||||
// WidgetImage is an image in a widget.
|
||||
type WidgetImage struct {
|
||||
Name string `json:"name"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
// WidgetLink is a link or a group of links that's part of a widget.
|
||||
type WidgetLink interface {
|
||||
// single returns whether or not the widget holds just one single link.
|
||||
// having un unexported method on an exported interface means it cannot be implemented by a client.
|
||||
single() bool
|
||||
}
|
||||
|
||||
// WidgetLinkSingle is a link that's part of a widget.
|
||||
type WidgetLinkSingle struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
func (l *WidgetLinkSingle) single() bool { return true }
|
||||
|
||||
// WidgetLinkMultiple is a dropdown of multiple links that's part of a widget.
|
||||
type WidgetLinkMultiple struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
URLs []*WidgetLinkSingle `json:"children,omitempty"`
|
||||
}
|
||||
|
||||
func (l *WidgetLinkMultiple) single() bool { return false }
|
||||
|
||||
// WidgetLinkList is a list of widgets links.
|
||||
type WidgetLinkList []WidgetLink
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (l *WidgetLinkList) UnmarshalJSON(data []byte) error {
|
||||
var dataMap []json.RawMessage
|
||||
err := json.Unmarshal(data, &dataMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = make(WidgetLinkList, 0, len(dataMap))
|
||||
for _, d := range dataMap {
|
||||
var widgetLinkDataMap map[string]json.RawMessage
|
||||
err = json.Unmarshal(d, &widgetLinkDataMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wl WidgetLink
|
||||
if _, ok := widgetLinkDataMap["children"]; ok {
|
||||
wl = new(WidgetLinkMultiple)
|
||||
} else {
|
||||
wl = new(WidgetLinkSingle)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(d, wl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*l = append(*l, wl)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WidgetImageLink is an image that links to an URL within a widget.
|
||||
type WidgetImageLink struct {
|
||||
URL string `json:"url,omitempty"`
|
||||
LinkURL string `json:"linkURL,omitempty"`
|
||||
}
|
||||
|
||||
// WidgetCommunity is a community (subreddit) that's displayed in a widget.
|
||||
type WidgetCommunity struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Subscribers int `json:"subscribers"`
|
||||
Subscribed bool `json:"isSubscribed"`
|
||||
NSFW bool `json:"isNSFW"`
|
||||
}
|
||||
|
||||
// WidgetButton is a button that's part of a widget.
|
||||
type WidgetButton struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
TextColor string `json:"textColor,omitempty"`
|
||||
FillColor string `json:"fillColor,omitempty"`
|
||||
// The color of the button's "outline".
|
||||
StrokeColor string `json:"color,omitempty"`
|
||||
HoverState *WidgetButtonHoverState `json:"hoverState,omitempty"`
|
||||
}
|
||||
|
||||
// WidgetButtonHoverState is the behaviour of a button that's part of a widget when it's hovered over with the mouse.
|
||||
type WidgetButtonHoverState struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
TextColor string `json:"textColor,omitempty"`
|
||||
FillColor string `json:"fillColor,omitempty"`
|
||||
// The color of the button's "outline".
|
||||
StrokeColor string `json:"color,omitempty"`
|
||||
}
|
||||
|
||||
// WidgetCreateRequest represents a request to create a widget.
|
||||
type WidgetCreateRequest interface {
|
||||
requestKind() string
|
||||
}
|
||||
|
||||
// TextAreaWidgetCreateRequest represents a requets to create a text area widget.
|
||||
type TextAreaWidgetCreateRequest struct {
|
||||
Style *WidgetStyle `json:"styles,omitempty"`
|
||||
// No longer than 30 characters.
|
||||
Name string `json:"shortName,omitempty"`
|
||||
// Raw markdown text.
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
func (*TextAreaWidgetCreateRequest) requestKind() string { return widgetKindTextArea }
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (r *TextAreaWidgetCreateRequest) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Kind string `json:"kind"`
|
||||
Style *WidgetStyle `json:"styles,omitempty"`
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}{r.requestKind(), r.Style, r.Name, r.Text})
|
||||
}
|
||||
|
||||
// CommunityListWidgetCreateRequest represents a requets to create a community list widget.
|
||||
type CommunityListWidgetCreateRequest struct {
|
||||
Style *WidgetStyle `json:"styles,omitempty"`
|
||||
// No longer than 30 characters.
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Communities []string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func (*CommunityListWidgetCreateRequest) requestKind() string { return widgetKindCommunityList }
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface.
|
||||
func (r *CommunityListWidgetCreateRequest) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Kind string `json:"kind"`
|
||||
Style *WidgetStyle `json:"styles,omitempty"`
|
||||
Name string `json:"shortName,omitempty"`
|
||||
Communities []string `json:"data,omitempty"`
|
||||
}{r.requestKind(), r.Style, r.Name, r.Communities})
|
||||
}
|
||||
|
||||
// Get the subreddit's widgets.
|
||||
func (s *WidgetService) Get(ctx context.Context, subreddit string) ([]Widget, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/widgets?progressive_images=true", subreddit)
|
||||
req, err := s.client.NewRequest(http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
Widgets WidgetList `json:"items"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Widgets, resp, nil
|
||||
}
|
||||
|
||||
// Create a widget for the subreddit.
|
||||
func (s *WidgetService) Create(ctx context.Context, subreddit string, request WidgetCreateRequest) (Widget, *Response, error) {
|
||||
if request == nil {
|
||||
return nil, nil, errors.New("WidgetCreateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/widget", subreddit)
|
||||
req, err := s.client.NewJSONRequest(http.MethodPost, path, request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(rootWidget)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data, resp, nil
|
||||
}
|
||||
|
||||
// Delete a widget via its id.
|
||||
func (s *WidgetService) Delete(ctx context.Context, subreddit, id string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/widget/%s", subreddit, id)
|
||||
req, err := s.client.NewRequest(http.MethodDelete, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Reorder the widgets in the subreddit.
|
||||
// The order should contain every single widget id in the subreddit; omitting any id will result in an error.
|
||||
// The id list should only contain sidebar widgets. It should exclude the community details and moderators widgets.
|
||||
func (s *WidgetService) Reorder(ctx context.Context, subreddit string, ids []string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/widget_order/sidebar", subreddit)
|
||||
req, err := s.client.NewJSONRequest(http.MethodPatch, path, ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedWidgets = []Widget{
|
||||
&TextAreaWidget{
|
||||
widget: widget{
|
||||
ID: "widget_15p7borvnnw5a",
|
||||
Kind: "textarea",
|
||||
Style: &WidgetStyle{
|
||||
HeaderColor: "#373c3f",
|
||||
BackgroundColor: "#cc5289",
|
||||
},
|
||||
},
|
||||
Name: "test title",
|
||||
Text: "test text",
|
||||
},
|
||||
|
||||
&ButtonWidget{
|
||||
widget: widget{
|
||||
ID: "widget_15paxrbiodp8v",
|
||||
Kind: "button",
|
||||
Style: &WidgetStyle{},
|
||||
},
|
||||
Name: "test text",
|
||||
Description: "test description",
|
||||
Buttons: []*WidgetButton{
|
||||
{
|
||||
Text: "test text",
|
||||
URL: "https://example.com",
|
||||
TextColor: "#ff66ac",
|
||||
FillColor: "#014980",
|
||||
StrokeColor: "#73ad34",
|
||||
HoverState: &WidgetButtonHoverState{
|
||||
Text: "test text",
|
||||
TextColor: "#000000",
|
||||
FillColor: "#00a6a5",
|
||||
StrokeColor: "#000000",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
&ImageWidget{
|
||||
widget: widget{
|
||||
ID: "widget_15p7o01nqr5tu",
|
||||
Kind: "image",
|
||||
Style: &WidgetStyle{},
|
||||
},
|
||||
Name: "test title",
|
||||
Images: []*WidgetImageLink{
|
||||
{
|
||||
URL: "https://www.redditstatic.com/image-processing.png",
|
||||
LinkURL: "https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
&CommunityListWidget{
|
||||
widget: widget{
|
||||
ID: "widget_15p7qwb2kxc6j",
|
||||
Kind: "community-list",
|
||||
Style: &WidgetStyle{
|
||||
HeaderColor: "#ffb000",
|
||||
},
|
||||
},
|
||||
Name: "test title",
|
||||
Communities: []*WidgetCommunity{
|
||||
{
|
||||
Name: "nba",
|
||||
Subscribers: 3571840,
|
||||
Subscribed: true,
|
||||
NSFW: false,
|
||||
},
|
||||
{
|
||||
Name: "golang",
|
||||
Subscribers: 125961,
|
||||
Subscribed: true,
|
||||
NSFW: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
&SubredditRulesWidget{
|
||||
widget: widget{
|
||||
ID: "widget_rules-2uquw1",
|
||||
Kind: "subreddit-rules",
|
||||
Style: &WidgetStyle{},
|
||||
},
|
||||
Name: "Subreddit Rules",
|
||||
Display: "compact",
|
||||
Rules: []string{"be nice"},
|
||||
},
|
||||
|
||||
&CommunityDetailsWidget{
|
||||
widget: widget{
|
||||
ID: "widget_id-card-2uquw1",
|
||||
Kind: "id-card",
|
||||
Style: &WidgetStyle{},
|
||||
},
|
||||
Name: "Community Details",
|
||||
Description: "Community Description",
|
||||
Subscribers: 2,
|
||||
CurrentlyViewing: 3,
|
||||
SubscribersText: "subscriberz",
|
||||
CurrentlyViewingText: "viewerz",
|
||||
},
|
||||
|
||||
&MenuWidget{
|
||||
widget: widget{
|
||||
ID: "widget_15owrhqvgfhke",
|
||||
Kind: "menu",
|
||||
Style: &WidgetStyle{},
|
||||
},
|
||||
ShowWiki: true,
|
||||
Links: []WidgetLink{
|
||||
&WidgetLinkSingle{
|
||||
Text: "link1",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
&WidgetLinkMultiple{
|
||||
Text: "test",
|
||||
URLs: []*WidgetLinkSingle{
|
||||
{
|
||||
Text: "link2",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
{
|
||||
Text: "link3",
|
||||
URL: "https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
&ModeratorsWidget{
|
||||
widget: widget{
|
||||
ID: "widget_moderators-2uquw1",
|
||||
Kind: "moderators",
|
||||
Style: &WidgetStyle{},
|
||||
},
|
||||
Mods: []string{"testuser"},
|
||||
Total: 1,
|
||||
},
|
||||
|
||||
&CustomWidget{
|
||||
widget: widget{
|
||||
ID: "widget_15osq4jms4tdo",
|
||||
Kind: "custom",
|
||||
Style: &WidgetStyle{},
|
||||
},
|
||||
Name: "custom image widget",
|
||||
Text: "some image",
|
||||
StyleSheet: "* {}",
|
||||
StyleSheetURL: "https://styles.redditmedia.com/t5_2uquw1/styles/customWidget-stylesheet-n2q86gjf04o51.css",
|
||||
Images: []*WidgetImage{
|
||||
{
|
||||
Name: "test",
|
||||
URL: "https://www.redditstatic.com/image-processing.png",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestWidgetService_Get(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/widget/widgets.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/widgets", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("progressive_images", "true")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
widgets, _, err := client.Widget.Get(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
require.ElementsMatch(t, expectedWidgets, widgets)
|
||||
}
|
||||
|
||||
func TestWidgetService_Create(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/widget", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
body := new(struct {
|
||||
Name string `json:"shortName"`
|
||||
Text string `json:"text"`
|
||||
})
|
||||
|
||||
err := json.NewDecoder(r.Body).Decode(body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "test name", body.Name)
|
||||
require.Equal(t, "test text", body.Text)
|
||||
|
||||
fmt.Fprint(w, `{
|
||||
"text": "test text",
|
||||
"kind": "textarea",
|
||||
"shortName": "test name",
|
||||
"id": "id123"
|
||||
}`)
|
||||
})
|
||||
|
||||
_, _, err := client.Widget.Create(ctx, "testsubreddit", nil)
|
||||
require.EqualError(t, err, "WidgetCreateRequest: cannot be nil")
|
||||
|
||||
createdWidget, _, err := client.Widget.Create(ctx, "testsubreddit", &TextAreaWidgetCreateRequest{
|
||||
Name: "test name",
|
||||
Text: "test text",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &TextAreaWidget{
|
||||
widget: widget{
|
||||
ID: "id123",
|
||||
Kind: "textarea",
|
||||
},
|
||||
Name: "test name",
|
||||
Text: "test text",
|
||||
}, createdWidget)
|
||||
}
|
||||
|
||||
func TestWidgetService_Delete(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/widget/abc123", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodDelete, r.Method)
|
||||
})
|
||||
|
||||
_, err := client.Widget.Delete(ctx, "testsubreddit", "abc123")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWidgetService_Reorder(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/widget_order/sidebar", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPatch, r.Method)
|
||||
|
||||
var ids []string
|
||||
err := json.NewDecoder(r.Body).Decode(&ids)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"test1", "test2", "test3", "test4"}, ids)
|
||||
})
|
||||
|
||||
_, err := client.Widget.Reorder(ctx, "testsubreddit", []string{"test1", "test2", "test3", "test4"})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// WikiService handles communication with the wiki
|
||||
// related methods of the Reddit API.
|
||||
//
|
||||
// Reddit API docs: https://www.reddit.com/dev/api/#section_wiki
|
||||
type WikiService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// WikiPage is a wiki page in a subreddit.
|
||||
type WikiPage struct {
|
||||
Content string `json:"content_md,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
MayRevise bool `json:"may_revise"`
|
||||
|
||||
RevisionID string `json:"revision_id,omitempty"`
|
||||
RevisionDate *Timestamp `json:"revision_date,omitempty"`
|
||||
RevisionBy *User `json:"revision_by,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (p *WikiPage) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
Content string `json:"content_md,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
MayRevise bool `json:"may_revise"`
|
||||
|
||||
RevisionID string `json:"revision_id,omitempty"`
|
||||
RevisionDate *Timestamp `json:"revision_date,omitempty"`
|
||||
RevisionBy thing `json:"revision_by,omitempty"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.Content = root.Content
|
||||
p.Reason = root.Reason
|
||||
p.MayRevise = root.MayRevise
|
||||
|
||||
p.RevisionID = root.RevisionID
|
||||
p.RevisionDate = root.RevisionDate
|
||||
|
||||
if user, ok := root.RevisionBy.User(); ok {
|
||||
p.RevisionBy = user
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// WikiPageEditRequest represents a request to edit a wiki page in a subreddit.
|
||||
type WikiPageEditRequest struct {
|
||||
Subreddit string `url:"-"`
|
||||
Page string `url:"page"`
|
||||
Content string `url:"content"`
|
||||
// Optional, up to 256 characters long.
|
||||
Reason string `url:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// WikiPagePermissionLevel defines who can edit a specific wiki page in a subreddit.
|
||||
type WikiPagePermissionLevel int
|
||||
|
||||
const (
|
||||
// PermissionSubredditWikiPermissions uses subreddit wiki permissions.
|
||||
PermissionSubredditWikiPermissions WikiPagePermissionLevel = iota
|
||||
// PermissionApprovedContributorsOnly is only for approved wiki contributors.
|
||||
PermissionApprovedContributorsOnly
|
||||
// PermissionModeratorsOnly is only for moderators.
|
||||
PermissionModeratorsOnly
|
||||
)
|
||||
|
||||
// WikiPageSettings holds the settings for a specific wiki page.
|
||||
type WikiPageSettings struct {
|
||||
PermissionLevel WikiPagePermissionLevel `json:"permlevel"`
|
||||
Listed bool `json:"listed"`
|
||||
Editors []*User `json:"editors"`
|
||||
}
|
||||
|
||||
// WikiPageSettingsUpdateRequest represents a request to update the visibility and
|
||||
// permissions of a wiki page.
|
||||
type WikiPageSettingsUpdateRequest struct {
|
||||
// This HAS to be provided no matter what, or else we get a 500 response.
|
||||
PermissionLevel WikiPagePermissionLevel `url:"permlevel"`
|
||||
Listed *bool `url:"listed,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (s *WikiPageSettings) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
PermissionLevel WikiPagePermissionLevel `json:"permlevel"`
|
||||
Listed bool `json:"listed"`
|
||||
Things []thing `json:"editors"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.PermissionLevel = root.PermissionLevel
|
||||
s.Listed = root.Listed
|
||||
|
||||
for _, thing := range root.Things {
|
||||
if user, ok := thing.User(); ok {
|
||||
s.Editors = append(s.Editors, user)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type wikiPageRevisionListing struct {
|
||||
Data struct {
|
||||
Revisions []*WikiPageRevision `json:"children"`
|
||||
After string `json:"after"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func (l *wikiPageRevisionListing) After() string {
|
||||
return l.Data.After
|
||||
}
|
||||
|
||||
// WikiPageRevision is a revision of a wiki page.
|
||||
type WikiPageRevision struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Page string `json:"page,omitempty"`
|
||||
Created *Timestamp `json:"timestamp,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Hidden bool `json:"revision_hidden"`
|
||||
Author *User `json:"author,omitempty"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements the json.Unmarshaler interface.
|
||||
func (r *WikiPageRevision) UnmarshalJSON(b []byte) error {
|
||||
root := new(struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Page string `json:"page,omitempty"`
|
||||
Created *Timestamp `json:"timestamp,omitempty"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Hidden bool `json:"revision_hidden"`
|
||||
Author thing `json:"author,omitempty"`
|
||||
})
|
||||
|
||||
err := json.Unmarshal(b, root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r.ID = root.ID
|
||||
r.Page = root.Page
|
||||
r.Created = root.Created
|
||||
r.Reason = root.Reason
|
||||
r.Hidden = root.Hidden
|
||||
|
||||
if user, ok := root.Author.User(); ok {
|
||||
r.Author = user
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Page gets a wiki page.
|
||||
func (s *WikiService) Page(ctx context.Context, subreddit, page string) (*WikiPage, *Response, error) {
|
||||
return s.PageRevision(ctx, subreddit, page, "")
|
||||
}
|
||||
|
||||
// PageRevision gets a wiki page at the version it was at the revisionID provided.
|
||||
// If revisionID is an empty string, it will get the most recent version.
|
||||
func (s *WikiService) PageRevision(ctx context.Context, subreddit, page, revisionID string) (*WikiPage, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/wiki/%s", subreddit, page)
|
||||
|
||||
params := struct {
|
||||
RevisionID string `url:"v,omitempty"`
|
||||
}{revisionID}
|
||||
|
||||
t, resp, err := s.client.getThing(ctx, path, params)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
wikiPage, _ := t.WikiPage()
|
||||
return wikiPage, resp, nil
|
||||
}
|
||||
|
||||
// Pages gets a list of wiki pages in the subreddit.
|
||||
// Returns 403 Forbidden if the wiki is disabled.
|
||||
func (s *WikiService) Pages(ctx context.Context, subreddit string) ([]string, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/wiki/pages", subreddit)
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
wikiPages, _ := t.WikiPages()
|
||||
return wikiPages, resp, nil
|
||||
}
|
||||
|
||||
// Edit a wiki page.
|
||||
func (s *WikiService) Edit(ctx context.Context, editRequest *WikiPageEditRequest) (*Response, error) {
|
||||
if editRequest == nil {
|
||||
return nil, errors.New("*WikiPageEditRequest: cannot be nil")
|
||||
}
|
||||
|
||||
form, err := query.Values(editRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/api/wiki/edit", editRequest.Subreddit)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Revert a wiki page to a specific revision.
|
||||
func (s *WikiService) Revert(ctx context.Context, subreddit, page, revisionID string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/wiki/revert", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", page)
|
||||
form.Set("revision", revisionID)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Settings gets the subreddit's wiki page's settings.
|
||||
func (s *WikiService) Settings(ctx context.Context, subreddit, page string) (*WikiPageSettings, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/wiki/settings/%s", subreddit, page)
|
||||
t, resp, err := s.client.getThing(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
settings, _ := t.WikiPageSettings()
|
||||
return settings, resp, nil
|
||||
}
|
||||
|
||||
// UpdateSettings updates the subreddit's wiki page's settings.
|
||||
func (s *WikiService) UpdateSettings(ctx context.Context, subreddit, page string, updateRequest *WikiPageSettingsUpdateRequest) (*WikiPageSettings, *Response, error) {
|
||||
if updateRequest == nil {
|
||||
return nil, nil, errors.New("*WikiPageSettingsUpdateRequest: cannot be nil")
|
||||
}
|
||||
|
||||
form, err := query.Values(updateRequest)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("r/%s/wiki/settings/%s", subreddit, page)
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
root := new(thing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
settings, _ := root.WikiPageSettings()
|
||||
return settings, resp, nil
|
||||
}
|
||||
|
||||
// Discussions gets a list of discussions (posts) about the wiki page.
|
||||
func (s *WikiService) Discussions(ctx context.Context, subreddit, page string, opts *ListOptions) ([]*Post, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/wiki/discussions/%s", subreddit, page)
|
||||
l, resp, err := s.client.getListing(ctx, path, opts)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return l.Posts(), resp, nil
|
||||
}
|
||||
|
||||
// ToggleVisibility toggles the public visibility of a wiki page revision.
|
||||
// The returned bool is whether the page was set to hidden or not.
|
||||
func (s *WikiService) ToggleVisibility(ctx context.Context, subreddit, page, revisionID string) (bool, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/wiki/hide", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", page)
|
||||
form.Set("revision", revisionID)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
root := new(struct {
|
||||
Status bool `json:"status"`
|
||||
})
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return false, resp, err
|
||||
}
|
||||
|
||||
return root.Status, resp, nil
|
||||
}
|
||||
|
||||
func (s *WikiService) revisions(ctx context.Context, subreddit, page string, opts *ListOptions) ([]*WikiPageRevision, *Response, error) {
|
||||
path := fmt.Sprintf("r/%s/wiki/revisions", subreddit)
|
||||
if page != "" {
|
||||
path += "/" + page
|
||||
}
|
||||
|
||||
if opts != nil {
|
||||
const idPrefix = "WikiRevision_"
|
||||
if opts.After != "" && !strings.HasPrefix(opts.After, idPrefix) {
|
||||
opts.After = idPrefix + opts.After
|
||||
}
|
||||
if opts.Before != "" && !strings.HasPrefix(opts.Before, idPrefix) {
|
||||
opts.Before = idPrefix + opts.Before
|
||||
}
|
||||
}
|
||||
|
||||
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(wikiPageRevisionListing)
|
||||
resp, err := s.client.Do(ctx, req, root)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return root.Data.Revisions, resp, nil
|
||||
}
|
||||
|
||||
// Revisions gets revisions of all pages in the wiki.
|
||||
func (s *WikiService) Revisions(ctx context.Context, subreddit string, opts *ListOptions) ([]*WikiPageRevision, *Response, error) {
|
||||
return s.revisions(ctx, subreddit, "", opts)
|
||||
}
|
||||
|
||||
// RevisionsPage gets revisions of the specific wiki page.
|
||||
// If page is an empty string, it gets revisions of all pages in the wiki.
|
||||
func (s *WikiService) RevisionsPage(ctx context.Context, subreddit, page string, opts *ListOptions) ([]*WikiPageRevision, *Response, error) {
|
||||
return s.revisions(ctx, subreddit, page, opts)
|
||||
}
|
||||
|
||||
// Allow the user to edit the specified wiki page in the subreddit.
|
||||
func (s *WikiService) Allow(ctx context.Context, subreddit, page, username string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/wiki/alloweditor/add", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", page)
|
||||
form.Set("username", username)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
|
||||
// Deny the user the ability to edit the specified wiki page in the subreddit.
|
||||
func (s *WikiService) Deny(ctx context.Context, subreddit, page, username string) (*Response, error) {
|
||||
path := fmt.Sprintf("r/%s/api/wiki/alloweditor/del", subreddit)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", page)
|
||||
form.Set("username", username)
|
||||
|
||||
req, err := s.client.NewRequest(http.MethodPost, path, form)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.client.Do(ctx, req, nil)
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
package reddit
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var expectedWikiPage = &WikiPage{
|
||||
Content: "test reason",
|
||||
Reason: "this is a reason!",
|
||||
MayRevise: true,
|
||||
|
||||
RevisionID: "3c4e9fab-ef2c-11ea-90b6-0e9189256887",
|
||||
RevisionDate: &Timestamp{time.Date(2020, 9, 5, 3, 59, 45, 0, time.UTC)},
|
||||
RevisionBy: &User{
|
||||
ID: "164ab8",
|
||||
Name: "v_95",
|
||||
Created: &Timestamp{time.Date(2017, 3, 12, 4, 56, 47, 0, time.UTC)},
|
||||
|
||||
PostKarma: 691,
|
||||
CommentKarma: 22235,
|
||||
|
||||
HasVerifiedEmail: true,
|
||||
NSFW: true,
|
||||
},
|
||||
}
|
||||
|
||||
var expectedWikiPageSettings = &WikiPageSettings{
|
||||
PermissionLevel: PermissionSubredditWikiPermissions,
|
||||
Listed: true,
|
||||
Editors: []*User{
|
||||
{
|
||||
ID: "164ab8",
|
||||
Name: "v_95",
|
||||
Created: &Timestamp{time.Date(2017, 3, 12, 4, 56, 47, 0, time.UTC)},
|
||||
|
||||
PostKarma: 691,
|
||||
CommentKarma: 22235,
|
||||
|
||||
HasVerifiedEmail: true,
|
||||
NSFW: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var expectedWikiPageDiscussions = []*Post{
|
||||
{
|
||||
ID: "imj8g5",
|
||||
FullID: "t3_imj8g5",
|
||||
Created: &Timestamp{time.Date(2020, 9, 4, 16, 33, 33, 0, time.UTC)},
|
||||
Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)},
|
||||
|
||||
Permalink: "/r/helloworldtestt/comments/imj8g5/test/",
|
||||
URL: "https://www.reddit.com/r/helloworldtestt/wiki/index",
|
||||
|
||||
Title: "test",
|
||||
|
||||
Likes: Bool(true),
|
||||
|
||||
Score: 1,
|
||||
UpvoteRatio: 1,
|
||||
NumberOfComments: 0,
|
||||
|
||||
SubredditName: "helloworldtestt",
|
||||
SubredditNamePrefixed: "r/helloworldtestt",
|
||||
SubredditID: "t5_2uquw1",
|
||||
SubredditSubscribers: 2,
|
||||
|
||||
Author: "v_95",
|
||||
AuthorID: "t2_164ab8",
|
||||
},
|
||||
}
|
||||
|
||||
var expectedWikiPageRevisions = []*WikiPageRevision{
|
||||
{
|
||||
ID: "3b28c343-effb-11ea-859e-0efe313b2cd3",
|
||||
Page: "index",
|
||||
Created: &Timestamp{time.Date(2020, 9, 6, 4, 41, 29, 0, time.UTC)},
|
||||
Reason: "reverted back 1 day",
|
||||
Hidden: false,
|
||||
Author: &User{
|
||||
ID: "164ab8",
|
||||
Name: "v_95",
|
||||
Created: &Timestamp{time.Date(2017, 3, 12, 4, 56, 47, 0, time.UTC)},
|
||||
|
||||
PostKarma: 691,
|
||||
CommentKarma: 22235,
|
||||
|
||||
HasVerifiedEmail: true,
|
||||
NSFW: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "d217592d-effa-11ea-90af-0e913d9ded0b",
|
||||
Page: "index",
|
||||
Created: &Timestamp{time.Date(2020, 9, 6, 4, 38, 33, 0, time.UTC)},
|
||||
Reason: "reverted back 10 minutes",
|
||||
Hidden: false,
|
||||
Author: &User{
|
||||
ID: "164ab8",
|
||||
Name: "v_95",
|
||||
Created: &Timestamp{time.Date(2017, 3, 12, 4, 56, 47, 0, time.UTC)},
|
||||
|
||||
PostKarma: 691,
|
||||
CommentKarma: 22235,
|
||||
|
||||
HasVerifiedEmail: true,
|
||||
NSFW: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func TestWikiService_Page(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/wiki/page.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/testpage", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
wikiPage, _, err := client.Wiki.Page(ctx, "testsubreddit", "testpage")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWikiPage, wikiPage)
|
||||
}
|
||||
|
||||
func TestWikiService_PageRevision(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/wiki/page.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/testpage", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("v", "testrevision")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
wikiPage, _, err := client.Wiki.PageRevision(ctx, "testsubreddit", "testpage", "testrevision")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWikiPage, wikiPage)
|
||||
}
|
||||
|
||||
func TestWikiService_Pages(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/pages", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, `{
|
||||
"kind": "wikipagelisting",
|
||||
"data": [
|
||||
"faq",
|
||||
"index"
|
||||
]
|
||||
}`)
|
||||
})
|
||||
|
||||
wikiPages, _, err := client.Wiki.Pages(ctx, "testsubreddit")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"faq", "index"}, wikiPages)
|
||||
}
|
||||
|
||||
func TestWikiService_Edit(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/wiki/edit", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", "testpage")
|
||||
form.Set("content", "testcontent")
|
||||
form.Set("reason", "testreason")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Wiki.Edit(ctx, nil)
|
||||
require.EqualError(t, err, "*WikiPageEditRequest: cannot be nil")
|
||||
|
||||
_, err = client.Wiki.Edit(ctx, &WikiPageEditRequest{
|
||||
Subreddit: "testsubreddit",
|
||||
Page: "testpage",
|
||||
Content: "testcontent",
|
||||
Reason: "testreason",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWikiService_Revert(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/wiki/revert", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", "testpage")
|
||||
form.Set("revision", "testrevision")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Wiki.Revert(ctx, "testsubreddit", "testpage", "testrevision")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWikiService_Settings(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/wiki/page-settings.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/settings/testpage", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
wikiPageSettings, _, err := client.Wiki.Settings(ctx, "testsubreddit", "testpage")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWikiPageSettings, wikiPageSettings)
|
||||
}
|
||||
|
||||
func TestWikiService_UpdateSettings(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/wiki/page-settings.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/settings/testpage", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("permlevel", "1")
|
||||
form.Set("listed", "false")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
_, _, err = client.Wiki.UpdateSettings(ctx, "testsubreddit", "testpage", nil)
|
||||
require.EqualError(t, err, "*WikiPageSettingsUpdateRequest: cannot be nil")
|
||||
|
||||
wikiPageSettings, _, err := client.Wiki.UpdateSettings(ctx, "testsubreddit", "testpage", &WikiPageSettingsUpdateRequest{
|
||||
Listed: Bool(false),
|
||||
PermissionLevel: PermissionApprovedContributorsOnly,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWikiPageSettings, wikiPageSettings)
|
||||
}
|
||||
|
||||
func TestWikiService_Discussions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/wiki/discussions.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/discussions/testpage", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
wikiPageDiscussions, _, err := client.Wiki.Discussions(ctx, "testsubreddit", "testpage", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWikiPageDiscussions, wikiPageDiscussions)
|
||||
}
|
||||
|
||||
func TestWikiService_ToggleVisibility(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/wiki/hide", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", "testpage")
|
||||
form.Set("revision", "testrevision")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
|
||||
fmt.Fprint(w, `{"status": true}`)
|
||||
})
|
||||
|
||||
hidden, _, err := client.Wiki.ToggleVisibility(ctx, "testsubreddit", "testpage", "testrevision")
|
||||
require.NoError(t, err)
|
||||
require.True(t, hidden)
|
||||
}
|
||||
|
||||
func TestWikiService_Revisions(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/wiki/revisions.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/revisions", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
wikiPageRevisions, _, err := client.Wiki.Revisions(ctx, "testsubreddit", nil)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWikiPageRevisions, wikiPageRevisions)
|
||||
}
|
||||
|
||||
func TestWikiService_RevisionsPage(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
blob, err := readFileContents("../testdata/wiki/revisions.json")
|
||||
require.NoError(t, err)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/wiki/revisions/testpage", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodGet, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("limit", "10")
|
||||
form.Set("after", "WikiRevision_wikiId1")
|
||||
form.Set("before", "WikiRevision_wikiId2")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.Form)
|
||||
|
||||
fmt.Fprint(w, blob)
|
||||
})
|
||||
|
||||
wikiPageRevisions, _, err := client.Wiki.RevisionsPage(ctx, "testsubreddit", "testpage", &ListOptions{
|
||||
Limit: 10,
|
||||
After: "wikiId1",
|
||||
Before: "wikiId2",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedWikiPageRevisions, wikiPageRevisions)
|
||||
}
|
||||
|
||||
func TestWikiService_Allow(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/wiki/alloweditor/add", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", "testpage")
|
||||
form.Set("username", "testusername")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Wiki.Allow(ctx, "testsubreddit", "testpage", "testusername")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestWikiService_Deny(t *testing.T) {
|
||||
client, mux := setup(t)
|
||||
|
||||
mux.HandleFunc("/r/testsubreddit/api/wiki/alloweditor/del", func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, http.MethodPost, r.Method)
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("page", "testpage")
|
||||
form.Set("username", "testusername")
|
||||
|
||||
err := r.ParseForm()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, form, r.PostForm)
|
||||
})
|
||||
|
||||
_, err := client.Wiki.Deny(ctx, "testsubreddit", "testpage", "testusername")
|
||||
require.NoError(t, err)
|
||||
}
|
||||
Reference in New Issue
Block a user