{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
module Irc.RawIrcMsg
(
RawIrcMsg(..)
, TagEntry(..)
, rawIrcMsg
, msgTags
, msgPrefix
, msgCommand
, msgParams
, parseRawIrcMsg
, renderRawIrcMsg
, prefixParser
, simpleTokenParser
, asUtf8
) where
import Control.Applicative
import Data.Attoparsec.Text as P
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as Builder
import Data.List
import Data.Maybe
import Data.Monoid
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Vector (Vector)
import qualified Data.Vector as Vector
import Irc.UserInfo
import View
data RawIrcMsg = RawIrcMsg
{ _msgTags :: [TagEntry]
, _msgPrefix :: Maybe UserInfo
, _msgCommand :: !Text
, _msgParams :: [Text]
}
deriving (Eq, Read, Show)
data TagEntry = TagEntry {-# UNPACK #-} !Text {-# UNPACK #-} !Text
deriving (Eq, Read, Show)
msgTags :: Functor f => ([TagEntry] -> f [TagEntry]) -> RawIrcMsg -> f RawIrcMsg
msgTags f m = (\x -> m { _msgTags = x }) <$> f (_msgTags m)
msgPrefix :: Functor f => (Maybe UserInfo -> f (Maybe UserInfo)) -> RawIrcMsg -> f RawIrcMsg
msgPrefix f m = (\x -> m { _msgPrefix = x }) <$> f (_msgPrefix m)
msgCommand :: Functor f => (Text -> f Text) -> RawIrcMsg -> f RawIrcMsg
msgCommand f m = (\x -> m { _msgCommand = x }) <$> f (_msgCommand m)
msgParams :: Functor f => ([Text] -> f [Text]) -> RawIrcMsg -> f RawIrcMsg
msgParams f m = (\x -> m { _msgParams = x }) <$> f (_msgParams m)
parseRawIrcMsg :: Text -> Maybe RawIrcMsg
parseRawIrcMsg x =
case parseOnly rawIrcMsgParser x of
Left{} -> Nothing
Right r -> Just r
maxMiddleParams :: Int
maxMiddleParams = 14
rawIrcMsgParser :: Parser RawIrcMsg
rawIrcMsgParser =
do tags <- fromMaybe [] <$> guarded '@' tagsParser
prefix <- guarded ':' prefixParser
cmd <- simpleTokenParser
params <- paramsParser maxMiddleParams
return $! RawIrcMsg
{ _msgTags = tags
, _msgPrefix = prefix
, _msgCommand = cmd
, _msgParams = params
}
paramsParser ::
Int -> Parser [Text]
paramsParser !n =
do end <- P.atEnd
if end
then return []
else do isColon <- optionalChar ':'
if isColon || n == 0
then finalParam
else middleParam
where
finalParam =
do x <- takeText
let !x' = Text.copy x
return [x']
middleParam =
do x <- simpleTokenParser
xs <- paramsParser (n-1)
return (x:xs)
tagsParser :: Parser [TagEntry]
tagsParser = tagParser `sepBy1` char ';' <* spaces
tagParser :: Parser TagEntry
tagParser =
do key <- P.takeWhile (notInClass "=; ")
_ <- optional (char '=')
val <- P.takeWhile (notInClass "; ")
return $! TagEntry key (unescapeTagVal val)
unescapeTagVal :: Text -> Text
unescapeTagVal = Text.pack . aux . Text.unpack
where
aux ('\\':':':xs) = ';':aux xs
aux ('\\':'s':xs) = ' ':aux xs
aux ('\\':'\\':xs) = '\\':aux xs
aux ('\\':'r':xs) = '\r':aux xs
aux ('\\':'n':xs) = '\n':aux xs
aux (x:xs) = x : aux xs
aux "" = ""
escapeTagVal :: Text -> Text
escapeTagVal = Text.concatMap aux
where
aux ';' = "\\:"
aux ' ' = "\\s"
aux '\\' = "\\\\"
aux '\r' = "\\r"
aux '\n' = "\\n"
aux x = Text.singleton x
prefixParser :: Parser UserInfo
prefixParser =
do tok <- simpleTokenParser
return $! parseUserInfo tok
simpleTokenParser :: Parser Text
simpleTokenParser =
do xs <- P.takeWhile1 (/= ' ')
spaces
return $! Text.copy xs
spaces :: Parser ()
spaces = P.skipWhile (== ' ')
renderRawIrcMsg :: RawIrcMsg -> ByteString
renderRawIrcMsg !m
= L.toStrict
$ Builder.toLazyByteString
$ renderTags (view msgTags m)
<> maybe mempty renderPrefix (view msgPrefix m)
<> Text.encodeUtf8Builder (view msgCommand m)
<> buildParams (view msgParams m)
<> Builder.char8 '\r'
<> Builder.char8 '\n'
rawIrcMsg ::
Text ->
[Text] -> RawIrcMsg
rawIrcMsg = RawIrcMsg [] Nothing
renderTags :: [TagEntry] -> Builder
renderTags [] = mempty
renderTags xs
= Builder.char8 '@'
<> mconcat (intersperse (Builder.char8 ';') (map renderTag xs))
<> Builder.char8 ' '
renderTag :: TagEntry -> Builder
renderTag (TagEntry key val)
| Text.null val = Text.encodeUtf8Builder key
| otherwise = Text.encodeUtf8Builder key
<> Builder.char8 '='
<> Text.encodeUtf8Builder (escapeTagVal val)
renderPrefix :: UserInfo -> Builder
renderPrefix u
= Builder.char8 ':'
<> Text.encodeUtf8Builder (renderUserInfo u)
<> Builder.char8 ' '
buildParams :: [Text] -> Builder
buildParams [x]
| " " `Text.isInfixOf` x || ":" `Text.isPrefixOf` x || Text.null x
= Builder.char8 ' ' <> Builder.char8 ':' <> Text.encodeUtf8Builder x
buildParams (x:xs)
= Builder.char8 ' ' <> Text.encodeUtf8Builder x <> buildParams xs
buildParams [] = mempty
guarded :: Char -> Parser b -> Parser (Maybe b)
guarded c p =
do success <- optionalChar c
if success then Just <$> p else pure Nothing
optionalChar :: Char -> Parser Bool
optionalChar c = True <$ char c <|> pure False
asUtf8 :: ByteString -> Text
asUtf8 x = case Text.decodeUtf8' x of
Right txt -> txt
Left{} -> decodeCP1252 x
decodeCP1252 :: ByteString -> Text
decodeCP1252 bs = Text.pack [ cp1252 Vector.! fromIntegral x | x <- B.unpack bs ]
cp1252 :: Vector Char
cp1252 = Vector.fromList
$ ['\x00'..'\x7f']
++ "€\x81‚ƒ„…†‡ˆ‰Š‹Œ\x8dŽ\x8f\x90‘’“”•–—˜™š›œ\x9džŸ"
++ ['\xa0'..'\xff']