replicant-0.1.0.0: src/Replicant/Adapters/Slack.hs
module Replicant.Adapters.Slack
( adapter
) where
import Prelude hiding (takeWhile)
import Replicant.Base
import qualified Replicant.Adapters.Slack.Api as S
import qualified Replicant.Adapters.Slack.Types as S
import Control.Monad.Trans.Control (control)
import Data.Aeson (eitherDecode)
import Data.Attoparsec.Text
import qualified Data.ByteString.Lazy as LBS
import qualified Data.List as L
import Data.Maybe (isJust)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Database.Redis.Namespace as R
import Network.Socket (withSocketsDo)
import qualified Network.WebSockets as WS
import qualified Wuss as WS (runSecureClient)
import Replicant.Bot (botDirectives, redis)
import Replicant.Plugin
import Replicant.Plugins.Base (whitespace)
import qualified Replicant.Logging as Log
adapter :: Replicant e m => Adapter m
adapter = Adapter
{ bootBot = _bootBot
, sendToUserId = _sendToUserId
, sendToRoom = _sendToRoom
, parseCommand = parseSlackCommand
, getRoomByName = getSlackRoomByName
, getRoomMembers = getSlackRoomMembers
}
_bootBot :: Replicant e m => BotSpec m -> m ()
_bootBot spec@BotSpec{..} = do
url <- S.getWebsocket botRecord
let (domain, path) = T.breakOn "/" . T.drop 6 $ url
control $ \run -> withSocketsDo $
WS.runSecureClient (T.unpack domain) 443 (T.unpack path) $ \conn -> do
WS.forkPingThread conn 15
forever $ WS.receiveData conn >>= run . dispatchEvents spec
dispatchEvents :: Replicant e m => BotSpec m -> LBS.ByteString -> m ()
dispatchEvents spec msg = case eitherDecode msg of
Left err -> liftIO . T.putStrLn $ "Failed to parse event: " <> T.pack err
Right event -> withMessages (botDirectives spec) event
toMessage :: S.Message -> Message
toMessage sm =
let r = S.messageChannel sm
u = maybe "" id $ S.messageUser sm
in Message
{ messageRoom = Room { roomId = r, roomName = r }
, messageUser = User { userId = u, userName = u }
, messageText = S.messageBody sm
, messageDirect = isDirect sm
}
withMessages :: Monad m => (Message -> m ()) -> S.Event -> m ()
withMessages f (S.MessageEvent m) = f $ toMessage m
withMessages _ _ = return ()
commandParser :: Parser (Text, Text)
commandParser = do
whitespace
_ <- string "<@"
userId <- takeWhile $ \c -> c /= '>'
_ <- char '>'
whitespace
_ <- optional ":"
whitespace
msg <- takeWhile $ const True
return (userId, msg)
parseSlackCommand :: Bot -> Message -> Maybe Text
parseSlackCommand bot Message{..} =
case parseOnly commandParser messageText of
Right (_id, command) ->
if _id == botUserId bot
then Just command
else Nothing
_ ->
if messageDirect
then Just messageText
else Nothing
isDirect :: S.Message -> Bool
isDirect S.Message{..} = channelIsDirect && isFromAHuman
where
channelIsDirect = T.isPrefixOf "D" messageChannel
isFromAHuman = isJust messageUser -- TODO: improve?
_sendToUserId :: Replicant e m => Bot -> UserId -> Text -> m ()
_sendToUserId bot userId text =
getDmRoomId bot userId >>= \case
Nothing -> return () -- TODO??
Just im -> S.sendMessage bot im text
_sendToRoom :: Replicant e m => Bot -> Room -> Text -> m ()
_sendToRoom bot Room{..} = S.sendMessage bot roomId
-- TODO: support to- / from- json and cache room name => room lookup
cached :: Replicant e m => Bot -> Text -> Text -> m (Maybe Text) -> m (Maybe Text)
cached bot collection key q = do
let rk = encodeUtf8 $ "cache:" <> collection <> ":" <> key
found <- redis $ R.get rk
case found of
Just val -> do
Log.cacheHit (botName bot) collection key
return . Just $ decodeUtf8 val
Nothing -> q >>= \case
Nothing -> return Nothing
Just val -> do
redis . R.set rk $ encodeUtf8 val
return $ Just val
getDmRoomId :: Replicant e m => Bot -> UserId -> m (Maybe RoomId)
getDmRoomId bot userId = cached bot "im-ids" userId $ do
rooms <- S.getImList bot
return $ snd <$> L.find (\(u,_) -> u == userId) rooms
channelToRoom :: S.Channel -> Room
channelToRoom ch = Room
{ roomId = S.channelId ch
, roomName = S.channelName ch
}
getSlackRoomByName :: Replicant e m => Bot -> Text -> m (Maybe Room)
getSlackRoomByName bot text = do
channels <- S.getChannels bot
return $ channelToRoom <$> L.find (\c -> text == S.channelName c) channels
getSlackRoomMembers :: Replicant e m => Bot -> Room -> m [User]
getSlackRoomMembers bot Room{..} = do
members <- S.getChannelMembers bot roomId
return $ map memberToUser members
memberToUser :: S.User -> User
memberToUser su = User
{ userId = S.userId su
, userName = S.userName su
}