More refactoring of error handling and making better use of lenses

master
Levi Pearson 2014-03-01 17:11:16 -07:00
parent b6299f59ba
commit 8f5e224a8f
4 changed files with 188 additions and 167 deletions

View File

@ -3,16 +3,18 @@
module Pipes.IRC.Server.IrcMonad module Pipes.IRC.Server.IrcMonad
where where
import Control.Error
import Control.Lens import Control.Lens
import Control.Monad.RWS import Control.Monad.RWS
import Data.ByteString.Char8 (ByteString) import Data.ByteString.Char8 (ByteString)
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.Map as M import qualified Data.Map as M
import Data.Maybe (catMaybes, fromJust, isJust) import Data.Maybe (fromJust)
import qualified Data.Set as S import qualified Data.Set as S
import Pipes.IRC.Message.Types import Pipes.IRC.Message.Types
import Pipes.IRC.Server.Channel import Pipes.IRC.Server.Channel
import Pipes.IRC.Server.Server
import Pipes.IRC.Server.Types import Pipes.IRC.Server.Types
import Pipes.IRC.Server.User import Pipes.IRC.Server.User
@ -39,7 +41,11 @@ ppHostPreference hp = case hp of
-- * Monadic utilities -- * Monadic utilities
useChan :: ChanKey -> IrcMonad (Maybe IrcChannel) useChan :: ChanKey -> IrcMonad (Maybe IrcChannel)
useChan cname = fmap (M.lookup cname) $ use (clientServer . ircChannels) useChan cname = use $ clientServer . ircChannels . at cname
useUserChans :: NickKey -> IrcMonad (S.Set ChanKey)
useUserChans nn =
use (clientServer . ircUsers . at nn . traverse . userChannels)
useNick :: IrcMonad (Maybe NickKey) useNick :: IrcMonad (Maybe NickKey)
useNick = do useNick = do
@ -49,15 +55,6 @@ useNick = do
RegUser (NickName nn _ _) -> Just nn RegUser (NickName nn _ _) -> Just nn
_ -> Nothing _ -> Nothing
validateNick :: NickKey -> IrcMonad Bool
validateNick nickname = do
nickSet <- use $ clientServer . ircNicks
if S.member nickname nickSet
then do
tellNumeric err_nicknameinuse [nickname, ":Nickname is already in use."]
return False
else return True
channelTargets :: ChanKey -> IrcMonad [Int] channelTargets :: ChanKey -> IrcMonad [Int]
channelTargets chname = do channelTargets chname = do
srv <- use clientServer srv <- use clientServer
@ -128,8 +125,78 @@ allChanEcho iMsg = do
let chans = S.elems $ usr ^. userChannels let chans = S.elems $ usr ^. userChannels
chanEcho chans iMsg chanEcho chans iMsg
disconnectUser :: Int -> IrcMessage -> IrcMonad ()
disconnectUser cid msg = do
allChanEcho msg
tell [Close cid]
-- * Command validation utilities
type ErrParam = (IrcReply, [IrcParam])
type IrcMonadErr = EitherT ErrParam IrcMonad
tellErr :: Either (IrcReply, [IrcParam]) a -> IrcMonad ()
tellErr (Left (r, ps)) = void $ tellNumeric r ps
tellErr _ = return ()
runValidation :: IrcMonadErr () -> IrcMonad ()
runValidation = tellErr <=< runEitherT
ensure :: Bool -> IrcReply -> [IrcParam] -> IrcMonadErr ()
ensure p r ps = when p $ left (r, ps)
ensureUse :: IrcMonad (Maybe a) -> ErrParam -> IrcMonadErr a
ensureUse u e = lift u >>= hoistEither . note e
checkParamLength :: BS.ByteString -> [IrcParam] -> Int -> IrcMonadErr ()
checkParamLength cmd ps n =
ensure (length ps >= n)
err_needmoreparams [cmd, ":Need more parameters"]
checkSuppliedNickname :: [IrcParam] -> IrcMonadErr NickKey
checkSuppliedNickname ps = do
ensure (not . null $ ps) err_nonicknamegiven [":must supply a nickname"]
return (head ps)
checkRegistration :: IrcMonadErr NickKey
checkRegistration =
ensureUse useNick (err_notregistered, [":You have not registered"])
checkChannelPresence :: ChanKey -> IrcMonadErr IrcChannel
checkChannelPresence ckey =
ensureUse (useChan ckey) (err_badchannelkey, [ckey, ":No such channel"])
checkUserOnChan :: NickKey -> ChanKey -> IrcChannel -> IrcMonadErr ()
checkUserOnChan nn c ch =
ensure (chanHasUser nn ch) err_notonchannel [c, ":Not on channel"]
checkUserNotOnChan :: NickKey -> ChanKey -> IrcChannel -> IrcMonadErr ()
checkUserNotOnChan nn c ch =
ensure (not . chanHasUser nn $ ch) err_notonchannel [c, ":Already on channel"]
checkInvitation :: NickKey -> ChanKey -> IrcChannel -> IrcMonadErr ()
checkInvitation nn c ch = ensure (ircInviteCheck nn ch)
err_inviteonlychan [c, ":Cannot join channel (+i)"]
checkPassKey :: Maybe PassKey -> ChanKey -> IrcChannel -> IrcMonadErr ()
checkPassKey k c chan = ensure (ircPassCheck k chan)
err_badchannelkey [c, ":Cannot join channel (+k)"]
checkNickFree :: NickKey -> IrcMonadErr ()
checkNickFree nickname = do
nickSet <- lift (use $ clientServer . ircNicks)
ensure (S.member nickname nickSet)
err_nicknameinuse [nickname, ":Nickname is already in use."]
-- * Adding responses to the Writer portion of the monad -- * Adding responses to the Writer portion of the monad
renderQuitMsg :: Maybe BS.ByteString -> IrcMonad BS.ByteString
renderQuitMsg (Just msg) = return $ BS.append "Quit: " msg
renderQuitMsg Nothing = useNick >>= \nickname ->
return $ case nickname of
Just n -> BS.append "Quit: " n
Nothing -> ""
tellYOURHOST :: NickKey -> IrcMonad () tellYOURHOST :: NickKey -> IrcMonad ()
tellYOURHOST nickname = do tellYOURHOST nickname = do
srvname <- view ircHostName srvname <- view ircHostName

