packages feed

macbeth-lib-0.0.12: src/Macbeth/Fics/FicsConnection.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE LambdaCase #-}

module Macbeth.Fics.FicsConnection (
  ficsConnection
) where

import Macbeth.Fics.Configuration
import Macbeth.Fics.FicsMessage hiding (gameId)
import Macbeth.Fics.Api.Move hiding (Observing)
import Macbeth.Fics.Parsers.FicsMessageParser

import Control.Concurrent.Chan
import Control.Monad
import Control.Monad.State
import Control.Monad.Trans.Resource
import Data.Char
import Data.Conduit
import Data.Conduit.Binary
import Data.Conduit.List hiding (filter)
import Data.Maybe
import Data.Time
import Network
import System.FilePath
import System.IO

import qualified Data.ByteString.Char8 as BS


data HelperState = HelperState { config :: Config
                               , takebackAccptedBy :: Maybe String -- ^ oponent accepted takeback request
                               , observingGameId :: Maybe Int
                               , newGameId :: Maybe Int }


ficsConnection :: IO (Handle, Chan FicsMessage)
ficsConnection = runResourceT $ do
  (_, hsock) <- allocate (connectTo "freechess.org" $ PortNumber 5000) hClose
  liftIO $ hSetBuffering hsock LineBuffering
  config <- liftIO loadConfig
  chan <- liftIO newChan
  resourceForkIO $ liftIO $ chain hsock config chan
  return (hsock, chan)


chain :: Handle -> Config-> Chan FicsMessage -> IO ()
chain h config chan = flip evalStateT emptyState $ transPipe lift
  (sourceHandle h) $$
  linesC =$
  blockC BS.empty =$
  unblockC =$
  fileLoggerC =$
  parseC =$
  stateC =$
  copyC chan =$
  loggingC =$
  sink chan
  where emptyState = HelperState config Nothing Nothing Nothing


sink :: Chan FicsMessage -> Sink FicsMessage (StateT HelperState IO) ()
sink chan = awaitForever $ liftIO . writeChan chan


loggingC :: Conduit FicsMessage (StateT HelperState IO) FicsMessage
loggingC = awaitForever $ \cmd -> do
  logToStdOut <- (stdOut . logging . config) `fmap` get
  when logToStdOut $ liftIO (printCmdMsg cmd)
  yield cmd


copyC :: Chan FicsMessage -> Conduit FicsMessage (StateT HelperState IO) FicsMessage
copyC chan = awaitForever $ \case
  m@(MatchAccepted move) -> do
    chan' <- liftIO $ dupChan chan
    sourceList [m, WxMatchAccepted move chan']
  m@(Observe move) -> do
    chan' <- liftIO $ dupChan chan
    sourceList [m, WxObserve move chan']
  cmd -> yield cmd


stateC :: Conduit FicsMessage (StateT HelperState IO) FicsMessage
stateC = awaitForever $ \cmd -> do
  state <- get
  case cmd of
    GameCreation id -> do
      put $ state {newGameId = Just id}
      sourceNull

    Observing id -> do
      put $ state {observingGameId = Just id}
      sourceNull

    TakebackAccepted username -> do
      put $ state {takebackAccptedBy = Just username}
      sourceNull

    m@(GameMove _ move)
      | isCheckmate move && isOponentMove move -> sourceList $ cmd : [toGameResult move]
      | isNewGameUser move && fromMaybe False ((== gameId move) <$> newGameId state) -> do
          put $ state {newGameId = Nothing }
          sourceList [MatchAccepted move]
      | fromMaybe False ((== gameId move) <$> observingGameId state) -> do
          put $ state {observingGameId = Nothing }
          sourceList [Observe move]
      | isJust $ takebackAccptedBy state -> do
          put $ state {takebackAccptedBy = Nothing}
          sourceList [m{context = Takeback $ takebackAccptedBy state}]
      | otherwise -> sourceList [m]

    _ -> yield cmd


parseC :: Conduit BS.ByteString (StateT HelperState IO) FicsMessage
parseC = awaitForever $ \str -> case parseFicsMessage str of
  Left _    -> yield $ TextMessage $ BS.unpack str
  Right cmd -> yield cmd


unblockC :: Conduit BS.ByteString (StateT HelperState IO) BS.ByteString
unblockC = awaitForever $ \block -> case ord $ BS.head block of
  21 -> if readId block `elem` [
       1 -- game move
    , 10 -- Abort
    , 11 -- Accept (Draw, Abort)
    , 33 -- Decline (ie match offer)
    , 34 -- Draw (Mutual)
    , 80 -- Observing
    , 103 -- Resign
    , 155 -- Seek (Users' seek matches a seek already posted)
    , 158 -- Play (User selects a seek)
    , 56 -- BLK_ISET, seekinfo set.
    ]
    then sourceList (lines $ crop block) else yield block
  _ -> yield block
  where
    crop :: BS.ByteString -> BS.ByteString
    crop bs = let lastIdx = maybe 0 (+1) (BS.elemIndexEnd (chr 22) bs) in
              BS.drop lastIdx $ BS.init bs

    lines :: BS.ByteString -> [BS.ByteString]
    lines bs = fmap (`BS.snoc` '\n') $ init $ filter (not . BS.null) $ BS.split '\n' bs

    readId :: BS.ByteString -> Int
    readId = read . BS.unpack . BS.takeWhile (/= chr 22) . BS.tail . BS.dropWhile (/= chr 22)


blockC :: BS.ByteString -> Conduit BS.ByteString (StateT HelperState IO) BS.ByteString
blockC block = awaitForever $ \line -> case ord $ BS.head line of
  21 -> blockC line
  23 -> yield (block `BS.append` line) >> blockC BS.empty -- ^ don't ever delete this again!
  _ | BS.null block -> unless (line `elem` ignore) $ yield line
    | otherwise -> blockC $ block `BS.append` line
  where ignore = [BS.pack "\n", BS.pack "\a\n"]


-- remember!! "fics% \NAK4\SYN87\SYNThere are no offers pending to other players."
linesC :: Conduit BS.ByteString (StateT HelperState IO) BS.ByteString
linesC = awaitForever $ sourceList . filter (not . BS.null) . fmap dropPrompt . BS.split '\r'


dropPrompt :: BS.ByteString -> BS.ByteString
dropPrompt line
  | BS.take promptSz line == prompt = BS.drop promptSz line
  | otherwise = line
  where prompt = BS.pack "fics% "
        promptSz = BS.length prompt


fileLoggerC :: Conduit BS.ByteString (StateT HelperState IO) BS.ByteString
fileLoggerC = awaitForever $ \chunk -> do
   config <- config `fmap` get
   when (file (logging config) && isJust (directory config)) $
     liftIO $ logFile (fromJust $ directory config) chunk
   yield chunk


toGameResult :: Move -> FicsMessage
toGameResult move = GameResult id reason result
  where (id, reason, result) = toGameResultTuple move


printCmdMsg :: FicsMessage -> IO ()
printCmdMsg NewSeek {} = return ()
printCmdMsg RemoveSeeks {} = return ()
printCmdMsg cmd = print cmd


logFile :: FilePath -> BS.ByteString -> IO ()
logFile path chunk = do
  date <- fmap (formatTime Data.Time.defaultTimeLocale "%Y-%m-%d_") getZonedTime
  dateTime <- fmap (formatTime Data.Time.defaultTimeLocale "%Y-%m-%d %H-%M-%S: ") getZonedTime
  appendFile (path </> date ++ "macbeth.log") $
    (foldr (.) (showString $ "\n" ++ dateTime) $ fmap showLitChar (BS.unpack chunk)) ""