packages feed

glirc-2.40: src/Client/WhoReply.hs

{-# Language TemplateHaskell, OverloadedStrings, BangPatterns #-}

{-|
Module      : Client.WhoReply
Description : Parsed replies from WHO
Copyright   : (c) TheDaemoness, 2023
License     : ISC
Maintainer  : emertens@gmail.com

Because WHOX allows for a LOT of fiddliness regarding parameters,
this is extracted from Client.State.Network and given its own module.
-}

module Client.WhoReply
  ( WhoReply
  , WhoReplyItem
  , newWhoReply
  , finishWhoReply
  , recordWhoReply
  , recordWhoXReply
  , mapJoinWhoFields
  , whoFilterText
  
  -- Lenses
  , whoQuery
  , whoFields
  , whoToken
  , whoDone
  , whoItems
  , whoUserInfo
  , whoIp
  , whoServer
  , whoAway
  , whoMiscFlags
  , whoHops
  , whoIdleSecs
  , whoAcct
  , whoOpLvl
  , whoRealname
  ) where

import           Client.Image.Message (cleanText)
import           Control.Lens
import           Control.Lens.Unsound (lensProduct) -- Don't worry about it. Ctrl+F SOUNDNESS.
import           Data.List (sort)
import           Data.Set (Set)
import qualified Data.Set as Set
import           Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Lazy as LText
import           Irc.Identifier
import           Irc.UserInfo
import           Text.Read (readMaybe)
  
data WhoReply = WhoReply
  { _whoQuery  :: !(Text, Maybe Text)
  , _whoFields :: !(Set Char)
  , _whoToken  :: !String
  , _whoDone   :: !Bool
  , _whoItems  :: ![WhoReplyItem]
  }

data WhoReplyItem = WhoReplyItem
  { _whoUserInfo  :: !UserInfo
  , _whoAcct      :: !Identifier
  , _whoIp        :: !Text -- We don't have iproute; (Maybe IP) would be nice here.
  , _whoServer    :: !Identifier
  , _whoAway      :: !(Maybe Bool)
  , _whoMiscFlags :: !Text
  , _whoHops      :: !(Maybe Int)
  , _whoIdleSecs  :: !String -- This can be a Maybe Int, but prettyTime takes a String.
  , _whoOpLvl     :: !Text
  , _whoRealname  :: !Text
  } deriving (Eq, Ord)

makeLenses ''WhoReply
makeLenses ''WhoReplyItem

newWhoReply :: Text -> String -> WhoReply
newWhoReply query "" = WhoReply
  { _whoQuery = (query, Nothing)
  , _whoToken = ""
  , _whoFields = Set.empty
  , _whoDone = False
  , _whoItems = []
  }
newWhoReply query ('%':arg) = WhoReply
  { _whoQuery = (query, Just $ Text.pack ('%':arg))
  , _whoToken = if Set.member 't' fieldSet then token' else ""
  , _whoFields = fieldSet
  , _whoDone = False
  , _whoItems = []
  }
  where
    fieldSet = Set.fromList fields
    (fields, token) = over _2 (drop 1) $ break (== ',') arg
    token' = if null token then "0" else token
newWhoReply query arg = WhoReply
  { _whoQuery = (query, Just $ Text.pack arg)
  , _whoToken = ""
  , _whoFields = Set.empty
  , _whoDone = False
  , _whoItems = []
  }

splitFlags :: String -> (Maybe Bool, Text)
splitFlags ('G':rest) = (Just True,  Text.pack rest)
splitFlags ('H':rest) = (Just False, Text.pack rest)
splitFlags rest       = (Nothing,    Text.pack rest)

newWhoReplyItem :: WhoReplyItem
newWhoReplyItem = WhoReplyItem
  { _whoUserInfo  = UserInfo
    { userNick = mkId ""
    , userName = ""
    , userHost = ""
    }
  , _whoAcct      = "0"
  , _whoIp        = "255.255.255.255"
  , _whoServer    = ""
  , _whoAway      = Nothing
  , _whoMiscFlags = ""
  , _whoHops      = Nothing
  , _whoIdleSecs  = ""
  , _whoOpLvl     = "n/a"
  , _whoRealname  = ""
  }

finishWhoReply :: WhoReply -> WhoReply
finishWhoReply wr = wr { _whoDone = True, _whoItems = reverse $ sort (_whoItems wr) }

recordWhoReply :: [Text] -> WhoReply -> WhoReply
recordWhoReply [_, _, uname, host, server, nick, flags, hcrn] reply
  | _whoDone reply = reply
  | otherwise = reply { _whoItems = wri:_whoItems reply}
  where
    wri = newWhoReplyItem
      { _whoUserInfo = UserInfo { userNick = mkId nick, userName = uname, userHost = host }
      , _whoServer = mkId server
      , _whoAway = away
      , _whoMiscFlags = miscFlags
      , _whoHops = readMaybe $ Text.unpack hops
      , _whoRealname = Text.stripStart realname
      }
    (hops, realname) = Text.break (== ' ') hcrn
    (away, miscFlags) = splitFlags $ Text.unpack flags
recordWhoReply _ reply = reply

-- | Field names for WHOX replies in order, excluding 't'.
whoXReplyFields :: [Char]
whoXReplyFields = "cuihsnfdlaor"

recordWhoXReply :: [Text] -> WhoReply -> WhoReply
recordWhoXReply []       reply = reply
recordWhoXReply (_:args) reply
  | _whoDone reply = reply
  | _whoToken reply == "" = withWri args
  | null args = reply
  | _whoToken reply == Text.unpack (head args) = withWri $ tail args
  | otherwise = reply
  where
    fields = filter ((flip Set.member) (_whoFields reply)) whoXReplyFields
    withWri args' = reply { _whoItems = recordWhoXReply' (zip args' fields) newWhoReplyItem:_whoItems reply}

recordWhoXReply' :: [(Text, Char)] -> WhoReplyItem -> WhoReplyItem
recordWhoXReply' [] = id
recordWhoXReply' ((arg, kind):rest) = recordWhoXReply' rest . updateFn
  where
    updateFn = case kind of
      'a' -> set whoAcct (mkId arg)
      -- Skip c
      'd' -> set whoHops (readMaybe $ Text.unpack arg)
      -- SOUNDNESS: whoAway and whoMiscFlags project disjoint parts of WhoReplyItem
      'f' -> set (lensProduct whoAway whoMiscFlags) flagsSplit
      'h' -> set (whoUserInfo . uiHost) arg
      'i' -> set whoIp arg
      'l' -> set whoIdleSecs (Text.unpack arg)
      'n' -> set (whoUserInfo . uiNick) (mkId arg)
      'o' -> set whoOpLvl arg
      'r' -> set whoRealname arg
      's' -> set whoServer (mkId arg)
      'u' -> set (whoUserInfo . uiName) arg
      _   -> id
    flagsSplit = splitFlags $ Text.unpack arg

-- Map non-default field values and join them into a list.
mapJoinWhoFields :: WhoReplyItem ->
  (UserInfo -> a) ->
  (Identifier -> a) ->
  (Text -> a) ->
  (Identifier -> a) ->
  a ->
  (Text -> a) ->
  (Int -> a) ->
  (String -> a) ->
  (Text -> a) ->
  (Text -> a) ->
  [a]
mapJoinWhoFields wri userinfo acct ip server away flags hops idle oplvl gecos = reverse $
  addFieldIf (require notNull . view whoRealname) gecos $
  addFieldIf (require (/= "n/a") . view whoOpLvl) oplvl $
  addFieldIf (require notNullOrZero . view whoIdleSecs) idle $
  addFieldIf (\n -> view whoHops n >>= require (> 0)) hops $
  addFieldIf (require notNull . view whoMiscFlags) flags $
  addFieldIf (\n -> view whoAway n >>= require id) (const away) $
  addFieldIf (require (notNull . idText) . view whoServer) server $
  addFieldIf (require (/= "255.255.255.255") . view whoIp) ip $
  addFieldIf (require (/= "0") . view whoAcct) acct
  [userinfo $ view whoUserInfo wri]
  where
    addFieldIf :: (WhoReplyItem -> Maybe a) -> (a -> b) -> [b] -> [b]
    addFieldIf getF mapF list = case getF wri of
      Just v -> mapF v:list
      Nothing -> list
    notNull = not . Text.null
    notNullOrZero ""  = False
    notNullOrZero "0" = False
    notNullOrZero _   = True
    require f v
      | f v = Just v
      | otherwise = Nothing

whoFilterText :: WhoReplyItem -> LText.Text
whoFilterText entry = LText.fromChunks $ concat $ mapJoinWhoFields entry
  (\x -> [renderUserInfo x])
  (\x -> [" $a:", idText x])
  (\x -> [" ip: ", x])
  (\x -> [" server: ", idText x])
  [" away"]
  (\x -> [" flags: ", x])
  (\x -> [" hops: ", Text.pack $ show x])
  (\x -> [" idle: ", Text.pack x])
  (\x -> [" oplvl: ", x])
  (\x -> [" gecos: ", cleanText x])