funbot-0.1.0.0: src/FunBot/Memos.hs
{- This file is part of funbot.
-
- Written in 2015 by fr33domlover <fr33domlover@rel4tion.org>.
-
- ♡ Copying is an act of love. Please copy, reuse and share.
-
- The author(s) have dedicated all copyright and related and neighboring
- rights to this software to the public domain worldwide. This software is
- distributed without any warranty.
-
- You should have received a copy of the CC0 Public Domain Dedication along
- with this software. If not, see
- <http://creativecommons.org/publicdomain/zero/1.0/>.
-}
-- For JSON field names and irc-fun-color StyledString
{-# LANGUAGE OverloadedStrings #-}
module FunBot.Memos
( submitMemo
, reportMemos
, loadBotMemos
, mkSaveBotMemos
--, saveBotMemos
)
where
import Control.Applicative ((<$>), (<*>))
import Control.Monad (liftM, mzero, unless)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson hiding (encode)
import qualified Data.HashMap.Lazy as M
import Data.Monoid ((<>))
import Data.Settings.Persist
import Data.Time.Units (Second)
import FunBot.Types
import FunBot.Util ((!?))
import Network.IRC.Fun.Bot.Chat (sendToChannel, sendToUser)
import Network.IRC.Fun.Bot.Nicks (isInChannel, presence)
import Network.IRC.Fun.Bot.State
import Network.IRC.Fun.Color
import Text.Printf (printf)
-------------------------------------------------------------------------------
-- Utilities
-------------------------------------------------------------------------------
getMemos :: BotSession (M.HashMap String [Memo])
getMemos = getStateS memos
putMemos :: M.HashMap String [Memo] -> BotSession ()
putMemos ms = modifyState $ \ s -> s { memos = ms }
modifyMemos :: (M.HashMap String [Memo] -> M.HashMap String [Memo])
-> BotSession ()
modifyMemos f = modifyState $ \ s -> s { memos = f $ memos s }
getTimeStr :: BotSession String
getTimeStr = do
getTime <- askTimeGetter
liftIO $ liftM snd getTime
-- | Get a list of the memos saved for a user, in the order they were sent.
getUserMemos :: String -- ^ User nickname
-> BotSession [Memo]
getUserMemos recip = liftM (M.lookupDefault [] recip) getMemos
insertMemo :: String -> Memo -> BotSession ()
insertMemo recip memo = do
ms <- getMemos
let oldList = M.lookupDefault [] recip ms
newList = oldList ++ [memo]
putMemos $ M.insert recip newList ms
-- | Delete all memos for a given recipient, if any exist.
deleteUserMemos :: String -- ^ Recipient nickname
-> BotSession ()
deleteUserMemos recip = modifyMemos $ M.delete recip
-- | Prepare an IRC message which displays a memo.
formatMemo :: Maybe String -- ^ Optional recipient nickname to mention
-> Int -- ^ Memo index to display
-> Memo -- ^ Memo to format
-> String
formatMemo (Just recip) _idx memo =
printf "%v, %v said in %v UTC:\n\"%v\""
recip
(memoSender memo)
(memoTime memo)
(memoContent memo)
formatMemo Nothing idx memo =
let n = Maroon #> ("[" <> Pure (show idx) <> "]")
time = Purple #> Pure (memoTime memo ++ " UTC")
sender = Gray #> "<" <> Green #> Pure (memoSender memo) <> Gray #> ">"
content = Pure $ memoContent memo
in encode $ n <> " " <> time <> " " <> sender <> " " <> content
-- | Send a memo to its destination, nicely formatted.
sendMemo :: String -- ^ Recipient nickname
-> Int -- ^ Memo index number for display (i.e. 1-based)
-> Memo -- ^ Memo to display on IRC
-> BotSession ()
sendMemo recip idx memo =
case memoSendIn memo of
Just chan -> sendToChannel chan $ formatMemo (Just recip) idx memo
Nothing -> sendToUser recip $ formatMemo Nothing idx memo
-- | Send a memo to its destination, nicely formatted.
sendMemoList :: String -- ^ Recipient nickname
-> Int -- ^ First memo's index number for display
-> [Memo] -- ^ Memos to display on IRC
-> BotSession ()
sendMemoList recip idx ms =
let send (i, m) = sendMemo recip i m
in mapM_ send $ zip [idx..] ms
-- | An instant memo response into the source channel or in PM.
sendInstant :: String -- ^ Sender nickname
-> Maybe String -- ^ Source channel
-> String -- ^ Recipient nickname
-> String -- ^ Message
-> BotSession ()
sendInstant sender mchan recip content =
case mchan of
Just chan -> sendToChannel chan msg
Nothing -> sendToUser recip msg
where
msg = printf "%v, %v says: %v" recip sender content
-- | Report to sender than their memo has been saved.
confirm :: String -- ^ Sender nickname
-> Maybe String -- ^ Whether sent 'Just' in channel or in PM.
-> String -- ^ Recipient nickname
-> BotSession ()
confirm sender (Just chan) recip =
sendToChannel chan $
printf "%v, your memo for %v has been saved." sender recip
confirm sender Nothing recip =
sendToUser sender $
printf "Your memo for %v has been saved." recip
-------------------------------------------------------------------------------
-- Operations
-------------------------------------------------------------------------------
-- | Record a new memo for a given user.
addMemo :: String -- ^ Sender nickname
-> Maybe String -- ^ Whether received in 'Just' a channel, or in PM
-> Maybe String -- ^ Whether to send in 'Just' a channel, or in PM
-> String -- ^ Recipient nickname
-> String -- ^ Memo content
-> BotSession ()
addMemo sender recv send recip content = do
time <- getTimeStr
let memo = Memo
{ memoTime = time
, memoSender = sender
, memoRecvIn = recv
, memoSendIn = send
, memoContent = content
}
insertMemo recip memo
-- | Send a memo with the given index if exists. Return 'Nothing' on success,
-- or 'Just' the number of saved memos for the nickname on failure (invalid
-- index).
sendOneMemo :: String -- ^ Recipient nickname
-> Int -- ^ Memo number, 0-based
-> BotSession (Maybe Int)
sendOneMemo recip idx = do
ms <- getMemos
case M.lookup recip ms of
Just l -> case l !? idx of
Just memo -> sendMemo recip (idx + 1) memo >> return Nothing
Nothing -> return $ Just $ length l
Nothing -> return $ Just 0
-- | Delete a memo for a given recipient with the given index (position in the
-- memo list). On success, return 'Nothing'. On error, return 'Just' the number
-- of saved memos the receipient has.
deleteOneMemo :: String -- ^ Recipient nickname
-> Int -- ^ Memo index number, 0-based
-> BotSession (Maybe Int)
deleteOneMemo recip idx = do
ms <- getMemos
case M.lookup recip ms of
Just l -> case splitAt idx l of
([], _:[]) -> do
putMemos $ M.delete recip ms
return Nothing
(b, _:a) -> do
putMemos $ M.insert recip (b ++ a) ms
return Nothing
_ -> return $ Just $ length l
Nothing -> return $ Just 0
-------------------------------------------------------------------------------
-- Handlers
-------------------------------------------------------------------------------
-- | React to a user's request to make a new memo.
--
-- If user is online in same channel, send instantly to channel.
-- If user is online in another channel, send in PM (and report to sender).
-- If user not online, save memo and report to sender.
submitMemo :: String -- ^ Sender nickname
-> Maybe String -- ^ Whether sent in 'Just' a channel, or in PM
-> String -- ^ Recipient nickname
-> String -- ^ Memo content
-> BotSession ()
submitMemo sender source recip content = do
let instantToChan =
case source of
Just chan -> do
isin <- recip `isInChannel` chan
if isin
then do
sendInstant sender (Just chan) recip content
return True
else return False
Nothing -> return False
instantToUser = do
p <- presence recip
if not $ null p
then do
sendInstant sender Nothing recip content
return True
else return False
keepForLater = do
addMemo sender source Nothing recip content
saveBotMemos
confirm sender source recip
succ1 <- instantToChan
unless succ1 $ do
succ2 <- instantToUser
unless succ2 keepForLater
-- | When a user logs in, use this to send them a report of the memos saved for
-- them, if any exist.
reportMemos :: String -- ^ User nickname
-> BotSession ()
reportMemos recip = do
ms <- getUserMemos recip
unless (null ms) $ do
sendToUser recip $ "You have " ++ show (length ms) ++ " memos:"
sendMemoList recip 1 ms
deleteUserMemos recip
saveBotMemos
-------------------------------------------------------------------------------
-- Persistence
-------------------------------------------------------------------------------
instance FromJSON Memo where
parseJSON (Object o) =
Memo <$>
o .: "time" <*>
o .: "sender" <*>
o .: "recv-in" <*>
o .: "send-in" <*>
o .: "content" {-<*>
o .: "read"-}
parseJSON _ = mzero
instance ToJSON Memo where
toJSON (Memo time sender recvIn sendIn content {-rd-}) = object
[ "time" .= time
, "sender" .= sender
, "recv-in" .= recvIn
, "send-in" .= sendIn
, "content" .= content
--, "read" .= rd
]
memosFilename = "state/memos.json"
saveInterval = 3 :: Second
loadBotMemos :: IO (M.HashMap String [Memo])
loadBotMemos = do
r <- loadSettings memosFilename
case r of
Left (False, e) -> error $ "Failed to read memos file: " ++ e
Left (True, e) -> error $ "Failed to parse memos file: " ++ e
Right s -> return s
mkSaveBotMemos :: IO (M.HashMap String [Memo] -> IO ())
mkSaveBotMemos = mkSaveSettings saveInterval memosFilename
saveBotMemos :: BotSession ()
saveBotMemos = do
ms <- getStateS memos
save <- askEnvS saveMemos
liftIO $ save ms