reddit 0.1.1.0 → 0.2.0.0
raw patch · 22 files changed
+297/−298 lines, 22 filesdep +data-default-classdep +freedep +http-clientdep −data-defaultdep −http-conduitdep −stmdep ~aesondep ~api-builderdep ~bytestringPVP ok
version bump matches the API change (PVP)
Dependencies added: data-default-class, free, http-client, http-client-tls
Dependencies removed: data-default, http-conduit, stm
Dependency ranges changed: aeson, api-builder, bytestring, http-types, network, text, time, unordered-containers, vector
API changes (from Hackage documentation)
- Reddit.Types.Reddit: instance (Functor m, Monad m) => Applicative (RedditT m)
- Reddit.Types.Reddit: instance Functor m => Functor (RedditT m)
- Reddit.Types.Reddit: unRedditT :: RedditT m a -> APIT (TVar RateLimits) RedditError m a
+ Reddit: defaultRedditOptions :: RedditOptions
+ Reddit: interpretIO :: MonadIO m => RedditState -> RedditT m a -> m (Either (APIError RedditError, Maybe (RedditT m a)) a)
+ Reddit: runResumeRedditWith :: MonadIO m => RedditOptions -> RedditT m a -> m (Either (APIError RedditError, Maybe (RedditT m a)) a)
+ Reddit.Actions.Message: replyMessage :: (Monad m, Thing a) => a -> Text -> RedditT m MessageID
+ Reddit.Actions.User: getUserPosts :: Monad m => Username -> RedditT m PostListing
+ Reddit.Actions.User: getUserPosts' :: Monad m => Options PostID -> Username -> RedditT m PostListing
+ Reddit.Types.Message: instance FromJSON (POSTWrapped MessageID)
+ Reddit.Types.Message: instance Ord MessageID
+ Reddit.Types.Message: instance Ord MessageKind
+ Reddit.Types.Reddit: FailWith :: APIError RedditError -> RedditF m a
+ Reddit.Types.Reddit: Nest :: RedditT m b -> (Either (APIError RedditError) b -> a) -> RedditF m a
+ Reddit.Types.Reddit: NestResuming :: RedditT m b -> (Either (APIError RedditError, Maybe (RedditT m b)) b -> a) -> RedditF m a
+ Reddit.Types.Reddit: ReceiveRoute :: Route -> (b -> a) -> RedditF m a
+ Reddit.Types.Reddit: RunRoute :: Route -> (b -> a) -> RedditF m a
+ Reddit.Types.Reddit: WithBaseURL :: Text -> RedditT m b -> (b -> a) -> RedditF m a
+ Reddit.Types.Reddit: data RedditF m a
+ Reddit.Types.Reddit: instance Functor (RedditF m)
+ Reddit.Types.Reddit: instance Monad m => Applicative (RedditT m)
+ Reddit.Types.Reddit: instance Monad m => Functor (RedditT m)
+ Reddit.Types.Reddit: instance Receivable LoginDetails
+ Reddit.Types.Reddit: receiveRoute :: (Receivable a, Monad m) => Route -> RedditT m a
+ Reddit.Types.Reddit: runRoute :: (FromJSON a, Monad m) => Route -> RedditT m a
+ Reddit.Types.Reddit: withBaseURL :: Monad m => Text -> RedditT m a -> RedditT m a
- Reddit.Actions.Captcha: needsCaptcha :: MonadIO m => RedditT m Bool
+ Reddit.Actions.Captcha: needsCaptcha :: Monad m => RedditT m Bool
- Reddit.Actions.Captcha: newCaptcha :: MonadIO m => RedditT m CaptchaID
+ Reddit.Actions.Captcha: newCaptcha :: Monad m => RedditT m CaptchaID
- Reddit.Actions.Comment: deleteComment :: MonadIO m => CommentID -> RedditT m ()
+ Reddit.Actions.Comment: deleteComment :: Monad m => CommentID -> RedditT m ()
- Reddit.Actions.Comment: editComment :: MonadIO m => CommentID -> Text -> RedditT m Comment
+ Reddit.Actions.Comment: editComment :: Monad m => CommentID -> Text -> RedditT m Comment
- Reddit.Actions.Comment: getCommentInfo :: MonadIO m => CommentID -> RedditT m Comment
+ Reddit.Actions.Comment: getCommentInfo :: Monad m => CommentID -> RedditT m Comment
- Reddit.Actions.Comment: getCommentsInfo :: MonadIO m => [CommentID] -> RedditT m CommentListing
+ Reddit.Actions.Comment: getCommentsInfo :: Monad m => [CommentID] -> RedditT m CommentListing
- Reddit.Actions.Comment: getMoreChildren :: MonadIO m => PostID -> [CommentID] -> RedditT m [CommentReference]
+ Reddit.Actions.Comment: getMoreChildren :: Monad m => PostID -> [CommentID] -> RedditT m [CommentReference]
- Reddit.Actions.Comment: getNewComments :: MonadIO m => Maybe SubredditName -> RedditT m CommentListing
+ Reddit.Actions.Comment: getNewComments :: Monad m => Maybe SubredditName -> RedditT m CommentListing
- Reddit.Actions.Comment: getNewComments' :: MonadIO m => Options CommentID -> Maybe SubredditName -> RedditT m CommentListing
+ Reddit.Actions.Comment: getNewComments' :: Monad m => Options CommentID -> Maybe SubredditName -> RedditT m CommentListing
- Reddit.Actions.Comment: removeComment :: MonadIO m => CommentID -> RedditT m ()
+ Reddit.Actions.Comment: removeComment :: Monad m => CommentID -> RedditT m ()
- Reddit.Actions.Flair: addLinkFlair :: MonadIO m => SubredditName -> Text -> Text -> Bool -> RedditT m ()
+ Reddit.Actions.Flair: addLinkFlair :: Monad m => SubredditName -> Text -> Text -> Bool -> RedditT m ()
- Reddit.Actions.Flair: flairCSV :: MonadIO m => SubredditName -> [(Username, Text, Text)] -> RedditT m Value
+ Reddit.Actions.Flair: flairCSV :: Monad m => SubredditName -> [(Username, Text, Text)] -> RedditT m Value
- Reddit.Actions.Flair: getFlairList :: MonadIO m => SubredditName -> RedditT m FlairListing
+ Reddit.Actions.Flair: getFlairList :: Monad m => SubredditName -> RedditT m FlairListing
- Reddit.Actions.Flair: getFlairList' :: MonadIO m => Options UserID -> SubredditName -> RedditT m FlairListing
+ Reddit.Actions.Flair: getFlairList' :: Monad m => Options UserID -> SubredditName -> RedditT m FlairListing
- Reddit.Actions.Message: getInbox :: MonadIO m => RedditT m (Listing MessageKind Message)
+ Reddit.Actions.Message: getInbox :: Monad m => RedditT m (Listing MessageKind Message)
- Reddit.Actions.Message: getInbox' :: MonadIO m => Bool -> Options MessageKind -> RedditT m (Listing MessageKind Message)
+ Reddit.Actions.Message: getInbox' :: Monad m => Bool -> Options MessageKind -> RedditT m (Listing MessageKind Message)
- Reddit.Actions.Message: getUnread :: MonadIO m => RedditT m (Listing MessageKind Message)
+ Reddit.Actions.Message: getUnread :: Monad m => RedditT m (Listing MessageKind Message)
- Reddit.Actions.Message: getUnread' :: MonadIO m => Bool -> Options MessageKind -> RedditT m (Listing MessageKind Message)
+ Reddit.Actions.Message: getUnread' :: Monad m => Bool -> Options MessageKind -> RedditT m (Listing MessageKind Message)
- Reddit.Actions.Message: markRead :: (ToQuery a, Thing a, MonadIO m) => a -> RedditT m ()
+ Reddit.Actions.Message: markRead :: (ToQuery a, Thing a, Monad m) => a -> RedditT m ()
- Reddit.Actions.Message: sendMessage :: MonadIO m => Username -> Text -> Text -> RedditT m ()
+ Reddit.Actions.Message: sendMessage :: Monad m => Username -> Text -> Text -> RedditT m ()
- Reddit.Actions.Message: sendMessageWithCaptcha :: MonadIO m => Username -> Text -> Text -> CaptchaID -> Text -> RedditT m ()
+ Reddit.Actions.Message: sendMessageWithCaptcha :: Monad m => Username -> Text -> Text -> CaptchaID -> Text -> RedditT m ()
- Reddit.Actions.Moderation: bans :: Options BanID -> SubredditName -> Reddit (Listing BanID Ban)
+ Reddit.Actions.Moderation: bans :: Monad m => Options BanID -> SubredditName -> RedditT m (Listing BanID Ban)
- Reddit.Actions.Moderation: lookupBan :: Username -> SubredditName -> Reddit (Maybe Ban)
+ Reddit.Actions.Moderation: lookupBan :: Monad m => Username -> SubredditName -> RedditT m (Maybe Ban)
- Reddit.Actions.Post: deletePost :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Post: deletePost :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Post: editPost :: MonadIO m => PostID -> Text -> RedditT m ()
+ Reddit.Actions.Post: editPost :: Monad m => PostID -> Text -> RedditT m ()
- Reddit.Actions.Post: getComments :: MonadIO m => PostID -> RedditT m [CommentReference]
+ Reddit.Actions.Post: getComments :: Monad m => PostID -> RedditT m [CommentReference]
- Reddit.Actions.Post: getPostComments :: MonadIO m => PostID -> RedditT m PostComments
+ Reddit.Actions.Post: getPostComments :: Monad m => PostID -> RedditT m PostComments
- Reddit.Actions.Post: getPostInfo :: MonadIO m => PostID -> RedditT m Post
+ Reddit.Actions.Post: getPostInfo :: Monad m => PostID -> RedditT m Post
- Reddit.Actions.Post: getPostSubComments :: MonadIO m => PostID -> CommentID -> RedditT m PostComments
+ Reddit.Actions.Post: getPostSubComments :: Monad m => PostID -> CommentID -> RedditT m PostComments
- Reddit.Actions.Post: getPosts :: MonadIO m => RedditT m PostListing
+ Reddit.Actions.Post: getPosts :: Monad m => RedditT m PostListing
- Reddit.Actions.Post: getPosts' :: MonadIO m => Options PostID -> ListingType -> Maybe SubredditName -> RedditT m PostListing
+ Reddit.Actions.Post: getPosts' :: Monad m => Options PostID -> ListingType -> Maybe SubredditName -> RedditT m PostListing
- Reddit.Actions.Post: getPostsInfo :: MonadIO m => [PostID] -> RedditT m PostListing
+ Reddit.Actions.Post: getPostsInfo :: Monad m => [PostID] -> RedditT m PostListing
- Reddit.Actions.Post: markPostSpam :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Post: markPostSpam :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Post: removePost :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Post: removePost :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Post: savePost :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Post: savePost :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Post: setContestMode :: MonadIO m => Bool -> PostID -> RedditT m ()
+ Reddit.Actions.Post: setContestMode :: Monad m => Bool -> PostID -> RedditT m ()
- Reddit.Actions.Post: setInboxReplies :: MonadIO m => Bool -> PostID -> RedditT m ()
+ Reddit.Actions.Post: setInboxReplies :: Monad m => Bool -> PostID -> RedditT m ()
- Reddit.Actions.Post: setPostFlair :: MonadIO m => SubredditName -> PostID -> Text -> Text -> RedditT m ()
+ Reddit.Actions.Post: setPostFlair :: Monad m => SubredditName -> PostID -> Text -> Text -> RedditT m ()
- Reddit.Actions.Post: stickyPost :: MonadIO m => PostID -> Maybe Integer -> RedditT m ()
+ Reddit.Actions.Post: stickyPost :: Monad m => PostID -> Maybe Integer -> RedditT m ()
- Reddit.Actions.Post: submitLink :: MonadIO m => SubredditName -> Text -> Text -> RedditT m PostID
+ Reddit.Actions.Post: submitLink :: Monad m => SubredditName -> Text -> Text -> RedditT m PostID
- Reddit.Actions.Post: submitLinkWithCaptcha :: MonadIO m => SubredditName -> Text -> Text -> CaptchaID -> Text -> RedditT m PostID
+ Reddit.Actions.Post: submitLinkWithCaptcha :: Monad m => SubredditName -> Text -> Text -> CaptchaID -> Text -> RedditT m PostID
- Reddit.Actions.Post: submitSelfPost :: MonadIO m => SubredditName -> Text -> Text -> RedditT m PostID
+ Reddit.Actions.Post: submitSelfPost :: Monad m => SubredditName -> Text -> Text -> RedditT m PostID
- Reddit.Actions.Post: submitSelfPostWithCaptcha :: MonadIO m => SubredditName -> Text -> Text -> CaptchaID -> Text -> RedditT m PostID
+ Reddit.Actions.Post: submitSelfPostWithCaptcha :: Monad m => SubredditName -> Text -> Text -> CaptchaID -> Text -> RedditT m PostID
- Reddit.Actions.Post: unsavePost :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Post: unsavePost :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Post: unstickyPost :: MonadIO m => PostID -> Maybe Integer -> RedditT m ()
+ Reddit.Actions.Post: unstickyPost :: Monad m => PostID -> Maybe Integer -> RedditT m ()
- Reddit.Actions.Search: cloudSearch :: Maybe SubredditName -> Options PostID -> Order -> Text -> Reddit PostListing
+ Reddit.Actions.Search: cloudSearch :: Monad m => Maybe SubredditName -> Options PostID -> Order -> Text -> RedditT m PostListing
- Reddit.Actions.Search: luceneSearch :: Maybe SubredditName -> Options PostID -> Order -> Text -> Reddit PostListing
+ Reddit.Actions.Search: luceneSearch :: Monad m => Maybe SubredditName -> Options PostID -> Order -> Text -> RedditT m PostListing
- Reddit.Actions.Search: search :: Maybe SubredditName -> Options PostID -> Order -> Text -> Reddit PostListing
+ Reddit.Actions.Search: search :: Monad m => Maybe SubredditName -> Options PostID -> Order -> Text -> RedditT m PostListing
- Reddit.Actions.Subreddit: getSubredditInfo :: MonadIO m => SubredditName -> RedditT m Subreddit
+ Reddit.Actions.Subreddit: getSubredditInfo :: Monad m => SubredditName -> RedditT m Subreddit
- Reddit.Actions.Subreddit: getSubredditSettings :: MonadIO m => SubredditName -> RedditT m SubredditSettings
+ Reddit.Actions.Subreddit: getSubredditSettings :: Monad m => SubredditName -> RedditT m SubredditSettings
- Reddit.Actions.Subreddit: setSubredditSettings :: MonadIO m => SubredditID -> SubredditSettings -> RedditT m ()
+ Reddit.Actions.Subreddit: setSubredditSettings :: Monad m => SubredditID -> SubredditSettings -> RedditT m ()
- Reddit.Actions.Thing: delete :: (MonadIO m, Thing a) => a -> RedditT m ()
+ Reddit.Actions.Thing: delete :: (Monad m, Thing a) => a -> RedditT m ()
- Reddit.Actions.Thing: reply :: (MonadIO m, Thing a) => a -> Text -> RedditT m CommentID
+ Reddit.Actions.Thing: reply :: (Monad m, Thing a) => a -> Text -> RedditT m CommentID
- Reddit.Actions.Thing: report :: (MonadIO m, Thing a) => a -> Text -> RedditT m ()
+ Reddit.Actions.Thing: report :: (Monad m, Thing a) => a -> Text -> RedditT m ()
- Reddit.Actions.User: aboutMe :: MonadIO m => RedditT m User
+ Reddit.Actions.User: aboutMe :: Monad m => RedditT m User
- Reddit.Actions.User: getBlockedUsers :: MonadIO m => RedditT m [Relationship]
+ Reddit.Actions.User: getBlockedUsers :: Monad m => RedditT m [Relationship]
- Reddit.Actions.User: getFriends :: MonadIO m => RedditT m [Relationship]
+ Reddit.Actions.User: getFriends :: Monad m => RedditT m [Relationship]
- Reddit.Actions.User: getUserComments :: MonadIO m => Username -> RedditT m CommentListing
+ Reddit.Actions.User: getUserComments :: Monad m => Username -> RedditT m CommentListing
- Reddit.Actions.User: getUserComments' :: MonadIO m => Options CommentID -> Username -> RedditT m CommentListing
+ Reddit.Actions.User: getUserComments' :: Monad m => Options CommentID -> Username -> RedditT m CommentListing
- Reddit.Actions.User: getUserInfo :: MonadIO m => Username -> RedditT m User
+ Reddit.Actions.User: getUserInfo :: Monad m => Username -> RedditT m User
- Reddit.Actions.User: isUsernameAvailable :: MonadIO m => Username -> RedditT m Bool
+ Reddit.Actions.User: isUsernameAvailable :: Monad m => Username -> RedditT m Bool
- Reddit.Actions.User: lookupUserFlair :: MonadIO m => SubredditName -> Username -> RedditT m Flair
+ Reddit.Actions.User: lookupUserFlair :: Monad m => SubredditName -> Username -> RedditT m Flair
- Reddit.Actions.User: setUserFlair :: MonadIO m => SubredditName -> Username -> Text -> Text -> RedditT m ()
+ Reddit.Actions.User: setUserFlair :: Monad m => SubredditName -> Username -> Text -> Text -> RedditT m ()
- Reddit.Actions.Voting: downvoteComment :: MonadIO m => CommentID -> RedditT m ()
+ Reddit.Actions.Voting: downvoteComment :: Monad m => CommentID -> RedditT m ()
- Reddit.Actions.Voting: downvotePost :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Voting: downvotePost :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Voting: unvoteComment :: MonadIO m => CommentID -> RedditT m ()
+ Reddit.Actions.Voting: unvoteComment :: Monad m => CommentID -> RedditT m ()
- Reddit.Actions.Voting: unvotePost :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Voting: unvotePost :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Voting: upvoteComment :: MonadIO m => CommentID -> RedditT m ()
+ Reddit.Actions.Voting: upvoteComment :: Monad m => CommentID -> RedditT m ()
- Reddit.Actions.Voting: upvotePost :: MonadIO m => PostID -> RedditT m ()
+ Reddit.Actions.Voting: upvotePost :: Monad m => PostID -> RedditT m ()
- Reddit.Actions.Wiki: editWikiPage :: MonadIO m => SubredditName -> Text -> Text -> Text -> RedditT m ()
+ Reddit.Actions.Wiki: editWikiPage :: Monad m => SubredditName -> Text -> Text -> Text -> RedditT m ()
- Reddit.Actions.Wiki: getWikiPage :: MonadIO m => SubredditName -> Text -> RedditT m WikiPage
+ Reddit.Actions.Wiki: getWikiPage :: Monad m => SubredditName -> Text -> RedditT m WikiPage
- Reddit.Login: login :: MonadIO m => Text -> Text -> RedditT m LoginDetails
+ Reddit.Login: login :: Monad m => Text -> Text -> RedditT m LoginDetails
- Reddit.Types.Reddit: RedditT :: APIT (TVar RateLimits) RedditError m a -> RedditT m a
+ Reddit.Types.Reddit: RedditT :: (FreeT (RedditF m) m a) -> RedditT m a
- Reddit.Types.Reddit: nest :: MonadIO m => RedditT m a -> RedditT m (Either (APIError RedditError) a)
+ Reddit.Types.Reddit: nest :: Monad m => RedditT m a -> RedditT m (Either (APIError RedditError) a)
Files
- README.md +6/−4
- reddit.cabal +23/−20
- src/Reddit.hs +87/−21
- src/Reddit/Actions/Captcha.hs +2/−5
- src/Reddit/Actions/Comment.hs +9/−11
- src/Reddit/Actions/Flair.hs +5/−7
- src/Reddit/Actions/Message.hs +20/−11
- src/Reddit/Actions/Moderation.hs +9/−8
- src/Reddit/Actions/Post.hs +23/−25
- src/Reddit/Actions/Search.hs +3/−4
- src/Reddit/Actions/Subreddit.hs +4/−6
- src/Reddit/Actions/Thing.hs +3/−5
- src/Reddit/Actions/User.hs +21/−12
- src/Reddit/Actions/Voting.hs +8/−10
- src/Reddit/Actions/Wiki.hs +2/−4
- src/Reddit/Login.hs +5/−29
- src/Reddit/Routes/Run.hs +0/−81
- src/Reddit/Routes/User.hs +10/−0
- src/Reddit/Types/Message.hs +12/−2
- src/Reddit/Types/Options.hs +1/−1
- src/Reddit/Types/Reddit.hs +43/−31
- src/Reddit/Types/SubredditSettings.hs +1/−1
README.md view
@@ -1,4 +1,4 @@-reddit for haskell [](https://travis-ci.org/intolerable/reddit)+reddit for haskell [](https://travis-ci.org/intolerable/reddit) --- A Haskell library for interacting with the [Reddit API](http://reddit.com/api).@@ -8,6 +8,7 @@ Let's get all the posts from the frontpage of Reddit and write a summary of each of them to the console: ```haskell+{-# LANGUAGE OverloadedStrings #-} import Reddit import Reddit.Types.Post @@ -27,9 +28,10 @@ tshow = Text.pack . show ``` -Let's check to see which of a group of has the highest link karma:+Let's check to see which of a group of users has the highest link karma: ```haskell+{-# LANGUAGE OverloadedStrings #-} import Reddit import Reddit.Types.User @@ -39,8 +41,8 @@ usersToCheck = ["nikita-volkov", "simonmar", "bos", "roche"] main = runRedditAnon $ do- infos <- mapM (getUserInfo . Username) usersToCheck- return $ maximumBy (comparing linkKarma) infos+ infos <- mapM (getUserInfo . Username) usersToCheck+ return $ maximumBy (comparing linkKarma) infos ``` Testing
reddit.cabal view
@@ -1,14 +1,14 @@ name: reddit-version: 0.1.1.0+version: 0.2.0.0 synopsis: Library for interfacing with Reddit's API description: A library for interfacing with Reddit's API in Haskell. Handles- logins, rate-limiting and converted to and from JSON responses.+ logins, rate-limiting and converting to and from JSON responses. Supports most user-facing Reddit API functions, as well as some moderator endpoints. - Check out the readme at https://github.com/intolerable/reddit.+ Check out the readme at <https://github.com/intolerable/reddit>. Contributions are welcome. license: BSD2@@ -73,7 +73,6 @@ Reddit.Routes.Message Reddit.Routes.Moderation Reddit.Routes.Post- Reddit.Routes.Run Reddit.Routes.Search Reddit.Routes.Subreddit Reddit.Routes.Thing@@ -83,25 +82,29 @@ Reddit.Types.Empty Reddit.Utilities default-extensions:- FlexibleInstances,+ FlexibleInstances+ GADTs+ GeneralizedNewtypeDeriving+ LambdaCase OverloadedStrings default-language: Haskell2010 hs-source-dirs: src/ build-depends: base >= 4.6 && < 4.9,- aeson,- api-builder >= 0.9 && < 0.10,- bytestring,- data-default,- http-conduit,- http-types,- network,- stm,- text,- time,+ aeson >= 0.9 && < 0.10,+ api-builder >= 0.10 && < 0.12,+ bytestring == 0.10.*,+ data-default-class == 0.0.1,+ free >= 4 && < 5,+ http-client >= 0.4.11 && < 0.4.21,+ http-client-tls >= 0.2 && < 0.2.3,+ http-types == 0.8.*,+ network == 2.6.*,+ text == 1.*,+ time == 1.5.*, transformers == 0.4.*,- unordered-containers,- vector+ unordered-containers == 0.2.5.*,+ vector >= 0.10 && < 0.12 ghc-options: -Wall test-suite test@@ -113,9 +116,9 @@ build-depends: base == 4.*, Cabal >= 1.16.0,+ aeson, api-builder, bytestring,- http-conduit, reddit, hspec, text,@@ -136,7 +139,8 @@ Cabal >= 1.16.0, api-builder, bytestring,- http-conduit,+ http-client,+ http-client-tls, reddit, hspec, text,@@ -156,7 +160,6 @@ base == 4.*, Cabal >= 1.16.0, api-builder,- http-conduit, reddit, hspec, text,
src/Reddit.hs view
@@ -8,7 +8,10 @@ ( runReddit , runRedditAnon , runRedditWith+ , runResumeRedditWith+ , interpretIO , RedditOptions(..)+ , defaultRedditOptions , LoginMethod(..) -- * Re-exports , APIError(..)@@ -23,14 +26,18 @@ import Reddit.Types import Reddit.Types.Reddit hiding (info, should) -import Control.Concurrent.STM.TVar+import Control.Concurrent+import Control.Monad import Control.Monad.IO.Class+import Control.Monad.Trans.Free import Data.ByteString.Char8 (ByteString)-import Data.Default+import Data.Default.Class import Data.Text (Text) import Data.Text.Encoding (encodeUtf8)-import Network.API.Builder-import Network.HTTP.Conduit+import Network.API.Builder as API+import Network.HTTP.Client+import Network.HTTP.Client.TLS+import Network.HTTP.Types -- | Options for how we should run the 'Reddit' action. --@@ -53,6 +60,10 @@ instance Default RedditOptions where def = RedditOptions True Nothing Anonymous Nothing +-- | The default set of options+defaultRedditOptions :: RedditOptions+defaultRedditOptions = def+ -- | Should we log in to Reddit? If so, should we use a stored set of credentials -- or get a new fresh set? data LoginMethod = Anonymous -- ^ Don't login, instead use an anonymous account@@ -82,23 +93,78 @@ -- most things, but it's handy if you want to persist a connection over multiple 'Reddit' sessions or -- use a custom user agent string. runRedditWith :: MonadIO m => RedditOptions -> RedditT m a -> m (Either (APIError RedditError) a)-runRedditWith (RedditOptions rl man lm ua) (RedditT reddit) = do- rli <- liftIO $ newTVarIO $ RateLimits rl Nothing+runRedditWith opts reddit = liftM dropResume $ runResumeRedditWith opts reddit++-- | Run a 'Reddit' or 'RedditT' action with custom settings. You probably won't need this function for+-- most things, but it's handy if you want to persist a connection over multiple 'Reddit' sessions or+-- use a custom user agent string.+runResumeRedditWith :: MonadIO m => RedditOptions -> RedditT m a -> m (Either (APIError RedditError, Maybe (RedditT m a)) a)+runResumeRedditWith (RedditOptions rl man lm _ua) reddit = do manager <- case man of Just m -> return m- Nothing -> liftIO $ newManager conduitManagerSettings- (res, _, _) <- runAPI builder manager rli $ do- customizeRequest $ addHeader ua- case lm of- StoredDetails (LoginDetails (Modhash mh) cj) ->- customizeRequest $ \r ->- addHeader ua r { cookieJar = Just cj- , requestHeaders = ("X-Modhash", encodeUtf8 mh):requestHeaders r }- Credentials user pass -> do- LoginDetails (Modhash mh) cj <- unRedditT $ login user pass- customizeRequest $ \r ->- addHeader ua r { cookieJar = Just cj- , requestHeaders = ("X-Modhash", encodeUtf8 mh):requestHeaders r }- Anonymous -> return ()- reddit+ Nothing -> liftIO $ newManager tlsManagerSettings+ loginCreds <- case lm of+ Anonymous -> return $ Right Nothing+ StoredDetails ld -> return $ Right $ Just ld+ Credentials user pass -> liftM (fmap Just) $ interpretIO (RedditState loginBaseURL rl manager [] Nothing) $ login user pass+ case loginCreds of+ Left (err, _) -> return $ Left (err, Just reddit)+ Right lds ->+ interpretIO+ (RedditState mainBaseURL rl manager [("User-Agent", "reddit-haskell dev version")] lds) reddit++interpretIO :: MonadIO m => RedditState -> RedditT m a -> m (Either (APIError RedditError, Maybe (RedditT m a)) a)+interpretIO rstate (RedditT r) =+ runFreeT r >>= \case+ Pure x -> return $ Right x+ Free (WithBaseURL u x n) ->+ interpretIO (rstate { currentBaseURL = u }) x >>= \case+ Left (err, Just resume) ->+ return $ Left (err, Just $ resume >>= RedditT . n)+ Left (err, Nothing) -> return $ Left (err, Nothing)+ Right res -> interpretIO rstate $ RedditT $ n res+ Free (FailWith x) -> return $ Left (x, Nothing)+ Free (Nest x n) ->+ interpretIO rstate $ RedditT $ wrap $ NestResuming x (n . dropResume)+ Free (NestResuming x n) -> do+ res <- interpretIO rstate x+ interpretIO rstate $ RedditT $ n res+ Free (RunRoute route n) ->+ interpretIO rstate $ RedditT $ wrap $ ReceiveRoute route (n . unwrapJSON)+ Free (ReceiveRoute route n) ->+ handleReceive route rstate >>= \case+ Left err@(APIError (RateLimitError secs _)) ->+ if rateLimit rstate+ then do+ liftIO $ threadDelay $ fromInteger secs * 1000 * 1000+ interpretIO rstate $ RedditT $ wrap $ ReceiveRoute route n+ else return $ Left (err, Just $ RedditT $ wrap $ ReceiveRoute route n)+ Left err -> return $ Left (err, Just $ RedditT $ wrap $ ReceiveRoute route n)+ Right x -> interpretIO rstate $ RedditT $ n x++dropResume :: Either (APIError RedditError, Maybe (RedditT m a)) a -> Either (APIError RedditError) a+dropResume (Left (x, _)) = Left x+dropResume (Right x) = Right x++handleReceive :: (MonadIO m, Receivable a) => Route -> RedditState -> m (Either (APIError RedditError) a)+handleReceive r rstate = do+ (res, _, _) <- runAPI (builderFromState rstate) (connMgr rstate) () $+ API.runRoute r return res++builderFromState :: RedditState -> Builder+builderFromState (RedditState burl _ _ hdrs (Just (LoginDetails (Modhash mh) cj))) =+ Builder "Reddit" burl addAPIType $+ \req -> addHeaders (("X-Modhash", encodeUtf8 mh):hdrs) req { cookieJar = Just cj }+builderFromState (RedditState burl _ _ hdrs Nothing) =+ Builder "Reddit" burl addAPIType (addHeaders hdrs)++addHeaders :: [Header] -> Request -> Request+addHeaders xs req = req { requestHeaders = requestHeaders req ++ xs }++data RedditState =+ RedditState { currentBaseURL :: Text+ , rateLimit :: Bool+ , connMgr :: Manager+ , _extraHeaders :: [Header]+ , _creds :: Maybe LoginDetails }
src/Reddit/Actions/Captcha.hs view
@@ -7,21 +7,18 @@ ( needsCaptcha , newCaptcha ) where -import Reddit.Routes.Run import Reddit.Types.Captcha import Reddit.Types.Reddit import qualified Reddit.Routes.Captcha as Route -import Control.Monad.IO.Class- -- | Find out if the account currently logged in requires a captcha to be submitted for -- certain requests (like sending a private message or submitting a post).-needsCaptcha :: MonadIO m => RedditT m Bool+needsCaptcha :: Monad m => RedditT m Bool needsCaptcha = runRoute Route.needsCaptcha -- | Returns the ID of a captcha to be completed (the image for which can be found at -- <http://reddit.com/captcha/$CAPTCHA_ID>)-newCaptcha :: MonadIO m => RedditT m CaptchaID+newCaptcha :: Monad m => RedditT m CaptchaID newCaptcha = do POSTWrapped c <- runRoute Route.newCaptcha return c
src/Reddit/Actions/Comment.hs view
@@ -10,7 +10,6 @@ , deleteComment , removeComment ) where -import Reddit.Routes.Run import Reddit.Types.Comment import Reddit.Types.Empty import Reddit.Types.Error@@ -21,8 +20,7 @@ import Reddit.Types.Subreddit import qualified Reddit.Routes as Route -import Control.Monad.IO.Class-import Data.Default+import Data.Default.Class import Data.Text (Text) import Network.API.Builder (APIError(..)) @@ -30,18 +28,18 @@ -- This maps to <http://reddit.com/r/$SUBREDDIT/comments>, or <http://reddit.com/comments> -- if the subreddit is not specified. -- Note that none of the comments returned will have any child comments.-getNewComments :: MonadIO m => Maybe SubredditName -> RedditT m CommentListing+getNewComments :: Monad m => Maybe SubredditName -> RedditT m CommentListing getNewComments = getNewComments' def -- | Get a 'CommentListing' for the most recent comments with the specified 'Options' and -- 'SubredditName'. Note that none of the comments returned will have any child comments. -- If the 'Options' is 'def', then this function is identical to 'getNewComments'.-getNewComments' :: MonadIO m => Options CommentID -> Maybe SubredditName -> RedditT m CommentListing+getNewComments' :: Monad m => Options CommentID -> Maybe SubredditName -> RedditT m CommentListing getNewComments' opts r = runRoute $ Route.newComments opts r -- | Expand children comments that weren't fetched on initial load. -- Equivalent to the web UI's "load more comments" button.-getMoreChildren :: MonadIO m+getMoreChildren :: Monad m => PostID -- ^ @PostID@ for the top-level -> [CommentID] -- ^ List of @CommentID@s to expand -> RedditT m [CommentReference]@@ -53,7 +51,7 @@ return $ rs ++ more -- | Given a 'CommentID', 'getCommentInfo' will return the full details for that comment.-getCommentInfo :: MonadIO m => CommentID -> RedditT m Comment+getCommentInfo :: Monad m => CommentID -> RedditT m Comment getCommentInfo c = do res <- getCommentsInfo [c] case res of@@ -63,7 +61,7 @@ -- | Given a list of 'CommentID's, 'getCommentsInfo' will return another list containing -- the full details for all the comments. Note that Reddit's -- API imposes a limitation of 100 comments per request, so this function will fail immediately if given a list of more than 100 IDs.-getCommentsInfo :: MonadIO m => [CommentID] -> RedditT m CommentListing+getCommentsInfo :: Monad m => [CommentID] -> RedditT m CommentListing getCommentsInfo cs = if null $ drop 100 cs then do@@ -79,7 +77,7 @@ sameLength _ _ = False -- | Edit a comment.-editComment :: MonadIO m+editComment :: Monad m => CommentID -- ^ Comment to edit -> Text -- ^ New comment text -> RedditT m Comment@@ -89,10 +87,10 @@ -- | Deletes one of your own comments. Note that this is different from -- removing a comment as a moderator action.-deleteComment :: MonadIO m => CommentID -> RedditT m ()+deleteComment :: Monad m => CommentID -> RedditT m () deleteComment = nothing . runRoute . Route.delete -- | Removes a comment (as a moderator action). Note that this is different -- from deleting a comment.-removeComment :: MonadIO m => CommentID -> RedditT m ()+removeComment :: Monad m => CommentID -> RedditT m () removeComment = nothing . runRoute . Route.removePost False
src/Reddit/Actions/Flair.hs view
@@ -6,7 +6,6 @@ , flairCSV ) where import Reddit.Routes.Flair-import Reddit.Routes.Run import Reddit.Types.Empty import Reddit.Types.Flair import Reddit.Types.Options@@ -15,24 +14,23 @@ import Reddit.Types.User import Control.Monad-import Control.Monad.IO.Class import Data.Aeson-import Data.Default+import Data.Default.Class import Data.Text (Text) -- | Get the flair list for a subreddit. Requires moderator privileges on -- the subreddit.-getFlairList :: MonadIO m => SubredditName -> RedditT m FlairListing+getFlairList :: Monad m => SubredditName -> RedditT m FlairListing getFlairList = getFlairList' def -- | Get the flair list for a subreddit (with 'Options'). Requires moderator -- privileges on the subreddit.-getFlairList' :: MonadIO m => Options UserID -> SubredditName -> RedditT m FlairListing+getFlairList' :: Monad m => Options UserID -> SubredditName -> RedditT m FlairListing getFlairList' opts r = liftM flistToListing $ runRoute (flairList opts r) -- | Add link flair to the subreddit-wide template for a subreddit that you moderate. -- Requires moderator privileges on the subreddit.-addLinkFlair :: MonadIO m+addLinkFlair :: Monad m => SubredditName -- ^ The subreddit whose template you want to modify -> Text -- ^ The intended CSS class of the new link flair -> Text -- ^ The intended text label of the new link flair@@ -41,6 +39,6 @@ addLinkFlair r c l e = nothing $ runRoute $ addLinkFlairTemplate r c l e -flairCSV :: MonadIO m => SubredditName -> [(Username, Text, Text)] -> RedditT m Value+flairCSV :: Monad m => SubredditName -> [(Username, Text, Text)] -> RedditT m Value flairCSV r sets = runRoute $ flairCSVRoute r sets
src/Reddit/Actions/Message.hs view
@@ -7,9 +7,9 @@ , getUnread' , markRead , sendMessage- , sendMessageWithCaptcha ) where+ , sendMessageWithCaptcha+ , replyMessage ) where -import Reddit.Routes.Run import Reddit.Types.Captcha import Reddit.Types.Empty import Reddit.Types.Listing@@ -19,38 +19,38 @@ import Reddit.Types.Thing import Reddit.Types.User import qualified Reddit.Routes.Message as Route+import qualified Reddit.Routes.Thing as Route -import Control.Monad.IO.Class-import Data.Default+import Data.Default.Class import Data.Text (Text) import Network.API.Builder.Query -- | Get the message inbox for the current user.-getInbox :: MonadIO m => RedditT m (Listing MessageKind Message)+getInbox :: Monad m => RedditT m (Listing MessageKind Message) getInbox = runRoute $ Route.inbox False def -- | Don't use this for watching for new messages, Reddit's ordering on -- | inbox messages is odd and not likely to work how you expect.-getInbox' :: MonadIO m => Bool -> Options MessageKind -> RedditT m (Listing MessageKind Message)+getInbox' :: Monad m => Bool -> Options MessageKind -> RedditT m (Listing MessageKind Message) getInbox' m o = runRoute $ Route.inbox m o -- | Get any unread messages for the current user.-getUnread :: MonadIO m => RedditT m (Listing MessageKind Message)+getUnread :: Monad m => RedditT m (Listing MessageKind Message) getUnread = runRoute $ Route.unread False def -- | Get unread messages for the current user, with options.-getUnread' :: MonadIO m+getUnread' :: Monad m => Bool -- ^ Whether the orangered notifier should be marked "off" -> Options MessageKind -> RedditT m (Listing MessageKind Message) getUnread' m o = runRoute $ Route.unread m o -- | Mark a message as read.-markRead :: (ToQuery a, Thing a, MonadIO m) => a -> RedditT m ()+markRead :: (ToQuery a, Thing a, Monad m) => a -> RedditT m () markRead = nothing . runRoute . Route.readMessage -- | Send a private message to another user.-sendMessage :: MonadIO m+sendMessage :: Monad m => Username -- ^ The username to send the message to -> Text -- ^ The subject of the message being sent -> Text -- ^ The body of the message being sent@@ -58,7 +58,7 @@ sendMessage u s b = nothing $ runRoute $ Route.sendMessage u s b -- | Send a private message (with a captcha).-sendMessageWithCaptcha :: MonadIO m+sendMessageWithCaptcha :: Monad m => Username -- ^ The username to send the message to -> Text -- ^ The subject of the message being sent -> Text -- ^ The body of the message being sent@@ -66,3 +66,12 @@ -> Text -- ^ The answer to the specified captcha -> RedditT m () sendMessageWithCaptcha u s b i c = nothing $ runRoute $ Route.sendMessage u s b `withCaptcha` (i, c)++-- | Reply to a message+replyMessage :: (Monad m, Thing a)+ => a -- ^ Thing to reply to+ -> Text -- ^ Response contents+ -> RedditT m MessageID+replyMessage t b = do+ POSTWrapped res <- runRoute $ Route.reply t b+ return res
src/Reddit/Actions/Moderation.hs view
@@ -1,7 +1,6 @@ module Reddit.Actions.Moderation where import Reddit.Routes.Moderation-import Reddit.Routes.Run import Reddit.Types.Error import Reddit.Types.Listing import Reddit.Types.Moderation@@ -14,15 +13,17 @@ -- | Get a list of existing bans on a subreddit. -- User must be a moderator of the subreddit.-bans :: Options BanID -> SubredditName -> Reddit (Listing BanID Ban)+bans :: Monad m => Options BanID -> SubredditName -> RedditT m (Listing BanID Ban) bans opts r = runRoute $ bansListing opts r -- | Check to see if a user is banned from a subreddit. Logged-in user must -- be a moderator of the subreddit-lookupBan :: Username -> SubredditName -> Reddit (Maybe Ban)+lookupBan :: Monad m => Username -> SubredditName -> RedditT m (Maybe Ban) lookupBan u r = do- Listing _ _ bs <- runRoute $ banLookup u r :: Reddit (Listing BanID Ban)- case bs of- [b] -> return $ Just b- [] -> return Nothing- _-> failWith (APIError InvalidResponseError)+ res <- runRoute $ banLookup u r+ case res :: Listing BanID Ban of+ Listing _ _ bs ->+ case bs of+ [b] -> return $ Just b+ [] -> return Nothing+ _-> failWith (APIError InvalidResponseError)
src/Reddit/Actions/Post.hs view
@@ -26,7 +26,6 @@ , setContestMode ) where import qualified Reddit.Routes as Route-import Reddit.Routes.Run import Reddit.Types import Reddit.Types.Captcha import Reddit.Types.Comment@@ -34,15 +33,14 @@ import Reddit.Types.Listing import Reddit.Types.Reddit -import Control.Monad.IO.Class-import Data.Default+import Data.Default.Class import Data.Text (Text) import Network.API.Builder.Error (APIError(..)) import qualified Data.Char as Char import qualified Data.Text as Text -- | Given a 'PostID', 'getPostInfo' will return the full details for that post.-getPostInfo :: MonadIO m => PostID -> RedditT m Post+getPostInfo :: Monad m => PostID -> RedditT m Post getPostInfo p = do res <- getPostsInfo [p] case res of@@ -52,7 +50,7 @@ -- | Given a list of 'PostID's, 'getPostsInfo' will return another list containing -- the full details for all the posts. Note that Reddit's -- API imposes a limitation of 100 posts per request, so this function will fail immediately if given a list of more than 100 IDs.-getPostsInfo :: MonadIO m => [PostID] -> RedditT m PostListing+getPostsInfo :: Monad m => [PostID] -> RedditT m PostListing getPostsInfo ps = -- we can only get 100 posts at a time or the api shits itself if null $ drop 100 ps@@ -70,24 +68,24 @@ -- | Get a 'PostListing' for the 'Hot' posts on the site overall. -- This maps to <http://reddit.com>.-getPosts :: MonadIO m => RedditT m PostListing+getPosts :: Monad m => RedditT m PostListing getPosts = getPosts' def Hot Nothing -- | Get a 'PostListing' for a specified listing.-getPosts' :: MonadIO m => Options PostID -> ListingType -> Maybe SubredditName -> RedditT m PostListing+getPosts' :: Monad m => Options PostID -> ListingType -> Maybe SubredditName -> RedditT m PostListing getPosts' o l r = runRoute $ Route.postsListing o r (Text.pack $ lower $ show l) where lower = map Char.toLower -- | Save a post.-savePost :: MonadIO m => PostID -> RedditT m ()+savePost :: Monad m => PostID -> RedditT m () savePost = nothing . runRoute . Route.savePost -- | Remove a saved post from your "saved posts" list.-unsavePost :: MonadIO m => PostID -> RedditT m ()+unsavePost :: Monad m => PostID -> RedditT m () unsavePost = nothing . runRoute . Route.unsavePost -- | Submit a new link to Reddit.-submitLink :: MonadIO m+submitLink :: Monad m => SubredditName -- ^ The subreddit to which you're posting the link -> Text -- ^ The title of the link post -> Text -- ^ The link that you're posting@@ -97,7 +95,7 @@ return res -- | Submit a new link to Reddit (answering a Captcha to prove we aren't a robot).-submitLinkWithCaptcha :: MonadIO m+submitLinkWithCaptcha :: Monad m => SubredditName -- ^ The subreddit to which you're posting the link -> Text -- ^ The title of the link post -> Text -- ^ The link that you're posting@@ -109,7 +107,7 @@ return res -- | Submit a new selfpost to Reddit.-submitSelfPost :: MonadIO m+submitSelfPost :: Monad m => SubredditName -- ^ The subreddit to which you're posting the selfpost -> Text -- ^ The title of the selfpost -> Text -- ^ The body of the selfpost@@ -119,7 +117,7 @@ return res -- | Submit a new selfpost to Reddit (answering a Captcha to prove we aren't a robot).-submitSelfPostWithCaptcha :: MonadIO m+submitSelfPostWithCaptcha :: Monad m => SubredditName -- ^ The subreddit to which you're posting the selfpost -> Text -- ^ The title of the selfpost -> Text -- ^ The body of the selfpost@@ -132,12 +130,12 @@ -- | Deletes one of your own posts. Note that this is different from -- removing a post as a moderator action.-deletePost :: MonadIO m => PostID -> RedditT m ()+deletePost :: Monad m => PostID -> RedditT m () deletePost = nothing . runRoute . Route.delete -- | Set the link flair for a post you've submitted (or any post on a subreddit -- that you moderate).-setPostFlair :: MonadIO m+setPostFlair :: Monad m => SubredditName -- ^ The subreddit on which to set the flair -> PostID -- ^ The post whose flair should be set -> Text -- ^ The text label for the post's new flair@@ -146,49 +144,49 @@ setPostFlair r p text css = nothing $ runRoute $ Route.postFlair r p text css -- | Edit the text of a self-post.-editPost :: MonadIO m => PostID -> Text -> RedditT m ()+editPost :: Monad m => PostID -> Text -> RedditT m () editPost thing text = nothing $ runRoute $ Route.edit thing text -- | Get a post and all its comments.-getPostComments :: MonadIO m => PostID -> RedditT m PostComments+getPostComments :: Monad m => PostID -> RedditT m PostComments getPostComments p = runRoute $ Route.getComments p Nothing -- | Get a post and a specific sub-tree of comments.-getPostSubComments :: MonadIO m => PostID -> CommentID -> RedditT m PostComments+getPostSubComments :: Monad m => PostID -> CommentID -> RedditT m PostComments getPostSubComments p c = runRoute $ Route.getComments p (Just c) -- | Get the comments for a post. Ignore the actual post itself.-getComments :: MonadIO m => PostID -> RedditT m [CommentReference]+getComments :: Monad m => PostID -> RedditT m [CommentReference] getComments p = do PostComments _ c <- getPostComments p return c -- | Set the state of inbox replies for the specified thread.-setInboxReplies :: MonadIO m => Bool -> PostID -> RedditT m ()+setInboxReplies :: Monad m => Bool -> PostID -> RedditT m () setInboxReplies enabled = nothing . runRoute . Route.sendReplies enabled -- | Set the state of contest for the specified thread as a moderator action.-setContestMode :: MonadIO m => Bool -> PostID -> RedditT m ()+setContestMode :: Monad m => Bool -> PostID -> RedditT m () setContestMode enabled = nothing . runRoute . Route.setContestMode enabled -- | Removes a post (as a moderator action). Note that this is different -- from deleting a post.-removePost :: MonadIO m => PostID -> RedditT m ()+removePost :: Monad m => PostID -> RedditT m () removePost = nothing . runRoute . Route.removePost False -- | Mark a post as spam as a moderator action.-markPostSpam :: MonadIO m => PostID -> RedditT m ()+markPostSpam :: Monad m => PostID -> RedditT m () markPostSpam = nothing . runRoute . Route.removePost True -- | Sticky a post on the subreddit on which it's posted.-stickyPost :: MonadIO m+stickyPost :: Monad m => PostID -- ^ The post to be stickied -> Maybe Integer -- ^ The position to which it should be stickied -> RedditT m () stickyPost p n = nothing $ runRoute $ Route.stickyPost True p n -- | Unsticky a post from the subreddit on which it's posted.-unstickyPost :: MonadIO m+unstickyPost :: Monad m => PostID -- ^ The post to be unstickied -> Maybe Integer -- ^ The position from which it should be unstickied -> RedditT m ()
src/Reddit/Actions/Search.hs view
@@ -1,6 +1,5 @@ module Reddit.Actions.Search where -import Reddit.Routes.Run import Reddit.Routes.Search import Reddit.Types.Options import Reddit.Types.Post@@ -10,14 +9,14 @@ import Data.Text (Text) -search :: Maybe SubredditName -> Options PostID -> Search.Order -> Text -> Reddit PostListing+search :: Monad m => Maybe SubredditName -> Options PostID -> Search.Order -> Text -> RedditT m PostListing search sub opts order query = runRoute $ searchRoute sub opts order "plain" query -luceneSearch :: Maybe SubredditName -> Options PostID -> Search.Order -> Text -> Reddit PostListing+luceneSearch :: Monad m => Maybe SubredditName -> Options PostID -> Search.Order -> Text -> RedditT m PostListing luceneSearch sub opts order query = runRoute $ searchRoute sub opts order "lucene" query -cloudSearch :: Maybe SubredditName -> Options PostID -> Search.Order -> Text -> Reddit PostListing+cloudSearch :: Monad m => Maybe SubredditName -> Options PostID -> Search.Order -> Text -> RedditT m PostListing cloudSearch sub opts order query = runRoute $ searchRoute sub opts order "cloudsearch" query
src/Reddit/Actions/Subreddit.hs view
@@ -4,24 +4,22 @@ , getSubredditSettings , setSubredditSettings ) where -import Reddit.Routes.Run import Reddit.Types import Reddit.Types.Empty+import Reddit.Types.Reddit import Reddit.Types.Subreddit import Reddit.Types.SubredditSettings import qualified Reddit.Routes as Route -import Control.Monad.IO.Class- -- | Get the info for a specific subreddit. This info includes things like -- sidebar contents, description and ID.-getSubredditInfo :: MonadIO m => SubredditName -> RedditT m Subreddit+getSubredditInfo :: Monad m => SubredditName -> RedditT m Subreddit getSubredditInfo = runRoute . Route.aboutSubreddit -- | Get the settings for a subreddit that you moderate.-getSubredditSettings :: MonadIO m => SubredditName -> RedditT m SubredditSettings+getSubredditSettings :: Monad m => SubredditName -> RedditT m SubredditSettings getSubredditSettings = runRoute . Route.subredditSettings -- | Modify the settings for a subreddit that you moderate.-setSubredditSettings :: MonadIO m => SubredditID -> SubredditSettings -> RedditT m ()+setSubredditSettings :: Monad m => SubredditID -> SubredditSettings -> RedditT m () setSubredditSettings r s = nothing $ runRoute (Route.setSubredditSettings r s)
src/Reddit/Actions/Thing.hs view
@@ -5,17 +5,15 @@ , Reddit.Actions.Thing.delete , Reddit.Actions.Thing.report ) where -import Reddit.Routes.Run import Reddit.Types import Reddit.Types.Empty import Reddit.Types.Reddit import qualified Reddit.Routes.Thing as Route -import Control.Monad.IO.Class import Data.Text (Text) -- | Reply to a something (a post \/ comment \/ message)-reply :: (MonadIO m, Thing a)+reply :: (Monad m, Thing a) => a -- ^ Thing to reply to -> Text -- ^ Response contents -> RedditT m CommentID@@ -26,13 +24,13 @@ -- | Delete something you created. Note that this is different to removing -- a post / comment as a moderator action. Deleting something you don't -- own won't error (but naturally won't delete anything either).-delete :: (MonadIO m, Thing a)+delete :: (Monad m, Thing a) => a -- ^ Thing to delete -> RedditT m () delete = nothing . runRoute . Route.delete -- | Report something.-report :: (MonadIO m, Thing a)+report :: (Monad m, Thing a) => a -- ^ Thing to report -> Text -- ^ Reason for reporting -> RedditT m ()
src/Reddit/Actions/User.hs view
@@ -5,66 +5,75 @@ , aboutMe , getUserComments , getUserComments'+ , getUserPosts+ , getUserPosts' , isUsernameAvailable , getBlockedUsers , getFriends , lookupUserFlair , setUserFlair ) where -import Reddit.Routes.Run import Reddit.Types.Comment import Reddit.Types.Empty import Reddit.Types.Flair hiding (user) import Reddit.Types.Error import Reddit.Types.Listing import Reddit.Types.Options+import Reddit.Types.Post import Reddit.Types.Reddit import Reddit.Types.Subreddit import Reddit.Types.User import qualified Reddit.Routes.User as Route import Control.Monad-import Control.Monad.IO.Class-import Data.Default+import Data.Default.Class import Data.Text (Text) import Network.API.Builder.Error import qualified Data.Text as Text -- | Get the information Reddit exposes on user behind the specified username-getUserInfo :: MonadIO m => Username -> RedditT m User+getUserInfo :: Monad m => Username -> RedditT m User getUserInfo = runRoute . Route.aboutUser -- | Get the listing of comments authored by the specified user.-getUserComments :: MonadIO m => Username -> RedditT m CommentListing+getUserComments :: Monad m => Username -> RedditT m CommentListing getUserComments = getUserComments' def -- | Get the listing of comments authored by the specified user, with Options.-getUserComments' :: MonadIO m => Options CommentID -> Username -> RedditT m CommentListing+getUserComments' :: Monad m => Options CommentID -> Username -> RedditT m CommentListing getUserComments' opts user = runRoute $ Route.userComments opts user +-- | Get the listing of posts authored by the specified user.+getUserPosts :: Monad m => Username -> RedditT m PostListing+getUserPosts = getUserPosts' def++-- | Get the listing of posts authored by the specified user, with Options.+getUserPosts' :: Monad m => Options PostID -> Username -> RedditT m PostListing+getUserPosts' opts user = runRoute $ Route.userPosts opts user+ -- | Check whether the specified username is still available or has been taken.-isUsernameAvailable :: MonadIO m => Username -> RedditT m Bool+isUsernameAvailable :: Monad m => Username -> RedditT m Bool isUsernameAvailable = runRoute . Route.usernameAvailable -- | Get information of the currently-logged-in user.-aboutMe :: MonadIO m => RedditT m User+aboutMe :: Monad m => RedditT m User aboutMe = runRoute Route.aboutMe -- | Get users blocked by the currently-logged-in user.-getBlockedUsers :: MonadIO m => RedditT m [Relationship]+getBlockedUsers :: Monad m => RedditT m [Relationship] getBlockedUsers = do UserList rs <- runRoute Route.blocked return rs -- | Get friends of the currently-logged-in user.-getFriends :: MonadIO m => RedditT m [Relationship]+getFriends :: Monad m => RedditT m [Relationship] getFriends = do UserList rs <- runRoute Route.friends return rs -- | Check if a user has chosen (or been assign) user flair on a particular -- subreddit. Requires moderator privileges on the specified subreddit.-lookupUserFlair :: MonadIO m => SubredditName -> Username -> RedditT m Flair+lookupUserFlair :: Monad m => SubredditName -> Username -> RedditT m Flair lookupUserFlair r u = do res <- liftM flistToListing $ runRoute (Route.lookupUserFlair r u) case res of@@ -73,7 +82,7 @@ -- | Set a user's flair on the specified subreddit. Requires moderator -- privileges on the specified subreddit.-setUserFlair :: MonadIO m => SubredditName -> Username -> Text -> Text -> RedditT m ()+setUserFlair :: Monad m => SubredditName -> Username -> Text -> Text -> RedditT m () setUserFlair r u txt cls = if Text.length txt > 64 then fail "Flair text too long!"
src/Reddit/Actions/Voting.hs view
@@ -14,37 +14,35 @@ , downvoteComment , unvoteComment ) where -import Reddit.Routes.Run import Reddit.Routes.Vote (VoteDirection(..)) import Reddit.Types import Reddit.Types.Empty+import Reddit.Types.Reddit import qualified Reddit.Routes as Route -import Control.Monad.IO.Class--vote :: (MonadIO m, Thing a) => VoteDirection -> a -> RedditT m ()+vote :: (Monad m, Thing a) => VoteDirection -> a -> RedditT m () vote dir = nothing . runRoute . Route.vote dir -- | Upvote a post.-upvotePost :: MonadIO m => PostID -> RedditT m ()+upvotePost :: Monad m => PostID -> RedditT m () upvotePost = vote UpVote -- | Downvote a post.-downvotePost :: MonadIO m => PostID -> RedditT m ()+downvotePost :: Monad m => PostID -> RedditT m () downvotePost = vote DownVote -- | Remove a vote from a post.-unvotePost :: MonadIO m => PostID -> RedditT m ()+unvotePost :: Monad m => PostID -> RedditT m () unvotePost = vote RemoveVote -- | Upvote a comment.-upvoteComment :: MonadIO m => CommentID -> RedditT m ()+upvoteComment :: Monad m => CommentID -> RedditT m () upvoteComment = vote UpVote -- | Downvote a comment.-downvoteComment :: MonadIO m => CommentID -> RedditT m ()+downvoteComment :: Monad m => CommentID -> RedditT m () downvoteComment = vote RemoveVote -- | Remove a previously-cast vote from a comment.-unvoteComment :: MonadIO m => CommentID -> RedditT m ()+unvoteComment :: Monad m => CommentID -> RedditT m () unvoteComment = vote DownVote
src/Reddit/Actions/Wiki.hs view
@@ -3,24 +3,22 @@ ( getWikiPage , editWikiPage ) where -import Reddit.Routes.Run import Reddit.Types.Empty import Reddit.Types.Reddit import Reddit.Types.Subreddit import Reddit.Types.Wiki import qualified Reddit.Routes as Route -import Control.Monad.IO.Class import Data.Text (Text) -- | Get the specified wiki page on a particular subreddit. Requires -- permission to view the specified wiki page.-getWikiPage :: MonadIO m => SubredditName -> Text -> RedditT m WikiPage+getWikiPage :: Monad m => SubredditName -> Text -> RedditT m WikiPage getWikiPage sub page = runRoute $ Route.wikiPage sub page -- | Edit the specified wiki page on a particular subreddit. Requires -- permission to edit the specified wiki page.-editWikiPage :: MonadIO m+editWikiPage :: Monad m => SubredditName -- ^ Subreddit whose wiki to modify -> Text -- ^ The name of the page you're editing -> Text -- ^ The new markdown content of the page you're editing
src/Reddit/Login.hs view
@@ -1,17 +1,10 @@ module Reddit.Login ( login ) where -import Reddit.Types.Error import Reddit.Types.Reddit -import Control.Concurrent (threadDelay)-import Control.Concurrent.STM.TVar-import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad.Trans.Except-import Control.Monad.Trans.State import Data.Text (Text)-import Network.API.Builder-import Network.HTTP.Conduit+import Network.API.Builder hiding (runRoute) loginRoute :: Text -> Text -> Route loginRoute user pass = Route [ "api", "login" ]@@ -20,29 +13,12 @@ , "passwd" =. pass ] "POST" -getLoginDetails :: MonadIO m => Text -> Text -> RedditT m LoginDetails-getLoginDetails user pass = do- resp <- RedditT $ sendRoute () $ loginRoute user pass- let cj = responseCookieJar resp- case unwrapJSON `fmap` receive resp of- Left x@(APIError (RateLimitError wait _)) -> do- RateLimits limiting _ <- RedditT $ liftState get >>= liftIO . readTVarIO- if limiting- then do- liftIO $ threadDelay $ (fromIntegral wait + 5) * 1000000- getLoginDetails user pass- else RedditT $ ExceptT $ return $ Left x- Left x -> RedditT $ ExceptT $ return $ Left x- Right modhash -> return $ LoginDetails modhash cj-+getLoginDetails :: Monad m => Text -> Text -> RedditT m LoginDetails+getLoginDetails user pass = receiveRoute $ loginRoute user pass -- | Make a login request with the given username and password.-login :: MonadIO m+login :: Monad m => Text -- ^ Username to login with -> Text -- ^ Password to login with -> RedditT m LoginDetails-login user pass = do- RedditT $ baseURL loginBaseURL- d <- getLoginDetails user pass- RedditT $ baseURL mainBaseURL- return d+login = getLoginDetails
− src/Reddit/Routes/Run.hs
@@ -1,81 +0,0 @@-module Reddit.Routes.Run- ( runRoute ) where--import Reddit.Types.Error-import Reddit.Types.Reddit--import Control.Applicative-import Control.Concurrent (threadDelay)-import Control.Concurrent.STM-import Control.Monad (liftM, when)-import Control.Monad.IO.Class (MonadIO, liftIO)-import Control.Monad.Trans.Except-import Control.Monad.Trans.State-import Data.Aeson (FromJSON(..))-import Data.ByteString.Lazy (ByteString)-import Data.Time.Clock-import Network.API.Builder.Routes (Route)-import Network.HTTP.Conduit-import Network.HTTP.Types-import Prelude-import qualified Network.API.Builder as API--runRoute :: (MonadIO m, FromJSON a) => Route -> RedditT m a-runRoute route = do- RateLimits limiting _ <- RedditT $ API.liftState get >>= liftIO . readTVarIO- if limiting- then runRouteWithLimiting route- else RedditT $ API.unwrapJSON `liftM` API.runRoute route--runRouteWithLimiting :: (MonadIO m, FromJSON a) => Route -> RedditT m a-runRouteWithLimiting route = do- RateLimits _ rli <- RedditT $ API.liftState get >>= liftIO . readTVarIO- case rli of- Just r -> do- when (needsReset r) $ waitForReset $ resetTime r- requestAndUpdate- Nothing -> requestAndUpdate- where- requestAndUpdate = do- resp <- nest $ RedditT $ API.routeResponse route- case resp of- Left (API.APIError (RateLimitError resetIn _)) -> do- updateFromZero resetIn- runRouteWithLimiting route- Left x -> RedditT $ ExceptT $ return $ Left x- Right x -> do- case API.unwrapJSON <$> API.receive x of- Left (API.APIError (RateLimitError resetIn _)) -> do- updateFromZero resetIn- runRouteWithLimiting route- Left y -> RedditT $ ExceptT $ return $ Left y- Right y -> do- updateRateLimitInfo $ responseHeaders x- return y--updateRateLimitInfo :: MonadIO m => ResponseHeaders -> RedditT m ()-updateRateLimitInfo hs = do- time <- liftIO getCurrentTime- case headersToRateLimitInfo hs time of- Just rli ->- RedditT $ do- r <- API.liftState get- liftIO $ atomically $ modifyTVar r (\(RateLimits b _) -> RateLimits b $ Just rli)- Nothing -> return ()--updateFromZero :: MonadIO m => Integer -> RedditT m ()-updateFromZero resetIn = do- time <- liftIO getCurrentTime- let resetAt = addUTCTime (fromInteger resetIn) time- RedditT $ do- r <- API.liftState get- liftIO $ atomically $ modifyTVar r (\(RateLimits b _) -> RateLimits b $ Just $ RateLimitInfo 0 0 resetAt)--waitForReset :: MonadIO m => UTCTime -> RedditT m ()-waitForReset dt = do- time <- liftIO getCurrentTime- let wait = floor $ diffUTCTime dt time- liftIO $ threadDelay $ (wait + 5) * 1000000 -- wait five extra seconds to account for timing differences--needsReset :: RateLimitInfo -> Bool-needsReset = (<= 0) . remaining
src/Reddit/Routes/User.hs view
@@ -2,6 +2,7 @@ import Reddit.Types.Comment (CommentID) import Reddit.Types.Options+import Reddit.Types.Post import Reddit.Types.Subreddit import Reddit.Types.User @@ -25,6 +26,15 @@ , "before" =. before opts , "after" =. after opts ] "GET"++userPosts :: Options PostID -> Username -> Route+userPosts opts (Username user) = + Route [ "user", user, "submitted" ]+ [ "limit" =. limit opts + , "before" =. before opts+ , "after" =. after opts ]+ "GET"+ usernameAvailable :: Username -> Route usernameAvailable user = Route [ "api", "username_available.json" ]
src/Reddit/Types/Message.hs view
@@ -3,6 +3,7 @@ import Reddit.Parser import Reddit.Types.Comment import Reddit.Types.Listing+import Reddit.Types.Reddit import Reddit.Types.Thing import Reddit.Types.User import Reddit.Utilities@@ -14,6 +15,7 @@ import Data.Text (Text) import Network.API.Builder.Query import Prelude+import qualified Data.Vector as Vector data Message = Message { messageID :: MessageKind , new :: Bool@@ -57,7 +59,7 @@ _ -> False data MessageID = MessageID Text- deriving (Show, Read, Eq)+ deriving (Show, Read, Eq, Ord) instance FromJSON MessageID where parseJSON (String s) =@@ -70,12 +72,20 @@ instance ToQuery MessageID where toQuery k m = toQuery k (fullName m) +instance FromJSON (POSTWrapped MessageID) where+ parseJSON (Object o) = do+ ms <- (o .: "json") >>= (.: "data") >>= (.: "things")+ case Vector.toList ms of+ [v] -> POSTWrapped <$> (v .: "data" >>= (.: "id"))+ _ -> mempty+ parseJSON _ = mempty+ messagePrefix :: Text messagePrefix = "t4" data MessageKind = CommentMessage CommentID | PrivateMessage MessageID- deriving (Show, Read, Eq)+ deriving (Show, Read, Eq, Ord) instance FromJSON MessageKind where parseJSON s =
src/Reddit/Types/Options.hs view
@@ -1,6 +1,6 @@ module Reddit.Types.Options where -import Data.Default+import Data.Default.Class data PaginationOption a = Before a | After a
src/Reddit/Types/Reddit.hs view
@@ -1,8 +1,12 @@ module Reddit.Types.Reddit ( Reddit , RedditT(..)+ , RedditF(..)+ , runRoute+ , receiveRoute , nest , failWith+ , withBaseURL , Modhash(..) , LoginDetails(..) , POSTWrapped(..)@@ -19,59 +23,62 @@ import Reddit.Types.Error import Control.Applicative-import Control.Concurrent.STM.TVar import Control.Monad.IO.Class+import Control.Monad.Trans.Free import Control.Monad.Trans.Class-import Control.Monad.Trans.Except-import Control.Monad.Trans.Reader (ask)-import Control.Monad.Trans.State (get, put) import Data.Aeson+import Data.ByteString.Lazy (ByteString) import Data.Monoid import Data.Text (Text) import Data.Time.Clock-import Network.API.Builder-import Network.HTTP.Conduit hiding (path)+import Network.API.Builder hiding (runRoute)+import Network.HTTP.Client hiding (path) import Network.HTTP.Types import Prelude import Text.Read (readMaybe) import qualified Data.ByteString.Char8 as BS-import qualified Data.Text as Text type Reddit a = RedditT IO a -newtype RedditT m a = RedditT { unRedditT :: APIT (TVar RateLimits) RedditError m a }+data RedditF m a where+ FailWith :: APIError RedditError -> RedditF m a+ Nest :: RedditT m b -> (Either (APIError RedditError) b -> a) -> RedditF m a+ NestResuming :: RedditT m b -> (Either (APIError RedditError, Maybe (RedditT m b)) b -> a) -> RedditF m a+ ReceiveRoute :: Receivable b => Route -> (b -> a) -> RedditF m a+ RunRoute :: FromJSON b => Route -> (b -> a) -> RedditF m a+ WithBaseURL :: Text -> RedditT m b -> (b -> a) -> RedditF m a -instance Functor m => Functor (RedditT m) where- fmap f (RedditT a) = RedditT (fmap f a)+instance Functor (RedditF m) where+ fmap _ (FailWith x) = FailWith x+ fmap f (Nest a x) = Nest a (fmap f x)+ fmap f (NestResuming a x) = NestResuming a (fmap f x)+ fmap f (ReceiveRoute r x) = ReceiveRoute r (fmap f x)+ fmap f (RunRoute r x) = RunRoute r (fmap f x)+ fmap f (WithBaseURL u a x) = WithBaseURL u a (fmap f x) -instance (Functor m, Monad m) => Applicative (RedditT m) where- pure a = RedditT (pure a)- (RedditT f) <*> (RedditT a) = RedditT (f <*> a)+newtype RedditT m a = RedditT (FreeT (RedditF m) m a)+ deriving (Functor, Applicative, Monad) -instance Monad m => Monad (RedditT m) where- return a = RedditT (return a)- (RedditT a) >>= f = RedditT (a >>= unRedditT . f)- fail = failWith . APIError . FailError . Text.pack+instance MonadTrans RedditT where+ lift = RedditT . lift instance MonadIO m => MonadIO (RedditT m) where- liftIO a = RedditT (liftIO a)+ liftIO = RedditT . liftIO -instance MonadTrans RedditT where- lift = RedditT . lift . lift . lift . lift+runRoute :: (FromJSON a, Monad m) => Route -> RedditT m a+runRoute r = RedditT $ liftF $ RunRoute r id -nest :: MonadIO m => RedditT m a -> RedditT m (Either (APIError RedditError) a)-nest (RedditT a) = do- b <- RedditT $ liftBuilder get- rl <- RedditT $ liftState get- m <- RedditT $ liftManager ask- (res, b', rl') <- lift $ runAPI b m rl a- RedditT $ do- liftBuilder $ put b'- liftState $ put rl'- return res+receiveRoute :: (Receivable a, Monad m) => Route -> RedditT m a+receiveRoute r = RedditT $ liftF $ ReceiveRoute r id +nest :: Monad m => RedditT m a -> RedditT m (Either (APIError RedditError) a)+nest f = RedditT $ liftF $ Nest f id++withBaseURL :: Monad m => Text -> RedditT m a -> RedditT m a+withBaseURL u f = RedditT $ liftF $ WithBaseURL u f id+ failWith :: Monad m => APIError RedditError -> RedditT m a-failWith = RedditT . throwE+failWith = RedditT . liftF . FailWith newtype Modhash = Modhash Text deriving (Show, Read, Eq)@@ -83,6 +90,11 @@ data LoginDetails = LoginDetails Modhash CookieJar deriving (Show, Eq)++instance Receivable LoginDetails where+ receive x = do+ (resp, mh) <- receive x+ return $ LoginDetails (unwrapJSON mh) (responseCookieJar (resp :: Response ByteString)) newtype POSTWrapped a = POSTWrapped a deriving (Show, Read, Eq)
src/Reddit/Types/SubredditSettings.hs view
@@ -10,7 +10,7 @@ import Control.Applicative import Data.Aeson-import Data.Default+import Data.Default.Class import Data.Monoid hiding (Any(..)) import Data.Text (Text) import Network.API.Builder.Query