An IRC bot for learning, fun and collaboration in the Freepost community.
Clone
HTTPS:
git clone https://vervis.peers.community/repos/VvM9v
SSH:
git clone USERNAME@vervis.peers.community:VvM9v
Branches
Tags
Memos.hs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 | {- This file is part of funbot.
-
- Written in 2015, 2016 by fr33domlover <fr33domlover@riseup.net>.
-
- ♡ 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
, reportMemosAll
, loadBotMemos
, mkSaveBotMemos
)
where
import Control.Applicative ((<$>), (<*>))
import Control.Monad (liftM, mzero, unless)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson hiding (encode)
import Data.Aeson.Types (typeMismatch)
import Data.JsonState
import Data.List (partition)
import Data.Maybe (isJust)
import Data.Monoid ((<>))
import Data.Time.Units (Second)
import Formatting hiding (text)
import FunBot.Config (stateSaveInterval, configuration, memosFilename)
import FunBot.Settings.Instances
import FunBot.Types
import FunBot.Util ((!?), getTimeStr)
import Network.IRC.Fun.Bot.Chat (sendToChannel, sendToUser)
import Network.IRC.Fun.Bot.Nicks (channelIsTracked, isInChannel, presence)
import Network.IRC.Fun.Bot.State
import Network.IRC.Fun.Bot.Types (Config (cfgStateRepo))
import Network.IRC.Fun.Color
import Network.IRC.Fun.Types.Base
import qualified Data.HashMap.Lazy as M
import qualified Data.Text as T
-------------------------------------------------------------------------------
-- Utilities
-------------------------------------------------------------------------------
getMemos :: BotSession (M.HashMap Nickname [Memo])
getMemos = getStateS bsMemos
putMemos :: M.HashMap Nickname [Memo] -> BotSession ()
putMemos ms = modifyState $ \ s -> s { bsMemos = ms }
modifyMemos :: (M.HashMap Nickname [Memo] -> M.HashMap Nickname [Memo])
-> BotSession ()
modifyMemos f = modifyState $ \ s -> s { bsMemos = f $ bsMemos s }
-- | Get a list of the memos saved for a user, in the order they were sent.
getUserMemos :: Nickname
-> BotSession [Memo]
getUserMemos recip = fmap (M.lookupDefault [] recip) getMemos
insertMemo :: Nickname -> Memo -> BotSession ()
insertMemo recip memo = do
ms <- getMemos
let oldList = M.lookupDefault [] recip ms
newList = oldList ++ [memo]
putMemos $ M.insert recip newList ms
-- | Set (override) a user's memo list to the given list, discarding the memos
-- previously stored there.
setUserMemos :: Nickname -> [Memo] -> BotSession ()
setUserMemos recip memos =
modifyMemos $ if null memos then M.delete recip else M.insert recip memos
-- | Delete all memos for a given recipient, if any exist.
deleteUserMemos :: Nickname -> BotSession ()
deleteUserMemos recip = modifyMemos $ M.delete recip
-- | Prepare an IRC message which displays a memo.
formatMemo :: Maybe Nickname -- ^ Optional recipient nickname to mention
-> Int -- ^ Memo index to display
-> Memo -- ^ Memo to format
-> MsgContent
formatMemo (Just recip) _idx memo =
MsgContent $ sformat
( stext
% ", "
% stext
% " said in "
% stext
% " UTC:\n“"
% stext
% "”"
)
(unNickname recip)
(unNickname $ memoSender memo)
(memoTime memo)
(unMsgContent $ memoContent memo)
formatMemo Nothing idx memo =
let n = Maroon #> plain (sformat ("[" % int % "]") idx)
time = Purple #> plain (memoTime memo <> " UTC")
sender = Gray #> "<" <> Green #> plain (unNickname $ memoSender memo) <> Gray #> ">"
content = plain $ unMsgContent $ memoContent memo
in MsgContent $
encode $ n <> " " <> time <> " " <> sender <> " " <> content
-- | Send a memo to its destination, nicely formatted.
sendMemo
:: Nickname -- ^ 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
:: Nickname -- ^ 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
:: Nickname -- ^ Sender nickname
-> Maybe Channel -- ^ Source channel
-> Nickname -- ^ Recipient nickname
-> MsgContent -- ^ Message
-> BotSession ()
sendInstant sender mchan recip content =
case mchan of
Just chan -> sendToChannel chan msg
Nothing -> sendToUser recip msg
where
msg = MsgContent $
unNickname recip <>
", " <>
unNickname sender <>
" says: " <>
unMsgContent content
-- | Report to sender than their memo has been saved.
confirm
:: Nickname -- ^ Sender nickname
-> Maybe Channel -- ^ Whether sent 'Just' in channel or in PM.
-> Nickname -- ^ Recipient nickname
-> BotSession ()
confirm sender (Just chan) recip = do
sendToChannel chan $ MsgContent $ sformat
( stext
% ", your memo for "
% stext
% " has been saved."
)
(unNickname sender)
(unNickname recip)
t <- channelIsTracked chan
unless t $ sendToChannel chan $ MsgContent
"Note that tracking of user joins and quits for this channel is \
\currently disabled in bot settings."
confirm sender Nothing recip =
sendToUser sender $ MsgContent $
"Your memo for " <> unNickname recip <> " has been saved."
-------------------------------------------------------------------------------
-- Operations
-------------------------------------------------------------------------------
-- | Record a new memo for a given user.
addMemo
:: Nickname -- ^ Sender nickname
-> Maybe Channel -- ^ Whether received in 'Just' a channel, or in PM
-> Maybe Channel -- ^ Whether to send in 'Just' a channel, or in PM
-> Nickname -- ^ Recipient nickname
-> MsgContent -- ^ 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 :: Nickname -- ^ 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
:: Nickname -- ^ 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
:: Nickname
-- ^ Sender nickname
-> Maybe Channel
-- ^ Whether sent in 'Just' a channel, or in PM
-> Nickname
-- ^ Recipient nickname
-> Bool
-- ^ Whether to always send memo privately (True) or the same as source
-- (False)
-> MsgContent
-- ^ Memo content
-> BotSession ()
submitMemo sender source recip private content = do
let send = if private then Nothing else source
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 send recip content
saveBotMemos
confirm sender source recip
succ1 <- instantToChan
unless succ1 $ do
succ2 <- instantToUser
unless succ2 keepForLater
-- Send user memos. For a specific joined channel, or for all channels.
reportMemos'
:: Nickname -- ^ User nickname
-> Maybe Channel -- ^ The channel the user joined
-> BotSession ()
reportMemos' recip mchan = do
ms <- getUserMemos recip
let (msChan, msPriv) = partition (isJust . memoSendIn) ms
(msChanSend, msChanOther) <- case mchan of
Just chan ->
let isThis Nothing = False
isThis (Just channel) = channel == chan
in return $ partition (isThis . memoSendIn) msChan
Nothing -> do
chans <- presence recip
let isThese Nothing = False
isThese (Just channel) = channel `elem` chans
return $ partition (isThese . memoSendIn) msChan
unless (null msPriv) $ do
let n = length msPriv
sendToUser recip $ MsgContent $
sformat ("Hello! You have " % int % " private memos:") n
sendMemoList recip 1 msPriv
sendMemoList recip 1 msChanSend
unless (null msPriv && null msChanSend) $ do
setUserMemos recip msChanOther
saveBotMemos
-- | When a user logs in, use this to send them a report of the memos saved for
-- them, if any exist.
reportMemos
:: Nickname -- ^ User nickname
-> Channel -- ^ The channel the user joined triggering the report
-> BotSession ()
reportMemos recip chan = reportMemos' recip (Just chan)
-- | Like 'reportMemos', but reports memos to all channels in which the user is
-- present.
reportMemosAll :: Nickname -> BotSession ()
reportMemosAll recip = reportMemos' recip Nothing
-------------------------------------------------------------------------------
-- Persistence
-------------------------------------------------------------------------------
instance FromJSON Memo where
parseJSON (Object o) =
Memo <$>
o .: "time" <*>
(Nickname <$> o .: "sender") <*>
o .: "recv-in" <*>
o .: "send-in" <*>
(MsgContent <$> o .: "content")
parseJSON v = typeMismatch "Memo" v
instance ToJSON Memo where
toJSON (Memo time sender recvIn sendIn content) = object
[ "time" .= time
, "sender" .= unNickname sender
, "recv-in" .= recvIn
, "send-in" .= sendIn
, "content" .= unMsgContent content
]
loadBotMemos :: IO (M.HashMap Nickname [Memo])
loadBotMemos = do
r <- loadState $ stateFilePath memosFilename (cfgStateRepo configuration)
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 Nickname [Memo] -> IO ())
mkSaveBotMemos =
mkSaveStateChoose
stateSaveInterval
memosFilename
(cfgStateRepo configuration)
"auto commit by funbot"
saveBotMemos :: BotSession ()
saveBotMemos = do
ms <- getStateS bsMemos
save <- askEnvS saveMemos
liftIO $ save ms
|