kurita-0: src/Kurita/Bot.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
module Kurita.Bot where
import qualified Data.Aeson as Aeson
import Text.Megaparsec
import Text.Megaparsec.Char (string)
import Text.Megaparsec.Char.Lexer (decimal)
import Control.Monad.Except
import Data.Foldable (toList)
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Data.SortedList as SL
import Data.Text (Text)
import Data.Bifunctor
import qualified Data.ByteString.Lazy as BSL
import Data.Either
import Data.Int
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import qualified Data.Text.IO as Text
import Control.Concurrent.STM
import Kurita.Bot.Types
import Kurita.Prompt
import Kurita.Prompt.Internal
import Kurita.Protocol
import Network.Linklater
import Data.ByteString (ByteString)
import Data.Word
data KuritaBot c = KuritaBot {
kbotAddPrompt :: Prompt -> IO Int
, kbotPrompts :: IO (Seq Prompt)
, kbotAddTerm :: Text -> Text -> IO ()
, kbotTerms :: IO (Map Text (Set Text))
, kbotBracket :: IO (Bracket c Int64 KuritaGame)
, kbotSetPrompt :: Text -> IO ()
, kbotSlackConfig :: BotConfig
, kbotHash :: ByteString -> Word64
}
loadKBotConfig :: FilePath -> FilePath -> (ByteString -> Word64) -> BotConfig -> IO (Bracket c Int64 KuritaGame) -> (Text -> IO ()) -> IO (KuritaBot c)
loadKBotConfig promptFile termsFile hash botCfg getBracket setPrompt = do
promptResults <- fmap parsePrompt . Text.lines <$> Text.readFile promptFile
promptsVar <- newTMVarIO $ Seq.fromList $ rights promptResults
let
addPrompt p = do
prompts <- atomically $ takeTMVar promptsVar
Text.appendFile promptFile $ (displayPrompt p) <> "\n"
let
prompts' = prompts Seq.|> p
atomically $ putTMVar promptsVar $ prompts'
pure $ Seq.length prompts' - 1
getPrompts = atomically $ readTMVar promptsVar
termResults <- fmap parseTerm . Text.lines <$> Text.readFile termsFile
termsVar <- newTMVarIO $ foldr addToTermMap Map.empty $ rights termResults
let
addTerm k v = do
terms <- atomically $ takeTMVar termsVar
Text.appendFile termsFile $ (displayTerm k v) <> "\n"
atomically $ putTMVar termsVar $ addToTermMap (k, v) terms
getTerms = atomically $ readTMVar termsVar
pure $ KuritaBot {
kbotAddPrompt = addPrompt
, kbotPrompts = getPrompts
, kbotAddTerm = addTerm
, kbotTerms = getTerms
, kbotBracket = getBracket
, kbotSetPrompt = setPrompt
, kbotSlackConfig = botCfg
, kbotHash = hash
}
addToTermMap :: (Ord k, Ord t) => (k, t) -> Map k (Set t) -> Map k (Set t)
addToTermMap (key, val) terms =
Map.alter (Just . maybe (Set.singleton val) (Set.insert val)) key terms
parseTerm :: Text -> Either Text (Text, Text)
parseTerm = first Text.pack . Aeson.eitherDecode . BSL.fromStrict . Text.encodeUtf8
displayTerm :: Text -> Text -> Text
displayTerm k v = Text.decodeUtf8 $ BSL.toStrict $ Aeson.encode (k, v)
runBotEvent :: Ord c => (c -> Text) -> KuritaBot c -> BotEvent -> IO ()
runBotEvent toText kbot (EventMessage message) =
case parseKBotMessage $ messageText message of
Right cmd -> runBotCommand toText kbot cmd
Left pErr ->
when ("!" `Text.isPrefixOf` messageText message)
$ kuritaSend (kbotSlackConfig kbot) $ "Uh oh that's a parse error: " <> Text.pack (show pErr)
runBotEvent _toText _kbot (EventMessageReply _reply) = pure ()
runBotEvent _toText _kbot (ReactionAdded _reaction) = pure ()
runBotEvent _toText _kbot (ReactionRemoved _reaction) = pure ()
runBotCommand :: Ord c => (c -> Text) -> KuritaBot c -> BotCommand -> IO ()
runBotCommand _toText kbot (AddPrompt prompt) = do
promptIndex <- kbotAddPrompt kbot prompt
kuritaSend (kbotSlackConfig kbot) $
"New prompt ID: "
<> (Text.pack $ show promptIndex)
<> " added - \"" <> displayPrompt prompt <> "\""
runBotCommand _toText kbot (List i j) = do
prompts <- kbotPrompts kbot
let
indexSeq = Seq.fromList $ [0..Seq.length prompts]
promptsWithIndex = Seq.zip indexSeq $ displayPrompt <$> prompts
prompts' = Seq.drop i $ Seq.take j promptsWithIndex
kuritaSend (kbotSlackConfig kbot) $
Text.intercalate "\n" $ (\(index,v) -> Text.pack (show index) <> " - " <> v) <$> toList prompts'
runBotCommand toText kbot (CurrentBattle) = do
current <- kbotBracket kbot
case _bCurrent current of
Nothing -> kuritaSend (kbotSlackConfig kbot) "I wasn't able to find a running duel for some reason"
Just game ->
kuritaSend (kbotSlackConfig kbot) $ Text.concat [
Text.intercalate " vs. " (displayVotes <$> SL.fromSortedList (_gameSorted game)), "\n"
, Text.intercalate "\n" (displayCommentary <$> (_kgCommentary $ _gameExtra game))
]
where
displayVotes (voteCount, competitor) =
toText competitor <> " (" <> Text.pack (show voteCount) <> ")"
displayCommentary (time, prompt) =
Text.pack (show time) <> " - " <> prompt
runBotCommand _toText kbot (AddData var val) = do
kbotAddTerm kbot var val
terms <- kbotTerms kbot
maybe
(kuritaSend (kbotSlackConfig kbot) $ "There was an error inserting " <> var <> " -> " <> val)
(\vals -> kuritaSend (kbotSlackConfig kbot) $ var <> " now contains: " <> (Text.pack $ show $ Set.toList vals))
$ Map.lookup var terms
runBotCommand _toText kbot (RemoveData _var _val) =
kuritaSend (kbotSlackConfig kbot) "I'm sorry dave I can't do that"
runBotCommand _toText kbot (SearchPrompts term) = do
prompts <- kbotPrompts kbot
let
indexSeq = Seq.fromList $ [0..Seq.length prompts]
promptsWithIndex = Seq.zip indexSeq $ displayPrompt <$> prompts
filtered = Seq.take 25 $ Seq.filter (\(_, v) -> term `Text.isInfixOf` v) promptsWithIndex
kuritaSend (kbotSlackConfig kbot) $ Text.intercalate "\n" $ (\(i,v) -> (Text.pack (show i) <> " - " <> v)) <$> toList filtered
runBotCommand toText kbot (TestPrompt i) = do
prompts <- kbotPrompts kbot
meta <- kbotTerms kbot
br <- kbotBracket kbot
case renderPrompt toText (kbotHash kbot) meta br <$> Seq.lookup i prompts of
Nothing -> kuritaSend (kbotSlackConfig kbot) $ "Unable to find prompt at index " <> Text.pack (show i)
Just (Left err) -> kuritaSend (kbotSlackConfig kbot) $ "Error rendering prompt: " <> Text.pack err
Just (Right prompt) -> kuritaSend (kbotSlackConfig kbot) prompt
runBotCommand toText kbot (SetPrompt i) = do
prompts <- kbotPrompts kbot
meta <- kbotTerms kbot
br <- kbotBracket kbot
case renderPrompt toText (kbotHash kbot) meta br <$> Seq.lookup i prompts of
Nothing -> kuritaSend (kbotSlackConfig kbot) $ "Unable to find prompt at index " <> Text.pack (show i)
Just (Left err) -> kuritaSend (kbotSlackConfig kbot) $ "Error rendering prompt: " <> Text.pack err
Just (Right prompt) -> do
kbotSetPrompt kbot prompt
kuritaSend (kbotSlackConfig kbot) $ "Prompt set to \"" <> prompt <> "\""
runBotCommand toText kbot (SetLive pr) = do
meta <- kbotTerms kbot
br <- kbotBracket kbot
case renderPrompt toText (kbotHash kbot) meta br pr of
Left err -> kuritaSend (kbotSlackConfig kbot) $ "Error rendering prompt: " <> Text.pack err
Right prompt -> do
kbotSetPrompt kbot prompt
kuritaSend (kbotSlackConfig kbot) $ "Prompt set to \"" <> prompt <> "\""
runBotCommand toText kbot NextUp = do
br <- kbotBracket kbot
case _bUpcoming br of
[a, b]:_ -> do
kuritaSend (kbotSlackConfig kbot) $ "Up next, "<>(toText a)<>" vs. "<>(toText b)<>", I can't wait!"
_ -> pure ()
runBotCommand toText kbot LastRoundWinners = do
br <- kbotBracket kbot
case (map (map (toText . last . map snd . toList . _gameSorted)) . _bPlayed $ br) of
_:lr:_ -> kuritaSend (kbotSlackConfig kbot) $ "The winners of the last round were: " <>
(Text.intercalate ", " $ map (Text.pack . show) lr)
_ -> pure ()
parseKBotMessage :: Text -> Either Text BotCommand
parseKBotMessage t =
first (Text.pack . show) $ runParser parsers "" t
where
parsers =
parseAdd
<|> parseLive
<|> parseList
<|> parseCurrentBattle
<|> parseAddData
<|> parseSearch
<|> parseTestPrompt
<|> parseSet
<|> parseNext
<|> parseWinners
parseAdd = do
_ <- try $ string "!add "
prompt <- promptParser
pure $ AddPrompt prompt
parseLive = do
_ <- try $ string "!live "
prompt <- promptParser
pure $ SetLive prompt
parseList = do
_ <- try $ string "!list "
i <- decimal
_ <- string " "
j <- decimal
pure $ List i j
parseCurrentBattle = do
_ <- try $ string "!battle"
pure CurrentBattle
parseAddData = do
_ <- try $ string "!meta "
var <- takeWhile1P Nothing (\c -> c /= ' ')
_ <- string " -> "
val <- takeRest
pure $ AddData var val
parseSearch = do
_ <- try $ string "!search "
val <- takeRest
pure $ SearchPrompts val
parseTestPrompt = do
_ <- try $ string "!test "
i <- decimal
pure $ TestPrompt i
parseSet = do
_ <- try $ string "!set "
i <- decimal
pure $ SetPrompt i
parseNext = do
_ <- try $ string "!next"
pure $ NextUp
parseWinners = do
_ <- try $ string "!lastwinners"
pure $ LastRoundWinners
kuritaSend :: BotConfig -> Text -> IO ()
kuritaSend botCfg msg = do
err <- runExceptT $ say (SimpleMessage (EmojiIcon ":robot:") "kurita_bot" ch msg) cfg
case err of
Left e -> print e
_ -> pure ()
where
(ch, cfg) = botConfigHook botCfg