packages feed

follower-0.0.1: follower.hs

{-# LANGUAGE DeriveDataTypeable #-}

module Main where

import Paths_follower (version)

import Web.Twitter
import Web.Twitter.Monad
import Web.Twitter.Types
import Web.Twitter.Fetch
import Data.Time
import Data.Time.Format
import Data.Version (showVersion)
import Control.Monad
import System.Locale
import System.Directory
import System.Console.CmdArgs
import qualified System.IO.Strict as S
import qualified System.FilePath as Fp
import Text.PrettyPrint.ANSI.Leijen hiding (list)

data Argument = Argument {follow :: Maybe String,
                          no_follow :: Maybe String,
                          list :: Bool}
              deriving (Show, Data, Typeable)

type State = [(UserName, StatusId)]

instance Eq Status where
  x == y = (statusId x) == (statusId y)

instance Ord Status where
  compare x y = compare xt yt
    where
      xt :: UTCTime
      xt = parseStatusDate $ statusCreated x
      yt :: UTCTime
      yt = parseStatusDate $ statusCreated y

parseStatusDate :: (ParseTime a) => DateString -> a
parseStatusDate dstr = case (parseTime defaultTimeLocale (dateTimeFmt defaultTimeLocale) dstr) of
  Just t -> t
  Nothing -> error "Cannot parse time!"

getTweetsBy :: (UserName, StatusId) -> TM [Status]
getTweetsBy (name,since) = getUserTimeline (Just name) Nothing (Just since)

formatTweets :: [Status] -> Doc
formatTweets = (<$> empty) . vsep . map formatTweet

shortenTime :: DateString -> String
shortenTime dstr = formatTime defaultTimeLocale "%F %R %Z" tweetTime
  where
    tweetTime :: UTCTime
    tweetTime = parseStatusDate dstr

formatTweet :: Status -> Doc
formatTweet tweet = (bold $ langle <>
                     (blue $ text username) <>
                     rangle) <+>
                    (align $ (foldr (</>) empty) $ (map text) $ words tweettext) <$>
                    (text "--") <+> lparen <> (red $ text tweetdate) <> rparen
                     
  where
    username = userScreenName (statusUser tweet)
    tweetdate = shortenTime $ statusCreated tweet
    tweettext = statusText tweet

notReply :: Status -> Bool
notReply tweet = case (statusInReplyTo tweet) of
  Just _ -> False
  Nothing -> True

merge :: (Ord a) => [[a]] -> [a]
merge = foldr merge' []
    where
      merge' [] [] = []
      merge' [] (x:xs) = x:xs
      merge' (x:xs) [] = x:xs
      merge' (x:xs) (y:ys) = case (compare x y) of
        LT -> y : (merge' (x:xs) ys)
        EQ -> y : x : (merge' xs ys)
        GT -> x : (merge' xs (y:ys))

followAll :: State -> IO State
followAll us = do results <- mapM (runTM nullAuthUser . getTweetsBy) us
                  putDoc $ formatTweets $ filter notReply $ merge results
                  return $ updateState us results
  where
    updateState :: State -> [[Status]] -> State
    updateState [] [] = []
    updateState (p:ps) (t:ts) = case t of
      [] -> p : (updateState ps ts)
      (x:xs) -> ((fst p), (statusId x)) : (updateState ps ts)
    updateState _ _ = error "This should not happen"

stalk :: UserName -> IO (Maybe StatusId)
stalk un = do tweets <- runTM nullAuthUser $ getUserTimeline (Just un) Nothing Nothing
              case tweets of
                [] -> return Nothing
                (x:xs) -> do putDoc $ formatTweets tweets
                             return $ Just $ statusId x

addFollow :: State -> UserName -> IO State
addFollow curr un = do result <- stalk un
                       return $ case result of
                         Just state -> (un, state) : curr
                         Nothing -> curr

rmFollow :: State -> UserName -> IO State
rmFollow curr un = return $ filter ((/= un) . fst) curr

saveState :: (Show a) => a -> IO ()
saveState st = do confDir <- getAppUserDataDirectory appName
                  createDirectoryIfMissing True confDir
                  writeFile (confDir Fp.</> stateFileName) (show st)

loadState :: (Read a) => IO (Maybe a)
loadState = do confDir <- getAppUserDataDirectory appName
               let confFile = confDir Fp.</> stateFileName
               exists <- doesFileExist confFile
               case exists of
                 True ->  do contents <- S.readFile confFile
                             return $ Just $ read contents
                 False -> return Nothing

stateFileName = "state"
appName = "follower"

arguments = Argument { follow = def &= typ "SCREEN_NAME" &= help "Follow the user with the given screen name",
                       no_follow = def &= typ "SCREEN_NAME" &= help "Stop following the user with the given screen name",
                       list = def &= help "Display the current follow list"
                     } &= program appName &= summary (appName ++ " " ++ (showVersion version) ++ " " ++ "Copyright R. Emre Başar 2011 - ...")

argumentsGiven :: Argument -> Bool
argumentsGiven args = (follow args /= Nothing) || (no_follow args /= Nothing) || (list args)

applyArgs :: Argument -> State -> IO State
applyArgs args cs = if (argumentsGiven args) then
                      processArguments args cs
                    else
                      followAll cs

listFollowed state = putDoc $ (vsep $ map (text . fst) state) <$> empty

processArguments :: Argument -> State -> IO State
processArguments args state = do newState <- case (follow args) of
                                                  Just name -> addFollow state name
                                                  Nothing -> return state
                                 newState <- case (no_follow args) of
                                                  Just name -> rmFollow newState name
                                                  Nothing -> return newState
                                 when (list args) $ listFollowed newState
                                 return newState

main :: IO ()
main = do args <- cmdArgs arguments
          currentState <- loadState
          case currentState of
            Just st -> do latestState <- applyArgs args st
                          saveState latestState
            Nothing -> putStrLn "You are not following anyone. Try adding some people with --follow"