packages feed

clit 0.1.1.2 → 0.2.0.0

raw patch · 8 files changed

+222/−110 lines, 8 filesdep +data-defaultdep +lensdep ~aesondep ~authenticate-oauthdep ~basePVP ok

version bump matches the API change (PVP)

Dependencies added: data-default, lens

Dependency ranges changed: aeson, authenticate-oauth, base, bytestring, http-client, http-client-tls, http-types, optparse-applicative, split

API changes (from Hackage documentation)

- Web.Exec: exec :: IO ()
- Web.Tweet: instance Data.Aeson.Types.Class.ToJSON Web.Tweet.Tweet
- Web.Tweet: instance GHC.Generics.Generic Web.Tweet.Tweet
- Web.Tweet: tweet :: FilePath -> ByteString -> IO ()
+ Web.Tweet: Tweet :: String -> Bool -> [String] -> Maybe Int -> Tweet
+ Web.Tweet: [_handles] :: Tweet -> [String]
+ Web.Tweet: [_replyID] :: Tweet -> Maybe Int
+ Web.Tweet: [_status] :: Tweet -> String
+ Web.Tweet: [_trimUser] :: Tweet -> Bool
+ Web.Tweet: basicTweet :: String -> FilePath -> IO Int
+ Web.Tweet: data Tweet
+ Web.Tweet: handles :: Lens' Tweet [String]
+ Web.Tweet: replyID :: Lens' Tweet (Maybe Int)
+ Web.Tweet: status :: Lens' Tweet String
+ Web.Tweet: thread :: String -> [String] -> Maybe Int -> Int -> FilePath -> IO ()
+ Web.Tweet: trimUser :: Lens' Tweet Bool
+ Web.Tweet: tweetData :: Tweet -> FilePath -> IO Int
+ Web.Tweet.Exec: Program :: Maybe FilePath -> Maybe String -> Maybe String -> Maybe [String] -> Program
+ Web.Tweet.Exec: data Program
+ Web.Tweet.Exec: exec :: IO ()
- Web.Tweet: urlString :: ByteString -> IO String
+ Web.Tweet: urlString :: Tweet -> String

Files

