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.master
commit
41b457dd30
|
@ -0,0 +1,30 @@
|
||||||
|
Copyright (c) 2013, Levi Pearson
|
||||||
|
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
|
||||||
|
* Neither the name of Levi Pearson nor the names of other
|
||||||
|
contributors may be used to endorse or promote products derived
|
||||||
|
from this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -0,0 +1,220 @@
|
||||||
|
ABNF from rfc2812:
|
||||||
|
------------------
|
||||||
|
|
||||||
|
message = [ ":" prefix SPACE ] command [ params ] crlf
|
||||||
|
prefix = servername / ( nickname [ [ "!" user ] "@" host ] )
|
||||||
|
command = 1*letter / 3digit
|
||||||
|
params = *14( SPACE middle ) [ SPACE ":" trailing ]
|
||||||
|
=/ 14( SPACE middle ) [ SPACE [ ":" ] trailing ]
|
||||||
|
|
||||||
|
nospcrlfcl = %x01-09 / %x0B-0C / %x0E-1F / %x21-39 / %x3B-FF
|
||||||
|
; any octet except NUL, CR, LF, " " and ":"
|
||||||
|
middle = nospcrlfcl *( ":" / nospcrlfcl )
|
||||||
|
trailing = *( ":" / " " / nospcrlfcl )
|
||||||
|
|
||||||
|
SPACE = %x20 ; space character
|
||||||
|
crlf = %x0D %x0A ; "carriage return" "linefeed"
|
||||||
|
|
||||||
|
target = nickname / server
|
||||||
|
msgtarget = msgto *( "," msgto )
|
||||||
|
msgto = channel / ( user [ "%" host ] "@" servername )
|
||||||
|
msgto =/ ( user "%" host ) / targetmask
|
||||||
|
msgto =/ nickname / ( nickname "!" user "@" host )
|
||||||
|
channel = ( "#" / "+" / ( "!" channelid ) / "&" ) chanstring
|
||||||
|
[ ":" chanstring ]
|
||||||
|
servername = hostname
|
||||||
|
host = hostname / hostaddr
|
||||||
|
hostname = shortname *( "." shortname )
|
||||||
|
shortname = ( letter / digit ) *( letter / digit / "-" )
|
||||||
|
*( letter / digit )
|
||||||
|
; as specified in RFC 1123 [HNAME]
|
||||||
|
hostaddr = ip4addr / ip6addr
|
||||||
|
ip4addr = 1*3digit "." 1*3digit "." 1*3digit "." 1*3digit
|
||||||
|
ip6addr = 1*hexdigit 7( ":" 1*hexdigit )
|
||||||
|
ip6addr =/ "0:0:0:0:0:" ( "0" / "FFFF" ) ":" ip4addr
|
||||||
|
nickname = ( letter / special ) *8( letter / digit / special / "-" )
|
||||||
|
targetmask = ( "$" / "#" ) mask
|
||||||
|
; see details on allowed masks in section 3.3.1
|
||||||
|
chanstring = %x01-07 / %x08-09 / %x0B-0C / %x0E-1F / %x21-2B
|
||||||
|
chanstring =/ %x2D-39 / %x3B-FF
|
||||||
|
; any octet except NUL, BELL, CR, LF, " ", "," and ":"
|
||||||
|
channelid = 5( %x41-5A / digit ) ; 5( A-Z / 0-9 )
|
||||||
|
|
||||||
|
user = 1*( %x01-09 / %x0B-0C / %x0E-1F / %x21-3F / %x41-FF )
|
||||||
|
; any octet except NUL, CR, LF, " " and "@"
|
||||||
|
key = 1*23( %x01-05 / %x07-08 / %x0C / %x0E-1F / %x21-7F )
|
||||||
|
; any 7-bit US_ASCII character,
|
||||||
|
; except NUL, CR, LF, FF, h/v TABs, and " "
|
||||||
|
letter = %x41-5A / %x61-7A ; A-Z / a-z
|
||||||
|
digit = %x30-39 ; 0-9
|
||||||
|
hexdigit = digit / "A" / "B" / "C" / "D" / "E" / "F"
|
||||||
|
special = %x5B-60 / %x7B-7D
|
||||||
|
; "[", "]", "\", "`", "_", "^", "{", "|", "}"
|
||||||
|
|
||||||
|
mask = *( nowild / noesc wildone / noesc wildmany )
|
||||||
|
wildone = %x3F
|
||||||
|
wildmany = %x2A
|
||||||
|
nowild = %x01-29 / %x2B-3E / %x40-FF
|
||||||
|
; any octet except NUL, "*", "?"
|
||||||
|
noesc = %x01-5B / %x5D-FF
|
||||||
|
; any octet except NUL and "\"
|
||||||
|
matchone = %x01-FF
|
||||||
|
; matches wildone
|
||||||
|
matchmany = *matchone
|
||||||
|
; matches wildmany
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
a?c ; Matches any string of 3 characters in length starting
|
||||||
|
with "a" and ending with "c"
|
||||||
|
|
||||||
|
a*c ; Matches any string of at least 2 characters in length
|
||||||
|
starting with "a" and ending with "c"
|
||||||
|
|
||||||
|
Server State
|
||||||
|
------------
|
||||||
|
|
||||||
|
* List of local connections, which may belong to clients (users and
|
||||||
|
services) or other servers
|
||||||
|
|
||||||
|
* List of servers in the network
|
||||||
|
* Server name, max of 63 chars
|
||||||
|
* Hostmasks can group servers with matching names into a 'virtual
|
||||||
|
server' seen by non-matching servers
|
||||||
|
* Hopcount
|
||||||
|
* Peer identifier token
|
||||||
|
* Link flags
|
||||||
|
|
||||||
|
* List of clients in the network
|
||||||
|
* Network-unique identifier; format depends on type
|
||||||
|
* Server to which the client is connected
|
||||||
|
|
||||||
|
* List of users in the network
|
||||||
|
* Has a unique nickname on the network (std says 9 chars max)
|
||||||
|
* Name of the host the user is running on
|
||||||
|
* Username of user on that host
|
||||||
|
* Server the user is connected to
|
||||||
|
|
||||||
|
* List of services in the network known by the server
|
||||||
|
* Service name = nickname + server name
|
||||||
|
* Service type
|
||||||
|
|
||||||
|
* List of channels in the network known by the server
|
||||||
|
* Channel name (prefix + up to 49 other chars)
|
||||||
|
* Channel members
|
||||||
|
* Channel modes
|
||||||
|
|
||||||
|
* Recent nickname changes
|
||||||
|
|
||||||
|
* Nickname re-use delay
|
||||||
|
|
||||||
|
Connection State
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* A new connection starts out as Unregistered
|
||||||
|
|
||||||
|
* A password before registering is *optional*
|
||||||
|
|
||||||
|
* Connection can register as a user, service, or server
|
||||||
|
|
||||||
|
* Once registered, a client is told the id it was registered with
|
||||||
|
|
||||||
|
* Idle connections are periodically sent a PING and are disconnected
|
||||||
|
if a PONG is not received in reply
|
||||||
|
|
||||||
|
* Flood control applicability and timer information
|
||||||
|
|
||||||
|
User State
|
||||||
|
----------
|
||||||
|
|
||||||
|
* User has a nickname that is first set during registration, but can
|
||||||
|
later change.
|
||||||
|
|
||||||
|
* User has mode flags that can also be set during registration and
|
||||||
|
change during operation.
|
||||||
|
* a: away (set by AWAY, not MODE)
|
||||||
|
* i: invisible
|
||||||
|
* w: receives wallops
|
||||||
|
* r: restricted connection (can't be cleared with MODE, but can be set)
|
||||||
|
* o: operator (set by OPER, cleared with MODE)
|
||||||
|
* O: local operator (set by OPER, cleared with MODE)
|
||||||
|
* s: receives server notices
|
||||||
|
|
||||||
|
* User has a user name, hostname, and real name. These are set at
|
||||||
|
registration and do not change thereafter.
|
||||||
|
|
||||||
|
* User has a hopcount to determine distance from local server
|
||||||
|
|
||||||
|
* Connection by which user can be sent messages
|
||||||
|
|
||||||
|
Service State
|
||||||
|
-------------
|
||||||
|
|
||||||
|
* Service name
|
||||||
|
|
||||||
|
* Servertoken
|
||||||
|
|
||||||
|
* Distribution mask
|
||||||
|
|
||||||
|
* Hopcount
|
||||||
|
|
||||||
|
* Type
|
||||||
|
|
||||||
|
Channel State
|
||||||
|
-------------
|
||||||
|
|
||||||
|
* Channels are typically created when users join them
|
||||||
|
|
||||||
|
* Services cannot join channels
|
||||||
|
|
||||||
|
* A channel has a name
|
||||||
|
* Up to 50 chars, first char is prefix
|
||||||
|
* &: local to single server
|
||||||
|
* #: normal distribution
|
||||||
|
* +: no channel modes
|
||||||
|
* !: safe channels
|
||||||
|
|
||||||
|
* A channel (that is not a local-only channel) may have a channel mask
|
||||||
|
to limit distribution
|
||||||
|
|
||||||
|
* A channel has a topic
|
||||||
|
|
||||||
|
* A channel has a set of joined users
|
||||||
|
|
||||||
|
* Channels may require keys to join
|
||||||
|
|
||||||
|
* Channels have modes, some of which are flags and some of which are
|
||||||
|
records of management-related data, such as a list of channel ops.
|
||||||
|
|
||||||
|
* Channels may keep track of users that have been invited when they
|
||||||
|
are set +i (invite-only)
|
||||||
|
|
||||||
|
Connection Registration
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
Valid combinations of registration commands are:
|
||||||
|
|
||||||
|
NICK -> USER
|
||||||
|
PASS -> NICK -> USER
|
||||||
|
NICK -> PASS -> USER
|
||||||
|
PASS -> SERVICE
|
||||||
|
SERVICE
|
||||||
|
|
||||||
|
When registration succeeds for a user, the server sends the following:
|
||||||
|
|
||||||
|
RPL_WELCOME
|
||||||
|
RPL_YOURHOST
|
||||||
|
RPL_CREATED
|
||||||
|
RPL_MYINFO
|
||||||
|
LUSER
|
||||||
|
MOTD
|
||||||
|
|
||||||
|
When registration succeds for a service, the server sends the
|
||||||
|
following:
|
||||||
|
|
||||||
|
RPL_YOURESERVICE
|
||||||
|
RPL_YOURHOST
|
||||||
|
RPL_MYINFO
|
||||||
|
|
||||||
|
The server then informs any other servers it is connected to of the
|
||||||
|
new client.
|
|
@ -0,0 +1,37 @@
|
||||||
|
-- Initial pipes-irc-server.cabal generated by cabal init. For further
|
||||||
|
-- documentation, see http://haskell.org/cabal/users-guide/
|
||||||
|
|
||||||
|
name: pipes-irc-server
|
||||||
|
version: 0.1.0.0
|
||||||
|
synopsis: IRC server based on pipes
|
||||||
|
-- description:
|
||||||
|
license: BSD3
|
||||||
|
license-file: LICENSE
|
||||||
|
author: Levi Pearson
|
||||||
|
maintainer: levipearson@gmail.com
|
||||||
|
-- copyright:
|
||||||
|
category: Network
|
||||||
|
build-type: Simple
|
||||||
|
-- extra-source-files:
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
|
executable pipes-irc-server
|
||||||
|
main-is: Main.hs
|
||||||
|
-- other-modules:
|
||||||
|
-- other-extensions:
|
||||||
|
build-depends: base >= 4.6 && < 4.7
|
||||||
|
, bytestring >= 0.10 && < 0.11
|
||||||
|
, text >= 0.11.3 && < 0.12
|
||||||
|
, attoparsec >= 0.10 && < 0.11
|
||||||
|
, pipes >= 4 && < 5
|
||||||
|
, pipes-concurrency >= 2 && < 3
|
||||||
|
, pipes-bytestring >= 1.0 && < 2
|
||||||
|
, pipes-parse >= 2.0 && < 3
|
||||||
|
, pipes-attoparsec >= 0.3 && < 1
|
||||||
|
, pipes-network >= 0.6 && < 1
|
||||||
|
, stm >= 2 && < 3
|
||||||
|
, async >= 2 && < 3
|
||||||
|
, free >= 3 && < 4
|
||||||
|
|
||||||
|
hs-source-dirs: src
|
||||||
|
default-language: Haskell2010
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
||||||
|
module Pipes.IRC.Message
|
||||||
|
( parseMessage
|
||||||
|
, module Pipes.IRC.Message.Types
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Pipes.IRC.Message.Types
|
||||||
|
|
||||||
|
parseMessage = undefined
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
Loading…
Reference in New Issue