heddit 0.0.1 → 0.0.2
raw patch · 7 files changed
+391/−5 lines, 7 filesdep +monad-loopsdep ~basenew-component:exe:modsnew-component:exe:paginatingnew-component:exe:streamingPVP ok
version bump matches the API change (PVP)
Dependencies added: monad-loops
Dependency ranges changed: base
API changes (from Hackage documentation)
+ Network.Reddit: class Paginable a
Files
- CHANGELOG.org +5/−0
- README.org +91/−3
- examples/Mods.hs +48/−0
- examples/Paginating.hs +113/−0
- examples/Streaming.hs +74/−0
- heddit.cabal +58/−1
- src/Network/Reddit.hs +2/−1
CHANGELOG.org view
@@ -1,4 +1,9 @@ #+TITLE: CHANGELOG +* 0.0.2++ Add more documentation and examples programs++ Export ~Paginable~ class from ~Reddit.hs~++ ~firstPage~ now gets the API limit of items (100)+ * 0.0.1 + Initial release
README.org view
@@ -3,23 +3,97 @@ ~heddit~ is a Reddit API client library for Haskell. It aims to be (almost) as comprehensive as libraries such as [[https://github.com/praw-dev/praw][praw]], from which it takes major inspiration. Although it is not quite there, it does offer access to various parts of the Reddit API, including: - comments and submissions - user accounts and private messages- - subreddits, wikis, collections, and emojis+ - subreddits, multireddits, wikis, collections, and sub emojis - live threads - moderation and various mod actions- - ... and more+ - ... and more! To start using ~heddit~, visit [[file:./doc/start.org][the (very) quick start guide]] or browse the haddocks in [[file:./src/Network][src/Network]] *WARNING*: ~heddit~ is /definitely/ alpha-quality at the moment. Expect bugs, instability, and incomplete documentation for now! * Table of Contents+- [[Brief example]] - [[file:./doc/start.org][Quick start]] - [[file:./doc/auth.org][Authenticating]] - [[Caveats]] - [[Examples]]+- [[Helpful libraries]] - [[Tests]] - [[License]] +* Brief example+We can briefly demonstrate what you can do with ~heddit~ by getting a list of r/haskell's moderators, printing out some information about them, and then finding out which mod has the highest link karma. You can also directly run this example with [[file:./examples/Mods.hs][the mods example program]] that contains the same code.++#+begin_src haskell+{-# LANGUAGE OverloadedLabels #-}++import Control.Monad.IO.Class ( MonadIO(liftIO) )++import Data.Foldable ( for_, maximumBy )+import Data.Generics.Labels ()+import Data.Generics.Wrapped+import Data.Ord ( comparing )+import qualified Data.Text.IO as T++import Lens.Micro.Platform++import Network.Reddit+-- This module is not exported by default from Network.Reddit, as it exports+-- quite a large number of actions and types+import Network.Reddit.Moderation ( getModerators )++-- This is a small example of how I would use the library. Note that I'm using+-- @generic-lens@ and @microlens-platform@ for record access. This isn't entirely+-- necessary, but I decided to hide record selectors by default. This library+-- defines a lot of record types, and exporting all of the selectors would+-- steal a lot of top-level names in calling code. If you want to use them,+-- you can import the relevant modules directly. For this small example, if+-- you didn't want to use the generic-lens-based approach I'm using, you+-- would need to import Network.Reddit.Types.Account (Account(..))+main :: IO ()+main =+ -- First we'll use @loadClient@ to get a @Client@ by reading the auth+ -- details from an @auth.ini@ file either in the working directory or in+ -- $XDG_DATA_HOME/heddit. @Nothing@ indicates that we'll use the section+ -- marked @[DEFAULT]@ in the ini file.+ --+ -- @runReddit@ takes a @Client@ and a @RedditT@ action to run, which+ -- in this example is @modsInfo@ below+ loadClient Nothing >>= (`runReddit` modsKarmaInfo)+ where+ modsKarmaInfo = do+ mods <-+ -- First, we'll get a @Seq ModAccount@, which is like an+ -- @Account@ but has less information+ (getModerators =<< mkSubredditName "haskell")+ -- So we'll pipe each one into @getUser@ by extracting+ -- the username+ >>= traverse (getUser . (^. #username))+ -- Then, we can print the @linkKarma@ for each account+ -- (I'm using @wrappedTo@ from Data.Generics.Wrapped,+ -- which generically unwraps single-constructor types,+ -- to unwrap the @Username@ and get the @Text@ value)+ for_ mods $ \m -> liftIO . T.putStrLn+ $ mconcat [ m ^. #username & wrappedTo+ , " has "+ , m ^. #linkKarma . to show . packed+ ]++ -- Finally, we can see which mod account has the most+ -- link karma by comparing the sequence of mods using+ -- the @topKarma@ function below+ liftIO . T.putStrLn+ $ mconcat [ "The mod with the most karma is "+ , topKarma mods ^. #username & wrappedTo+ ]++ topKarma = maximumBy (comparing (^. #linkKarma))++#+end_src++See the [[Examples]] section below for more.+ * Caveats There are some major caveats to using ~heddit~ that you may want to consider before deciding to use it, namely: - It is currently alpha quality. It needs more tests and users to work out some of the bugs that are assuredly lurking deep within. The exposed API is a little weird and may be difficult to work with.@@ -27,11 +101,25 @@ - ~heddit~ only supports authenticating via OAuth (see [[file:./doc/auth.org][Authenticating]]) and cannot access Reddit's old JSON API. This means that it is not possible to create a truly anonymous client. Instead, you must register all applications, even those without a user context. * Examples-I would like to add some more executable examples of how to use ~heddit~, particularly to demonstrate some of the weirder points of the API. At the moment, the available example programs are:+Here are some executable examples using different features from ~heddit~ that hopefully illustrate how to use the library and accomplish different tasks. At the moment, the available example programs are:+- The [[file:./examples/Mods.hs][mods example]] shown above+- An [[file:./examples/Paginating.hs][example]] showing how to use ~Listing t a~ and ~Paginator t a~ to paginate through data returned from Reddit (or avoid it altogether).+- Infinitely [[file:./examples/Streaming.hs][streaming]] paginable things as they are created - Getting an initial [[file:./examples/RefreshTokens.hs][refresh token]] +All of the example programs live in the [[file:./examples][examples]] directory and can be run using, for example, ~cabal run exes:<name>~+ * Tests ~heddit~ has two test suites: a simple test of JSON decoding of API responses that runs by default, and the ~io-tests~ that are disabled by default behind a flag of the same name. If you wish to run the ~io-tests~, be aware that these make real requests against Reddit's API endpoints using the authenticated account. After enabling the tests, make sure to create an ~auth.ini~ and ~profile.json~ in the working directory or in ~$XDG_CONFIG_HOME/heddit~. See the ~io-tests~ modules for more info.++* Helpful libraries+There are some other libraries that you may wish to use with ~heddit~:+- ~containers~, for ~Seq~+- ~conduit~, if you wish to [[file:./src/Network/Reddit.hs::stream][stream]] Reddit actions+- ~exceptions~, for ~MonadThrow~ and ~MonadCatch~+- Possibly ~unliftio~, for the ~MonadUnliftIO~ constraint, although this is also exported by ~conduit~. This is really only needed for initializing a ~Client~, which you could just do at the top-level in IO anyway+- ~generic-lens~ if you want to use the lens labels as demonstrated above. Also provides several other helpful lenses and functions for dealing with data generically. Note that the ~generic-lens~ labels are not used internally by ~heddit~, so it is safe to use with other libraries that rely on ~-XOverloadedLabels~+- ~microlens-platform~, ~lens~, or another lens-compatible lib to use with ~generic-lens~, should you choose to use the latter * License ~heddit~ is distributed under a three-clause BSD license. See [[file:LICENSE][the license]] for details. Parts of this library are directly inspired by and/or adapted from ~praw~, which is distributed under a simplified BSD two-clause license. See [[https://github.com/praw-dev/praw/blob/master/LICENSE.txt]]
+ examples/Mods.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE OverloadedLabels #-}++module Mods where++import Control.Monad.IO.Class ( MonadIO(liftIO) )++import Data.Foldable ( for_, maximumBy )+import Data.Generics.Labels ()+import Data.Generics.Wrapped+import Data.Ord ( comparing )+import qualified Data.Text.IO as T++import Lens.Micro.Platform++import Network.Reddit+import Network.Reddit.Moderation ( getModerators )++-- | This tiny program will get the current moderators of r\/haskell, print the+-- link karma for each one, and then find the mod with the highest karma and print+-- the username. At the moment, running this example produces:+--+-- > dons has 76424+-- > jfredett has 748+-- > edwardkmett has 6180+-- > taylorfausak has 6216+-- > Iceland_jack has 1402+-- > BoteboTsebo has 229+-- > AutoModerator has 1000+-- > The mod with the most karma is dons+--+main :: IO ()+main = loadClient Nothing >>= (`runReddit` modsKarmaInfo)+ where+ modsKarmaInfo = do+ mods <- (getModerators =<< mkSubredditName "haskell")+ >>= traverse (getUser . (^. #username))+ for_ mods $ \m -> liftIO . T.putStrLn+ $ mconcat [ m ^. #username & wrappedTo+ , " has "+ , m ^. #linkKarma . to show . packed+ ]++ liftIO . T.putStrLn+ $ mconcat [ "The mod with the most karma is "+ , topKarma mods ^. #username & wrappedTo+ ]++ topKarma = maximumBy (comparing (^. #linkKarma))
+ examples/Paginating.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedLabels #-}++-- | This example program shows how to use 'Paginator's and 'Listing's, which+-- are used in many API endpoints. You may notice that several of the actions+-- exported by this library have a type signature similar to+-- @... Paginator t a -> Listing t a@. In many cases, when you make a request to+-- Reddit, it will respond with a \"view\" of the data along with controls that+-- allow you to paginate through entries. This is represented as the 'Listing'+-- type in @heddit@. You can use this listing to feed the next action to fetch+-- more items in the form of a 'Paginator'. In addition, many of the types that+-- are 'Paginable' (and thus can be used in a 'Paginator') have extra options+-- that can be passed to the 'Paginator' in order to filter, limit or sort items.+--+-- There are are a couple of ways to deal with 'Listing's and 'Paginator's in+-- @heddit@, as the examples below aim to illustrate+module Paginating where++import Data.Foldable ( for_ )+import Data.Generics.Labels ()+import qualified Data.Text.IO as T++import Lens.Micro.Platform++import Network.Reddit++main :: IO ()+main = do+ c <- loadClient Nothing+ subname <- mkSubredditName "haskell"+ results <- runReddit c . sequence+ $ [ ignorePagination, withOptions, secondPage ] <*> [ subname ]+ for_ (zip descs results)+ $ \(desc, res) -> T.putStrLn $ desc <> ": " <> getTitle res+ where+ descs = [ "1st result (without pagination)"+ , "1st result (all-time)"+ , "11th result (with pagination)"+ ]++ getTitle = maybe "No results!" (^. #title)++-- | If you do not want to deal with any of this 'Listing' or 'Paginator',+-- business, but would rather just get the first results that Reddit returns,+-- you can use the convenience function 'firstPage' with your action. This+-- ignores the pagination controls entirely, and just gets the items from the+-- endpoint. You can\'t fetch subsequent items, however, or pass any additional+-- options. You are thus limited to the first 100 items, with default options+ignorePagination :: MonadReddit m => SubredditName -> m (Maybe Submission)+ignorePagination subname =+ -- 'firstPage' just gets up to the first 100 items, using the default+ -- sort and other options+ firstPage (getTopSubmissions subname) <&> (^? _head)++-- | This example uses a 'Paginator' to add an option telling Reddit the timeframe+-- we are interested in.+--+-- With actions that take a @Paginator@ and return a @Listing@, you can+-- provide an initial paginator with @emptyPaginator@. @Paginator@ has+-- a field @opts@ that holds an instance of the 'PaginateOptions' type+-- family. Check the haddocks for the options of different 'Paginable'+-- types+withOptions :: MonadReddit m => SubredditName -> m (Maybe Submission)+withOptions subname = do+ -- In this case, we specify the time range that we are interested in+ submissions <- getTopSubmissions subname+ $ emptyPaginator & #opts . #itemTime ?~ AllTime+ -- @submissions@ is a @Listing SubmissionID Submission@. The @children@+ -- field of a listing holds the actual results. The other fields will+ -- be discussed below+ pure $ submissions ^? #children . _head++-- | In this example, we can use the 'Listing' resulting from an action to get+-- the next \"page\" of results+secondPage :: MonadReddit m => SubredditName -> m (Maybe Submission)+secondPage subname = do+ -- All @Paginator@s have a @limit@ field to specify the number of items+ -- desired, which we will set to 10 (the maximum is ostensibly 100,+ -- although Reddit doesn't seem to care if you exceed this; the default+ -- is 25)+ firstListing <- getTopSubmissions subname firstPaginator+ -- Now that we have a listing, we can turn it into a @Paginator@ to feed+ -- the next API call by using the function 'nextPage'. You can optionally+ -- provide the initial paginator as well to avoid having to set the same+ -- options again.+ --+ -- All @Listing@s have @before@ and @after@ fields which act like anchors+ -- in the "slice" of data. They can be used as pagination controls, but are+ -- not necessarily present. Using @nextPage@ will create a @Paginator@ with+ -- corresponding @before@ and @after@ fields set to the values from the+ -- @Listing@. When both fields are @Just@, @after@ takes precedence.+ --+ -- The behavior of @before@ is a little counter-intuitive (at least to me).+ -- It does /not/ point to data preceding the current first item in the+ -- @children@ field, and will only be non-null if items have been created+ -- that would precede it in the interim since obtaining the @Listing@. That+ -- is, even if you are on the nth page above 1 of the data set, @before@ will+ -- still be @Nothing@ unless entirely new items have been created on+ -- Reddit's end+ nextListing <- getTopSubmissions subname+ $ nextPage (Just firstPaginator) firstListing+ -- We could continue in the same vein, but we'll stop here and return the+ -- first child+ pure $ nextListing ^? #children . _head+ where+ firstPaginator = emptyPaginator & #limit .~ 10++nTimes :: MonadReddit m => SubredditName -> m (Maybe Submission)+nTimes = do+ undefined+ where+ unfoldSubs paginator = do+ undefined
+ examples/Streaming.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE OverloadedLabels #-}++-- | This example will show how to use the 'stream' action to get an infinite+-- stream of new 'Paginable' things. In this example, we will stream new+-- submissions, filter them based on some criteria, and print out some information+-- about them. In a real program, instead of simply printing information, we could+-- take some 'MonadReddit' action instead, such as replying to them+--+-- Running this example will produce the following type of output (truncated for+-- readability here):+--+-- > On 2021-07-18 07:32:21 UTC u/Pututu_Life posted "What is your go to comfort ..+-- > On 2021-07-18 07:32:29 UTC u/Kenney93 posted "What do you do when you are ...+-- > On 2021-07-18 07:32:45 UTC u/CYS801 posted "What is your proudest ...+-- > On 2021-07-18 07:32:53 UTC u/Ambitious-Ad8598 posted "What is the name of ...+-- > On 2021-07-18 07:33:08 UTC u/Additional_Quantity5 posted "What is your ...+--+module Streaming where++import Conduit++import Data.Generics.Labels ()+import Data.Generics.Wrapped+import Data.Text ( Text )+import qualified Data.Text as T+import qualified Data.Text.IO as T++import Lens.Micro.Platform++import Network.Reddit++main :: IO ()+main = loadClient Nothing >>= (`runReddit` streamed)+ where+ -- We can use r/askreddit, which has a fairly high and steady+ -- volume of new submissions being posted+ streamed = streamSubmissions =<< mkSubredditName "askreddit"++-- | Run the stream until interrupted+streamSubmissions :: MonadReddit m => SubredditName -> m ()+streamSubmissions subname = runConduit+ $ stream (Just True) -- This will discard elements that were+ -- already "in" the stream before starting,+ -- so we only get actually new submissions+ action+ .| filterC isAskingWhat+ .| submissionInfo+ .| mapM_C (liftIO . T.putStrLn) -- This will look nicer than+ -- @printC@+ where+ action = getNewSubmissions subname++-- | A silly heuristic to detect questions asking "what ... ?". Of course+-- this will miss questions using the popular format "...s of Reddit,+-- what...?", but r/askreddit is high-volume enough that it will still+-- catch a lot of submissions+isAskingWhat :: Submission -> Bool+isAskingWhat s = case s ^? #title . to (T.words . T.toLower) . _head of+ Just "what" -> True+ _ -> False++-- | Print some information about the submission+submissionInfo :: Monad m => ConduitT Submission Text m ()+submissionInfo = awaitForever $ \s -> yield+ $ mconcat [ "On "+ , s ^. #created . to show . packed+ , " u/"+ , s ^. #author & wrappedTo+ , " posted "+ , "\""+ , s ^. #title+ , "\""+ ]
heddit.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: heddit-version: 0.0.1+version: 0.0.2 synopsis: Reddit API bindings description: See the README at https://gitlab.com/ngua/heddit license: BSD-3-Clause@@ -133,6 +133,63 @@ main-is: RefreshTokens.hs ghc-options: -main-is RefreshTokens+ default-language: Haskell2010++executable mods+ import: common-options, common-extensions+ hs-source-dirs: examples++ if !flag(examples)+ buildable: False++ build-depends:+ , base >=4.13 && <5+ , generic-lens >=1.1 && <2.2+ , heddit+ , microlens-platform ^>=0.4.2+ , text ^>=1.2++ main-is: Mods.hs+ ghc-options: -main-is Mods+ default-language: Haskell2010++executable streaming+ import: common-options, common-extensions+ hs-source-dirs: examples++ if !flag(examples)+ buildable: False++ build-depends:+ , base >=4.13 && <5+ , conduit ^>=1.3+ , generic-lens >=1.1 && <2.2+ , heddit+ , microlens-platform ^>=0.4.2+ , text ^>=1.2++ main-is: Streaming.hs+ ghc-options: -main-is Streaming+ default-language: Haskell2010++executable paginating+ import: common-options, common-extensions+ hs-source-dirs: examples++ -- if !flag(examples)+ -- buildable: False++ build-depends:+ , base >=4.13 && <5+ , containers ^>=0.6+ , generic-lens >=1.1 && <2.2+ , heddit+ , microlens-platform ^>=0.4.2+ , monad-loops ^>=0.4.3+ , text ^>=1.2++ main-is: Paginating.hs+ ghc-options: -main-is Paginating default-language: Haskell2010 test-suite tests
src/Network/Reddit.hs view
@@ -41,6 +41,7 @@ , RateLimits(RateLimits) , Listing(Listing) , Paginator(Paginator)+ , Paginable , ItemOpts(ItemOpts) , defaultItemOpts , ItemSort(..)@@ -256,7 +257,7 @@ firstPage :: (MonadReddit m, Paginable a) => (Paginator t a -> m (Listing t a)) -> m (Seq a)-firstPage f = f emptyPaginator <&> (^. field @"children")+firstPage f = f emptyPaginator { limit = 100 } <&> (^. field @"children") -- | Update a 'Paginator' with a 'Listing' to make a query for the next \"page\" -- of content. If the first argument is @Nothing@, defaults will be used for