line-2.1.0.2: src/Line/Messaging/Webhook.hs
{-|
This module provides webhook handlers both general and WAI-specific.
-}
module Line.Messaging.Webhook (
-- * Types
-- | Re-exported for convenience.
module Line.Messaging.Webhook.Types,
-- * Basic webhook
webhook,
-- * Webhook as a WAI application
webhookApp,
defaultOnFailure,
-- * Webhook as a Scotty action
webhookAction,
defaultOnFailure',
) where
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT, throwE, runExceptT)
import Data.Aeson (decode')
import Data.ByteString.Builder (string8)
import Data.Text.Encoding (encodeUtf8)
import Line.Messaging.Webhook.Types
import Line.Messaging.Webhook.Validation (validateSignature)
import Network.HTTP.Types.Status
import Network.Wai
import qualified Data.Text.Lazy as TL
import qualified Data.ByteString.Lazy as BL
import qualified Web.Scotty as Scotty
-- | A basic webhook function. It validates a request with a channel secret,
-- signature and body, and parses the body into a list of webhook events.
--
-- To handle failures, the result is in the form of @'ExceptT' 'WebhookFailure'@.
webhook :: (Monad m)
=> ChannelSecret
-> BL.ByteString -- ^ Request body
-> Signature -- ^ Auth signature in @X-Line-Signature@ header
-> ExceptT WebhookFailure m [Event]
webhook secret body sig = do
if not $ validateSignature secret body sig
then throwE SignatureVerificationFailed
else do
case decode' body of
Nothing -> throwE MessageDecodeFailed
Just (Body events) -> return events
getSigFromWaiReq :: Request -> Maybe Signature
getSigFromWaiReq = lookup "X-Line-Signature" . requestHeaders
-- | A webhook handler for WAI. It uses 'webhook' internally and returns a WAI
-- 'Application'.
--
-- An example webhook server using WAI will be like below:
--
-- @
-- app :: Application
-- app req f = case pathInfo req of
-- "webhook" : _ -> do
-- secret <- getChannelSecret
-- webhookApp secret handler defaultOnFailure req f
-- _ -> undefined
--
-- handler :: [Event] -> IO ()
-- handler events = forM_ events handleEvent
--
-- handleEvent :: Event -> IO ()
-- handleEvent (MessageEvent event) = undefined -- handle a message event
-- handleEvent _ = return ()
-- @
webhookApp :: ChannelSecret -- ^ Channel secret
-> ([Event] -> IO ()) -- ^ Event handler
-> (WebhookFailure -> Application)
-- ^ Error handler. Just to return 400 for failures, use 'defaultOnFailure'.
-> Application
webhookApp secret handler failHandler req f = do
body <- lazyRequestBody req
let maybeSig = getSigFromWaiReq req
case maybeSig of
Nothing -> failHandler SignatureVerificationFailed req f
Just sig -> do
result <- runExceptT $ webhook secret body sig
case result of
Right events -> handler events >> (f $ responseBuilder status200 [] "")
Left exception -> failHandler exception req f
-- | A basic error handler to be used with 'webhookApp'. It returns 400 Bad
-- Request with the 'WebhookFailure' code for its body.
defaultOnFailure :: WebhookFailure -> Application
defaultOnFailure failure _ f = f .
responseBuilder status400 [] . string8 . show $ failure
-- | A webhook handler for Scotty. It uses 'webhook' internally and returns a
-- Scotty action of type 'ActionM' @()@
--
-- An example webhook server using WAI will be like below:
--
-- @
-- main :: IO ()
-- main = scotty 3000 $ do
-- post "/webhook" $ webhookAction handler defaultOnFailure'
--
-- handler :: [Event] -> IO ()
-- handler events = forM_ events handleEvent
--
-- handleEvent :: Event -> IO ()
-- handleEvent (MessageEvent event) = undefined -- handle a message event
-- handleEvent _ = return ()
-- @
webhookAction :: ChannelSecret -- ^ Channel secret
-> ([Event] -> IO ()) -- ^ Event handler
-> (WebhookFailure -> Scotty.ActionM ())
-- ^ Error handler. Just to return 400 for failures, use 'defaultOnFailure''.
-> Scotty.ActionM ()
webhookAction secret handler failHandler = do
body <- Scotty.body
maybeSig <- Scotty.header "X-Line-Signature"
case maybeSig of
Nothing -> failHandler SignatureVerificationFailed
Just sig -> do
result <- runExceptT $ webhook secret body (encodeUtf8 $ TL.toStrict sig)
case result of
Right events -> (liftIO $ handler events) >> Scotty.text ""
Left exception -> failHandler exception
-- | A basic error handler to be used with 'webhookAction'. It returns 400 Bad
-- Request with the 'WebhookFailure' code for its body. It has the same purpose
-- as 'defaultOnFailure', except that it is for Scotty.
defaultOnFailure' :: WebhookFailure -> Scotty.ActionM ()
defaultOnFailure' err = do
Scotty.status status400
Scotty.text . TL.pack . show $ err