clit (empty) → 0.1.0.0
raw patch · 6 files changed
+212/−0 lines, 6 filesdep +aesondep +authenticate-oauthdep +basesetup-changed
Dependencies added: aeson, authenticate-oauth, base, bytestring, clit, http-client, http-client-tls, http-types, optparse-applicative, split
Files
- LICENSE +30/−0
- Setup.hs +2/−0
- app/Main.hs +6/−0
- clit.cabal +40/−0
- src/Web/Exec.hs +46/−0
- src/Web/Tweet.hs +88/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Vanessa McHale (c) 2016++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Vanessa McHale nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,6 @@+module Main where++import Web.Exec++main :: IO ()+main = exec
+ clit.cabal view
@@ -0,0 +1,40 @@+name: clit+version: 0.1.0.0+synopsis: Post tweets that you pipe to stdin+description: Please see README.md+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+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Web.Tweet+ , Web.Exec+ build-depends: base >= 4.7 && < 5+ , aeson+ , http-client-tls+ , http-client+ , http-types+ , authenticate-oauth+ , bytestring+ , split+ , optparse-applicative+ default-language: Haskell2010++executable tweet+ hs-source-dirs: app+ main-is: Main.hs+ ghc-options: -threaded -rtsopts -with-rtsopts=-N -O2+ build-depends: base+ , clit+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/vmchale/command-line-tweeter
+ src/Web/Exec.hs view
@@ -0,0 +1,46 @@+-- | 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
@@ -0,0 +1,88 @@+{-# 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 in the correct order:+--+-- api-key: API_KEY+--+-- api-sec: API_SECRE+--+-- tok: OAUTH_TOKEN+--+-- tok-sec: TOKEN_SECRET++module Web.Tweet+ ( tweet+ , signRequest+ , 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)++-- | Data type for our request+data Tweet = Tweet+ { status :: String+ , trim_user :: Bool+ } deriving Generic++instance ToJSON Tweet where++-- | tweet a byteString, with credentials from a given file+tweet :: FilePath -> BS.ByteString -> IO ()+tweet filepath content = do+ requestString <- urlString content+ 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 ()+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 <- (flip (!!) 1) <$> getConfigData filepath+ key <- (flip (!!) 0) <$> 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 = (flip (!!) 2) <$> getConfigData filepath+ secretToken = (flip (!!) 3) <$> getConfigData filepath++getConfigData :: FilePath -> IO [BS.ByteString]+getConfigData filepath = ((map (BS.pack . filterLine)) . lines) <$> readFile filepath++-- | 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 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)