tweet-hs 0.5.3.13 → 1.0.2.3
raw patch · 20 files changed
Files
- LICENSE +1/−1
- README.md +27/−8
- Setup.hs +0/−2
- app/Main.hs +339/−2
- bench/Bench.hs +15/−14
- cabal.project.local +3/−0
- src/Web/Tweet.hs +42/−31
- src/Web/Tweet/API.hs +324/−69
- src/Web/Tweet/API/Internal.hs +41/−0
- src/Web/Tweet/Exec.hs +0/−276
- src/Web/Tweet/Parser.hs +75/−51
- src/Web/Tweet/Parser/FastParser.hs +0/−32
- src/Web/Tweet/Sign.hs +54/−10
- src/Web/Tweet/Types.hs +24/−24
- src/Web/Tweet/Utils.hs +91/−70
- src/Web/Tweet/Utils/API.hs +21/−26
- src/Web/Tweet/Utils/Colors.hs +8/−1
- stack.yaml +0/−66
- test/Spec.hs +0/−23
- tweet-hs.cabal +108/−117
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 +[](https://ci.appveyor.com/project/vmchale/command-line-tweeter) [](https://travis-ci.org/vmchale/command-line-tweeter)+[](http://hackage.haskell.org/package/tweet-hs)  @@ -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