flowdock-api-0.1.0.0: src/Flowdock/Push.hs
-- |
-- Module : Flowdock.Push
-- License : MIT License
-- Maintainer : Gabriel McArthur <gabriel.mcarthur@gmail.com>
-- Stability : experimental
-- Portability : portable
--
-- This is a module for interacting with the Push API of Flowdock.
--
-- Example usage, by sending a chat message to another user might be,
-- in the IO monad:
--
-- > let chat = Chat { chatContent = "Hey, how are you?",
-- > , externalUserName = "ian"
-- > , chatTags = Nothing }
-- > let auth = PushApiToken "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-- > pushAPI conn $ do
-- > push chat
--
-- If you happen to be in a monad that implements MonadIO, MonadLogger,
-- and MonadBaseControl IO (as in Yesod), you can also use
-- `runPushApiT` or `runPushApiLogging`.
module Flowdock.Push
(
-- * Types of messages
PushEvent(..)
-- * Authentication
, PushApiToken(..)
, mkApiToken
-- * Monad Transformers
, PushAPI
, pushAPI
-- * Sending messages
, push
) where
import Control.Applicative
import Control.Monad
import Control.Monad.CatchIO
import Control.Monad.IO.Class (liftIO, MonadIO)
import Control.Monad.Trans.Reader
import Data.Aeson
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import Data.Char (isHexDigit)
import Data.Maybe
import Data.Text (Text,unpack)
import qualified Data.Text as Text
import Data.Text.Encoding (decodeUtf8)
import Network.Http.Client
import OpenSSL (withOpenSSL)
import System.IO.Streams (InputStream)
import qualified System.IO.Streams as Streams
import Flowdock.Internal
-- -----------------------------------------------------------------------------
-- Push Events
data PushEvent
-- | A kind of message that is sent to a team inbox.
= TeamInbox
-- Required
{ source :: !Text -- ^ Human-readable name of the application that uses Flowdock
, fromAddress :: !Text -- ^ Email address of the message sender
, subject :: !Text -- ^ Subject line of the message
, inboxContent :: !Text -- ^ HTML-like body content
-- Optional
, fromName :: !(Maybe Text) -- ^ The name of the message sender.
, replyTo :: !(Maybe Text) -- ^ The email address for replies to the message
, project :: !(Maybe Text) -- ^ Project for message categorization
, inboxTags :: !(Maybe [Text]) -- ^ A list of tags
, link :: !(Maybe Text) -- ^ Link associated with the message
}
-- | A kind of message that is sent to another user.
| Chat
{ chatContent :: !Text -- ^ Content of message.
, chatUserName :: !Text -- ^ The external user.
, chatTags :: !(Maybe [Text]) -- ^ Optional tags to add to the message.
} deriving (Show, Eq)
instance ToJSON PushEvent where
toJSON TeamInbox{..} =
object $ catMaybes [ Just $ "source" .= source
, Just $ "from_address" .= fromAddress
, Just $ "subject" .= subject
, Just $ "content" .= inboxContent
, ("from_name" .=) <$> fromName
, ("reply_to" .=) <$> replyTo
, ("content" .=) <$> project
, ("tags" .=) <$> inboxTags
, ("link" .=) <$> link
]
toJSON Chat{..} =
object $ catMaybes [ Just $ "content" .= chatContent
, Just $ "external_user_name" .= chatUserName
, ("tags" .=) <$> chatTags
]
-- To get the relative URLs from the types of messages
class RelativeUrl a where
getRelativeUrl :: a -> Text
instance RelativeUrl PushEvent where
getRelativeUrl TeamInbox{..} = "/v1/messages/team_inbox/"
getRelativeUrl Chat{..} = "/v1/messages/chat/"
-- -----------------------------------------------------------------------------
-- API Tokens
-- | Flowdock tokens are 32 character hexadecimal digits.
newtype PushApiToken = PushApiToken {pushApiToken :: Text}
deriving (Eq, Show)
-- | Parses some text into a possible Api token
mkApiToken :: Text -> Maybe PushApiToken
mkApiToken txt =
if rightLength && allHex
then Just $ PushApiToken txt
else Nothing
where
allHex = Text.all isHexDigit txt
rightLength = Text.length txt == 32
-- -----------------------------------------------------------------------------
-- PushApi Requests
data Env = Env
{ apiToken :: !PushApiToken -- ^ The API token to access the server
}
data Error
= Internal String
| External Text
instance Show Error where
show (Internal str) = "Inernal Error: " ++ str
show (External txt) = "Exteranal Error: " ++ unpack txt
-- -----------------------------------------------------------------------------
-- PushApi Requests
newtype PushAPI a = PushAPI { unwrap :: ReaderT Env IO a }
deriving (Applicative, Functor, Monad, MonadIO, MonadCatchIO, MonadPlus)
pushAPI :: MonadIO m => PushApiToken -> PushAPI a => m a
pushAPI token api = liftIO $ runReaderT (unwrap api) conn
where
conn = Env token
push :: PushEvent
-> PushAPI (Maybe Error)
push event = PushAPI $ do
Env{..} <- ask
liftIO $ withOpenSSL $
bracket (open "api.flowdock.com") closeConnection (request apiToken)
where
open host = do
ctx <- baselineContextSSL
openConnectionSSL ctx host 443
request apiToken conn = do
enc <- Streams.fromLazyByteString $ encode event
req <- buildRequest $ do
http POST $ createUrl [getRelativeUrl event, pushApiToken apiToken] []
setAccept "application/json"
setContentType "application/json"
sendRequest conn req $ inputStreamBody enc
receiveResponse conn response
-- -----------------------------------------------------------------------------
-- Internal
response :: Response -> InputStream ByteString -> IO (Maybe Error)
response resp body =
case getStatusCode resp of
200 -> return Nothing
201 -> return Nothing
400 -> failure
500 -> failure
n -> return $ Just $ Internal $ "Flowdock returned a bad status code: " ++ show n
where
failure = do
body' <- BS.concat <$> Streams.toList body
return $ Just $ External $ decodeUtf8 body'