fastirc-0.2.0: Network/FastIRC/Messages.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module: Network.FastIRC.Messages
-- Copyright: (c) 2010 Ertugrul Soeylemez
-- License: BSD3
-- Maintainer: Ertugrul Soeylemez <es@ertes.de>
-- Stability: alpha
--
-- Parser and printer for IRC messages.
module Network.FastIRC.Messages
( -- * IRC messages
Message(..),
messageParser,
readMessage,
showMessage,
-- * IRC commands
Command(..),
commandParser,
showCommand
)
where
import qualified Data.ByteString.Char8 as B
import qualified Data.Map as M
import qualified Data.Set as S
import Control.Applicative
import Control.Monad
import Data.Attoparsec.Char8 as P hiding (many)
import Data.Char
import Data.Map (Map)
import Data.Maybe
import Data.Set (Set)
import Network.FastIRC.Types
import Network.FastIRC.Users
import Network.FastIRC.Utils
import Text.Printf
-- | Data type for IRC messages.
data Message =
Message {
msgOrigin :: !(Maybe UserSpec), -- ^ Message origin (user/server).
msgCommand :: !Command -- ^ Message command or numeric.
}
deriving (Eq, Read, Show)
-- | Data type for IRC commands.
data Command
= StringCmd CommandName [CommandArg] -- ^ Arbitrary string command.
| NumericCmd Integer [CommandArg] -- ^ Arbitrary numeric command.
| JoinCmd (Map ChannelName (Maybe ChannelKey))
| KickCmd (Set ChannelName) (Set NickName) (Maybe CommandArg)
| ModeCmd (Maybe (TargetName, CommandArg, [CommandArg]))
| NickCmd NickName (Maybe Int)
| NoticeCmd (Set TargetName) CommandArg
| PartCmd (Set ChannelName) (Maybe CommandArg)
| PassCmd CommandArg
| PingCmd ServerName (Maybe ServerName)
| PongCmd ServerName (Maybe ServerName)
| PrivMsgCmd (Set TargetName) CommandArg
| QuitCmd (Maybe CommandArg)
| TopicCmd ChannelName (Maybe CommandArg)
| UserCmd UserName CommandArg CommandArg CommandArg
deriving (Eq, Read, Show)
-- | Parser for IRC commands and their arguments.
commandParser :: Parser Command
commandParser =
try numCmd <|>
stringCmd
where
cmdArg :: Parser CommandArg
cmdArg = do
skipMany1 (char ' ')
try lastArg <|> takeWhile1 isIRCTokChar
where
lastArg :: Parser CommandArg
lastArg = char ':' *> P.takeWhile isMessageChar
commaArg :: Parser (Set CommandArg)
commaArg = S.filter (not . B.null) . S.fromList . B.split ',' <$> cmdArg
intArg :: Parser (Maybe Int)
intArg = option Nothing (fmap fst . B.readInt <$> cmdArg)
joinCmd :: Parser Command
joinCmd = do
channels <- B.split ',' <$> cmdArg
keys <- option [] $ B.split ',' <$> cmdArg
many cmdArg
return . JoinCmd . M.fromList $
zip channels (map Just keys ++ repeat Nothing)
numCmd :: Parser Command
numCmd = NumericCmd <$> decimal <*> many cmdArg
optArg :: Parser (Maybe CommandArg)
optArg = option Nothing (Just <$> cmdArg)
stringCmd :: Parser Command
stringCmd = do
cmd <- B.map toUpper <$> takeWhile1 isCommandChar
case cmd of
"JOIN" -> joinCmd
"KICK" -> KickCmd <$> commaArg <*> commaArg <*> optArg <* many cmdArg
"MODE" ->
try ((\a b c -> ModeCmd (Just (a,b,c)))
<$> cmdArg
<*> cmdArg
<*> many cmdArg)
<|> (many cmdArg >>= guard . null >> pure (ModeCmd Nothing))
"NICK" -> NickCmd <$> cmdArg <*> intArg <* many cmdArg
"NOTICE" -> NoticeCmd <$> commaArg <*> cmdArg <* many cmdArg
"PART" -> PartCmd <$> commaArg <*> optArg <* many cmdArg
"PASS" -> PassCmd <$> cmdArg <* many cmdArg
"PING" -> PingCmd <$> cmdArg <*> optArg <* many cmdArg
"PONG" -> PongCmd <$> cmdArg <*> optArg <* many cmdArg
"PRIVMSG" -> PrivMsgCmd <$> commaArg <*> cmdArg <* many cmdArg
"QUIT" -> QuitCmd <$> optArg <* many cmdArg
"TOPIC" -> TopicCmd <$> cmdArg <*> optArg <* many cmdArg
"USER" -> UserCmd <$> cmdArg <*> cmdArg <*> cmdArg <*> cmdArg <* many cmdArg
_ -> StringCmd cmd <$> many cmdArg
-- | Parser for IRC messages.
messageParser :: Parser Message
messageParser =
Message <$> option Nothing (Just <$> try userSpec)
<*> commandParser
where
userSpec :: Parser UserSpec
userSpec = char ':' *> userParser <* skipMany1 (char ' ')
-- | Run the 'messageParser' parser.
readMessage :: MsgString -> Maybe Message
readMessage = parseComplete messageParser
-- | Turn a 'Command' into a 'B.ByteString'. If you need to specify an
-- origin for the command, you should use 'Message' together with
-- 'showMessage'.
showCommand :: Command -> MsgString
showCommand cmd =
case cmd of
StringCmd cmdStr args -> B.append cmdStr (showArgs args)
NumericCmd cmdNum args ->
B.append (B.pack . printf "%03i" $ cmdNum)
(showArgs args)
JoinCmd channels ->
case formatJoins channels of
(chanList, "") -> "JOIN" +-+ [chanList]
(chanList, keyList) -> "JOIN" +-+ [chanList, keyList]
KickCmd channels nicks Nothing ->
"KICK" +-+ [commaList channels, commaList nicks]
KickCmd channels nicks (Just reason) ->
"KICK" +-+ [commaList channels, commaList nicks, reason]
ModeCmd Nothing -> "MODE"
ModeCmd (Just (target, mode, args)) ->
"MODE" +-+ [target, mode] ++ args
NickCmd nick (Just hc) -> "NICK" +-+ [nick, B.pack (show hc)]
NickCmd nick Nothing -> "NICK" +-+ [nick]
NoticeCmd targets text -> "NOTICE" +-+ [commaList targets, text]
PartCmd chans Nothing -> "PART" +-+ [commaList chans]
PartCmd chans (Just reason) ->
"PART" +-+ [commaList chans, reason]
PassCmd pwd -> "PASS" +-+ [pwd]
PingCmd srv1 Nothing -> "PING" +-+ [srv1]
PingCmd srv1 (Just srv2) -> "PING" +-+ [srv1, srv2]
PongCmd srv1 Nothing -> "PONG" +-+ [srv1]
PongCmd srv1 (Just srv2) -> "PONG" +-+ [srv1, srv2]
PrivMsgCmd targets text -> "PRIVMSG" +-+ [commaList targets, text]
QuitCmd Nothing -> "QUIT" +-+ []
QuitCmd (Just reason) -> "QUIT" +-+ [reason]
TopicCmd channel Nothing -> "TOPIC" +-+ [channel]
TopicCmd channel (Just newTopic) ->
"TOPIC" +-+ [channel, newTopic]
UserCmd user vhost vport realName ->
"USER" +-+ [user, vhost, vport, realName]
where
(+-+) :: B.ByteString -> [B.ByteString] -> B.ByteString
cmd +-+ args = B.append cmd (showArgs args)
infix 4 +-+
formatJoins :: Map ChannelName (Maybe ChannelKey) ->
(CommandArg, CommandArg)
formatJoins channels = (chanList, keyList)
where
(withKey, withoutKey) = M.partition isJust channels
chanWithKeyAssocs = M.assocs withKey
chanList = B.intercalate "," $ map fst chanWithKeyAssocs ++
M.keys withoutKey
keyList = B.intercalate "," $ map (fromJust . snd) chanWithKeyAssocs
commaList :: Set CommandArg -> CommandArg
commaList = B.intercalate "," . S.toList
showArgs :: [CommandArg] -> MsgString
showArgs [] = B.empty
showArgs [arg]
| B.null arg = " :"
| B.head arg == ':' = B.append " :" arg
| B.elem ' ' arg = B.append " :" arg
| otherwise = B.cons ' ' arg
showArgs (arg:args) =
B.append (B.cons ' ' arg) (showArgs args)
-- | Turn a 'Message' into a 'B.ByteString'.
showMessage :: Message -> MsgString
showMessage (Message origin cmd) =
case origin of
Nothing -> showCommand cmd
Just o ->
B.append (':' `B.cons` showUserSpec o)
(' ' `B.cons` showCommand cmd)