From bd1544ad104daa78ab6f063d0d286f5290c57832 Mon Sep 17 00:00:00 2001 From: long Date: Wed, 27 May 2026 17:59:11 -0500 Subject: [PATCH] Revert "added v2 package" This reverts commit 88d73f0f2e6f7e14d54091b6da38b1a98bc3372a. --- docker/module-testing/Dockerfile | 6 - v2/go.mod | 34 - v2/go.sum | 69 -- v2/reddit/account.go | 400 ------- v2/reddit/account_test.go | 313 ------ v2/reddit/collection.go | 290 ----- v2/reddit/collection_test.go | 342 ------ v2/reddit/comment.go | 122 --- v2/reddit/comment_test.go | 355 ------ v2/reddit/compression.go | 26 - v2/reddit/emoji.go | 278 ----- v2/reddit/emoji_test.go | 227 ---- v2/reddit/errors.go | 105 -- v2/reddit/flair.go | 562 ---------- v2/reddit/flair_test.go | 701 ------------ v2/reddit/gold.go | 49 - v2/reddit/gold_test.go | 44 - v2/reddit/listings.go | 40 - v2/reddit/listings_test.go | 183 ---- v2/reddit/live-thread.go | 610 ----------- v2/reddit/live-thread_test.go | 687 ------------ v2/reddit/message.go | 294 ----- v2/reddit/message_test.go | 258 ----- v2/reddit/moderation.go | 494 --------- v2/reddit/moderation_test.go | 701 ------------ v2/reddit/multi.go | 307 ------ v2/reddit/multi_test.go | 280 ----- v2/reddit/post-and-comment.go | 182 ---- v2/reddit/post.go | 582 ---------- v2/reddit/post_test.go | 1135 -------------------- v2/reddit/reddit-oauth.go | 77 -- v2/reddit/reddit-options.go | 78 -- v2/reddit/reddit-options_test.go | 74 -- v2/reddit/reddit-user-agent.go | 44 - v2/reddit/reddit.go | 671 ------------ v2/reddit/reddit_test.go | 295 ----- v2/reddit/stream.go | 116 -- v2/reddit/stream_test.go | 310 ------ v2/reddit/streamer.go | 44 - v2/reddit/subreddit.go | 1182 -------------------- v2/reddit/subreddit_test.go | 1722 ------------------------------ v2/reddit/things.go | 791 -------------- v2/reddit/timestamp.go | 47 - v2/reddit/timestamp_test.go | 182 ---- v2/reddit/user.go | 413 ------- v2/reddit/user_test.go | 925 ---------------- v2/reddit/widget.go | 471 -------- v2/reddit/widget_test.go | 265 ----- v2/reddit/wiki.go | 396 ------- v2/reddit/wiki_test.go | 391 ------- 50 files changed, 18170 deletions(-) delete mode 100644 docker/module-testing/Dockerfile delete mode 100644 v2/go.mod delete mode 100644 v2/go.sum delete mode 100644 v2/reddit/account.go delete mode 100644 v2/reddit/account_test.go delete mode 100644 v2/reddit/collection.go delete mode 100644 v2/reddit/collection_test.go delete mode 100644 v2/reddit/comment.go delete mode 100644 v2/reddit/comment_test.go delete mode 100644 v2/reddit/compression.go delete mode 100644 v2/reddit/emoji.go delete mode 100644 v2/reddit/emoji_test.go delete mode 100644 v2/reddit/errors.go delete mode 100644 v2/reddit/flair.go delete mode 100644 v2/reddit/flair_test.go delete mode 100644 v2/reddit/gold.go delete mode 100644 v2/reddit/gold_test.go delete mode 100644 v2/reddit/listings.go delete mode 100644 v2/reddit/listings_test.go delete mode 100644 v2/reddit/live-thread.go delete mode 100644 v2/reddit/live-thread_test.go delete mode 100644 v2/reddit/message.go delete mode 100644 v2/reddit/message_test.go delete mode 100644 v2/reddit/moderation.go delete mode 100644 v2/reddit/moderation_test.go delete mode 100644 v2/reddit/multi.go delete mode 100644 v2/reddit/multi_test.go delete mode 100644 v2/reddit/post-and-comment.go delete mode 100644 v2/reddit/post.go delete mode 100644 v2/reddit/post_test.go delete mode 100644 v2/reddit/reddit-oauth.go delete mode 100644 v2/reddit/reddit-options.go delete mode 100644 v2/reddit/reddit-options_test.go delete mode 100644 v2/reddit/reddit-user-agent.go delete mode 100644 v2/reddit/reddit.go delete mode 100644 v2/reddit/reddit_test.go delete mode 100644 v2/reddit/stream.go delete mode 100644 v2/reddit/stream_test.go delete mode 100644 v2/reddit/streamer.go delete mode 100644 v2/reddit/subreddit.go delete mode 100644 v2/reddit/subreddit_test.go delete mode 100644 v2/reddit/things.go delete mode 100644 v2/reddit/timestamp.go delete mode 100644 v2/reddit/timestamp_test.go delete mode 100644 v2/reddit/user.go delete mode 100644 v2/reddit/user_test.go delete mode 100644 v2/reddit/widget.go delete mode 100644 v2/reddit/widget_test.go delete mode 100644 v2/reddit/wiki.go delete mode 100644 v2/reddit/wiki_test.go diff --git a/docker/module-testing/Dockerfile b/docker/module-testing/Dockerfile deleted file mode 100644 index f189214..0000000 --- a/docker/module-testing/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM golang:1.26-alpine - -RUN apk add git - -WORKDIR /project -RUN go mod init project diff --git a/v2/go.mod b/v2/go.mod deleted file mode 100644 index d3b6546..0000000 --- a/v2/go.mod +++ /dev/null @@ -1,34 +0,0 @@ -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 -) diff --git a/v2/go.sum b/v2/go.sum deleted file mode 100644 index df78e0c..0000000 --- a/v2/go.sum +++ /dev/null @@ -1,69 +0,0 @@ -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= diff --git a/v2/reddit/account.go b/v2/reddit/account.go deleted file mode 100644 index a4ed04d..0000000 --- a/v2/reddit/account.go +++ /dev/null @@ -1,400 +0,0 @@ -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) -} diff --git a/v2/reddit/account_test.go b/v2/reddit/account_test.go deleted file mode 100644 index a555210..0000000 --- a/v2/reddit/account_test.go +++ /dev/null @@ -1,313 +0,0 @@ -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) -} diff --git a/v2/reddit/collection.go b/v2/reddit/collection.go deleted file mode 100644 index d05087e..0000000 --- a/v2/reddit/collection.go +++ /dev/null @@ -1,290 +0,0 @@ -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) -} diff --git a/v2/reddit/collection_test.go b/v2/reddit/collection_test.go deleted file mode 100644 index f75acc6..0000000 --- a/v2/reddit/collection_test.go +++ /dev/null @@ -1,342 +0,0 @@ -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) -} diff --git a/v2/reddit/comment.go b/v2/reddit/comment.go deleted file mode 100644 index ca6389d..0000000 --- a/v2/reddit/comment.go +++ /dev/null @@ -1,122 +0,0 @@ -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 -} diff --git a/v2/reddit/comment_test.go b/v2/reddit/comment_test.go deleted file mode 100644 index e902550..0000000 --- a/v2/reddit/comment_test.go +++ /dev/null @@ -1,355 +0,0 @@ -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) -} diff --git a/v2/reddit/compression.go b/v2/reddit/compression.go deleted file mode 100644 index c3781f6..0000000 --- a/v2/reddit/compression.go +++ /dev/null @@ -1,26 +0,0 @@ -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 -} diff --git a/v2/reddit/emoji.go b/v2/reddit/emoji.go deleted file mode 100644 index 7c72690..0000000 --- a/v2/reddit/emoji.go +++ /dev/null @@ -1,278 +0,0 @@ -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) -} diff --git a/v2/reddit/emoji_test.go b/v2/reddit/emoji_test.go deleted file mode 100644 index 1edb9fe..0000000 --- a/v2/reddit/emoji_test.go +++ /dev/null @@ -1,227 +0,0 @@ -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) -} diff --git a/v2/reddit/errors.go b/v2/reddit/errors.go deleted file mode 100644 index 3b20418..0000000 --- a/v2/reddit/errors.go +++ /dev/null @@ -1,105 +0,0 @@ -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) -} diff --git a/v2/reddit/flair.go b/v2/reddit/flair.go deleted file mode 100644 index 0ecc8e8..0000000 --- a/v2/reddit/flair.go +++ /dev/null @@ -1,562 +0,0 @@ -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 -} diff --git a/v2/reddit/flair_test.go b/v2/reddit/flair_test.go deleted file mode 100644 index dd92533..0000000 --- a/v2/reddit/flair_test.go +++ /dev/null @@ -1,701 +0,0 @@ -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) -} diff --git a/v2/reddit/gold.go b/v2/reddit/gold.go deleted file mode 100644 index 6b63b44..0000000 --- a/v2/reddit/gold.go +++ /dev/null @@ -1,49 +0,0 @@ -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) -} diff --git a/v2/reddit/gold_test.go b/v2/reddit/gold_test.go deleted file mode 100644 index 11de810..0000000 --- a/v2/reddit/gold_test.go +++ /dev/null @@ -1,44 +0,0 @@ -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) -} diff --git a/v2/reddit/listings.go b/v2/reddit/listings.go deleted file mode 100644 index a7b8bb1..0000000 --- a/v2/reddit/listings.go +++ /dev/null @@ -1,40 +0,0 @@ -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 -} diff --git a/v2/reddit/listings_test.go b/v2/reddit/listings_test.go deleted file mode 100644 index 3cac649..0000000 --- a/v2/reddit/listings_test.go +++ /dev/null @@ -1,183 +0,0 @@ -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) -} diff --git a/v2/reddit/live-thread.go b/v2/reddit/live-thread.go deleted file mode 100644 index 5dac208..0000000 --- a/v2/reddit/live-thread.go +++ /dev/null @@ -1,610 +0,0 @@ -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) -} diff --git a/v2/reddit/live-thread_test.go b/v2/reddit/live-thread_test.go deleted file mode 100644 index dc52715..0000000 --- a/v2/reddit/live-thread_test.go +++ /dev/null @@ -1,687 +0,0 @@ -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) -} diff --git a/v2/reddit/message.go b/v2/reddit/message.go deleted file mode 100644 index c4fc86c..0000000 --- a/v2/reddit/message.go +++ /dev/null @@ -1,294 +0,0 @@ -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 -} diff --git a/v2/reddit/message_test.go b/v2/reddit/message_test.go deleted file mode 100644 index b624653..0000000 --- a/v2/reddit/message_test.go +++ /dev/null @@ -1,258 +0,0 @@ -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) -} diff --git a/v2/reddit/moderation.go b/v2/reddit/moderation.go deleted file mode 100644 index a1af7d2..0000000 --- a/v2/reddit/moderation.go +++ /dev/null @@ -1,494 +0,0 @@ -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) -} diff --git a/v2/reddit/moderation_test.go b/v2/reddit/moderation_test.go deleted file mode 100644 index 0966be8..0000000 --- a/v2/reddit/moderation_test.go +++ /dev/null @@ -1,701 +0,0 @@ -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) -} diff --git a/v2/reddit/multi.go b/v2/reddit/multi.go deleted file mode 100644 index 6e7c3a8..0000000 --- a/v2/reddit/multi.go +++ /dev/null @@ -1,307 +0,0 @@ -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) -} diff --git a/v2/reddit/multi_test.go b/v2/reddit/multi_test.go deleted file mode 100644 index 72423b6..0000000 --- a/v2/reddit/multi_test.go +++ /dev/null @@ -1,280 +0,0 @@ -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) -} diff --git a/v2/reddit/post-and-comment.go b/v2/reddit/post-and-comment.go deleted file mode 100644 index 209604b..0000000 --- a/v2/reddit/post-and-comment.go +++ /dev/null @@ -1,182 +0,0 @@ -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) -} diff --git a/v2/reddit/post.go b/v2/reddit/post.go deleted file mode 100644 index 26e485a..0000000 --- a/v2/reddit/post.go +++ /dev/null @@ -1,582 +0,0 @@ -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) -} diff --git a/v2/reddit/post_test.go b/v2/reddit/post_test.go deleted file mode 100644 index 78d26b3..0000000 --- a/v2/reddit/post_test.go +++ /dev/null @@ -1,1135 +0,0 @@ -package reddit - -import ( - "fmt" - "net/http" - "net/url" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -var expectedPostAndComments = &PostAndComments{ - Post: &Post{ - ID: "testpost", - FullID: "t3_testpost", - Created: &Timestamp{time.Date(2020, 7, 18, 10, 26, 7, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - Permalink: "/r/test/comments/testpost/test/", - URL: "https://www.reddit.com/r/test/comments/testpost/test/", - - Title: "Test", - Body: "Hello", - - Score: 1, - UpvoteRatio: 1, - NumberOfComments: 2, - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - SubredditSubscribers: 8077, - - Author: "testuser", - AuthorID: "t2_testuser", - - IsSelfPost: true, - }, - Comments: []*Comment{ - { - ID: "testc1", - FullID: "t1_testc1", - Created: &Timestamp{time.Date(2020, 7, 18, 10, 31, 59, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - ParentID: "t3_testpost", - Permalink: "/r/test/comments/testpost/test/testc1/", - - Body: "Hi", - Author: "testuser", - AuthorID: "t2_testuser", - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - - Score: 1, - Controversiality: 0, - - PostID: "t3_testpost", - - IsSubmitter: true, - CanGild: true, - - Replies: Replies{ - Comments: []*Comment{ - { - ID: "testc2", - FullID: "t1_testc2", - Created: &Timestamp{time.Date(2020, 7, 18, 10, 32, 28, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - ParentID: "t1_testc1", - Permalink: "/r/test/comments/testpost/test/testc2/", - - Body: "Hello", - Author: "testuser", - AuthorID: "t2_testuser", - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - - Score: 1, - Controversiality: 0, - - PostID: "t3_testpost", - - IsSubmitter: true, - CanGild: true, - }, - }, - }, - }, - }, -} - -var expectedSubmittedPost = &Submitted{ - ID: "hw6l6a", - FullID: "t3_hw6l6a", - URL: "https://www.reddit.com/r/test/comments/hw6l6a/test_title/", -} - -var expectedEditedPost = &Post{ - ID: "hw6l6a", - FullID: "t3_hw6l6a", - Created: &Timestamp{time.Date(2020, 7, 23, 1, 24, 55, 0, time.UTC)}, - Edited: &Timestamp{time.Date(2020, 7, 23, 1, 42, 44, 0, time.UTC)}, - - Permalink: "/r/test/comments/hw6l6a/test_title/", - URL: "https://www.reddit.com/r/test/comments/hw6l6a/test_title/", - - Title: "Test Title", - Body: "this is edited", - - Likes: Bool(true), - - Score: 1, - UpvoteRatio: 1, - NumberOfComments: 0, - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - SubredditSubscribers: 8128, - - Author: "v_95", - AuthorID: "t2_164ab8", - - Spoiler: true, - IsSelfPost: true, -} - -var expectedPost2 = &Post{ - 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: 8278, - - Author: "v_95", - AuthorID: "t2_164ab8", -} - -var expectedPostDuplicates = []*Post{ - { - ID: "8kbs85", - FullID: "t3_8kbs85", - Created: &Timestamp{time.Date(2018, 5, 18, 9, 10, 18, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - Permalink: "/r/test/comments/8kbs85/test/", - URL: "http://example.com", - - Title: "test", - - Likes: nil, - - Score: 1, - UpvoteRatio: 0.66, - NumberOfComments: 1, - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - SubredditSubscribers: 8278, - - Author: "GarlicoinAccount", - AuthorID: "t2_d2v1r90", - }, - { - ID: "le1tc", - FullID: "t3_le1tc", - Created: &Timestamp{time.Date(2011, 10, 16, 13, 26, 40, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - Permalink: "/r/test/comments/le1tc/test_to_see_if_this_fixes_the_problem_of_my_likes/", - URL: "http://www.example.com", - - Title: "Test to see if this fixes the problem of my \"likes\" from the last 7 months vanishing.", - - Likes: nil, - - Score: 2, - UpvoteRatio: 1, - NumberOfComments: 1, - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - SubredditSubscribers: 8278, - - Author: "prog101", - AuthorID: "t2_8dyo", - }, -} - -func TestPostService_Get(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/post.json") - require.NoError(t, err) - - mux.HandleFunc("/comments/abc123", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - postAndComments, _, err := client.Post.Get(ctx, "abc123") - require.NoError(t, err) - require.Equal(t, expectedPostAndComments, postAndComments) -} - -func TestPostService_Duplicates(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/duplicates.json") - require.NoError(t, err) - - mux.HandleFunc("/duplicates/abc123", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("limit", "2") - form.Set("sr", "test") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - post, postDuplicates, resp, err := client.Post.Duplicates(ctx, "abc123", &ListDuplicatePostOptions{ - ListOptions: ListOptions{ - Limit: 2, - }, - Subreddit: "test", - }) - require.NoError(t, err) - require.Equal(t, expectedPost2, post) - require.Equal(t, expectedPostDuplicates, postDuplicates) - require.Equal(t, "t3_le1tc", resp.After) -} - -func TestPostService_SubmitText(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/submit.json") - require.NoError(t, err) - - mux.HandleFunc("/api/submit", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("kind", "self") - form.Set("sr", "test") - form.Set("title", "Test Title") - form.Set("text", "Test Text") - form.Set("spoiler", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - - fmt.Fprint(w, blob) - }) - - submittedPost, _, err := client.Post.SubmitText(ctx, SubmitTextRequest{ - Subreddit: "test", - Title: "Test Title", - Text: "Test Text", - Spoiler: true, - }) - require.NoError(t, err) - require.Equal(t, expectedSubmittedPost, submittedPost) -} - -func TestPostService_SubmitLink(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/submit.json") - require.NoError(t, err) - - mux.HandleFunc("/api/submit", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("kind", "link") - form.Set("sr", "test") - form.Set("title", "Test Title") - form.Set("url", "https://www.example.com") - form.Set("sendreplies", "false") - form.Set("resubmit", "true") - form.Set("nsfw", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - - fmt.Fprint(w, blob) - }) - - submittedPost, _, err := client.Post.SubmitLink(ctx, SubmitLinkRequest{ - Subreddit: "test", - Title: "Test Title", - URL: "https://www.example.com", - SendReplies: Bool(false), - Resubmit: true, - NSFW: true, - }) - require.NoError(t, err) - require.Equal(t, expectedSubmittedPost, submittedPost) -} - -func TestPostService_Edit(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/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", "t3_test") - form.Set("text", "test edit") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - - fmt.Fprint(w, blob) - }) - - editedPost, _, err := client.Post.Edit(ctx, "t3_test", "test edit") - require.NoError(t, err) - require.Equal(t, expectedEditedPost, editedPost) -} - -func TestPostService_Hide(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/hide", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("id", "1,2,3") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Post.Hide(ctx) - require.EqualError(t, err, "must provide at least 1 id") - - resp, err := client.Post.Hide(ctx, "1", "2", "3") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_Unhide(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/unhide", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("id", "1,2,3") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Post.Unhide(ctx) - require.EqualError(t, err, "must provide at least 1 id") - - resp, err := client.Post.Unhide(ctx, "1", "2", "3") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_MarkNSFW(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/marknsfw", 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) - }) - - resp, err := client.Post.MarkNSFW(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_UnmarkNSFW(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/unmarknsfw", 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) - }) - - resp, err := client.Post.UnmarkNSFW(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_Spoiler(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/spoiler", 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) - }) - - resp, err := client.Post.Spoiler(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_Unspoiler(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/unspoiler", 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) - }) - - resp, err := client.Post.Unspoiler(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_Sticky(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_subreddit_sticky", 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", "t3_test") - form.Set("state", "true") - form.Set("num", "1") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.Sticky(ctx, "t3_test", false) - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_Unsticky(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_subreddit_sticky", 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", "t3_test") - form.Set("state", "false") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.Unsticky(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_PinToProfile(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_subreddit_sticky", 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", "t3_test") - form.Set("state", "true") - form.Set("to_profile", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.PinToProfile(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_UnpinFromProfile(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_subreddit_sticky", 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", "t3_test") - form.Set("state", "false") - form.Set("to_profile", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.UnpinFromProfile(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortBest(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "confidence") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortBest(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortTop(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "top") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortTop(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortNew(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "new") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortNew(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortControversial(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "controversial") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortControversial(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortOld(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "old") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortOld(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortRandom(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "random") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortRandom(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortAMA(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "qa") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortAMA(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_SetSuggestedSortLive(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "live") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.SetSuggestedSortLive(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_ClearSuggestedSort(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_suggested_sort", 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", "t3_test") - form.Set("sort", "") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.ClearSuggestedSort(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_EnableContestMode(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_contest_mode", 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", "t3_test") - form.Set("state", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.EnableContestMode(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_DisableContestMode(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/set_contest_mode", 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", "t3_test") - form.Set("state", "false") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.DisableContestMode(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_LoadMoreReplies(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/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.Post.LoadMoreComments(ctx, nil) - require.EqualError(t, err, "*PostAndComments: cannot be nil") - - resp, err := client.Post.LoadMoreComments(ctx, &PostAndComments{}) - require.Nil(t, resp) - require.Nil(t, err) - - pc := &PostAndComments{ - Post: &Post{ - FullID: "t3_123", - }, - Comments: []*Comment{ - { - FullID: "t1_abc", - }, - }, - More: &More{ - Children: []string{"def", "ghi", "jkl"}, - }, - } - - _, err = client.Post.LoadMoreComments(ctx, pc) - require.NoError(t, err) - require.False(t, pc.HasMore()) - require.Len(t, pc.Comments, 2) - require.True(t, pc.Comments[1].HasMore()) - require.Len(t, pc.Comments[0].Replies.Comments, 1) - require.Len(t, pc.Comments[0].Replies.Comments[0].Replies.Comments, 1) -} - -func TestPostService_RandomFromSubreddits(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/post.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/random", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - postAndComments, _, err := client.Post.RandomFromSubreddits(ctx, "test") - require.NoError(t, err) - require.Equal(t, expectedPostAndComments, postAndComments) -} - -func TestPostService_Random(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/post.json") - require.NoError(t, err) - - mux.HandleFunc("/r/all/random", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - postAndComments, _, err := client.Post.Random(ctx) - require.NoError(t, err) - require.Equal(t, expectedPostAndComments, postAndComments) -} - -func TestPostService_RandomFromSubscriptions(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/post.json") - require.NoError(t, err) - - mux.HandleFunc("/random", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - postAndComments, _, err := client.Post.RandomFromSubscriptions(ctx) - require.NoError(t, err) - require.Equal(t, expectedPostAndComments, postAndComments) -} - -func TestPostService_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", "t3_test") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.Delete(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_test") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.Save(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_test") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.Unsave(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_test") - form.Set("state", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.EnableReplies(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_test") - form.Set("state", "false") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.DisableReplies(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_test") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.Lock(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_test") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - resp, err := client.Post.Unlock(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_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.Post.Upvote(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_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.Post.Downvote(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_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", "t3_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.Post.RemoveVote(ctx, "t3_test") - require.NoError(t, err) - require.Equal(t, http.StatusOK, resp.StatusCode) -} - -func TestPostService_MarkVisited(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/store_visits", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("links", "t3_test1,t3_test2,t3_test3") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Post.MarkVisited(ctx) - require.EqualError(t, err, "must provide at least 1 id") - - _, err = client.Post.MarkVisited(ctx, "t3_test1", "t3_test2", "t3_test3") - require.NoError(t, err) -} - -func TestPostService_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", "t3_test") - form.Set("reason", "test reason") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Post.Report(ctx, "t3_test", "test reason") - require.NoError(t, err) -} diff --git a/v2/reddit/reddit-oauth.go b/v2/reddit/reddit-oauth.go deleted file mode 100644 index 13030d6..0000000 --- a/v2/reddit/reddit-oauth.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -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, - } -} diff --git a/v2/reddit/reddit-options.go b/v2/reddit/reddit-options.go deleted file mode 100644 index 25d397b..0000000 --- a/v2/reddit/reddit-options.go +++ /dev/null @@ -1,78 +0,0 @@ -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: -// :: (by /u/) -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 -} diff --git a/v2/reddit/reddit-options_test.go b/v2/reddit/reddit-options_test.go deleted file mode 100644 index 3db5ce8..0000000 --- a/v2/reddit/reddit-options_test.go +++ /dev/null @@ -1,74 +0,0 @@ -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) -} diff --git a/v2/reddit/reddit-user-agent.go b/v2/reddit/reddit-user-agent.go deleted file mode 100644 index 0e69b67..0000000 --- a/v2/reddit/reddit-user-agent.go +++ /dev/null @@ -1,44 +0,0 @@ -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 -} diff --git a/v2/reddit/reddit.go b/v2/reddit/reddit.go deleted file mode 100644 index 6073ef8..0000000 --- a/v2/reddit/reddit.go +++ /dev/null @@ -1,671 +0,0 @@ -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 -} diff --git a/v2/reddit/reddit_test.go b/v2/reddit/reddit_test.go deleted file mode 100644 index 4853cab..0000000 --- a/v2/reddit/reddit_test.go +++ /dev/null @@ -1,295 +0,0 @@ -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) -} diff --git a/v2/reddit/stream.go b/v2/reddit/stream.go deleted file mode 100644 index 09c4d32..0000000 --- a/v2/reddit/stream.go +++ /dev/null @@ -1,116 +0,0 @@ -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 -} diff --git a/v2/reddit/stream_test.go b/v2/reddit/stream_test.go deleted file mode 100644 index 9ae48c6..0000000 --- a/v2/reddit/stream_test.go +++ /dev/null @@ -1,310 +0,0 @@ -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) -} diff --git a/v2/reddit/streamer.go b/v2/reddit/streamer.go deleted file mode 100644 index 2216ab8..0000000 --- a/v2/reddit/streamer.go +++ /dev/null @@ -1,44 +0,0 @@ -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()) -// } diff --git a/v2/reddit/subreddit.go b/v2/reddit/subreddit.go deleted file mode 100644 index dc94838..0000000 --- a/v2/reddit/subreddit.go +++ /dev/null @@ -1,1182 +0,0 @@ -package reddit - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "mime/multipart" - "net/http" - "net/url" - "os" - "path/filepath" - "strings" - - "github.com/google/go-querystring/query" -) - -// SubredditService handles communication with the subreddit -// related methods of the Reddit API. -// -// Reddit API docs: https://www.reddit.com/dev/api/#section_subreddits -type SubredditService struct { - client *Client -} - -type rootSubredditNames struct { - Names []string `json:"names,omitempty"` -} - -// Relationship holds information about a relationship (friend/blocked). -type Relationship struct { - ID string `json:"rel_id,omitempty"` - User string `json:"name,omitempty"` - UserID string `json:"id,omitempty"` - Created *Timestamp `json:"date,omitempty"` -} - -// Moderator is a user who moderates a subreddit. -type Moderator struct { - *Relationship - Permissions []string `json:"mod_permissions"` -} - -// Ban represents a banned relationship. -type Ban struct { - *Relationship - // nil means the ban is permanent - DaysLeft *int `json:"days_left"` - Note string `json:"note,omitempty"` -} - -// SubredditRule is a rule in the subreddit. -type SubredditRule struct { - // One of: comment, link (i.e. post), or all (i.e. both comment and link). - Kind string `json:"kind,omitempty"` - // Short description of the rule. - Name string `json:"short_name,omitempty"` - // The reason that will appear when a thing is reported in violation to this rule. - ViolationReason string `json:"violation_reason,omitempty"` - Description string `json:"description,omitempty"` - Priority int `json:"priority"` - Created *Timestamp `json:"created_utc,omitempty"` -} - -// SubredditRuleCreateRequest represents a request to add a subreddit rule. -type SubredditRuleCreateRequest struct { - // One of: comment, link (i.e. post) or all (i.e. both). - Kind string `url:"kind"` - // Short description of the rule. No longer than 100 characters. - Name string `url:"short_name"` - // The reason that will appear when a thing is reported in violation to this rule. - // If this is empty, Reddit will set its value to Name by default. - // No longer than 100 characters. - ViolationReason string `url:"violation_reason,omitempty"` - // Optional. No longer than 500 characters. - Description string `url:"description,omitempty"` -} - -func (r *SubredditRuleCreateRequest) validate() error { - if r == nil { - return errors.New("*SubredditRuleCreateRequest: cannot be nil") - } - - switch r.Kind { - case "comment", "link", "all": - // intentionally left blank - default: - return errors.New("(*SubredditRuleCreateRequest).Kind: must be one of: comment, link, all") - } - - if r.Name == "" || len(r.Name) > 100 { - return errors.New("(*SubredditRuleCreateRequest).Name: must be between 1-100 characters") - } - - if len(r.ViolationReason) > 100 { - return errors.New("(*SubredditRuleCreateRequest).ViolationReason: cannot be longer than 100 characters") - } - - if len(r.Description) > 500 { - return errors.New("(*SubredditRuleCreateRequest).Description: cannot be longer than 500 characters") - } - - return nil -} - -// SubredditTrafficStats hold information about subreddit traffic. -type SubredditTrafficStats struct { - // Traffic data is returned in the form of day, hour, and month. - // Start is a timestamp indicating the start of the category, i.e. - // start of the day for day, start of the hour for hour, and start of the month for month. - Start *Timestamp `json:"start"` - UniqueViews int `json:"unique_views"` - TotalViews int `json:"total_views"` - // This is only available for "day" traffic, not hour and month. - // Therefore, it is always 0 by default for hour and month. - Subscribers int `json:"subscribers"` -} - -// SubredditImage is an image part of the image set of a subreddit. -type SubredditImage struct { - Name string `json:"name"` - Link string `json:"link"` - URL string `json:"url"` -} - -// SubredditStyleSheet contains the subreddit's styling information. -type SubredditStyleSheet struct { - SubredditID string `json:"subreddit_id"` - Images []*SubredditImage `json:"images"` - StyleSheet string `json:"stylesheet"` -} - -// UnmarshalJSON implements the json.Unmarshaler interface. -func (s *SubredditTrafficStats) UnmarshalJSON(b []byte) error { - var data [4]int - err := json.Unmarshal(b, &data) - if err != nil { - return err - } - - timestampByteValue, err := json.Marshal(data[0]) - if err != nil { - return err - } - - timestamp := new(Timestamp) - err = timestamp.UnmarshalJSON(timestampByteValue) - if err != nil { - return err - } - - s.Start = timestamp - s.UniqueViews = data[1] - s.TotalViews = data[2] - s.Subscribers = data[3] - - return nil -} - -// SubredditSettings are a subreddit's settings. -type SubredditSettings struct { - // The id of the subreddit. - ID string `url:"-" json:"subreddit_id,omitempty"` - - // One of: public, restricted, private, gold_restricted, archived, employees_only, gold_only, user. - Type *string `url:"type,omitempty" json:"subreddit_type,omitempty"` - - // A valid IETF language tag (underscore separated). - Language *string `url:"lang,omitempty" json:"language,omitempty"` - - // No longer than 100 characters. - Title *string `url:"title,omitempty" json:"title,omitempty"` - // Raw markdown text. No longer than 500 characters. - Description *string `url:"public_description,omitempty" json:"public_description,omitempty"` - // Raw markdown text. No longer than 10240 characters. - Sidebar *string `url:"description,omitempty" json:"description,omitempty"` - // Raw markdown text. No longer than 1024 characters. - SubmissionText *string `url:"submit_text,omitempty" json:"submit_text,omitempty"` - // Raw markdown text. No longer than 5000 characters. - WelcomeMessage *string `url:"welcome_message_text,omitempty" json:"welcome_message_text,omitempty"` - WelcomeMessageEnabled *bool `url:"welcome_message_enabled,omitempty" json:"welcome_message_enabled,omitempty"` - - AllowCrossposts *bool `url:"allow_post_crossposts,omitempty" json:"allow_post_crossposts,omitempty"` - AllowChatPosts *bool `url:"allow_chat_post_creation,omitempty" json:"allow_chat_post_creation,omitempty"` - AllowPollPosts *bool `url:"allow_polls,omitempty" json:"allow_polls,omitempty"` - AllowFreeFormReports *bool `url:"free_form_reports,omitempty" json:"free_form_reports,omitempty"` - AllowOriginalContent *bool `url:"original_content_tag_enabled,omitempty" json:"original_content_tag_enabled,omitempty"` - // Allow image uploads and links to image hosting sites. - AllowImages *bool `url:"allow_images,omitempty" json:"allow_images,omitempty"` - AllowMultipleImagesPerPost *bool `url:"allow_galleries,omitempty" json:"allow_galleries,omitempty"` - - ExcludeSitewideBannedUsersContent *bool `url:"exclude_banned_modqueue,omitempty" json:"exclude_banned_modqueue,omitempty"` - - // An integer from 0 to 3. - CrowdControlChalLevel *int `url:"crowd_control_chat_level,omitempty" json:"crowd_control_chat_level,omitempty"` - - // Mark all posts in this subreddit as Original Content (OC) on the desktop redesign. - AllOriginalContent *bool `url:"all_original_content,omitempty" json:"all_original_content,omitempty"` - - // One of: none (recommended), confidence, top, new, controversial, old, random, qa, live. - SuggestedCommentSort *string `url:"suggested_comment_sort,omitempty" json:"suggested_comment_sort,omitempty"` - - // No longer than 60 characters. - SubmitLinkPostLabel *string `url:"submit_link_label,omitempty" json:"submit_link_label,omitempty"` - // No longer than 60 characters. - SubmitTextPostLabel *string `url:"submit_text_label,omitempty" json:"submit_text_label,omitempty"` - - // One of: any, link, self. - PostType *string `url:"link_type,omitempty" json:"content_options,omitempty"` - - // One of: low (disable most filtering), high (standard), all (filter everything, requiring mod approval). - SpamFilterStrengthLinkPosts *string `url:"spam_links,omitempty" json:"spam_links,omitempty"` - // One of: low (disable most filtering), high (standard), all (filter everything, requiring mod approval). - SpamFilterStrengthTextPosts *string `url:"spam_selfposts,omitempty" json:"spam_selfposts,omitempty"` - // One of: low (disable most filtering), high (standard), all (filter everything, requiring mod approval). - SpamFilterStrengthComments *string `url:"spam_comments,omitempty" json:"spam_comments,omitempty"` - - ShowContentThumbnails *bool `url:"show_media,omitempty" json:"show_media,omitempty"` - ExpandMediaPreviewsOnCommentsPages *bool `url:"show_media_preview,omitempty" json:"show_media_preview,omitempty"` - - CollapseDeletedComments *bool `url:"collapse_deleted_comments,omitempty" json:"collapse_deleted_comments,omitempty"` - // An integer between 0 and 1440. - MinutesToHideCommentScores *int `url:"comment_score_hide_mins,omitempty" json:"comment_score_hide_mins,omitempty"` - - // Enable marking posts as containing spoilers. - SpoilersEnabled *bool `url:"spoilers_enabled,omitempty" json:"spoilers_enabled,omitempty"` - - // If there's an image header set, hovering the mouse over it will display this text. - HeaderMouseoverText *string `url:"header-title,omitempty" json:"header_hover_text,omitempty"` - - // 6-digit rgb hex colour, e.g. #AABBCC. - // Thematic colour for the subreddit on mobile. - MobileColour *string `url:"key_color,omitempty" json:"key_color,omitempty"` - - // Can only be set to true if subreddit type is gold_only. - HideAds *bool `url:"hide_ads,omitempty" json:"hide_ads,omitempty"` - // Require viewers to be over 18 years old. - NSFW *bool `url:"over_18,omitempty" json:"over_18,omitempty"` - - // Show up in high-traffic feeds: Allow your community to be in r/all, r/popular, and trending lists where it can be - // seen by the general Reddit population. - AllowDiscoveryInHighTrafficFeeds *bool `url:"allow_top,omitempty" json:"default_set,omitempty"` - // Get recommended to individual redditors. Let Reddit recommend your community to people who have similar interests. - AllowDiscoveryByIndividualUsers *bool `url:"allow_discovery,omitempty" json:"allow_discovery,omitempty"` - - // One of: - // - disabled: wiki is disabled for everyone except mods. - // - modonly: only mods, approved wiki contributors, or those on a page's edit list may edit. - // - anyone: anyone who can submit to the subreddit may edit. - WikiMode *string `url:"wikimode,omitempty" json:"wikimode,omitempty"` - // Account age (in days) required to create and edit wiki pages. - WikiMinimumAccountAge *int `url:"wiki_edit_age,omitempty" json:"wiki_edit_age,omitempty"` - // Subreddit karma required to create and edit wiki pages. - WikiMinimumKarma *int `url:"wiki_edit_karma,omitempty" json:"wiki_edit_karma,omitempty"` -} - -// SubredditPostRequirements is a list of moderator-designed requirements to post to a subreddit. -type SubredditPostRequirements struct { - // Sentence or two on how to successfully post to the subreddit. - Guidelines string `json:"guidelines_text"` - GuidelinesDisplayPolicy string `json:"guidelines_display_policy"` - - TitleMinLength int `json:"title_text_min_length"` - TitleMaxLength int `json:"title_text_max_length"` - - BodyMinLength int `json:"body_text_min_length"` - BodyMaxLength int `json:"body_text_max_length"` - - // Do not allow any of these words in the title. - TitleBlacklistedStrings []string `json:"title_blacklisted_strings"` - // Do not allow any of these words in the body. - BodyBlacklistedStrings []string `json:"body_blacklisted_strings"` - - // Require at least one of these words in the title. - TitleRequiredStrings []string `json:"title_required_strings"` - // Require at least one of these words in the body. - BodyRequiredStrings []string `json:"body_required_strings"` - - // Block link posts with these domains. - DomainBlacklist []string `json:"domain_blacklist"` - // Only allow link posts with these domains. - DomainWhitelist []string `json:"domain_whitelist"` - - // One of: required, notAllowed, none. - // Meaning that a selftext post's body is required, not allowed, or optional, respectively. - BodyRestrictionPolicy string `json:"body_restriction_policy"` - LinkRestrictionPolicy string `json:"link_restriction_policy"` - - GalleryMinItems int `json:"gallery_min_items"` - GalleryMaxItems int `json:"gallery_max_items"` - GalleryCaptionsRequirement string `json:"gallery_captions_requirement"` - GalleryURLsRequirement string `json:"gallery_urls_requirement"` - - // Prevent users from posting a link that was already posted to your community within x days of the original. - LinkRepostAge int `json:"link_repost_age"` - FlairRequired bool `json:"is_flair_required"` - - TitleRegexes []string `json:"title_regexes"` - BodyRegexes []string `json:"body_regexes"` -} - -func (s *SubredditService) getPosts(ctx context.Context, sort string, subreddit string, opts interface{}) ([]*Post, *Response, error) { - path := sort - if subreddit != "" { - path = fmt.Sprintf("r/%s/%s", subreddit, sort) - } - l, resp, err := s.client.getListing(ctx, path, opts) - if err != nil { - return nil, resp, err - } - return l.Posts(), resp, nil -} - -// HotPosts returns the hottest posts from the specified subreddit. -// To search through multiple, separate the names with a plus (+), e.g. "golang+test". -// If none are defined, it returns the ones from your subscribed subreddits. -// To search through all, just specify "all". -// To search through all and filter out subreddits, provide "all-name1-name2". -// Note: when looking for hot posts in a subreddit, it will include the stickied -// posts (if any) PLUS posts from the limit parameter (25 by default). -func (s *SubredditService) HotPosts(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, *Response, error) { - return s.getPosts(ctx, "hot", subreddit, opts) -} - -// NewPosts returns the newest posts from the specified subreddit. -// To search through multiple, separate the names with a plus (+), e.g. "golang+test". -// If none are defined, it returns the ones from your subscribed subreddits. -// To search through all, just specify "all". -// To search through all and filter out subreddits, provide "all-name1-name2". -func (s *SubredditService) NewPosts(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, *Response, error) { - return s.getPosts(ctx, "new", subreddit, opts) -} - -// RisingPosts returns the rising posts from the specified subreddit. -// To search through multiple, separate the names with a plus (+), e.g. "golang+test". -// If none are defined, it returns the ones from your subscribed subreddits. -// To search through all, just specify "all". -// To search through all and filter out subreddits, provide "all-name1-name2". -func (s *SubredditService) RisingPosts(ctx context.Context, subreddit string, opts *ListOptions) ([]*Post, *Response, error) { - return s.getPosts(ctx, "rising", subreddit, opts) -} - -// ControversialPosts returns the most controversial posts from the specified subreddit. -// To search through multiple, separate the names with a plus (+), e.g. "golang+test". -// If none are defined, it returns the ones from your subscribed subreddits. -// To search through all, just specify "all". -// To search through all and filter out subreddits, provide "all-name1-name2". -func (s *SubredditService) ControversialPosts(ctx context.Context, subreddit string, opts *ListPostOptions) ([]*Post, *Response, error) { - return s.getPosts(ctx, "controversial", subreddit, opts) -} - -// TopPosts returns the top posts from the specified subreddit. -// To search through multiple, separate the names with a plus (+), e.g. "golang+test". -// If none are defined, it returns the ones from your subscribed subreddits. -// To search through all, just specify "all". -// To search through all and filter out subreddits, provide "all-name1-name2". -func (s *SubredditService) TopPosts(ctx context.Context, subreddit string, opts *ListPostOptions) ([]*Post, *Response, error) { - return s.getPosts(ctx, "top", subreddit, opts) -} - -// Get a subreddit by name. -func (s *SubredditService) Get(ctx context.Context, name string) (*Subreddit, *Response, error) { - if name == "" { - return nil, nil, errors.New("name: cannot be empty") - } - - path := fmt.Sprintf("r/%s/about", name) - t, resp, err := s.client.getThing(ctx, path, nil) - if err != nil { - return nil, resp, err - } - - sr, _ := t.Subreddit() - return sr, resp, nil -} - -// Popular returns popular subreddits. -func (s *SubredditService) Popular(ctx context.Context, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - return s.getSubreddits(ctx, "subreddits/popular", opts) -} - -// New returns new subreddits. -func (s *SubredditService) New(ctx context.Context, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - return s.getSubreddits(ctx, "subreddits/new", opts) -} - -// Gold returns gold subreddits (i.e. only accessible to users with gold). -// It seems like it returns an empty list if you don't have gold. -func (s *SubredditService) Gold(ctx context.Context, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - return s.getSubreddits(ctx, "subreddits/gold", opts) -} - -// Default returns default subreddits. -func (s *SubredditService) Default(ctx context.Context, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - return s.getSubreddits(ctx, "subreddits/default", opts) -} - -// Subscribed returns the list of subreddits you are subscribed to. -func (s *SubredditService) Subscribed(ctx context.Context, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - return s.getSubreddits(ctx, "subreddits/mine/subscriber", opts) -} - -// Approved returns the list of subreddits you are an approved user in. -func (s *SubredditService) Approved(ctx context.Context, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - return s.getSubreddits(ctx, "subreddits/mine/contributor", opts) -} - -// Moderated returns the list of subreddits you are a moderator of. -func (s *SubredditService) Moderated(ctx context.Context, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - return s.getSubreddits(ctx, "subreddits/mine/moderator", opts) -} - -// GetSticky1 returns the first stickied post on a subreddit (if it exists). -func (s *SubredditService) GetSticky1(ctx context.Context, subreddit string) (*PostAndComments, *Response, error) { - return s.getSticky(ctx, subreddit, 1) -} - -// GetSticky2 returns the second stickied post on a subreddit (if it exists). -func (s *SubredditService) GetSticky2(ctx context.Context, subreddit string) (*PostAndComments, *Response, error) { - return s.getSticky(ctx, subreddit, 2) -} - -func (s *SubredditService) handleSubscription(ctx context.Context, form url.Values) (*Response, error) { - path := "api/subscribe" - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - return s.client.Do(ctx, req, nil) -} - -// Subscribe subscribes to subreddits based on their names. -func (s *SubredditService) Subscribe(ctx context.Context, subreddits ...string) (*Response, error) { - form := url.Values{} - form.Set("action", "sub") - form.Set("sr_name", strings.Join(subreddits, ",")) - return s.handleSubscription(ctx, form) -} - -// SubscribeByID subscribes to subreddits based on their id. -func (s *SubredditService) SubscribeByID(ctx context.Context, ids ...string) (*Response, error) { - form := url.Values{} - form.Set("action", "sub") - form.Set("sr", strings.Join(ids, ",")) - return s.handleSubscription(ctx, form) -} - -// Unsubscribe unsubscribes from subreddits based on their names. -func (s *SubredditService) Unsubscribe(ctx context.Context, subreddits ...string) (*Response, error) { - form := url.Values{} - form.Set("action", "unsub") - form.Set("sr_name", strings.Join(subreddits, ",")) - return s.handleSubscription(ctx, form) -} - -// UnsubscribeByID unsubscribes from subreddits based on their id. -func (s *SubredditService) UnsubscribeByID(ctx context.Context, ids ...string) (*Response, error) { - form := url.Values{} - form.Set("action", "unsub") - form.Set("sr", strings.Join(ids, ",")) - return s.handleSubscription(ctx, form) -} - -// Favorite the subreddit. -func (s *SubredditService) Favorite(ctx context.Context, subreddit string) (*Response, error) { - path := "api/favorite" - - form := url.Values{} - form.Set("sr_name", subreddit) - form.Set("make_favorite", "true") - form.Set("api_type", "json") - - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// Unfavorite the subreddit. -func (s *SubredditService) Unfavorite(ctx context.Context, subreddit string) (*Response, error) { - path := "api/favorite" - - form := url.Values{} - form.Set("sr_name", subreddit) - form.Set("make_favorite", "false") - form.Set("api_type", "json") - - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// Search for subreddits. -func (s *SubredditService) Search(ctx context.Context, query string, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - path := fmt.Sprintf("subreddits/search?q=%s", query) - l, resp, err := s.client.getListing(ctx, path, opts) - if err != nil { - return nil, resp, err - } - return l.Subreddits(), resp, nil -} - -// SearchNames searches for subreddits with names beginning with the query provided. -func (s *SubredditService) SearchNames(ctx context.Context, query string) ([]string, *Response, error) { - path := fmt.Sprintf("api/search_reddit_names?query=%s", query) - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(rootSubredditNames) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.Names, resp, nil -} - -// SearchPosts searches for posts in the specified subreddit. -// To search through multiple, separate the names with a plus (+), e.g. "golang+test". -// If no subreddit is provided, the search is run against r/all. -func (s *SubredditService) SearchPosts(ctx context.Context, query string, subreddit string, opts *ListPostSearchOptions) ([]*Post, *Response, error) { - if subreddit == "" { - subreddit = "all" - } - - path := fmt.Sprintf("r/%s/search", subreddit) - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - notAll := !strings.EqualFold(subreddit, "all") - - params := struct { - Query string `url:"q"` - RestrictSubreddits bool `url:"restrict_sr,omitempty"` - }{query, notAll} - - t, resp, err := s.client.getThing(ctx, path, params) - if err != nil { - return nil, resp, err - } - - l, _ := t.Listing() - return l.Posts(), resp, nil -} - -func (s *SubredditService) getSubreddits(ctx context.Context, path string, opts *ListSubredditOptions) ([]*Subreddit, *Response, error) { - l, resp, err := s.client.getListing(ctx, path, opts) - if err != nil { - return nil, resp, err - } - return l.Subreddits(), resp, nil -} - -// getSticky returns one of the 2 stickied posts of the subreddit (if they exist). -// Num should be equal to 1 or 2, depending on which one you want. -func (s *SubredditService) getSticky(ctx context.Context, subreddit string, num int) (*PostAndComments, *Response, error) { - params := struct { - Num int `url:"num"` - }{num} - - path := fmt.Sprintf("r/%s/about/sticky", subreddit) - 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 := new(PostAndComments) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root, resp, nil -} - -// todo: sr_detail's NSFW indicator is over_18 instead of over18 -func (s *SubredditService) random(ctx context.Context, nsfw bool) (*Subreddit, *Response, error) { - path := "r/random" - if nsfw { - path = "r/randnsfw" - } - - params := struct { - ExpandSubreddit bool `url:"sr_detail"` - Limit int `url:"limit,omitempty"` - }{true, 1} - - 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 := new(struct { - Data struct { - Children [1]struct { - Data struct { - Subreddit *Subreddit `json:"sr_detail"` - } `json:"data"` - } `json:"children"` - } `json:"data"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - subreddit := root.Data.Children[0].Data.Subreddit - return subreddit, resp, nil -} - -// Random returns a random SFW subreddit. -func (s *SubredditService) Random(ctx context.Context) (*Subreddit, *Response, error) { - return s.random(ctx, false) -} - -// RandomNSFW returns a random NSFW subreddit. -func (s *SubredditService) RandomNSFW(ctx context.Context) (*Subreddit, *Response, error) { - return s.random(ctx, true) -} - -// SubmissionText gets the submission text for the subreddit. -// This text is set by the subreddit moderators and intended to be displayed on the submission form. -func (s *SubredditService) SubmissionText(ctx context.Context, name string) (string, *Response, error) { - if name == "" { - return "", nil, errors.New("name: cannot be empty") - } - - path := fmt.Sprintf("r/%s/api/submit_text", name) - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return "", nil, err - } - - root := new(struct { - Text string `json:"submit_text"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return "", resp, err - } - - return root.Text, resp, err -} - -// Banned gets banned users from the subreddit. -func (s *SubredditService) Banned(ctx context.Context, subreddit string, opts *ListOptions) ([]*Ban, *Response, error) { - path := fmt.Sprintf("r/%s/about/banned", subreddit) - - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(struct { - Data struct { - Bans []*Ban `json:"children"` - After string `json:"after"` - } `json:"data"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - resp.After = root.Data.After - return root.Data.Bans, resp, nil -} - -// Muted gets muted users from the subreddit. -func (s *SubredditService) Muted(ctx context.Context, subreddit string, opts *ListOptions) ([]*Relationship, *Response, error) { - path := fmt.Sprintf("r/%s/about/muted", subreddit) - - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(struct { - Data struct { - Relationships []*Relationship `json:"children"` - After string `json:"after"` - } `json:"data"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - resp.After = root.Data.After - return root.Data.Relationships, resp, nil -} - -// WikiBanned gets banned users from the subreddit. -func (s *SubredditService) WikiBanned(ctx context.Context, subreddit string, opts *ListOptions) ([]*Ban, *Response, error) { - path := fmt.Sprintf("r/%s/about/wikibanned", subreddit) - - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(struct { - Data struct { - Bans []*Ban `json:"children"` - After string `json:"after"` - } `json:"data"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - resp.After = root.Data.After - return root.Data.Bans, resp, nil -} - -// Contributors gets contributors (also known as approved users) from the subreddit. -func (s *SubredditService) Contributors(ctx context.Context, subreddit string, opts *ListOptions) ([]*Relationship, *Response, error) { - path := fmt.Sprintf("r/%s/about/contributors", subreddit) - - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(struct { - Data struct { - Relationships []*Relationship `json:"children"` - After string `json:"after"` - } `json:"data"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - resp.After = root.Data.After - return root.Data.Relationships, resp, nil -} - -// WikiContributors gets contributors of the wiki from the subreddit. -func (s *SubredditService) WikiContributors(ctx context.Context, subreddit string, opts *ListOptions) ([]*Relationship, *Response, error) { - path := fmt.Sprintf("r/%s/about/wikicontributors", subreddit) - - path, err := addOptions(path, opts) - if err != nil { - return nil, nil, err - } - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(struct { - Data struct { - Relationships []*Relationship `json:"children"` - After string `json:"after"` - } `json:"data"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - resp.After = root.Data.After - return root.Data.Relationships, resp, nil -} - -// Moderators gets the moderators of the subreddit. -func (s *SubredditService) Moderators(ctx context.Context, subreddit string) ([]*Moderator, *Response, error) { - path := fmt.Sprintf("r/%s/about/moderators", subreddit) - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(struct { - Data struct { - Moderators []*Moderator `json:"children"` - } `json:"data"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.Data.Moderators, resp, nil -} - -// Rules gets the rules of the subreddit. -func (s *SubredditService) Rules(ctx context.Context, subreddit string) ([]*SubredditRule, *Response, error) { - path := fmt.Sprintf("r/%s/about/rules", subreddit) - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(struct { - Rules []*SubredditRule `json:"rules"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root.Rules, resp, nil -} - -// CreateRule adds a rule to the subreddit. -func (s *SubredditService) CreateRule(ctx context.Context, subreddit string, request *SubredditRuleCreateRequest) (*Response, error) { - err := request.validate() - if err != nil { - return nil, err - } - - form, err := query.Values(request) - if err != nil { - return nil, err - } - form.Set("api_type", "json") - - path := fmt.Sprintf("r/%s/api/add_subreddit_rule", subreddit) - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// Traffic gets the traffic data of the subreddit. -// It returns traffic data by day, hour, and month, respectively. -func (s *SubredditService) Traffic(ctx context.Context, subreddit string) ([]*SubredditTrafficStats, []*SubredditTrafficStats, []*SubredditTrafficStats, *Response, error) { - path := fmt.Sprintf("r/%s/about/traffic", subreddit) - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, nil, nil, err - } - - root := new(struct { - Day []*SubredditTrafficStats `json:"day"` - Hour []*SubredditTrafficStats `json:"hour"` - Month []*SubredditTrafficStats `json:"month"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, nil, nil, resp, err - } - - return root.Day, root.Hour, root.Month, resp, nil -} - -// StyleSheet returns the subreddit's style sheet, as well as some information about images. -func (s *SubredditService) StyleSheet(ctx context.Context, subreddit string) (*SubredditStyleSheet, *Response, error) { - path := fmt.Sprintf("r/%s/about/stylesheet", subreddit) - t, resp, err := s.client.getThing(ctx, path, nil) - if err != nil { - return nil, resp, err - } - styleSheet, _ := t.StyleSheet() - return styleSheet, resp, nil -} - -// StyleSheetRaw returns the subreddit's style sheet with all comments and newlines stripped. -func (s *SubredditService) StyleSheetRaw(ctx context.Context, subreddit string) (string, *Response, error) { - path := fmt.Sprintf("r/%s/stylesheet", subreddit) - - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return "", nil, err - } - - buf := new(bytes.Buffer) - resp, err := s.client.Do(ctx, req, buf) - if err != nil { - return "", resp, err - } - - return buf.String(), resp, nil -} - -// UpdateStyleSheet updates the style sheet of the subreddit. -// Providing a reason is optional. -func (s *SubredditService) UpdateStyleSheet(ctx context.Context, subreddit, styleSheet, reason string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/subreddit_stylesheet", subreddit) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("op", "save") - form.Set("stylesheet_contents", styleSheet) - if reason != "" { - 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) -} - -// RemoveImage removes an image from the subreddit's custom image set. -// The call succeeds even if the named image does not exist. -func (s *SubredditService) RemoveImage(ctx context.Context, subreddit, imageName string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/delete_sr_img", subreddit) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("img_name", imageName) - - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// RemoveHeader removes the subreddit's current header image. -// The call succeeds even if there's no header image. -func (s *SubredditService) RemoveHeader(ctx context.Context, subreddit string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/delete_sr_header", 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) -} - -// RemoveMobileHeader removes the subreddit's current mobile header. -// The call succeeds even if there's no mobile header. -func (s *SubredditService) RemoveMobileHeader(ctx context.Context, subreddit string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/delete_sr_banner", 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) -} - -// RemoveMobileIcon removes the subreddit's current mobile icon. -// The call succeeds even if there's no mobile icon. -func (s *SubredditService) RemoveMobileIcon(ctx context.Context, subreddit string) (*Response, error) { - path := fmt.Sprintf("r/%s/api/delete_sr_icon", 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) -} - -func (s *SubredditService) uploadImage(ctx context.Context, subreddit, imagePath, imageType, imageName string) (string, *Response, error) { - file, err := os.Open(imagePath) - if err != nil { - return "", nil, err - } - defer file.Close() - - form := url.Values{} - form.Set("upload_type", imageType) - form.Set("name", imageName) - form.Set("img_type", "png") - - ext := filepath.Ext(file.Name()) - if strings.EqualFold(ext, ".jpg") { - form.Set("img_type", "jpg") - } - - body := new(bytes.Buffer) - writer := multipart.NewWriter(body) - - for k := range form { - writer.WriteField(k, form.Get(k)) - } - - 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 - } - - path := fmt.Sprintf("r/%s/api/upload_sr_img", subreddit) - u, err := s.client.BaseURL.Parse(path) - if err != nil { - return "", nil, err - } - - req, err := http.NewRequest(http.MethodPost, u.String(), body) - if err != nil { - return "", nil, err - } - req.Header.Set(headerContentType, writer.FormDataContentType()) - - root := new(struct { - Errors []string `json:"errors"` - ErrorValues []string `json:"errors_values"` - ImageSource string `json:"img_src"` - }) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return "", resp, err - } - - if len(root.ErrorValues) > 0 { - err = fmt.Errorf("could not upload image: %s", strings.Join(root.ErrorValues, "; ")) - return "", resp, err - } - - return root.ImageSource, resp, nil -} - -// UploadImage uploads an image to the subreddit. -// If an image with the image name already exists, it it replaced. -// A successful call returns a link to the uploaded image. -func (s *SubredditService) UploadImage(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { - return s.uploadImage(ctx, subreddit, imagePath, "img", imageName) -} - -// UploadHeader uploads an image to be user as the subreddit's header image. -// A successful call returns a link to the uploaded image. -func (s *SubredditService) UploadHeader(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { - return s.uploadImage(ctx, subreddit, imagePath, "header", imageName) -} - -// UploadMobileHeader uploads an image to be user as the subreddit's mobile header image. -// A successful call returns a link to the uploaded image. -func (s *SubredditService) UploadMobileHeader(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { - return s.uploadImage(ctx, subreddit, imagePath, "banner", imageName) -} - -// UploadMobileIcon uploads an image to be user as the subreddit's mobile icon. -// A successful call returns a link to the uploaded image. -func (s *SubredditService) UploadMobileIcon(ctx context.Context, subreddit, imagePath, imageName string) (string, *Response, error) { - return s.uploadImage(ctx, subreddit, imagePath, "icon", imageName) -} - -// Create a subreddit. -func (s *SubredditService) Create(ctx context.Context, name string, request *SubredditSettings) (*Response, error) { - if request == nil { - return nil, errors.New("*SubredditSettings: cannot be nil") - } - - form, err := query.Values(request) - if err != nil { - return nil, err - } - form.Set("name", name) - form.Set("api_type", "json") - - path := "api/site_admin" - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// Edit a subreddit. -// This endpoint expects all values of the request to be provided. -// To make this easier, it might be useful to get the subreddit's current settings via GetSettings(), -// and use that as a starting point. It even returns the subreddit's ID, which is needed for this. -func (s *SubredditService) Edit(ctx context.Context, subredditID string, request *SubredditSettings) (*Response, error) { - if request == nil { - return nil, errors.New("*SubredditSettings: cannot be nil") - } - - form, err := query.Values(request) - if err != nil { - return nil, err - } - form.Set("sr", subredditID) - form.Set("api_type", "json") - - path := "api/site_admin" - req, err := s.client.NewRequest(http.MethodPost, path, form) - if err != nil { - return nil, err - } - - return s.client.Do(ctx, req, nil) -} - -// GetSettings gets the settings of a subreddit. -func (s *SubredditService) GetSettings(ctx context.Context, subreddit string) (*SubredditSettings, *Response, error) { - path := fmt.Sprintf("r/%s/about/edit", subreddit) - t, resp, err := s.client.getThing(ctx, path, nil) - if err != nil { - return nil, resp, err - } - settings, _ := t.SubredditSettings() - return settings, resp, nil -} - -// PostRequirements returns the subreddit's moderator-designed requirements to post to it. -// Clients may use the values returned by this method to pre-validate submissions to the subreddit. -func (s *SubredditService) PostRequirements(ctx context.Context, subreddit string) (*SubredditPostRequirements, *Response, error) { - path := fmt.Sprintf("api/v1/%s/post_requirements", subreddit) - req, err := s.client.NewRequest(http.MethodGet, path, nil) - if err != nil { - return nil, nil, err - } - - root := new(SubredditPostRequirements) - resp, err := s.client.Do(ctx, req, root) - if err != nil { - return nil, resp, err - } - - return root, resp, nil -} diff --git a/v2/reddit/subreddit_test.go b/v2/reddit/subreddit_test.go deleted file mode 100644 index 9bf9049..0000000 --- a/v2/reddit/subreddit_test.go +++ /dev/null @@ -1,1722 +0,0 @@ -package reddit - -import ( - "bytes" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/url" - "os" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/require" -) - -var expectedPosts = []*Post{ - { - ID: "agi5zf", - FullID: "t3_agi5zf", - Created: &Timestamp{time.Date(2019, 1, 16, 5, 57, 51, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - Permalink: "/r/test/comments/agi5zf/test/", - URL: "https://www.reddit.com/r/test/comments/agi5zf/test/", - - Title: "test", - Body: "test", - - Score: 253, - UpvoteRatio: 0.99, - NumberOfComments: 1634, - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - SubredditSubscribers: 8154, - - Author: "kmiller0112", - AuthorID: "t2_30a5ktgt", - - IsSelfPost: true, - Stickied: true, - }, - { - ID: "hyhquk", - FullID: "t3_hyhquk", - Created: &Timestamp{time.Date(2020, 7, 27, 0, 5, 10, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - Permalink: "/r/test/comments/hyhquk/veggies/", - URL: "https://i.imgur.com/LrN2mPw.jpg", - - Title: "Veggies", - - Score: 4, - UpvoteRatio: 1, - NumberOfComments: 0, - - SubredditName: "test", - SubredditNamePrefixed: "r/test", - SubredditID: "t5_2qh23", - SubredditSubscribers: 8154, - - Author: "MuckleMcDuckle", - AuthorID: "t2_6fqntbwq", - }, -} - -var expectedSubreddit = &Subreddit{ - ID: "2rc7j", - FullID: "t5_2rc7j", - Created: &Timestamp{time.Date(2009, 11, 11, 0, 54, 28, 0, time.UTC)}, - - URL: "/r/golang/", - Name: "golang", - NamePrefixed: "r/golang", - Title: "The Go Programming Language", - Description: "Ask questions and post articles about the Go programming language and related tools, events etc.", - Type: "public", - - Subscribers: 116532, - ActiveUserCount: Int(386), - NSFW: false, - UserIsMod: false, - Subscribed: true, -} - -var expectedSubreddits = []*Subreddit{ - { - ID: "2qs0k", - FullID: "t5_2qs0k", - Created: &Timestamp{time.Date(2009, 1, 25, 2, 25, 57, 0, time.UTC)}, - - URL: "/r/Home/", - Name: "Home", - NamePrefixed: "r/Home", - Title: "Home", - Type: "public", - - Subscribers: 15336, - NSFW: false, - UserIsMod: false, - Subscribed: true, - Favorite: false, - }, - { - ID: "2qh1i", - FullID: "t5_2qh1i", - Created: &Timestamp{time.Date(2008, 1, 25, 3, 52, 15, 0, time.UTC)}, - - URL: "/r/AskReddit/", - Name: "AskReddit", - NamePrefixed: "r/AskReddit", - Title: "Ask Reddit...", - Description: "r/AskReddit is the place to ask and answer thought-provoking questions.", - Type: "public", - - Subscribers: 28449174, - NSFW: false, - UserIsMod: false, - Subscribed: true, - Favorite: true, - }, - { - ID: "2qh0u", - FullID: "t5_2qh0u", - Created: &Timestamp{time.Date(2008, 1, 25, 0, 31, 9, 0, time.UTC)}, - - URL: "/r/pics/", - Name: "pics", - NamePrefixed: "r/pics", - Title: "Reddit Pics", - Description: "A place for pictures and photographs.", - Type: "public", - - Subscribers: 24987753, - NSFW: false, - UserIsMod: false, - Subscribed: false, - Favorite: false, - }, -} - -var expectedSubredditNames = []string{ - "golang", - "golang_infosec", - "GolangJobOfferings", - "golanguage", - "golang2", - "GolangUnofficial", - "golang_jobs", -} - -var expectedSearchPosts = []*Post{ - { - ID: "hybow9", - FullID: "t3_hybow9", - Created: &Timestamp{time.Date(2020, 7, 26, 18, 14, 24, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - Permalink: "/r/WatchPeopleDieInside/comments/hybow9/pregnancy_test/", - URL: "https://v.redd.it/ra4qnt8bt8d51", - - Title: "Pregnancy test", - - Score: 103829, - UpvoteRatio: 0.88, - NumberOfComments: 3748, - - SubredditName: "WatchPeopleDieInside", - SubredditNamePrefixed: "r/WatchPeopleDieInside", - SubredditID: "t5_3h4zq", - SubredditSubscribers: 2599948, - - Author: "chocolat_ice_cream", - AuthorID: "t2_3p32m02", - }, - { - ID: "hmwhd7", - FullID: "t3_hmwhd7", - Created: &Timestamp{time.Date(2020, 7, 7, 15, 19, 42, 0, time.UTC)}, - Edited: &Timestamp{time.Date(1, 1, 1, 0, 0, 0, 0, time.UTC)}, - - Permalink: "/r/worldnews/comments/hmwhd7/brazilian_president_jair_bolsonaro_tests_positive/", - URL: "https://www.theguardian.com/world/2020/jul/07/jair-bolsonaro-coronavirus-positive-test-brazil-president", - - Title: "Brazilian president Jair Bolsonaro tests positive for coronavirus", - - Score: 149238, - UpvoteRatio: 0.94, - NumberOfComments: 7415, - - SubredditName: "worldnews", - SubredditNamePrefixed: "r/worldnews", - SubredditID: "t5_2qh13", - SubredditSubscribers: 24651441, - - Author: "Jeremy_Martin", - AuthorID: "t2_wgrkg", - }, -} - -var expectedRandomSubreddit = &Subreddit{ - FullID: "t5_2wi4l", - Created: &Timestamp{time.Date(2013, 3, 1, 4, 4, 18, 0, time.UTC)}, - - URL: "/r/GalaxyS8/", - Name: "GalaxyS8", - NamePrefixed: "r/GalaxyS8", - Title: "Samsung Galaxy S8", - Description: "The only place for news, discussion, photos, and everything else Samsung Galaxy S8.", - Type: "public", - - Subscribers: 52357, -} - -var expectedRelationships3 = []*Relationship{ - { - ID: "rel_id1", - Created: &Timestamp{time.Date(2020, 8, 11, 2, 35, 2, 0, time.UTC)}, - User: "testuser1", - UserID: "t2_user1", - }, - { - ID: "rel_id2", - Created: &Timestamp{time.Date(2020, 8, 11, 2, 35, 0, 0, time.UTC)}, - User: "testuser2", - UserID: "t2_user2", - }, -} - -var expectedBans = []*Ban{ - { - Relationship: &Relationship{ - ID: "rb_123", - Created: &Timestamp{time.Date(2020, 8, 11, 2, 35, 2, 0, time.UTC)}, - - User: "testuser1", - UserID: "t2_user1", - }, - - DaysLeft: Int(43), - Note: "Spam", - }, - { - Relationship: &Relationship{ - ID: "rb_456", - Created: &Timestamp{time.Date(2020, 8, 11, 2, 35, 0, 0, time.UTC)}, - - User: "testuser2", - UserID: "t2_user2", - }, - - DaysLeft: nil, - Note: "Spam", - }, -} - -var expectedModerators = []*Moderator{ - { - Relationship: &Relationship{ - ID: "rb_tmatb9", - User: "testuser1", - UserID: "t2_test1", - Created: &Timestamp{time.Date(2013, 7, 29, 20, 44, 27, 0, time.UTC)}, - }, - Permissions: []string{"all"}, - }, - { - Relationship: &Relationship{ - ID: "rb_5c9s4d", - User: "testuser2", - UserID: "t2_test2", - Created: &Timestamp{time.Date(2014, 3, 1, 18, 13, 53, 0, time.UTC)}, - }, - Permissions: []string{"all"}, - }, -} - -var expectedRules = []*SubredditRule{ - { - Kind: "link", - Name: "Read the Rules Before Posting", - ViolationReason: "Read the Rules Before Posting", - Description: "https://www.reddit.com/r/Fitness/wiki/rules", - Priority: 0, - Created: &Timestamp{time.Date(2019, 5, 22, 5, 32, 58, 0, time.UTC)}, - }, - { - Kind: "link", - Name: "Read the Wiki Before Posting", - ViolationReason: "Read the Wiki Before Posting", - Description: "https://thefitness.wiki", - Priority: 1, - Created: &Timestamp{time.Date(2019, 11, 9, 7, 56, 33, 0, time.UTC)}, - }, -} - -var expectedDayTraffic = []*SubredditTrafficStats{ - {&Timestamp{time.Date(2020, 9, 13, 0, 0, 0, 0, time.UTC)}, 0, 0, 0}, - {&Timestamp{time.Date(2020, 9, 12, 0, 0, 0, 0, time.UTC)}, 1, 12, 0}, - {&Timestamp{time.Date(2020, 9, 11, 0, 0, 0, 0, time.UTC)}, 5, 85, 0}, - {&Timestamp{time.Date(2020, 9, 10, 0, 0, 0, 0, time.UTC)}, 4, 20, 0}, - {&Timestamp{time.Date(2020, 9, 9, 0, 0, 0, 0, time.UTC)}, 2, 64, 0}, - {&Timestamp{time.Date(2020, 9, 8, 0, 0, 0, 0, time.UTC)}, 2, 95, 0}, - {&Timestamp{time.Date(2020, 9, 7, 0, 0, 0, 0, time.UTC)}, 3, 41, 0}, -} - -var expectedHourTraffic = []*SubredditTrafficStats{ - {&Timestamp{time.Date(2020, 9, 12, 20, 0, 0, 0, time.UTC)}, 1, 12, 0}, - {&Timestamp{time.Date(2020, 9, 11, 3, 0, 0, 0, time.UTC)}, 4, 57, 0}, - {&Timestamp{time.Date(2020, 9, 11, 2, 0, 0, 0, time.UTC)}, 4, 28, 0}, -} - -var expectedMonthTraffic = []*SubredditTrafficStats{ - {&Timestamp{time.Date(2020, 9, 1, 0, 0, 0, 0, time.UTC)}, 7, 481, 0}, - {&Timestamp{time.Date(2020, 8, 1, 0, 0, 0, 0, time.UTC)}, 5, 346, 0}, - {&Timestamp{time.Date(2020, 7, 1, 0, 0, 0, 0, time.UTC)}, 4, 264, 0}, -} - -var expectedStyleSheet = &SubredditStyleSheet{ - SubredditID: "t5_2rc7j", - Images: []*SubredditImage{ - { - Name: "gopher", - Link: "url(%%gopher%%)", - URL: "http://b.thumbs.redditmedia.com/q5Wb6hTPm2Bd6Of9_xMrTu4n5qgAljJNqtnbE3Tging.png", - }, - }, - StyleSheet: `.flair-gopher { - background: url(%%gopher%%) no-repeat; - border: 0; - padding: 0; - width: 16px; - height: 16px; -}`, -} - -var expectedSubredditSettings = &SubredditSettings{ - ID: "t5_test", - - Type: String("private"), - - Language: String("en"), - - Title: String("hello!"), - Description: String("description"), - Sidebar: String("sidebar"), - SubmissionText: String(""), - WelcomeMessage: String(""), - WelcomeMessageEnabled: Bool(false), - - AllowCrossposts: Bool(false), - AllowChatPosts: Bool(true), - AllowPollPosts: Bool(false), - AllowFreeFormReports: Bool(true), - AllowOriginalContent: Bool(false), - AllowImages: Bool(true), - AllowMultipleImagesPerPost: Bool(true), - - ExcludeSitewideBannedUsersContent: Bool(false), - - CrowdControlChalLevel: Int(2), - - AllOriginalContent: Bool(false), - - SuggestedCommentSort: nil, - - SubmitLinkPostLabel: String("submit a link!"), - SubmitTextPostLabel: String("submit a post!"), - - PostType: String("any"), - - SpamFilterStrengthLinkPosts: String("low"), - SpamFilterStrengthTextPosts: String("low"), - SpamFilterStrengthComments: String("low"), - - ShowContentThumbnails: Bool(false), - ExpandMediaPreviewsOnCommentsPages: Bool(true), - - CollapseDeletedComments: Bool(false), - MinutesToHideCommentScores: Int(0), - - SpoilersEnabled: Bool(true), - - HeaderMouseoverText: String("hello!"), - - MobileColour: String(""), - - HideAds: Bool(false), - NSFW: Bool(false), - - AllowDiscoveryInHighTrafficFeeds: Bool(true), - AllowDiscoveryByIndividualUsers: Bool(true), - - WikiMode: String("modonly"), - WikiMinimumAccountAge: Int(0), - WikiMinimumKarma: Int(0), -} - -var expectedSubredditPostRequirements = &SubredditPostRequirements{ - Guidelines: "test", - GuidelinesDisplayPolicy: "", - - TitleMinLength: 50, - TitleMaxLength: 200, - - BodyMinLength: 50, - BodyMaxLength: 2000, - - TitleBlacklistedStrings: []string{"no"}, - BodyBlacklistedStrings: []string{"no"}, - - TitleRequiredStrings: []string{"yes"}, - BodyRequiredStrings: []string{"yes"}, - - DomainBlacklist: []string{"example.com"}, - DomainWhitelist: []string{}, - - BodyRestrictionPolicy: "none", - LinkRestrictionPolicy: "none", - - GalleryMinItems: 2, - GalleryMaxItems: 20, - GalleryCaptionsRequirement: "none", - GalleryURLsRequirement: "none", - - LinkRepostAge: 2, - FlairRequired: false, - - TitleRegexes: []string{}, - BodyRegexes: []string{}, -} - -func TestSubredditService_HotPosts(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/hot", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - posts, resp, err := client.Subreddit.HotPosts(ctx, "test", nil) - require.NoError(t, err) - require.Equal(t, expectedPosts, posts) - require.Equal(t, "t3_hyhquk", resp.After) -} - -func TestSubredditService_NewPosts(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/new", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - posts, resp, err := client.Subreddit.NewPosts(ctx, "test", nil) - require.NoError(t, err) - require.Equal(t, expectedPosts, posts) - require.Equal(t, "t3_hyhquk", resp.After) -} - -func TestSubredditService_RisingPosts(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/rising", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - posts, resp, err := client.Subreddit.RisingPosts(ctx, "test", nil) - require.NoError(t, err) - require.Equal(t, expectedPosts, posts) - require.Equal(t, "t3_hyhquk", resp.After) -} - -func TestSubredditService_ControversialPosts(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/controversial", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - posts, resp, err := client.Subreddit.ControversialPosts(ctx, "test", nil) - require.NoError(t, err) - require.Equal(t, expectedPosts, posts) - require.Equal(t, "t3_hyhquk", resp.After) -} - -func TestSubredditService_TopPosts(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/top", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - posts, resp, err := client.Subreddit.TopPosts(ctx, "test", nil) - require.NoError(t, err) - require.Equal(t, expectedPosts, posts) - require.Equal(t, "t3_hyhquk", resp.After) -} - -func TestSubredditService_Get(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/about.json") - require.NoError(t, err) - - mux.HandleFunc("/r/golang/about", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - _, _, err = client.Subreddit.Get(ctx, "") - require.EqualError(t, err, "name: cannot be empty") - - subreddit, _, err := client.Subreddit.Get(ctx, "golang") - require.NoError(t, err) - require.Equal(t, expectedSubreddit, subreddit) -} - -func TestSubredditService_Popular(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/popular", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.Popular(ctx, nil) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_New(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/new", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.New(ctx, nil) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_Gold(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/gold", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.Gold(ctx, nil) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_Default(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/default", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.Default(ctx, nil) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_Subscribed(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/mine/subscriber", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.Subscribed(ctx, nil) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_Approved(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/mine/contributor", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.Approved(ctx, nil) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_Moderated(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/mine/moderator", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.Moderated(ctx, nil) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_GetSticky1(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/post.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/sticky", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, "1", r.Form.Get("num")) - - fmt.Fprint(w, blob) - }) - - postAndComments, _, err := client.Subreddit.GetSticky1(ctx, "test") - require.NoError(t, err) - require.Equal(t, expectedPostAndComments, postAndComments) -} - -func TestSubredditService_GetSticky2(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/post/post.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/sticky", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, "2", r.Form.Get("num")) - - fmt.Fprint(w, blob) - }) - - postAndComments, _, err := client.Subreddit.GetSticky2(ctx, "test") - require.NoError(t, err) - require.Equal(t, expectedPostAndComments, postAndComments) -} - -func TestSubredditService_Subscribe(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/subscribe", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("action", "sub") - form.Set("sr_name", "test,golang,nba") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.Subscribe(ctx, "test", "golang", "nba") - require.NoError(t, err) -} - -func TestSubredditService_SubscribeByID(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/subscribe", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("action", "sub") - form.Set("sr", "t5_test1,t5_test2,t5_test3") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.SubscribeByID(ctx, "t5_test1", "t5_test2", "t5_test3") - require.NoError(t, err) -} - -func TestSubredditService_Unsubscribe(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/subscribe", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("action", "unsub") - form.Set("sr_name", "test,golang,nba") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.Unsubscribe(ctx, "test", "golang", "nba") - require.NoError(t, err) -} - -func TestSubredditService_UnsubscribeByID(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/subscribe", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("action", "unsub") - form.Set("sr", "t5_test1,t5_test2,t5_test3") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.UnsubscribeByID(ctx, "t5_test1", "t5_test2", "t5_test3") - require.NoError(t, err) -} - -func TestSubredditService_Favorite(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/favorite", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("sr_name", "testsubreddit") - form.Set("make_favorite", "true") - form.Set("api_type", "json") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.Favorite(ctx, "testsubreddit") - require.NoError(t, err) -} - -func TestSubredditService_Unfavorite(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/favorite", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("sr_name", "testsubreddit") - form.Set("make_favorite", "false") - form.Set("api_type", "json") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.Unfavorite(ctx, "testsubreddit") - require.NoError(t, err) -} - -func TestSubredditService_Search(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/list.json") - require.NoError(t, err) - - mux.HandleFunc("/subreddits/search", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("q", "golang") - form.Set("limit", "10") - form.Set("sort", "activity") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - subreddits, resp, err := client.Subreddit.Search(ctx, "golang", &ListSubredditOptions{ - ListOptions: ListOptions{ - Limit: 10, - }, - Sort: "activity", - }) - require.NoError(t, err) - require.Equal(t, expectedSubreddits, subreddits) - require.Equal(t, "t5_2qh0u", resp.After) -} - -func TestSubredditService_SearchNames(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/search-names.json") - require.NoError(t, err) - - mux.HandleFunc("/api/search_reddit_names", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("query", "golang") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - names, _, err := client.Subreddit.SearchNames(ctx, "golang") - require.NoError(t, err) - require.Equal(t, expectedSubredditNames, names) -} - -func TestSubredditService_SearchPosts(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/search-posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/all/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) - }) - - posts, resp, err := client.Subreddit.SearchPosts(ctx, "test", "", nil) - require.NoError(t, err) - require.Equal(t, expectedSearchPosts, posts) - require.Equal(t, "t3_hmwhd7", resp.After) -} - -func TestSubredditService_SearchPosts_InSubreddit(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/search-posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/search", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("q", "test") - form.Set("restrict_sr", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - posts, resp, err := client.Subreddit.SearchPosts(ctx, "test", "test", nil) - require.NoError(t, err) - require.Equal(t, expectedSearchPosts, posts) - require.Equal(t, "t3_hmwhd7", resp.After) -} - -func TestSubredditService_SearchPosts_InSubreddits(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/search-posts.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test+golang+nba/search", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("q", "test") - form.Set("restrict_sr", "true") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - posts, resp, err := client.Subreddit.SearchPosts(ctx, "test", "test+golang+nba", nil) - require.NoError(t, err) - require.Equal(t, expectedSearchPosts, posts) - require.Equal(t, "t3_hmwhd7", resp.After) -} - -func TestSubredditService_Random(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/random.json") - require.NoError(t, err) - - mux.HandleFunc("/r/random", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, "true", r.Form.Get("sr_detail")) - require.Equal(t, "1", r.Form.Get("limit")) - - fmt.Fprint(w, blob) - }) - - subreddit, _, err := client.Subreddit.Random(ctx) - require.NoError(t, err) - require.Equal(t, expectedRandomSubreddit, subreddit) -} - -func TestSubredditService_RandomNSFW(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/random.json") - require.NoError(t, err) - - mux.HandleFunc("/r/randnsfw", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, "true", r.Form.Get("sr_detail")) - require.Equal(t, "1", r.Form.Get("limit")) - - fmt.Fprint(w, blob) - }) - - subreddit, _, err := client.Subreddit.RandomNSFW(ctx) - require.NoError(t, err) - require.Equal(t, expectedRandomSubreddit, subreddit) -} - -func TestSubredditService_SubmissionText(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/test/api/submit_text", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, `{ - "submit_text": "this is a test", - "submit_text_html": "" - }`) - }) - - text, _, err := client.Subreddit.SubmissionText(ctx, "test") - require.NoError(t, err) - require.Equal(t, "this is a test", text) -} - -func TestSubredditService_Banned(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/banned-users.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/banned", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("after", "testafter") - form.Set("limit", "10") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - bans, _, err := client.Subreddit.Banned(ctx, "test", &ListOptions{After: "testafter", Limit: 10}) - require.NoError(t, err) - require.Equal(t, expectedBans, bans) -} - -func TestSubredditService_Muted(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/relationships.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/muted", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("before", "testbefore") - form.Set("limit", "50") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - mutes, _, err := client.Subreddit.Muted(ctx, "test", &ListOptions{Before: "testbefore", Limit: 50}) - require.NoError(t, err) - require.Equal(t, expectedRelationships3, mutes) -} - -func TestSubredditService_WikiBanned(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/banned-users.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/wikibanned", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("after", "testafter") - form.Set("limit", "15") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - bans, _, err := client.Subreddit.WikiBanned(ctx, "test", &ListOptions{After: "testafter", Limit: 15}) - require.NoError(t, err) - require.Equal(t, expectedBans, bans) -} - -func TestSubredditService_Contributors(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/relationships.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/contributors", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("limit", "5") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - contributors, _, err := client.Subreddit.Contributors(ctx, "test", &ListOptions{Limit: 5}) - require.NoError(t, err) - require.Equal(t, expectedRelationships3, contributors) -} - -func TestSubredditService_WikiContributors(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/relationships.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/wikicontributors", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - form := url.Values{} - form.Set("limit", "99") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.Form) - - fmt.Fprint(w, blob) - }) - - contributors, _, err := client.Subreddit.WikiContributors(ctx, "test", &ListOptions{Limit: 99}) - require.NoError(t, err) - require.Equal(t, expectedRelationships3, contributors) -} - -func TestSubredditService_Moderators(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/moderators.json") - require.NoError(t, err) - - mux.HandleFunc("/r/test/about/moderators", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - moderators, _, err := client.Subreddit.Moderators(ctx, "test") - require.NoError(t, err) - require.Equal(t, expectedModerators, moderators) -} - -func TestSubredditService_Rules(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/rules.json") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/about/rules", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - rules, _, err := client.Subreddit.Rules(ctx, "testsubreddit") - require.NoError(t, err) - require.Equal(t, expectedRules, rules) -} - -func TestSubredditService_CreateRule(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/testsubreddit/api/add_subreddit_rule", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("kind", "all") - form.Set("short_name", "testname") - form.Set("violation_reason", "testreason") - form.Set("description", "testdescription") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.CreateRule(ctx, "testsubreddit", &SubredditRuleCreateRequest{ - Kind: "all", - Name: "testname", - ViolationReason: "testreason", - Description: "testdescription", - }) - require.NoError(t, err) -} - -func TestSubredditService_CreateRule_Error(t *testing.T) { - client, _ := setup(t) - - _, err := client.Subreddit.CreateRule(ctx, "testsubreddit", nil) - require.EqualError(t, err, "*SubredditRuleCreateRequest: cannot be nil") - - _, err = client.Subreddit.CreateRule(ctx, "testsubreddit", &SubredditRuleCreateRequest{Kind: "invalid"}) - require.EqualError(t, err, "(*SubredditRuleCreateRequest).Kind: must be one of: comment, link, all") - - _, err = client.Subreddit.CreateRule(ctx, "testsubreddit", &SubredditRuleCreateRequest{Kind: "all", Name: ""}) - require.EqualError(t, err, "(*SubredditRuleCreateRequest).Name: must be between 1-100 characters") - - _, err = client.Subreddit.CreateRule(ctx, "testsubreddit", &SubredditRuleCreateRequest{ - Kind: "all", - Name: "testname", - ViolationReason: strings.Repeat("x", 101), - }) - require.EqualError(t, err, "(*SubredditRuleCreateRequest).ViolationReason: cannot be longer than 100 characters") - - _, err = client.Subreddit.CreateRule(ctx, "testsubreddit", &SubredditRuleCreateRequest{ - Kind: "all", - Name: "testname", - Description: strings.Repeat("x", 501), - }) - require.EqualError(t, err, "(*SubredditRuleCreateRequest).Description: cannot be longer than 500 characters") -} - -func TestSubredditService_Traffic(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/traffic.json") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/about/traffic", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - dayTraffic, hourTraffic, monthTraffic, _, err := client.Subreddit.Traffic(ctx, "testsubreddit") - require.NoError(t, err) - require.Equal(t, expectedDayTraffic, dayTraffic) - require.Equal(t, expectedHourTraffic, hourTraffic) - require.Equal(t, expectedMonthTraffic, monthTraffic) -} - -func TestSubredditService_StyleSheet(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/stylesheet.json") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/about/stylesheet", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - styleSheet, _, err := client.Subreddit.StyleSheet(ctx, "testsubreddit") - require.NoError(t, err) - require.Equal(t, expectedStyleSheet, styleSheet) -} - -func TestSubredditService_StyleSheetRaw(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/testsubreddit/stylesheet", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, "* { box-sizing: border-box; }") - }) - - styleSheet, _, err := client.Subreddit.StyleSheetRaw(ctx, "testsubreddit") - require.NoError(t, err) - require.Equal(t, "* { box-sizing: border-box; }", styleSheet) -} - -func TestSubredditService_UpdateStyleSheet(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/testsubreddit/api/subreddit_stylesheet", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("op", "save") - form.Set("stylesheet_contents", "* { box-sizing: border-box; }") - form.Set("reason", "testreason") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.UpdateStyleSheet(ctx, "testsubreddit", "* { box-sizing: border-box; }", "testreason") - require.NoError(t, err) -} - -func TestSubredditService_RemoveImage(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/testsubreddit/api/delete_sr_img", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("img_name", "testimage") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.RemoveImage(ctx, "testsubreddit", "testimage") - require.NoError(t, err) -} - -func TestSubredditService_RemoveHeader(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/testsubreddit/api/delete_sr_header", 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.Subreddit.RemoveHeader(ctx, "testsubreddit") - require.NoError(t, err) -} - -func TestSubredditService_RemoveMobileHeader(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/testsubreddit/api/delete_sr_banner", 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.Subreddit.RemoveMobileHeader(ctx, "testsubreddit") - require.NoError(t, err) -} - -func TestSubredditService_RemoveMobileIcon(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/r/testsubreddit/api/delete_sr_icon", 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.Subreddit.RemoveMobileIcon(ctx, "testsubreddit") - require.NoError(t, err) -} - -func TestSubredditService_UploadImage(t *testing.T) { - client, mux := setup(t) - - imageFile, err := ioutil.TempFile("/tmp", "emoji*.png") - require.NoError(t, err) - defer func() { - imageFile.Close() - os.Remove(imageFile.Name()) - }() - - _, err = imageFile.WriteString("this is a test") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", 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("upload_type", "img") - form.Set("name", "testname") - form.Set("img_type", "png") - - err = r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - - fmt.Fprint(w, `{ - "img_src": "https://example.com/test.png" - }`) - }) - - link, _, err := client.Subreddit.UploadImage(ctx, "testsubreddit", imageFile.Name(), "testname") - require.NoError(t, err) - require.Equal(t, "https://example.com/test.png", link) -} - -func TestSubredditService_UploadHeader(t *testing.T) { - client, mux := setup(t) - - imageFile, err := ioutil.TempFile("/tmp", "emoji*.png") - require.NoError(t, err) - defer func() { - imageFile.Close() - os.Remove(imageFile.Name()) - }() - - _, err = imageFile.WriteString("this is a test") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", 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("upload_type", "header") - form.Set("name", "testname") - form.Set("img_type", "png") - - err = r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - - fmt.Fprint(w, `{ - "img_src": "https://example.com/test.png" - }`) - }) - - link, _, err := client.Subreddit.UploadHeader(ctx, "testsubreddit", imageFile.Name(), "testname") - require.NoError(t, err) - require.Equal(t, "https://example.com/test.png", link) -} - -func TestSubredditService_UploadMobileHeader(t *testing.T) { - client, mux := setup(t) - - imageFile, err := ioutil.TempFile("/tmp", "emoji*.png") - require.NoError(t, err) - defer func() { - imageFile.Close() - os.Remove(imageFile.Name()) - }() - - _, err = imageFile.WriteString("this is a test") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", 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("upload_type", "banner") - form.Set("name", "testname") - form.Set("img_type", "png") - - err = r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - - fmt.Fprint(w, `{ - "img_src": "https://example.com/test.png" - }`) - }) - - link, _, err := client.Subreddit.UploadMobileHeader(ctx, "testsubreddit", imageFile.Name(), "testname") - require.NoError(t, err) - require.Equal(t, "https://example.com/test.png", link) -} - -func TestSubredditService_UploadMobileIcon(t *testing.T) { - client, mux := setup(t) - - imageFile, err := ioutil.TempFile("/tmp", "emoji*.jpg") - require.NoError(t, err) - defer func() { - imageFile.Close() - os.Remove(imageFile.Name()) - }() - - _, err = imageFile.WriteString("this is a test") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", 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("upload_type", "icon") - form.Set("name", "testname") - form.Set("img_type", "jpg") - - err = r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - - fmt.Fprint(w, `{ - "img_src": "https://example.com/test.jpg" - }`) - }) - - link, _, err := client.Subreddit.UploadMobileIcon(ctx, "testsubreddit", imageFile.Name(), "testname") - require.NoError(t, err) - require.Equal(t, "https://example.com/test.jpg", link) -} - -func TestSubredditService_UploadImage_Error(t *testing.T) { - client, mux := setup(t) - - imageFile, err := ioutil.TempFile("/tmp", "emoji*.jpg") - require.NoError(t, err) - defer func() { - imageFile.Close() - os.Remove(imageFile.Name()) - }() - - mux.HandleFunc("/r/testsubreddit/api/upload_sr_img", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - fmt.Fprint(w, `{ - "errors_values": [ - "error one", - "error two" - ] - }`) - }) - - _, _, err = client.Subreddit.UploadImage(ctx, "testsubreddit", "does-not-exist.jpg", "testname") - require.EqualError(t, err, "open does-not-exist.jpg: no such file or directory") - - _, _, err = client.Subreddit.UploadImage(ctx, "testsubreddit", imageFile.Name(), "testname") - require.EqualError(t, err, "could not upload image: error one; error two") -} - -func TestSubredditService_Create(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/site_admin", 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", "testsubreddit") - form.Set("type", "private") - form.Set("lang", "en") - form.Set("title", "hello!") - form.Set("public_description", "description") - form.Set("description", "sidebar") - form.Set("submit_text", "") - form.Set("welcome_message_text", "") - form.Set("welcome_message_enabled", "false") - form.Set("allow_post_crossposts", "false") - form.Set("allow_chat_post_creation", "true") - form.Set("allow_polls", "false") - form.Set("free_form_reports", "true") - form.Set("original_content_tag_enabled", "false") - form.Set("allow_images", "true") - form.Set("allow_galleries", "true") - form.Set("exclude_banned_modqueue", "false") - form.Set("crowd_control_chat_level", "2") - form.Set("all_original_content", "false") - form.Set("submit_link_label", "submit a link!") - form.Set("submit_text_label", "submit a post!") - form.Set("link_type", "any") - form.Set("spam_links", "low") - form.Set("spam_selfposts", "low") - form.Set("spam_comments", "low") - form.Set("show_media", "false") - form.Set("show_media_preview", "true") - form.Set("collapse_deleted_comments", "false") - form.Set("comment_score_hide_mins", "0") - form.Set("spoilers_enabled", "true") - form.Set("header-title", "hello!") - form.Set("key_color", "") - form.Set("hide_ads", "false") - form.Set("over_18", "false") - form.Set("allow_top", "true") - form.Set("allow_discovery", "true") - form.Set("wikimode", "modonly") - form.Set("wiki_edit_age", "0") - form.Set("wiki_edit_karma", "0") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.Create(ctx, "testsubreddit", nil) - require.EqualError(t, err, "*SubredditSettings: cannot be nil") - - _, err = client.Subreddit.Create(ctx, "testsubreddit", expectedSubredditSettings) - require.NoError(t, err) -} - -func TestSubredditService_Edit(t *testing.T) { - client, mux := setup(t) - - mux.HandleFunc("/api/site_admin", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - form := url.Values{} - form.Set("api_type", "json") - form.Set("sr", "t5_test") - form.Set("type", "private") - form.Set("lang", "en") - form.Set("title", "hello!") - form.Set("public_description", "description") - form.Set("description", "sidebar") - form.Set("submit_text", "") - form.Set("welcome_message_text", "") - form.Set("welcome_message_enabled", "false") - form.Set("allow_post_crossposts", "false") - form.Set("allow_chat_post_creation", "true") - form.Set("allow_polls", "false") - form.Set("free_form_reports", "true") - form.Set("original_content_tag_enabled", "false") - form.Set("allow_images", "true") - form.Set("allow_galleries", "true") - form.Set("exclude_banned_modqueue", "false") - form.Set("crowd_control_chat_level", "2") - form.Set("all_original_content", "false") - form.Set("submit_link_label", "submit a link!") - form.Set("submit_text_label", "submit a post!") - form.Set("link_type", "any") - form.Set("spam_links", "low") - form.Set("spam_selfposts", "low") - form.Set("spam_comments", "low") - form.Set("show_media", "false") - form.Set("show_media_preview", "true") - form.Set("collapse_deleted_comments", "false") - form.Set("comment_score_hide_mins", "0") - form.Set("spoilers_enabled", "true") - form.Set("header-title", "hello!") - form.Set("key_color", "") - form.Set("hide_ads", "false") - form.Set("over_18", "false") - form.Set("allow_top", "true") - form.Set("allow_discovery", "true") - form.Set("wikimode", "modonly") - form.Set("wiki_edit_age", "0") - form.Set("wiki_edit_karma", "0") - - err := r.ParseForm() - require.NoError(t, err) - require.Equal(t, form, r.PostForm) - }) - - _, err := client.Subreddit.Edit(ctx, "t5_test", nil) - require.EqualError(t, err, "*SubredditSettings: cannot be nil") - - _, err = client.Subreddit.Edit(ctx, "t5_test", expectedSubredditSettings) - require.NoError(t, err) -} - -func TestSubredditService_GetSettings(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/settings.json") - require.NoError(t, err) - - mux.HandleFunc("/r/testsubreddit/about/edit", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - subredditSettings, _, err := client.Subreddit.GetSettings(ctx, "testsubreddit") - require.NoError(t, err) - require.Equal(t, expectedSubredditSettings, subredditSettings) -} - -func TestSubredditService_PostRequirements(t *testing.T) { - client, mux := setup(t) - - blob, err := readFileContents("../testdata/subreddit/post-requirements.json") - require.NoError(t, err) - - mux.HandleFunc("/api/v1/testsubreddit/post_requirements", func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - fmt.Fprint(w, blob) - }) - - postRequirements, _, err := client.Subreddit.PostRequirements(ctx, "testsubreddit") - require.NoError(t, err) - require.Equal(t, expectedSubredditPostRequirements, postRequirements) -} diff --git a/v2/reddit/things.go b/v2/reddit/things.go deleted file mode 100644 index 1a591fd..0000000 --- a/v2/reddit/things.go +++ /dev/null @@ -1,791 +0,0 @@ -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) - } -} diff --git a/v2/reddit/timestamp.go b/v2/reddit/timestamp.go deleted file mode 100644 index 7f90cb4..0000000 --- a/v2/reddit/timestamp.go +++ /dev/null @@ -1,47 +0,0 @@ -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) -} diff --git a/v2/reddit/timestamp_test.go b/v2/reddit/timestamp_test.go deleted file mode 100644 index 3ef9760..0000000 --- a/v2/reddit/timestamp_test.go +++ /dev/null @@ -1,182 +0,0 @@ -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) - } - } -} diff --git a/v2/reddit/user.go b/v2/reddit/user.go deleted file mode 100644 index f1839f5..0000000 --- a/v2/reddit/user.go +++ /dev/null @@ -1,413 +0,0 @@ -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 -} diff --git a/v2/reddit/user_test.go b/v2/reddit/user_test.go deleted file mode 100644 index 81a2fd0..0000000 --- a/v2/reddit/user_test.go +++ /dev/null @@ -1,925 +0,0 @@ -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) -} diff --git a/v2/reddit/widget.go b/v2/reddit/widget.go deleted file mode 100644 index 34436d6..0000000 --- a/v2/reddit/widget.go +++ /dev/null @@ -1,471 +0,0 @@ -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) -} diff --git a/v2/reddit/widget_test.go b/v2/reddit/widget_test.go deleted file mode 100644 index 40692b6..0000000 --- a/v2/reddit/widget_test.go +++ /dev/null @@ -1,265 +0,0 @@ -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) -} diff --git a/v2/reddit/wiki.go b/v2/reddit/wiki.go deleted file mode 100644 index ef7acda..0000000 --- a/v2/reddit/wiki.go +++ /dev/null @@ -1,396 +0,0 @@ -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) -} diff --git a/v2/reddit/wiki_test.go b/v2/reddit/wiki_test.go deleted file mode 100644 index cc9b961..0000000 --- a/v2/reddit/wiki_test.go +++ /dev/null @@ -1,391 +0,0 @@ -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) -}