{- This file is part of irc-fun-bot. - - Written in 2015, 2016 by fr33domlover . - - ♡ 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 - . -} -- For JSON field names {-# LANGUAGE OverloadedStrings #-} -- To allow 'Channel' as a JSON object key {-# LANGUAGE FlexibleInstances #-} module Network.IRC.Fun.Bot.Internal.Persist ( loadBotState , mkSaveBotState , saveBotState , selectChannel , unselectChannel , addChannelState ) where import Control.Applicative import Control.Monad (mzero, unless, when) import Control.Monad.IO.Class (liftIO) import Data.Aeson import Data.JsonState import Data.List (union) import Data.Time.Interval import Data.Time.Units (Microsecond) import Network.IRC.Fun.Bot.Internal.Instances () import Network.IRC.Fun.Bot.Internal.IrcLog (makeLogger) import Network.IRC.Fun.Bot.Internal.Monad import Network.IRC.Fun.Bot.Internal.State import Network.IRC.Fun.Bot.Internal.Types import Network.IRC.Fun.Client.NickTracker (newNetwork) import Network.IRC.Fun.Types (Channel (..)) import qualified Data.HashMap.Lazy as M import qualified Data.HashSet as S data BotStateJ = BotStateJ (M.HashMap Channel ChanInfo) (S.HashSet Channel) toJ :: BotState s -> BotStateJ toJ bstate = BotStateJ (M.map stateToInfo $ bsChannels bstate) (bsSelChans bstate) fromJ :: BotEnv e s -> BotStateJ -> s -> IO (BotState s) fromJ env (BotStateJ stateJ selJ) pub = do let f chan (ChanInfo tracking counting logging hls defresp) = do mlogger <- if logging then fmap Just $ makeLogger env chan else return Nothing return $ ChanState tracking counting mlogger hls defresp defstate = ChanInfo False False False 0 True chansConf = S.toList selJ `union` cfgChannels (beConfig env) stateConf = M.fromList $ zip chansConf (repeat defstate) stateAll = stateJ `M.union` stateConf cstate <- M.traverseWithKey f stateAll return $ BotState newNetwork cstate S.empty selJ M.empty M.empty pub instance FromJSON a => FromJSON (M.HashMap Channel a) where parseJSON v = let f (c, x) = (Channel c, x) in M.fromList . map f . M.toList <$> parseJSON v instance ToJSON a => ToJSON (M.HashMap Channel a) where toJSON m = let f (Channel c, x) = (c, x) in toJSON $ M.fromList $ map f $ M.toList m instance FromJSON ChanInfo where parseJSON (Object o) = ChanInfo <$> o .: "track" <*> o .: "count" <*> o .: "log" <*> o .: "history-lines" <*> o .: "default-response" parseJSON _ = mzero instance ToJSON ChanInfo where toJSON (ChanInfo tracking counting logging hls defresp) = object [ "track" .= tracking , "count" .= counting , "log" .= logging , "history-lines" .= hls , "default-response" .= defresp ] instance FromJSON BotStateJ where parseJSON (Object o) = BotStateJ <$> o .: "chan-state" <*> o .: "chans-join" parseJSON _ = mzero instance ToJSON BotStateJ where toJSON (BotStateJ chans sel) = object [ "chan-state" .= chans , "chans-join" .= sel ] instance ToJSON (BotState s) where toJSON bstate = toJSON $ toJ bstate loadBotState :: BotEnv e s -> s -> IO (BotState s) loadBotState env pub = do let conf = beConfig env r <- loadState $ stateFilePath (cfgStateFile conf) (cfgStateRepo conf) case r of Left (False, e) -> error $ "Failed to read state file: " ++ e Left (True, e) -> error $ "Failed to parse state file: " ++ e Right sj -> fromJ env sj pub mkSaveBotState :: Config -> IO (BotState s -> IO ()) mkSaveBotState conf = let iv = fromInteger $ microseconds $ cfgSaveInterval conf :: Microsecond msg = "auto commit by irc-fun-bot" in mkSaveStateChoose iv (cfgStateFile conf) (cfgStateRepo conf) msg saveBotState :: Session e s () saveBotState = do bstate <- get save <- asks beSaveState liftIO $ save bstate -- | Add a channel to the persistent list of channels to be joined. Next time -- the bot launches (or, say, 'joinConfig` is called), it will join this -- channel. If the channel is already listed, nothing happens. selectChannel :: Channel -> Session e s () selectChannel chan = do chans <- gets bsSelChans unless (chan `S.member` chans) $ do modify $ \ s -> s { bsSelChans = S.insert chan chans } saveBotState -- | Remove a channel from the persistent list of channels to be joined. Next -- time the bot launches, it won't join this channel (unless listed in the -- config or otherwise requested). If the channel isn't listed, nothing -- happens. unselectChannel :: Channel -> Session e s () unselectChannel chan = do chans <- gets bsSelChans when (chan `S.member` chans) $ do modify $ \ s -> s { bsSelChans = S.delete chan chans } saveBotState -- | Add default channel state for the given channel. It will be stored into -- the state file. If the channel already has state, nothing will happen. addChannelState :: Channel -> Session e s () addChannelState chan = do chans <- getChans unless (chan `M.member` chans) $ do putChans $ M.insert chan (ChanState False False Nothing 0 True) chans saveBotState