View File

@ -5,20 +5,17 @@ module Pipes.IRC.Server.MessageHandler
( ircMessageHandler ) ( ircMessageHandler )
where where
import Control.Applicative ((<$>))
import Control.Error import Control.Error
import Control.Lens import Control.Lens
import Control.Monad.RWS import Control.Monad.RWS
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.Map as M
import Data.Maybe
import Data.Set as S import Data.Set as S
import Pipes.IRC.Message.Types import Pipes.IRC.Message.Types
import Pipes.IRC.Server.Channel
import Pipes.IRC.Server.IrcMonad import Pipes.IRC.Server.IrcMonad
import Pipes.IRC.Server.Server import Pipes.IRC.Server.Server
import Pipes.IRC.Server.Types import Pipes.IRC.Server.Types
import Pipes.IRC.Server.User
import Pipes.IRC.Server.Util import Pipes.IRC.Server.Util
ircMessageHandler :: IrcMessage -> IrcMonad () ircMessageHandler :: IrcMessage -> IrcMonad ()
@ -42,155 +39,6 @@ unregHandler msg@IrcMessage{..} =
Left QUIT -> handleQUIT msg Left QUIT -> handleQUIT msg
_ -> return () _ -> return ()
unregPASS :: IrcMessage -> IrcMonad ()
unregPASS IrcMessage{..} =
if length params < 1
then tellNumeric err_needmoreparams ["PASS", ":Need more parameters"]
else do
clientReg . rcvdPass .= (Just $ head params)
return ()
unregNICK :: IrcMessage -> IrcMonad ()
unregNICK IrcMessage{..} =
if length params /= 1
then tellNumeric err_nonicknamegiven [":must supply a nickname"]
else let nickname = head params in
validateNick nickname >>= flip when
(clientReg . rcvdNick .= Just nickname >>
clientServer . ircNicks %= S.insert nickname >>
tryRegistration)
unregUSER :: IrcMessage -> IrcMonad ()
unregUSER IrcMessage{..} =
if length params < 4
then tellNumeric err_needmoreparams ["USER", ":need more parameters"]
else do
clientReg . rcvdName .= (Just $ head params)
tryRegistration
renderQuitMsg :: Maybe BS.ByteString -> IrcMonad BS.ByteString
renderQuitMsg (Just msg) = return $ BS.append "Quit: " msg
renderQuitMsg Nothing = useNick >>= \nickname ->
return $ case nickname of
Just nick -> BS.append "Quit: " nick
Nothing -> ""
handlePING :: IrcMessage -> IrcMonad ()
handlePING _ = do
srvname <- view ircHostName
tellCommand PONG [":" <> srvname]
handlePONG :: IrcMessage -> IrcMonad ()
handlePONG _ = do
cid <- use clientConn
tell [Pong cid]
-- JOIN, MODE, KICK, PART, QUIT and of course PRIVMSG/NOTICE need to be
-- echoed to channels that the user belongs to
doQuit :: IrcMessage -> Maybe BS.ByteString -> IrcMonad ()
doQuit msg quitParamIn = do
connId <- use clientConn
quitMsg <- renderQuitMsg quitParamIn
hostname <- use clientHost
let hoststr = fromMaybe "unknown hostname" hostname
let quitParam = BS.concat [ ":Closing Link: ", hoststr
, " (", quitMsg, ")"]
tellCommand ERROR [quitParam]
allChanEcho msg{params = [quitParam]}
tell [Close connId]
return ()
handleQUIT :: IrcMessage -> IrcMonad ()
handleQUIT msg@IrcMessage{..} =
doQuit msg $ case params of
[] -> Nothing
p:_ -> Just p
handleJOIN :: IrcMessage -> IrcMonad ()
handleJOIN msg@IrcMessage{..} =
case params of
[] -> tellNumeric err_needmoreparams ["JOIN", ":Not enough parameters"]
["0"] -> do
Just nn <- useNick
cs <- use $ clientServer . ircUsers . at nn
let Just chans = fmap (^. userChannels) cs
doPart msg{command=Left PART} (S.elems chans) Nothing
cs:[] -> doJoin msg $ zipParams (parseParamList cs) []
cs:ks:_ -> doJoin msg $ zipParams (parseParamList cs) (parseParamList ks)
handlePART :: IrcMessage -> IrcMonad ()
handlePART msg@IrcMessage{..} =
case params of
[] -> tellNumeric err_needmoreparams ["PART", ":Not enough parameters"]
cs:[] -> doPart msg (parseParamList cs) Nothing
cs:pm:_ -> doPart msg (parseParamList cs) (Just pm)
tellErr :: IrcReply -> [IrcParam] -> IrcMonadErr ()
tellErr r ps = lift (tellNumeric r ps) >> left []
checkRegistration :: IrcMonadErr BS.ByteString
checkRegistration = do
mNick <- lift useNick
when (isNothing mNick) $
tellErr err_notregistered [":You have not registered"]
right $ fromJust mNick
doJoin :: IrcMessage -> [(BS.ByteString, Maybe BS.ByteString)] -> IrcMonad ()
doJoin msg chans = forM_ chans $ \(c, k) -> runEitherT $ do
nick <- checkRegistration
mChan <- lift $ useChan c
when (isJust mChan) $ do
let chan = fromJust mChan
when (chanHasUser nick chan) $ left "already on channel"
when (ircInviteCheck nick chan) $
tellErr err_inviteonlychan [c, ":Cannot join channel (+i)"]
when (ircPassCheck k chan) $
tellErr err_badchannelkey [c, ":Cannot join channel (+k)"]
lift $ do
clientServer %= ircJoin nick c
tellTOPIC c
tellNAMES [c]
chanEcho [c] msg
doPart :: IrcMessage -> [BS.ByteString] -> Maybe BS.ByteString -> IrcMonad ()
doPart msg chans pmsg = forM_ chans $ \c -> runEitherT $ do
nn <- checkRegistration
mChan <- lift . use $ clientServer . ircChannels . at c
when (isNothing mChan) $ tellErr err_nosuchchannel [c, ":No such channel"]
let ch = fromJust mChan
unless (chanHasUser nn ch) $ tellErr err_notonchannel [c, ":Not on channel"]
lift $ do
let plist = case pmsg of
Just bs -> [c, BS.append ":" bs]
Nothing -> [c]
let newMsg = msg{params = plist}
chanEcho [c] newMsg
findReceivers [nn] >>= fwdMsgNoReplace msg
clientServer %= ircPart nn c
tryRegistration :: IrcMonad ()
tryRegistration = do
regState <- use clientReg
hostname <- use clientHost
case regState of
Unreg _ (Just nickname) (Just name) -> do
let nn = NickName nickname (Just name) hostname
clientReg .= RegUser nn
usr <- mkUser
clientServer . ircUsers %= M.insert nickname usr
tellWELCOME nickname
tellYOURHOST nickname
tellMOTD nickname
_ -> return ()
regHandler :: IrcMessage -> IrcMonad () regHandler :: IrcMessage -> IrcMonad ()
regHandler msg@IrcMessage{..} = do regHandler msg@IrcMessage{..} = do
pMsg <- addUserPrefix msg pMsg <- addUserPrefix msg
@ -205,6 +53,112 @@ regHandler msg@IrcMessage{..} = do
Left QUIT -> handleQUIT pMsg Left QUIT -> handleQUIT pMsg
_ -> return () _ -> return ()
-- * Handlers for messages coming from unregistered users
unregPASS :: IrcMessage -> IrcMonad ()
unregPASS IrcMessage{..} = runValidation $ do
checkParamLength "PASS" params 1
lift $ clientReg . rcvdPass ?= head params
unregNICK :: IrcMessage -> IrcMonad ()
unregNICK IrcMessage{..} = runValidation $ do
nickname <- checkSuppliedNickname params
checkNickFree nickname
lift $ do
clientReg . rcvdNick ?= nickname
clientServer . ircNicks . contains nickname .= True
tryRegistration
unregUSER :: IrcMessage -> IrcMonad ()
unregUSER IrcMessage{..} = runValidation $ do
checkParamLength "USER" params 4
lift $ do
clientReg . rcvdName ?= head params
tryRegistration
tryRegistration :: IrcMonad ()
tryRegistration = do
regState <- use clientReg
hostname <- use clientHost
case regState of
Unreg _ (Just nickname) (Just name) -> do
usr <- mkUser
clientReg .= (RegUser $ NickName nickname (Just name) hostname)
clientServer . ircUsers . at nickname ?= usr
tellWELCOME nickname
tellYOURHOST nickname
tellMOTD nickname
_ -> return ()
-- * Handlers for messages coming from registered users
handlePING :: IrcMessage -> IrcMonad ()
handlePING _ = do
srvname <- view ircHostName
tellCommand PONG [":" <> srvname]
handlePONG :: IrcMessage -> IrcMonad ()
handlePONG _ = do
cid <- use clientConn
tell [Pong cid]
-- JOIN, MODE, KICK, PART, QUIT and of course PRIVMSG/NOTICE need to be
-- echoed to channels that the user belongs to
handleQUIT :: IrcMessage -> IrcMonad ()
handleQUIT msg@IrcMessage{..} = do
connId <- use clientConn
hostname <- fromMaybe "unknown hostname" <$> use clientHost
quitMsg <- renderQuitMsg (headMay params)
let quitParam = mconcat [":Closing Link: ", hostname, " (", quitMsg, ")"]
tellCommand ERROR [quitParam]
disconnectUser connId msg{params = [quitParam]}
handleJOIN :: IrcMessage -> IrcMonad ()
handleJOIN msg@IrcMessage{..} = runValidation $ do
checkParamLength "JOIN" params 1
nn <- checkRegistration
lift $ case params of
["0"] -> do cs <- useUserChans nn
doPart msg{command=Left PART} (S.elems cs) Nothing
cs:[] -> doJoin msg $ zipParams (parseParamList cs) []
cs:ks:_ -> doJoin msg $ zipParams (parseParamList cs) (parseParamList ks)
handlePART :: IrcMessage -> IrcMonad ()
handlePART msg@IrcMessage{..} = runValidation $ do
checkParamLength "PART" params 1
lift $ case params of
cs:[] -> doPart msg (parseParamList cs) Nothing
cs:pm:_ -> doPart msg (parseParamList cs) (Just pm)
doJoin :: IrcMessage -> [(ChanKey, Maybe PassKey)] -> IrcMonad ()
doJoin msg chans = forM_ chans $ \(c, k) -> runValidation $ do
nick <- checkRegistration
chan <- checkChannelPresence c
checkUserNotOnChan nick c chan
checkInvitation nick c chan
checkPassKey k c chan
lift $ do
clientServer %= ircJoin nick c
tellTOPIC c
tellNAMES [c]
chanEcho [c] msg
doPart :: IrcMessage -> [BS.ByteString] -> Maybe BS.ByteString -> IrcMonad ()
doPart msg chans pmsg = forM_ chans $ \c -> runValidation $ do
nn <- checkRegistration
ch <- checkChannelPresence c
checkUserOnChan nn c ch
let newMsg = msg{params = c : maybe [] ((:[]) . BS.append ":") pmsg}
lift $ do
chanEcho [c] newMsg
rs <- findReceivers [nn]
fwdMsgNoReplace msg rs
clientServer %= ircPart nn c
handlePRIVMSG :: IrcMessage -> IrcMonad () handlePRIVMSG :: IrcMessage -> IrcMonad ()
handlePRIVMSG msg@IrcMessage{..} = do handlePRIVMSG msg@IrcMessage{..} = do
case params of case params of