app/Main.hs view
@@ -1,6 +1,6 @@ module Main where -import Web.Exec+import Web.Tweet.Exec  main :: IO () main = exec
clit.cabal view
@@ -1,5 +1,5 @@ name:                clit-version:             0.1.1.2+version:             0.2.0.0 synopsis:            Post tweets from stdin description:         a Command Line Interface Tweeter homepage:            https://github.com/vmchale/command-line-tweeter#readme@@ -16,7 +16,10 @@ library   hs-source-dirs:      src   exposed-modules:     Web.Tweet-                     , Web.Exec+                     , Web.Tweet.Exec+  other-modules:       Web.Tweet.Types+                     , Web.Tweet.Utils+                     , Web.Tweet.Sign   build-depends:       base >= 4.7 && < 5                      , aeson                      , http-client-tls@@ -26,6 +29,8 @@                      , bytestring                      , split                      , optparse-applicative < 0.13.0.0+                     , lens+                     , data-default   default-language:    Haskell2010  executable tweet
− src/Web/Exec.hs
@@ -1,46 +0,0 @@--- | Provides IO action that parses command line options and tweets from stdin-module Web.Exec (exec) where--import Web.Tweet-import Options.Applicative-import qualified Data.ByteString.Char8 as BS-import Data.List.Split (chunksOf)---- | Executes parser-exec :: IO ()-exec = execParser opts >>= select-    where -        opts = info (helper <*> program)-            (fullDesc-            <> progDesc "Tweet from stdin!"-            <> header "clit - a Command Line Interface Tweeter")---- | query twitter to post stdin-fromStdIn :: Int -> FilePath -> IO ()-fromStdIn num filepath = do-    content <- fmap ((take num) . (map BS.pack) .  (chunksOf 140)) getContents-    sequence_ $ fmap (tweet filepath) content---- | Data type for our program; just one optional argument for path to credential file.-data Program = Program { cred :: Maybe FilePath , tweets :: Maybe String }---- | Executes program-select :: Program -> IO ()-select (Program Nothing (Just n)) = fromStdIn (read n) ".cred"-select (Program Nothing Nothing) = fromStdIn 4 ".cred"-select (Program (Just file) (Just n)) = fromStdIn (read n) file-select (Program (Just file) Nothing) = fromStdIn 4 file---- | Parser to return a program datatype-program :: Parser Program-program = Program-    <$> (optional $ strOption-        (long "cred"-        <> short 'c'-        <> metavar "CREDENTIALS"-        <> help "path to credentials"))-    <*> (optional $ strOption-        (long "tweets"-        <> short 't'-        <> metavar "NUM"-        <> help "Number of tweets in a row, default 4"))
src/Web/Tweet.hs view
@@ -1,10 +1,11 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE DeriveGeneric     #-}  -- | Various utilities to tweet using the twitter api -- --- Make sure you have a file credentials file (default `.cred`) with the following info:+-- Make sure you have a file credentials file (default the executable looks for is `.cred`) with the following info: --+-- ```+-- -- api-key: API_KEY -- -- api-sec: API_SECRE@@ -12,88 +13,95 @@ -- tok: OAUTH_TOKEN -- -- tok-sec: TOKEN_SECRET+--+-- ```  module Web.Tweet-    ( tweet+    (+    -- * Functions to tweet+    basicTweet+    , tweetData+    , thread+    -- * Data type for a tweet+    , module Web.Tweet.Types+    -- * Functions to sign API requests     , signRequest+    -- * Functions to generate a URL string from a `Tweet`     , urlString     ) where -import Data.Aeson-import GHC.Generics import Network.HTTP.Client import Network.HTTP.Client.TLS import Network.HTTP.Types.Status (statusCode)-import Web.Authenticate.OAuth import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Lazy.Char8 as BSL import Data.Char (toLower)+import Web.Tweet.Types+import Web.Tweet.Utils+import Control.Monad+import Data.List.Split (chunksOf)+import Data.Maybe+import Control.Lens+import Web.Authenticate.OAuth+import Web.Tweet.Sign --- | Data type for our request-data Tweet = Tweet-    { status    :: String-    , trim_user :: Bool-    } deriving Generic+-- | thread tweets together nicely. Takes a string, a list of handles to reply to, plus the ID of the status you're replying to.+-- If you need to thread tweets without replying, pass a `Nothing` as the third argument.+-- `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+    print $ urlString (Tweet { _status = content !! 0, _trimUser = True, _handles = hs, _replyID = idNum})+    let f = (\str i -> (flip tweetData filepath) (Tweet { _status = str, _trimUser = True, _handles = hs, _replyID = if i == 0 then Nothing else Just i }))+    let initial = f (content !! 0)+    void $ foldr ((>=>) . f) initial (drop 1 content) $ maybe 0 id idNum -instance ToJSON Tweet where+-- | Reply with a single tweet. Works the same as `thread` but doesn't take the fourth argument.+-- `reply "Idk what that means" ["friend1"] (Just 189943500) ".cred"`+reply :: String -> [String] -> Maybe Int -> FilePath -> IO ()+reply contents hs idNum filepath = thread contents hs idNum 1 filepath --- | tweet a byteString, with credentials from a given file-tweet :: FilePath -> BS.ByteString -> IO ()-tweet filepath content = do-    requestString <- urlString content+-- | Tweet a string given a path to credentials; return the id of the status.+-- `basicTweet "On the airplane." ".cred"+basicTweet :: String -> FilePath -> IO Int+basicTweet contents = tweetData (mkTweet contents)++-- | Make a `Tweet` with only the contents.+mkTweet :: String -> Tweet+mkTweet contents = over (status) (const (contents)) $ def ++-- | 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     manager <- newManager tlsManagerSettings     initialRequest <- parseRequest ("https://api.twitter.com/1.1/statuses/update.json" ++ requestString)     request <- signRequest filepath $ initialRequest { method = "POST" }     response request manager --- | print output of a request-response :: Request -> Manager -> IO ()+-- | print output of a request and return status id as an `Int`. +response :: Request -> Manager -> IO Int response request manager = do     response <- httpLbs request manager     putStrLn $ "The status code was: " ++ show (statusCode $ responseStatus response)     BSL.putStrLn $ responseBody response---- | Sign a request using your OAuth dev token.--- Uses the IO monad because signatures require a timestamp-signRequest :: FilePath -> Request -> IO Request-signRequest filepath req = do-    o <- oAuth filepath-    c <- credential filepath-    signOAuth o c req---- | Create an OAuth token-oAuth :: FilePath -> IO OAuth-oAuth filepath = do-    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 }---- | Create a new credential from a token and secret component of that token-credential :: FilePath -> IO Credential-credential filepath = newCredential <$> token <*> secretToken-    where token       = (lineByKey "tok") <$> getConfigData filepath-          secretToken = (lineByKey "tok-sec") <$> getConfigData filepath---- | 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))---- | 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 . keyLine)) . lines <$> file-          file    = readFile filepath--keyLine :: String -> String-keyLine = takeWhile (/=':')+    return $ (read . (takeWhile (/=',')) . (drop 52)) (BSL.unpack $ responseBody response) --- | Filter a line of a file for only the actual data and no descriptors-filterLine :: String -> String-filterLine = reverse . (takeWhile (not . (`elem` (" :" :: String)))) . reverse+-- | 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 "")+                         , reply ]+    where trim  = _trimUser tweet+          reply = maybe "" id (fmap show $ _replyID $ tweet) --- | Convert a byteString to the percent-encoded version-urlString :: BS.ByteString -> IO String-urlString content = do-    return $ "?status=" ++ ((BS.unpack . paramEncode) content) ++ "&trim_user" ++ (map toLower $ show True)+-- | Percent-ncode the status update so it's fit for a URL+tweetEncode :: Tweet -> BS.ByteString+tweetEncode tweet = paramEncode $ handleStr `BS.append` content+    where content   = BS.pack . _status $ tweet+          handleStr = BS.pack $ concatMap ((++ " ") . ((++) "@")) hs+          hs        = _handles tweet
+ src/Web/Tweet/Exec.hs view
@@ -0,0 +1,66 @@+-- | Provides IO action that parses command line options and tweets from stdin+module Web.Tweet.Exec ( exec+                      , Program (Program)) where++import Web.Tweet+import Options.Applicative+import qualified Data.ByteString.Char8 as BS+import Control.Monad+import Data.Foldable (fold)++-- | Data type for our program: one optional path to a credential file, (optionally) the number of tweets to make, the id of the status you're replying to, and a list of users you wish to mention.+data Program = Program { cred :: Maybe FilePath , tweets :: Maybe String, replyId :: Maybe String, replyHandles :: Maybe [String] }++-- | query twitter to post stdin with no fancy options+fromStdIn :: Int -> FilePath -> IO ()+fromStdIn = threadStdIn [] Nothing++-- | Threaded tweets 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+        opts = info (helper <*> program)+            (fullDesc+            <> progDesc "Tweet from stdin!"+            <> header "clit - a Command Line Interface Tweeter")++-- | Executes program+select :: Program -> IO ()+select (Program Nothing (Just n) Nothing Nothing) = fromStdIn (read n) ".cred"+select (Program Nothing Nothing Nothing Nothing) = fromStdIn 4 ".cred"+select (Program (Just file) (Just n) Nothing Nothing) = fromStdIn (read n) file+select (Program (Just file) Nothing Nothing Nothing) = fromStdIn 4 file+select (Program Nothing (Just n) (Just id) (Just handles)) = threadStdIn handles (read id) (read n) ".cred"+select (Program (Just file) (Just n) (Just id) (Just handles)) = threadStdIn handles (pure . read $ id) (read n) file+select (Program (Just file) Nothing (Just id) (Just handles)) = threadStdIn handles (pure . read $ id) 4 file+select (Program Nothing (Just n) (Just id) Nothing) = threadStdIn [] (pure . read $ id) (read n) ".cred"+select (Program Nothing Nothing (Just id) Nothing) = threadStdIn [] (pure . read $ id) 4 ".cred"+select (Program Nothing Nothing (Just id) (Just handles)) = threadStdIn handles (pure . read $ id) 4 ".cred"+select (Program (Just file) (Just n) (Just id) Nothing) = threadStdIn [] (pure . read $ id) (read n) file++-- | Parser to return a program datatype+program :: Parser Program+program = Program+    <$> (optional $ strOption+        (long "cred"+        <> short 'c'+        <> metavar "CREDENTIALS"+        <> help "path to credentials"))+    <*> (optional $ strOption+        (long "tweets"+        <> short 't'+        <> metavar "NUM"+        <> help "Number of tweets in a row, default 4"))+    <*> (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/Sign.hs view
@@ -0,0 +1,29 @@+{-# LANGUAGE OverloadedStrings #-}++module Web.Tweet.Sign where++import Web.Tweet.Utils+import Web.Authenticate.OAuth+import Network.HTTP.Client++-- | Sign a request using your OAuth dev token.+-- Uses the IO monad because signatures require a timestamp+signRequest :: FilePath -> Request -> IO Request+signRequest filepath req = do+    o <- oAuth filepath+    c <- credential filepath+    signOAuth o c req++-- | Create an OAuth 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+    let url = "api.twitter.com"+    return newOAuth { oauthConsumerKey = key , oauthConsumerSecret = secret , oauthServerName = url }++-- | 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
+ src/Web/Tweet/Types.hs view
@@ -0,0 +1,27 @@+{-# LANGUAGE DeriveGeneric   #-}+{-# LANGUAGE DeriveAnyClass  #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Exports the `Tweet` type, a datatype for building tweets easily+module Web.Tweet.Types where++import Data.Aeson+import GHC.Generics+import Control.Lens+import Data.Default++-- | Default value for Bool for trim_user (`True` in our case)+instance Default Bool where+    def = True++-- | 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 Tweet = Tweet+    { _status   :: String+    , _trimUser :: Bool+    , _handles  :: [String]+    , _replyID  :: Maybe Int+    } deriving (Generic, Default)++makeLenses ''Tweet++instance ToJSON Tweet where
+ src/Web/Tweet/Utils.hs view
@@ -0,0 +1,23 @@+-- | Miscellaneous functions that don't fit the project directly+module Web.Tweet.Utils where++import qualified Data.ByteString.Char8 as BS++-- | helper function to get the key as read from a file+keyLinePie :: String -> String+keyLinePie = takeWhile (/=':')++-- | 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))++-- | Filter a line of a file for only the actual data and no descriptors+filterLine :: String -> String+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+          file    = readFile filepath