{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE TypeFamilies #-} module Main where import Control.Applicative import Control.Concurrent (threadDelay) import Control.Lens import Control.Monad (when,forever) import Control.Monad.Catch (try) import Control.Monad.IO.Class import Control.Monad.Logger import Control.Monad.Reader (ask) import Control.Monad.State (modify) import Control.Monad.Trans.Resource import Data.Acid import Data.SafeCopy import Data.String.Utils import Data.Typeable import Language.Haskell.Interpreter (runInterpreter) import Mueval.ArgsParse import Mueval.Interpreter import Web.Twitter.Conduit import Web.Twitter.Types.Lens import qualified Data.Conduit as C import qualified Data.Conduit.List as CL import qualified Data.Text as T import qualified Data.Text.IO as T import Tokens import Common -- Strip off the first world (which is assumed to be the screenname of the -- bot). getHaskellExpression :: T.Text -> T.Text getHaskellExpression t = case T.breakOn " " $ T.strip t of (a, "") -> a (_, rest) -> T.strip rest isHaskellPost :: T.Text -> Status -> Bool isHaskellPost userName status = (T.isPrefixOf userName $ status ^. statusText) && (status ^. statusUser ^. userScreenName) /= botScreenName evalExpr :: String -> IO String evalExpr e = case getOptions ["--expression", e] of Left t@(b, e) -> return $ show t Right opts -> do r <- runInterpreter (interpreter opts) case r of Left err -> return $ show err Right (e,et,val) -> do (out, b) <- getResult val return out getResult :: (Functor m, MonadIO m) => String -> m (String, Bool) getResult str = render 1024 str statusToText :: Status -> T.Text statusToText status = T.concat [ T.pack . show $ status ^. statusId , ": " , status ^. statusUser . userScreenName , ": " , status ^. statusText ] evalExpression :: MonadIO m => Status -> m String evalExpression status = do r <- liftIO $ evalExpr $ decodeHtml $ T.unpack $ getHaskellExpression $ status ^. statusText return $ take 140 r {-TODO: Make this more comprehensive-} decodeHtml :: String -> String decodeHtml s = replace "<" "<" $ replace ">" ">" $ replace "&" "&" $ replace """ "\"" $ replace "'" "'" $ s -- res <- call $ update "Hello World" reply :: Integer -> T.Text -> APIRequest StatusesUpdate Status reply i s = Web.Twitter.Conduit.update s & inReplyToStatusId ?~ i postreply :: (MonadResource m, MonadLogger m) => Status -> Integer -> String -> TW m Status postreply status i res = call (reply i $ (T.take 140 $ T.concat ["@", status ^. statusUser ^. userScreenName, " ", T.pack res])) {-Acid State database to keep track of replies-} data TweetId = TweetId { tweetId :: Integer } deriving (Eq, Show, Typeable) data LambdaTwitDb = LambdaTwitDb { allReplyIds :: [TweetId] } deriving (Typeable) allReplies :: Query LambdaTwitDb [TweetId] allReplies = allReplyIds <$> ask addReply :: TweetId -> Update LambdaTwitDb () addReply tweetId = modify go where go (LambdaTwitDb db) = LambdaTwitDb $ tweetId : db {-The Acid State magic-} deriveSafeCopy 0 'base ''TweetId deriveSafeCopy 0 'base ''LambdaTwitDb makeAcidic ''LambdaTwitDb ['allReplies, 'addReply] conduitmain :: IO () conduitmain = do state <- openLocalState (LambdaTwitDb []) forever $ do {-TODO: Use Data.Configurator to read in the oauth keys without needing a recompile: - http://hackage.haskell.org/package/configurator-} runNoLoggingT . runTwitterFromEnv $ do sourceWithMaxId mentionsTimeline C.$= CL.isolate 100 C.$$ CL.mapM_ $ \status -> do replies <- liftIO $ query state AllReplies if ((TweetId (status ^. statusId)) `elem` replies) then do liftIO $ putStrLn "Already replied to:" liftIO $ T.putStrLn $ statusToText status liftIO $ threadDelay $ 60 * 1000000 else do when (isHaskellPost botScreenName status) $ do liftIO $ T.putStrLn $ statusToText status res <- evalExpression status liftIO $ putStrLn res postres <- try $ postreply status (status ^. statusId) res case postres of Left (FromJSONError e) -> liftIO $ print e Left (TwitterErrorResponse s resH errs) -> liftIO $ print errs Left (TwitterStatusError s resH val) -> liftIO $ print val Right status -> liftIO $ print $ statusToText status liftIO $ Data.Acid.update state (AddReply $ TweetId (status ^. statusId)) -- AA TODO: Better rate limiting, this probably blocks every tweet. -- We should only wait for 60 seconds after each mentionsTimeline grab liftIO $ threadDelay $ 60 * 1000000 main :: IO () main = conduitmain {-TODO: Import lens:-} {-https://twitter.com/relrod6/status/516785803100688384-}