packages feed

hulk 0.1.2 → 0.2.0

raw patch · 26 files changed

+1481/−1154 lines, 26 filesdep +aesondep +bytestringdep +case-insensitivedep −ircdep ~ConfigFiledep ~Cryptodep ~base

Dependencies added: aeson, bytestring, case-insensitive, directory, fastirc, filepath, ghc-prim, monad-extras, strict, text

Dependencies removed: irc

Dependency ranges changed: ConfigFile, Crypto, base, cmdargs, containers, mtl, network, split, time, unix, utf8-string

Files

README.md view
@@ -14,12 +14,18 @@  ## Usage +See a complete example in the `example/` directory in the root of the+project. You can go into that directory and run `hulk -chulk.conf` to+have an immediate server running on localhost port 6667.+ ### Configuration -    $ cp hulk.conf.example hulk.conf-    +    $ cp example/hulk.conf hulk.conf+ You can edit the port, server name and MOTD file in here.-    ++Detailed instructions follow.+ ### Auth      $ mkdir auth@@ -67,9 +73,9 @@     [hulk]     connect = 127.0.0.1:6666     accept = 6667-    + Then run it:-    +     stunnel stunnel.conf  (It may be in `/usr/sbin/stunnel` depending on your system.)@@ -105,21 +111,3 @@      $ rss2irc http://myserver/foo.atom announce@127.0.0.1/#dev \       -p 6667 -i 1 -l  --pass myannouncepass--## Utility for 'telling' people things externally--Sometimes you want automatic scripts to send notices to the IRC.--Save this script in `tell.sh`:--    (cat /mypath/tell-login; echo "NICK $1"; echo "TELL $2 :$3") | nc serverip 6667 -w 1 > /dev/null--Run it like this:--    $ tell announce '#dev' 'Stuff happened!'--And put this in your /mypath/tell-login:--    $ cat /opt/hulk/tell-login-    PASS mypassword-    USER myuser * * *
+ example/auth/passwd view
@@ -0,0 +1,2 @@+hulk bac95aac1e4e1a3791b4aa044be19bb11b3b+hulk2 bac95aac1e4e1a3791b4aa044be19bb11b3b
+ example/auth/passwd-key view
@@ -0,0 +1,1 @@+:-)
+ example/hulk.conf view
@@ -0,0 +1,18 @@+[LISTEN]+port = 6667+hostname = hulk-server++[STRINGS]+motd_file = txt/MOTD+preface_file = txt/PREFACE++[AUTH]+passwd_file = auth/passwd+passwd_key = auth/passwd-key++[USERS]+data_dir=users/++[LOGS]+event_log=log/events.log+channels=#hulk
+ example/log/events.log view
@@ -0,0 +1,32 @@+["2013-11-02T17:06:35.619Z","hulk","RPL_PRIVMSG",["#hulk","Hello!"]]+["2013-11-03T01:53:58.106Z","hulk2","RPL_PRIVMSG",["#hulk","hrm"]]+["2013-11-03T01:54:02.393Z","hulk","RPL_PRIVMSG",["#hulk","yeah"]]+["2013-11-03T01:57:28.112Z","hulk2","RPL_PRIVMSG",["#hulk","2"]]+["2013-11-03T01:57:30.952Z","hulk","RPL_PRIVMSG",["#hulk","sdf"]]+["2013-11-03T01:57:35.393Z","hulk2","RPL_PRIVMSG",["#hulk","hmer"]]+["2013-11-03T01:57:38.809Z","hulk","RPL_PRIVMSG",["#hulk","heymar"]]+["2013-11-03T01:58:44.009Z","hulk2","RPL_PRIVMSG",["#hulk","sfsdfsdf"]]+["2013-11-03T01:58:44.114Z","hulk","RPL_PRIVMSG",["#hulk","sdf"]]+["2013-11-03T01:59:06.823Z","hulk","RPL_PRIVMSG",["#hulk","hello"]]+["2013-11-03T01:59:09.807Z","hulk2","RPL_PRIVMSG",["#hulk","hai"]]+["2013-11-03T01:59:30.808Z","hulk","RPL_PRIVMSG",["#hulk","apples"]]+["2013-11-03T01:59:33.231Z","hulk2","RPL_PRIVMSG",["#hulk","pears"]]+["2013-11-03T01:59:37.615Z","hulk","RPL_PRIVMSG",["#hulk","bacon"]]+["2013-11-03T01:59:41.175Z","hulk2","RPL_PRIVMSG",["#hulk","chop liver"]]+["2013-11-03T01:59:44.591Z","hulk","RPL_PRIVMSG",["#hulk","eh?"]]+["2013-11-03T01:59:48.471Z","hulk2","RPL_PRIVMSG",["#hulk","hurrr"]]+["2013-11-03T02:00:13.543Z","hulk","RPL_PRIVMSG",["#hulk","zer"]]+["2013-11-03T02:00:16.591Z","hulk2","RPL_PRIVMSG",["#hulk","var"]]+["2013-11-03T02:03:08.485Z","hulk2","RPL_PRIVMSG",["#hulk","hello"]]+["2013-11-03T02:03:10.813Z","hulk","RPL_PRIVMSG",["#hulk","world"]]+["2013-11-03T02:03:12.073Z","hulk","RPL_PRIVMSG",["#hulk","world"]]+["2013-11-03T02:03:13.581Z","hulk2","RPL_PRIVMSG",["#hulk","yup"]]+["2013-11-03T02:21:41.371Z","hulk","RPL_PRIVMSG",["#hulk","test"]]+["2013-11-03T02:32:02.087Z","hulk","RPL_PRIVMSG",["#foo","hey"]]+["2013-11-03T02:32:19.997Z","hulk","RPL_PRIVMSG",["hulk","sup"]]+["2013-11-03T02:41:55.108Z","hulk","RPL_PRIVMSG",["#hulk","huh"]]+["2013-11-03T02:44:26.426Z","hulk","RPL_PRIVMSG",["#hulk","jl"]]+["2013-11-03T02:45:20.923Z","hulk","RPL_PRIVMSG",["#hulk",",."]]+["2013-11-03T03:35:31.801Z","hulk2","RPL_PRIVMSG",["#hulk","dfg"]]+["2013-11-03T03:35:34.505Z","hulk","RPL_PRIVMSG",["#hulk","berk"]]+["2013-11-03T03:35:36.777Z","hulk2","RPL_PRIVMSG",["#hulk","heh"]]
+ example/txt/MOTD view
@@ -0,0 +1,7 @@+Hello and welcome to this IRC server. I am your host, Dr. Bruce Banner+PhD in nuclear physics and HRRRRRNNNNNNNNNNNNGGGGGGGGGGGGGGGGGGGGGGGGG+HRRRRRNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN+NNRUN FOR YOUR LIFENNNNNNNNNNNNNNNNNNNNNNNNNGGGGGGGGNNNNNNNNNNNNNNNNNN+NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN+GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG+GG..
+ example/txt/PREFACE view
@@ -0,0 +1,14 @@+You must login with a username and password. If you have not already+created one, you should do so by generating one and adding it to the+Hulk passwd file in passwd. To generate a user/password+key/pair, use:++ $ genpass myusername++It will prompt for a password. Inspect it first to confirm it is+correct. To write it to the passwd file use++ $ genpass myusername >> /path/to/hulk/data/passwd++And configure your IRC client to use your username and to send a+*server* password.
+ example/users/hulk view
@@ -0,0 +1,1 @@+{"userDataUser":"hulk","userDataLastSeen":"2013-11-03T02:51:50.980Z"}
+ example/users/hulk2 view
@@ -0,0 +1,1 @@+{"userDataUser":"hulk2","userDataLastSeen":"2013-11-03T02:30:29.083Z"}
hulk.cabal view
@@ -1,55 +1,93 @@ Name:                hulk-Version:             0.1.2-Synopsis:            IRC daemon.-Description:         An IRC daemon with mandatory authentication.+Version:             0.2.0+Synopsis:            IRC server written in Haskell.+Description:         An IRC server with (mandatory) server authentication, log recall, bumping.+                     Intended for private business use or hobby work. License:             BSD3+Stability:           Stable License-file:        LICENSE Author:              Chris Done Maintainer:          chrisdone@gmail.com-Copyright:           2010 Chris Done +Copyright:           2010-2013 Chris Done Category:            Network Build-type:          Simple Cabal-version:       >=1.2-Extra-source-files:  README.md, hulk.conf.example, txt/MOTD, txt/PREFACE+Extra-source-files:+  README.md+  example/log/events.log+  example/auth/passwd+  example/auth/passwd-key+  example/txt/PREFACE+  example/txt/MOTD+  example/hulk.conf+  example/users/hulk2+  example/users/hulk +Library+  Exposed-Modules:+     Hulk.Server+     Hulk.Options+     Hulk.Client+     Hulk.Config+     Hulk.Types+     Hulk.Auth+  Ghc-options:       -threaded -O2 -Wall+  Hs-Source-Dirs:    src+  Build-depends:     base >= 4 && <5,+                     text,+                     ConfigFile,+                     fastirc,+                     network,+                     ghc-prim,+                     time,+                     containers,+                     aeson,+                     mtl,+                     utf8-string,+                     Crypto,+                     cmdargs,+                     strict,+                     filepath,+                     directory,+                     bytestring,+                     split,+                     unix,+                     case-insensitive,+                     monad-extras == 0.3.2.0+ Executable hulk   Main-is:           Main.hs-  Other-Modules:     Control.Concurrent.Delay, Control.Monad.IO,-                     Data.String,-                     Hulk.Auth, Hulk.Client, Hulk.Concurrent, Hulk.Config,-                     Hulk.Event, Hulk.Log, Hulk.Options, Hulk.Providers,-                     Hulk.Server, Hulk.Types-  Ghc-options:       -threaded+  Other-Modules:     Hulk.Auth,+                     Hulk.Client,+                     Hulk.Config,+                     Hulk.Server,+                     Hulk.Types+  Ghc-options:       -threaded -O2 -Wall   Hs-Source-Dirs:    src-  Build-depends:     base >= 4 && <5-                    ,cmdargs >= 0.6 && <0.7-                    ,network >= 2.2 && < 2.4-                    ,ConfigFile >= 1.0 && < 1.1-                    ,unix >= 2.4 && < 2.5-                    ,mtl >= 1.1 && < 1.2-                    ,containers >= 0.3 && < 0.4-                    ,irc >= 0.4 && < 0.5-                    ,split >= 0.1 && < 0.2-                    ,utf8-string >= 0.3 && < 0.4-                    ,Crypto>=4.2 && < 4.3-                    ,time >= 1.1 && < 1.2+  Build-depends:     base >= 4 && <5,+                     text,+                     ConfigFile,+                     fastirc,+                     network,+                     ghc-prim,+                     time,+                     containers,+                     aeson,+                     mtl,+                     utf8-string,+                     Crypto,+                     cmdargs,+                     strict,+                     filepath,+                     directory,+                     bytestring,+                     split,+                     unix,+                     case-insensitive,+                     monad-extras == 0.3.2.0  Executable hulk-generate-pass   Main-is:          GeneratePass.hs-  Other-Modules:     Control.Concurrent.Delay, Control.Monad.IO,-                     Data.String,-                     Hulk.Auth, Hulk.Client, Hulk.Concurrent, Hulk.Config,-                     Hulk.Event, Hulk.Log, Hulk.Options, Hulk.Providers,-                     Hulk.Server, Hulk.Types   Hs-Source-Dirs:   src-  Build-depends:    base >= 4 && <5-                    ,cmdargs >= 0.6 && <0.7-                    ,network >= 2.2 && < 2.4-                    ,ConfigFile >= 1.0 && < 1.1-                    ,unix >= 2.4 && < 2.5-                    ,mtl >= 1.1 && < 1.2-                    ,containers >= 0.3 && < 0.4-                    ,irc >= 0.4 && < 0.5-                    ,split >= 0.1 && < 0.2-                    ,utf8-string >= 0.3 && <0.4-                    ,Crypto>=4.2 && <4.3+  Build-depends:    base+  Ghc-options:       -threaded -O2 -Wall
− hulk.conf.example
@@ -1,11 +0,0 @@-[LISTEN]-port = 6667-hostname = myhostname--[STRINGS]-motd_file = txt/MOTD-preface_file = txt/PREFACE--[AUTH]-passwd_file = auth/passwd-passwd_key = auth/passwd-key
− src/Control/Concurrent/Delay.hs
@@ -1,10 +0,0 @@-module Control.Concurrent.Delay where--import Control.Concurrent--delaySeconds :: Integer -> IO ()-delaySeconds 0 = return ()-delaySeconds n = do threadDelay (1000 * 1000); delaySeconds (n-1)--delayMinutes :: Integer -> IO ()-delayMinutes = delaySeconds . (*60)
− src/Control/Monad/IO.hs
@@ -1,6 +0,0 @@-module Control.Monad.IO (io) where--import Control.Monad.Trans--io :: MonadIO m => IO a -> m a-io = liftIO
− src/Data/String.hs
@@ -1,8 +0,0 @@-module Data.String where--import Data.List-import Data.Char--downcase = map toLower--digilet c = isDigit c || isLetter c
src/GeneratePass.hs view
@@ -2,16 +2,16 @@ {-# OPTIONS -Wall -fno-warn-missing-signatures -fno-warn-name-shadowing #-} module Main where -import System.Console.CmdArgs-import Data.HMAC-import Codec.Binary.UTF8.String-import Numeric-import Data.String-import Control.Applicative- import Hulk.Types import Hulk.Config +import Codec.Binary.UTF8.String+import Control.Applicative+import Data.Char+import Data.HMAC+import Numeric+import System.Console.CmdArgs+ data Options = Options   { conf      :: FilePath   , user      :: String@@ -35,3 +35,6 @@   putStrLn $ ((user options ++ " ") ++)            $ concat $ map (\x -> showHex x "")            $ hmac_sha1 (encode key) (encode pass)++  where downcase = map toLower+        digilet c = isDigit c || isLetter c
src/Hulk/Auth.hs view
@@ -1,25 +1,39 @@-{-# OPTIONS -Wall -fno-warn-name-shadowing #-}-module Hulk.Auth where+-- | Password authorization for the server. +module Hulk.Auth+  (authenticate+  ,sha1)+  where+ import Codec.Binary.UTF8.String import Control.Arrow-import Control.Monad+ import Data.Char import Data.HMAC+import Data.Text (Text,unpack) import Numeric -import Hulk.Types+-- | Authenticate the user.+authenticate :: String -- ^ Salt.+             -> String -- ^ Password file.+             -> Text   -- ^ User.+             -> Text   -- ^ Password.+             -> Bool   -- ^ Authenticated?+authenticate keystr passwords user pass =+  any (== (unpack user,sha1 key (unpack pass)))+      passwds -sha1 :: String -> String -> String-sha1 key str =-  concat $ map (\x -> showHex x "")-         $ hmac_sha1 (encode key) (encode str)+  where key = filter keyChars keystr+        passwds = getPasswds passwords -authenticate :: MonadProvider m => String -> String -> m Bool-authenticate user pass = do-  key <- filter keyChars `liftM` provideKey-  passwds <- liftM getPasswds $ providePasswords-  return $ any (== (user,sha1 key pass)) passwds-  where getPasswds = map readPair . lines+        getPasswds = map readPair . lines             where readPair = second (drop 1) . span (/=' ')         keyChars c = elem c ['a'..'z'] || isDigit c++-- | Make a sha1 string with the given salt.+sha1 :: String -- ^ Salt.+     -> String -- ^ String.+     -> String -- ^ Hashed string.+sha1 key str =+  concat $ map (\x -> showHex x "")+         $ hmac_sha1 (encode key) (encode str)
src/Hulk/Client.hs view
@@ -1,697 +1,871 @@-{-# LANGUAGE RecordWildCards, ViewPatterns #-}-{-# OPTIONS -Wall -fno-warn-name-shadowing #-}-module Hulk.Client -    (handleLine)-    where--import           Control.Applicative-import           Control.Monad.Reader-import           Control.Monad.State-import           Control.Monad.Writer-import           Data.Char-import           Data.List.Split-import           Data.Map             (Map)-import qualified Data.Map             as M-import           Data.Maybe-import           Data.Time-import           Network.IRC          hiding (Channel)-import           Prelude              hiding (log)--import           Hulk.Auth-import           Hulk.Event-import           Hulk.Types---- Entry point handler---- | Handle an incoming line, try to parse it and handle the message.-handleLine :: MonadProvider m-              => Env -> UTCTime -> Conn -> String -              -> m ([Reply],Env)-handleLine env now conn line = runClient env now conn $-  case decode line of-    Just Message{..} -> handleMsg line (readEventType msg_command,msg_params)-    Nothing  -> errorReply $ "Unable to parse " ++ show line---- | Run the client monad.    -runClient :: MonadProvider m -          => Env -> UTCTime -> Conn -> IRC m () -          -> m ([Reply],Env)-runClient env now conn m = do-  flip runStateT env $ -    execWriterT $ -      flip runReaderT (now,conn) $-        runIRC m---- | Handle an incoming message.-handleMsg :: MonadProvider m => String -> (Event,[String]) -> IRC m ()-handleMsg line msg = do-  case msg of-    (NOTHING,_)   -> invalidMessage line-    (PASS,[pass]) -> asUnregistered $ handlePass pass-    (PINGPONG,_)  -> do incoming line; handlePingPong-    safeToLog     -> handleMsgSafeToLog line safeToLog-    --- | Handle messages that are safe to log.-handleMsgSafeToLog :: MonadProvider m => String -> (Event,[String]) -                   -> IRC m ()-handleMsgSafeToLog line safeToLog = do-  incoming line-  updateLastPong-  case safeToLog of-    (PONG,_)      -> return ()-    (USER,[user,_,_,realname]) -> asUnregistered $ handleUser user realname-    (NICK,[nick])   -> handleNick nick-    (PING,[param])  -> handlePing param-    (QUIT,[msg])    -> handleQuit RequestedQuit msg-    (TELL,[to,msg]) -> handleTell to msg-    (DISCONNECT,[msg]) -> handleQuit SocketQuit msg-    (CONNECT,_)        -> handleConnect-    mustBeReg'd -> handleMsgReg'd line mustBeReg'd---- | Handle messages that can only be used when registered.-handleMsgReg'd :: Monad m => String -> (Event,[String]) -> IRC m ()-handleMsgReg'd line mustBeReg'd =-  asRegistered $-   case mustBeReg'd of-     (JOIN,(name:_))    -> handleJoin name-     (PART,[chan,msg])  -> handlePart chan msg-     (PRIVMSG,[to,msg]) -> handlePrivmsg to msg-     (TOPIC,[chan,topic]) -> handleTopic chan topic-     (NOTICE,[to,msg])  -> handleNotice to msg-     (WHOIS,(nick:_))   -> handleWhoIs nick-     (ISON,people)      -> handleIsOn people-     _                  -> invalidMessage line---- | Log an invalid message.-invalidMessage :: Monad m => String -> IRC m ()-invalidMessage line = do-    incoming line-    errorReply $ "Invalid or unknown message type, or not" ++ -                 " enough parameters: " ++ show line---- Message handlers---- | Handle the PINGPONG event. Disconnect the client if timedout.-handlePingPong :: Monad m => IRC m ()-handlePingPong = do-  lastPong <- clientLastPong <$> getClient-  now <- asks fst-  let n = diffUTCTime now lastPong-  if n > 60*4-     then handleQuit RequestedQuit $ "Ping timeout: " ++ show n ++ " seconds"-     else do hostname <- asks $ connServerName . snd-             thisCmdReply RPL_PING [hostname]---- | Handle the CONNECT event.-handleConnect :: MonadProvider m => IRC m ()-handleConnect = do-  motd <- fmap lines <$> lift providePreface-  case motd of-    Nothing -> return ()-    Just lines -> mapM_ notice lines---- | Handle the PASS message.-handlePass :: MonadProvider m => String -> IRC m ()-handlePass pass = do-  modifyUnregistered $ \u -> u { unregUserPass = Just pass }-  notice "Received password."-  tryRegister---- | Handle the USER message.-handleUser :: MonadProvider m => String -> String -> IRC m ()-handleUser user realname = do-  withSentPass $-    if validUser user-       then do modifyUnregistered $ \u -> u { unregUserUser = Just user-                                            , unregUserName = Just realname }-               notice "Recieved user details."-               tryRegister-       else errorReply "Invalid user format."---- | Handle the USER message.-handleNick :: MonadProvider m => String -> IRC m ()-handleNick nick =-  withSentPass $-    withValidNick nick $ \nick ->-      ifUniqueNick nick $ do-        ref <- getRef-        withRegistered $ \RegUser{regUserNick=nick} -> -          modifyNicks $ M.delete nick-        withUnegistered $ \UnregUser{unregUserNick=nick} -> -          maybe (return ()) (modifyNicks . M.delete) nick-        modifyNicks $ M.insert nick ref-        modifyUnregistered $ \u -> u { unregUserNick = Just nick }-        tryRegister-        asRegistered $ do-          thisClientReply RPL_NICK [unNick nick]-          (myChannels >>=) $ mapM_ $ \Channel{..} -> do-            channelReply channelName RPL_NICK [unNick nick] ExcludeMe-          modifyRegistered $ \u -> u { regUserNick = nick }---- | Handle the PING message.-handlePing :: Monad m => String -> IRC m ()-handlePing p = do-  hostname <- asks $ connServerName . snd-  thisServerReply RPL_PONG [hostname,p]---- | Handle the QUIT message.-handleQuit :: Monad m => QuitType -> String -> IRC m ()-handleQuit quitType msg = do- (myChannels >>=) $ mapM_ $ \Channel{..} -> do-   channelReply channelName RPL_QUIT [msg] ExcludeMe-   removeFromChan channelName- withRegistered $ \RegUser{regUserNick=nick} -> do-   modifyNicks $ M.delete nick- withUnegistered $ \UnregUser{unregUserNick=nick} -> do-   maybe (return ()) (modifyNicks . M.delete) nick- ref <- getRef- modifyClients $ M.delete ref- notice msg- when (quitType == RequestedQuit) $ tell [Close]---- | Handle the TELL message.-handleTell :: Monad m => String -> String -> IRC m ()-handleTell name msg = sendMsgTo RPL_NOTICE name msg---- | Handle the JOIN message.-handleJoin :: Monad m => String -> IRC m ()-handleJoin chans = do-  let names = splitWhen (==',') chans-  forM_ names $ flip withValidChanName $ \name -> do-      exists <- M.member name <$> gets envChannels-      unless exists $ insertChannel name-      joined <- inChannel name-      unless joined $ joinChannel name---- | Handle the PART message.-handlePart :: Monad m => String -> String -> IRC m ()-handlePart name msg =-  withValidChanName name $ \name -> do-    removeFromChan name-    channelReply name RPL_PART [msg] IncludeMe---- | Remove a user from a channel.-removeFromChan :: Monad m => ChannelName -> IRC m ()-removeFromChan name = do-  ref <- getRef-  let remMe c = c { channelUsers = filter (/=ref) (channelUsers c) }-  modifyChannels $ M.adjust remMe name---- | Handle the TOPIC message.-handleTopic :: Monad m => String -> String -> IRC m ()-handleTopic name topic =-  withValidChanName name $ \name -> do-    let setTopic c = c { channelTopic = Just topic }-    modifyChannels $ M.adjust setTopic name-    channelReply name RPL_TOPIC [unChanName name,topic] IncludeMe---- | Handle the PRIVMSG message.-handlePrivmsg :: Monad m => String -> String -> IRC m ()-handlePrivmsg name msg = sendMsgTo RPL_PRIVMSG name msg---- | Handle the NOTICE message.-handleNotice :: Monad m => String -> String -> IRC m ()-handleNotice name msg = sendMsgTo RPL_NOTICE name msg---- | Handle WHOIS message.-handleWhoIs :: Monad m => String -> IRC m ()-handleWhoIs nick =-  withValidNick nick $ \nick ->-    withClientByNick nick $ \Client{..} ->-      withRegUserByNick nick $ \RegUser{..} -> do-        thisNickServerReply RPL_WHOISUSER -                            [unNick regUserNick-                            ,regUserUser-                            ,clientHostname-                            ,"*"-                            ,regUserName]-        thisNickServerReply RPL_ENDOFWHOIS-                            [unNick regUserNick-                            ,"End of WHOIS list."]---- | Handle the ISON ('is on?') message.-handleIsOn :: Monad m => [String] -> IRC m ()-handleIsOn (catMaybes . map readNick -> nicks) =-  asRegistered $ do-    online <- catMaybes <$> mapM regUserByNick nicks-    let nicks = unwords $ map (unNick.regUserNick) online-    unless (null nicks) $ thisNickServerReply RPL_ISON [nicks ++ " "]---- Generic message functions---- | Send a message to a user or a channel (it figures it out).-sendMsgTo :: Monad m => RPL -> String -> String -> IRC m ()-sendMsgTo typ name msg =-  if validChannel name-     then withValidChanName name $ \name -> -            channelReply name typ [unChanName name,msg] ExcludeMe-     else userReply name typ [name,msg]-     --- Channel functions---- | Get channels that the current client is in.-myChannels :: Monad m => IRC m [Channel]-myChannels = do-  ref <- getRef-  filter (elem ref . channelUsers) . map snd . M.toList <$> gets envChannels---- | Join a channel.-joinChannel :: Monad m => ChannelName -> IRC m ()-joinChannel name = do-  ref <- getRef-  let addMe c = c { channelUsers = channelUsers c ++ [ref] }-  modifyChannels $ M.adjust addMe name-  channelReply name RPL_JOIN [unChanName name] IncludeMe-  sendNamesList name-  withChannel name $ \Channel{..} -> do-    case channelTopic of-      Just topic -> thisServerReply RPL_TOPIC [unChanName name,topic]-      Nothing -> return ()---- | Send the names list of a channel.-sendNamesList :: Monad m => ChannelName -> IRC m ()-sendNamesList name = do-  asRegistered $-    withChannel name $ \Channel{..} -> do-      clients <- catMaybes <$> mapM getClientByRef channelUsers-      let nicks = map regUserNick . catMaybes . map clientRegUser $ clients-      forM_ (splitEvery 10 nicks) $ \nicks ->-        thisNickServerReply RPL_NAMEREPLY ["@",unChanName name-                                          ,unwords $ map unNick nicks]-      thisNickServerReply RPL_ENDOFNAMES [unChanName name-                                         ,"End of /NAMES list."]---- | Am I in a channel?-inChannel :: Monad m => ChannelName -> IRC m Bool-inChannel name = do-  chan <- M.lookup name <$> gets envChannels-  case chan of-    Nothing -> return False-    Just Channel{..} -> (`elem` channelUsers) <$> getRef---- | Insert a new channel.-insertChannel :: Monad m => ChannelName -> IRC m ()-insertChannel name = modifyChannels $ M.insert name newChan where-  newChan = Channel { channelName  = name-                    , channelTopic = Nothing-                    , channelUsers = []-                    }---- | Modify the channel map.-modifyChannels :: Monad m -               => (Map ChannelName Channel -> Map ChannelName Channel) -               -> IRC m ()-modifyChannels f = modify $ \e -> e { envChannels = f (envChannels e) }---- | Perform an action with an existing channel, sends error if not exists.-withChannel :: Monad m => ChannelName -> (Channel -> IRC m ()) -> IRC m ()-withChannel name m = do-  chan <- M.lookup name <$> gets envChannels-  case chan of-    Nothing -> thisServerReply ERR_NOSUCHCHANNEL [unChanName name-                                                 ,"No such channel."]-    Just chan -> m chan-    -withValidChanName :: Monad m => String -> (ChannelName -> IRC m ()) -                  -> IRC m ()-withValidChanName name m -    | validChannel name = m $ ChannelName name-    | otherwise         = errorReply $ "Invalid channel name: " ++ name---- Client/user access functions---- | Update the last pong reply time.-updateLastPong :: Monad m => IRC m ()-updateLastPong = do-  ref <- getRef-  now <- asks fst-  let adjust client@Client{..} = client { clientLastPong = now }-  modifyClients $ M.adjust adjust ref---- | Read a valid nick.-readNick :: String -> Maybe Nick-readNick n | validNick n = Just $ Nick n-           | otherwise   = Nothing---- | Modify the nicks mapping.-modifyNicks :: Monad m => (Map Nick Ref -> Map Nick Ref) -> IRC m ()-modifyNicks f = modify $ \env -> env { envNicks = f (envNicks env) }---- | With a valid nickname, perform an action.-withValidNick :: Monad m => String -> (Nick -> IRC m ()) -> IRC m ()-withValidNick nick m-    | validNick nick = m (Nick nick)-    | otherwise      = errorReply $ "Invalid nick format: " ++ nick---- | Perform an action if a nickname is unique, otherwise send error.-ifUniqueNick :: Monad m => Nick -> IRC m () -> IRC m ()-ifUniqueNick nick m = do-  clients <- gets envClients-  client <- (M.lookup nick >=> (`M.lookup` clients)) <$> gets envNicks-  case client of-    Nothing -> m-    Just{}  -> thisServerReply ERR_NICKNAMEINUSE -                               [unNick nick,"Nick is already in use."]---- | Try to register the user with the USER/NICK/PASS that have been given.-tryRegister :: MonadProvider m => IRC m ()-tryRegister =-  withUnegistered $ \UnregUser{..} -> do-    let details = (,,,) <$> unregUserName-                        <*> unregUserNick-                        <*> unregUserUser-                        <*> unregUserPass-    case details of-      Nothing -> return ()-      Just (name,nick,user,pass) -> do-        authentic <- lift $ authenticate user pass-        if not authentic-           then errorReply $ "Wrong user/pass."-           else do modifyUser $ \_ ->-                     Registered $ RegUser name nick user pass-                   sendWelcome-                   sendMotd---- | Send a client reply to a user.-userReply :: Monad m => String -> RPL -> [String] -> IRC m ()-userReply nick typ ps = -  withValidNick nick $ \nick ->-    withClientByNick nick $ \Client{..} ->-      clientReply clientRef typ ps---- | Perform an action with a client by nickname.-withClientByNick :: Monad m => Nick -> (Client -> IRC m ()) -> IRC m ()-withClientByNick nick m = do-    client <- clientByNick nick-    case client of-      Nothing -> sendNoSuchNick nick-      Just client@Client{..} -          | isRegistered clientUser -> m client-          | otherwise -> sendNoSuchNick nick---- | Perform an action with a registered user by its nickname.-withRegUserByNick :: Monad m => Nick -> (RegUser -> IRC m ()) -> IRC m ()-withRegUserByNick nick m = do-  user <- regUserByNick nick-  case user of-    Just user -> m user-    Nothing -> sendNoSuchNick nick-    --- | Send the RPL_NOSUCHNICK reply.-sendNoSuchNick :: Monad m => Nick -> IRC m ()-sendNoSuchNick nick =-  thisServerReply ERR_NOSUCHNICK [unNick nick,"No such nick."]---- | Get a registered user by nickname.-regUserByNick :: Monad m => Nick -> IRC m (Maybe RegUser)-regUserByNick nick = do-  c <- clientByNick nick-  case clientUser <$> c of-    Just (Registered u) -> return $ Just u-    _ -> return Nothing---- | Get a client by nickname.-clientByNick :: Monad m => Nick -> IRC m (Maybe Client)-clientByNick nick = do-  clients <- gets envClients-  (M.lookup nick >=> (`M.lookup` clients)) <$> gets envNicks---- | Maybe get a registered user from a client.-clientRegUser :: Client -> Maybe RegUser-clientRegUser Client{..} = -    case clientUser of-      Registered u -> Just u-      _ -> Nothing-  --- | Modify the current user if unregistered.-modifyUnregistered :: Monad m => (UnregUser -> UnregUser) -> IRC m ()-modifyUnregistered f = do-  modifyUser $ \user -> -      case user of-        Unregistered user -> Unregistered (f user)-        u -> u---- | Modify the current user if registered.-modifyRegistered :: Monad m => (RegUser -> RegUser) -> IRC m ()-modifyRegistered f = do-  modifyUser $ \user -> -      case user of-        Registered user -> Registered (f user)-        u -> u---- | Modify the current user.-modifyUser :: Monad m => (User -> User) -> IRC m ()-modifyUser f = do-  ref <- getRef-  let modUser c = c { clientUser = f (clientUser c) }-      modClient = M.adjust modUser ref-  modify $ \env -> env { envClients = modClient (envClients env) }---- | Only perform command if the client is registered.-asRegistered :: Monad m => IRC m () -> IRC m ()-asRegistered m = do-  registered <- isRegistered <$> getUser-  when registered m---- | Perform command with a registered user.-withRegistered :: Monad m => (RegUser -> IRC m ()) -> IRC m ()-withRegistered m = do-  user <- getUser-  case user of-    Registered user -> m user-    _ -> return ()-    --- | With sent pass.-withSentPass :: Monad m => IRC m () -> IRC m ()-withSentPass m = do-  asRegistered m-  withUnegistered $ \UnregUser{..} -> do-    case unregUserPass of-      Just{} -> m-      Nothing -> return ()---- | Perform command with a registered user.-withUnegistered :: Monad m => (UnregUser -> IRC m ()) -> IRC m ()-withUnegistered m = do-  user <- getUser-  case user of-    Unregistered user -> m user-    _ -> return ()---- | Only perform command if the client is registered.-asUnregistered :: Monad m => IRC m () -> IRC m ()-asUnregistered m = do-  registered <- isRegistered <$> getUser-  unless registered m---- | Is a user registered?-isRegistered :: User -> Bool-isRegistered Registered{} = True-isRegistered _ = False---- | Get the current client's user.-getUser :: Monad m => IRC m User-getUser = clientUser <$> getClient---- | Get the current client.-getClientByRef :: Monad m => Ref -> IRC m (Maybe Client)-getClientByRef ref = do-  clients <- gets envClients-  return $ M.lookup ref clients---- | Get the current client.-getClient :: Monad m => IRC m Client-getClient = do-  ref <- getRef-  clients <- gets envClients-  case M.lookup ref clients of-    Just client -> return $ client-    Nothing -> makeNewClient-    --- | Modify the clients table.-modifyClients :: Monad m => (Map Ref Client -> Map Ref Client) -> IRC m ()-modifyClients f = modify $ \env -> env { envClients = f (envClients env) }---- | Make a current client based on the current connection.-makeNewClient :: Monad m => IRC m Client-makeNewClient = do-  Conn{..} <- asks snd-  let client = Client { clientRef = connRef-                      , clientHostname = connHostname-                      , clientUser = newUnregisteredUser -                      , clientLastPong = connTime }-  modifyClients $ M.insert connRef client-  return client---- | Make a new unregistered user.-newUnregisteredUser :: User-newUnregisteredUser = Unregistered $ UnregUser {-   unregUserName = Nothing-  ,unregUserNick = Nothing-  ,unregUserUser = Nothing-  ,unregUserPass = Nothing-  }---- Channel replies---- | Send a client reply to everyone in a channel.-channelReply :: Monad m => ChannelName -> RPL -> [String] -             -> ChannelReplyType-             -> IRC m ()-channelReply name cmd params typ = do-  withChannel name $ \Channel{..} -> do-    ref <- getRef-    forM_ channelUsers $ \theirRef -> do-      unless (typ == ExcludeMe && ref == theirRef) $ -        clientReply theirRef cmd params---- Client replies---- | Send a client reply to the current client.-thisClientReply :: Monad m => RPL -> [String] -> IRC m ()-thisClientReply typ params = do-  ref <- getRef-  clientReply ref typ params---- | Send a client reply of the given type with the given params, on--- the given connection reference.-clientReply :: Monad m => Ref -> RPL -> [String] -> IRC m ()-clientReply ref typ params = do-  withRegistered $ \user -> do-    client <- getClient-    msg <- newClientMsg client user typ params-    reply ref msg---- | Make a new IRC message from the current client.-newClientMsg :: Monad m => Client -> RegUser -> RPL -> [String] -             -> IRC m Message-newClientMsg Client{..} RegUser{..} cmd ps = do-  let nickName = NickName (unNick regUserNick)-                          (Just regUserUser)-                          (Just clientHostname)-  return $ Message {-    msg_prefix = Just $ nickName-   ,msg_command = fromRPL cmd-   ,msg_params = ps-  }---- Server replies---- | Send the welcome message.-sendWelcome :: Monad m => IRC m ()-sendWelcome = do-  withRegistered $ \RegUser{..} -> do-    thisNickServerReply RPL_WELCOME ["Welcome."]---- | Send the MOTD.    -sendMotd :: MonadProvider m => IRC m ()-sendMotd = do-  asRegistered $ do-    thisNickServerReply RPL_MOTDSTART ["MOTD"]-    motd <- fmap lines <$> lift provideMotd-    let motdLine line = thisNickServerReply RPL_MOTD [line]-    case motd of-      Nothing -> motdLine "None."-      Just lines -> mapM_ motdLine lines-    thisNickServerReply RPL_ENDOFMOTD ["/MOTD."]---- | Send a message reply.-notice :: Monad m => String -> IRC m ()-notice msg = thisServerReply RPL_NOTICE ["*",msg]--thisNickServerReply :: Monad m => RPL -> [String] -> IRC m ()-thisNickServerReply typ params = do-  withRegistered $ \RegUser{regUserNick=Nick nick} ->-    thisServerReply typ (nick : params)---- | Send a server reply of the given type with the given params.-thisServerReply :: Monad m => RPL -> [String] -> IRC m ()-thisServerReply typ params = do-  ref <- getRef-  serverReply ref typ params---- | Send a server reply of the given type with the given params.-serverReply :: Monad m => Ref -> RPL -> [String] -> IRC m ()-serverReply ref typ params = do-  msg <- newServerMsg typ params-  reply ref msg---- | Make a new IRC message from the server.-newServerMsg :: Monad m => RPL -> [String] -> IRC m Message-newServerMsg cmd ps = do-  hostname <- asks $ connServerName . snd-  return $ Message {-    msg_prefix = Just $ Server hostname-   ,msg_command = fromRPL cmd-   ,msg_params = ps-  }-  --- | Send a cmd reply of the given type with the given params.-thisCmdReply :: Monad m => RPL -> [String] -> IRC m ()-thisCmdReply typ params = do-  ref <- getRef-  cmdReply ref typ params---- | Send a cmd reply of the given type with the given params.-cmdReply :: Monad m => Ref -> RPL -> [String] -> IRC m ()-cmdReply ref typ params = do-  msg <- newCmdMsg typ params-  reply ref msg---- | Make a new IRC message from the cmd.-newCmdMsg :: Monad m => RPL -> [String] -> IRC m Message-newCmdMsg cmd ps = do-  return $ Message {-    msg_prefix = Nothing-   ,msg_command = fromRPL cmd-   ,msg_params = ps-  }---- | Get the current connection ref.-getRef :: Monad m => IRC m Ref-getRef = asks $ connRef . snd---- Output functions---- | Send an error reply.-errorReply :: Monad m => String -> IRC m ()-errorReply m = do-  notice $ "ERROR: " ++ m-  log $ "ERROR: " ++ m---- | Send a message reply.-reply :: Monad m => Ref -> Message -> IRC m ()-reply ref msg = do-  outgoing $ encode msg-  tell . return $ MessageReply ref msg---- | Log an incoming line.-incoming :: Monad m => String -> IRC m ()-incoming = log . ("<- " ++)---- | Log an outgoing line.-outgoing :: Monad m => String -> IRC m ()-outgoing = log . ("-> " ++)---- | Log a line.-log :: Monad m => String -> IRC m ()-log line = do-  ref <- getRef-  tell . return . LogReply $ show (unRef ref) ++ ": " ++ line---- Validation functions---- | Is a username valid?-validUser :: String -> Bool-validUser = validNick---- | Is a nickname valid? Digit/letter or one of these: -_/\\;()[]{}?`'-validNick :: String -> Bool-validNick s = all ok s && length s > 0 where-  ok c = isDigit c || isLetter c || elem c "-_/\\;()[]{}?`'"---- | Valid channel name?-validChannel :: String -> Bool-validChannel ('#':cs) = all ok cs && length cs > 0 where-  ok c = isDigit c || isLetter c || elem c "-_/\\;()[]{}?`'"-validChannel _ = False+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE ViewPatterns      #-}++-- | This module communicates and manages the state of clients+-- connected to the server.++module Hulk.Client+    where++import           Hulk.Auth+import           Hulk.Types++import           Control.Applicative+import           Control.Monad.RWS hiding (pass)+import           Data.CaseInsensitive (mk)+import           Data.Char+import           Data.List+import           Data.List.Split+import           Data.Map             (Map)+import qualified Data.Map             as M+import           Data.Maybe+import qualified Data.Set             as S+import           Data.Text            (Text, pack)+import qualified Data.Text            as T+import           Data.Text.Encoding   (decodeUtf8,encodeUtf8)+import           Data.Time hiding (readTime)+import           Network.FastIRC      (Command (..), CommandArg, Message (..), UserSpec(..))+import qualified Network.FastIRC      as IRC+import           Prelude              hiding (log)++--------------------------------------------------------------------------------+-- * Top-level dispatchers++-- | Run the client monad.+handleCommand+  :: Config                        -- ^ Server configuration.+  -> HulkState                     -- ^ Server state.+  -> UTCTime                       -- ^ Current time.+  -> Conn                          -- ^ Current client connection.+  -> (String,String)               -- ^ Authorization info.+  -> Command                       -- ^ The command.+  -> ((), HulkState, [HulkWriter]) -- ^ The new transformed state and any instructions.+handleCommand config state' now conn auth cmd = do+  runRWS (runHulk (handleCmd cmd))+         (HulkReader now conn config Nothing auth)+         state'++-- | Handle an incoming command.+handleCmd :: Command -- ^ A command which shouldn't be logged (e.g. PASS).+          -> Hulk ()+handleCmd cmd = do+  case cmd of+    PassCmd (decodeUtf8 -> pass) -> do+      incoming cmd+      asUnregistered $ handlePass pass+    StringCmd "PINGPONG" _ -> do+      incoming cmd+      handlePingPong+    _ -> handleMsgSafeToLog cmd++-- | Handle commands that are safe to log.+handleMsgSafeToLog :: Command -- ^ A command which is safe to log+                              -- (PONG, NICK, etc.).+                   -> Hulk ()+handleMsgSafeToLog cmd = do+  incoming cmd+  updateLastPong+  case cmd of+    PongCmd{} -> handlePong+    NickCmd (decodeUtf8 -> nick) _ -> handleNick nick+    PingCmd (decodeUtf8 -> param) _ -> handlePing param+    UserCmd (decodeUtf8 -> user) _ _ (decodeUtf8 -> realname) ->+      asUnregistered $ handleUser user realname+    QuitCmd mmsg ->+      handleQuit RequestedQuit+                 (maybe "Quit (no message given)"+                        decodeUtf8+                        mmsg)+    StringCmd "DISCONNECT" _ ->+      handleQuit SocketQuit "Connection lost."+    _ -> handleMsgReg'd cmd++-- | Handle commands that can only be used when registered.+handleMsgReg'd :: Command -- ^ A command that users use after+                          -- registration (e.g. JOIN, PART, etc.).+               -> Hulk ()+handleMsgReg'd cmd =+  asRegistered $+   case cmd of+     JoinCmd names ->+       mapM_ handleJoin (map decodeUtf8 (M.keys names))+     PartCmd names mmsg  ->+       mapM_ (flip handlePart (maybe "" decodeUtf8 mmsg))+             (map decodeUtf8 (S.toList names))+     PrivMsgCmd targets msg ->+       mapM_ (flip handlePrivmsg (decodeUtf8 msg))+             (map decodeUtf8 (S.toList targets))+     TopicCmd chan (fmap decodeUtf8 -> Just topic) -> handleTopic (decodeUtf8 chan) topic+     NoticeCmd targets msg  ->+       mapM_ (flip handleNotice (decodeUtf8 msg))+             (map decodeUtf8 (S.toList targets))+     StringCmd "WHOIS" [nick] -> handleWhoIs (decodeUtf8 nick)+     StringCmd "ISON" people  -> handleIsOn (map decodeUtf8 people)+     StringCmd "NAMES" [chan] -> handleNames (decodeUtf8 chan)+     _ -> invalidCmd cmd++-- | Log an invalid cmd.+invalidCmd :: Command -- ^ The given command that we don't know how to handle.+           -> Hulk ()+invalidCmd cmd = do+  errorReply $ "Invalid or unknown message type, or not" <>+               " enough parameters: " <> decodeUtf8 (IRC.showCommand cmd)++--------------------------------------------------------------------------------+-- * Command handlers++-- | Handle the PONG command. This updates the user's “last seen”+-- value on file.+handlePong :: Hulk ()+handlePong = do+  now <- asks readTime+  withRegistered $ \RegUser{regUserUser=user} -> do+    tell [UpdateUserData UserData { userDataUser = user+                                  , userDataLastSeen = now+                                  }]++-- | Handle the PINGPONG event. Disconnect the client if timedout.+handlePingPong :: Hulk ()+handlePingPong = do+  lastPong <- clientLastPong <$> getClient+  now <- asks readTime+  let n = diffUTCTime now lastPong+  if n > 60*4+     then handleQuit RequestedQuit $ "Ping timeout: " <> pack (show n) <> " seconds"+     else do hostname <- asks (connServerName . readConn)+             thisCmdReply RPL_PING [hostname]++-- | Handle the PASS message.+handlePass :: Text -> Hulk ()+handlePass pass = do+  modifyUnregistered $ \u -> u { unregUserPass = Just pass }+  notice "Received password."+  tryRegister++-- | Handle the USER message.+handleUser :: Text -> Text -> Hulk ()+handleUser user realname = do+  withSentPass $+    if validUser user+       then do modifyUnregistered $ \u -> u { unregUserUser = Just (UserName (mk user))+                                            , unregUserName = Just realname }+               notice "Recieved user details."+               tryRegister+       else errorReply "Invalid user format."++-- | Handle the USER message.+handleNick :: Text -> Hulk ()+handleNick nick' =+  withSentPass $+    withValidNick nick' $ \nick ->+      ifNotMyNick nick $+        ifUniqueNick nick (updateNickAndTryRegistration nick)+                          (Just (tryBumpingSomeoneElseOff nick))++  where updateNickAndTryRegistration nick = do+          ref <- getRef+          withRegistered $ \RegUser{regUserNick=rnick} ->+            modifyNicks $ M.delete rnick+          withUnregistered $ \UnregUser{unregUserNick=rnick} ->+            maybe (return ()) (modifyNicks . M.delete) rnick+          modifyNicks $ M.insert nick ref+          modifyUnregistered $ \u -> u { unregUserNick = Just nick }+          tryRegister+          asRegistered $ do+            thisClientReply RPL_NICK [nickText nick]+            (myChannels >>=) $ mapM_ $ \Channel{..} -> do+              channelReply channelName RPL_NICK [nickText nick] ExcludeMe+            modifyRegistered $ \u -> u { regUserNick = nick }++        tryBumpingSomeoneElseOff nick = \error_reply -> do+          registered <- isRegistered <$> getUser+          if registered+             then bumpAndRegister nick error_reply+             else do+               withUnregistered $ \unreg -> do+                 (authentic,_) <- isAuthentic unreg { unregUserNick = Just nick }+                 if not authentic+                    then error_reply " (Registration not valid, can't bump off this user.)"+                    else bumpAndRegister nick error_reply++        bumpAndRegister nick error_reply = do+          username <- getUsername+          if fmap (mk.userText) username == Just (mk (nickText nick))+             then do bumpOff nick+                     updateNickAndTryRegistration nick+             else error_reply $+               let x = "username=" <> pack (show username) <> ", nick=" <> nickText nick+               in " (Can only bump the nick that matches your username. Debug: " <> x <> ")"++-- | Handle the PING message.+handlePing :: Text -> Hulk ()+handlePing p = do+  hostname <- asks (connServerName . readConn)+  thisServerReply RPL_PONG [hostname,p]++-- | Handle the QUIT message.+handleQuit :: QuitType -> Text -> Hulk ()+handleQuit quitType msg = do+  clearQuittedUser msg+  when (quitType == RequestedQuit) $+    tell [Close]++-- | Handle the TELL message.+handleTell :: Text -> Text -> Hulk ()+handleTell name msg = sendMsgTo RPL_NOTICE name msg++-- | Handle the NAMES list request.+handleNames :: Text -> Hulk ()+handleNames chan = do+  withValidChanName chan sendNamesList++-- | Handle the JOIN message.+handleJoin :: Text -> Hulk ()+handleJoin chans = do+  let names = T.split (==',') chans+  forM_ names $ flip withValidChanName $ \name -> do+      exists <- M.member name <$> gets stateChannels+      unless exists $ insertChannel name+      joined <- inChannel name+      unless joined $ joinChannel name++-- | Handle the PART message.+handlePart :: Text -> Text -> Hulk ()+handlePart name msg =+  withValidChanName name $ \vname -> do+    removeFromChan vname+    channelReply vname RPL_PART [msg] IncludeMe++-- | Handle the TOPIC message.+handleTopic :: Text -> Text -> Hulk ()+handleTopic name' topic =+  withValidChanName name' $ \name -> do+    let setTopic c = c { channelTopic = Just topic }+    modifyChannels $ M.adjust setTopic name+    channelReply name RPL_TOPIC [channelNameText name,topic] IncludeMe++-- | Handle the PRIVMSG message.+handlePrivmsg :: Text -> Text -> Hulk ()+handlePrivmsg name msg = do+  sendMsgTo RPL_PRIVMSG name msg+  historyLog RPL_PRIVMSG [name,msg]++-- | Handle the NOTICE message.+handleNotice :: Text -> Text -> Hulk ()+handleNotice name msg = sendMsgTo RPL_NOTICE name msg++-- | Handle WHOIS message.+handleWhoIs :: Text -> Hulk ()+handleWhoIs nick' =+  withValidNick nick' $ \nick ->+    withClientByNick nick $ \Client{..} ->+      withRegUserByNick nick $ \RegUser{..} -> do+        thisNickServerReply RPL_WHOISUSER+                            [nickText regUserNick+                            ,userText regUserUser+                            ,clientHostname+                            ,"*"+                            ,regUserName]+        thisNickServerReply RPL_ENDOFWHOIS+                            [nickText regUserNick+                            ,"End of WHOIS list."]++-- | Handle the ISON ('is on?') message.+handleIsOn :: [Text] -> Hulk ()+handleIsOn (catMaybes . map readNick -> nicks') =+  asRegistered $ do+    online <- catMaybes <$> mapM regUserByNick nicks'+    let nicks = T.unwords $ map (nickText.regUserNick) online+    unless (T.null nicks) $ thisNickServerReply RPL_ISON [nicks <> " "]++--------------------------------------------------------------------------------+-- * General actions++-- | Send a message to a user or a channel (it figures it out).+sendMsgTo :: RPL -> Text -> Text -> Hulk ()+sendMsgTo typ name' msg =+  if validChannel name'+     then withValidChanName name' $ \name ->+            channelReply name typ [channelNameText name,msg] ExcludeMe+     else userReply name' typ [name',msg]++--------------------------------------------------------------------------------+-- * Users++-- | Is a username valid?+validUser :: Text -> Bool+validUser = validNick++-- | Get the username.+getUsername :: Hulk (Maybe UserName)+getUsername = do+  user <- getUser+  return $ case user of+    Unregistered (UnregUser{unregUserUser=username}) -> username+    Registered (RegUser{regUserUser=username})       -> Just username++-- | Get the current connection ref.+getRef :: Hulk Ref+getRef = connRef <$> asks readConn++-- | Bump off the given nick.+bumpOff :: Nick -> Hulk ()+bumpOff nick = ifNotMyNick nick $ do+  notice $ "Bumping off user " <> nickText nick <> "…"+  withClientByNick nick $ \Client{clientRef=ref} ->+    local (\r -> r { readConn = (readConn r) { connRef = ref } }) $ do+      clearQuittedUser msg+      tell [Bump ref]++  where msg = "Bumped off."++-- | Clear a quitted user from channels and nick list, and notify+-- people in channels of their leaving.+clearQuittedUser :: Text -> Hulk ()+clearQuittedUser msg = do+  (myChannels >>=) $ mapM_ $ \Channel{..} -> do+    channelReply channelName RPL_QUIT [msg] ExcludeMe+    removeFromChan channelName+  withRegistered $ \RegUser{regUserNick=nick} -> do+    modifyNicks $ M.delete nick+  withUnregistered $ \UnregUser{unregUserNick=nick} -> do+    maybe (return ()) (modifyNicks . M.delete) nick+  ref <- getRef+  modifyClients $ M.delete ref+  notice msg++-- | Update the last pong reply time.+updateLastPong :: Hulk ()+updateLastPong = do+  ref <- getRef+  now <- asks readTime+  let adjust client@Client{..} = client { clientLastPong = now }+  modifyClients $ M.adjust adjust ref++-- | Send a client reply to a user.+userReply :: Text -> RPL -> [Text] -> Hulk ()+userReply nick' typ ps =+  withValidNick nick' $ \nick ->+    withClientByNick nick $ \Client{..} ->+      clientReply clientRef typ ps++-- | Perform an action with a registered user by its nickname.+withRegUserByNick :: Nick -> (RegUser -> Hulk ()) -> Hulk ()+withRegUserByNick nick m = do+  user <- regUserByNick nick+  case user of+    Just user' -> m user'+    Nothing -> sendNoSuchNick nick++-- | Send the RPL_NOSUCHNICK reply.+sendNoSuchNick :: Nick -> Hulk ()+sendNoSuchNick nick =+  thisServerReply ERR_NOSUCHNICK [nickText nick,"No such nick."]++-- | Modify the current user.+modifyUser :: (User -> User) -> Hulk ()+modifyUser f = do+  ref <- getRef+  let modUser c = c { clientUser = f (clientUser c) }+      modClient = M.adjust modUser ref+  modify $ \env -> env { stateClients = modClient (stateClients env) }++-- | Get the current client's user.+getUser :: Hulk User+getUser = clientUser <$> getClient++--------------------------------------------------------------------------------+-- * Clients++-- | Get the current client.+getClientByRef :: Ref -> Hulk (Maybe Client)+getClientByRef ref = do+  clients <- gets stateClients+  return $ M.lookup ref clients++-- | Get a client by nickname.+clientByNick :: Nick -> Hulk (Maybe Client)+clientByNick nick = do+  clients <- gets stateClients+  (M.lookup nick >=> (`M.lookup` clients)) <$> gets stateNicks++-- | Perform an action with a client by nickname.+withClientByNick :: Nick -> (Client -> Hulk ()) -> Hulk ()+withClientByNick nick m = do+  client' <- clientByNick nick+  case client' of+    Nothing -> sendNoSuchNick nick+    Just client@Client{..}+        | isRegistered clientUser -> m client+        | otherwise -> sendNoSuchNick nick++-- | Get the current client.+getClient :: Hulk Client+getClient = do+  ref <- getRef+  clients <- gets stateClients+  case M.lookup ref clients of+    Just client -> return $ client+    Nothing -> makeNewClient++-- | Modify the clients table.+modifyClients :: (Map Ref Client -> Map Ref Client) -> Hulk ()+modifyClients f = modify $ \env -> env { stateClients = f (stateClients env) }++-- | Make a current client based on the current connection.+makeNewClient :: Hulk Client+makeNewClient = do+  Conn{..} <- asks readConn+  let client = Client { clientRef = connRef+                      , clientHostname = connHostname+                      , clientUser = newUnregisteredUser+                      , clientLastPong = connTime }+  modifyClients $ M.insert connRef client+  return client+++--------------------------------------------------------------------------------+-- * Registration++-- | Get a registered user by nickname.+regUserByNick :: Nick -> Hulk (Maybe RegUser)+regUserByNick nick = do+  c <- clientByNick nick+  case clientUser <$> c of+    Just (Registered u) -> return $ Just u+    _ -> return Nothing++-- | Maybe get a registered user from a client.+clientRegUser :: Client -> Maybe RegUser+clientRegUser Client{..} =+    case clientUser of+      Registered u -> Just u+      _ -> Nothing++-- | Modify the current user if unregistered.+modifyUnregistered :: (UnregUser -> UnregUser) -> Hulk ()+modifyUnregistered f = do+  modifyUser $ \user ->+      case user of+        Unregistered user' -> Unregistered (f user')+        u -> u++-- | Modify the current user if registered.+modifyRegistered :: (RegUser -> RegUser) -> Hulk ()+modifyRegistered f = do+  modifyUser $ \user ->+      case user of+        Registered user' -> Registered (f user')+        u -> u++-- | Only perform command if the client is registered.+asRegistered :: Hulk () -> Hulk ()+asRegistered m = do+  registered <- isRegistered <$> getUser+  when registered m++-- | Perform command with a registered user.+withRegistered :: (RegUser -> Hulk ()) -> Hulk ()+withRegistered m = do+  user <- getUser+  case user of+    Registered user' -> m user'+    _ -> return ()++-- | With sent pass.+withSentPass :: Hulk () -> Hulk ()+withSentPass m = do+  asRegistered m+  withUnregistered $ \UnregUser{..} -> do+    case unregUserPass of+      Just{} -> m+      Nothing -> return ()++-- | Perform command with a registered user.+withUnregistered :: (UnregUser -> Hulk ()) -> Hulk ()+withUnregistered m = do+  user <- getUser+  case user of+    Unregistered user' -> m user'+    _ -> return ()++-- | Only perform command if the client is registered.+asUnregistered :: Hulk () -> Hulk ()+asUnregistered m = do+  registered <- isRegistered <$> getUser+  unless registered m++-- | Is a user registered?+isRegistered :: User -> Bool+isRegistered Registered{} = True+isRegistered _ = False+++-- | Make a new unregistered user.+newUnregisteredUser :: User+newUnregisteredUser = Unregistered $ UnregUser {+   unregUserName = Nothing+  ,unregUserNick = Nothing+  ,unregUserUser = Nothing+  ,unregUserPass = Nothing+  }++-- | Try to register the user with the USER/NICK/PASS that have been given.+tryRegister :: Hulk ()+tryRegister =+  withUnregistered $ \unreg -> do+    check <- isAuthentic unreg+    case check of+      (True,Just (name,user,nick)) -> do+        modifyUser $ \_ ->+          Registered $ RegUser name nick user ""+        sendWelcome+        sendMotd+        sendEvents+      (False,Just{}) -> errorReply $ "Wrong user/pass."+      _ -> return ()++isAuthentic :: UnregUser -> Hulk (Bool,Maybe (Text,UserName,Nick))+isAuthentic UnregUser{..} = do+  let details = (,,,) <$> unregUserName+                      <*> unregUserNick+                      <*> unregUserUser+                      <*> unregUserPass+  case details of+    Nothing -> return (False,Nothing)+    Just (name,nick,user,pass) -> do+      (keystr,passwords) <- asks readAuth+      let authentic = authenticate keystr passwords (userText user) pass+      return (authentic,Just (name,user,nick))++--------------------------------------------------------------------------------+-- * Nicknames++-- | Read a valid nick.+readNick :: Text -> Maybe Nick+readNick n | validNick n = Just $ NickName (mk n)+           | otherwise   = Nothing++-- | Modify the nicks mapping.+modifyNicks :: (Map Nick Ref -> Map Nick Ref) -> Hulk ()+modifyNicks f = modify $ \env -> env { stateNicks = f (stateNicks env) }++-- | With a valid nickname, perform an action.+withValidNick :: Text -> (Nick -> Hulk ()) -> Hulk ()+withValidNick nick m+    | validNick nick = m (NickName (mk nick))+    | otherwise      = errorReply $ "Invalid nick format: " <> nick++-- | Perform an action if a nickname is unique, otherwise send error.+ifUniqueNick :: Nick -> Hulk () -> Maybe ((Text -> Hulk ()) -> Hulk ()) -> Hulk ()+ifUniqueNick nick then_m else_m = do+  clients <- gets stateClients+  client <- (M.lookup nick >=> (`M.lookup` clients)) <$> gets stateNicks+  case client of+    Nothing -> then_m+    Just{}  -> do+      case else_m of+        Just else_m' -> else_m' error_reply+        Nothing -> error_reply ""++  where error_reply x = thisServerReply ERR_NICKNAMEINUSE+                                      [nickText nick,"Nick is already in use." <> x]+++-- | Is a nickname valid? Digit/letter or one of these: -_/\\;()[]{}?`'+validNick :: Text -> Bool+validNick s = T.all ok s && T.length s > 0 where+  ok c = isDigit c || isLetter c || elem c "-_/\\;()[]{}?`'"++-- | If the given nick is not my nick name, ….+ifNotMyNick :: Nick -> Hulk () -> Hulk ()+ifNotMyNick nick m = do+  user <- getUser+  case user of+    Registered RegUser{..}     | regUserNick /= nick -> m+    Unregistered UnregUser{..} | unregUserNick /= Just nick -> m+    _ -> return ()++--------------------------------------------------------------------------------+-- * Channels++-- | Valid channel name?+validChannel :: Text -> Bool+validChannel (T.uncons -> Just ('#',cs)) = T.all ok cs && T.length cs > 0 where+  ok c = isDigit c || isLetter c || elem c "-_/\\;()[]{}?`'"+validChannel _ = False++-- | Remove a user from a channel.+removeFromChan :: ChannelName -> Hulk ()+removeFromChan name = do+  ref <- getRef+  let remMe c = c { channelUsers = S.delete ref (channelUsers c) }+  modifyChannels $ M.adjust remMe name++-- | Get channels that the current client is in.+myChannels :: Hulk [Channel]+myChannels = do+  ref <- getRef+  filter (S.member ref . channelUsers) . map snd . M.toList <$> gets stateChannels++-- | Join a channel.+joinChannel :: ChannelName -> Hulk ()+joinChannel name = do+  ref <- getRef+  let addMe c = c { channelUsers = S.insert ref (channelUsers c) }+  modifyChannels $ M.adjust addMe name+  channelReply name RPL_JOIN [channelNameText name] IncludeMe+  sendNamesList name+  withChannel name $ \Channel{..} -> do+    case channelTopic of+      Just topic -> thisServerReply RPL_TOPIC [channelNameText name,topic]+      Nothing -> return ()++-- | Send the names list of a channel.+sendNamesList :: ChannelName -> Hulk ()+sendNamesList name = do+  asRegistered $+    withChannel name $ \Channel{..} -> do+      clients <- catMaybes <$> mapM getClientByRef (S.toList channelUsers)+      let nicks = map regUserNick . catMaybes . map clientRegUser $ clients+      forM_ (chunksOf 10 nicks) $ \nicks' ->+        thisNickServerReply RPL_NAMEREPLY ["@",channelNameText name+                                          ,T.unwords $ map nickText nicks']+      thisNickServerReply RPL_ENDOFNAMES [channelNameText name+                                         ,"End of /NAMES list."]++-- | Am I in a channel?+inChannel :: ChannelName -> Hulk Bool+inChannel name = do+  chan <- M.lookup name <$> gets stateChannels+  case chan of+    Nothing -> return False+    Just Channel{..} -> (`S.member` channelUsers) <$> getRef++-- | Insert a new channel.+insertChannel :: ChannelName -> Hulk ()+insertChannel name = modifyChannels $ M.insert name newChan where+  newChan = Channel { channelName  = name+                    , channelTopic = Nothing+                    , channelUsers = S.empty+                    }++-- | Modify the channel map.+modifyChannels :: (Map ChannelName Channel -> Map ChannelName Channel)+               -> Hulk ()+modifyChannels f = modify $ \e -> e { stateChannels = f (stateChannels e) }++withValidChanName :: Text -> (ChannelName -> Hulk ())+                  -> Hulk ()+withValidChanName name m+    | validChannel name = m $ ChannelName (mk name)+    | otherwise         = errorReply $ "Invalid channel name: " <> name++-- | Perform an action with an existing channel, sends error if not exists.+withChannel :: ChannelName -> (Channel -> Hulk ()) -> Hulk ()+withChannel name m = do+  chan <- M.lookup name <$> gets stateChannels+  case chan of+    Nothing -> thisServerReply ERR_NOSUCHCHANNEL [channelNameText name+                                                 ,"No such channel."]+    Just chan' -> m chan'++-- | Send a client reply to everyone in a channel.+channelReply :: ChannelName -> RPL -> [Text]+             -> ChannelReplyType+             -> Hulk ()+channelReply name cmd params typ = do+  withChannel name $ \Channel{..} -> do+    ref <- getRef+    forM_ (S.toList channelUsers) $ \theirRef -> do+      unless (typ == ExcludeMe && ref == theirRef) $+        clientReply theirRef cmd params++--------------------------------------------------------------------------------+-- * Client replies++-- | Send a client reply to the current client.+thisClientReply :: RPL -> [Text] -> Hulk ()+thisClientReply typ params = do+  ref <- getRef+  clientReply ref typ params++-- | Send a client reply of the given type with the given params, on+-- the given connection reference.+clientReply :: Ref -> RPL -> [Text] -> Hulk ()+clientReply ref typ params = do+  withRegistered $ \user -> do+    client <- getClient+    msg <- newClientMsg client user typ params+    reply ref msg++-- | Make a new IRC message from the current client.+newClientMsg :: Client -> RegUser -> RPL -> [Text]+             -> Hulk Message+newClientMsg Client{..} RegUser{..} cmd ps = do+  return (Message (Just (User (encodeUtf8 (nickText regUserNick))+                              (encodeUtf8 (userText regUserUser))+                              (encodeUtf8 clientHostname)))+                  (makeCommand cmd ps))++--------------------------------------------------------------------------------+-- * Server replies++-- | Send the welcome message.+sendWelcome :: Hulk ()+sendWelcome = do+  withRegistered $ \RegUser{..} -> do+    thisNickServerReply RPL_WELCOME ["Welcome."]++-- | Send the MOTD.+sendMotd :: Hulk ()+sendMotd = do+  asRegistered $ do+    thisNickServerReply RPL_MOTDSTART ["MOTD"]+    motd <- fmap (fmap T.lines) (asks readMotd)+    let motdLine line = thisNickServerReply RPL_MOTD [line]+    case motd of+      Nothing -> motdLine "None."+      Just lines' -> mapM_ motdLine lines'+    thisNickServerReply RPL_ENDOFMOTD ["/MOTD."]++-- | Send events that the user missed.+sendEvents :: Hulk ()+sendEvents = do+  chans <- configLogChans <$> asks readConfig+  unless (null chans) $ do+    withRegistered $ \RegUser{regUserUser=user} -> do+      ref <- getRef+      forM_ chans handleJoin+      tell [SendEvents ref user]++--------------------------------------------------------------------------------+-- * Output functions++-- | Send a message reply.+notice :: Text -> Hulk ()+notice msg = thisServerReply RPL_NOTICE ["*",msg]++thisNickServerReply :: RPL -> [Text] -> Hulk ()+thisNickServerReply typ params = do+  withRegistered $ \RegUser{regUserNick=nick} ->+    thisServerReply typ (nickText nick : params)++-- | Send a server reply of the given type with the given params.+thisServerReply :: RPL -> [Text] -> Hulk ()+thisServerReply typ params = do+  ref <- getRef+  serverReply ref typ params++-- | Send a server reply of the given type with the given params.+serverReply :: Ref -> RPL -> [Text] -> Hulk ()+serverReply ref typ params = do+  msg <- newServerMsg typ params+  reply ref msg++-- | Make a new IRC message from the server.+newServerMsg :: RPL -> [Text] -> Hulk Message+newServerMsg cmd ps = do+  hostname <- asks (connServerName.readConn)+  return (Message (Just (Nick (encodeUtf8 hostname)))+                  (makeCommand cmd ps))++-- | Send a cmd reply of the given type with the given params.+thisCmdReply :: RPL -> [Text] -> Hulk ()+thisCmdReply typ params = do+  ref <- getRef+  cmdReply ref typ params++-- | Send a cmd reply of the given type with the given params.+cmdReply :: Ref -> RPL -> [Text] -> Hulk ()+cmdReply ref typ params = do+  let msg = newCmdMsg typ params+  reply ref msg++-- | Send an error reply.+errorReply :: Text -> Hulk ()+errorReply m = do+  notice $ "ERROR: " <> m+  log $ "ERROR: " <> m++-- | Send a message reply.+reply :: Ref -> Message -> Hulk ()+reply ref msg = do+  outgoing msg+  tell . return $ MessageReply ref msg++-- | Log an incoming line.+incoming :: Command -> Hulk ()+incoming = log . ("<- " <>) . decodeUtf8 . IRC.showCommand++-- | Log an outgoing line.+outgoing :: Message -> Hulk ()+outgoing msg = do+  ref <- getRef+  tell [outgoingWriter ref msg]++-- | Log a line.+log :: Text -> Hulk ()+log line = do+  ref <- getRef+  tell . return . LogReply $ pack (show (unRef ref)) <> ": " <> line++-- | Make a writer reply.+outgoingWriter :: Ref -> Message -> HulkWriter+outgoingWriter ref =+  LogReply .+  (pack (show (unRef ref)) <>) .+  (": -> " <>) .+  decodeUtf8 .+  IRC.showMessage++historyLog :: RPL -> [Text] -> Hulk ()+historyLog rpl params = do+  chans <- asks (configLogChans . readConfig)+  unless (null chans) $ do+    withRegistered $ \RegUser{regUserUser=name} -> do+      let send = tell [SaveLog (userText name) rpl params]+      case (rpl,params) of+        (RPL_PRIVMSG,[chan])+          | chan `elem` chans -> send+          | otherwise         -> return ()+        _                     -> send++--------------------------------------------------------------------------------+-- * Command Construction++-- | Make a command.+makeCommand :: RPL -> [Text] -> Command+makeCommand rpl xs = fromRPL rpl (map encodeUtf8 xs)++-- | Convert from a reply to an appropriate protocol format.+fromRPL :: RPL -> ([CommandArg] -> Command)+fromRPL RPL_NICK          = StringCmd "NICK"+fromRPL RPL_PONG          = StringCmd "PONG"+fromRPL RPL_QUIT          = StringCmd "QUIT"+fromRPL RPL_JOIN          = StringCmd "JOIN"+fromRPL RPL_NOTICE        = StringCmd "NOTICE"+fromRPL RPL_PART          = StringCmd "PART"+fromRPL RPL_PRIVMSG       = StringCmd "PRIVMSG"+fromRPL RPL_JOINS         = StringCmd "JOIN"+fromRPL RPL_TOPIC         = StringCmd "TOPIC"+fromRPL RPL_PING          = StringCmd "PING"+fromRPL RPL_WHOISUSER     = NumericCmd 311+fromRPL RPL_ISON          = NumericCmd 303+fromRPL RPL_NAMEREPLY     = NumericCmd 353+fromRPL RPL_ENDOFNAMES    = NumericCmd 366+fromRPL RPL_WELCOME       = NumericCmd 001+fromRPL RPL_MOTDSTART     = NumericCmd 375+fromRPL RPL_MOTD          = NumericCmd 372+fromRPL RPL_ENDOFMOTD     = NumericCmd 376+fromRPL RPL_WHOISIDLE     = NumericCmd 317+fromRPL RPL_WHOISCHANNELS = NumericCmd 319+fromRPL RPL_ENDOFWHOIS    = NumericCmd 318+fromRPL ERR_NICKNAMEINUSE = NumericCmd 433+fromRPL ERR_NOSUCHNICK    = NumericCmd 401+fromRPL ERR_NOSUCHCHANNEL = NumericCmd 403++-- | Make a new IRC message from the cmd.+newCmdMsg :: RPL -> [Text] -> Message+newCmdMsg cmd ps = Message Nothing (makeCommand cmd ps)
− src/Hulk/Concurrent.hs
@@ -1,18 +0,0 @@-module Hulk.Concurrent where--import Control.Concurrent-import Control.Monad.Reader-import Control.Monad.IO--import Hulk.Types--fork :: Hulk a -> Hulk ThreadId-fork m = do-  env <- ask-  io $ forkIO $ do _ <- runReaderT (runHulk m) env-                   return ()--withVar :: MVar a -> (a -> Hulk b) -> Hulk b-withVar var m = do-  env <- ask-  io $ withMVar var $ \var -> runReaderT (runHulk (m var)) env
src/Hulk/Config.hs view
@@ -1,13 +1,15 @@-{-# OPTIONS -Wall -fno-warn-missing-signatures -fno-warn-name-shadowing #-}+-- | Configuration parser for the server.+ module Hulk.Config     (Config(..)     ,getConfig)     where +import Hulk.Types+ import Data.Word import Data.ConfigFile--import Hulk.Types+import Data.Text (pack)  getConfig :: FilePath -> IO Config getConfig conf = do@@ -20,13 +22,20 @@         preface <- get c "STRINGS" "preface_file"         passwd <- get c "AUTH" "passwd_file"         key <- get c "AUTH" "passwd_key"-        return Config { configListen = fromIntegral (listen::Word16)-                      , configMotd = Just motd -                      , configHostname = hostname-                      , configPasswd = passwd-                      , configPasswdKey = key-                      , configPreface = Just preface-                      }+        users <- get c "USERS" "data_dir"+        logs <- get c "LOGS" "event_log"+        chans <- fmap words $ get c "LOGS" "channels"+        return Config+          { configListen    = fromIntegral (listen::Word16)+          , configMotd      = Just motd+          , configHostname  = pack hostname+          , configPasswd    = passwd+          , configPasswdKey = key+          , configPreface   = Just preface+          , configUserData  = users+          , configLogFile   = logs+          , configLogChans  = map pack chans+          }   case config of-    Left cperr -> error $ show cperr-    Right config -> return config+    Left cperr   -> error $ show cperr+    Right config' -> return config'
− src/Hulk/Event.hs
@@ -1,9 +0,0 @@-module Hulk.Event where--import Hulk.Types--readEventType :: String -> Event-readEventType str = -  case reads str of-    [(a,"")] -> a-    _        -> NOTHING
− src/Hulk/Log.hs
@@ -1,49 +0,0 @@-module Hulk.Log -    (doing-    ,tell-    ,performing-    ,incoming-    ,outgoing-    ,note)-    where--import Control.Monad.Reader-import Control.Concurrent-import System.IO-import Control.Monad.IO-import qualified System.IO.UTF8 as UTF8--import Hulk.Types-import Hulk.Config--instance Loggable Hulk where-  tell str = do-    h <- asks envTellHandle-    col <- (*2) `fmap` asks envLogColumn-    io $ withMVar h $ \h -> hPutStrLn h $ replicate col ' ' ++ str--instance Loggable IRC where-  tell str = liftHulk $ tell str--performing :: String -> Hulk a -> Hulk a-performing str m = do -  doing str-  v <- indent  m-  tell "Done."-  return v--doing :: String -> Hulk ()-doing str = tell $ str ++ " ..."--indent :: Hulk a -> Hulk a-indent = local inc where-  inc env = env { envLogColumn = envLogColumn env + 1 }--incoming :: String -> IRC ()-incoming str = tell $ "<- " ++ str--outgoing :: String -> IRC ()-outgoing str = tell $ "-> " ++ str--note :: String -> IRC ()-note str = tell $ "Notice: " ++ str
− src/Hulk/Providers.hs
@@ -1,23 +0,0 @@-{-# OPTIONS -Wall -fno-warn-name-shadowing -fno-warn-orphans #-}-module Hulk.Providers where--import Control.Applicative-import Control.Monad.Reader--import Hulk.Types--instance MonadProvider HulkIO where-  providePreface = maybeReadFile configPreface-  provideMotd = maybeReadFile configMotd-  provideKey = mustReadFile configPasswdKey-  providePasswords = mustReadFile configPasswd--maybeReadFile :: (Config -> Maybe FilePath) -> HulkIO (Maybe String)-maybeReadFile get = do -  path <- asks get-  case path of-    Nothing -> return Nothing-    Just path -> Just <$> liftIO (readFile path)--mustReadFile :: (Config -> FilePath) -> HulkIO String-mustReadFile get = asks get >>= liftIO . readFile
src/Hulk/Server.hs view
@@ -1,92 +1,183 @@-{-# LANGUAGE RecordWildCards #-}-{-# OPTIONS -Wall -fno-warn-name-shadowing #-}-module Hulk.Server (start) where+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGUAGE RecordWildCards     #-}+{-# LANGUAGE ScopedTypeVariables #-} +module Hulk.Server where++import           Hulk.Client                (handleCommand, makeCommand,+                                             outgoingWriter)+import           Hulk.Types+ import           Control.Applicative import           Control.Concurrent import           Control.Concurrent.Delay+import           Control.Exception          (IOException, try) import           Control.Monad import           Control.Monad.Fix-import           Control.Monad.Reader-import qualified Data.Map                 as M+import           Data.Aeson+import qualified Data.ByteString.Char8      as S8+import qualified Data.ByteString.Lazy       as L+import qualified Data.ByteString.Lazy.Char8 as L8+import           Data.CaseInsensitive+import           Data.Char+import qualified Data.Map                   as M+import           Data.Maybe+import           Data.Monoid+import           Data.Text                  (Text, pack, unpack)+import qualified Data.Text                  as T+import           Data.Text.Encoding         (encodeUtf8)+import qualified Data.Text.IO               as T import           Data.Time import           Network-import           Network.IRC+import           Network.FastIRC            hiding (UserName)+import qualified Network.FastIRC.IO         as IRC+import           Prelude                    hiding (catch)+import           System.Directory+import           System.FilePath import           System.IO-import           System.IO.UTF8           as UTF8 -import           Hulk.Client-import           Hulk.Providers           ()-import           Hulk.Types- -- | Start an IRC server with the given configuration. start :: Config -> IO () start config = withSocketsDo $ do   hSetBuffering stdout LineBuffering   listenSock <- listenOn $ PortNumber (configListen config)-  envar <- newMVar Env { envClients = M.empty-                       , envNicks = M.empty-                       , envChannels = M.empty }+  statevar <- newMVar HulkState { stateClients  = M.empty+                                , stateNicks    = M.empty+                                , stateChannels = M.empty }+  lvar <- newMVar ()   forever $ do     (handle,host,_port) <- accept listenSock     hSetBuffering handle NoBuffering     now <- getCurrentTime-    let conn = Conn { connRef = newRef handle-                    , connHostname = host+    let conn = Conn { connRef = mkRef handle+                    , connHostname = pack host                     , connServerName = configHostname config                     , connTime = now                     }-    _ <- forkIO $ handleClient config handle envar conn-    return ()+    auth <- getAuth config+    void $ forkIO $ handleClient lvar config handle statevar auth conn  -- | Handle a client connection.-handleClient :: Config -> Handle -> MVar Env -> Conn -> IO ()-handleClient config handle env conn = do-  let runHandle = runClientHandler config env handle conn-      runLine x y = runHandle $ makeLine x y-  pinger <- forkIO $ forever $ do delayMinutes 2; runLine PINGPONG []-  fix $ \loop -> do-    line <- catch (Right <$> UTF8.hGetLine handle)-                  (\e -> do killThread pinger-                            return $ Left e)-    case filter (not.newline) <$> line of-      Right []   -> loop-      Right line -> do runHandle (line++"\r"); loop-      Left _err  -> runLine DISCONNECT ["Connection lost."]+handleClient :: MVar () -> Config -> Handle -> MVar HulkState -> (String,String) -> Conn -> IO ()+handleClient lvar config handle env auth conn = do+  messages <- newChan+  let writeMsg cmd =+        writeChan messages (Message Nothing (StringCmd cmd [])) -  where newline c = c=='\n' || c=='\r'+  pinger <- forkIO $ forever $ do+    delayMinutes 2+    writeMsg "PINGPONG" --- | Make an internal IRC event to give to the client handler.-makeLine :: Event -> [String] -> String-makeLine event params = (++"\r") $ encode $-  Message { msg_prefix = Nothing-          , msg_command = show event-          , msg_params = params }+  void $ forkIO $ fix $ \loop -> do+    eline <- try (S8.hGetLine handle)+    case eline of+      Left (_::IOException) -> do killThread pinger+                                  writeMsg "DISCONNECT"+      Right line ->+        case readMessage line of+          Just msg -> do writeChan messages msg+                         loop+          Nothing -> loop --- | Handle a received line from the client.-runClientHandler :: Config -> MVar Env -> Handle -> Conn -> String -> IO ()-runClientHandler config env handle conn line = do+  fix $ \loop -> do+     msg <- readChan messages+     runClientHandler lvar config env handle conn auth msg+     case msg of+       Message  _  (StringCmd "DISCONNECT" _) -> return ()+       _ -> loop++-- | Handle a received message from the client.+runClientHandler :: MVar () -> Config -> MVar HulkState -> Handle -> Conn -> (String,String) -> Message -> IO ()+runClientHandler lvar config mstate handle conn auth msg = do   now <- getCurrentTime-  modifyMVar_ env $ \env -> do-    (replies,env) <- runReaderT (runHulkIO $ handleLine env now conn line)-                                config-    mapM_ (handleReplies handle) replies-    return env+  instructions <- modifyMVar mstate $ \state -> return $+    let ((),newstate,instructions) = handleCommand config state now conn auth (msgCommand msg)+    in (newstate,instructions)+  forM_ instructions $ handleWriter lvar config handle --- | Act on replies from the client.-handleReplies :: Handle -> Reply -> IO ()-handleReplies handle reply = do-  case reply of+-- | Act on writer from the client.+handleWriter :: MVar () -> Config -> Handle -> HulkWriter -> IO ()+handleWriter lvar config@Config{..} handle writer = do+  case writer of+    SaveLog name rpl params -> saveToLog lvar config name rpl params     MessageReply ref msg -> sendMessage ref msg     LogReply line -> logLine line     Close -> hClose handle+    Bump (Ref h) -> hClose h+    UpdateUserData udata -> do+      L.writeFile (configUserData </> normalizeUser (unpack (userText (userDataUser udata))))+                  (encode udata)+    SendEvents ref user -> do+      writers <- sendEvents config ref user+      mapM_ (handleWriter lvar config handle) (concat writers)  -- | Send a message to a client. sendMessage :: Ref -> Message -> IO ()-sendMessage (Ref handle) msg = do-  catch (UTF8.hPutStrLn handle (encode msg ++ "\r"))-        (\_ -> hClose handle)+sendMessage (Ref handle) msg =+  void $ (try $ IRC.hPutMessage handle msg :: IO (Either IOException ()))  -- | Add a line to the log file.-logLine :: String -> IO ()-logLine = UTF8.putStrLn+logLine :: Text -> IO ()+logLine = T.putStrLn++-- | Normalize the username for a filename.+normalizeUser :: [Char] -> [Char]+normalizeUser = filter (\c -> isDigit c || isLetter c)++-- | Send events that the user missed.+sendEvents :: Config -> Ref -> UserName -> IO [[HulkWriter]]+sendEvents config ref user = do+  events <- getLog config+  UserData{userDataLastSeen=lastSeen} <- getUser config (userText user)+  let filtered = flip filter events $ \(time,_from,_typ,_params) ->+                  time >. lastSeen+  forM filtered $ \msg -> do+    case msg of+      (time,from',rpl@RPL_PRIVMSG,[name,msg'])+        | name == userText user || "#" `T.isPrefixOf` name -> do+        let from = T.filter (\c -> isDigit c || isLetter c) from'+            user' = User (encodeUtf8 from)+                         (encodeUtf8 from)+                         "offline"+            message = Message (Just user')+                              (makeCommand rpl+                                           [name,"[" <> pack (show time) <> "] " <> msg'])+        return [MessageReply ref message+               ,outgoing message]+      _ -> return []++  where x >. y = x `diffUTCTime` y > 0+        outgoing msg = outgoingWriter ref msg++-- | Get the log.+getLog :: FromJSON b => Config -> IO [b]+getLog Config{..} = do+  contents <- L.readFile configLogFile+  return $ mapMaybe decode $ L8.lines contents++-- | Get the user data.+getUser :: Config -> Text -> IO UserData+getUser Config{..} name = do+  let fname = configUserData </> normalizeUser (unpack name)+  now <- getCurrentTime+  exists <- doesFileExist fname+  if exists+     then do contents <- L.readFile fname+             case decode contents of+               Just u -> return u+               Nothing -> error ("unable to parse user file: " ++ fname)+     else return $ UserData (UserName (mk name)) now++-- | Get authorization info.+getAuth :: Config -> IO (String,String)+getAuth Config{..} =+  (,) <$> readFile configPasswdKey+      <*> readFile configPasswd++-- | Save the message to the log.+saveToLog :: MVar () -> Config -> Text -> RPL -> [Text] -> IO ()+saveToLog lvar Config{..} name rpl params = do+  now <- getCurrentTime+  withMVar lvar $ const $+    L.appendFile configLogFile $+      encode ((now,name,rpl,params)) <> "\n"
src/Hulk/Types.hs view
@@ -1,186 +1,275 @@-{-# OPTIONS -Wall -fno-warn-name-shadowing #-}+{-# LANGUAGE DeriveFunctor              #-}+{-# LANGUAGE DeriveGeneric              #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}-module Hulk.Types where+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RecordWildCards            #-} -import Control.Monad.Identity-import Control.Monad.Reader-import Control.Monad.State-import Control.Monad.Writer-import Data.Char-import Data.Function+module Hulk.Types+  (Config (..)+  ,Nick (..) -- FIXME:+  ,nickText+  ,UserName (..) -- FIXME:+  ,userText+  ,ChannelName (..) -- FIXME:+  ,channelNameText+  ,Channel (..)+  ,Client (..)+  ,User (..)+  ,UnregUser (..)+  ,RegUser (..)+  ,Ref (..)+  ,mkRef+  ,UserData (..)+  ,Conn (..)+  ,Event (..)+  ,RPL (..)+  ,QuitType (..)+  ,ChannelReplyType (..)+  ,Hulk+  ,runHulk+  ,HulkReader(..)+  ,HulkWriter(..)+  ,HulkState(..))+  where++import Control.Applicative+import Control.Monad.RWS+import Data.Aeson+import Data.CaseInsensitive import Data.Map               (Map)+import Data.Set               (Set)+import Data.Text              (Text) import Data.Time+import GHC.Generics import Network-import Network.IRC            hiding (Channel)+import Network.FastIRC        (Message) import System.IO -data Config = Config {-      configListen :: PortNumber-    , configHostname :: String-    , configMotd :: Maybe FilePath-    , configPreface :: Maybe FilePath-    , configPasswd :: FilePath-    , configPasswdKey :: FilePath-    } deriving (Show)+--------------------------------------------------------------------------------+-- Configuration -newtype Ref = Ref { unRef :: Handle } -    deriving (Show,Eq)+-- | Server configuration.+data Config = Config+  { configListen    :: !PortNumber+  , configHostname  :: !Text+  , configMotd      :: !(Maybe FilePath)+  , configPreface   :: !(Maybe FilePath)+  , configPasswd    :: !FilePath+  , configPasswdKey :: !FilePath+  , configUserData  :: !FilePath+  , configLogFile   :: !FilePath+  , configLogChans  :: ![Text]+  } deriving (Show) -instance Ord Ref where-  compare = on compare show+--------------------------------------------------------------------------------+-- Fundamental IRC data types --- | Construct a Ref value.-newRef :: Handle -> Ref-newRef = Ref+-- | A case-insensitive nickname.+newtype Nick = NickName (CI Text)+  deriving (Show,Eq,Ord) -data Error = Error String+-- | Extract the text of a nickname for use in output.+nickText :: Nick -> Text+nickText (NickName ci) = original ci -data Env = Env {-   envClients :: Map Ref Client-  ,envNicks :: Map Nick Ref-  ,envChannels :: Map ChannelName Channel-}+-- | A case-insensitive username.+newtype UserName = UserName (CI Text)+  deriving (Show,Eq,Ord,Generic) -newtype Nick = Nick { unNick :: String } deriving Show+instance ToJSON UserName where+  toJSON (UserName ci) = toJSON (original ci)+instance FromJSON UserName where+  parseJSON = fmap (UserName . mk) . parseJSON -instance Ord Nick where-  compare = on compare (map toLower . unNick)-instance Eq Nick where-  (==) = on (==) (map toLower . unNick)+-- | Extract the text of a username for use in output.+userText :: UserName -> Text+userText (UserName ci) = original ci -newtype ChannelName = ChannelName { unChanName :: String } deriving Show-  -instance Ord ChannelName where-  compare = on compare (map toLower . unChanName)-instance Eq ChannelName where-  (==) = on (==) (map toLower . unChanName)+-- | A case-insensitive channel name.+newtype ChannelName = ChannelName (CI Text)+  deriving (Show,Eq,Ord) -data Channel = Channel {-      channelName :: ChannelName-    , channelTopic :: Maybe String-    , channelUsers :: [Ref]-} deriving Show+-- | Extract the text of a channelname for use in output.+channelNameText :: ChannelName -> Text+channelNameText (ChannelName ci) = original ci -data User = Unregistered UnregUser | Registered RegUser+--------------------------------------------------------------------------------+-- Server state types++-- | A channel.+data Channel = Channel+  { channelName  :: !ChannelName+  , channelTopic :: !(Maybe Text)+  , channelUsers :: !(Set Ref)+  } deriving (Show)++--------------------------------------------------------------------------------+-- Client data types++-- | A connected client.+data Client = Client+  { clientRef      :: !Ref+  , clientUser     :: !User+  , clientHostname :: !Text+  , clientLastPong :: !UTCTime+  } deriving (Show)++-- | Some user, either unregistered or registered.+data User+  = Unregistered UnregUser+  | Registered RegUser   deriving Show -data UnregUser = UnregUser {-   unregUserName :: Maybe String-  ,unregUserNick :: Maybe Nick-  ,unregUserUser :: Maybe String-  ,unregUserPass :: Maybe String-} deriving Show+-- | An unregistered user.+data UnregUser = UnregUser+  { unregUserName :: !(Maybe Text)+  , unregUserNick :: !(Maybe Nick)+  , unregUserUser :: !(Maybe UserName)+  , unregUserPass :: !(Maybe Text)+  } deriving (Show) -data RegUser = RegUser {-   regUserName :: String-  ,regUserNick :: Nick-  ,regUserUser :: String-  ,regUserPass :: String-} deriving Show+-- | A registered user.+data RegUser = RegUser+  { regUserName :: !Text+  , regUserNick :: !Nick+  , regUserUser :: !UserName+  , regUserPass :: !Text+  } deriving (Show) -data Client = Client {-      clientRef      :: Ref-    , clientUser     :: User-    , clientHostname :: String-    , clientLastPong :: UTCTime-    } deriving Show+-- | A reference for a client.+newtype Ref = Ref { unRef :: Handle }+  deriving (Show,Eq) -data Conn = Conn {-   connRef        :: Ref-  ,connHostname   :: String-  ,connServerName :: String-  ,connTime       :: UTCTime-} deriving Show+-- | Make a ref.+mkRef :: Handle -> Ref+mkRef = Ref -data Reply = MessageReply Ref Message | LogReply String | Close+-- | Use for refs in maps.+instance Ord Ref where+  compare x y = if x == y then EQ else LT -newtype IRC m a = IRC { -    runIRC :: ReaderT (UTCTime,Conn) (WriterT [Reply] (StateT Env m)) a-  }-  deriving (Monad-           ,Functor-           ,MonadWriter [Reply]-           ,MonadState Env-           ,MonadReader (UTCTime,Conn))+-- | Data saved about a user for later actions like log recall.+data UserData = UserData+   { userDataUser     :: !UserName+   , userDataLastSeen :: !UTCTime+   } deriving (Show,Generic) -data Event = PASS | USER | NICK | PING | QUIT | TELL | JOIN | PART | PRIVMSG-           | NOTICE | ISON | WHOIS | TOPIC | CONNECT | DISCONNECT | PINGPONG-           | PONG-           | NOTHING-  deriving (Read,Show)+instance ToJSON UserData+instance FromJSON UserData +--------------------------------------------------------------------------------+-- Client handling types -data RPL = RPL_WHOISUSER-         | RPL_NICK-         | RPL_PONG-         | RPL_JOIN-         | RPL_QUIT-         | RPL_NOTICE-         | RPL_PART-         | RPL_PRIVMSG-         | RPL_ISON-         | RPL_JOINS-         | RPL_TOPIC-         | RPL_NAMEREPLY-         | RPL_ENDOFNAMES-         | ERR_NICKNAMEINUSE-         | RPL_WELCOME-         | RPL_MOTDSTART-         | RPL_MOTD-         | RPL_ENDOFMOTD-         | RPL_WHOISIDLE-         | RPL_ENDOFWHOIS-         | RPL_WHOISCHANNELS-         | ERR_NOSUCHNICK-         | ERR_NOSUCHCHANNEL-         | RPL_PING-  deriving Show+-- | The Hulk client monad.+newtype Hulk a = Hulk { runHulk :: RWS HulkReader [HulkWriter] HulkState a }+  deriving (Monad,+            Functor,+            Applicative,+            MonadReader HulkReader,+            MonadWriter [HulkWriter],+            MonadState HulkState) -fromRPL :: RPL -> String-fromRPL RPL_WHOISUSER     = "311"-fromRPL RPL_NICK          = "NICK"-fromRPL RPL_PONG          = "PONG"-fromRPL RPL_QUIT          = "QUIT"-fromRPL RPL_JOIN          = "JOIN"-fromRPL RPL_NOTICE        = "NOTICE"-fromRPL RPL_PART          = "PART"-fromRPL RPL_PRIVMSG       = "PRIVMSG"-fromRPL RPL_ISON          = "303"-fromRPL RPL_JOINS         = "JOIN"-fromRPL RPL_TOPIC         = "TOPIC"-fromRPL RPL_NAMEREPLY     = "353"-fromRPL RPL_ENDOFNAMES    = "366"-fromRPL RPL_WELCOME       = "001"-fromRPL RPL_MOTDSTART     = "375"-fromRPL RPL_MOTD          = "372"-fromRPL RPL_ENDOFMOTD     = "376"-fromRPL RPL_WHOISIDLE     = "317"-fromRPL RPL_WHOISCHANNELS = "319"-fromRPL RPL_ENDOFWHOIS    = "318"-fromRPL ERR_NICKNAMEINUSE = "433"-fromRPL ERR_NOSUCHNICK    = "401"-fromRPL ERR_NOSUCHCHANNEL = "403"-fromRPL RPL_PING          = "PING"+-- | Configuration/environment information for running the client+-- handler.+data HulkReader = HulkReader+  { readTime   :: !UTCTime+  , readConn   :: !Conn+  , readConfig :: !Config+  , readMotd   :: !(Maybe Text)+  , readAuth   :: (String,String)+  } deriving (Show) -data QuitType = RequestedQuit | SocketQuit deriving Eq+-- | State of the whole server, which the client handles.+data HulkState = HulkState+  { stateClients  :: !(Map Ref Client)+  , stateNicks    :: !(Map Nick Ref)+  , stateChannels :: !(Map ChannelName Channel)+  } deriving (Show) -data ChannelReplyType = IncludeMe | ExcludeMe deriving Eq+-- | Replies are generated by the client after some messages.+data HulkWriter+  = MessageReply !Ref !Message+  | LogReply !Text+  | Close+  | Bump !Ref+  | UpdateUserData !UserData+  | SaveLog !Text !RPL ![Text]+  | SendEvents !Ref !UserName+  deriving (Show) -class Monad m => MonadProvider m where-  providePreface   :: m (Maybe String)-  provideMotd      :: m (Maybe String)-  provideKey       :: m String-  providePasswords :: m String+-- | Used when handling a line from a client.+data Conn = Conn+  { connRef        :: !Ref+  , connHostname   :: !Text+  , connServerName :: !Text+  , connTime       :: !UTCTime+  } deriving (Show) -newtype HulkIO a = HulkIO { runHulkIO :: ReaderT Config IO a }- deriving (Monad,MonadReader Config,Functor,MonadIO)+-- | An incoming client message.+data Event+  = PASS+  | USER+  | NICK+  | PING+  | QUIT+  | TELL+  | JOIN+  | PART+  | PRIVMSG+  | NOTICE+  | ISON+  | WHOIS+  | TOPIC+  | CONNECT+  | DISCONNECT+  | PINGPONG+  | PONG+  | NAMES+  | NOTHING+  deriving (Show,Read) -newtype HulkP a = HulkP { runHulkPure :: Identity a }- deriving (Monad)+-- | An outgoing server reply.+data RPL+  = RPL_WHOISUSER+  | RPL_NICK+  | RPL_PONG+  | RPL_JOIN+  | RPL_QUIT+  | RPL_NOTICE+  | RPL_PART+  | RPL_PRIVMSG+  | RPL_ISON+  | RPL_JOINS+  | RPL_TOPIC+  | RPL_NAMEREPLY+  | RPL_ENDOFNAMES+  | ERR_NICKNAMEINUSE+  | RPL_WELCOME+  | RPL_MOTDSTART+  | RPL_MOTD+  | RPL_ENDOFMOTD+  | RPL_WHOISIDLE+  | RPL_ENDOFWHOIS+  | RPL_WHOISCHANNELS+  | ERR_NOSUCHNICK+  | ERR_NOSUCHCHANNEL+  | RPL_PING+  deriving (Show,Generic) -instance MonadTrans IRC where-  lift m = do-    s <- get-    IRC $ ReaderT $ \_ -> WriterT $ StateT $ \_ -> do-      a <- m-      return ((a,[]),s)+instance ToJSON RPL+instance FromJSON RPL++-- | When quitting it can either be due to user request, ping timeout,+-- or the socket was closed.+data QuitType+  = RequestedQuit+  | SocketQuit+  deriving (Show,Eq)++-- | When sending a channel reply, it can either include the current+-- client or exclude them (e.g. when the client sends a message, it's+-- no use echoing it back to that user).+data ChannelReplyType+  = IncludeMe+  | ExcludeMe+  deriving (Show,Eq)
− txt/MOTD
@@ -1,7 +0,0 @@-Hello and welcome to this IRC server. I am your host, Dr. Bruce Banner-PhD in nuclear physics and HRRRRRNNNNNNNNNNNNGGGGGGGGGGGGGGGGGGGGGGGGG-HRRRRRNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN-NNRUN FOR YOUR LIFENNNNNNNNNNNNNNNNNNNNNNNNNGGGGGGGGNNNNNNNNNNNNNNNNNN-NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN-GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG-GG..
− txt/PREFACE
@@ -1,14 +0,0 @@-You must login with a username and password. If you have not already-created one, you should do so by generating one and adding it to the-Hulk passwd file in passwd. To generate a user/password-key/pair, use:-- $ genpass myusername--It will prompt for a password. Inspect it first to confirm it is-correct. To write it to the passwd file use-- $ genpass myusername >> /path/to/hulk/data/passwd--And configure your IRC client to use your username and to send a-*server* password.