packages feed

hog-0.1: src/Main.hs

{-# LANGUAGE DeriveDataTypeable, RecordWildCards, ScopedTypeVariables,
  ViewPatterns #-} 
{-# OPTIONS -Wall -fno-warn-missing-signatures -fno-warn-name-shadowing #-}

-- | Main entry point, just connect to the given IRC server and join
-- the given channels and log all messages received, to the given
-- file(s).

module Main where

import Control.Concurrent.Delay
import Control.Monad
import Control.Monad.Fix
import Data.Char
import Data.List
import Data.Time
import Network
import Network.IRC
import System.Console.CmdArgs
import System.FilePath
import System.IO
import System.Locale
import System.Posix

-- | Options for the executable.
data Options = Options
  { host      :: String       -- ^ The IRC server host/IP.
  , port      :: Int          -- ^ The remote port to connect to.
  , channels  :: String       -- ^ The channels to join (comma or
                              -- space sep'd).
  , logpath   :: FilePath     -- ^ Which directory to log to.
  , pass      :: Maybe String -- ^ Maybe a *server* password.
  , nick      :: String       -- ^ The nick.
  , user      :: String       -- ^ User (not real name) to use.
  , delay     :: Integer      -- ^ Reconnect delay (secs).
  } deriving (Show,Data,Typeable)

-- | Options for the executable.
options :: Options
options  = Options
  { host = def &= opt "irc.freenode.net" &= help "The IRC server."
  , port = def &= opt (6667::Int) &= help "The remote port."
  , channels = def &= opt "#hog" &= help "The channels to join."
  , logpath = def &= opt "." &= help "The directory to save log files."
  , pass = Nothing
  , nick = def &= opt "hog" &= help "The nickname to use."
  , user = def &= opt "hog" &= help "The user name to use."
  , delay = def &= opt (30::Integer) &= help "Reconnect delay (secs)."
  }
  &= summary "Hog IRC logger (C) Chris Done 2011"
  &= help "Simple IRC logger bot."

-- | Main entry point.
main :: IO ()
main = withSocketsDo $ do
  _ <- installHandler sigPIPE Ignore Nothing
  cmdArgs options >>= start

-- | Connect to the IRC server and start logging.
start :: Options -> IO ()
start options@Options{..} = do
  hSetBuffering stdout NoBuffering
  h <- connectTo host (PortNumber (fromIntegral port))
  hSetBuffering h NoBuffering
  register h options
  fix $ \repeat -> do
    line <- catch (Just `fmap` hGetLine h) $ \_e -> do
      delaySeconds delay
      start options
      return Nothing
    flip (maybe (return ())) line $ \line -> do
       putStrLn $ "<- " ++ line
       handleLine options h line
       repeat

-- | Register to the server by sending user/nick/pass/etc.
register :: Handle -> Options -> IO ()
register h Options{..} = do
  let send = sendLine h
  maybe (return ()) (send . ("PASS "++)) pass
  send $ "USER " ++ user ++ " * * *"
  send $ "NICK " ++ nick

-- | Handle incoming lines; ping/pong, privmsg, etc.
handleLine :: Options -> Handle -> String -> IO ()
handleLine options handle line = 
  case decode line of
    Nothing -> putStrLn $ "Unable to decode line " ++ show line
    Just msg -> handleMsg options handle msg
    
-- | Handle an IRC message.
handleMsg :: Options -> Handle -> Message -> IO ()
handleMsg options h msg =
  case msg_command msg of
    "PING" -> reply $ msg {msg_command="PONG"}
    "376"  -> joinChannels options h
    "PRIVMSG" -> logMsg options msg
    _ -> return ()
    
  where reply = sendLine h . encode
  
-- | Log a privmsg of a given channel to the right file.
logMsg :: Options -> Message -> IO ()
logMsg options@Options{..} msg@Message{..} = do
  case msg_params of
    [('#':chan),msg] -> logLine options chan from msg
    _ -> putStrLn $ "Bogus message: " ++ show msg
    
    where from = case msg_prefix of
                   Just (NickName str _ _) -> "<" ++ str ++ "> "
                   _ -> "unknown"

-- | Log a line of a channel.
logLine :: Options -> String -> String -> String -> IO ()
logLine Options{..} (sanitize -> chan) from line =
  when (not (null chan)) $ do
    now <- fmap format getCurrentTime
    appendFile (logpath </> chan) $ 
      now ++ " " ++ from ++ line ++ "\n"
      
  where format = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S %Z"

-- | Sanitize a channel name.
sanitize :: String -> String
sanitize = filter ok where
  ok c = isDigit c || elem (toLower c) ['a'..'z']

-- | Join the requested channels.
joinChannels :: Options -> Handle -> IO ()
joinChannels Options{..} h = do
  let chans = words $ replace ',' ' ' channels
  putStrLn $ "Joining channels: " ++ show chans
  sendLine h $ "JOIN :" ++ intercalate "," chans

-- | Replace x with y in xs.
replace :: Eq a => a -> a -> [a] -> [a]
replace c y = go where
    go [] = []
    go (c':cs) | c'==c     = y : go cs
               | otherwise = c' : go cs

-- | Send a line on a handle, ignoring errors (like, if the socket's
-- closed.)
sendLine :: Handle -> String -> IO ()
sendLine h line = do
  catch (do hPutStrLn h line; putStrLn $ "-> " ++ line) $
    \_e -> return ()