Initial checkin of pipes-based IRC server; work-in-progress

Currently there's a network server that parses commands received on a
connection and then renders them on all currently live connections.

There are still a couple of bugs in the parser. There's an initial stab
at types for managing the server's records for connections. users, and
channels, but they will probably evolve a bit.
This commit is contained in:
Levi Pearson
2013-11-11 23:56:23 -07:00
commit 41b457dd30
11 changed files with 1339 additions and 0 deletions

18
src/Main.hs Normal file
View File

@@ -0,0 +1,18 @@
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Control.Concurrent.Async (wait)
import Pipes.IRC.Server (listenHandler, mkIrcServer,
startIrcServer)
import Pipes.IRC.Server.Types (HostPreference (Host),
IrcConfig (..))
main :: IO ()
main =
let
ircConf = IrcConfig "6665" (Host "127.0.0.1")
in do
srv <- mkIrcServer ircConf
listener <- startIrcServer srv (listenHandler srv)
wait listener

8
src/Pipes/IRC/Message.hs Normal file
View File

@@ -0,0 +1,8 @@
module Pipes.IRC.Message
( parseMessage
, module Pipes.IRC.Message.Types
) where
import Pipes.IRC.Message.Types
parseMessage = undefined

View File

@@ -0,0 +1,182 @@
{-# LANGUAGE OverloadedStrings #-}
module Pipes.IRC.Message.Parse where
import Control.Applicative
import Data.Attoparsec.ByteString.Char8 as P
import qualified Data.ByteString as BS
import Data.ByteString.Char8 as C8
import Data.Char as C
import Pipes.IRC.Message.Types
--
-- Parsers
--
parseMsgOrLine :: Parser (Either ByteString IrcMessage)
parseMsgOrLine =
Right <$> parseIrcMessage <|>
Left <$> parseToEOL
parseToEOL :: Parser ByteString
parseToEOL = C8.append <$> P.takeWhile (not . isCRLFChar)
<*> P.takeWhile isCRLFChar
parseIrcMessage :: Parser IrcMessage
parseIrcMessage =
IrcMessage <$> option Nothing (Just <$> parseMsgPrefix )
<*> parseCommand
<*> parseParams
<* parseLineEnd
<?> "parseIrcMessage"
parseMsgPrefix :: Parser MsgPrefix
parseMsgPrefix =
Left <$ char ':' <*> parseServerName <* takeWhile1 (== ' ') <|>
Right <$ char ':' <*> parseNickName <* takeWhile1 (== ' ')
<?> "parseMsgPrefix"
parseServerName :: Parser ServerName
parseServerName = C8.intercalate "." <$> parseName `sepBy1` char '.'
<?> "parseServerName"
parseName :: Parser ByteString
parseName = C8.cons
<$> satisfy isLetOrDig
<*> parseHyphenated
<?> "parseName"
parseHyphenated :: Parser ByteString
parseHyphenated =
C8.intercalate "-" <$> P.takeWhile1 isLetOrDig `sepBy` char '-'
<?> "parseHyphenated"
parseNickName :: Parser NickName
parseNickName =
NickName <$> parseNick
<*> parseUser
<*> parseHost
<?> "parseNickName"
parseNick :: Parser ByteString
parseNick = takeWhile1 isNickChar <?> "parseNick"
parseUser :: Parser (Maybe ByteString)
parseUser = option Nothing
(Just <$ char '!' <*> takeWhile1 isUserChar
<?> "parseUser")
parseHost :: Parser (Maybe ByteString)
parseHost = option Nothing
(Just <$ char '@' <*> parseServerName
<?> "parseHost")
parseCommand :: Parser MsgCommand
parseCommand =
Right <$> parseIrcReply <|>
Left <$> parseIrcCommand
<?> "parseCommand"
parseIrcCommand :: Parser IrcCommand
parseIrcCommand = (toCmd . C8.map toUpper) <$> P.takeWhile isAlpha_ascii
<?> "parseIrcCommand"
where
toCmd cmd = case cmd of
"PASS" -> PASS
"NICK" -> NICK
"USER" -> USER
"SERVER" -> SERVER
"OPER" -> OPER
"QUIT" -> QUIT
"SQUIT" -> SQUIT
"JOIN" -> JOIN
"PART" -> PART
"MODE" -> MODE
"TOPIC" -> TOPIC
"NAMES" -> NAMES
"LIST" -> LIST
"INVITE" -> INVITE
"KICK" -> KICK
"VERSION" -> VERSION
"STATS" -> STATS
"LINKS" -> LINKS
"TIME" -> TIME
"CONNECT" -> CONNECT
"TRACE" -> TRACE
"ADMIN" -> ADMIN
"INFO" -> INFO
"PRIVMSG" -> PRIVMSG
"NOTICE" -> NOTICE
"WHO" -> WHO
"WHOIS" -> WHOIS
"WHOWAS" -> WHOWAS
"KILL" -> KILL
"PING" -> PING
"PONG" -> PONG
"ERROR" -> ERROR
"AWAY" -> AWAY
"REHASH" -> REHASH
"RESTART" -> RESTART
"SUMMON" -> SUMMON
"USERS" -> USERS
"WALLOPS" -> WALLOPS
"USERHOST" -> USERHOST
"ISON" -> ISON
_ -> Unknown cmd
parseIrcReply :: Parser IrcReply
parseIrcReply = toReply <$> digit <*> digit <*> digit
<?> "ParseIrcReply"
where
toReply a b c = toEnum $
100 * digitToInt a + 10 * digitToInt b + digitToInt c
parseParams :: Parser [IrcParam]
parseParams = takeWhile1 (== ' ') *>
option [] ( parseTrail <|> parseMiddle )
where
parseTrail = (:[]) <$> parseTrailParam
parseMiddle = (:) <$> parseMiddleParam <*> parseParams
parseTrailParam :: Parser IrcParam
parseTrailParam = C8.cons <$> satisfy (== ':') <*> P.takeWhile isTrailingChar
parseMiddleParam :: Parser IrcParam
parseMiddleParam = C8.cons <$> satisfy (\c -> c /= ':' && isNonWhite c)
<*> P.takeWhile isNonWhite
<?> "parseMiddleParam"
parseLineEnd :: Parser ()
parseLineEnd = const () <$> ("\r\n" <|> "\n" <|> "\r")
<?> "parseLineEnd"
--
-- Character Predicates
--
isLetOrDig :: Char -> Bool
isLetOrDig c = P.isDigit c || isAlpha_ascii c
isLetOrDigOrHyph :: Char -> Bool
isLetOrDigOrHyph c = isLetOrDig c || c == '-'
isSpecial :: Char -> Bool
isSpecial c = c `C8.elem` "-[]\\`^{}"
isNonWhite :: Char -> Bool
isNonWhite c = not $ c `C8.elem` BS.pack [0x00, 0x20, 0x0d, 0x0a]
isTrailingChar :: Char -> Bool
isTrailingChar c = not $ c `C8.elem` BS.pack [0x00, 0x0d, 0x0a]
isCRLFChar :: Char -> Bool
isCRLFChar c = c `C8.elem` BS.pack [0x0d, 0x0a]
isNickChar :: Char -> Bool
isNickChar c = isLetOrDig c || isSpecial c
isNickNameSep :: Char -> Bool
isNickNameSep c = c == '!' || c == '@'
isUserChar :: Char -> Bool
isUserChar c = isNonWhite c && not (isNickNameSep c)

View File

@@ -0,0 +1,73 @@
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Pipes.IRC.Message.Render where
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as C8
import Data.ByteString.Lazy.Builder
import Data.ByteString.Lazy.Builder.ASCII (intDec)
import Data.Monoid
import Pipes.IRC.Message.Types
renderIrcMessage :: IrcMessage -> Builder
renderIrcMessage IrcMessage {..} =
renderMsgPrefix prefix
<> renderMsgCommand command
<> renderIrcParams params
<> byteString "\r\n"
renderMsgPrefix :: Maybe MsgPrefix -> Builder
renderMsgPrefix Nothing = mempty
renderMsgPrefix mp =
char8 ':'
<> case mp of
Just (Left sn) -> renderServerName sn
Just (Right nn) -> renderNickName nn
_ -> mempty
<> char8 ' '
renderServerName :: ServerName -> Builder
renderServerName = byteString
renderNickName :: NickName -> Builder
renderNickName NickName {..} =
byteString nick
<> renderUser user
<> renderHost host
renderUser :: Maybe ByteString -> Builder
renderUser user =
case user of
Nothing -> mempty
Just name -> byteString "!" <> byteString name
renderHost :: Maybe ByteString -> Builder
renderHost host =
case host of
Nothing -> mempty
Just name -> byteString "@" <> byteString name
renderMsgCommand :: MsgCommand -> Builder
renderMsgCommand cmd =
case cmd of
Left ircCmd -> renderIrcCommand ircCmd
Right ircReply -> renderIrcReply ircReply
<> byteString " "
renderIrcCommand :: IrcCommand -> Builder
renderIrcCommand cmd =
case cmd of
(Unknown name) -> byteString name
_ -> byteString . C8.pack . show $ cmd
renderIrcReply :: IrcReply -> Builder
renderIrcReply IrcReply {..} = intDec replyCode
renderIrcParams :: [IrcParam] -> Builder
renderIrcParams [] = mempty
renderIrcParams (p:ps) =
renderParam p <> mconcat [char8 ' ' <> renderParam p' | p' <- ps ]
renderParam :: IrcParam -> Builder
renderParam = byteString

View File

@@ -0,0 +1,557 @@
{-# LANGUAGE OverloadedStrings #-}
module Pipes.IRC.Message.Types where
import qualified Data.ByteString as B
data IrcMessage =
IrcMessage { prefix :: Maybe MsgPrefix
, command :: MsgCommand
, params :: [IrcParam]
} deriving (Show)
type MsgPrefix = Either ServerName NickName
type ServerName = B.ByteString
data NickName =
NickName { nick :: B.ByteString
, user :: Maybe B.ByteString
, host :: Maybe B.ByteString
} deriving (Show)
type MsgCommand = Either IrcCommand IrcReply
type IrcParam = B.ByteString
data IrcCommand = PASS
| NICK
| USER
| SERVER
| OPER
| QUIT
| SQUIT
| JOIN
| PART
| MODE
| TOPIC
| NAMES
| LIST
| INVITE
| KICK
| VERSION
| STATS
| LINKS
| TIME
| CONNECT
| TRACE
| ADMIN
| INFO
| PRIVMSG
| NOTICE
| WHO
| WHOIS
| WHOWAS
| KILL
| PING
| PONG
| ERROR
| AWAY
| REHASH
| RESTART
| SUMMON
| USERS
| WALLOPS
| USERHOST
| ISON
| Unknown B.ByteString
deriving (Show, Eq, Ord)
data IrcReply = IrcReply
{ replyCode :: !Int
, replyName :: !B.ByteString
} deriving (Show)
instance Eq IrcReply where
IrcReply { replyCode = a } == IrcReply { replyCode = b } = a == b
instance Ord IrcReply where
IrcReply { replyCode = a } `compare` IrcReply { replyCode = b } =
a `compare` b
instance Enum IrcReply where
fromEnum = replyCode
toEnum 200 = rpl_tracelink
toEnum 201 = rpl_traceconnecting
toEnum 202 = rpl_tracehandshake
toEnum 203 = rpl_traceunknown
toEnum 204 = rpl_traceoperator
toEnum 205 = rpl_traceuser
toEnum 206 = rpl_traceserver
toEnum 208 = rpl_tracenewtype
toEnum 211 = rpl_statslinkinfo
toEnum 212 = rpl_statscommands
toEnum 213 = rpl_statscline
toEnum 214 = rpl_statsnline
toEnum 215 = rpl_statsiline
toEnum 216 = rpl_statskline
toEnum 218 = rpl_statsyline
toEnum 219 = rpl_endofstats
toEnum 221 = rpl_umodeis
toEnum 241 = rpl_statslline
toEnum 242 = rpl_statsuptime
toEnum 243 = rpl_statsoline
toEnum 244 = rpl_statshline
toEnum 251 = rpl_luserclient
toEnum 252 = rpl_luserop
toEnum 253 = rpl_luserunknown
toEnum 254 = rpl_luserchannels
toEnum 255 = rpl_luserme
toEnum 256 = rpl_adminme
toEnum 257 = rpl_adminloc1
toEnum 258 = rpl_adminloc2
toEnum 259 = rpl_adminemail
toEnum 261 = rpl_tracelog
toEnum 300 = rpl_none
toEnum 301 = rpl_away
toEnum 302 = rpl_userhost
toEnum 303 = rpl_ison
toEnum 305 = rpl_unaway
toEnum 306 = rpl_nowaway
toEnum 311 = rpl_whoisuser
toEnum 312 = rpl_whoisserver
toEnum 313 = rpl_whoisoperator
toEnum 314 = rpl_whowasuser
toEnum 315 = rpl_endofwho
toEnum 317 = rpl_whoisidle
toEnum 318 = rpl_endofwhois
toEnum 319 = rpl_whoischannels
toEnum 321 = rpl_liststart
toEnum 322 = rpl_list
toEnum 323 = rpl_listend
toEnum 324 = rpl_channelmodeis
toEnum 331 = rpl_notopic
toEnum 332 = rpl_topic
toEnum 341 = rpl_inviting
toEnum 342 = rpl_summoning
toEnum 351 = rpl_version
toEnum 352 = rpl_whoreply
toEnum 353 = rpl_namreply
toEnum 364 = rpl_links
toEnum 365 = rpl_endoflinks
toEnum 366 = rpl_endofnames
toEnum 367 = rpl_banlist
toEnum 368 = rpl_endofbanlist
toEnum 369 = rpl_endofwhowas
toEnum 371 = rpl_info
toEnum 374 = rpl_endofinfo
toEnum 375 = rpl_motdstart
toEnum 376 = rpl_endofmotd
toEnum 381 = rpl_youreoper
toEnum 382 = rpl_rehashing
toEnum 391 = rpl_time
toEnum 392 = rpl_usersstart
toEnum 393 = rpl_users
toEnum 394 = rpl_endofusers
toEnum 395 = rpl_nousers
toEnum 401 = err_nosuchnick
toEnum 402 = err_nosuchserver
toEnum 403 = err_nosuchchannel
toEnum 404 = err_cannotsendtochan
toEnum 405 = err_toomanychannels
toEnum 406 = err_wasnosuchnick
toEnum 407 = err_toomanytargets
toEnum 409 = err_noorigin
toEnum 411 = err_norecipient
toEnum 412 = err_notexttosend
toEnum 413 = err_notoplevel
toEnum 414 = err_wildtoplevel
toEnum 421 = err_unknowncommand
toEnum 422 = err_nomotd
toEnum 423 = err_noadmininfo
toEnum 424 = err_fileerror
toEnum 431 = err_nonicknamegiven
toEnum 432 = err_erroneusnickname
toEnum 433 = err_nicknameinuse
toEnum 436 = err_nickcollision
toEnum 441 = err_usernotinchannel
toEnum 442 = err_notonchannel
toEnum 443 = err_useronchannel
toEnum 444 = err_nologin
toEnum 445 = err_summondisabled
toEnum 446 = err_usersdisabled
toEnum 451 = err_notregistered
toEnum 461 = err_needmoreparams
toEnum 462 = err_alreadyregistered
toEnum 463 = err_nopermforhost
toEnum 464 = err_passwdmismatch
toEnum 465 = err_yourebannedcreep
toEnum 467 = err_keyset
toEnum 471 = err_channelisfull
toEnum 472 = err_unknownmode
toEnum 473 = err_inviteonlychan
toEnum 474 = err_bannedfromchan
toEnum 475 = err_badchannelkey
toEnum 481 = err_noprivileges
toEnum 482 = err_chanoprivsneeded
toEnum 483 = err_cantkillserver
toEnum 491 = err_nooperhost
toEnum 501 = err_umodeunknownflag
toEnum 502 = err_usersdontmatch
toEnum c = mkIrcReply c B.empty
mkIrcReply :: Int -> B.ByteString -> IrcReply
mkIrcReply = IrcReply
rpl_tracelink :: IrcReply
rpl_tracelink = mkIrcReply 200 "RPL_TRACELINK"
rpl_traceconnecting :: IrcReply
rpl_traceconnecting = mkIrcReply 201 "RPL_TRACECONNECTING"
rpl_tracehandshake :: IrcReply
rpl_tracehandshake = mkIrcReply 202 "RPL_TRACEHANDSHAKE"
rpl_traceunknown :: IrcReply
rpl_traceunknown = mkIrcReply 203 "RPL_TRACEUNKNOWN"
rpl_traceoperator :: IrcReply
rpl_traceoperator = mkIrcReply 204 "RPL_TRACEOPERATOR"
rpl_traceuser :: IrcReply
rpl_traceuser = mkIrcReply 205 "RPL_TRACEUSER"
rpl_traceserver :: IrcReply
rpl_traceserver = mkIrcReply 206 "RPL_TRACESERVER"
rpl_tracenewtype :: IrcReply
rpl_tracenewtype = mkIrcReply 208 "RPL_TRACENEWTYPE"
rpl_statslinkinfo :: IrcReply
rpl_statslinkinfo = mkIrcReply 211 "RPL_STATSLINKINFO"
rpl_statscommands :: IrcReply
rpl_statscommands = mkIrcReply 212 "RPL_STATSCOMMANDS"
rpl_statscline :: IrcReply
rpl_statscline = mkIrcReply 213 "RPL_STATSCLINE"
rpl_statsnline :: IrcReply
rpl_statsnline = mkIrcReply 214 "RPL_STATSNLINE"
rpl_statsiline :: IrcReply
rpl_statsiline = mkIrcReply 215 "RPL_STATSILINE"
rpl_statskline :: IrcReply
rpl_statskline = mkIrcReply 216 "RPL_STATSKLINE"
rpl_statsyline :: IrcReply
rpl_statsyline = mkIrcReply 218 "RPL_STATSYLINE"
rpl_endofstats :: IrcReply
rpl_endofstats = mkIrcReply 219 "RPL_ENDOFSTATS"
rpl_umodeis :: IrcReply
rpl_umodeis = mkIrcReply 221 "RPL_UMODEIS"
rpl_statslline :: IrcReply
rpl_statslline = mkIrcReply 241 "RPL_STATSLLINE"
rpl_statsuptime :: IrcReply
rpl_statsuptime = mkIrcReply 242 "RPL_STATSUPTIME"
rpl_statsoline :: IrcReply
rpl_statsoline = mkIrcReply 243 "RPL_STATSOLINE"
rpl_statshline :: IrcReply
rpl_statshline = mkIrcReply 244 "RPL_STATSHLINE"
rpl_luserclient :: IrcReply
rpl_luserclient = mkIrcReply 251 "RPL_LUSERCLIENT"
rpl_luserop :: IrcReply
rpl_luserop = mkIrcReply 252 "RPL_LUSEROP"
rpl_luserunknown :: IrcReply
rpl_luserunknown = mkIrcReply 253 "RPL_LUSERUNKNOWN"
rpl_luserchannels :: IrcReply
rpl_luserchannels = mkIrcReply 254 "RPL_LUSERCHANNELS"
rpl_luserme :: IrcReply
rpl_luserme = mkIrcReply 255 "RPL_LUSERME"
rpl_adminme :: IrcReply
rpl_adminme = mkIrcReply 256 "RPL_ADMINME"
rpl_adminloc1 :: IrcReply
rpl_adminloc1 = mkIrcReply 257 "RPL_ADMINLOC1"
rpl_adminloc2 :: IrcReply
rpl_adminloc2 = mkIrcReply 258 "RPL_ADMINLOC2"
rpl_adminemail :: IrcReply
rpl_adminemail = mkIrcReply 259 "RPL_ADMINEMAIL"
rpl_tracelog :: IrcReply
rpl_tracelog = mkIrcReply 261 "RPL_TRACELOG"
rpl_none :: IrcReply
rpl_none = mkIrcReply 300 "RPL_NONE"
rpl_away :: IrcReply
rpl_away = mkIrcReply 301 "RPL_AWAY"
rpl_userhost :: IrcReply
rpl_userhost = mkIrcReply 302 "RPL_USERHOST"
rpl_ison :: IrcReply
rpl_ison = mkIrcReply 303 "RPL_ISON"
rpl_unaway :: IrcReply
rpl_unaway = mkIrcReply 305 "RPL_UNAWAY"
rpl_nowaway :: IrcReply
rpl_nowaway = mkIrcReply 306 "RPL_NOWAWAY"
rpl_whoisuser :: IrcReply
rpl_whoisuser = mkIrcReply 311 "RPL_WHOISUSER"
rpl_whoisserver :: IrcReply
rpl_whoisserver = mkIrcReply 312 "RPL_WHOISSERVER"
rpl_whoisoperator :: IrcReply
rpl_whoisoperator = mkIrcReply 313 "RPL_WHOISOPERATOR"
rpl_whowasuser :: IrcReply
rpl_whowasuser = mkIrcReply 314 "RPL_WHOWASUSER"
rpl_endofwho :: IrcReply
rpl_endofwho = mkIrcReply 315 "RPL_ENDOFWHO"
rpl_whoisidle :: IrcReply
rpl_whoisidle = mkIrcReply 317 "RPL_WHOISIDLE"
rpl_endofwhois :: IrcReply
rpl_endofwhois = mkIrcReply 318 "RPL_ENDOFWHOIS"
rpl_whoischannels :: IrcReply
rpl_whoischannels = mkIrcReply 319 "RPL_WHOISCHANNELS"
rpl_liststart :: IrcReply
rpl_liststart = mkIrcReply 321 "RPL_LISTSTART"
rpl_list :: IrcReply
rpl_list = mkIrcReply 322 "RPL_LIST"
rpl_listend :: IrcReply
rpl_listend = mkIrcReply 323 "RPL_LISTEND"
rpl_channelmodeis :: IrcReply
rpl_channelmodeis = mkIrcReply 234 "RPL_CHANNELMODEIS"
rpl_notopic :: IrcReply
rpl_notopic = mkIrcReply 331 "RPL_NOTOPIC"
rpl_topic :: IrcReply
rpl_topic = mkIrcReply 332 "RPL_TOPIC"
rpl_inviting :: IrcReply
rpl_inviting = mkIrcReply 341 "RPL_INVITING"
rpl_summoning :: IrcReply
rpl_summoning = mkIrcReply 342 "RPL_SUMMONING"
rpl_version :: IrcReply
rpl_version = mkIrcReply 351 "RPL_VERSION"
rpl_whoreply :: IrcReply
rpl_whoreply = mkIrcReply 352 "RPL_WHOREPLY"
rpl_namreply :: IrcReply
rpl_namreply = mkIrcReply 353 "RPL_NAMREPLY"
rpl_links :: IrcReply
rpl_links = mkIrcReply 364 "RPL_LINKS"
rpl_endoflinks :: IrcReply
rpl_endoflinks = mkIrcReply 365 "RPL_ENDOFLINKS"
rpl_endofnames :: IrcReply
rpl_endofnames = mkIrcReply 366 "RPL_ENDOFNAMES"
rpl_banlist :: IrcReply
rpl_banlist = mkIrcReply 367 "RPL_BANLIST"
rpl_endofbanlist :: IrcReply
rpl_endofbanlist = mkIrcReply 368 "RPL_ENDOFBANLIST"
rpl_endofwhowas :: IrcReply
rpl_endofwhowas = mkIrcReply 369 "RPL_ENDOFWHOWAS"
rpl_info :: IrcReply
rpl_info = mkIrcReply 371 "RPL_INFO"
rpl_endofinfo :: IrcReply
rpl_endofinfo = mkIrcReply 374 "RPL_ENDOFINFO"
rpl_motdstart :: IrcReply
rpl_motdstart = mkIrcReply 375 "RPL_MOTDSTART"
rpl_endofmotd :: IrcReply
rpl_endofmotd = mkIrcReply 376 "RPL_ENDOFMOTD"
rpl_youreoper :: IrcReply
rpl_youreoper = mkIrcReply 381 "RPL_YOUREOPER"
rpl_rehashing :: IrcReply
rpl_rehashing = mkIrcReply 382 "RPL_REHASHING"
rpl_time :: IrcReply
rpl_time = mkIrcReply 391 "RPL_TIME"
rpl_usersstart :: IrcReply
rpl_usersstart = mkIrcReply 392 "RPL_USERSSTART"
rpl_users :: IrcReply
rpl_users = mkIrcReply 393 "RPL_USERS"
rpl_endofusers :: IrcReply
rpl_endofusers = mkIrcReply 394 "RPL_ENDOFUSERS"
rpl_nousers :: IrcReply
rpl_nousers = mkIrcReply 395 "RPL_NOUSERS"
err_nosuchnick :: IrcReply
err_nosuchnick = mkIrcReply 401 "ERR_NOSUCHNICK"
err_nosuchserver :: IrcReply
err_nosuchserver = mkIrcReply 402 "ERR_NOSUCHSERVER"
err_nosuchchannel :: IrcReply
err_nosuchchannel = mkIrcReply 403 "ERR_NOSUCHCHANNEL"
err_cannotsendtochan :: IrcReply
err_cannotsendtochan = mkIrcReply 404 "ERR_CANNOTSENDTOCHAN"
err_toomanychannels :: IrcReply
err_toomanychannels = mkIrcReply 405 "ERR_TOOMANYCHANNELS"
err_wasnosuchnick :: IrcReply
err_wasnosuchnick = mkIrcReply 406 "ERR_WASNOSUCHNICK"
err_toomanytargets :: IrcReply
err_toomanytargets = mkIrcReply 407 "ERR_TOOMANYTARGETS"
err_noorigin :: IrcReply
err_noorigin = mkIrcReply 409 "ERR_NOORIGIN"
err_norecipient :: IrcReply
err_norecipient = mkIrcReply 411 "ERR_NORECIPIENT"
err_notexttosend :: IrcReply
err_notexttosend = mkIrcReply 412 "ERR_NOTEXTTOSEND"
err_notoplevel :: IrcReply
err_notoplevel = mkIrcReply 413 "ERR_NOTOPLEVEL"
err_wildtoplevel :: IrcReply
err_wildtoplevel = mkIrcReply 414 "ERR_WILDTOPLEVEL"
err_unknowncommand :: IrcReply
err_unknowncommand = mkIrcReply 421 "ERR_UNKNOWNCOMMAND"
err_nomotd :: IrcReply
err_nomotd = mkIrcReply 422 "ERR_NOMOTD"
err_noadmininfo :: IrcReply
err_noadmininfo = mkIrcReply 423 "ERR_NOADMININFO"
err_fileerror :: IrcReply
err_fileerror = mkIrcReply 424 "ERR_FILEERROR"
err_nonicknamegiven :: IrcReply
err_nonicknamegiven = mkIrcReply 431 "ERR_NONICKNAMEGIVEN"
err_erroneusnickname :: IrcReply
err_erroneusnickname = mkIrcReply 432 "ERR_ERRONEUSNICKNAME"
err_nicknameinuse :: IrcReply
err_nicknameinuse = mkIrcReply 433 "ERR_NICKNAMEINUSE"
err_nickcollision :: IrcReply
err_nickcollision = mkIrcReply 436 "ERR_NICKCOLLISION"
err_usernotinchannel :: IrcReply
err_usernotinchannel = mkIrcReply 441 "ERR_USERNOTINCHANNEL"
err_notonchannel :: IrcReply
err_notonchannel = mkIrcReply 442 "ERR_NOTONCHANNEL"
err_useronchannel :: IrcReply
err_useronchannel = mkIrcReply 443 "ERR_USERONCHANNEL"
err_nologin :: IrcReply
err_nologin = mkIrcReply 444 "ERR_NOLOGIN"
err_summondisabled :: IrcReply
err_summondisabled = mkIrcReply 445 "ERR_SUMMONDISABLED"
err_usersdisabled :: IrcReply
err_usersdisabled = mkIrcReply 446 "ERR_USERSDISABLED"
err_notregistered :: IrcReply
err_notregistered = mkIrcReply 451 "ERR_NOTREGISTERED"
err_needmoreparams :: IrcReply
err_needmoreparams = mkIrcReply 461 "ERR_NEEDMOREPARAMS"
err_alreadyregistered :: IrcReply
err_alreadyregistered = mkIrcReply 462 "ERR_ALREADYREGISTERED"
err_nopermforhost :: IrcReply
err_nopermforhost = mkIrcReply 463 "ERR_NOPERMFORHOST"
err_passwdmismatch :: IrcReply
err_passwdmismatch = mkIrcReply 464 "ERR_PASSWDMISMATCH"
err_yourebannedcreep :: IrcReply
err_yourebannedcreep = mkIrcReply 465 "ERR_YOUREBANNEDCREEP"
err_keyset :: IrcReply
err_keyset = mkIrcReply 467 "ERR_KEYSET"
err_channelisfull :: IrcReply
err_channelisfull = mkIrcReply 471 "ERR_CHANNELISFULL"
err_unknownmode :: IrcReply
err_unknownmode = mkIrcReply 472 "ERR_UNKNOWNMODE"
err_inviteonlychan :: IrcReply
err_inviteonlychan = mkIrcReply 473 "ERR_INVITEONLYCHAN"
err_bannedfromchan :: IrcReply
err_bannedfromchan = mkIrcReply 474 "ERR_BANNEDFROMCHAN"
err_badchannelkey :: IrcReply
err_badchannelkey = mkIrcReply 475 "ERR_BADCHANNELKEY"
err_noprivileges :: IrcReply
err_noprivileges = mkIrcReply 481 "ERR_NOPRIVILEGES"
err_chanoprivsneeded :: IrcReply
err_chanoprivsneeded = mkIrcReply 482 "ERR_CHANOPRIVSNEEDED"
err_cantkillserver :: IrcReply
err_cantkillserver = mkIrcReply 483 "ERR_CANTKILLSERVER"
err_nooperhost :: IrcReply
err_nooperhost = mkIrcReply 491 "ERR_NOOPERHOST"
err_umodeunknownflag :: IrcReply
err_umodeunknownflag = mkIrcReply 501 "ERR_UMODEUNKNOWNFLAG"
err_usersdontmatch :: IrcReply
err_usersdontmatch = mkIrcReply 502 "ERR_USERSDONTMATCH"
ircReplyIsErr :: IrcReply -> Bool
ircReplyIsErr (IrcReply {replyCode = c}) = c >= 400 && c <= 502

141
src/Pipes/IRC/Server.hs Normal file
View File

@@ -0,0 +1,141 @@
{-# LANGUAGE OverloadedStrings #-}
module Pipes.IRC.Server
( mkIrcServer
, startIrcServer
, listenHandler
, module Pipes.IRC.Server.Types
)
where
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Monad
import Data.ByteString.Char8 as BS
import Data.ByteString.Lazy as LB
import Data.ByteString.Lazy.Builder (toLazyByteString)
import Data.List as L
import Pipes
import Pipes.Attoparsec
import Pipes.Concurrent as PC
import Pipes.IRC.Message.Parse
import Pipes.IRC.Message.Render
import Pipes.IRC.Message.Types
import Pipes.IRC.Server.Types
import Pipes.Lift (runStateP)
import Pipes.Network.TCP
import Pipes.Parse as PP
sendToMany :: a -> [Output a] -> IO ()
sendToMany msg outs = do
resL <- forM outs $ \o ->
async $ atomically $ PC.send o msg
mapM_ wait resL
publishStream :: TVar [IrcConnection a] -> Consumer a IO ()
publishStream clients = forever $ do
msg <- await
cs <- lift $ atomically $ readTVar clients
let os = L.map out cs
lift $ sendToMany msg os
spawnEcho :: TVar [IrcConnection a] -> IO (Output a)
spawnEcho clients = do
(writeEnd, readEnd) <- spawn Unbounded
_ <- async $ forever $
runEffect $ fromInput readEnd >-> publishStream clients
return writeEnd
logMsg :: IrcMessage -> IO ()
logMsg msg = BS.putStr $ BS.concat
["LOG: ", toStrict $ toLazyByteString (renderIrcMessage msg)]
cmdHandler :: Output IrcMessage -> Consumer IrcMessage IO ()
cmdHandler echoSrv = toOutput echoSrv
filterMsgs :: Pipe (Either BS.ByteString IrcMessage) IrcMessage IO ()
filterMsgs = forever $ do
cmd <- await
case cmd of
Left bs -> lift $ BS.putStr $ BS.concat ["BAD COMMAND: ", bs]
Right c -> do lift $ logMsg c
yield c
addIrcConnection :: IrcConnection IrcMessage -> IrcServer -> IO ()
addIrcConnection client server = do
let clients = ircConnections server
atomically $ modifyTVar clients $ \cs -> client : cs
delIrcConnection :: IrcConnection IrcMessage -> IrcServer -> IO ()
delIrcConnection client server = do
let clients = ircConnections server
atomically $ modifyTVar clients $ delete client
parseMessage :: Producer BS.ByteString IO ()
-> Producer (Either BS.ByteString IrcMessage) IO ()
parseMessage prod = do
(me, src') <- runStateP prod go
case me of
Left e -> lift . print . show $ e
Right r -> return r
where
go = do
eof <- lift isEndOfParserInput
if eof
then do
liftIO $ BS.putStrLn "EOF Reached"
ra <- lift PP.draw
case ra of
Left r -> return (Right r)
Right _ -> error "parseMessage: impossible!!"
else do
eb <- lift (parse parseMsgOrLine)
case eb of
Left e -> return (Left e)
Right (_, b) -> yield b >> go
renderMessage :: Pipe IrcMessage BS.ByteString IO ()
renderMessage = forever $ do
msg <- await
let output = toIrcMessage msg
yield output
where
toIrcMessage = toStrict . toLazyByteString . renderIrcMessage
listenHandler :: IrcServer -> IrcHandler
listenHandler server (lsock, _) =
forever $ acceptFork lsock $ \(csock, caddr) ->
let
sockWriter = toSocket csock
sockReader = fromSocket csock 4096
handler = ircHandler server
in do
(writeEnd, readEnd) <- spawn Unbounded
let client = IrcConnection csock caddr writeEnd
addIrcConnection client server
r <- async $ runEffect $
parseMessage sockReader >-> filterMsgs >-> handler
w <- async $ runEffect $
fromInput readEnd >-> renderMessage >-> sockWriter
mapM_ wait [r,w]
delIrcConnection client server
mkIrcServer :: IrcConfig -> IO IrcServer
mkIrcServer config = do
conns <- atomically $ newTVar []
users <- atomically $ newTVar []
chans <- atomically $ newTVar []
echoEnd <- spawnEcho conns
return $ IrcServer config conns users chans (cmdHandler echoEnd)
startIrcServer :: IrcServer -> IrcHandler -> IO (Async ())
startIrcServer server handler =
async $ listen sHost sPort handler
where
sHost = ircHost . ircConfig $ server
sPort = ircPort . ircConfig $ server

View File

@@ -0,0 +1,71 @@
{-# LANGUAGE RecordWildCards #-}
module Pipes.IRC.Server.Types
( HostPreference(..)
, IrcHandler
, IrcMessage
, IrcConnection(..)
, IrcConfig(..)
, IrcServer(..)
, IrcUser(..)
, IrcChannel(..)
) where
import Control.Concurrent.STM (TVar)
import Data.ByteString (ByteString)
import Pipes (Consumer)
import Pipes.Concurrent (Output)
import Pipes.IRC.Message.Types (IrcMessage)
import Pipes.Network.TCP (HostPreference (..), ServiceName,
SockAddr, Socket)
data IrcServer =
IrcServer { ircConfig :: !IrcConfig
, ircConnections :: !(TVar [IrcConnection IrcMessage])
, ircUsers :: !(TVar [IrcUser])
, ircChannels :: !(TVar [IrcChannel])
, ircHandler :: !(Consumer IrcMessage IO ())
}
data IrcConfig =
IrcConfig { ircPort :: !ServiceName
, ircHost :: !HostPreference
} deriving (Show)
data IrcConnection a =
IrcConnection { sock :: !Socket
, addr :: !SockAddr
, out :: !(Output a)
}
instance Eq (IrcConnection a) where
(IrcConnection s1 _ _) == (IrcConnection s2 _ _) = s1 == s2
instance Show (IrcConnection a) where
show IrcConnection {..} =
"IrcConnection " ++ show sock ++ " " ++ show addr
type IrcHandler = (Socket, SockAddr) -> IO ()
data IrcUser =
IrcUser { userNick :: !ByteString
, userClientId :: !ByteString
, userServerName :: !ByteString
, userName :: !ByteString
, userHostName :: !ByteString
, userModes :: ![IrcUserMode]
, userConn :: !(IrcConnection IrcMessage)
} deriving (Show, Eq)
data IrcUserMode = Away | Invisible | WallOps | Restricted
| Oper | LocalOper | ServerNotices
deriving (Show, Eq, Enum)
data IrcChannel =
IrcChannel { chanName :: !ByteString
, chanTopic :: !ByteString
, chanModes :: ![IrcChanMode]
, chanUsers :: ![IrcUser]
} deriving (Show, Eq)
data IrcChanMode = Anonymous | InviteOnly | Moderated | Quiet | Secret
deriving (Show, Eq, Enum)