hutton-1.0.0: Main.hs
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Applicative ((<$>), (<*>))
import Control.Monad (forever, mzero, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson (FromJSON, Value (Object), eitherDecode, parseJSON, (.:))
import Data.ByteString.Lazy.Char8 (unpack)
import Data.Char (isDigit)
import Data.IORef (IORef, newIORef, readIORef, writeIORef)
import Data.List (intersperse)
import Data.String (fromString)
import Data.Time.Clock (UTCTime)
import Data.Time.Format (defaultTimeLocale, parseTimeOrError)
import Flow ((|>), (<|))
import Network.HTTP.Client (Manager, Request)
import Network.HTTP.Conduit (httpLbs, parseUrl, requestHeaders, responseBody,
simpleHttp, urlEncodedBody, withManager)
import Network.WebSockets (Connection, receiveData)
import Rainbow (Chunk, Radiant, blue, cyan, fore, green, magenta, putChunk,
red, yellow, (<>))
import System.Environment (getArgs)
import System.Exit (exitFailure, exitSuccess)
import Text.Regex (matchRegex, mkRegex)
import Wuss (runSecureClient)
import Prelude
-- Get command-line arguments.
main :: IO ()
main = do
args <- getArgs
case args of
[threshold', username, password] -> main1 threshold' username password
_ -> do
putStrLn "Usage: hutton THRESHOLD USERNAME PASSWORD"
exitFailure
-- Set up login request.
main1 :: String -> String -> String -> IO ()
main1 threshold' username password = do
let threshold = read threshold' :: Int
putStrLn "Logging in..."
initialLoginRequest <- parseUrl "https://www.reddit.com/api/login"
let loginQuery =
[ ("api_type", "json")
, ("user", fromString username)
, ("passwd", fromString password)
]
loginRequest = urlEncodedBody loginQuery initialLoginRequest
withManager (main2 threshold loginRequest)
-- Actually log in.
main2 :: (MonadIO m) => Int -> Request -> Manager -> m ()
main2 threshold loginRequest manager = do
loginResponse <- httpLbs loginRequest manager
liftIO <| case eitherDecode (responseBody loginResponse) of
Right login -> main3 threshold login manager
Left message -> do
putStrLn message
exitFailure
-- Get query parameters.
main3 :: Int -> LoginResponse -> Manager -> IO ()
main3 threshold login manager = do
putStrLn "Logged in."
putStrLn (" cookie = " ++ cookie login)
putStrLn (" modhash = " ++ modhash login)
putStrLn "Getting query parameters..."
buttonResponse <- simpleHttp "https://www.reddit.com/r/thebutton"
let needle = mkRegex "\\?h=([0-9a-f]+)&e=([0-9]+)"
haystack = unpack buttonResponse
case matchRegex needle haystack of
Just [h, e] -> main4 h e threshold login manager
_ -> do
putStrLn "Failed to get query parameters."
exitFailure
-- Connect to WebSocket.
main4 :: String -> String -> Int -> LoginResponse -> Manager -> IO ()
main4 h e threshold login manager = do
putStrLn "Got query parameters."
putStrLn (" h = " ++ h)
putStrLn (" e = " ++ e)
putStrLn "Connecting to WebSocket..."
let host = "wss.redditmedia.com"
port = 443
path = "/thebutton?h=" ++ h ++ "&e=" ++ e
runSecureClient host port path (main5 threshold login manager)
-- Set up WebSocket client.
main5 :: Int -> LoginResponse -> Manager -> Connection -> IO ()
main5 threshold login manager connection = do
putStrLn "Connected to WebSocket."
ref <- newIORef 0
forever (main6 connection ref threshold login manager)
-- Listen for messages.
main6 :: Connection -> IORef Int -> Int -> LoginResponse -> Manager -> IO ()
main6 connection ref threshold login manager = do
value <- receiveData connection
case eitherDecode value of
Right datum -> main7 datum ref threshold login manager
Left message -> putStrLn message
-- Figure out what to do with messages.
main7 :: Datum -> IORef Int -> Int -> LoginResponse -> Manager -> IO ()
main7 datum ref threshold login manager = do
let thisNumPressers = numPressers datum
lastNumPressers <- readIORef ref
when (thisNumPressers > lastNumPressers) (main8 ref thisNumPressers)
prettyPrint datum
when (secondsLeft datum <= threshold) (main9 datum login manager)
-- Update the number of pressers.
main8 :: IORef Int -> Int -> IO ()
main8 ref thisNumPressers = do
writeIORef ref thisNumPressers
putStrLn ""
-- Press the button!
main9 :: Datum -> LoginResponse -> Manager -> IO ()
main9 datum login manager = do
putStrLn "Pressing the button..."
initialPressRequest <- parseUrl "https://www.reddit.com/api/press_button"
-- All of this probably isn't necessary. I don't have enough throwaway
-- accounts to figure what the minimal set of parameters is.
let pressQuery =
[ ("prev_seconds", datum |> secondsLeft |> show |> fromString)
, ("r", "thebutton")
, ("renderstyle", "html")
, ("seconds", datum |> secondsLeft |> show |> fromString)
, ("tick_mac", datum |> mac |> fromString)
, ("tick_time", datum |> datum_payload |> payload_nowStr |> fromString)
, ("uh", login |> modhash |> fromString)
]
pressHeaders =
[ ("Accept", "application/json, text/javascript, */*; q=0.01")
, ("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
, ("Cookie", "reddit_session=" <> (login |> cookie |> fromString))
, ("Origin", "http://www.reddit.com")
, ("Referer", "http://www.reddit.com/r/thebutton/")
, ("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.90 Safari/537.36")
, ("X-Requested-With", "XMLHttpRequest")
]
pressRequest = urlEncodedBody pressQuery initialPressRequest
{ requestHeaders = pressHeaders
}
pressResponse <- httpLbs pressRequest manager
print pressResponse
putStrLn "Pressed the button."
exitSuccess
data LoginResponse = LoginResponse
{ login_json :: LoginJson
} deriving (Show)
instance FromJSON LoginResponse where
parseJSON (Object o) = LoginResponse
<$> o .: "json"
parseJSON _ = mzero
data LoginJson = LoginJson
{ login_data :: LoginData
} deriving (Show)
instance FromJSON LoginJson where
parseJSON (Object o) = LoginJson
<$> o .: "data"
parseJSON _ = mzero
data LoginData = LoginData
{ login_cookie :: String
, login_modhash :: String
} deriving (Show)
instance FromJSON LoginData where
parseJSON (Object o) = LoginData
<$> o .: "cookie"
<*> o .: "modhash"
parseJSON _ = mzero
cookie :: LoginResponse -> String
cookie response = response
|> login_json
|> login_data
|> login_cookie
modhash :: LoginResponse -> String
modhash response = response
|> login_json
|> login_data
|> login_modhash
data Datum = Datum
{ datum_payload :: Payload
, datum_type :: String
} deriving (Show)
instance FromJSON Datum where
parseJSON (Object o) = Datum
<$> o .: "payload"
<*> o .: "type"
parseJSON _ = mzero
data Payload = Payload
{ payload_nowStr :: String
, payload_participantsText :: String
, payload_secondsLeft :: Float
, payload_tickMac :: String
} deriving (Show)
instance FromJSON Payload where
parseJSON (Object o) = Payload
<$> o .: "now_str"
<*> o .: "participants_text"
<*> o .: "seconds_left"
<*> o .: "tick_mac"
parseJSON _ = mzero
prettyPrint :: Datum -> IO ()
prettyPrint datum = do
mapM_ putChunk (format datum)
putStrLn ""
format :: Datum -> [Chunk]
format datum = intersperse "\t"
[ (datum |> secondsLeft |> show |> fromString) <> (datum |> color |> fore)
, datum |> numPressers |> show |> fromString
, datum |> time |> show |> fromString
, datum |> mac |> fromString
]
secondsLeft :: Datum -> Int
secondsLeft datum = datum
|> datum_payload
|> payload_secondsLeft
|> floor
color :: Datum -> Radiant
color datum = datum |> secondsLeft |> go where
go x
| x > 51 = magenta -- purple
| x > 41 = blue
| x > 31 = green
| x > 21 = yellow
| x > 11 = cyan -- orange
| otherwise = red
numPressers :: Datum -> Int
numPressers datum = datum
|> datum_payload
|> payload_participantsText
|> filter isDigit
|> read
time :: Datum -> UTCTime
time datum = datum
|> datum_payload
|> payload_nowStr
|> parseTimeOrError False defaultTimeLocale "%Y-%m-%d-%H-%M-%S"
mac :: Datum -> String
mac datum = datum
|> datum_payload
|> payload_tickMac