View File

@ -117,7 +117,7 @@ ircInviteCheck n chan =
-- | Determine whether the given channel will disallow joining due to -- | Determine whether the given channel will disallow joining due to
-- a missing or incorrect password. A 'True' value indicates that -- a missing or incorrect password. A 'True' value indicates that
-- conditions for joining are met. -- conditions for joining are met.
ircPassCheck :: Maybe ByteString -- ^ password supplied by user ircPassCheck :: Maybe PassKey -- ^ password supplied by user
-> IrcChannel -- ^ channel to check -> IrcChannel -- ^ channel to check
-> Bool -- ^ may the user join? -> Bool -- ^ may the user join?
ircPassCheck k chan = ircPassCheck k chan =

View File

@ -22,6 +22,7 @@ import Pipes.Network.TCP (HostPreference (..), ServiceName,
type NickKey = ByteString type NickKey = ByteString
type ChanKey = ByteString type ChanKey = ByteString
type PassKey = ByteString
type IrcEvents = [IrcEvent] type IrcEvents = [IrcEvent]
@ -112,5 +113,4 @@ data ClientState =
} deriving (Show) } deriving (Show)
makeLenses ''ClientState makeLenses ''ClientState
type IrcMonad a = RWS IrcConfig IrcEvents ClientState a type IrcMonad = RWS IrcConfig IrcEvents ClientState
type IrcMonadErr a = EitherT String (RWS IrcConfig IrcEvents ClientState) a