{-# Language BlockArguments, TemplateHaskell, OverloadedStrings, BangPatterns #-}
module Client.State.Network
(
NetworkState(..)
, AuthenticateState(..)
, ConnectRestriction(..)
, newNetworkState
, csNick
, csChannels
, csSocket
, csModeTypes
, csChannelTypes
, csTransaction
, csModes
, csSnomask
, csStatusMsg
, csSettings
, csUserInfo
, csUsers
, csUser
, csModeCount
, csNetwork
, csNextPingTime
, csPingStatus
, csLatency
, csLastReceived
, csCertificate
, csMessageHooks
, csAuthenticationState
, csSeed
, Transaction(..)
, isChannelIdentifier
, iHaveOp
, sendMsg
, initialMessages
, squelchIrcMsg
, Apply(..)
, applyMessage
, hideMessage
, PingStatus(..)
, _PingConnecting
, TimedAction(..)
, nextTimedAction
, applyTimedAction
, useChanServ
, sendModeration
, sendTopic
) where
import qualified Client.Authentication.Ecdsa as Ecdsa
import Client.Configuration.ServerSettings
import Client.Network.Async
import Client.State.Channel
import Client.UserHost
import Client.Hook (MessageHook)
import Client.Hooks (messageHooks)
import Control.Lens
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashSet as HashSet
import qualified Data.ByteString as B
import qualified Data.Map.Strict as Map
import Data.Bits
import Data.Foldable
import Data.List
import qualified Data.List.NonEmpty as NonEmpty
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.Text.Read as Text
import Data.Time
import Data.Time.Clock.POSIX
import Irc.Codes
import Irc.Commands
import Irc.Identifier
import Irc.Message
import Irc.Modes
import Irc.RawIrcMsg
import Irc.UserInfo
import LensUtils
import qualified System.Random as Random
data NetworkState = NetworkState
{ _csChannels :: !(HashMap Identifier ChannelState)
, _csSocket :: !NetworkConnection
, _csModeTypes :: !ModeTypes
, _csUmodeTypes :: !ModeTypes
, _csChannelTypes :: ![Char]
, _csTransaction :: !Transaction
, _csModes :: ![Char]
, _csSnomask :: ![Char]
, _csStatusMsg :: ![Char]
, _csSettings :: !ServerSettings
, _csUserInfo :: !UserInfo
, _csUsers :: !(HashMap Identifier UserAndHost)
, _csModeCount :: !Int
, _csNetwork :: !Text
, _csMessageHooks :: ![MessageHook]
, _csAuthenticationState :: !AuthenticateState
, _csNextPingTime :: !(Maybe UTCTime)
, _csLatency :: !(Maybe NominalDiffTime)
, _csPingStatus :: !PingStatus
, _csLastReceived :: !(Maybe UTCTime)
, _csCertificate :: ![Text]
, _csSeed :: Random.StdGen
}
data AuthenticateState
= AS_None
| AS_PlainStarted
| AS_EcdsaStarted
| AS_EcdsaWaitChallenge
| AS_ExternalStarted
deriving Show
data PingStatus
= PingSent !UTCTime
| PingNone
| PingConnecting !Int !(Maybe UTCTime) !ConnectRestriction
deriving Show
data ConnectRestriction
= NoRestriction
| StartTLSRestriction
| WaitTLSRestriction
deriving Show
data TimedAction
= TimedDisconnect
| TimedSendPing
| TimedForgetLatency
deriving (Eq, Ord, Show)
data Transaction
= NoTransaction
| NamesTransaction [Text]
| BanTransaction [(Text,MaskListEntry)]
| WhoTransaction [UserInfo]
| CapLsTransaction [(Text, Maybe Text)]
deriving Show
makeLenses ''NetworkState
makePrisms ''Transaction
makePrisms ''PingStatus
makePrisms ''TimedAction
defaultChannelTypes :: String
defaultChannelTypes = "#&"
csNick :: Lens' NetworkState Identifier
csNick = csUserInfo . uiNick
sendMsg :: NetworkState -> RawIrcMsg -> IO ()
sendMsg cs msg =
case (view msgCommand msg, view msgParams msg) of
("PRIVMSG", [tgt,txt]) -> multiline "PRIVMSG" tgt txt
("NOTICE", [tgt,txt]) -> multiline "NOTICE" tgt txt
_ -> transmit msg
where
transmit = send (view csSocket cs) . renderRawIrcMsg
actionPrefix = "\^AACTION "
actionSuffix = "\^A"
multiline cmd tgt txt
| Just txt1 <- Text.stripPrefix actionPrefix txt
, Just txt2 <- Text.stripSuffix actionSuffix txt1 =
let txtChunks = utf8ChunksOf maxContentLen txt2
maxContentLen = computeMaxMessageLength (view csUserInfo cs) tgt
- Text.length actionPrefix - Text.length actionSuffix
in for_ txtChunks $ \txtChunk ->
transmit $ rawIrcMsg cmd [tgt, actionPrefix <> txtChunk <> actionSuffix]
multiline cmd tgt txt =
let txtChunks = utf8ChunksOf maxContentLen txt
maxContentLen = computeMaxMessageLength (view csUserInfo cs) tgt
in for_ txtChunks $ \txtChunk ->
transmit $ rawIrcMsg cmd [tgt, txtChunk]
utf8ChunksOf :: Int -> Text -> [Text]
utf8ChunksOf n txt
| B.length enc <= n = [txt]
| otherwise = search 0 0 txt info
where
isBeginning b = b .&. 0xc0 /= 0x80
enc = Text.encodeUtf8 txt
beginnings = B.findIndices isBeginning enc
info = zip3 [0..]
beginnings
(drop 1 beginnings ++ [B.length enc])
search startByte startChar currentTxt xs =
case dropWhile (\(_,_,byteLen) -> byteLen-startByte <= n) xs of
[] -> [currentTxt]
(charIx,byteIx,_):xs' ->
case Text.splitAt (charIx - startChar) currentTxt of
(a,b) -> a : search byteIx charIx b xs'
newNetworkState ::
Text ->
ServerSettings ->
NetworkConnection ->
PingStatus ->
Random.StdGen ->
NetworkState
newNetworkState network settings sock ping seed = NetworkState
{ _csUserInfo = UserInfo "*" "" ""
, _csChannels = HashMap.empty
, _csSocket = sock
, _csChannelTypes = defaultChannelTypes
, _csModeTypes = defaultModeTypes
, _csUmodeTypes = defaultUmodeTypes
, _csTransaction = NoTransaction
, _csModes = ""
, _csSnomask = ""
, _csStatusMsg = ""
, _csSettings = settings
, _csModeCount = 3
, _csUsers = HashMap.empty
, _csNetwork = network
, _csMessageHooks = buildMessageHooks (view ssMessageHooks settings)
, _csAuthenticationState = AS_None
, _csPingStatus = ping
, _csLatency = Nothing
, _csNextPingTime = Nothing
, _csLastReceived = Nothing
, _csCertificate = []
, _csSeed = seed
}
buildMessageHooks :: [HookConfig] -> [MessageHook]
buildMessageHooks = mapMaybe \(HookConfig name args) ->
do hookFun <- HashMap.lookup name messageHooks
hookFun args
data Apply = Apply [RawIrcMsg] NetworkState
hideMessage :: IrcMsg -> Bool
hideMessage m =
case m of
Authenticate{} -> True
BatchStart{} -> True
BatchEnd{} -> True
Ping{} -> True
Pong{} -> True
Reply RPL_WHOSPCRPL [_,"616",_,_,_,_] -> True
_ -> False
noReply :: NetworkState -> Apply
noReply = reply []
reply :: [RawIrcMsg] -> NetworkState -> Apply
reply = Apply
overChannel :: Identifier -> (ChannelState -> ChannelState) -> NetworkState -> NetworkState
overChannel chan = overStrict (csChannels . ix chan)
overChannels :: (ChannelState -> ChannelState) -> NetworkState -> NetworkState
overChannels = overStrict (csChannels . traverse)
applyMessage :: ZonedTime -> IrcMsg -> NetworkState -> Apply
applyMessage msgWhen msg cs
= applyMessage' msgWhen msg
$ set csLastReceived (Just $! zonedTimeToUTC msgWhen) cs
applyMessage' :: ZonedTime -> IrcMsg -> NetworkState -> Apply
applyMessage' msgWhen msg cs =
case msg of
Ping args -> reply [ircPong args] cs
Pong _ -> noReply (doPong msgWhen cs)
Join user chan acct _ ->
reply response
$ recordUser user acct
$ overChannel chan (joinChannel (userNick user))
$ createOnJoin user chan cs
where
showAccounts = view (csSettings . ssShowAccounts) cs
response
| userNick user == view csNick cs =
ircMode chan [] :
[ircWho [idText chan, "%tuhna,616"] | showAccounts ]
| otherwise = []
Account user acct ->
noReply
$ recordUser user acct cs
Chghost user newUser newHost ->
noReply
$ updateUserInfo (userNick user) newUser newHost cs
Quit user _reason ->
noReply
$ forgetUser (userNick user)
$ overChannels (partChannel (userNick user)) cs
Part user chan _mbreason -> exitChannel chan (userNick user)
Kick _kicker chan nick _reason -> exitChannel chan nick
Nick oldNick newNick ->
noReply
$ renameUser (userNick oldNick) newNick
$ updateMyNick (userNick oldNick) newNick
$ overChannels (nickChange (userNick oldNick) newNick) cs
Reply RPL_WELCOME (me:_) -> doWelcome msgWhen (mkId me) cs
Reply RPL_SASLSUCCESS _ -> reply [ircCapEnd] cs
Reply RPL_SASLFAIL _ -> reply [ircCapEnd] cs
Reply ERR_NICKNAMEINUSE (_:badnick:_)
| PingConnecting{} <- view csPingStatus cs -> doBadNick badnick cs
Reply ERR_BANNEDNICK (_:badnick:_)
| PingConnecting{} <- view csPingStatus cs -> doBadNick badnick cs
Reply ERR_ERRONEUSNICKNAME (_:badnick:_)
| PingConnecting{} <- view csPingStatus cs -> doBadNick badnick cs
Reply ERR_UNAVAILRESOURCE (_:badnick:_)
| PingConnecting{} <- view csPingStatus cs -> doBadNick badnick cs
Reply RPL_HOSTHIDDEN (_:host:_) ->
noReply (set (csUserInfo . uiHost) host cs)
Reply RPL_WHOSPCRPL [_me,"616",user,host,nick,acct] ->
let acct' = if acct == "0" then "*" else acct
in noReply (recordUser (UserInfo (mkId nick) user host) acct' cs)
Reply code args -> doRpl code msgWhen args cs
Cap cmd -> doCap cmd cs
Authenticate param -> doAuthenticate param cs
Mode who target (modes:params) -> doMode msgWhen who target modes params cs
Topic user chan topic -> noReply (doTopic msgWhen user chan topic cs)
_ -> noReply cs
where
exitChannel chan nick
| nick == view csNick cs = noReply $ pruneUsers
$ over csChannels (sans chan) cs
| otherwise = noReply $ forgetUser' nick
$ overChannel chan (partChannel nick) cs
pruneUsers :: NetworkState -> NetworkState
pruneUsers cs = over csUsers (`HashMap.intersection` u) cs
where
u = foldOf (csChannels . folded . chanUsers) cs
doWelcome ::
ZonedTime ->
Identifier ->
NetworkState ->
Apply
doWelcome msgWhen me
= noReply
. set csNick me
. set csNextPingTime (Just $! addUTCTime 30 (zonedTimeToUTC msgWhen))
. set csPingStatus PingNone
doBadNick ::
Text ->
NetworkState ->
Apply
doBadNick badNick cs =
case NonEmpty.dropWhile (badNick/=) (view (csSettings . ssNicks) cs) of
_:next:_ -> reply [ircNick next] cs
_ -> doRandomNick cs
doRandomNick :: NetworkState -> Apply
doRandomNick cs = reply [ircNick candidate] cs'
where
limit = 9
range = (0, 99999::Int)
suffix = show n
primaryNick = NonEmpty.head (view (csSettings . ssNicks) cs)
candidate = Text.take (limit-length suffix) primaryNick <> Text.pack suffix
(n, cs') = cs & csSeed %%~ Random.uniformR range
doTopic :: ZonedTime -> UserInfo -> Identifier -> Text -> NetworkState -> NetworkState
doTopic when user chan topic =
overChannel chan (setTopic topic . set chanTopicProvenance (Just $! prov))
where
prov = TopicProvenance
{ _topicAuthor = user
, _topicTime = zonedTimeToUTC when
}
parseTimeParam :: Text -> Maybe UTCTime
parseTimeParam txt =
case Text.decimal txt of
Right (i, rest) | Text.null rest ->
Just $! posixSecondsToUTCTime (fromInteger i)
_ -> Nothing
doRpl :: ReplyCode -> ZonedTime -> [Text] -> NetworkState -> Apply
doRpl cmd msgWhen args cs =
case cmd of
RPL_UMODEIS ->
case args of
_me:modes:params
| Just xs <- splitModes (view csUmodeTypes cs) modes params ->
noReply
$ doMyModes xs
$ set csModes "" cs
_ -> noReply cs
RPL_SNOMASK ->
case args of
_me:snomask0:_
| Just snomask <- Text.stripPrefix "+" snomask0 ->
noReply (set csSnomask (Text.unpack snomask) cs)
_ -> noReply cs
RPL_NOTOPIC ->
case args of
_me:chan:_ -> noReply
$ overChannel
(mkId chan)
(setTopic "" . set chanTopicProvenance Nothing)
cs
_ -> noReply cs
RPL_TOPIC ->
case args of
_me:chan:topic:_ -> noReply (overChannel (mkId chan) (setTopic topic) cs)
_ -> noReply cs
RPL_TOPICWHOTIME ->
case args of
_me:chan:who:whenTxt:_ | Just when <- parseTimeParam whenTxt ->
let !prov = TopicProvenance
{ _topicAuthor = parseUserInfo who
, _topicTime = when
}
in noReply (overChannel (mkId chan) (set chanTopicProvenance (Just prov)) cs)
_ -> noReply cs
RPL_CREATIONTIME ->
case args of
_me:chan:whenTxt:_ | Just when <- parseTimeParam whenTxt ->
noReply (overChannel (mkId chan) (set chanCreation (Just when)) cs)
_ -> noReply cs
RPL_CHANNEL_URL ->
case args of
_me:chan:urlTxt:_ ->
noReply (overChannel (mkId chan) (set chanUrl (Just urlTxt)) cs)
_ -> noReply cs
RPL_MYINFO -> noReply (myinfo args cs)
RPL_ISUPPORT -> noReply (isupport args cs)
RPL_NAMREPLY ->
case args of
_me:_sym:_tgt:x:_ ->
noReply $
over csTransaction
(\t -> let xs = view _NamesTransaction t
in xs `seq` NamesTransaction (x:xs))
cs
_ -> noReply cs
RPL_ENDOFNAMES ->
case args of
_me:tgt:_ -> noReply (loadNamesList (mkId tgt) cs)
_ -> noReply cs
RPL_BANLIST ->
case args of
_me:_tgt:mask:who:whenTxt:_ -> noReply (recordListEntry mask who whenTxt cs)
_ -> noReply cs
RPL_ENDOFBANLIST ->
case args of
_me:tgt:_ -> noReply (saveList 'b' tgt cs)
_ -> noReply cs
RPL_QUIETLIST ->
case args of
_me:_tgt:_q:mask:who:whenTxt:_ -> noReply (recordListEntry mask who whenTxt cs)
_ -> noReply cs
RPL_ENDOFQUIETLIST ->
case args of
_me:tgt:_ -> noReply (saveList 'q' tgt cs)
_ -> noReply cs
RPL_INVEXLIST ->
case args of
_me:_tgt:mask:who:whenTxt:_ -> noReply (recordListEntry mask who whenTxt cs)
_ -> noReply cs
RPL_ENDOFINVEXLIST ->
case args of
_me:tgt:_ -> noReply (saveList 'I' tgt cs)
_ -> noReply cs
RPL_EXCEPTLIST ->
case args of
_me:_tgt:mask:who:whenTxt:_ -> noReply (recordListEntry mask who whenTxt cs)
_ -> noReply cs
RPL_ENDOFEXCEPTLIST ->
case args of
_me:tgt:_ -> noReply (saveList 'e' tgt cs)
_ -> noReply cs
RPL_WHOREPLY ->
case args of
_me:_tgt:uname:host:_server:nick:_ ->
noReply $
over csTransaction (\t ->
let !x = UserInfo (mkId nick) uname host
!xs = view _WhoTransaction t
in WhoTransaction (x : xs))
cs
_ -> noReply cs
RPL_ENDOFWHO -> noReply (massRegistration cs)
RPL_CHANNELMODEIS ->
case args of
_me:chan:modes:params ->
doMode msgWhen who chanId modes params
$ set (csChannels . ix chanId . chanModes) Map.empty cs
where chanId = mkId chan
!who = UserInfo "*" "" ""
_ -> noReply cs
_ -> noReply cs
recordListEntry ::
Text ->
Text ->
Text ->
NetworkState -> NetworkState
recordListEntry mask who whenTxt =
case parseTimeParam whenTxt of
Nothing -> id
Just when ->
over csTransaction $ \t ->
let !x = MaskListEntry
{ _maskListSetter = who
, _maskListTime = when
}
!xs = view _BanTransaction t
in BanTransaction ((mask,x):xs)
saveList ::
Char ->
Text ->
NetworkState -> NetworkState
saveList mode tgt cs
= set csTransaction NoTransaction
$ setStrict
(csChannels . ix (mkId tgt) . chanLists . at mode)
(Just $! newList)
cs
where
newList = HashMap.fromList (view (csTransaction . _BanTransaction) cs)
squelchReply :: ReplyCode -> Bool
squelchReply rpl =
case rpl of
RPL_NAMREPLY -> True
RPL_ENDOFNAMES -> True
RPL_BANLIST -> True
RPL_ENDOFBANLIST -> True
RPL_INVEXLIST -> True
RPL_ENDOFINVEXLIST -> True
RPL_EXCEPTLIST -> True
RPL_ENDOFEXCEPTLIST -> True
RPL_QUIETLIST -> True
RPL_ENDOFQUIETLIST -> True
RPL_CHANNELMODEIS -> True
RPL_UMODEIS -> True
RPL_SNOMASK -> True
RPL_WHOREPLY -> True
RPL_ENDOFWHO -> True
RPL_WHOSPCRPL -> True
RPL_TOPICWHOTIME -> True
RPL_CREATIONTIME -> True
RPL_CHANNEL_URL -> True
RPL_NOTOPIC -> True
_ -> False
squelchIrcMsg :: IrcMsg -> Bool
squelchIrcMsg (Reply rpl _) = squelchReply rpl
squelchIrcMsg _ = False
doMode ::
ZonedTime ->
UserInfo ->
Identifier ->
Text ->
[Text] ->
NetworkState ->
Apply
doMode when who target modes args cs
| view csNick cs == target
, Just xs <- splitModes (view csUmodeTypes cs) modes args =
noReply (doMyModes xs cs)
| isChannelIdentifier cs target
, Just xs <- splitModes (view csModeTypes cs) modes args
, let cs' = doChannelModes when who target xs cs =
if iHaveOp target cs'
then let (response, cs_) = cs' & csChannels . ix target . chanQueuedModeration <<.~ []
in reply response cs_
else noReply cs'
doMode _ _ _ _ _ cs = noReply cs
iHaveOp :: Identifier -> NetworkState -> Bool
iHaveOp channel cs =
elemOf (csChannels . ix channel . chanUsers . ix me . folded) '@' cs
where
me = view csNick cs
doChannelModes :: ZonedTime -> UserInfo -> Identifier -> [(Bool, Char, Text)] -> NetworkState -> NetworkState
doChannelModes when who chan changes cs = overChannel chan applyChannelModes cs
where
modeTypes = view csModeTypes cs
sigilMap = view modesPrefixModes modeTypes
listModes = view modesLists modeTypes
applyChannelModes c = foldl' applyChannelMode c changes
applyChannelMode c (polarity, mode, arg)
| Just sigil <- lookup mode sigilMap =
overStrict (chanUsers . ix (mkId arg))
(setPrefixMode polarity sigil)
c
| mode `elem` listModes =
let entry | polarity = Just $! MaskListEntry
{ _maskListSetter = renderUserInfo who
, _maskListTime = zonedTimeToUTC when
}
| otherwise = Nothing
in setStrict (chanLists . ix mode . at arg) entry c
| polarity = set (chanModes . at mode) (Just arg) c
| otherwise = over chanModes (sans mode) c
setPrefixMode polarity sigil sigils
| not polarity = delete sigil sigils
| sigil `elem` sigils = sigils
| otherwise = filter (`elem` sigils') (map snd sigilMap)
where
sigils' = sigil : sigils
doMyModes :: [(Bool, Char, Text)] -> NetworkState -> NetworkState
doMyModes changes = over csModes $ \modes -> sort (foldl' applyOne modes changes)
where
applyOne modes (True, mode, _)
| mode `elem` modes = modes
| otherwise = mode:modes
applyOne modes (False, mode, _) = delete mode modes
selectCaps ::
NetworkState ->
[(Text, Maybe Text)] ->
[Text]
selectCaps cs offered = (supported `intersect` Map.keys capMap)
`union`
view (csSettings . ssCapabilities) cs
where
capMap = Map.fromList offered
supported =
sasl ++ serverTime ++
["multi-prefix", "batch", "znc.in/playback", "znc.in/self-message"
, "cap-notify", "extended-join", "account-notify", "chghost"
, "userhost-in-names", "account-tag" ]
serverTime
| "server-time" `Map.member` capMap = ["server-time"]
| "znc.in/server-time-iso" `Map.member` capMap = ["znc.in/server-time-iso"]
| otherwise = []
ss = view csSettings cs
sasl = ["sasl" | isJust (view ssSaslMechanism ss) ]
doAuthenticate :: Text -> NetworkState -> Apply
doAuthenticate param cs =
case view csAuthenticationState cs of
AS_PlainStarted
| "+" <- param
, Just (SaslPlain mbAuthz authc (SecretText pass)) <- view ssSaslMechanism ss
, let authz = fromMaybe "" mbAuthz
-> reply
(ircAuthenticates (encodePlainAuthentication authz authc pass))
(set csAuthenticationState AS_None cs)
AS_ExternalStarted
| "+" <- param
, Just (SaslExternal mbAuthz) <- view ssSaslMechanism ss
, let authz = fromMaybe "" mbAuthz
-> reply
(ircAuthenticates (encodeExternalAuthentication authz))
(set csAuthenticationState AS_None cs)
AS_EcdsaStarted
| "+" <- param
, Just (SaslEcdsa mbAuthz authc _) <- view ssSaslMechanism ss
, let authz = fromMaybe authc mbAuthz
-> reply
(ircAuthenticates (Ecdsa.encodeAuthentication authz authc))
(set csAuthenticationState AS_EcdsaWaitChallenge cs)
AS_EcdsaWaitChallenge -> noReply cs
_ -> reply [ircCapEnd] cs
where
ss = view csSettings cs
doCap :: CapCmd -> NetworkState -> Apply
doCap cmd cs =
case cmd of
(CapLs CapMore caps) ->
noReply (set csTransaction (CapLsTransaction (caps ++ prevCaps)) cs)
where
prevCaps = view (csTransaction . _CapLsTransaction) cs
CapLs CapDone caps
| null reqCaps -> reply [ircCapEnd] cs'
| otherwise -> reply [ircCapReq reqCaps] cs'
where
reqCaps = selectCaps cs (caps ++ view (csTransaction . _CapLsTransaction) cs)
cs' = set csTransaction NoTransaction cs
CapNew caps
| null reqCaps -> noReply cs
| otherwise -> reply [ircCapReq reqCaps] cs
where
reqCaps = selectCaps cs caps
CapDel _ -> noReply cs
CapAck caps
| let ss = view csSettings cs
, "sasl" `elem` caps
, Just mech <- view ssSaslMechanism ss ->
case mech of
SaslEcdsa{} ->
reply [ircAuthenticate Ecdsa.authenticationMode]
(set csAuthenticationState AS_EcdsaStarted cs)
SaslPlain{} ->
reply [ircAuthenticate "PLAIN"]
(set csAuthenticationState AS_PlainStarted cs)
SaslExternal{} ->
reply [ircAuthenticate "EXTERNAL"]
(set csAuthenticationState AS_ExternalStarted cs)
_ -> reply [ircCapEnd] cs
initialMessages :: NetworkState -> [RawIrcMsg]
initialMessages cs
= [ ircCapLs ]
++ [ ircPass pass | Just (SecretText pass) <- [view ssPassword ss]]
++ [ ircNick (views ssNicks NonEmpty.head ss)
, ircUser (view ssUser ss) (view ssReal ss)
]
where
ss = view csSettings cs
loadNamesList :: Identifier -> NetworkState -> NetworkState
loadNamesList chan cs
= set csTransaction NoTransaction
$ flip (foldl' (flip learnUserInfo)) (fst <$> entries)
$ setStrict (csChannels . ix chan . chanUsers) newChanUsers
$ cs
where
newChanUsers = HashMap.fromList [ (view uiNick ui, modes) | (ui, modes) <- entries ]
learnUserInfo (UserInfo n u h)
| Text.null u || Text.null h = id
| otherwise = updateUserInfo n u h
sigils = toListOf (csModeTypes . modesPrefixModes . folded . _2) cs
splitEntry modes str
| Text.head str `elem` sigils = splitEntry (Text.head str : modes)
(Text.tail str)
| otherwise = (parseUserInfo str, reverse modes)
entries :: [(UserInfo, [Char])]
entries = fmap (splitEntry "")
$ concatMap Text.words
$ view (csTransaction . _NamesTransaction) cs
createOnJoin :: UserInfo -> Identifier -> NetworkState -> NetworkState
createOnJoin who chan cs
| userNick who == view csNick cs =
set csUserInfo who
$ set (csChannels . at chan) (Just newChannel) cs
| otherwise = cs
updateMyNick :: Identifier -> Identifier -> NetworkState -> NetworkState
updateMyNick oldNick newNick cs
| oldNick == view csNick cs = set csNick newNick cs
| otherwise = cs
myinfo ::
[Text] ->
NetworkState ->
NetworkState
myinfo (_me : _host : _version : umodes : _) =
set (csUmodeTypes . modesNeverArg) (delete 's' (Text.unpack umodes))
myinfo _ = id
isupport ::
[Text] ->
NetworkState ->
NetworkState
isupport [] conn = conn
isupport params conn = foldl' (flip isupport1) conn
$ map parseISupport
$ init params
where
isupport1 ("CHANTYPES",types) = set csChannelTypes (Text.unpack types)
isupport1 ("CHANMODES",modes) = updateChanModes modes
isupport1 ("PREFIX" ,modes) = updateChanPrefix modes
isupport1 ("STATUSMSG",prefix) = set csStatusMsg (Text.unpack prefix)
isupport1 ("MODES",nstr) | Right (n,"") <- Text.decimal nstr =
set csModeCount n
isupport1 _ = id
parseISupport :: Text -> (Text,Text)
parseISupport str =
case Text.break (=='=') str of
(key,val) -> (key, Text.drop 1 val)
updateChanModes ::
Text ->
NetworkState ->
NetworkState
updateChanModes modes
= over csModeTypes
$ set modesLists listModes
. set modesAlwaysArg alwaysModes
. set modesSetArg setModes
. set modesNeverArg neverModes
where
next = over _2 (drop 1) . break (==',')
(listModes ,modes1) = next (Text.unpack modes)
(alwaysModes,modes2) = next modes1
(setModes ,modes3) = next modes2
(neverModes ,_) = next modes3
updateChanPrefix ::
Text ->
NetworkState ->
NetworkState
updateChanPrefix txt =
case parsePrefixes txt of
Just prefixes -> set (csModeTypes . modesPrefixModes) prefixes
Nothing -> id
parsePrefixes :: Text -> Maybe [(Char,Char)]
parsePrefixes txt =
case uncurry Text.zip (Text.break (==')') txt) of
('(',')'):rest -> Just rest
_ -> Nothing
isChannelIdentifier :: NetworkState -> Identifier -> Bool
isChannelIdentifier cs ident =
case Text.uncons (idText ident) of
Just (p, _) -> p `elem` view csChannelTypes cs
_ -> False
csUser :: Functor f => Identifier -> LensLike' f NetworkState (Maybe UserAndHost)
csUser i = csUsers . at i
recordUser :: UserInfo -> Text -> NetworkState -> NetworkState
recordUser (UserInfo nick user host) acct
| Text.null user || Text.null host = id
| otherwise = set (csUsers . at nick)
(Just $! UserAndHost user host acct)
updateUserInfo ::
Identifier ->
Text ->
Text ->
NetworkState -> NetworkState
updateUserInfo nick user host =
over (csUsers . at nick) $ \old ->
Just $! UserAndHost user host (maybe "" _uhAccount old)
forgetUser :: Identifier -> NetworkState -> NetworkState
forgetUser = over csUsers . sans
renameUser :: Identifier -> Identifier -> NetworkState -> NetworkState
renameUser old new cs = set (csUsers . at new) entry cs'
where
(entry,cs') = cs & csUsers . at old <<.~ Nothing
forgetUser' :: Identifier -> NetworkState -> NetworkState
forgetUser' nick cs
| keep = cs
| otherwise = forgetUser nick cs
where
keep = has (csChannels . folded . chanUsers . ix nick) cs
massRegistration :: NetworkState -> NetworkState
massRegistration cs
= set csTransaction NoTransaction
$ over csUsers updateUsers cs
where
infos = view (csTransaction . _WhoTransaction) cs
channelUsers =
HashSet.fromList (views (csChannels . folded . chanUsers) HashMap.keys cs)
updateUsers users = foldl' updateUser users infos
updateUser users (UserInfo nick user host)
| not (Text.null user)
, not (Text.null host)
, HashSet.member nick channelUsers =
HashMap.alter
(\mb -> case mb of
Nothing -> Just $! UserAndHost user host ""
Just (UserAndHost _ _ acct) -> Just $! UserAndHost user host acct
) nick users
| otherwise = users
nextTimedAction :: NetworkState -> Maybe (UTCTime, TimedAction)
nextTimedAction ns = minimumOf (folded.folded) actions
where
actions = [nextPingAction ns, nextForgetAction ns]
nextForgetAction :: NetworkState -> Maybe (UTCTime, TimedAction)
nextForgetAction ns =
do sentAt <- preview (csPingStatus . _PingSent) ns
latency <- view csLatency ns
let delay = max 0.1 (3 * latency)
eventAt = addUTCTime delay sentAt
return (eventAt, TimedForgetLatency)
nextPingAction :: NetworkState -> Maybe (UTCTime, TimedAction)
nextPingAction cs =
do runAt <- view csNextPingTime cs
return (runAt, action)
where
action =
case view csPingStatus cs of
PingSent{} -> TimedDisconnect
PingNone -> TimedSendPing
PingConnecting{} -> TimedSendPing
doPong :: ZonedTime -> NetworkState -> NetworkState
doPong when cs = set csPingStatus PingNone
$ set csLatency (Just delta) cs
where
delta =
case view csPingStatus cs of
PingSent sent -> diffUTCTime (zonedTimeToUTC when) sent
_ -> 0
applyTimedAction :: TimedAction -> NetworkState -> IO NetworkState
applyTimedAction action cs =
case action of
TimedForgetLatency ->
do return $! set csLatency Nothing cs
TimedDisconnect ->
do abortConnection PingTimeout (view csSocket cs)
return $! set csNextPingTime Nothing cs
TimedSendPing ->
do now <- getCurrentTime
sendMsg cs (ircPing ["ping"])
return $! set csNextPingTime (Just $! addUTCTime 60 now)
$ set csPingStatus (PingSent now) cs
sendModeration ::
Identifier ->
[RawIrcMsg] ->
NetworkState ->
IO NetworkState
sendModeration channel cmds cs
| useChanServ channel cs =
do let cmd = ircPrivmsg "ChanServ" (Text.unwords ["OP", idText channel])
sendMsg cs cmd
return $ csChannels . ix channel . chanQueuedModeration <>~ cmds $ cs
| otherwise = cs <$ traverse_ (sendMsg cs) cmds
useChanServ ::
Identifier ->
NetworkState ->
Bool
useChanServ channel cs =
channel `elem` view (csSettings . ssChanservChannels) cs &&
not (iHaveOp channel cs)
sendTopic ::
Identifier ->
Text ->
NetworkState ->
IO ()
sendTopic channelId topic cs = sendMsg cs cmd
where
chanservTopicCmd =
ircPrivmsg
"ChanServ"
(Text.unwords ["TOPIC", idText channelId, topic])
cmd
| Text.null topic = ircTopic channelId ""
| useChanServ channelId cs = chanservTopicCmd
| otherwise = ircTopic channelId topic