packages feed

tweet-hs (empty) → 0.4.0.7

raw patch · 13 files changed

+888/−0 lines, 13 filesdep +MissingHdep +ansi-wl-pprintdep +authenticate-oauthsetup-changed

Dependencies added: MissingH, ansi-wl-pprint, authenticate-oauth, base, bytestring, data-default, directory, http-client, http-client-tls, http-types, lens, megaparsec, optparse-applicative, split, text, tweet-hs

Files

+ 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.
+ README.md view
@@ -0,0 +1,98 @@+# Command Line Interface Tweeter++![Displaying a user timeline in a terminal.](https://raw.githubusercontent.com/vmchale/command-line-tweeter/master/screenshot.png)+## 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:++```+api-key: API_KEY_HERE+api-sec: API_SECRET_HERE+tok: OAUTH_TOKEN_HERE+tok-sec: TOKEN_SECRET_HERE+```++## Installation++If you're on Linux/Windows the best way is probably to download the binaries+from the releases page [here](https://github.com/vmchale/command-line-tweeter/releases).++To build from source, install [haskell stack](https://docs.haskellstack.org/en/stable/README/#how-to-install); on unix systems this is as simple as++```+wget -qO- https://get.haskellstack.org/ | sh+```++Then type `stack install tweet-hs` it will generate an executable called `tweet`, which is what we want.++## Use++### View Profiles and timelines++To get your timeline, simply type:++```+tweet view+```++To view a user's profile, type e.g.++```+tweet user pinepapplesmear --color+```++### Sending tweets+To tweet from stderr, run a command that pipes stderr to stdin, i.e.++```+YOUR_BUILD_COMMAND 2>&1 >/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++```+tweet --help+```++This script powers the twitter account [@my\_build\_errors](https://twitter.com/my_build_errors) for instance. There's an example bash script for in `bash/example`++### Viewing your timeline++You can also use++```+tweet view+```++or ++```+tweet view --color+```++to view your own timeline.++### Completions++The directory `bash/` has a `mkCompletions` script to allow command completions for your convenice.++## Library++A haskell package is included. It's fairly easy to use once you have the credentials set up, with two main functions: `thread` and `basicTweet`: the first for threading your own tweets or replying to someone else's and the second for just tweeting.++### Finer details++The function `tweetData` will tweet an object of type `Tweet`. Its use is pretty self-explanatory, but how to best form `Tweet`s is not immediately obvious.++`Tweet` is an instance of `Default` so you can use `def` to get an empty tweet replying to nobody and not fetching extended user data. This is especially useful if you want to use lenses and avoid ugly record syntax, e.g.++```+set status "This is the new status field" $ def+```++will give you a `Tweet` with sensible defaults and the desired text.++### Haskell++This 
+ 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.Tweet.Exec++main :: IO ()+main = exec
+ bash/mkCompletions view
@@ -0,0 +1,4 @@+#!/bin/bash+tweet --bash-completion-script `which tweet` > tmp+sudo cp tmp /etc/bash_completion.d/tweet+rm tmp
+ src/Web/Tweet.hs view
@@ -0,0 +1,198 @@+{-# LANGUAGE OverloadedStrings #-}++-- | 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:+--+-- @+--+-- api-key: API_KEY+--+-- api-sec: API_SECRE+--+-- tok: OAUTH_TOKEN+--+-- tok-sec: TOKEN_SECRET+--+-- @++module Web.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+    , getTimeline+    , showTimeline+    , getProfile+    , showProfile+    , getDMs+    , showDMs+    , getRaw+    ) where++import Network.HTTP.Client+import Network.HTTP.Client.TLS+import Network.HTTP.Types.Status (statusCode)+import qualified Data.ByteString.Char8 as BS+import qualified Data.ByteString.Lazy.Char8 as BSL+import qualified Data.Text as T+import Data.Text.Encoding+import Data.Char+import Web.Tweet.Types+import Web.Tweet.Utils+import Control.Monad+import Data.List.Split (chunksOf)+import Data.Maybe+import Control.Lens+import Control.Lens.Tuple+import Web.Authenticate.OAuth+import Web.Tweet.Sign+import Data.List.Utils+import qualified Data.Text as T++-- | 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+    case idNum of+        (Just i) -> thread' content hs idNum num filepath+        Nothing -> case content of+            [] -> pure ()+            [x] -> void $ basicTweet x filepath+            y@(x:xs) -> thread' y hs (Just 0) num filepath++-- | Helper function to make `thread` easier to write. +thread' :: [String] -> [String] -> Maybe Int -> Int -> FilePath -> IO ()+thread' content hs idNum num filepath = do+    -- fix the stuff with the handles.+    let f = \str i -> tweetData (Tweet { _status = str, _trimUser = True, _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 last filepath++-- | 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 = thread contents hs idNum 1++-- | 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" }+    responseInt request manager++-- | Get tweets (text only) for some user+getRaw screenName filepath = do+    let requestString = "?screen_name=" ++ screenName ++ "&count=3200"+    manager <- newManager tlsManagerSettings+    initialRequest <- parseRequest ("https://api.twitter.com/1.1/statuses/user_timeline.json" ++ requestString)+    request <- signRequest filepath $ initialRequest { method = "GET"}+    let fromRight (Right a) = a+    map (view _2) . fromRight .  getTweets . BSL.unpack <$> responseBS request manager++-- | Get user profile given screen name and how many tweets to return+getProfile screenName count filepath = do+    let requestString = "?screen_name=" ++ screenName ++ "&count=" ++ (show count)+    manager <- newManager tlsManagerSettings+    initialRequest <- parseRequest ("https://api.twitter.com/1.1/statuses/user_timeline.json" ++ requestString)+    request <- signRequest filepath $ initialRequest { method = "GET"}+    responseBS request manager -- TODO+    getTweets . BSL.unpack <$> responseBS request manager++-- | Show your DMs, given how many to return and whether or not to use color.+showDMs count color filepath = showTweets color <$> getDMs count filepath++-- | Show a user profile given screen name, how many tweets to return (API+-- maximum is 3200), and whether to print them in color.+showProfile :: Show t => String -> t -> Bool -> FilePath -> IO String+showProfile screenName count color filepath = showTweets color <$> getProfile screenName count filepath++-- | Display user timeline+showTimeline count color filepath = showTweets color <$> getTimeline count filepath++-- | Display user timeline in color+showTweets color = (either show id) . (fmap (if color then displayTimelineColor else displayTimeline))++-- | Get user's DMs.+getDMs count filepath = do+    let requestString = "?count=" ++ (show count)+    manager <- newManager tlsManagerSettings+    initialRequest <- parseRequest ("https://api.twitter.com/1.1/direct_messages.json" ++ requestString)+    request <- signRequest filepath $ initialRequest { method = "GET" }+    getTweets . BSL.unpack <$> responseBS request manager++-- | Get a timeline+getTimeline count filepath = do+    let requestString = "?count=" ++ (show count)+    manager <- newManager tlsManagerSettings+    initialRequest <- parseRequest ("https://api.twitter.com/1.1/statuses/home_timeline.json" ++ requestString)+    request <- signRequest filepath $ initialRequest { method = "GET" }+    getTweets . BSL.unpack <$> responseBS request manager++-- | Delete a tweet given its id+deleteTweet id filepath = do+    manager <- newManager tlsManagerSettings+    initialRequest <- parseRequest ("https://api.twitter.com/1.1/statuses/destroy/" ++ (show id) ++ ".json")+    request <- signRequest filepath $ initialRequest { method = "POST" }+    void $ responseBS request manager++-- | Return HTTP request's result as a bytestring+responseBS :: Request -> Manager -> IO BSL.ByteString+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"+    -- print $ generalCategory ('📚')+    -- print $ (toEnum (0x1F48C) :: Char) -- (0xF079)+    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)++-- | 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 = fromMaybe "" (show <$> _replyID tweet)++-- | Percent-encode the status update so it's fit for a URL and UTF-encode it as well. +tweetEncode :: Tweet -> BS.ByteString+tweetEncode tweet = paramEncode . encodeUtf8 $ handleStr `T.append` content+    where content   = T.pack . _status $ tweet+          handleStr = T.pack $ concatMap ((++ " ") . ((++) "@")) hs+          hs        = _handles tweet
+ src/Web/Tweet/Exec.hs view
@@ -0,0 +1,183 @@+-- | 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.Char8 as BS+import Control.Monad+import Data.Foldable (fold)+import Data.List+import Data.Monoid+import System.Directory++-- | 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 }++-- | Data type for a +data Command = Timeline { count :: Maybe Int , color :: Bool } +    | SendInput { tweetInputs :: Maybe Int, replyId :: Maybe String, replyHandles :: Maybe [String] } +    | Profile { count :: Maybe Int , color :: Bool , screenName :: String } +    | Raw { screenName :: String } +    | Send { tweets :: Maybe Int , replyId :: Maybe String , replyHandles :: Maybe [String] , userInput :: 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+        opts = info (helper <*> program)+            (fullDesc+            <> progDesc "SendInput from stdin!"+            <> header "clit - a Command Line Interface SendInputer")++-- | Executes program given parsed `Program`+select :: Program -> IO ()+select (Program (Send (Just n) Nothing Nothing input) Nothing) = fromCLI input n =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Send Nothing Nothing Nothing input) Nothing) = fromCLI input 15  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Send (Just n) Nothing Nothing input) (Just file))  = fromCLI input n file+select (Program (Send Nothing Nothing Nothing input) (Just file)) = fromCLI input 15 file+select (Program (Send (Just n) (Just id) (Just handles) input) Nothing) = thread input handles (read id) n  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Send (Just n) (Just id) (Just handles) input) (Just file)) = thread input handles (pure . read $ id) n file+select (Program (Send Nothing (Just id) (Just handles) input) (Just file)) = thread input handles (pure . read $ id) 15 file+select (Program (Send (Just n) (Just id) Nothing input) Nothing) = (thread input [] (pure . read $ id) n)  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Send Nothing (Just id) Nothing input) Nothing) = thread input [] (pure . read $ id) 15  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Send Nothing (Just id) (Just handles) input) Nothing) = thread input handles (pure . read $ id) 15  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Send (Just n) (Just id) Nothing input) (Just file)) = thread input [] (pure . read $ id) n file+select (Program (Send (Just n) Nothing (Just handles) input) (Just file)) = thread input handles Nothing n file+select (Program (SendInput (Just n) Nothing Nothing) Nothing) = fromStdIn n =<< (++ "/.cred") <$> getHomeDirectory+select (Program (SendInput Nothing Nothing Nothing) Nothing) = fromStdIn 15  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (SendInput (Just n) Nothing Nothing) (Just file))  = fromStdIn n file+select (Program (SendInput Nothing Nothing Nothing) (Just file) ) = fromStdIn 15 file+select (Program (SendInput (Just n) (Just id) (Just handles)) Nothing) = threadStdIn handles (read id) n  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (SendInput (Just n) (Just id) (Just handles)) (Just file)) = threadStdIn handles (pure . read $ id) n file+select (Program (SendInput Nothing (Just id) (Just handles)) (Just file))  = threadStdIn handles (pure . read $ id) 15 file+select (Program (SendInput (Just n) (Just id) Nothing) Nothing) = threadStdIn [] (pure . read $ id) n  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (SendInput Nothing (Just id) Nothing) Nothing) = threadStdIn [] (pure . read $ id) 15  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (SendInput Nothing (Just id) (Just handles)) Nothing) = threadStdIn handles (pure . read $ id) 15  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (SendInput (Just n) (Just id) Nothing) (Just file)) = threadStdIn [] (pure . read $ id) n file+select (Program (SendInput (Just n) Nothing (Just handles)) (Just file)) = threadStdIn handles Nothing n file+select (Program (Timeline Nothing False) Nothing) = putStrLn =<< showTimeline 8 False  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Timeline Nothing False) (Just file)) = putStrLn =<< showTimeline 8 False file+select (Program (Timeline (Just n) False) (Just file)) = putStrLn =<< showTimeline 8 False file+select (Program (Timeline (Just n) False) Nothing) = putStrLn =<< showTimeline 8 False  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Timeline Nothing True) Nothing) = putStrLn =<< showTimeline 8 True  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Timeline Nothing True) (Just file)) = putStrLn =<< showTimeline 8 True file+select (Program (Timeline (Just n) True) (Just file)) = putStrLn =<< showTimeline 8 True file+select (Program (Timeline (Just n) True) Nothing) = putStrLn =<< showTimeline 8 True  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Profile (Just n) True name) (Just file)) = putStrLn =<< showProfile name n True file+select (Program (Profile Nothing True name) (Just file)) = putStrLn =<< showProfile name 12 True file+select (Program (Profile (Just n) True name) Nothing) = putStrLn =<< showProfile name n True  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Profile Nothing True name) Nothing) = putStrLn =<< showProfile name 12 True  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Profile (Just n) False name) (Just file)) = putStrLn =<< showProfile name n False file+select (Program (Profile Nothing False name) (Just file)) = putStrLn =<< showProfile name 12 False file+select (Program (Profile (Just n) False name) Nothing) = putStrLn =<< showProfile name n False  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Profile Nothing False name) Nothing) = putStrLn =<< showProfile name 12 False  =<< (++ "/.cred") <$> getHomeDirectory+select (Program (Raw name) Nothing) = do+    raw <- getRaw name =<< (++ "/.cred") <$> getHomeDirectory+    sequence_ $ putStrLn <$> raw+select (Program (Raw name) (Just file)) = do+    raw <- getRaw name file+    sequence_ $ putStrLn <$> raw --fix this idk++-- | 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 "raw" (info raw (progDesc "Grab tweetInputs en masse."))))+    <*> (optional $ strOption+        (long "cred"+        <> short 'c'+        <> metavar "CREDENTIALS"+        <> help "path to credentials"))++-- | 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"))+    <*> switch+        (long "color"+        <> short 'l'+        <> help "Display timeline with colorized terminal output.")++-- | Parser for the raw subcommand+raw :: Parser Command+raw = Raw+  <$> argument str+    (metavar "SCREEN_NAME"+    <> help "Screen name of user whose tweetInputs you want in bulk.")++-- | 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"))+    <*> switch+        (long "color"+        <> short 'l'+        <> help "Whether to display profile with colorized terminal output")+    <*> argument str+        (metavar "SCREEN_NAME"+        <> help "Screen name of user you want to view.")++-- | 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/Sign.hs view
@@ -0,0 +1,30 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Functions to sign HTTP requests with oAuth+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,40 @@+{-# LANGUAGE DeriveGeneric   #-}+{-# LANGUAGE DeriveAnyClass  #-}+{-# LANGUAGE TemplateHaskell #-}++-- | Exports the `Tweet` type, a datatype for building tweets easily+module Web.Tweet.Types where++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)++-- | Data type for tweets as they are returned+data TweetEntity = TweetEntity+    { _text :: String+    , _name :: String+    , _screenName :: Maybe String+    , _tweetId :: Int+    , _isQuoteStatus :: Bool+    , _retweets :: Int+    , _favorites :: Int+    } deriving (Generic, Default)++-- | Stores data like (name, text, favoriteCount, retweetCount)+type Timeline = [(String, String, String, String)]++makeLenses ''Tweet++makeLenses ''TweetEntity
+ src/Web/Tweet/Utils.hs view
@@ -0,0 +1,149 @@+-- | Miscellaneous functions that don't fit the project directly+module Web.Tweet.Utils where++import qualified Data.ByteString.Char8 as BS+import Text.Megaparsec.String+import Text.Megaparsec.Lexer as L+import Text.Megaparsec+import Data.Char+import Data.List+import Data.Monoid+import Data.Maybe+import Web.Tweet.Types+import Control.Monad+import Control.Lens.Tuple+import Control.Lens hiding (noneOf)+import Data.Function+import Web.Tweet.Utils.Colors++-- `FIXME` +parseDMs = zip <$> (extractEvery 2 <$> filterStr "screen_name") <*> (filterStr "text")+    where extractEvery n = map snd . filter ((== n) . fst) . zip (cycle [1..n])++-- | Display Timeline without color+displayTimeline :: Timeline -> String+displayTimeline ((user,content,fave,rts):rest) = (user <> ":\n    " <> content) <> "\n    " <> "♥ " {-- ♡💛--} <> fave <> " ♺ " <> rts <> "\n\n" <> (displayTimeline rest) -- +displayTimeline [] = []++-- | Display Timeline in color+displayTimelineColor :: Timeline -> String+displayTimelineColor ((user,content,fave,rts):rest) = ((toYellow user) <> ":\n    " <> content) <> "\n    " <> (toRed "♥ ") {-- ♡💛--} <> fave <> (toGreen " ♺ ") <> rts <> "\n\n" <> (displayTimelineColor rest) -- +displayTimelineColor [] = []++-- | Get a list of tweets from a response, returning author, favorites, retweets, and content. +getTweets = parse parseTweet "" ++-- | Parse some number of tweets+parseTweet :: Parser Timeline+parseTweet = concat <$> many (try getData <|> (const (pure ("","","","")) <$> eof))++hits = sortFaves . filterRTs++filterRTs :: Timeline -> Timeline+filterRTs = filter ((/="RT") . take 2 . (view _2))++sortFaves :: Timeline -> Timeline+sortFaves = sortBy compareFavorites+    where compareFavorites = on compare ((read :: String -> Int) . (view _3))++-- | Parse a single tweet's: name, text, fave count, retweet count+getData :: Parser [(String, String, String, String)]+getData = do+    text <- filterStr "text"+    skipMentions+    name <- filterStr "name"+    isQuote <- filterStr "is_quote_status"+    case isQuote of+        "false" -> do+            rts <- filterStr "retweet_count"+            faves <- filterStr "favorite_count"+            pure $ pure (name, text, faves, rts)+        "true" -> do+            textQuoted <- filterStr "text"+            skipMentions+            nameQuoted <- filterStr "name"+            rtsQuoted <- filterStr "retweet_count"+            favesQuoted <- filterStr "favorite_count"+            rts <- filterStr "retweet_count"+            faves <- filterStr "favorite_count"+            pure [(name, text, faves, rts), (nameQuoted, textQuoted, favesQuoted, rtsQuoted)]++-- TODO make it work when user names include ]+skipInsideBrackets :: Parser ()+skipInsideBrackets =void (between (char '[') (char ']') $ many (skipInsideBrackets <|> void (noneOf "[]")))++skipMentions :: Parser ()+skipMentions = do+    many $ try $ anyChar >> notFollowedBy (string ("\"user_mentions\":"))+    char ','+    string "\"user_mentions\":"+    skipInsideBrackets --between (char '[') (char ']') (many $ anyChar)+    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 ','+    filterTag str++-- | Parse a field given its tag+filterTag :: String -> Parser String+filterTag str = do+    string $ "\"" <> str <> "\":"+    open <- optional $ char '\"'+    let forbidden = if (isJust open) then "\\\"" else "\\\","+    want <- many $ noneOf forbidden <|> specialChar '\"' <|> specialChar '/' <|> newlineChar <|> unicodeChar -- specialChar 'u'+    pure want++-- | Parse a newline+newlineChar :: Parser Char+newlineChar = do+    string "\\n"+    pure '\n'++-- TODO+--emoji :: Parser Char+--emoji = do+--    string "\\u"+    --d83d dc8c 💌++-- | Parser for unicode; twitter will give us something like "/u320a"+unicodeChar :: Parser Char+unicodeChar = do+    string "\\u"+    num <- fromHex . filterEmoji <$> count 4 anyChar+    pure . toEnum . fromIntegral $ num++-- | ignore emoji +filterEmoji str = if head str == 'd' then "FFFD" else str++-- | Parse escaped characters+specialChar :: Char -> Parser Char+specialChar c = do+    string $ "\\" ++ pure c+    pure c++-- | Convert a string of four hexadecimal digits to an integer.+fromHex :: String -> Integer+fromHex = fromRight . (parse (L.hexadecimal :: Parser Integer) "")+    where fromRight (Right a) = a++-- | 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
+ src/Web/Tweet/Utils/Colors.hs view
@@ -0,0 +1,12 @@+module Web.Tweet.Utils.Colors where++import Text.PrettyPrint.ANSI.Leijen++toRed :: String -> String+toRed = show . red . text++toYellow :: String -> String+toYellow = show . dullyellow . text++toGreen :: String -> String+toGreen = show . dullgreen . text
+ stack.yaml view
@@ -0,0 +1,66 @@+# 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.5++# 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
+ tweet-hs.cabal view
@@ -0,0 +1,70 @@+name: tweet-hs+version: 0.4.0.7+cabal-version: >=1.10+build-type: Simple+license: BSD3+license-file: LICENSE+copyright: 2016 Vanessa McHale+maintainer: tmchale@wisc.edu+stability: stable+homepage: https://github.com/vmchale/command-line-tweeter#readme+synopsis: Post tweets from stdin+description:+    a Command Line Interface Tweeter+category: Web+author: Vanessa McHale+extra-source-files:+    README.md+    stack.yaml+    bash/mkCompletions++source-repository head+    type: git+    location: https://github.com/vmchale/command-line-tweeter++flag llvm-fast+    description:+        Enable build with llvm backend+    default: False++library+    exposed-modules:+        Web.Tweet+        Web.Tweet.Exec+    build-depends:+        base >=4.7 && <5,+        http-client-tls >=0.3.4 && <0.4,+        http-client >=0.5.6.1 && <0.6,+        http-types >=0.9.1 && <0.10,+        authenticate-oauth ==1.6.*,+        bytestring >=0.10.8.1 && <0.11,+        split >=0.2.3.1 && <0.3,+        optparse-applicative >=0.13.2.0 && <0.14,+        lens >=4.15.1 && <4.16,+        data-default >=0.7.1.1 && <0.8,+        text >=1.2.2.1 && <1.3,+        megaparsec >=5.2.0 && <5.3,+        ansi-wl-pprint >=0.6.7.3 && <0.7,+        MissingH >=1.4.0.1 && <1.5,+        directory >=1.3.0.0 && <1.4+    default-language: Haskell2010+    hs-source-dirs: src+    other-modules:+        Web.Tweet.Types+        Web.Tweet.Utils+        Web.Tweet.Utils.Colors+        Web.Tweet.Sign++executable tweet+    +    if flag(llvm-fast)+        ghc-options: -threaded -rtsopts -with-rtsopts=-N -fllvm -optlo-O3 -O3+    else+        ghc-options: -threaded -rtsopts -with-rtsopts=-N+    main-is: Main.hs+    build-depends:+        base >=4.9.1.0 && <4.10,+        tweet-hs >=0.4.0.7 && <0.5+    default-language: Haskell2010+    hs-source-dirs: app+