packages feed

danibot-0.2.0.0: lib/Network/Danibot/Slack.hs

{-# LANGUAGE OverloadedStrings #-}

module Network.Danibot.Slack (
      worker
    , makeChatState
    , eventFold
    ) where

import Data.Text (Text)
import Data.Char
import qualified Data.Attoparsec.Text as Atto

import Control.Lens
import Control.Exception
import Control.Monad
import Streaming (Stream)
import Streaming.Prelude (Of)
import qualified Streaming.Prelude as Streaming
import Control.Foldl (FoldM(..))
import Control.Concurrent
import Control.Concurrent.STM (atomically)
import Control.Concurrent.STM.TVar
import Control.Concurrent.STM.TChan

import Network.Danibot.Slack.Types 

{-| Takes a request handling function, returns a channel that takes
    (requests,post-processing actions) along with a worker action that drains the
    channel and forks a thread for each request.	

-}
worker :: (Text -> IO Text) -- ^ A request handler function.
       -> IO (TChan (Text,Text -> IO ()), IO ()) -- ^ Action that creates the request channel and worker action.
worker handler = do
    chan <- atomically newTChan  
    let go = forever (do (task,post) <- atomically (readTChan chan)
                         forkIO (do
                             result <- handler task
                             post result))
    pure (chan,go)                            

{-| Builds a Fold that consumes messages coming from the Slack RTM connection.		

-}
eventFold :: TChan (Text,Text -> IO ()) 
          -> ChatState
          -> FoldM IO Event ()
eventFold pool cs =
    FoldM reactToEvent (pure InitialState) (\_ -> pure ())
    where
    reactToEvent protocolState event =
        case (protocolState,event) of
            (InitialState,HelloEvent) -> 
                pure NormalState
            (InitialState,_) -> 
                throwIO (userError "wrong start")
            (NormalState,MessageEvent (Message _ (Right (UserMessage channel_ user_ text_ NotMe)))) -> do
                currentcs <- atomically (readTVar (chatVar cs)) 
                let whoami = identity (self currentcs) 
                    send = sendMessageToChannel cs channel_
                if has (ims.ix channel_) currentcs 
                  then -- IM message?
                    case isDirectedTo text_ of
                        Just (target,text') | user_ /= whoami && target == whoami -> do
                            atomically (writeTChan pool 
                                                   (text',send))
                        Nothing             | user_ /= whoami -> do
                            atomically (writeTChan pool 
                                                   (text_,send))
                        _ -> pure ()
                  else -- message in general channel? 
                    case isDirectedTo text_ of
                        Just (target,text') | user_ /= whoami && target == whoami -> do
                            atomically (writeTChan pool 
                                                   (text',send . addressTo user_))
                        _ -> pure ()
                pure NormalState
            _ -> do
                pure NormalState

data ChatState = ChatState
    {
       chatVar :: TVar Chat 
    ,  nextMsgIdVar :: TVar Integer 
    ,  outboundChan :: TChan OutboundMessage 
    } 

makeChatState :: Chat -> IO (ChatState, Stream (Of OutboundMessage) IO ())
makeChatState c = do
    chatVar' <- atomically (newTVar c)
    nextMsgIdVar' <- atomically (newTVar 0)
    outboundChan' <- atomically newTChan  
    pure (ChatState chatVar' nextMsgIdVar' outboundChan'
         ,Streaming.repeatM (atomically (readTChan outboundChan')))

data ProtocolState = 
      InitialState
    | NormalState

isDirectedTo :: Text -> Maybe (Text,Text)
isDirectedTo txt = case Atto.parse mentionParser txt of
        Atto.Done rest userId_ -> Just (userId_,rest)
        _                     -> Nothing
    where
    mentionParser = 
        Atto.string "<@" 
        *>  
        Atto.takeWhile isAlphaNum 
        <* 
        Atto.string ">:"
        <* 
        Atto.skipSpace

addressTo :: Text -> Text -> Text
addressTo uid msg = mconcat ["<@",uid,">: ",msg]

sendMessageToChannel :: ChatState -> Text -> Text -> IO () 
sendMessageToChannel cstate channelId_ msg = do
    atomically (do
         i <- readTVar (nextMsgIdVar cstate)
         modifyTVar' (nextMsgIdVar cstate) succ 
         writeTChan (outboundChan cstate) 
                    (OutboundMessage i channelId_ msg))