-- | -- 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