packages feed

tellbot-0.5.1.2: src/Main.hs

import Control.Concurrent ( threadDelay )
import Control.Exception ( SomeException, try )
import Control.Error
import Control.Monad
import Control.Monad.Identity
import Control.Monad.Trans
import Control.Monad.Trans.RWS
import Data.Bifunctor ( bimap, second )
import Data.Char ( toLower )
import Data.List ( intersperse )
import Data.List.Split ( chunksOf, splitOn )
import Data.Foldable ( traverse_ )
import Data.Time.Clock ( getCurrentTime, utctDay )
import Data.Version
import qualified Data.Map as M
import HTML
import Network
import Text.Regex.Posix ( (=~) )
import System.Environment ( getArgs )
import System.IO

version :: Version
version = Version [0,5,1,2] ["Apfelschorle"]

type Failable   = EitherT String Identity
type FailableIO = EitherT String IO
type Server     = String
type Chan       = String
type Session    = RWST ConInfo () Stories IO

data ConInfo = ConInfo {
    -- server host
    conHost   :: String
    -- chan
  , conChan   :: String
    -- our nick
  , conNick   :: String
    -- handle
  , conHandle :: Handle
    -- admin password
  , conPwd    :: String
  }

newtype Nick = Nick { unNick :: String }

instance Eq Nick where
  Nick a == Nick b = map toLower a == map toLower b

instance Show Nick where
  show (Nick a) = map toLower a

-- for each individual dudes, keep a list of stories to tell
type Stories = M.Map String [String]

ircPort :: Int
ircPort = 6667

floodThreshold :: Int
floodThreshold = 3

floodDelay :: Int
floodDelay = 500000 -- 500ms

reconnectDelay :: Int
reconnectDelay = 1000000 -- 1s

session :: ConInfo -> Session a -> IO a
session cinfo s = do
    (a,_,_) <- runRWST s cinfo M.empty
    return a

toIRC :: String -> Session ()
toIRC msg = asks conHandle >>= lift . flip hPutStrLn msg

fromIRC :: Session String
fromIRC = asks conHandle >>= lift . hGetLine

msgIRC :: String -> String -> Session ()
msgIRC to msg = toIRC $ "PRIVMSG " ++ to ++ " :" ++ msg

noticeIRC :: String -> String -> Session ()
noticeIRC to msg = toIRC $ "NOTICE " ++ to ++ " :" ++ msg

runFailable :: EitherT e Identity a -> Either e a
runFailable = runIdentity . runEitherT

runFailableIO :: EitherT e IO a -> IO (Either e a)
runFailableIO = runEitherT

main :: IO ()
main = do
    hSetBuffering stdout NoBuffering
    hSetBuffering stderr NoBuffering

    putStrLn . showVersion $ version
    args <- getArgs
    runFailableIO (start args) >>= either errLn return

getChan :: [String] -> Failable (Server,Chan,String,String)
getChan args = do
    unless ( length args == 4 ) . left $
      "expected server host, chan, nick and admin password"
    let [host,chan,nick,pwd] = args
    return (host,chan,nick,pwd)

start :: [String] -> FailableIO ()
start args = do
    (serv,chan,nick,pwd) <- hoistEither . runFailable $ getChan args
    liftIO . withSocketsDo $ connectIRC serv chan nick pwd

connectIRC :: Server -> Chan -> String -> String -> IO ()
connectIRC serv chan nick pwd = do
      putStrLn $ "connecting to " ++ serv
      eitherCon <- try $ do
        h <- connectTo serv (PortNumber . fromIntegral $ ircPort)
        hSetBuffering h NoBuffering
        session (ConInfo serv chan nick h pwd) $ do
          initIRC nick
          openChan
          ircSession
      either reconnect (const $ return ()) eitherCon
  where
    reconnect :: SomeException -> IO ()
    reconnect e = do
        err (show e)
        threadDelay reconnectDelay
        connectIRC serv chan nick pwd

initIRC :: String -> Session ()
initIRC nick = do
    toIRC "USER a b c :d"
    toIRC $ "NICK " ++ nick

openChan :: Session ()
openChan = do
    chan <- asks conChan
    liftIO . putStrLn $ "joining " ++ chan
    joinChan

joinChan :: Session ()
joinChan = asks conChan >>= toIRC . ("JOIN "++)

quitChan :: Session ()
quitChan = toIRC "QUIT"

ircSession :: Session ()
ircSession = forever $ fromIRC >>= onContent . purgeContent

purgeContent :: String -> String
purgeContent = filter (\c -> not $ c `elem` "\n\r")

onContent :: String -> Session ()
onContent c = do
    liftIO (putStrLn c)
    treat
  where
    tailC = tail c
    treat
        | isMsg c   = treatMsg tailC
        | isJoin c  = treatJoin tailC
        | isKick c  = treatKick c
        | isPing c  = treatPing c
        | otherwise = return ()

-- FIXME: those functions are not really safe and might be flaws
isPing :: String -> Bool
isPing c = "PING" `elem` words c

isMsg :: String -> Bool
isMsg c = "PRIVMSG" `elem` words c

isJoin :: String -> Bool
isJoin c = "JOIN" `elem` words c

isKick :: String -> Bool
isKick c = "KICK" `elem` words c

treatPing :: String -> Session ()
treatPing ping = do
    toIRC pong
  where
    pong        = "PONG" ++ numericPing
    numericPing = snd . break (==' ') $ ping

