packages feed

irc-core-1.0: driver/Moderation.hs

{-# 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