telegram-bot-0.5.4.0: src/Web/Telegram/Bot/Internal.hs
-- |
-- Module : Web.Telegram.Bot.Handler
-- Copyright : Alexander Krupenkin 2016
-- License : BSD3
--
-- Maintainer : mail@akru.me
-- Stability : experimental
-- Portability : portable
--
-- Telegram Bot runners.
--
module Web.Telegram.Bot.Internal (runBot, storyBot, sendMessageBot, forkBot) where
import Control.Concurrent (forkIO, forkFinally, killThread, ThreadId)
import Control.Monad.Trans.Reader (runReaderT, ask)
import Network.HTTP.Client.TLS (tlsManagerSettings)
import Network.HTTP.Client (newManager, Manager)
import Control.Exception (throwIO)
import Data.IntMap.Strict as I
import Control.Concurrent.Chan
import Control.Concurrent.MVar
import Control.Monad (forever)
import Web.Telegram.Bot.Story
import Web.Telegram.Bot.Types
import Data.Text (Text, pack)
import Data.Map.Strict as M
import Web.Telegram.API.Bot
import Pipes
-- | Try connection with Telegram Bot API
trySelf :: Token -> Manager -> IO ()
trySelf tok mgr = do
me <- getMe tok mgr
case me of
Left e -> throwIO e
Right (Response u) ->
putStrLn $ "Hello! I'm " ++ show (user_first_name u)
-- | Infinity loop for getting updates from API
updateLoop :: BotConfig a
=> (Update -> Bot a ())
-- ^ Update handler
-> Bot a ()
updateLoop handler = go 0
where updates o t = getUpdates t (Just o) Nothing . Just
go offset = do
(manager, config) <- ask
-- Take updates
upd <- liftIO $
updates offset (authToken config) (pollTimeout config) manager
-- Check for errors
case result <$> upd of
Left e -> liftIO (throwIO e)
Right [] -> go offset
Right xs -> do
-- Run handler for any update
mapM_ handler xs
-- Step for the new offset
go (maximum (update_id <$> xs) + 1)
-- | 'Producer' from 'Chan' creator
fromChan :: MonadIO m => Chan a -> Producer a m ()
fromChan c = forever $ liftIO (readChan c) >>= yield
-- | Incoming messages will be sended
toSender :: MonadIO m => (BotMessage -> m ()) -> Consumer BotMessage m ()
toSender sender = forever $ await >>= lift . sender
-- | Chat ID based message splitter
storyHandler :: BotConfig a
=> MVar (IntMap (Chan Message, ThreadId))
-> Map Text (Story a)
-> BotMessage
-> Update
-> Bot a ()
storyHandler chats stories help = go
where go (Update{message =
Just msg@(Message {from = Just user})}) = do
-- Get a chat id
let cid = chat_id (chat msg)
newStory item = modifyMVar_ chats (return . I.insert cid item)
deleteStory = modifyMVar_ chats (return . I.delete cid)
chatMap <- liftIO (readMVar chats)
-- Lookup chat id in the map
case I.lookup cid chatMap of
-- Chat exist => story is run now
Just (chan, tid) -> do
-- Want to cancel it?
case text msg of
Just "/cancel" -> do
liftIO (killThread tid)
sendMessageBot (chat msg) help
_ -> liftIO (writeChan chan msg)
-- Is no runned stories
Nothing ->
case text msg >>= flip M.lookup stories of
-- Unknown story, try to help
Nothing -> sendMessageBot (chat msg) help
-- Story exist
Just story -> do
-- Create chan
chan <- liftIO newChan
-- Story pipeline
let pipeline = fromChan chan
>-> (story (user, chat msg) >>= yield)
>-> toSender (sendMessageBot (chat msg))
-- Story effect
(manager, config) <- ask
let runStory = runReaderT (runEffect pipeline)
(manager, config)
-- Run story in separate thread
tid <- liftIO $ forkFinally runStory
(const deleteStory)
-- Update chanMap
liftIO (newStory (chan, tid))
go _ = return ()
sendMessageBot :: BotConfig a => Chat -> BotMessage -> Bot a ()
sendMessageBot c msg = do
(manager, config) <- ask
liftIO $ send (textChatId c) (authToken config) manager msg
where textChatId = pack . show . chat_id
send cid tok mgr BotTyping =
let r = sendChatActionRequest cid Typing
in sendChatAction tok r mgr >> return ()
send cid tok mgr (BotText t) =
let r = (sendMessageRequest cid t)
{ message_reply_markup = Just replyKeyboardHide
, message_parse_mode = Just Markdown }
in sendMessage tok r mgr >> return ()
send cid tok mgr (BotKeyboard txt btnTexts) =
let btns = fmap keyboardButton <$> btnTexts
keyboard = replyKeyboardMarkup btns
r = (sendMessageRequest cid txt)
{ message_reply_markup = Just keyboard
, message_parse_mode = Just Markdown }
in sendMessage tok r mgr >> return ()
-- | User story handler
storyBot :: (BotConfig a, ToBotMessage help) => help -> Map Text (Story a) -> Bot a ()
storyBot help stories = do
-- Create map from user to it story
chats <- liftIO (newMVar I.empty)
-- Run update loop
updateLoop (storyHandler chats stories $ toMessage help)
-- | Run bot monad
runBot :: BotConfig a => a -> Bot a b -> IO b
runBot config bot = do
-- Init connection manager
manager <- newManager tlsManagerSettings
-- Check connection
trySelf (authToken config) manager
-- Run bot
runReaderT bot (manager, config)
-- Fork bot thread
forkBot :: BotConfig a => Bot a () -> Bot a ThreadId
forkBot bot = ask >>= liftIO . forkIO . runReaderT bot