module Lambdabot.Plugin.Social.Seen (seenPlugin) where
import Lambdabot.Bot
import Lambdabot.Compat.AltTime
import Lambdabot.Compat.PackedNick
import Lambdabot.IRC
import Lambdabot.Logging
import qualified Lambdabot.Message as G
import Lambdabot.Monad
import Lambdabot.Nick
import Lambdabot.Plugin
import Lambdabot.Util
import Lambdabot.Plugin.Social.Seen.StopWatch
import Lambdabot.Plugin.Social.Seen.UserStatus
import Control.Exception
import Control.Monad
import Control.Monad.Trans
import Data.Binary
import qualified Data.ByteString.Char8 as P
import qualified Data.ByteString.Lazy as L
import Data.Char
import Data.List
import qualified Data.Map as M
import Text.Printf
type SeenState = (MaxMap, SeenMap)
type SeenMap = M.Map PackedNick UserStatus
type MaxMap = M.Map Channel Int
type Seen = ModuleT SeenState LB
seenPlugin :: Module (M.Map Channel Int, M.Map PackedNick UserStatus)
seenPlugin = newModule
{ moduleDefState = return (M.empty,M.empty)
, moduleCmds = return
[ (command "users")
{ help = say "users [chan]. Report the maximum number of users seen in a channel, and active users in the last 30 minutes"
, process = doUsers
}
, (command "seen")
{ help = say "seen <user>. Report if a user has been seen by the bot"
, process = doSeen
}
]
, moduleInit = do
sequence_
[ registerCallback signal (withSeenFM cb)
| (signal, cb) <- zip
["JOIN", "PART", "QUIT", "NICK", "353", "PRIVMSG"]
[joinCB, partCB, quitCB, nickCB, joinChanCB, msgCB]
]
c <- lb $ findLBFileForReading "seen"
s <- maybe (return (P.pack "")) (io . P.readFile) c
let ls = L.fromStrict s
mbDecoded <- io . try . evaluate $ decode ls
case mbDecoded of
Left exc@SomeException{} -> do
mbOld <- io . try . evaluate $ decode ls
case mbOld of
Left SomeException{} ->
warningM ("WARNING: failed to read Seen module state: " ++ show exc)
Right (maxMap, seenMap) ->
writeMS (M.mapKeys P.pack maxMap, seenMap)
Right decoded -> writeMS decoded
, moduleExit = do
chans <- lift $ ircGetChannels
unless (null chans) $ do
ct <- io getClockTime
modifyMS $ \(n,m) -> (n, botPart ct (map packNick chans) m)
withMS $ \s _ -> lb (findLBFileForWriting "seen") >>= \ c -> io (encodeFile c s)
}
lcNick :: Nick -> Nick
lcNick (Nick svr nck) = Nick svr (map toLower nck)
doUsers :: String -> Cmd Seen ()
doUsers rest = withMsg $ \msg -> do
chan <- getTarget
(m, seenFM) <- readMS
s <- io getClockTime
let who = packNick $ lcNick $ if null rest then chan else parseNick (G.server msg) rest
now = length [ () | (_,Present _ chans) <- M.toList seenFM
, who `elem` chans ]
n = case M.lookup who m of Nothing -> 1; Just n' -> n'
active = length [() | (_,st@(Present _ chans)) <- M.toList seenFM
, who `elem` chans && isActive st ]
isActive (Present (Just (ct,_td)) _cs) = recent ct
isActive _ = False
recent t = diffClockTimes s t < gap_minutes
gap_minutes = TimeDiff 1800
percent p q = 100 * (fromIntegral p / fromIntegral q) :: Double
total 0 0 = "0"
total p q = printf "%d (%0.1f%%)" p (percent p q)
say $! printf "Maximum users seen in %s: %d, currently: %s, active: %s"
(fmtNick (G.server msg) $ unpackNick who) n (total now n) (total active now)
doSeen :: String -> Cmd Seen ()
doSeen rest = withMsg $ \msg -> do
target <- getTarget
(_,seenFM) <- readMS
now <- io getClockTime
let (txt,safe) = (getAnswer msg rest seenFM now)
if safe || not ("#" `isPrefixOf` nName target)
then mapM_ say txt
else lb (ircPrivmsg (G.nick msg) (unlines txt))
getAnswer :: G.Message a => a -> String -> SeenMap -> ClockTime -> ([String], Bool)
getAnswer msg rest seenFM now
| null nick' =
let people = map fst $ filter isActive $ M.toList seenFM
isActive (_nick,state) = case state of
(Present (Just (ct,_td)) _cs) -> recent ct
_ -> False
recent t = diffClockTimes now t < gap_minutes
gap_minutes = TimeDiff 900
in (["Lately, I have seen " ++ (if null people then "nobody"
else listToStr "and" (map upAndShow people)) ++ "."], False)
| pnick == G.lambdabotName msg =
case M.lookup (packNick pnick) seenFM of
Just (Present _ cs) ->
(["Yes, I'm here. I'm in " ++ listToStr "and" (map upAndShow cs)], True)
_ -> error "I'm here, but not here. And very confused!"
| head (nName pnick) == '#' =
let people = map fst $ filter inChan $ M.toList seenFM
inChan (_nick,state) = case state of
(Present (Just _) cs)
-> packNick pnick `elem` cs
_ -> False
in (["In "++nick'++" I can see "
++ (if null people then "nobody"
else listToStr "and" (map upAndShow people)) ++ "."], False)
| otherwise = (return $ concat (case M.lookup (packNick pnick) seenFM of
Just (Present mct cs) -> nickPresent mct (map upAndShow cs)
Just (NotPresent ct td chans) -> nickNotPresent ct td (map upAndShow chans)
Just (WasPresent ct sw _ chans) -> nickWasPresent ct sw (map upAndShow chans)
Just (NewNick newnick) -> nickIsNew newnick
_ -> ["I haven't seen ", nick, "."]), True)
where
upAndShow = fmtNick (G.server msg) . unpackNick
nickPresent mct cs =
[ if you then "You are" else nick ++ " is"
, " in ", listToStr "and" cs, "."
, case mct of
Nothing -> concat [" I don't know when ", nick, " last spoke."]
Just (ct,missed) -> prettyMissed (Stopped missed)
(concat [" I last heard ", nick, " speak ",
lastSpoke ])
(" Last spoke " ++ lastSpoke)
where lastSpoke = clockDifference ct
]
nickNotPresent ct missed chans =
[ "I saw ", nick, " leaving ", listToStr "and" chans, " "
, clockDifference ct, prettyMissed missed ", and " ""
]
nickWasPresent ct sw chans =
[ "Last time I saw ", nick, " was when I left "
, listToStr "and" chans , " ", clockDifference ct
, prettyMissed sw ", and " ""
]
nickIsNew newnick =
[ if you then "You have" else nick++" has"
, " changed nick to ", us, "."
] ++ fst (getAnswer msg us seenFM now)
where
us = upAndShow $ findFunc newnick
findFunc pstr = case M.lookup pstr seenFM of
Just (NewNick pstr') -> findFunc pstr'
Just _ -> pstr
Nothing -> error "SeenModule.nickIsNew: Nothing"
nick' = takeWhile (not . isSpace) rest
you = pnick == lcNick (G.nick msg)
nick = if you then "you" else nick'
pnick = lcNick $ parseNick (G.server msg) nick'
clockDifference past
| all (==' ') diff = "just now"
| otherwise = diff ++ " ago"
where diff = timeDiffPretty . diffClockTimes now $ past
prettyMissed (Stopped _) _ifMissed _ = "."
prettyMissed _ _ _ifNotMissed = "."
msgChans :: G.Message a => a -> [Channel]
msgChans = map (packNick . lcNick) . G.channels
joinCB :: IrcMessage -> ClockTime -> PackedNick -> SeenMap -> Either String SeenMap
joinCB msg _ct nick fm
| nick == lbNick = Right fm
| otherwise = Right $! insertUpd (updateJ Nothing chans) nick newInfo fm
where
insertUpd f = M.insertWith (\_ -> f)
lbNick = packNick $ G.lambdabotName msg
newInfo = Present Nothing chans
chans = msgChans msg
botPart :: ClockTime -> [Channel] -> SeenMap -> SeenMap
botPart ct cs = fmap botPart'
where
botPart' (Present mct xs) = case xs \\ cs of
[] -> WasPresent ct (startWatch ct zeroWatch) mct cs
ys -> Present mct ys
botPart' (NotPresent ct' missed c)
| head c `elem` cs = NotPresent ct' (startWatch ct missed) c
botPart' (WasPresent ct' missed mct c)
| head c `elem` cs = WasPresent ct' (startWatch ct missed) mct c
botPart' us = us
partCB :: IrcMessage -> ClockTime -> PackedNick -> SeenMap -> Either String SeenMap
partCB msg ct nick fm
| nick == lbNick = Right $ botPart ct (msgChans msg) fm
| otherwise = case M.lookup nick fm of
Just (Present mct xs) ->
case xs \\ (msgChans msg) of
[] -> Right $! M.insert nick (NotPresent ct zeroWatch xs) fm
ys -> Right $! M.insert nick (Present mct ys) fm
_ -> Left "someone who isn't known parted"
where lbNick = packNick $ G.lambdabotName msg
quitCB :: IrcMessage -> ClockTime -> PackedNick -> SeenMap -> Either String SeenMap
quitCB _ ct nick fm = case M.lookup nick fm of
Just (Present _ct xs) -> Right $! M.insert nick (NotPresent ct zeroWatch xs) fm
_ -> Left "someone who isn't known has quit"
nickCB :: IrcMessage -> ClockTime -> PackedNick -> SeenMap -> Either String SeenMap
nickCB msg _ nick fm = case M.lookup nick fm of
Just status -> Right $! M.insert lcnewnick status
$ M.insert nick (NewNick lcnewnick) fm
_ -> Left "someone who isn't here changed nick"
where
newnick = drop 1 $ head (ircMsgParams msg)
lcnewnick = packNick $ lcNick $ parseNick (G.server msg) newnick
joinChanCB :: IrcMessage -> ClockTime -> PackedNick -> SeenMap -> Either String SeenMap
joinChanCB msg now _nick fm
= Right $! fmap (updateNP now chan) (foldl insertNick fm chanUsers)
where
l = ircMsgParams msg
chan = packNick $ lcNick $ parseNick (G.server msg) $ l !! 2
chanUsers = map (packNick . lcNick . parseNick (G.server msg)) $ words (drop 1 (l !! 3))
unUserMode nick = Nick (nTag nick) (dropWhile (`elem` "@+") $ nName nick)
insertUpd f = M.insertWith (\_ -> f)
insertNick fm' u = insertUpd (updateJ (Just now) [chan])
(packNick . unUserMode . lcNick . unpackNick $ u)
(Present Nothing [chan]) fm'
msgCB :: IrcMessage -> ClockTime -> PackedNick -> SeenMap -> Either String SeenMap
msgCB _ ct nick fm =
case M.lookup nick fm of
Just (Present _ xs) -> Right $!
M.insert nick (Present (Just (ct, noTimeDiff)) xs) fm
_ -> Left "someone who isn't here msg us"
withSeenFM :: G.Message a
=> (a -> ClockTime -> PackedNick -> SeenMap -> Either String SeenMap)
-> (a -> Seen ())
withSeenFM f msg = do
let chan = packNick . lcNick . head . G.channels $! msg
nick = packNick . lcNick . G.nick $ msg
withMS $ \(maxUsers,state) writer -> do
ct <- io getClockTime
case f msg ct nick state of
Left _ -> return ()
Right newstate -> do
let curUsers = length $!
[ () | (_,Present _ chans) <- M.toList state
, chan `elem` chans ]
newMax = case M.lookup chan maxUsers of
Nothing -> M.insert chan curUsers maxUsers
Just n -> if n < curUsers
then M.insert chan curUsers maxUsers
else maxUsers
newMax `seq` newstate `seq` writer (newMax, newstate)