treatMsg :: String -> Session ()
treatMsg msg = do
    nick <- asks conNick
    chan <- asks conChan
    liftIO . putStrLn $ "from: " ++ fromNick ++ ", to: " ++ to ++ ": " ++ content
    unless (null content || Nick fromNick == Nick nick) $ do
      tellStories fromNick
      let url = extractUrl content
      unless (null url) $ do
        title <- liftIO $ htmlTitle url
        traverse_ (\t -> msgIRC chan $ "« " ++ t ++ " »") title
      when (head content == '!') $ do
        onCmd fromNick to (tail content)
  where
    (fromNick,to,content) = emitterRecipientContent msg

extractUrl :: String -> String
extractUrl = (=~ "https?://[^ ]+")

treatJoin :: String -> Session ()
treatJoin _ = do
  return ()
{-
    nick <- asks conNick
    unless (from == nick) $ tellStories from
  where
    (from,to,_) = emitterRecipientContent msg
-}

treatKick :: String -> Session ()
treatKick msg = do
    nick <- asks conNick
    chan <- asks conChan
    when (kicked == nick) $ do
      liftIO . putStrLn $ "woah, I was kicked by " ++ from
      joinChan
      msgIRC chan $ from ++ ": you sonavabitch."
  where
    (from',_,content) = emitterRecipientContent msg
    from               = tailSafe from'
    kicked             = tailSafe $ dropWhile (/=':') content

-- Extract the emitter, the recipient and the message.
emitterRecipientContent :: String -> (String,String,String)
emitterRecipientContent msg = (from,to,content)
  where
    (from':_:to:content') = splitOn " " msg
    from                  = fst . break (=='!') $ from'
    content               = tailSafe (unwords content')

onCmd :: String -> String -> String -> Session ()
onCmd from to msg = do
    liftIO . putStrLn $ "searching command " ++ cmd ++ ": " ++ show found
    maybe unknownCmd treatCmd (M.lookup cmd commands)
  where
    (cmd,arg)  = second tailSafe . break (==' ') $ msg
    unknownCmd = return ()
    treatCmd c = c from to arg
    found = cmd `M.member` commands

commands :: M.Map String (String -> String -> String -> Session ())
commands = M.fromList
    [
      ("tell",tellCmd)
    , ("do",doCmd)
    , ("help",helpCmd)
    ]

tellCmd :: String -> String -> String -> Session ()
tellCmd from _ arg = do
    chan <- asks conChan
    treat chan
  where
    treat chan
        | length arg > 1 && not (null msg) = do
          nick <- fmap Nick (asks conNick)
          if fromNick == nick then
            msgIRC chan "I'll tell myself for sure pal!"
            else do
              -- FIXME: issue #2
              {-
              userPresent <- do
                toIRC $ "NAMES " ++ chan
                names <- (filter $ \c -> not $ c `elem` "?@!#:") `liftM` fromIRC
                liftIO . putStrLn $ "names: " ++ names
                return (fromNick `elem` words names)
              if userPresent then
                msgIRC chan "don't waste my time; that folk's here"
                else do
              -}
                  now <- liftIO $ utctDay `liftM` getCurrentTime
                  modify . M.insertWith (flip (++)) (show fromNick) $
                    [show now ++ ", " ++ from ++ " told " ++ (unNick fromNick) ++ ": " ++ msg]
                  msgIRC from "\\_o<"
        | otherwise = msgIRC chan "nope!"
    (fromNick,msg) = bimap Nick tailSafe . break (==' ') $ arg

doCmd :: String -> String -> String -> Session ()
doCmd from to arg = do
    chan   <- asks conChan
    myNick <- asks conNick
    pwd    <- asks conPwd
    treatDo chan myNick pwd
  where
    treatDo chan myNick pwd
        | to == chan = msgIRC from "I'm sorry, I feel naked in public ;)"
        | Nick to == Nick myNick && length args >= 3 = executeDo chan pwd
        | otherwise = msgIRC from "huhu, something went terribly wrong!"
    args = words arg
    userPwd:action:actionParams = args
    executeDo chan pwd
        | pwd /= userPwd = msgIRC from "wrong password!"
        | otherwise = executeAction chan
    executeAction chan
        | action == "op"     = mapM_ (toIRC . (mode chan "+o"++)) actionParams
        | action == "deop"   = mapM_ (toIRC . (mode chan "-o"++)) actionParams
        | action == "say"    = msgIRC chan (unwords actionParams)
        | action == "kick"   = mapM_ (toIRC . (("KICK " ++ chan ++ " ")++)) actionParams
        | action == "notice" = noticeIRC chan (unwords actionParams)
        | otherwise = msgIRC from "unknown action"
    mode chan m = "MODE " ++ chan ++ " " ++ m ++ " "

helpCmd :: String -> String -> String -> Session ()
helpCmd from _ _ = do
    myNick <- asks conNick
    msgIRC from $ "!tell dest msg: leave a message to a beloved"
    msgIRC from $ "!do pwd action params: perform an action"
    msgIRC from $ "-   -   op user0 user1...: grant op privileges"
    msgIRC from $ "-   -   deop user0 user1...: revoke op privileges"
    msgIRC from $ "-   -   kick user0 user1...: kick them all!"
    msgIRC from $ "-   -   say blabla: make " ++ myNick ++ " say something"
    msgIRC from $ "-   -   notice msg: notice the channel something"
    msgIRC from . showVersion $ version
    msgIRC from $ "written in Haskell by phaazon"

-- FIXME: host & ident
tellStories :: String -> Session ()
tellStories nick = do
    stories <- gets (maybeToList . M.lookup (show $ Nick nick))
    let
      cstories = concat stories
      chunks = map (mapM_ $ msgIRC nick) . chunksOf floodThreshold $ cstories
      tells  = intersperse (liftIO $ threadDelay floodDelay) chunks
    unless (null stories) $ do
      sequence_ tells
      modify (M.delete . show $ Nick nick)