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 +30/−0
- README.md +98/−0
- Setup.hs +2/−0
- app/Main.hs +6/−0
- bash/mkCompletions +4/−0
- src/Web/Tweet.hs +198/−0
- src/Web/Tweet/Exec.hs +183/−0
- src/Web/Tweet/Sign.hs +30/−0
- src/Web/Tweet/Types.hs +40/−0
- src/Web/Tweet/Utils.hs +149/−0
- src/Web/Tweet/Utils/Colors.hs +12/−0
- stack.yaml +66/−0
- tweet-hs.cabal +70/−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.
+ README.md view
@@ -0,0 +1,98 @@+# Command Line Interface Tweeter+++## 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+