packages feed

tweet-hs 0.5.3.13 → 1.0.2.3

raw patch · 20 files changed

Files

LICENSE view
@@ -1,4 +1,4 @@-Copyright Vanessa McHale (c) 2017+Copyright Vanessa McHale (c) 2017, 2020  All rights reserved. 
README.md view
@@ -1,6 +1,8 @@ # Command Line Interface Tweeter +[![Windows build status](https://ci.appveyor.com/api/projects/status/github/vmchale/command-line-tweeter?svg=true)](https://ci.appveyor.com/project/vmchale/command-line-tweeter) [![Build Status](https://travis-ci.org/vmchale/command-line-tweeter.svg?branch=master)](https://travis-ci.org/vmchale/command-line-tweeter)+[![Hackage](https://img.shields.io/hackage/v/tweet-hs.svg)](http://hackage.haskell.org/package/tweet-hs)  ![Displaying a user timeline in a terminal.](https://raw.githubusercontent.com/vmchale/command-line-tweeter/master/screenshot.png) @@ -8,7 +10,7 @@ its [rust counterpart](https://github.com/vmchale/clit-rs) and it's a bit slower.  -Reasons to use tweeth-s:+Reasons to use tweeth-hs:   - Faster than other tools ([t](https://github.com/sferik/t),   [oysttyer](https://github.com/oysttyer/oysttyer))   - Support for colored output. @@ -37,13 +39,14 @@ ## Config Generate a token to authorize access to your twitter account by following the guide [here](https://dev.twitter.com/oauth/overview/application-owner-access-tokens) -Then place your API keys and OAuth tokens in a file `~/.cred`, separated by a line break:+Then place your API keys and OAuth tokens in a file `~/.cred.toml`, as in the+following example:  ```-api-key: API_KEY_HERE-api-sec: API_SECRET_HERE-tok: OAUTH_TOKEN_HERE-tok-sec: TOKEN_SECRET_HERE+api-key = "API_KEY_HERE"+api-sec = "API_SECRET_HERE"+tok = "OAUTH_TOKEN_HERE"+tok-sec = "TOKEN_SECRET_HERE" ```  ## Installation@@ -76,13 +79,21 @@ ```  ### Sending tweets++To send a tweet:++```+tweet send "This is my tweet"+```++#### Input from stdin To tweet from stderr, run a command that pipes stderr to stdin, i.e.  ```-YOUR_BUILD_COMMAND 2>&1 >/dev/null | tweet input+stack build &>/dev/null | tweet input ``` -The `tweet` executable reads from stdIn only, but you can view the options (replies, number of tweets to thread, etc.) with+The `tweet` executable reads from stdin only, but you can view the options (replies, number of tweets to thread, etc.) with  ``` tweet --help@@ -105,6 +116,14 @@ ```  to view your own timeline.++### GHCi integration++You can define the following in your `~/.ghci`++```haskell+:def tweet (\str -> pure $ ":! tweet send \"" ++ str ++ "\"")+```  ### Completions 
− Setup.hs
@@ -1,2 +0,0 @@-import Distribution.Simple-main = defaultMain
app/Main.hs view
@@ -1,6 +1,343 @@ module Main where -import Web.Tweet.Exec+import qualified Data.ByteString.Lazy.Char8 as BSL+import           Data.Foldable              (traverse_)+import           Data.Maybe+import           Data.Version+import           Options.Applicative+import           Paths_tweet_hs+import           System.Directory+import           Web.Tweet +-- | Data type for our program: one optional path to a credential file, (optionally) the number of tweetInputs to make, the id of the status you're replying to, and a list of users you wish to mention.+data Program = Program { subcommand :: Command , cred :: Maybe FilePath , color :: Bool }++-- | Data type for a command+-- TODO add boolean option to show ids alongside tweets+data Command = Timeline { count :: Maybe Int }+    | SendInput { tweetInputs :: Maybe Int, replyId :: Maybe String, replyh :: Maybe [String] }+    | Profile { count :: Maybe Int , screenName'' :: Maybe String, withReplies :: Bool, withRetweets :: Bool }+    | Mentions { count :: Maybe Int }+    | Markov { screenName' :: String }+    | Send { tweets :: Maybe Int , replyId :: Maybe String , replyh :: Maybe [String] , userInput :: String }+    | Sort { screenName' :: String , count :: Maybe Int , includeReplies :: Bool }+    | Delete { twId :: Integer }+    | Fav { twId :: Integer }+    | Unfav { twId :: Integer }+    | Retweet { twId :: Integer }+    | Unretweet { twId :: Integer }+    | List { count :: Maybe Int , screenName' :: String }+    | Follow { screenName' :: String }+    | Unfollow { screenName' :: String }+    | Block { screenName' :: String }+    | Unblock { screenName' :: String }+    | Mute { screenName' :: String }+    | Unmute { screenName' :: String }+    | Dump { screenName' :: String }+    | Replies { screenName' :: String, twId :: Integer }+    | MuteReplies { screenName' :: String, twId :: Integer }+    | MuteMentions { screenName' :: String }++-- | query twitter to post stdin with no fancy options+fromStdIn :: Int -> FilePath -> IO ()+fromStdIn = threadStdIn [] Nothing++-- | Tweet string given to us, as parsed from the command line+fromCLI :: String -> Int -> FilePath -> IO ()+fromCLI s = thread s [] Nothing++-- | Threaded tweetInputs from stdIn+threadStdIn :: [String] -> Maybe Int -> Int -> FilePath -> IO ()+threadStdIn hs idNum num filepath = do+    contents <- getContents+    thread contents hs idNum num filepath++-- | Executes parser main :: IO ()-main = exec+main = execParser opts >>= select+    where+        versionInfo = infoOption ("tweet-hs version: " ++ showVersion version) (short 'v' <> long "version" <> help "Show version")+        opts        = info (helper <*> versionInfo <*> program)+            (fullDesc+            <> progDesc "Tweet and view tweets"+            <> header "tweet - a Command Line Interface Tweeter")++-- | Executes program given parsed `Program`+select :: Program -> IO ()+select (Program com maybeFile c) = case maybeFile of+    (Just file) -> selectCommand com (not c) file+    _ -> selectCommand com (not c) =<< (++ "/.cred.toml") <$> getHomeDirectory++-- | Executes subcommand given subcommand + filepath to configuration file+selectCommand :: Command -> Bool -> FilePath -> IO ()+selectCommand (Send maybeNum Nothing Nothing input) _ file = fromCLI input (fromMaybe 15 maybeNum) file+selectCommand (Send maybeNum (Just rId) Nothing input) _ file = thread input [] (pure . read $ rId) (fromMaybe 15 maybeNum) file+selectCommand (Send maybeNum Nothing (Just h) input) _ file = thread input h Nothing (fromMaybe 15 maybeNum) file+selectCommand (Send maybeNum (Just rId) (Just h) input) _ file = thread input h (pure . read $ rId) (fromMaybe 15 maybeNum) file+selectCommand (SendInput maybeNum Nothing Nothing) _ file  = fromStdIn (fromMaybe 15 maybeNum) file+selectCommand (SendInput maybeNum (Just rId) (Just h)) _ file = threadStdIn h (pure . read $ rId) (fromMaybe 15 maybeNum) file+selectCommand (SendInput maybeNum (Just rId) Nothing) _ file = threadStdIn [] (pure . read $ rId) (fromMaybe 15 maybeNum) file+selectCommand (SendInput maybeNum Nothing (Just h)) _ file = threadStdIn h Nothing (fromMaybe 15 maybeNum) file+selectCommand (Replies uname twid) c file = putStrLn =<< showReplies uname (fromIntegral twid) c file+selectCommand (MuteReplies uname twid) _ file = do+    muted <- muteRepliers uname (fromIntegral twid) file+    putStrLn "Muted:"+    traverse_ putStrLn muted+selectCommand (MuteMentions uname) _ file = muteMentions uname file+selectCommand (Timeline maybeNum) c file = putStrLn =<< showTimeline (fromMaybe 11 maybeNum) c file+selectCommand (Mentions maybeNum) c file = putStrLn =<< showTweets c <$> mentions (fromMaybe 11 maybeNum) file+selectCommand (Profile maybeNum n False False) c file = putStrLn =<< showProfile (fromMaybe mempty n) (fromMaybe 11 maybeNum) c file+selectCommand (Profile maybeNum n True False) c file = putStrLn =<< showFilteredTL [filterReplies] (fromMaybe mempty n) (fromMaybe 11 maybeNum) c file+selectCommand (Profile maybeNum n False True) c file = putStrLn =<< showFilteredTL [filterRTs] (fromMaybe mempty n) (fromMaybe 11 maybeNum) c file+selectCommand (Profile maybeNum n True True) c file = putStrLn =<< showFilteredTL [filterReplies, filterRTs] (fromMaybe mempty n) (fromMaybe 11 maybeNum) c file+selectCommand (Sort n maybeNum False) c file = putStrLn =<< showBest' n (fromMaybe 11 maybeNum) c file+selectCommand (Sort n maybeNum True) c file = putStrLn =<< showBest n (fromMaybe 11 maybeNum) c file+selectCommand (List maybeNum n) c file = putStrLn =<< showFavorites (fromMaybe 11 maybeNum) n c file+selectCommand (Markov n) _ file = do+    raw <- getMarkov n Nothing file+    appendFile (n ++ ".txt") (unlines raw)+    putStrLn $ "Written output to: " ++ n ++ ".txt"+selectCommand (Delete n) c file = do+    putStrLn "Deleted:\n"+    putStrLn =<< showTweets c <$> deleteTweetResponse n file+selectCommand (Fav n) c file = do+    putStrLn "Favorited:\n"+    putStrLn =<< showTweets c <$> favoriteTweetResponse n file+selectCommand (Unfav n) c file = do+    putStrLn "Unfavorited:\n"+    putStrLn =<< showTweets c <$> unfavoriteTweetResponse n file+selectCommand (Retweet n) c file = do+    putStrLn "Retweeted:\n"+    putStrLn =<< showTweets c <$> retweetTweetResponse n file+selectCommand (Unretweet n) c file = do+    putStrLn "Unretweeted:\n"+    putStrLn =<< showTweets c <$> unretweetTweetResponse n file+selectCommand (Follow sn) _ file = do+    follow sn file+    putStrLn ("..." ++ sn ++ " followed successfully!")+selectCommand (Unfollow sn) _ file = do+    unfollow sn file+    putStrLn ("..." ++ sn ++ " unfollowed successfully!")+selectCommand (Block sn) _ file = do+    block sn file+    putStrLn ("..." ++ sn ++ " blocked successfully")+selectCommand (Unblock sn) _ file = do+    unblock sn file+    putStrLn ("..." ++ sn ++ " unblocked successfully")+selectCommand (Mute sn) _ file = do+    mute sn file+    putStrLn ("..." ++ sn ++ " muted successfully")+selectCommand (Unmute sn) _ file = do+    unmute sn file+    putStrLn ("..." ++ sn ++ " unmuted successfully")+selectCommand (Dump sn) _ file = BSL.putStrLn =<< getProfileRaw sn 3200 file Nothing++-- | Parser to return a program datatype+program :: Parser Program+program = Program+    <$> hsubparser+        (command "send" (info tweet (progDesc "Send a tweet from the command-line"))+        <> command "input" (info tweetInput (progDesc "Send a tweet from stdIn"))+        <> command "view" (info timeline (progDesc "Get your timeline"))+        <> command "user" (info profile (progDesc "Get a user's profile"))+        <> command "markov" (info markov (progDesc "Grab tweets en masse."))+        <> command "hits" (info best (progDesc "View a user's top tweets."))+        <> command "del" (info delete (progDesc "Delete a tweet.")) -- TODO delete/favorite in bunches!+        <> command "fav" (info fav (progDesc "Favorite a tweet"))+        <> command "ufav" (info unfav (progDesc "Unfavorite a tweet"))+        <> command "urt" (info unrt (progDesc "Un-retweet a tweet"))+        <> command "rt" (info rt (progDesc "Retweet a tweet"))+        <> command "follow" (info fol (progDesc "Follow a user"))+        <> command "unfollow" (info unfol (progDesc "Unfollow a user"))+        <> command "replies" (info repliesParser (progDesc "Show replies to a tweet"))+        <> command "list" (info list (progDesc "List a user's favorites"))+        <> command "dump" (info dump (progDesc "Dump tweets (for debugging)"))+        <> command "block" (info blockParser (progDesc "Block a user"))+        <> command "unblock" (info unblockParser (progDesc "Unblock a user"))+        <> command "mute" (info muteParser (progDesc "Mute a user"))+        <> command "unmute" (info unmuteParser (progDesc "Unmute a user"))+        <> command "mute-replies" (info muteRepliesParser (progDesc "Mute everyone who replied to a particular tweet"))+        <> command "mute-mentions" (info muteMentionsParser (progDesc "Mute everyone who mentioned a particular user"))+        <> command "mentions" (info mentionsParser (progDesc "Fetch mentions")))+    <*> optional (strOption+        (long "cred"+        <> short 'c'+        <> metavar "CREDENTIALS"+        <> completer (bashCompleter "file -X '!*.toml' -o plusdirs")+        <> help "path to credentials"))+    <*> switch+        (long "color"+        <> short 'l'+        <> help "Turn off colorized terminal output.")++-- | Parser for the view subcommand+timeline :: Parser Command+timeline = Timeline+    <$> optional (read <$> strOption+        (long "count"+        <> short 'n'+        <> metavar "NUM"+        <> help "number of tweetInputs to fetch, default 5"))++-- | Parser for the markov subcommand+markov :: Parser Command+markov = Markov <$> user++-- | Parser for the follow subcommand+fol :: Parser Command+fol = Follow <$> user++-- | Parser for the block subcommand+blockParser :: Parser Command+blockParser = Block <$> user++-- | Parser for the unblock subcommand+unblockParser :: Parser Command+unblockParser = Unblock <$> user++-- | Parser for the dump subcommand+dump :: Parser Command+dump = Dump <$> user++-- | Parser for the unfollow subcommand+unfol :: Parser Command+unfol = Unfollow <$> user++-- | Parser for the list subcommand+list :: Parser Command+list = List+    <$> optional (read <$> strOption+        (long "count"+        <> short 'n'+        <> metavar "NUM"+        <> help "Number of tweetInputs to fetch, default 12"))+    <*> user++-- | Parser for the unfollow subcommand+muteParser :: Parser Command+muteParser = Mute <$> user++-- | Parser for the unfollow subcommand+unmuteParser :: Parser Command+unmuteParser = Unmute <$> user++-- | Parse a user screen name+user :: Parser String+user = argument str+    (metavar "SCREEN_NAME"+    <> help "Screen name of user.")++-- | Parser for the del subcommand+delete :: Parser Command+delete = Delete <$> getInt++-- | Parser for the fav subcommand+fav :: Parser Command+fav = Fav <$> getInt++-- | Parser for the fav subcommand+unfav :: Parser Command+unfav = Unfav <$> getInt++-- | Parser for the fav subcommand+unrt :: Parser Command+unrt = Unretweet <$> getInt++-- | Parser for the fav subcommand+rt :: Parser Command+rt = Retweet <$> getInt++-- | Parser for the del subcommand+getInt :: Parser Integer+getInt = read <$> argument str+    (metavar "TWEET_ID"+    <> help "ID of tweet")++repliesParser :: Parser Command+repliesParser = Replies <$> user <*> getInt++muteRepliesParser :: Parser Command+muteRepliesParser = MuteReplies <$> user <*> getInt++muteMentionsParser :: Parser Command+muteMentionsParser = MuteMentions <$> user++-- | Parser for the user subcommand+profile :: Parser Command+profile = Profile+    <$> optional (read <$> strOption+        (long "count"+        <> short 'n'+        <> metavar "NUM"+        <> help "Number of tweetInputs to fetch, default 12"))+    <*> optional user+    <*> switch (+           long "no-replies"+        <> short 'r'+        <> help "Don't display replies.")+    <*> switch (+           long "no-retweets"+        <> short 't'+        <> help "Don't display retweets.")++-- | Parser for the mention subcommand+mentionsParser :: Parser Command+mentionsParser = Mentions+    <$> optional (read <$> strOption+        (long "count"+        <> short 'n'+        <> metavar "NUM"+        <> help "Number of tweetInputs to fetch, default 12"))++-- | Parse best tweets+best :: Parser Command+best = Sort+    <$> argument str+        (metavar "SCREEN_NAME"+        <> help "Screen name of user you want to view.")+    <*> optional (read <$> strOption+        (long "count"+        <> short 'n'+        <> metavar "NUM"+        <> help "Number of tweetInputs to fetch, default 12"))+    <*> switch+        (long "replies"+        <> short 'r'+        <> help "Include replies in your all-time hits")++-- | Parser for the send subcommand+tweet :: Parser Command+tweet = Send+    <$> optional (read <$> strOption+        (long "tweets"+        <> short 't'+        <> metavar "NUM"+        <> help "Number of tweetInputs in a row, default 15"))+    <*> optional (strOption+        (long "reply"+        <> short 'r'+        <> help "id of status to reply to - be sure to include their handle, e.g. @my_build_errors"))+    <*> optional (some $ strOption+        (long "handle"+        <> short 'h'+        <> metavar "HANDLE1"+        <> help "h to include in replies"))+    <*> (unwords <$> some (argument str+        (metavar "TEXT"+        <> help "text of tweet to be sent")))++-- | Parser for the input command+tweetInput :: Parser Command+tweetInput = SendInput+    <$> optional (read <$> strOption+        (long "tweets"+        <> short 't'+        <> metavar "NUM"+        <> help "Number of tweets in a row, default 15"))+    <*> optional (strOption+        (long "reply"+        <> short 'r'+        <> help "id of status to reply to - be sure to include their handle, e.g. @my_build_errors"))+    <*> optional (some $ argument str+        (metavar "HANDLE1"+        <> help "h to include in replies"))
bench/Bench.hs view
@@ -1,19 +1,20 @@ module Main where -import Criterion.Main-import Text.Megaparsec-import Web.Tweet.Parser-import Web.Tweet.Parser.FastParser-import qualified Data.ByteString as BS+import           Criterion.Main+import qualified Data.ByteString  as BS+import           Text.Megaparsec+import           Web.Tweet.Parser -fun = parse parseTweet ""+setupEnv :: IO BS.ByteString+setupEnv = BS.readFile "test/data" -fast = fastParse+setupEnv' :: IO String+setupEnv' = readFile "test/data" -main = do-    file <- BS.readFile "test/data"-    defaultMain [ bgroup "parseTweet"-                      [ bench "226" $ whnf fun file ]-                , bgroup "fastParser"-                      [ bench "226" $ whnf fast file ]-                ]+main :: IO ()+main =+    defaultMain [+              env setupEnv' $ \ file ->+              bgroup "handrolled parser"+                  [ bench "226" $ whnf (parse parseTweet) file ]+            ]
+ cabal.project.local view
@@ -0,0 +1,3 @@+constraints: tweet-hs +development+optimization: 2+max-backjumps: 40000
src/Web/Tweet.hs view
@@ -1,47 +1,58 @@ -- | Various utilities to tweet using the twitter api--- --- Make sure you have a file credentials file (default the executable looks for is `.cred`) with the following info: --+-- Make sure you have a file credentials file (default the executable looks for is @$HOME/.cred.toml@) with the following info:+-- -- @ ----- api-key: API_KEY+-- api-key = "API_KEY" ----- api-sec: API_SECRE+-- api-sec = "API_SECRET" ----- tok: OAUTH_TOKEN+-- tok = "OAUTH_TOKEN" ----- tok-sec: TOKEN_SECRET+-- tok-sec = "TOKEN_SECRET" -- -- @  module Web.Tweet     (     -- * Functions to tweet-    basicTweet+      basicTweet     , thread+    , reply     -- * Data type for a tweet     , module Web.Tweet.Types     -- * Various API calls     , module Web.Tweet.API+    , module Web.Tweet.API.Internal     -- * Functions to sign API requests     , signRequest+    , oAuthMem+    , credentialMem     -- * Functions to generate a URL string from a `Tweet`     , urlString+    -- * Timeline filters+    , filterReplies+    , filterRTs+    , filterQuotes+    -- * Helper function to print a bird+    , bird     ) where-    -import Web.Tweet.Sign-import Web.Tweet.API-import Web.Tweet.Utils.API-import Web.Tweet.Types-import Data.List.Split (chunksOf)-import Control.Monad-import Control.Lens-import Data.Maybe-import Data.Default +import           Control.Monad+import           Data.List.Split        (chunksOf)+import           Data.Maybe+import           Lens.Micro+import           Web.Tweet.API+import           Web.Tweet.API.Internal+import           Web.Tweet.Sign+import           Web.Tweet.Types+import           Web.Tweet.Utils+import           Web.Tweet.Utils.API+ -- | Tweet a string given a path to credentials; return the id of the status. ----- > basicTweet "On the airplane." ".cred"+-- > basicTweet "On the airplane." ".cred.toml" basicTweet :: String -> FilePath -> IO Int basicTweet contents = tweetData (mkTweet contents) @@ -51,22 +62,22 @@ -- > thread "Hi I'm back in New York!" ["friend1","friend2"] Nothing 1 ".cred" thread :: String -> [String] -> Maybe Int -> Int -> FilePath -> IO () thread contents hs idNum num filepath = do-    let handleStr = concatMap (((++) " ") . ((++) "@")) hs-    let content = (take num) . (chunksOf (140-(length handleStr))) $ contents+    let handleStr = concatMap ((++) " " . (++) "@") hs+    let content = take num . chunksOf (280-length handleStr) $ contents     case idNum of-        (Just i) -> thread' content hs idNum num filepath+        (Just _) -> thread' content hs idNum filepath         Nothing -> case content of-            [] -> pure ()-            [x] -> void $ basicTweet x filepath-            y@(x:xs) -> thread' y hs (Just 0) num filepath+            []      -> pure ()+            [x]     -> void $ basicTweet x filepath+            y@(_:_) -> thread' y hs (Just 0) filepath --- | Helper function to make `thread` easier to write. -thread' :: [String] -> [String] -> Maybe Int -> Int -> FilePath -> IO ()-thread' content hs idNum num filepath = do-    let f = \str i -> tweetData (Tweet { _status = str, _handles = hs, _replyID = if i == 0 then Nothing else Just i }) filepath+-- | Helper function to make `thread` easier to write.+thread' :: [String] -> [String] -> Maybe Int -> FilePath -> IO ()+thread' content hs idNum filepath = do+    let f str i = tweetData Tweet { _status = str, _handles = hs, _replyID = if i == 0 then Nothing else Just i } filepath     let initial = f (head content)-    last <- foldr ((>=>) . f) initial (content) $ fromMaybe 0 idNum-    deleteTweet (fromIntegral last) filepath+    lastTweet <- foldr ((>=>) . f) initial content $ fromMaybe 0 idNum+    deleteTweet (fromIntegral lastTweet) filepath  -- | Reply with a single tweet. Works the same as `thread` but doesn't take the fourth argument. --@@ -76,4 +87,4 @@  -- | Make a `Tweet` with only the contents. mkTweet :: String -> Tweet-mkTweet contents = over (status) (const (contents)) def +mkTweet contents = over status (pure contents) pricklyTweet
src/Web/Tweet/API.hs view
@@ -1,192 +1,447 @@ {-# LANGUAGE OverloadedStrings #-} --- | Module containing the functions directly dealing with twitter's API+-- | Module containing the functions directly dealing with twitter's API. Most functions in this module have two versions - one which takes a path to a TOML file containing api keys/secrets and tokens/secrets, the other takes api keys/secrets and tokens/secrets as an argument. module Web.Tweet.API where +import           Control.Composition+import           Control.Monad import qualified Data.ByteString.Lazy.Char8 as BSL-import Web.Tweet.Types-import Web.Tweet.Utils-import Control.Monad-import Control.Lens-import Text.Megaparsec.Error-import Web.Tweet.Utils.API-import Data.Composition+import           Data.Containers.ListUtils  (nubOrd)+import           Data.Foldable              (traverse_)+import           Data.Functor               (($>))+import           Data.Maybe                 (isJust)+import qualified Data.Set                   as S+import           Data.Void+import           Lens.Micro+import           Lens.Micro.Extras+import           Text.Megaparsec.Error+import           Web.Tweet.Types+import           Web.Tweet.Utils+import           Web.Tweet.Utils.API  -- | Get tweets (text only) for some user getMarkov :: String -> Maybe Int -> FilePath -> IO [String]-getMarkov = (fmap (map (view text))) .** getAll +getMarkov = fmap (map (view text)) .** getAll  -- | Get all tweets by some user getAll :: String -> Maybe Int -> FilePath -> IO Timeline-getAll screenName maxId filepath = do-    tweets <- either (error "Parse tweets failed") id <$> getProfileMax screenName 200 filepath maxId-    let lastId = _tweetId . last $ tweets-    if (Just lastId) == maxId then +getAll sn maxId filepath = do+    tweets <- either (error "Parse tweets failed") id <$> getProfileMax sn 200 filepath maxId+    let lastId = _tweetId <$> tweets ^? _last+    if lastId == maxId then         pure []     else         do-            putStrLn $ "fetching tweets since " ++ show lastId ++ "..."-            next <- getAll screenName (Just lastId) filepath+            if isJust lastId+                then putStrLn $ "fetching tweets since " ++ show (lastId ^?! _Just) ++ "..."+                else pure ()+            next <- getAll sn lastId filepath             pure (tweets ++ next) +-- | tweet, given a `Tweet` and a `Config` containing necessary data to sign the request.+tweetDataMem :: Tweet -> Config -> IO Int+tweetDataMem tweet config = do+    let requestString = urlString tweet+    bytes <- postRequestMem ("https://api.twitter.com/1.1/statuses/update.json" ++ requestString) config+    putStrLn $ displayTimelineColor . either (error "failed to parse tweet") id . getTweets . BSL.toStrict $ bytes+    pure . view tweetId . head . either (error "failed to parse tweet") id . getTweets . BSL.toStrict $ bytes+ -- | tweet, given a `Tweet` and path to credentials. Return id of posted tweet. tweetData :: Tweet -> FilePath -> IO Int tweetData tweet filepath = do     let requestString = urlString tweet     bytes <- postRequest ("https://api.twitter.com/1.1/statuses/update.json" ++ requestString) filepath -- FIXME fix the coloration     putStrLn $ displayTimelineColor . either (error "failed to parse tweet") id . getTweets . BSL.toStrict $ bytes-    pure . (view tweetId) . head . either (error "failed to parse tweet") id . getTweets . BSL.toStrict $ bytes+    pure . view tweetId . head . either (error "failed to parse tweet") id . getTweets . BSL.toStrict $ bytes  -- | Gets user profile with max_id set.-getProfileMax :: String -> Int -> FilePath -> Maybe Int -> IO (Either (ParseError Char Dec) Timeline)+getProfileMax :: String -> Int -> FilePath -> Maybe Int -> IO (Either (ParseErrorBundle String Void) Timeline) getProfileMax = fmap (getTweets . BSL.toStrict) .*** getProfileRaw  -- | Gets user profile with max_id set.+getProfileMaxMem :: String -> Int -> Config -> Maybe Int -> IO (Either (ParseErrorBundle String Void) Timeline)+getProfileMaxMem = fmap (getTweets . BSL.toStrict) .*** getProfileRawMem++-- | Gets user profile with max_id set. getProfileRaw :: String -> Int -> FilePath -> Maybe Int -> IO BSL.ByteString-getProfileRaw screenName count filepath maxId = getRequest ("https://api.twitter.com/1.1/statuses/user_timeline.json" ++ requestString) filepath+getProfileRaw sn count filepath maxId = getRequest ("https://api.twitter.com/1.1/statuses/user_timeline.json" ++ requestString) filepath     where requestString = case maxId of {-        (Just id) -> "?screen_name=" ++ screenName ++ "&count=" ++ (show count) ++ "&max_id=" ++ (show id) ;-        Nothing -> "?screen_name=" ++ screenName ++ "&count=" ++ (show count) }+        (Just i) -> "?screen_name=" ++ sn ++ "&count=" ++ show count ++ "&max_id=" ++ show i ;+        Nothing -> "?screen_name=" ++ sn ++ "&count=" ++ show count } +-- | Gets user profile with max_id set+getProfileRawMem :: String -> Int -> Config -> Maybe Int -> IO BSL.ByteString+getProfileRawMem sn count config maxId = getRequestMem ("https://api.twitter.com/1.1/statuses/user_timeline.json" ++ requestString) config+    where requestString = case maxId of {+        (Just i) -> "?screen_name=" ++ sn ++ "&count=" ++ show count ++ "&max_id=" ++ show i ;+        Nothing -> "?screen_name=" ++ sn ++ "&count=" ++ show count }+ -- | Get mentions and parse response as a list of tweets-mentions :: Int -> FilePath -> IO (Either (ParseError Char Dec) Timeline)+mentions :: Int -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline) mentions = fmap (getTweets . BSL.toStrict) .* mentionsRaw +-- | Get mentions and parse response as a list of tweets+mentionsMem :: Int -> Config -> IO (Either (ParseErrorBundle String Void) Timeline)+mentionsMem = fmap (getTweets . BSL.toStrict) .* mentionsRawMem++searchRaw :: String -> FilePath -> IO BSL.ByteString+searchRaw str = getRequest ("https://api.twitter.com/1.1/search/tweets.json" ++ str)++searchMentionsRaw :: Maybe Int -- ^ Max ID+                  -> String -- ^ Username+                  -> FilePath+                  -> IO BSL.ByteString+searchMentionsRaw maxid uname = searchRaw ("?q=to%3A" ++ uname ++ maxidUrl)+    where maxidUrl = case maxid of+            Just id' -> "&max_id=" ++ show id'+            Nothing  -> ""++searchRepliesRaw :: Maybe Int -- ^ Max ID+                 -> String -- ^ Username+                 -> Int -- ^ Tweet ID+                 -> FilePath+                 -> IO BSL.ByteString+searchRepliesRaw maxid uname twid = searchRaw ("?q=to%3A" ++ uname ++ "&since_id=" ++ show twid ++ maxidUrl)+    where maxidUrl = case maxid of+            Just id' -> "&max_id=" ++ show id'+            Nothing  -> ""++loopMentions :: Maybe Int -> Maybe Int -> S.Set String -> String -> FilePath -> IO ()+loopMentions pastMax maxid alreadyMuted uname fp =+    if maxid == pastMax+        then pure ()+        else do+            next <- searchMentions maxid uname fp+            case next of+                Right [] -> pure ()+                Left{} -> pure ()+                Right tws -> do+                    let newMax = minimum (_tweetId <$> tws)+                    let toMute = [ tw | tw <- nubOrd (_screenName <$> tws), tw `S.notMember` alreadyMuted ]+                    let modAlready = thread [ S.insert k | k <- toMute ]+                    putStrLn "Muted:"+                    traverse_ (\u -> mute u fp *> putStrLn u) toMute+                    loopMentions maxid (Just newMax) (modAlready alreadyMuted) uname fp++muteMentions :: String -- ^ Screen name+             -> FilePath+             -> IO ()+muteMentions = loopMentions (Just 0) Nothing mempty++loopReplies :: Maybe Int -> Maybe Int -> String -> Int -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline)+loopReplies pastMax maxid uname twid fp =+    if maxid == pastMax+        then pure (Right [])+        else do+            next <- searchReplies maxid uname twid fp+            case next of+                Right [] -> pure (Right [])+                Left x -> pure (Left x)+                Right tws -> let newMax = minimum (_tweetId <$> tws)+                        in fmap (tws ++) <$> loopReplies maxid (Just newMax) uname twid fp++searchMentions :: Maybe Int -> String -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline)+searchMentions = fmap (getTweets . BSL.toStrict) .** searchMentionsRaw++searchReplies :: Maybe Int -> String -> Int -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline)+searchReplies = fmap (getTweets . BSL.toStrict) .*** searchRepliesRaw++getReplies :: String -- ^ Screen name+           -> Int -- ^ Tweet+           -> FilePath+           -> IO (Either (ParseErrorBundle String Void) Timeline)+getReplies str twid = fmap (fmap (filter p)) . loopReplies (Just 0) Nothing str twid+    where p entity = _replyTo entity == Just twid++muteRepliers :: String+             -> Int+             -> FilePath+             -> IO [String]+muteRepliers str twid fp = do+    us <- getReplies str twid fp+    case us of+        Left{} -> pure []+        -- TODO: add a delay so I don't get rate limited?+        Right xs -> let toMute = nubOrd (_screenName <$> xs) in traverse (\u -> mute u fp $> u) toMute+ -- | Gets mentions mentionsRaw :: Int -> FilePath -> IO BSL.ByteString mentionsRaw count = getRequest ("https://api.twitter.com/1.1/statuses/mentions_timeline.json" ++ requestString)-    where requestString = "?count=" ++ (show count)+    where requestString = "?count=" ++ show count +-- | Gets mentions+mentionsRawMem :: Int -> Config -> IO BSL.ByteString+mentionsRawMem count = getRequestMem ("https://api.twitter.com/1.1/statuses/mentions_timeline.json" ++ requestString)+    where requestString = "?count=" ++ show count+ -- | Get user profile given screen name and how many tweets to return-getProfile :: String -> Int -> FilePath -> IO (Either (ParseError Char Dec) Timeline)-getProfile screenName count filepath = getProfileMax screenName count filepath Nothing+getProfile :: String -> Int -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline)+getProfile sn count filepath = getProfileMax sn count filepath Nothing --- | Show a user profile given screen name, how many tweets to return, --- and whether to print them in color.-showProfile :: String -> Int -> Bool -> FilePath -> IO String-showProfile screenName count color = fmap (showTweets color) . getProfile screenName count+-- | Mute a user given their screen name+mute :: String -> FilePath -> IO ()+mute = fmap void . muteUserRaw --- | Show the most successful tweets by a given user, given their screen name. -showBest :: String -> Int -> Bool -> FilePath -> IO String-showBest screenName n color = fmap (showTweets color . pure . (take n . hits)) . getAll screenName Nothing+-- | Mute a user given their screen name+muteMem :: String -> Config -> IO ()+muteMem = fmap void . muteUserRawMem --- | Display user timeline-showTimeline :: Int -> Bool -> FilePath -> IO String-showTimeline count color = (fmap (showTweets color)) . getTimeline count +-- | Unmute a user given their screen name+unmute :: String -> FilePath -> IO ()+unmute = fmap void . unmuteUserRaw --- | Display user timeline in color, as appropriate-showTweets :: Bool -> Either (ParseError Char Dec) Timeline -> String-showTweets color = (either show id) . (fmap (if color then displayTimelineColor else displayTimeline))+-- | Unmute a user given their screen name+unmuteMem :: String -> Config -> IO ()+unmuteMem = fmap void . unmuteUserRawMem +-- | Mute a user given their screen name+muteUserRaw :: String -> FilePath -> IO BSL.ByteString+muteUserRaw sn = postRequest ("https://api.twitter.com/1.1/mutes/users/create.json?screen_name=" ++ sn)++-- | Mute a user given their screen name+muteUserRawMem :: String -> Config -> IO BSL.ByteString+muteUserRawMem sn = postRequestMem ("https://api.twitter.com/1.1/mutes/users/create.json?screen_name=" ++ sn)++-- | Unmute a user given their screen name+unmuteUserRaw :: String -> FilePath -> IO BSL.ByteString+unmuteUserRaw sn = postRequest ("https://api.twitter.com/1.1/mutes/users/destroy.json?screen_name=" ++ sn)++-- | Unmute a user given their screen name+unmuteUserRawMem :: String -> Config -> IO BSL.ByteString+unmuteUserRawMem sn = postRequestMem ("https://api.twitter.com/1.1/mutes/users/destroy.json?screen_name=" ++ sn)+ -- | Get user's DMs.+getDMsRaw :: Show p => p -> FilePath -> IO BSL.ByteString getDMsRaw count = getRequest ("https://api.twitter.com/1.1/direct_messages.json" ++ requestString)-    where requestString = "?count=" ++ (show count)+    where requestString = "?count=" ++ show count +-- | Get a user's favorites+getFavorites :: Int -> String -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline)+getFavorites count = fmap (fmap (take count) . getTweets . BSL.toStrict) .* favoriteTweetListRaw+ -- | Get a timeline-getTimeline :: Int -> FilePath -> IO (Either (ParseError Char Dec) Timeline)-getTimeline = (fmap (getTweets . BSL.toStrict)) .* getTimelineRaw+getTimeline :: Int -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline)+getTimeline = fmap (getTweets . BSL.toStrict) .* getTimelineRaw +-- | Get a timeline+getTimelineMem :: Int -> Config -> IO (Either (ParseErrorBundle String Void) Timeline)+getTimelineMem = fmap (getTweets . BSL.toStrict) .* getTimelineRawMem+ -- | Get a user's timeline and return response as a bytestring getTimelineRaw :: Int -> FilePath -> IO BSL.ByteString getTimelineRaw count = getRequest ("https://api.twitter.com/1.1/statuses/home_timeline.json" ++ requestString)-    where requestString = "?count=" ++ (show count)+    where requestString = "?count=" ++ show count +-- | Get a user's timeline and return response as a bytestring+getTimelineRawMem :: Int -> Config -> IO BSL.ByteString+getTimelineRawMem count = getRequestMem ("https://api.twitter.com/1.1/statuses/home_timeline.json" ++ requestString)+    where requestString = "?count=" ++ show count+ -- | Delete a tweet given its id deleteTweet :: Integer -> FilePath -> IO ()-deleteTweet = (fmap void) . deleteTweetRaw+deleteTweet = fmap void . deleteTweetRaw +-- | Delete a tweet given its id+deleteTweetMem :: Integer -> Config -> IO ()+deleteTweetMem = fmap void . deleteTweetRawMem+ -- | Get response, i.e. the tweet deleted-deleteTweetResponse :: Integer -> FilePath -> IO (Either (ParseError Char Dec) Timeline)+deleteTweetResponse :: Integer -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline) deleteTweetResponse = fmap (getTweets . BSL.toStrict) .* deleteTweetRaw +-- | Get response, i.e. the tweet deleted+deleteTweetResponseMem :: Integer -> Config -> IO (Either (ParseErrorBundle String Void) Timeline)+deleteTweetResponseMem = fmap (getTweets . BSL.toStrict) .* deleteTweetRawMem+ -- | Favorite a tweet given its id favoriteTweet :: Integer -> FilePath -> IO ()-favoriteTweet = (fmap void) . favoriteTweetRaw+favoriteTweet = fmap void . favoriteTweetRaw +-- | Favorite a tweet given its id+favoriteTweetMem :: Integer -> Config -> IO ()+favoriteTweetMem = fmap void . favoriteTweetRawMem+ -- | Favorite a tweet and returned the (parsed) response-favoriteTweetResponse :: Integer -> FilePath -> IO (Either (ParseError Char Dec) Timeline)+favoriteTweetList :: String -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline)+favoriteTweetList = fmap (getTweets . BSL.toStrict) .* favoriteTweetListRaw++-- | Favorite a tweet and returned the (parsed) response+favoriteTweetListMem :: String -> Config -> IO (Either (ParseErrorBundle String Void) Timeline)+favoriteTweetListMem = fmap (getTweets . BSL.toStrict) .* favoriteTweetListRawMem++-- | Favorite a tweet and returned the (parsed) response+favoriteTweetResponse :: Integer -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline) favoriteTweetResponse = fmap (getTweets . BSL.toStrict) .* favoriteTweetRaw  -- | Unfavorite a tweet given its id unfavoriteTweet :: Integer -> FilePath -> IO ()-unfavoriteTweet = (fmap void) . unfavoriteTweetRaw+unfavoriteTweet = fmap void . unfavoriteTweetRaw +-- | Unfavorite a tweet given its id+unfavoriteTweetMem :: Integer -> Config -> IO ()+unfavoriteTweetMem = fmap void . unfavoriteTweetRawMem+ -- | Unfavorite a tweet and returned the (parsed) response-unfavoriteTweetResponse :: Integer -> FilePath -> IO (Either (ParseError Char Dec) Timeline)+unfavoriteTweetResponse :: Integer -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline) unfavoriteTweetResponse = fmap (getTweets . BSL.toStrict) .* unfavoriteTweetRaw +-- | Unfavorite a tweet and returned the (parsed) response+unfavoriteTweetResponseMem :: Integer -> Config -> IO (Either (ParseErrorBundle String Void) Timeline)+unfavoriteTweetResponseMem = fmap (getTweets . BSL.toStrict) .* unfavoriteTweetRawMem+ -- | Unretweet a tweet given its id unretweetTweet :: Integer -> FilePath -> IO ()-unretweetTweet = (fmap void) . unretweetTweetRaw+unretweetTweet = fmap void . unretweetTweetRaw +-- | Unretweet a tweet given its id+unretweetTweetMem :: Integer -> Config -> IO ()+unretweetTweetMem = fmap void . unretweetTweetRawMem+ -- | Unretweet a tweet and returned the (parsed) response-unretweetTweetResponse :: Integer -> FilePath -> IO (Either (ParseError Char Dec) Timeline)+unretweetTweetResponse :: Integer -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline) unretweetTweetResponse = fmap (getTweets . BSL.toStrict) .* unretweetTweetRaw +-- | Unretweet a tweet and returned the (parsed) response+unretweetTweetResponseMem :: Integer -> Config -> IO (Either (ParseErrorBundle String Void) Timeline)+unretweetTweetResponseMem = fmap (getTweets . BSL.toStrict) .* unretweetTweetRawMem+ -- | Unfollow a user given their screen name unfollow :: String -> FilePath -> IO ()-unfollow = (fmap void) . unfollowUserRaw+unfollow = fmap void . unfollowUserRaw +-- | Unfollow a user given their screen name+unfollowMem :: String -> Config -> IO ()+unfollowMem = fmap void . unfollowUserRawMem+ -- | Follow a user given their screen name follow :: String -> FilePath -> IO ()-follow = (fmap void) . followUserRaw+follow = fmap void . followUserRaw +-- | Follow a user given their screen name+followMem :: String -> Config -> IO ()+followMem = fmap void . followUserRawMem+ -- | Block a user given their screen name block :: String -> FilePath -> IO ()-block = (fmap void) . blockUserRaw+block = fmap void . blockUserRaw +-- | Block a user given their screen name+blockMem :: String -> Config -> IO ()+blockMem = fmap void . blockUserRawMem+ -- | Unblock a user given their screen name unblock :: String -> FilePath -> IO ()-unblock = (fmap void) . unblockUserRaw+unblock = fmap void . unblockUserRaw +-- | Unblock a user given their screen name+unblockMem :: String -> Config -> IO ()+unblockMem = fmap void . unblockUserRawMem+ -- | Retweet a tweet given its id retweetTweet :: Integer -> FilePath -> IO ()-retweetTweet = (fmap void) . retweetTweetRaw+retweetTweet = fmap void . retweetTweetRaw +-- | Retweet a tweet given its id+retweetTweetMem :: Integer -> Config -> IO ()+retweetTweetMem = fmap void . retweetTweetRawMem+ -- | Retweet a tweet and returned the (parsed) response-retweetTweetResponse :: Integer -> FilePath -> IO (Either (ParseError Char Dec) Timeline)+retweetTweetResponse :: Integer -> FilePath -> IO (Either (ParseErrorBundle String Void) Timeline) retweetTweetResponse = fmap (getTweets . BSL.toStrict) .* retweetTweetRaw +-- | Retweet a tweet and returned the (parsed) response+retweetTweetResponseMem :: Integer -> Config -> IO (Either (ParseErrorBundle String Void) Timeline)+retweetTweetResponseMem = fmap (getTweets . BSL.toStrict) .* retweetTweetRawMem++-- | Get a lisr of favorited tweets by screen name; return bytestring response+favoriteTweetListRaw :: String -> FilePath -> IO BSL.ByteString+favoriteTweetListRaw sn = getRequest ("https://api.twitter.com/1.1/favorites/list.json?screen_name=" ++ sn)++-- | Get a lisr of favorited tweets by screen name; return bytestring response+favoriteTweetListRawMem :: String -> Config -> IO BSL.ByteString+favoriteTweetListRawMem sn = getRequestMem ("https://api.twitter.com/1.1/favorites/list.json?screen_name=" ++ sn)+ -- | Favorite a tweet given its id; return bytestring response favoriteTweetRaw :: Integer -> FilePath -> IO BSL.ByteString-favoriteTweetRaw id = postRequest ("https://api.twitter.com/1.1/favorites/create.json?id=" ++ (show id))+favoriteTweetRaw idNum = postRequest ("https://api.twitter.com/1.1/favorites/create.json?id=" ++ show idNum) --- | Retweet a tweet given its id; return bytestring response+-- | Favorite a tweet given its idNum; return bytestring response+favoriteTweetRawMem :: Integer -> Config -> IO BSL.ByteString+favoriteTweetRawMem idNum = postRequestMem ("https://api.twitter.com/1.1/favorites/create.json?id=" ++ show idNum)++-- | Retweet a tweet given its idNum; return bytestring response retweetTweetRaw :: Integer -> FilePath -> IO BSL.ByteString-retweetTweetRaw id = postRequest ("https://api.twitter.com/1.1/statuses/retweet/" ++ (show id) ++ ".json")+retweetTweetRaw idNum = postRequest ("https://api.twitter.com/1.1/statuses/retweet/" ++ show idNum ++ ".json") +-- | Retweet a tweet given its idNum; return bytestring response+retweetTweetRawMem :: Integer -> Config -> IO BSL.ByteString+retweetTweetRawMem idNum = postRequestMem ("https://api.twitter.com/1.1/statuses/retweet/" ++ show idNum ++ ".json")+ -- | Send a DM given text, screen name of recipient.-sendDMRaw txt screenName = postRequest ("https://api.twitter.com/1.1/direct_messages/new.json?text=" ++ encoded ++ "&screen_name" ++ screenName ++ ".json")+sendDMRaw :: String -> String -> FilePath -> IO BSL.ByteString+sendDMRaw txt sn = postRequest ("https://api.twitter.com/1.1/direct_messages/new.json?text=" ++ encoded ++ "&screen_name" ++ sn ++ ".json")     where encoded = strEncode txt  -- | Get DMs, return bytestring of response getDMs :: Int -> FilePath -> IO BSL.ByteString-getDMs count = getRequest ("https://dev.twitter.com/rest/reference/get/direct_messages.json?count=" ++ (show count))+getDMs count = getRequest ("https://dev.twitter.com/rest/reference/get/direct_messages.json?count=" ++ show count) +-- | Get DMs, return bytestring of response+getDMMem :: Int -> Config -> IO BSL.ByteString+getDMMem count = getRequestMem ("https://dev.twitter.com/rest/reference/get/direct_messages.json?count=" ++ show count)+ -- | Follow a user given their screen name followUserRaw :: String -> FilePath -> IO BSL.ByteString-followUserRaw screenName = postRequest ("https://api.twitter.com/1.1/friendships/create.json?screen_name=" ++ screenName)+followUserRaw sn = postRequest ("https://api.twitter.com/1.1/friendships/create.json?screen_name=" ++ sn) +-- | Follow a user given their screen name+followUserRawMem :: String -> Config -> IO BSL.ByteString+followUserRawMem sn = postRequestMem ("https://api.twitter.com/1.1/friendships/create.json?screen_name=" ++ sn)+ -- | Block a user given their screen name blockUserRaw :: String -> FilePath -> IO BSL.ByteString-blockUserRaw screenName = postRequest ("https://api.twitter.com/1.1/blocks/create.json?screen_name=" ++ screenName)+blockUserRaw sn = postRequest ("https://api.twitter.com/1.1/blocks/create.json?screen_name=" ++ sn) +-- | Block a user given their screen name+blockUserRawMem :: String -> Config -> IO BSL.ByteString+blockUserRawMem sn = postRequestMem ("https://api.twitter.com/1.1/blocks/create.json?screen_name=" ++ sn)+ -- | Unblock a user given their screen name unblockUserRaw :: String -> FilePath -> IO BSL.ByteString-unblockUserRaw screenName = postRequest ("https://api.twitter.com/1.1/blocks/destroy.json?screen_name=" ++ screenName)+unblockUserRaw sn = postRequest ("https://api.twitter.com/1.1/blocks/destroy.json?screen_name=" ++ sn) +-- | Unblock a user given their screen name+unblockUserRawMem :: String -> Config -> IO BSL.ByteString+unblockUserRawMem sn = postRequestMem ("https://api.twitter.com/1.1/blocks/destroy.json?screen_name=" ++ sn)+ -- | Follow a user given their screen name unfollowUserRaw :: String -> FilePath -> IO BSL.ByteString-unfollowUserRaw screenName = postRequest ("https://api.twitter.com/1.1/friendships/destroy.json?screen_name=" ++ screenName)+unfollowUserRaw sn = postRequest ("https://api.twitter.com/1.1/friendships/destroy.json?screen_name=" ++ sn) +-- | Follow a user given their screen name+unfollowUserRawMem :: String -> Config -> IO BSL.ByteString+unfollowUserRawMem sn = postRequestMem ("https://api.twitter.com/1.1/friendships/destroy.json?screen_name=" ++ sn)+ -- | Unretweet a tweet given its id; return bytestring response unretweetTweetRaw :: Integer -> FilePath -> IO BSL.ByteString-unretweetTweetRaw id = postRequest ("https://api.twitter.com/1.1/statuses/unretweet/" ++ (show id) ++ ".json")+unretweetTweetRaw idNum = postRequest ("https://api.twitter.com/1.1/statuses/unretweet/" ++ show idNum ++ ".json") --- | Favorite a tweet given its id; return bytestring response+-- | Unretweet a tweet given its idNum; return bytestring response+unretweetTweetRawMem :: Integer -> Config -> IO BSL.ByteString+unretweetTweetRawMem idNum = postRequestMem ("https://api.twitter.com/1.1/statuses/unretweet/" ++ show idNum ++ ".json")++-- | Unfavorite a tweet given its idNum; return bytestring response unfavoriteTweetRaw :: Integer -> FilePath -> IO BSL.ByteString-unfavoriteTweetRaw id = postRequest ("https://api.twitter.com/1.1/favorites/destroy.json?id=" ++ (show id))+unfavoriteTweetRaw idNum = postRequest ("https://api.twitter.com/1.1/favorites/destroy.json?id=" ++ show idNum) --- | Delete a tweet given its id; return bytestring response+-- | Unfavorite a tweet given its idNum; return bytestring response+unfavoriteTweetRawMem :: Integer -> Config -> IO BSL.ByteString+unfavoriteTweetRawMem idNum = postRequestMem ("https://api.twitter.com/1.1/favorites/destroy.json?id=" ++ show idNum)++-- | Delete a tweet given its idNum; return bytestring response deleteTweetRaw :: Integer -> FilePath -> IO BSL.ByteString-deleteTweetRaw id = postRequest ("https://api.twitter.com/1.1/statuses/destroy/" ++ (show id) ++ ".json")+deleteTweetRaw idNum = postRequest ("https://api.twitter.com/1.1/statuses/destroy/" ++ show idNum ++ ".json")++-- | Delete a tweet given its idNum; return bytestring response+deleteTweetRawMem :: Integer -> Config -> IO BSL.ByteString+deleteTweetRawMem idNum = postRequestMem ("https://api.twitter.com/1.1/statuses/destroy/" ++ show idNum ++ ".json")
+ src/Web/Tweet/API/Internal.hs view
@@ -0,0 +1,41 @@+-- | Helper functions for the command line tool.+module Web.Tweet.API.Internal where++import           Data.Void+import           Text.Megaparsec.Error+import           Web.Tweet.API+import           Web.Tweet.Types+import           Web.Tweet.Utils++type Filter = Timeline -> Timeline++-- | Show a user profile given screen name, how many tweets to return,+-- and whether to print them in color.+showProfile :: String -> Int -> Bool -> FilePath -> IO String+showProfile sn count color = fmap (showTweets color) . getProfile sn count++-- | Show the most successful tweets by a given user, given their screen name.+showBest :: String -> Int -> Bool -> FilePath -> IO String+showBest sn n color = fmap (showTweets color . pure . take n . hits) . getAll sn Nothing++-- | Show the most successful tweets by a given user, given their screen name. Additionally filter out replies.+showBest' :: String -> Int -> Bool -> FilePath -> IO String+showBest' sn n color = fmap (showTweets color . pure . take n . hits') . getAll sn Nothing++showReplies :: String -> Int -> Bool -> FilePath -> IO String+showReplies uname twid color = fmap (showTweets color) . getReplies uname twid++-- | Display user timeline+showTimeline :: Int -> Bool -> FilePath -> IO String+showTimeline count color = fmap (showTweets color) . getTimeline count++showFilteredTL :: [Filter] -> String -> Int -> Bool -> FilePath -> IO String+showFilteredTL filters sn count color = fmap (showTweets color . fmap (foldr (.) id filters)) . getProfile sn count++-- | Display user timeline in color, as appropriate+showTweets :: Bool -> Either (ParseErrorBundle String Void) Timeline -> String+showTweets color = either show id . fmap (if color then displayTimelineColor else displayTimeline)++-- | Display a user's favorites+showFavorites :: Int -> String -> Bool -> FilePath -> IO String+showFavorites count sn color = fmap (showTweets color) . getFavorites count sn
− src/Web/Tweet/Exec.hs
@@ -1,276 +0,0 @@--- | Provides IO action that parses command line options and tweetInputs from stdin-module Web.Tweet.Exec ( exec-                      , Program (..)-                      , Command (..)) where--import Web.Tweet-import Options.Applicative-import qualified Data.ByteString.Lazy.Char8 as BSL-import Data.Monoid hiding (getAll)-import System.Directory-import Data.Maybe-import Paths_tweet_hs-import Data.Version---- | Data type for our program: one optional path to a credential file, (optionally) the number of tweetInputs to make, the id of the status you're replying to, and a list of users you wish to mention.-data Program = Program { subcommand :: Command , cred :: Maybe FilePath , color :: Bool }---- | Data type for a command--- TODO add boolean option to show ids alongside tweets-data Command = Timeline { count :: Maybe Int }-    | SendInput { tweetInputs :: Maybe Int, replyId :: Maybe String, replyHandles :: Maybe [String] }-    | Profile { count :: Maybe Int , screenNameMaybe :: Maybe String }-    | Mentions { count :: Maybe Int }-    | Markov { screenName :: String }-    | Send { tweets :: Maybe Int , replyId :: Maybe String , replyHandles :: Maybe [String] , userInput :: String }-    | Sort { screenName :: String , count :: Maybe Int }-    | Delete { twId :: Integer }-    | Fav { twId :: Integer }-    | Unfav { twId :: Integer }-    | Retweet { twId :: Integer }-    | Unretweet { twId :: Integer }-    | Follow { screenName :: String }-    | Unfollow { screenName :: String }-    | Block { screenName :: String }-    | Unblock { screenName :: String }-    | Dump { screenName :: String }---- | query twitter to post stdin with no fancy options-fromStdIn :: Int -> FilePath -> IO ()-fromStdIn = threadStdIn [] Nothing---- | Tweet string given to us, as parsed from the command line-fromCLI :: String -> Int -> FilePath -> IO ()-fromCLI str = thread str [] Nothing---- | Threaded tweetInputs from stdIn-threadStdIn :: [String] -> Maybe Int -> Int -> FilePath -> IO ()-threadStdIn hs idNum num filepath = do-    contents <- getContents-    thread contents hs idNum num filepath---- | Executes parser-exec :: IO ()-exec = execParser opts >>= select-    where-        versionInfo = infoOption ("tweet-hs version: " ++ showVersion version) (short 'v' <> long "version" <> help "Show version")-        opts        = info (helper <*> versionInfo <*> program)-            (fullDesc-            <> progDesc "Tweet and view tweets"-            <> header "clit - a Command Line Interface Tweeter")---- | Executes program given parsed `Program`-select :: Program -> IO ()-select (Program com maybeFile color) = case maybeFile of-    (Just file) -> selectCommand com (not color) file-    _ -> selectCommand com (not color) =<< (++ "/.cred") <$> getHomeDirectory---- | Executes subcommand given subcommand + filepath to configuration file-selectCommand :: Command -> Bool -> FilePath -> IO ()-selectCommand (Send maybeNum Nothing Nothing input) _ file = fromCLI input (fromMaybe 15 maybeNum) file-selectCommand (Send maybeNum (Just replyId) Nothing input) _ file = thread input [] (pure . read $ replyId) (fromMaybe 15 maybeNum) file-selectCommand (Send maybeNum Nothing (Just handles) input) _ file = thread input handles Nothing (fromMaybe 15 maybeNum) file-selectCommand (SendInput maybeNum Nothing Nothing) _ file  = fromStdIn (fromMaybe 15 maybeNum) file-selectCommand (SendInput maybeNum (Just replyId) (Just handles)) _ file = threadStdIn handles (pure . read $ replyId) (fromMaybe 15 maybeNum) file-selectCommand (SendInput maybeNum (Just replyId) Nothing) _ file = threadStdIn [] (pure . read $ replyId) (fromMaybe 15 maybeNum) file-selectCommand (SendInput maybeNum Nothing (Just handles)) _ file = threadStdIn handles Nothing (fromMaybe 15 maybeNum) file-selectCommand (Timeline maybeNum) color file = putStrLn =<< showTimeline (fromMaybe 11 maybeNum) color file-selectCommand (Mentions maybeNum) color file = putStrLn =<< showTweets color <$> mentions (fromMaybe 11 maybeNum) file-selectCommand (Profile maybeNum name) color file = putStrLn =<< showProfile (fromMaybe mempty name) (fromMaybe 11 maybeNum) color file-selectCommand (Sort name maybeNum) color file = putStrLn =<< showBest name (fromMaybe 11 maybeNum) color file-selectCommand (Markov name) _ file = do-    raw <- getMarkov name Nothing file-    appendFile (name ++ ".txt") (unlines raw)-    putStrLn $ "Written output to: " ++ name ++ ".txt"-selectCommand (Delete n) color file = do-    putStrLn "Deleted:\n"-    putStrLn =<< showTweets color <$> deleteTweetResponse n file-selectCommand (Fav n) color file = do-    putStrLn "Favorited:\n"-    putStrLn =<< showTweets color <$> favoriteTweetResponse n file-selectCommand (Unfav n) color file = do-    putStrLn "Unfavorited:\n"-    putStrLn =<< showTweets color <$> unfavoriteTweetResponse n file-selectCommand (Retweet n) color file = do-    putStrLn "Retweeted:\n"-    putStrLn =<< showTweets color <$> retweetTweetResponse n file-selectCommand (Unretweet n) color file = do-    putStrLn "Unretweeted:\n"-    putStrLn =<< showTweets color <$> unretweetTweetResponse n file-selectCommand (Follow screenName) _ file = do-    follow screenName file-    putStrLn ("..." ++ screenName ++ " followed successfully!")-selectCommand (Unfollow screenName) _ file = do-    unfollow screenName file-    putStrLn ("..." ++ screenName ++ " unfollowed successfully!")-selectCommand (Block screenName) color file = do-    block screenName file-    putStrLn ("..." ++ screenName ++ " blocked successfully")-selectCommand (Unblock screenName) color file = do-    unblock screenName file-    putStrLn ("..." ++ screenName ++ " unblocked successfully")-selectCommand (Dump screenName) color file = BSL.putStrLn =<< (getProfileRaw screenName 3200 file Nothing)---- | Parser to return a program datatype-program :: Parser Program-program = Program-    <$> (hsubparser-        (command "send" (info tweet (progDesc "Send a tweet from the command-line"))-        <> command "input" (info tweetInput (progDesc "Send a tweet from stdIn"))-        <> command "view" (info timeline (progDesc "Get your timeline"))-        <> command "user" (info profile (progDesc "Get a user's profile"))-        <> command "markov" (info markov (progDesc "Grab tweets en masse."))-        <> command "hits" (info best (progDesc "View a user's top tweets."))-        <> command "del" (info delete (progDesc "Delete a tweet.")) -- TODO delete/favorite in bunches!-        <> command "fav" (info fav (progDesc "Favorite a tweet"))-        <> command "ufav" (info unfav (progDesc "Unfavorite a tweet"))-        <> command "urt" (info unrt (progDesc "Un-retweet a tweet"))-        <> command "rt" (info rt (progDesc "Retweet a tweet"))-        <> command "follow" (info fol (progDesc "Follow a user"))-        <> command "unfollow" (info unfol (progDesc "Unfollow a user"))-        <> command "dump" (info dump (progDesc "Dump tweets (for debugging)"))-        <> command "block" (info blockParser (progDesc "Block a user"))-        <> command "unblock" (info unblockParser (progDesc "Unblock a user"))-        <> command "mentions" (info mentionsParser (progDesc "Fetch mentions"))))-    <*> (optional $ strOption-        (long "cred"-        <> short 'c'-        <> metavar "CREDENTIALS"-        <> help "path to credentials"))-    <*> switch-        (long "color"-        <> short 'l'-        <> help "Turn off colorized terminal output.")---- | Parser for the view subcommand-timeline :: Parser Command-timeline = Timeline-    <$> (optional $ read <$> strOption-        (long "count"-        <> short 'n'-        <> metavar "NUM"-        <> help "number of tweetInputs to fetch, default 5"))---- | Parser for the markov subcommand-markov :: Parser Command-markov = Markov <$> user---- | Parser for the follow subcommand-fol :: Parser Command-fol = Follow <$> user---- | Parser for the block subcommand-blockParser :: Parser Command-blockParser = Block <$> user---- | Parser for the unblock subcommand-unblockParser :: Parser Command-unblockParser = Unblock <$> user---- | Parser for the dump subcommand-dump :: Parser Command-dump = Dump <$> user---- | Parser for the unfollow subcommand-unfol :: Parser Command-unfol = Unfollow <$> user---- | Parse a user screen name-user :: Parser String-user = argument str-    (metavar "SCREEN_NAME"-    <> help "Screen name of user.")---- | Parser for the del subcommand-delete :: Parser Command-delete = Delete <$> getInt---- | Parser for the fav subcommand-fav :: Parser Command-fav = Fav <$> getInt---- | Parser for the fav subcommand-unfav :: Parser Command-unfav = Unfav <$> getInt---- | Parser for the fav subcommand-unrt :: Parser Command-unrt = Unretweet <$> getInt---- | Parser for the fav subcommand-rt :: Parser Command-rt = Retweet <$> getInt---- | Parser for the del subcommand-getInt :: Parser Integer-getInt = read <$> (argument str-    (metavar "TWEET_ID"-    <> help "ID of tweet"))---- | Parser for the user subcommand-profile :: Parser Command-profile = Profile-    <$> (optional $ read <$> strOption-        (long "count"-        <> short 'n'-        <> metavar "NUM"-        <> help "Number of tweetInputs to fetch, default 12"))-    <*> optional user ---- | Parser for the mention subcommand-mentionsParser :: Parser Command-mentionsParser = Mentions-    <$> (optional $ read <$> strOption-        (long "count"-        <> short 'n'-        <> metavar "NUM"-        <> help "Number of tweetInputs to fetch, default 12"))---- | Parse best tweets-best :: Parser Command-best = Sort-    <$> argument str-        (metavar "SCREEN_NAME"-        <> help "Screen name of user you want to view.")-    <*> (optional $ read <$> strOption-        (long "count"-        <> short 'n'-        <> metavar "NUM"-        <> help "Number of tweetInputs to fetch, default 12"))---- | Parser for the send subcommand-tweet :: Parser Command-tweet = Send-    <$> (optional $ read <$> strOption-        (long "tweets"-        <> short 't'-        <> metavar "NUM"-        <> help "Number of tweetInputs in a row, default 15"))-    <*> (optional $ strOption-        (long "reply"-        <> short 'r'-        <> help "id of status to reply to - be sure to include their handle, e.g. @my_build_errors"))-    <*> (optional (some $ strOption-        (long "handle"-        <> short 'h'-        <> metavar "HANDLE1"-        <> help "Handles to include in replies")))-    <*> (unwords <$> (some $ argument str-        (metavar "TEXT"-        <> help "text of tweet to be sent")))---- | Parser for the input command-tweetInput :: Parser Command-tweetInput = SendInput-    <$> (optional $ read <$> strOption-        (long "tweets"-        <> short 't'-        <> metavar "NUM"-        <> help "Number of tweets in a row, default 15"))-    <*> (optional $ strOption-        (long "reply"-        <> short 'r'-        <> help "id of status to reply to - be sure to include their handle, e.g. @my_build_errors"))-    <*> (optional (some $ argument str-        (metavar "HANDLE1"-        <> help "handles to include in replies")))
src/Web/Tweet/Parser.hs view
@@ -1,42 +1,61 @@+{-# OPTIONS_GHC -fno-warn-unused-do-bind #-}+{-# LANGUAGE LambdaCase        #-} {-# LANGUAGE OverloadedStrings #-}--- FIXME make this module available under cabal file+ -- | Module containing parsers for tweet and response data. module Web.Tweet.Parser ( parseTweet                         , getData ) where -import qualified Data.ByteString as BS-import Text.Megaparsec.ByteString-import Text.Megaparsec.Lexer as L-import Text.Megaparsec-import Web.Tweet.Types-import Data.Monoid-import qualified Data.Map as M-import Data.Maybe-import Control.Monad+import           Control.Composition        ((.*))+import           Control.Monad+import qualified Data.ByteString            as BS+import           Data.List                  (isInfixOf)+import qualified Data.Map                   as M+import           Data.Maybe+import qualified Data.Text                  as T+import qualified Data.Text.Encoding         as TE+import           Data.Void+import           Text.Megaparsec+import           Text.Megaparsec.Byte+import           Text.Megaparsec.Byte.Lexer as L+import           Web.Tweet.Types +type Parser = Parsec Void String+ -- | Parse some number of tweets parseTweet :: Parser Timeline-parseTweet = many (try getData <|> (const (TweetEntity "" "" "" 0 Nothing 0 0) <$> eof))+parseTweet = many (try getData <|> (TweetEntity "" Nothing "" "" 0 mempty Nothing 0 0 <$ eof)) --- | Parse a single tweet's: name, text, fave count, retweet count+inReplyTo :: String -> Maybe Int+inReplyTo str =+    if "null" `isInfixOf` str+        then Nothing+        else pure (read str)++-- | Parse a single tweet's: n, text, fave count, retweet count getData :: Parser TweetEntity getData = do-    id <- read <$> filterStr "id" -    text <- filterStr "text"+    idNum <- read <$> filterStr "id"+    t <- filterStr "text"     skipMentions-    name <- filterStr "name"-    screenName' <- filterStr "screen_name"+    irt <- inReplyTo <$> filterStr "in_reply_to_status_id"+    n <- filterStr "name"+    screenn' <- filterStr "screen_name"+    --withheldCountries <- (catMaybes . sequence) <$> optional filterList+    let withheldCountries = mempty+    --let toBlock = "DE" `elem` (catMaybes (sequence bannedList))     isQuote <- filterStr "is_quote_status"     case isQuote of         "false" -> do             rts <- read <$> filterStr "retweet_count"             faves <- read <$> filterStr "favorite_count"-            pure (TweetEntity text name screenName' id Nothing rts faves)+            pure (TweetEntity t irt n screenn' idNum withheldCountries Nothing rts faves)         "true" -> do-            quoted <- parseQuoted+            q <- parseQuoted             rts <- read <$> filterStr "retweet_count"             faves <- read <$> filterStr "favorite_count"-            pure $ TweetEntity text name screenName' id quoted rts faves+            pure $ TweetEntity t irt n screenn' idNum withheldCountries q rts faves+        _ -> error "is_quote_status must have a value of \"true\" or \"false\""  -- | Parse a the quoted tweet parseQuoted :: Parser (Maybe TweetEntity)@@ -44,73 +63,78 @@     optional (string ",\"quoted_status_id" >> filterStr "quoted_status_id_str") -- FIXME it's skipping too many? prob is when two are deleted in a row twitter just dives in to RTs     contents <- optional $ string "\",\"quoted_status"     case contents of-        (Just contents) -> pure <$> getData-        _ -> pure Nothing-    +        (Just _) -> pure <$> getData+        _        -> pure Nothing  -- | Skip a set of square brackets [] skipInsideBrackets :: Parser ()-skipInsideBrackets = void (between (char '[') (char ']') $ many (skipInsideBrackets <|> void (noneOf ("[]" :: String))))+skipInsideBrackets = void (between (single '[') (single ']') $ many (skipInsideBrackets <|> void (noneOf ("[]" :: String)))) --- | Skip user mentions field to avoid parsing the wrong name+-- | Skip user mentions field to avoid parsing the wrong n skipMentions :: Parser () skipMentions = do-    many $ try $ anyChar >> notFollowedBy (string "\"user_mentions\":")-    char ','-    string "\"user_mentions\":"+    many $ try $ anySingle >> notFollowedBy (string "\"user_mentions\":")+    string ",\"user_mentions\":"     skipInsideBrackets-    pure ()  -- | Throw out input until we get to a relevant tag. filterStr :: String -> Parser String filterStr str = do-    many $ try $ anyChar >> notFollowedBy (string ("\"" <> str <> "\":"))-    char ','+    many $ try $ anySingle >> notFollowedBy (string ("\"" <> str <> "\":"))+    single ','     filterTag str  -- | Parse a field given its tag filterTag :: String -> Parser String filterTag str = do     string $ "\"" <> str <> "\":"-    open <- optional $ char '\"'+    open <- optional $ single '\"'     let forbidden = if isJust open then ("\\\"" :: String) else ("\\\"," :: String)-    want <- many $ parseHTMLChar <|> noneOf forbidden <|> specialChar '\"' <|> specialChar '/' <|> newlineChar <|> unicodeChar -- TODO modify parsec to make this parallel?-    pure want+    many $ parseHTMLChar <|> noneOf forbidden <|> specialChar '\"' <|> specialChar '/' <|> newlineChar <|> emojiChar <|> unicodeChar -- TODO modify parsec to make this parallel?  -- | Parse a newline newlineChar :: Parser Char-newlineChar = do-    string "\\n"-    pure '\n'+newlineChar = string "\\n" >> pure '\n'  -- | Parser for unicode; twitter will give us something like "/u320a" unicodeChar :: Parser Char-unicodeChar = do-    string "\\u"-    num <- fromHex . filterEmoji . BS.pack . map (fromIntegral . fromEnum) <$> count 4 anyChar-    pure . toEnum . fromIntegral $ num+unicodeChar = toEnum . fromIntegral . f <$> go+    where go = string "\\u" >> count 4 anySingle+          f = fromHex . filterEmoji . BS.pack . fmap (fromIntegral . fromEnum) +emojiChar :: Parser Char+emojiChar = go a+    where a = string "\\ud" >> count 3 anySingle+          go = (<*>) =<< (((T.head . decodeUtf16) .* ((<>) . (<> "d") . ("d" <>))) <$>)++decodeUtf16 :: String -> T.Text+decodeUtf16 = TE.decodeUtf16BE . BS.concat . go+    where+        go []             = []+        go (a:b:c:d:rest) = let sym = convert16 [a,b] [c,d] in sym : go rest+        go _              = error "Internal error: decodeUtf16 failed."+        convert16 x y = BS.pack [(read . ("0x"<>)) x, (read . ("0x"<>)) y]+ -- | helper function to ignore emoji+filterEmoji :: BS.ByteString -> BS.ByteString filterEmoji str = if BS.head str == (fromIntegral . fromEnum $ 'd') then "FFFD" else str  -- | Parse HTML chars parseHTMLChar :: Parser Char parseHTMLChar = do-    char '&'-    innards <- many $ noneOf (";" :: String)-    char ';'-    pure . (\case -        (Just a) -> a +    single '&'+    innards <- many $ anySingleBut ';'+    single ';'+    pure . (\case+        (Just a) -> a         Nothing -> '?') $ M.lookup innards (M.fromList [("amp",'&'),("gt",'>'),("lt",'<'),("quot",'"'),("euro",'€'),("ndash",'–'),("mdash",'—')]) - -- | Parse escaped characters specialChar :: Char -> Parser Char-specialChar c = do-    string $ "\\" ++ pure c-    pure c+specialChar c = string ("\\" <> pure c) >> pure c  -- | Convert a string of four hexadecimal digits to an integer. fromHex :: BS.ByteString -> Integer-fromHex = fromRight . (parse (L.hexadecimal :: Parser Integer) "")-    where fromRight (Right a) = a+fromHex = fromRight . parseMaybe (L.hexadecimal :: Parsec Void BS.ByteString Integer)+    where fromRight (Just a) = a+          fromRight _        = error "failed to parse hex"
− src/Web/Tweet/Parser/FastParser.hs
@@ -1,32 +0,0 @@-{-# LANGUAGE DeriveGeneric     #-}--module Web.Tweet.Parser.FastParser ( fastParse-                                   , FastTweet (..)-                                   ) where--import GHC.Generics-import Data.Aeson-import qualified Data.Text as T-import qualified Data.ByteString.Lazy as BSL-import qualified Data.ByteString as BS---import Data.Vector--data FastTweet = FastTweet-    { id :: !Int-    , text :: !T.Text-    , user :: User-    , quoted_status :: Maybe FastTweet-    , retweet_count :: !Int-    , favorite_count :: !Int-    } deriving (Generic, Eq, Show)--data User = User { name        :: !T.Text-                 , screen_name :: !T.Text } -                 deriving (Generic, Eq, Show)--instance FromJSON FastTweet--instance FromJSON User--fastParse :: BS.ByteString -> Either String [FastTweet] -- (Vector FastTweet)-fastParse = eitherDecode . BSL.fromStrict
src/Web/Tweet/Sign.hs view
@@ -3,12 +3,23 @@ -- | Functions to sign HTTP requests with oAuth module Web.Tweet.Sign ( signRequest                       , signRequestMem-                      , mkConfig ) where+                      , mkConfig+                      , mkConfigToml+                      , oAuthMem+                      , credentialMem ) where -import Web.Tweet.Utils-import Web.Authenticate.OAuth-import Network.HTTP.Client-import Web.Tweet.Types+import           Data.ByteString        as BS+import           Data.HashMap.Lazy+import           Data.Monoid+import qualified Data.Text              as T+import           Data.Text.Encoding     (encodeUtf8)+import qualified Data.Text.IO           as TIO+import           Network.HTTP.Client+import           Prelude                hiding (lookup)+import           Text.Toml+import           Web.Authenticate.OAuth+import           Web.Tweet.Types+import           Web.Tweet.Utils  -- | Sign a request using your OAuth dev token, as stored in a config file. -- Uses the IO monad because signatures require a timestamp@@ -19,14 +30,47 @@ signRequestMem :: Config -> Request -> IO Request signRequestMem = uncurry signOAuth +-- | Create an OAuth api key from two ByteStrings.+oAuthMem :: BS.ByteString -- ^ API key+         -> BS.ByteString -- ^ API secret+         -> OAuth+oAuthMem key secret = newOAuth { oauthConsumerKey = key, oauthConsumerSecret = secret, oauthServerName = "api.twitter.com" }++credentialMem :: BS.ByteString -- ^ Token+              -> BS.ByteString -- ^ Token secret+              -> Credential+credentialMem = newCredential+ -- | Create an OAuth api key from config data in a file oAuth :: FilePath -> IO OAuth oAuth filepath = do-    secret <- (lineByKey "api-sec") <$> getConfigData filepath-    key <- (lineByKey "api-key") <$> getConfigData filepath+    secret <- lineByKey "api-sec" <$> getConfigData filepath+    key <- lineByKey "api-key" <$> getConfigData filepath     let url = "api.twitter.com"-    return newOAuth { oauthConsumerKey = key , oauthConsumerSecret = secret , oauthServerName = url }+    pure newOAuth { oauthConsumerKey = key , oauthConsumerSecret = secret , oauthServerName = url } +getKey :: HashMap T.Text Node -> T.Text -> BS.ByteString+getKey hm key = case lookup key hm of+    (Just (VString k)) -> encodeUtf8 k+    (Just _) -> error $ "Key: " <> T.unpack key <> " found in the config file, but it is not a string."+    Nothing -> error $ "Key: " <> T.unpack key <> " not found in config file."++mkConfigToml :: FilePath -> IO Config+mkConfigToml filepath = do+    t <- TIO.readFile filepath+    let hm = case parseTomlDoc ("failed to read .toml at: " <> filepath) t of+            Right tab -> tab+            Left e    -> error (show e)+        secret = getKey hm "api-sec"+        key = getKey hm "api-key"+        tok = getKey hm "tok"+        tokSecret = getKey hm "tok-sec"+        url = "api.twitter.com"+        o = newOAuth { oauthConsumerKey = key , oauthConsumerSecret = secret , oauthServerName = url }+        c = newCredential tok tokSecret+    pure (o, c)++-- | Given a filepath, parse the contents of the file and return a configuration. mkConfig :: FilePath -> IO Config mkConfig filepath = do     o <- oAuth filepath@@ -36,5 +80,5 @@ -- | Create a new credential from a token and token secret credential :: FilePath -> IO Credential credential filepath = newCredential <$> token <*> secretToken-    where token       = (lineByKey "tok") <$> getConfigData filepath-          secretToken = (lineByKey "tok-sec") <$> getConfigData filepath+    where token       = lineByKey "tok" <$> getConfigData filepath+          secretToken = lineByKey "tok-sec" <$> getConfigData filepath
src/Web/Tweet/Types.hs view
@@ -1,32 +1,32 @@-{-# LANGUAGE DeriveGeneric   #-}-{-# LANGUAGE DeriveAnyClass  #-}- -- | Exports the `Tweet` type, a datatype for building tweets easily module Web.Tweet.Types where -import GHC.Generics-import Control.Lens-import Data.Default-import Web.Authenticate.OAuth+import           Lens.Micro+import           Web.Authenticate.OAuth --- | Data type for our request: consists of the status text, whether to trium user information in the response, the handles to mention, and optionally the id of the status to reply to.+-- | Data type for our request: consists of the status text, whether to trium u information in the response, the handles to mention, and optionally the id of the status to reply to. data Tweet = Tweet-    { _status   :: String-    , _handles  :: [String]-    , _replyID  :: Maybe Int-    } deriving (Generic, Default)+    { _status  :: String+    , _handles :: [String]+    , _replyID :: Maybe Int+    }  -- | Data type for tweets as they are returned data TweetEntity = TweetEntity-    { _text :: String-    , _name :: String+    { _text       :: String+    , _replyTo    :: Maybe Int+    , _name       :: String     , _screenName :: String-    , _tweetId :: Int-    , _quoted :: Maybe TweetEntity-    , _retweets :: Int-    , _favorites :: Int-    } deriving (Generic, Default, Eq, Show)+    , _tweetId    :: Int+    , _withheld   :: [String]+    , _quoted     :: Maybe TweetEntity+    , _retweets   :: Int+    , _favorites  :: Int+    } deriving (Eq) +pricklyTweet :: Tweet+pricklyTweet = Tweet undefined [] Nothing+ -- | Stores data like (name, text, favoriteCount, retweetCount) type Timeline = [TweetEntity] @@ -58,17 +58,17 @@ screenName f tweet@TweetEntity { _screenName = scr } = fmap (\scr' -> tweet { _screenName = scr'}) (f scr)  -- | Lens for `TweetEntity` accessing the `_tweetId` field.-tweetId :: Lens' TweetEntity Int +tweetId :: Lens' TweetEntity Int tweetId f tweet@TweetEntity { _tweetId = tw } = fmap (\tw' -> tweet { _tweetId = tw'}) (f tw)  -- | Lens for `TweetEntity` accessing the `_quoted` field.-quoted :: Lens' TweetEntity (Maybe TweetEntity) -quoted f tweet@TweetEntity { _quoted = quot } = fmap (\quot' -> tweet { _quoted = quot'}) (f quot)+quoted :: Lens' TweetEntity (Maybe TweetEntity)+quoted f tweet@TweetEntity { _quoted = q } = fmap (\q' -> tweet { _quoted = q'}) (f q)  -- | Lens for `TweetEntity` accessing the `_retweets` field.-retweets :: Lens' TweetEntity Int +retweets :: Lens' TweetEntity Int retweets f tweet@TweetEntity { _retweets = rts } = fmap (\rts' -> tweet { _retweets = rts'}) (f rts)  -- | Lens for `TweetEntity` accessing the `_favorites` field.-favorites :: Lens' TweetEntity Int +favorites :: Lens' TweetEntity Int favorites f tweet@TweetEntity { _favorites = fav } = fmap (\fav' -> tweet { _favorites = fav'}) (f fav)
src/Web/Tweet/Utils.hs view
@@ -1,119 +1,140 @@ -- | Miscellaneous functions that don't fit the project directly module Web.Tweet.Utils (     hits+  , hits'   , getTweets   , displayTimeline   , displayTimelineColor   , lineByKey-  , getConfigData ) where+  , bird+  , getConfigData+  , filterQuotes+  , filterReplies+  , filterRTs+  ) where -import qualified Data.ByteString.Char8 as BS-import qualified Data.ByteString as BS2-import Data.List-import Web.Tweet.Types-import Control.Lens hiding (noneOf)-import Web.Tweet.Utils.Colors-import Data.List.Extra-import Web.Tweet.Parser-import Text.Megaparsec+import           Control.Composition+import qualified Data.ByteString        as BS2+import qualified Data.ByteString.Char8  as BS+import           Data.List+import           Data.List.Extra+import           Data.Void+import           Lens.Micro.Extras+import           Text.Megaparsec+import           Web.Tweet.Parser+import           Web.Tweet.Types+import           Web.Tweet.Utils.Colors  -- | filter out retweets, and sort by most successful. hits :: Timeline -> Timeline-hits = sortTweets . filterRTs +hits = sortTweets . filterRTs +-- | Filter out retweets and replies, and sort by most sucessful.+hits' :: Timeline -> Timeline+hits' = hits . filterReplies+ -- | Filter out retweets filterRTs :: Timeline -> Timeline-filterRTs = filter ((/="RT @") . take 4 . (view text))+filterRTs = filter ((/="RT @") . take 4 . view text) +-- | Filter out replies+filterReplies :: Timeline -> Timeline+filterReplies = filter ((/="@") . take 1 . view text)+ -- | Filter out quotes filterQuotes :: Timeline -> Timeline-filterQuotes = filter ((==Nothing) . (view quoted))+filterQuotes = filter ((==Nothing) . view quoted) --- | Get a list of tweets from a response, returning author, favorites, retweets, and content. -getTweets :: BS2.ByteString -> Either (ParseError Char Dec) Timeline-getTweets = parse parseTweet "" +-- | Get a list of tweets from a response, returning author, favorites, retweets, and content.+getTweets :: BS2.ByteString -> Either (ParseErrorBundle String Void) Timeline+getTweets = parse parseTweet "" . BS.unpack  -- | Display Timeline without color displayTimeline :: Timeline -> String-displayTimeline ((TweetEntity content user screenName idTweet Nothing rts fave):rest) = concat [user+displayTimeline (TweetEntity content _ u sn idTweet _ Nothing rts fave:rest) = concat [u     , " ("-    , screenName+    , sn     , ")"-    ,":\n    " -    ,fixNewline content -    ,"\n    " -    ,"♥ " -    ,show fave -    ," ♺ " -    ,show rts +    ,":\n    "+    ,fixNewline content+    ,"\n    "+    , "💜"+    -- , "♥ "+    ,show fave+    , " \61561  "+    -- ," ♺ "+    ,show rts     , "  "     , show idTweet-    ,"\n\n" +    ,"\n\n"     ,displayTimeline rest]-displayTimeline ((TweetEntity content user screenName idTweet (Just quoted) rts fave):rest) = concat [user +displayTimeline (TweetEntity content _ u sn idTweet _ (Just q) rts fave:rest) = concat [u     , " ("-    , screenName+    , sn     , ")"-    , ":\n    " -    , fixNewline content -    , "\n    " -    , "♥ " -    , show fave -    , " ♺ " -    , show rts +    , ":\n    "+    , fixNewline content+    , "\n    "+    , "💜"+    , show fave+    , " \61561  "+    , show rts     , "  "     , show idTweet-    , "\n    " -    , _name quoted +    , "\n    "+    , _name q     , " ("-    , _screenName quoted+    , _screenName q     , ")"-    , ": " -    , _text quoted -    , "\n\n" +    , ": "+    , _text q+    , "\n\n"     , displayTimeline rest] displayTimeline [] = [] +bird :: String+bird = toPlainBlue "🐦\n"+ -- | Display Timeline in color displayTimelineColor :: Timeline -> String-displayTimelineColor ((TweetEntity content user screenName idTweet Nothing rts fave):rest) = concat [toYellow user +displayTimelineColor (TweetEntity content _ u sn idTweet _ Nothing rts fave:rest) = concat [toYellow u     , " ("-    , screenName+    , sn     , ")"-    , ":\n    " +    , ":\n    "     , fixNewline content-    , "\n    " -    , toRed "♥"+    , "\n    "+    , toRed "💜"     , " "-    , show fave -    , toGreen " ♺ " -    , show rts +    , show fave+    , toGreen " \61561  " -- ♺ "+    , show rts     , "  "     , toBlue (show idTweet)-    , "\n\n" +    , "\n\n"     , displayTimelineColor rest]-displayTimelineColor ((TweetEntity content user screenName  idTweet (Just quoted) rts fave):rest) = concat [toYellow user +displayTimelineColor (TweetEntity content _ u sn idTweet _ (Just q) rts fave:rest) = concat [toYellow u     , " ("-    , screenName+    , sn     , ")"-    , ":\n    " -    , fixNewline content -    , "\n    " -    , toRed "♥"+    , ":\n    "+    , fixNewline content+    , "\n    "+    , toRed "💜"     , " "-    , show fave -    , toGreen " ♺ " -    , show rts +    , show fave+    , toGreen " \61561  "+    , show rts     , "  "     , toBlue (show idTweet)-    , "\n    " -    , toYellow $ _name quoted +    , "\n    "+    , toYellow $ _name q     , " ("-    , _screenName quoted+    , _screenName q     , ")"-    , ": " -    , _text quoted -    , "\n\n" +    , ": "+    , _text q+    , "\n\n"     , displayTimelineColor rest] displayTimelineColor [] = [] @@ -124,7 +145,7 @@ -- | sort tweets by most successful sortTweets :: Timeline -> Timeline sortTweets = sortBy compareTweet-    where compareTweet (TweetEntity _ _ _ _ _ r1 f1) (TweetEntity _ _ _ _ _ r2 f2) = compare (2*r2 + f2) (2*r1 + f1)+    where compareTweet (TweetEntity _ _ _ _ _ _ _ r1 f1) (TweetEntity _ _ _ _ _ _ _ r2 f2) = compare (2*r2 + f2) (2*r1 + f1)  -- | helper function to get the key as read from a file keyLinePie :: String -> String@@ -132,15 +153,15 @@  -- | Pick out a key value from a key lineByKey :: BS.ByteString -> [(BS.ByteString, BS.ByteString)] -> BS.ByteString-lineByKey key = snd . head . (filter (\i -> fst i == key))+lineByKey = snd .* head .* (filter . (fst .@ (==)))  -- | Filter a line of a file for only the actual data and no descriptors filterLine :: String -> String-filterLine = reverse . (takeWhile (not . (`elem` (" :" :: String)))) . reverse+filterLine = reverse . takeWhile (not . (`elem` (" :" :: String))) . reverse  -- | Get pairs of "key" to search for and actual values getConfigData :: FilePath -> IO [(BS.ByteString, BS.ByteString)] getConfigData filepath = zip <$> keys <*> content-    where content = (map (BS.pack . filterLine)) . lines <$> file-          keys    = (map (BS.pack . keyLinePie)) . lines <$> file+    where content = map (BS.pack . filterLine) . lines <$> file+          keys    = map (BS.pack . keyLinePie) . lines <$> file           file    = readFile filepath
src/Web/Tweet/Utils/API.hs view
@@ -4,21 +4,24 @@ module Web.Tweet.Utils.API (     getRequest   , postRequest+  , getRequestMem+  , postRequestMem   , urlString   , strEncode ) where -import Network.HTTP.Client-import Network.HTTP.Client.TLS-import Network.HTTP.Types.Status (statusCode)+import           Control.Monad              ((<=<))+import qualified Data.ByteString.Char8      as BS import qualified Data.ByteString.Lazy.Char8 as BSL-import qualified Data.ByteString.Char8 as BS-import qualified Data.Text as T-import Web.Tweet.Types-import Data.Char-import Data.Maybe-import Web.Authenticate.OAuth-import Data.Text.Encoding-import Web.Tweet.Sign+import           Data.Char+import           Data.Maybe+import qualified Data.Text                  as T+import           Data.Text.Encoding+import           Network.HTTP.Client+import           Network.HTTP.Client.TLS+import           Network.HTTP.Types.Status  (statusCode)+import           Web.Authenticate.OAuth+import           Web.Tweet.Sign+import           Web.Tweet.Types  -- | Make a GET request to twitter given a request string getRequestMem :: String -> Config -> IO BSL.ByteString@@ -30,11 +33,11 @@  -- | Make a GET request to twitter given a request string getRequest :: String -> FilePath -> IO BSL.ByteString-getRequest = flip ((. getRequestMem) . (>>=) . mkConfig)+getRequest str = getRequestMem str <=< mkConfigToml  -- | Make a POST request to twitter given a request string postRequest :: String -> FilePath -> IO BSL.ByteString-postRequest = flip ((. postRequestMem) . (>>=) . mkConfig)+postRequest str = postRequestMem str <=< mkConfigToml  -- | Make a POST request to twitter given a request string postRequestMem :: String -> Config -> IO BSL.ByteString@@ -49,35 +52,27 @@ responseBS request manager = do     response <- httpLbs request manager     let code = statusCode $ responseStatus response-    putStr $ if (code == 200) then "" else "failed :(\n error code: " ++ (show code) ++ "\n"+    putStr $ if code == 200 then "" else "failed :(\n error code: " ++ show code ++ "\n"     pure . responseBody $ response --- | print output of a request and return status id as an `Int`. -responseInt :: Request -> Manager -> IO Int-responseInt request manager = do-    response <- httpLbs request manager-    let code = statusCode $ responseStatus response-    putStrLn $ if (code == 200) then "POST succesful!" else "failed :(\n error code: " ++ (show code)-    return $ (read . (takeWhile (/=',')) . (drop 52)) (BSL.unpack $ responseBody response) -- TODO use the more general parser- -- | Convert a tweet to a percent-encoded url for querying an API urlString :: Tweet -> String urlString tweet = concat [ "?status="                          , BS.unpack (tweetEncode tweet)                          , "&trim_user="                          , map toLower (show trim)-                         , (if isJust (_replyID tweet) then "&in_reply_to_status_id=" else "")+                         , if isJust (_replyID tweet) then "&in_reply_to_status_id=" else ""                          , reply ]     where trim  = False-          reply = fromMaybe "" (show <$> _replyID tweet)+          reply = maybe "" show (_replyID tweet)  -- | Percent-encode a string strEncode :: String -> String strEncode = BS.unpack . paramEncode . encodeUtf8 . T.pack --- | Percent-encode the status update so it's fit for a URL and UTF-encode it as well. +-- | Percent-encode the status update so it's fit for a URL. tweetEncode :: Tweet -> BS.ByteString tweetEncode tweet = paramEncode . encodeUtf8 $ handleStr `T.append` content     where content   = T.pack . _status $ tweet-          handleStr = T.pack $ concatMap ((++ " ") . ((++) "@")) hs+          handleStr = T.pack $ concatMap ((++ " ") . (++) "@") hs           hs        = _handles tweet
src/Web/Tweet/Utils/Colors.hs view
@@ -1,8 +1,11 @@ -- | Helper functions to color strings module Web.Tweet.Utils.Colors where -import Text.PrettyPrint.ANSI.Leijen+import           Text.PrettyPrint.ANSI.Leijen +--  😎+--  😐+ -- | Make a string red toRed :: String -> String toRed = show . dullred . text@@ -18,3 +21,7 @@ -- | Make a string blue toBlue :: String -> String toBlue = show . underline . dullblue . text++-- | Make a string blue; no underlining.+toPlainBlue :: String -> String+toPlainBlue = show . dullblue . text
− stack.yaml
@@ -1,66 +0,0 @@-# This file was automatically generated by 'stack init'-#-# Some commonly used options have been documented as comments in this file.-# For advanced use and comprehensive documentation of the format, please see:-# http://docs.haskellstack.org/en/stable/yaml_configuration/--# Resolver to choose a 'specific' stackage snapshot or a compiler version.-# A snapshot resolver dictates the compiler version and the set of packages-# to be used for project dependencies. For example:-#-# resolver: lts-3.5-# resolver: nightly-2015-09-21-# resolver: ghc-7.10.2-# resolver: ghcjs-0.1.0_ghc-7.10.2-# resolver:-#  name: custom-snapshot-#  location: "./custom-snapshot.yaml"-resolver: lts-8.17--# User packages to be built.-# Various formats can be used as shown in the example below.-#-# packages:-# - some-directory-# - https://example.com/foo/bar/baz-0.0.2.tar.gz-# - location:-#    git: https://github.com/commercialhaskell/stack.git-#    commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a-# - location: https://github.com/commercialhaskell/stack/commit/e7b331f14bcffb8367cd58fbfc8b40ec7642100a-#   extra-dep: true-#  subdirs:-#  - auto-update-#  - wai-#-# A package marked 'extra-dep: true' will only be built if demanded by a-# non-dependency (i.e. a user package), and its test suites and benchmarks-# will not be run. This is useful for tweaking upstream packages.-packages:-- '.'-# Dependency packages to be pulled from upstream that are not in the resolver-# (e.g., acme-missiles-0.3)-extra-deps: []--# Override default flag values for local packages and extra-deps-flags: {}--# Extra package databases containing global packages-extra-package-dbs: []--# Control whether we use the GHC we find on the path-# system-ghc: true-#-# Require a specific version of stack, using version ranges-# require-stack-version: -any # Default-# require-stack-version: ">=1.4"-#-# Override the architecture used by stack, especially useful on Windows-# arch: i386-# arch: x86_64-#-# Extra directories used by stack for building-# extra-include-dirs: [/path/to/dir]-# extra-lib-dirs: [/path/to/dir]-#-# Allow a newer minor version of GHC than the snapshot specifies-# compiler-check: newer-minor
− test/Spec.hs
@@ -1,23 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--import Test.Hspec-import Test.Hspec.Megaparsec-import Text.Megaparsec-import Web.Tweet.Parser-import qualified Data.ByteString as BS----import Web.Tweet.Parser.FastParser---- TODO make sure it's the right number of tweets as well-main :: IO ()-main = hspec $ do-    describe "parseTweet" $ do-        file <- runIO $ BS.readFile "test/data"-        parallel $ it "parses sample tweets" $ do-            parse parseTweet "" `shouldSucceedOn` file-            {---    describe "fastParse" $ do-        file <- runIO $ BS.readFile "test/data"-        parallel $ it "parses sample tweets wrong" $ do-            fastParse "" `shouldBe` Left "some error idk"-            --}
tweet-hs.cabal view
@@ -1,129 +1,120 @@-name:                tweet-hs-version:             0.5.3.13-synopsis:            Command-line tool for twitter-description:         a Command Line Interface Tweeter-homepage:            https://github.com/vmchale/command-line-tweeter#readme-license:             BSD3-license-file:        LICENSE-author:              Vanessa McHale-maintainer:          tmchale@wisc.edu-copyright:           2016 Vanessa McHale-category:            Web-build-type:          Simple-stability:           stable-extra-source-files:  README.md, stack.yaml, bash/mkCompletions, test/data-cabal-version:       >=1.10+cabal-version:      >=1.10+name:               tweet-hs+version:            1.0.2.3+license:            BSD3+license-file:       LICENSE+copyright:          2016-2020 Vanessa McHale+maintainer:         vamchale@gmail.com+author:             Vanessa McHale+stability:          stable+synopsis:           Command-line tool for twitter+description:        a Command Line Interface Tweeter+category:           Web+build-type:         Simple+extra-source-files:+    README.md+    cabal.project.local+    bash/mkCompletions+    test/data -Flag llvm-fast {-  Description: Enable build with llvm backend-  Default: False-}+source-repository head+    type:     git+    location: https://hub.darcs.net/vmchale/tweet-hs -Flag library {-  Description: Don't build an executable-  Default:     False-}+flag llvm-fast+    description: Enable build with llvm backend+    default:     False -Flag gold {-  Description: Use the gold linker-  Default:     True-}+flag development+    description: Enable -Werror+    default:     False+    manual:      True -Flag parallel-gc {-  Description: Use parallel garbage collector-  Default:     False-}+flag parallel-gc+    description: Use parallel garbage collector+    default:     False  library-  hs-source-dirs:      src-  exposed-modules:     Web.Tweet-                     , Web.Tweet.Exec-                     , Web.Tweet.Parser-                     , Web.Tweet.Parser.FastParser-  other-modules:       Web.Tweet.Types-                     , Web.Tweet.Utils-                     , Web.Tweet.Utils.Colors-                     , Web.Tweet.Sign-                     , Web.Tweet.API-                     , Web.Tweet.Utils.API-                     , Paths_tweet_hs-  build-depends:       base >= 4.7 && < 5-                     , http-client-tls-                     , http-client-                     , http-types-                     , authenticate-oauth-                     , bytestring-                     , split-                     , optparse-applicative -                     , lens-                     , data-default-                     , text-                     , megaparsec-                     , containers-                     , ansi-wl-pprint-                     , directory-                     , extra-                     , composition-                     , aeson-  default-language:    Haskell2010-  default-extensions:  LambdaCase-  if flag(gold) -    ghc-options:       -optl-fuse-ld=gold-    ld-options:        -fuse-ld=gold-  ghc-options:      -fwarn-unused-imports+    exposed-modules:+        Web.Tweet+        Web.Tweet.Parser+        Web.Tweet.Sign+        Web.Tweet.API+        Web.Tweet.Utils +    hs-source-dirs:   src+    other-modules:+        Web.Tweet.Types+        Web.Tweet.Utils.Colors+        Web.Tweet.API.Internal+        Web.Tweet.Utils.API++    default-language: Haskell2010+    ghc-options:+        -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates++    build-depends:+        base >=4.11 && <5,+        http-client-tls,+        http-client,+        http-types,+        authenticate-oauth,+        megaparsec >=7.0,+        bytestring,+        split,+        microlens,+        unordered-containers,+        htoml-megaparsec >=2.1.0.0,+        text,+        containers >=0.6.0.0,+        ansi-wl-pprint,+        composition-prelude >=3.0.0.0,+        extra++    if flag(development)+        ghc-options: -Werror+ executable tweet-  if flag(library)-    Buildable: False-  else-    Buildable: True-  hs-source-dirs:      app-  main-is:             Main.hs-  if flag(llvm-fast)-    ghc-options:       -fllvm -optlo-O3 -O3-  if flag(gold) -    ghc-options:       -optl-fuse-ld=gold-    ld-options:        -fuse-ld=gold-  if flag(parallel-gc)-    ghc-options:       -rtsopts -with-rtsopts=-N-  ghc-options:         -threaded-  build-depends:       base-                     , tweet-hs -  default-language:    Haskell2010+    main-is:          Main.hs+    hs-source-dirs:   app+    other-modules:    Paths_tweet_hs+    default-language: Haskell2010+    ghc-options:+        -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates +    build-depends:+        base,+        tweet-hs,+        optparse-applicative,+        directory,+        bytestring++    if flag(llvm-fast)+        ghc-options: -fllvm -optlo-O3 -O3++    if flag(parallel-gc)+        ghc-options: -rtsopts -with-rtsopts=-N++    if flag(development)+        ghc-options: -Werror+ benchmark tweeths-bench-  type:             exitcode-stdio-1.0-  hs-source-dirs:   bench-  main-is:          Bench.hs-  build-depends:    base-                  , criterion-                  , tweet-hs-                  , megaparsec-                  , bytestring-  if flag(llvm-fast)-    ghc-options:       -fllvm -optlo-O3 -O3-  if flag(gold) -    ghc-options:       -optl-fuse-ld=gold-    ld-options:        -fuse-ld=gold-  ghc-options:       -threaded -rtsopts -with-rtsopts=-N -O3-  default-language: Haskell2010+    type:             exitcode-stdio-1.0+    main-is:          Bench.hs+    hs-source-dirs:   bench+    default-language: Haskell2010+    ghc-options:+        -Wall -Wincomplete-uni-patterns -Wincomplete-record-updates -test-suite tweeths-test-  type:                exitcode-stdio-1.0-  hs-source-dirs:      test-  main-is:             Spec.hs-  build-depends:       base-                     , tweet-hs-                     , hspec-                     , hspec-megaparsec-                     , megaparsec-                     , bytestring-  if flag(gold) -    ghc-options:       -optl-fuse-ld=gold-    ld-options:        -fuse-ld=gold-  ghc-options:         -threaded -rtsopts -with-rtsopts=-N -  default-language:    Haskell2010+    build-depends:+        base,+        criterion,+        tweet-hs,+        bytestring,+        megaparsec -source-repository head-  type:     git-  location: https://github.com/vmchale/command-line-tweeter+    if flag(llvm-fast)+        ghc-options: -fllvm -optlo-O3 -O3++    if flag(development)+        ghc-options: -Werror