{-# LANGUAGE OverloadedStrings #-} {- | Administrative channel operations -} module Moderation where import Control.Applicative import Control.Lens import Data.ByteString (ByteString) import Data.Foldable (for_) import Data.Maybe (fromMaybe) import Data.Time import Data.Text (Text) import Data.List (nub, delete) import Data.List.Split (chunksOf) import Data.Monoid import qualified Data.ByteString.Char8 as B8 import qualified Data.Text.Encoding as Text import Irc.Cmd import Irc.Format import Irc.Model import Irc.Message import ClientState deopWaitDuration :: NominalDiffTime deopWaitDuration = 5*60 -- seconds -- | Perform a privileged operation. If the connection doesn't -- already have +o on the channel it will be requested from -- ChanServ and the privileged operation will be scheduled to -- run when the connection gets +o. doWithOps :: Identifier {- ^ channel -} -> (ClientState -> IO ClientState) {- ^ privileged operation -} -> ClientState -> IO ClientState doWithOps = doWithOps' False doWithOps' :: Bool {- ^ permanent change -} -> Identifier {- ^ channel -} -> (ClientState -> IO ClientState) {- ^ privileged operation -} -> ClientState -> IO ClientState doWithOps' perm chan privop st | initiallyOp = finishUp st | otherwise = getOpFirst where conn = view (clientServer0 . ccConnection) st myNick = view connNick conn -- was I op when the command was entered initiallyOp = nickHasModeInChannel myNick 'o' chan conn handler = EventHandler { _evName = "Get op for privop" , _evOnEvent = \evTgt evMsg evSt -> case view mesgType evMsg of ModeMsgType True 'o' modeNick | mkId modeNick == myNick , evTgt == chan -> finishUp evSt _ -> return (over clientAutomation (cons handler) evSt) } finishUp st1 = privop =<< installTimer st1 getOpFirst = do clientSend (privMsgCmd "chanserv" ("op " <> idDenote chan)) st return (over clientAutomation (cons handler) st) computeDeopTime = do now <- getCurrentTime return (addUTCTime deopWaitDuration now) installTimer st0 | perm && deopScheduled chan st0 = return $ filterTimerEvents (/= DropOperator chan) st0 | perm = return st0 | deopScheduled chan st0 || not initiallyOp = do time <- computeDeopTime return $ addTimerEvent time (DropOperator chan) $ filterTimerEvents (/= DropOperator chan) st0 | otherwise = return st0 -- | Predicate to determine if a deop is scheduled to happen deopScheduled :: Identifier {- ^ channel -} -> ClientState -> Bool deopScheduled = elemOf (clientTimers . folded . folded . _DropOperator) doAutoKickBan :: Identifier {- ^ channel -} -> Identifier {- ^ nick -} -> Text {- ^ reason -} -> ClientState -> IO ClientState doAutoKickBan chan nick reason st = -- TODO: Look up account name or hostname! do clientSend (modeCmd chan ["+b",banMask]) st clientSend (kickCmd chan nick (Text.encodeUtf8 reason)) st return st where conn = view (clientServer0 . ccConnection) st usr = view (connUsers . at nick) conn nickMask = idDenote nick <> "!*@*" banMask = fromMaybe nickMask $ previews (folded . usrAccount . folded) ("$a:"<>) usr <|> previews (folded . usrHost . folded) ("*!*@"<>) usr -- | Cancel any pending deop timer if I'm deopped cancelDeopTimerOnDeop :: EventHandler cancelDeopTimerOnDeop = EventHandler { _evName = "cancel deop timer on deop" , _evOnEvent = \evTgt evMsg evSt -> let evSt' = reschedule evSt conn = view (clientServer0 . ccConnection) evSt in case view mesgType evMsg of ModeMsgType False 'o' modeNick | mkId modeNick == view connNick conn -> return $ filterTimerEvents (/= DropOperator evTgt) evSt' _ -> return evSt' } where reschedule = over clientAutomation (cons cancelDeopTimerOnDeop) doOp :: ClientState -> [Identifier] -> IO ClientState doOp st nicks | Just chan <- focusedChan st = doWithOps' (null nicks || myNick `elem` nicks) -- permanent? chan (massModeChange True 'o' chan (nub (delete myNick nicks))) (clearInput st) | otherwise = return st where conn = view (clientServer0 . ccConnection) st myNick = view connNick conn doDeop :: ClientState -> [Identifier] -> IO ClientState doDeop st nicks | Just chan <- focusedChan st = doWithOps chan (massModeChange False 'o' chan nicks') (clearInput st) | otherwise = return st where -- deop myself last nicks' | null nicks = [myNick] | myNick `elem` nicks = nub (delete myNick nicks) ++ [myNick] | otherwise = nicks conn = view (clientServer0 . ccConnection) st myNick = view connNick conn doVoice :: ClientState -> [Identifier] -> IO ClientState doVoice st nicks | Just chan <- focusedChan st = doWithOps chan (massModeChange True 'v' chan nicks') (clearInput st) | otherwise = return st where conn = view (clientServer0 . ccConnection) st nicks' | null nicks = [view connNick conn] | otherwise = nub nicks doDevoice :: ClientState -> [Identifier] -> IO ClientState doDevoice st nicks | Just chan <- focusedChan st = doWithOps chan (massModeChange False 'v' chan nicks') (clearInput st) | otherwise = return st where conn = view (clientServer0 . ccConnection) st nicks' | null nicks = [view connNick conn] | otherwise = nub nicks massModeChange :: Bool {- ^ polarity -} -> Char {- ^ mode -} -> Identifier {- ^ channel -} -> [Identifier] {- ^ nicks -} -> ClientState -> IO ClientState massModeChange polarity mode chan nicks st = do let nickChunks = chunksOf (view connModes conn) nicks for_ nickChunks $ \nickChunk -> clientSend (modeCmd chan (modeArg (length nickChunk) : map idBytes nickChunk)) st return st where conn = view (clientServer0 . ccConnection) st polarityBs | polarity = B8.empty | otherwise = B8.singleton '-' modeArg n = polarityBs <> B8.replicate n mode doTopicCmd :: ByteString {- ^ new topic -} -> ClientState -> IO ClientState doTopicCmd topic st | not (B8.null topic) , Just chan <- focusedChan st = let go st' = st' <$ clientSend (topicCmd chan topic) st' in case preview (connChannels . ix chan . chanModes . folded) conn of -- check if it's known that the mode isn't +t Just modes | hasn't (ix 't') modes -> go (clearInput st) _ -> doWithOps chan go (clearInput st) | otherwise = return st where conn = view (clientServer0 . ccConnection) st doInvite :: ClientState -> Identifier {- ^ nickname -} -> IO ClientState doInvite st nick = case focusedChan st of Nothing -> return st Just chan -- 'g' is the "FREEINVITE" mode, don't check for ops | channelHasMode chan 'g' conn -> go (clearInput st) -- it's an error to invite someone already in channel | has (connChannels . ix chan . chanUsers . ix nick) conn -> return st | otherwise -> doWithOps chan go (clearInput st) where conn = view (clientServer0 . ccConnection) st go st' = st' <$ clientSend (inviteCmd nick chan) st' doKick :: ClientState -> Identifier {- ^ nick -} -> Text {- ^ reason -} -> IO ClientState doKick st nick msg | Just chan <- focusedChan st = doWithOps chan (\evSt -> evSt <$ clientSend (kickCmd chan nick (Text.encodeUtf8 msg)) evSt) (clearInput st) | otherwise = return st doRemove :: ClientState -> Identifier {- ^ nick -} -> Text {- ^ reason -} -> IO ClientState doRemove st nick msg | Just chan <- focusedChan st = doWithOps chan (\evSt -> evSt <$ clientSend (removeCmd chan nick (Text.encodeUtf8 msg)) evSt) (clearInput st) | otherwise = return st