packages feed

esotericbot-0.0.1: Esotericbot/Config.hs

{-# OPTIONS -XMagicHash #-}
module Esotericbot.Config where

import Control.Applicative

import Data.Tuple.Select
import Data.Tuple.Update
import Data.Attoparsec as A
import Data.List.Stream as L
import Data.Either
import qualified Data.ByteString.Lazy.Char8 as BSC
import qualified Data.ByteString.Lazy as BS
import Data.Maybe

import System.Posix.Types

import Control.Monad.Stream as C

import Esotericbot.BSUtils
import Esotericbot.EBTypes

data Var =
   SV SimpleVar
   | ListVar BS.ByteString [ BS.ByteString ]
   | ComplexVar BS.ByteString [ SimpleVar ]
   deriving ( Show , Eq )

data SimpleVar =
   SimpleVar BS.ByteString BS.ByteString
   deriving ( Show , Eq )
   
data ConfVar =
   Nick BS.ByteString
   | Pass BS.ByteString
   | Chans [ BS.ByteString ]
   | Server String
   | User CUid
   | MemLimit Integer
   | TimeLimit Integer -- where it is termed
   | HardTimeLimit Integer -- where it is killed
   | ChildrenMemLimit Integer
   | DefaultChildMemLimit Integer
   | OutputLimit Integer
   | Chroot String
   | CommandPrefix BS.ByteString
   | L Plugin
   deriving Show

-- Could this be done with pattern regex?
( Nick _ ) `same_var` ( Nick _ ) = True
( Pass _ ) `same_var` ( Pass _ ) = True
( Chans _ ) `same_var` ( Chans _ ) = True
( Server _ ) `same_var` ( Server _ ) = True
( User _ ) `same_var` ( User _ ) = True
( MemLimit _ ) `same_var` ( MemLimit _ ) = True
( TimeLimit _ ) `same_var` ( TimeLimit _ ) = True
( HardTimeLimit _ ) `same_var` ( HardTimeLimit _ ) = True
( ChildrenMemLimit _ ) `same_var` ( ChildrenMemLimit _ ) = True
( DefaultChildMemLimit _ ) `same_var` ( DefaultChildMemLimit _ ) = True
( OutputLimit _ ) `same_var` ( OutputLimit _ ) = True
( Chroot _ ) `same_var` ( Chroot _ ) = True
( L l1 ) `same_var` ( L l2 ) = prefix l1 == prefix l2
( CommandPrefix _ ) `same_var` ( CommandPrefix _ ) = True
_ `same_var` _ = False


type LangBuilder =
   ( Bool , Maybe String , Maybe BS.ByteString , Maybe BS.ByteString , Maybe Integer , Maybe BS.ByteString , Maybe BS.ByteString )

read_conf :: FilePath -> IO SmallBotConf
read_conf fp = do
   f <- BS.readFile fp
   let ( bs , vars ) = parse ( many $ conf_var ) f
   if BS.null bs
      then
         either ( error . (L.++) ( fp L.++ ": " ) . show )
                ( \ mlex_vars -> do
                   econfs <- C.mapM to_conf_var $ catMaybes mlex_vars
                   let leconfs = lefts econfs
                   if L.null leconfs
                      then
                        to_conf $ rights econfs
                      else do
                         putStrLn "Unknown settings: "             
                         C.mapM print leconfs
                         error "Invalid config file"
                )
                vars
      else do
         either print
                ( const $ return ( ) )
                vars
         error $ "Parse error in config file before:\n" L.++ ( L.take 50 $ bs2s bs ) L.++ "..."

to_conf conf_vars = do
   if contains_duplicates conf_vars -- are there any duplicates?
      then
         error "Configuration contains duplicate options!"
      else 
         maybe ( return $ L.foldl add_conf_var initial_state conf_vars )
               error
               valid_conf
   where
   is_default_child_mem_limit =
      same_var $ DefaultChildMemLimit undefined -- here, undefined should never be evaluated
   is_children_mem_limit =
      same_var $ ChildrenMemLimit undefined 
   -- perhaps weirdly, returning Nothing here means that the conf _is_ valid!
   valid_conf :: Maybe String
   valid_conf =
      let children_mem_test = 
             if contains_children_max_mem_limit
                then
                   if contains_child_default_mem_limit && contains_children_max_mem_limit
                      then Nothing
                      else Just "Configuration has 'children_mem_limit' but no 'default_child_mem_limit'\n( To limit the memory of all child processes, the esotericbot must know how much each process may be assigned - though admitedly, this is redundant if you manually specify the memory use for each plugin. )"
                else Nothing
          mandatory_ops_test =
             if contains_nick && contains_server
                then Nothing
                else Just "Configuration file must define _at_least_ the `nick` variable and the `server` variable.  See the README."
      in
         children_mem_test <|> mandatory_ops_test
   contains_nick = L.any is_nick conf_vars
   contains_server = L.any is_server conf_vars
   is_nick = same_var $ Nick undefined 
   is_server = same_var $ Server undefined
   contains_child_default_mem_limit = L.any is_default_child_mem_limit conf_vars
   contains_children_max_mem_limit = L.any is_children_mem_limit conf_vars
   add_conf_var st conf_var =
      case conf_var of
         Nick n ->
            st { nick = n }
         Pass p ->
            st { pass = p }
         Chans c ->
            st { chans = c }
         Server s ->
            st { server = s }
         Chroot ch ->
            st { chroot = Just ch }
         User u -> 
            st { user = Just u }
         MemLimit ml ->
            st { mmem_limit = Just ml }
         ChildrenMemLimit cml ->
            st { mchildren_mem_limit = Just cml }
         TimeLimit tl ->
            st { mtime_limit = Just tl }
         HardTimeLimit htl ->
            st { mhard_time_limit = Just htl }
         OutputLimit ol ->
            st { moutput_limit = Just $ fromIntegral ol }
         L plugin ->
            st { plugins = plugin : plugins st }
         DefaultChildMemLimit i ->
            st { default_child_mem_limit = Just i }
         CommandPrefix p ->
            st { command_prefix = p }

contains_duplicates l =
   L.any ( (<) 1 . L.length ) $ L.map ( \ i -> L.findIndices ( same_var i ) l ) l

to_conf_var var = do
   case var of
      SV sv@( SimpleVar var_name value ) -> do
         let simple_confs_unbox :: [ IO BS.ByteString ]
             simple_confs_unbox =
                [ 
                ls2bs 4 "nick"#
                , ls2bs 4 "pass"#
                , ls2bs 6 "server"#
                , ls2bs 6 "chroot"#
                , ls2bs 12 "command_prefix"#
                ]
             simple_conf_vals =
                [
                Nick value
                , Pass value
                , Server $ BSC.unpack value
                , Chroot $ BSC.unpack value
                , CommandPrefix value
                ]
             numeric_confs_unbox =
                [
                ls2bs 9 "mem_limit"#
                , ls2bs 10 "time_limit"#
                , ls2bs 15 "hard_time_limit"#
                , ls2bs 18 "children_mem_limit"#
                , ls2bs 12 "output_limit"#
                , ls2bs 23 "default_child_mem_limit"#
                , ls2bs 4 "user"#
                ]
             numeric_conf_vals i =
                [ MemLimit i
                , TimeLimit i
                , HardTimeLimit i
                , ChildrenMemLimit i
                , OutputLimit i
                , DefaultChildMemLimit i
                , User $ fromIntegral i
                ]
         simple_conf_names <- C.sequence simple_confs_unbox
         numeric_conf_names <- C.sequence numeric_confs_unbox
         let simple_confs :: [ ( BS.ByteString , ConfVar ) ] 
             simple_confs = L.zip simple_conf_names simple_conf_vals
             numeric_confs_waiting :: Integer -> [ ( BS.ByteString , ConfVar ) ]
             numeric_confs_waiting = L.zip numeric_conf_names . numeric_conf_vals
         return $ find_conf sv simple_confs numeric_confs_waiting
      lv@( ListVar var_name vars ) -> do
         cs <- ls2bs 5 "chans"#
         return $ if var_name == cs
                     then Right $ Chans vars
                     else Left lv
      cv@( ComplexVar var_name simple_vars ) -> do
         lang <- ls2bs 6 "plugin"#
         if var_name == lang
            then do
               cmd <- ls2bs 3 "cmd"#
               name <- ls2bs 4 "name"#
               prefix <- ls2bs 6 "prefix"#
               input_sep <- ls2bs 9 "input_sep"#
               memuse <- ls2bs 9 "mem_limit"#
               shortcut <- ls2bs 8 "shortcut"#
               let lang_table :: [ ( BS.ByteString , BS.ByteString -> LangBuilder -> LangBuilder ) ]
                   lang_table =
                      [ 
                      ( name , \ value -> with_failure_test sel6 $ upd6 $ Just value ) 
                      , ( cmd , \ value -> with_failure_test sel2 $ upd2 $ Just $ bs2s value )
                      , ( prefix , \ value -> with_failure_test sel3 $ upd3 $ Just value )
                      , ( input_sep , \ value -> with_failure_test sel4 $ upd4 $ Just value )
                      , ( memuse , \ value st -> maybe ( fail st ) ( \ ( i , bs ) -> with_failure_test sel5 ( upd5 $ Just i ) st ) $ BSC.readInteger value )
                      , ( shortcut , \ value -> with_failure_test sel7 $ upd7 $ Just value ) 
                      ]
                   lang_builder :: LangBuilder
                   lang_builder = ( False , Nothing , Nothing , Nothing , Nothing , Nothing , Nothing )
                   with_failure_test f g st =
                      if sel1 st
                         then st
                         else maybe ( g st ) ( const $ fail st ) $ f st
                   fail :: LangBuilder -> LangBuilder
                   fail = upd1 True
                   vars :: LangBuilder
                   vars = L.foldl ( \ lb ( SimpleVar name val ) ->
                                     maybe ( fail lb )
                                           ( \ f -> f val lb )
                                           $ L.lookup name lang_table
                                  ) lang_builder simple_vars
               case vars of
                  ( False , Just cmd , Just prefix , minput_sep , mmem_use , Just name , shortcut ) ->
                     return $ Right $ L $ Plugin cmd prefix minput_sep mmem_use name shortcut
                  _ ->
                    return $ Left cv 
            else
               return $ Left cv
   where
   find_conf :: SimpleVar -> [ ( BS.ByteString , ConfVar ) ] -> ( Integer -> [ ( BS.ByteString , ConfVar ) ] ) -> Either Var ConfVar
   find_conf sv@( SimpleVar var_name value ) simple_confs numeric_confs_waiting = do
      maybe ( Left $ SV sv )
            Right
            $ L.lookup var_name simple_confs <|> do ( i , _ ) <- BSC.readInteger value
                                                    let numeric_confs = numeric_confs_waiting i
                                                    L.lookup var_name numeric_confs


conf_var =
   try ( comment <?> "Comment" ) 
       <|>
       ( try ( fmap Just l_var <?> "List var" )
             <|>
             ( try ( complex_var <?> "Complex var" ) 
                   <|>
                   ( ( fmap ( Just . SV ) simple_var ) <?> "Simple var" )
             ) 
       )

comment = do
   word8 45
   word8 45 
   takeTill $ (==) 10 
   spaces
   return Nothing

label c =
   A.takeWhile $ flip L.notElem $ c : space_word8s

l_var = do
   spaces
   var_name <- label 61
   spaces
   word8 61
   spaces
   word8 91
   elems <- sepBy1 list_elem $ do spaces
                                  word8 44
   spaces
   word8 93
   spaces
   return $ ListVar var_name elems

list_elem = do
   spaces
   word8 34
   e <- takeTill $ (==) 34
   word8 34
   return e

simple_var = do
   spaces
   var_name <- label 61
   spaces
   word8 61
   spaces
   word8 34
   value <- takeTill $ (==) 34
   word8 34
   spaces
   return $ SimpleVar var_name value

complex_var = do
   spaces
   var_name <- label 123 
   spaces
   word8 123
   spaces
   simple_vars <- fmap lefts $ many $ eitherP simple_var comment
   spaces
   word8 125
   spaces
   return $ Just $ ComplexVar var_name simple_vars