packages feed

voicebase-0.2.0.0: lib/Voicebase/V2Beta/Client.hs

-- | Voicebase V2Beta API
--
-- Simpler, faster, more productive! This new API has a 'runVB'for
-- connection reuse.
--
-- Comes with command line submitTranscriptionr executable: "voicebase"
--
-- example:
--
-- @
-- import Voicebase.V2Beta.Client
--
-- main = do
--   runVB (defaultConfig token) (transcribe $ Bytes mempty "audio/wav" mempty)
--   >>= print
-- @
--
-- > voicebase: HttpExceptionRequest Request {
-- >   host                 = "apis.voicebase.com"
-- >   port                 = 443
-- >   secure               = True
-- >   requestHeaders       = [("Content-Type","multipart/form-data; boundary=----We
-- >   path                 = "/v2-beta/media/"
-- >   queryString          = ""
-- >   method               = "POST"
-- >   proxy                = Nothing
-- >   rawBody              = False
-- >   redirectCount        = 10
-- >   responseTimeout      = ResponseTimeoutDefault
-- >   requestVersion       = HTTP/1.1
-- > }
-- >  (StatusCodeException (Response {responseStatus = Status {statusCode = 401, sta070EFD2A2E0D5A2D;path=/;Secure;HttpOnly"),("Set-Cookie","SERVERID=; Expires=Thuntials","true"),("Access-Control-Allow-Methods","DELETE, GET, HEAD, OPTIONS, PO "{\"status\":401,\"errors\":{\"error\":\"The Authorization header you provided
-- >
-- >
--
-- <http://voicebase.readthedocs.io/en/v2-beta/>

{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE RecordWildCards            #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE ViewPatterns               #-}


module Voicebase.V2Beta.Client
    (
      -- * Running
      runVB,

      -- * Actions
      transcribe,
      transcribeAndFetchMedia,
      submitTranscription,
      fetchTranscript,
      fetchMedia,

      -- * General helpers
      vbPost,
      vbGet,

      -- * Data
      defaultConfig,
      withRedaction,
      BearerToken,
      VBUpload(..),
      VBConfig(..),
  Configuration(..), channels, language, detections, ChannelSpeakers(..), left,
  right, Speaker(..), Detection(..), Language(..), JSONInvertible(..),
  ) where

import           Control.Applicative
import           Control.Lens
import           Control.Lens.TH
import           Control.Monad
import           Control.Monad.IO.Class
import           Control.Monad.Reader
import           Data.Coerce
import           Data.String

import           Data.Bifunctor
import           Data.Maybe
import           Data.Monoid
import qualified Data.Text                      as Text
import           Data.Text.Encoding             as TE

import qualified Data.ByteString.Char8          as BS
import qualified Data.ByteString.Lazy           as LBS
import qualified Data.ByteString.Lazy.Char8     as C8LBS
import           Data.Text                      (Text, unpack)

import           Data.Aeson                     (FromJSON (parseJSON),
                                                 ToJSON (..), Value,
                                                 eitherDecode, encode, object,
                                                 (.=))
import           Data.Aeson.Lens
import           Data.Aeson.Roundtrip
import           Data.Aeson.Types               (parseEither)
import           Network.HTTP.Client            (defaultManagerSettings,
                                                 managerResponseTimeout,
                                                 responseTimeoutMicro)
import           Network.HTTP.Client.OpenSSL
import           Network.Mime                   (MimeType)
import           Network.Wreq                   (asJSON, defaults,
                                                 partFileSource, partLBS)
import           Network.Wreq.Lens
import           Network.Wreq.Session
import           Network.Wreq.Types             (Postable (..))
import           OpenSSL.Session                (context)

import           GHC.Generics

import           System.IO                      (fixIO)

import           Voicebase.V2Beta.Configuration

-- | get your bearer token at http://voicebase.readthedocs.io/en/v2-beta/how-to-guides/hello-world.html#token
newtype BearerToken = BearerToken BS.ByteString
  deriving IsString

v2beta :: String
v2beta = "https://apis.voicebase.com/v2-beta/media"

-- | Data for our 'VB' ReaderT
-- | Comes with lenses
data VBConfig = VBConfig {
    _apiConfig :: Configuration
  , _token     :: BearerToken
  , _baseURI   :: String
  , _session   :: Session
}

makeLenses ''VBConfig

defaultConfig :: BearerToken -> Session -> VBConfig
defaultConfig _token _session =
    VBConfig {
        _apiConfig = Configuration {
            _channels = Just ChannelSpeakers {
                _left = Speaker "Agent"
              , _right = Speaker "Caller"
            }
          , _language = EnglishAus
          , _detections = [RedactingPCI]
        }
      , _baseURI = v2beta
      , ..
    }

-- | Turn on PCI redaction, you may want to fetch the media with 'fetchMedia
withRedaction :: VBConfig -> VBConfig
withRedaction = apiConfig . detections <>~ [RedactingPCI]

type VB a = ReaderT VBConfig IO a

-- | Run a VB action with a tls connection.
runVB :: (Session -> VBConfig) -> VB a -> IO a
runVB ck k =
  -- Creates one TLS connection for performance, without cookie tracking.
  runReaderT k =<< (ck <$> liftIO newAPISession)

authOptions :: VB Options
authOptions = do
  token <- asks _token
  pure $ defaults
    &  manager
    .~ Left (opensslManagerSettings context)
    &  manager
    .~ Left (defaultManagerSettings { managerResponseTimeout = responseTimeoutMicro 10000 })
    &  header "Authorization"
    .~ ["Bearer " <> coerce token]

partConfiguration :: Configuration -> Part
partConfiguration config =
  partLBS "configuration" $ either error encode $ runBuilder syntax config

data VBUpload = Bytes { audio :: LBS.ByteString , mimetype :: MimeType, filename :: FilePath}
              | File { path :: FilePath }

-- | Put an API config and voicebase upload config together
uploadParts
  :: VBUpload
  -> VB [Part]
uploadParts upload = do
    config <- asks (partConfiguration . _apiConfig)
    pure [toMediaPart upload, config]

toMediaPart :: VBUpload -> Part
toMediaPart Bytes{..} = partLBS "media" audio & partContentType ?~ mimetype & partFileName ?~ filename
toMediaPart File{..} = partFileSource "media" path

-- @ transcribe upload = submitTranscription upload >>= waitFinish >>= fetchTranscript @
-- What you might want to do for a polling workflow. If interrupted, just upload
-- again.
transcribe :: VBUpload -> VB Value
transcribe upload = submitTranscription upload >>= waitFinish >>= fetchTranscript

-- | Same as 'transcribe' but also sequentially fetch the media (which may be different if PCI redacted)
transcribeAndFetchMedia :: VBUpload -> VB (Value, C8LBS.ByteString)
transcribeAndFetchMedia upload = do
  mid <- submitTranscription upload >>= waitFinish
  (,) <$> fetchTranscript mid <*> fetchMedia mid

-- | @ submitTranscription upload >>= waitFinish >>= fetchTranscript @
--
-- Uploads a file and waits for completion of the job by polling
submitTranscription :: VBUpload -> VB MediaID
submitTranscription upload = do
  parts <- uploadParts upload
  auth <- authOptions

  r <- asJSON =<< vbPost mempty parts
  case r ^? responseBody . key @Value "mediaId" . _String of
    Nothing -> error $ "expected mediaId in voicebase response: " <> show r
    Just (MediaID -> mid) -> pure mid

-- | Keep polling media until state is finished or failed
-- if it failed, explode with an error.
--
-- This error handling is terrible, but the API doesn't document errors so...
waitFinish :: MediaID -> VB MediaID
waitFinish mid = do
  st <- getProgress mid
  case st of
    "finished" -> return mid
    "failed" -> error $ "VoiceBase transcription failed for media id: " <> show mid
    _ -> waitFinish mid

newtype MediaID = MediaID { unMediaID :: Text }
  deriving Show

makeURL :: String -> VB String
makeURL p = (\b -> b <> "/" <> p) <$> asks _baseURI

vbGet :: String -> VB (Response LBS.ByteString)
vbGet = wrapwreq getWith

wrapwreq k path = do
  o <- authOptions
  s <- asks _session
  u <- makeURL path
  liftIO $ k o s u

vbPost :: Postable a => String -> a -> VB (Response LBS.ByteString)
vbPost p a = wrapwreq (\o s u -> postWith o s u a) p

-- | Fetch the current state of the media
getProgress :: MediaID -> VB Text
getProgress (unpack . unMediaID -> mid) = do
    aopts <- authOptions
    r <- asJSON =<< vbGet (mid <> "/progress")
    case r ^? responseBody . key @Value "status" . _String of
      Just st -> return st
      Nothing -> error "unexpected voicebase response"

-- | Fetch e.g. redacted media
fetchMedia :: MediaID -> VB LBS.ByteString
fetchMedia (unpack . unMediaID -> mid) = do
    r <- asJSON =<< vbGet (mid <> "/streams")
    case r ^? responseBody . key @Value "streams" . key "original" . _String of
      Just uri -> do
        s <- asks _session
        view responseBody <$> liftIO (get s $ Text.unpack uri)
      Nothing -> error "Coudn't find a link to meda in :mid/streams response"
--
-- | Fetch a transcript. You should be sure the transcript is ready first weth
-- e.g. waitFinish.
fetchTranscript :: MediaID -> VB Value
fetchTranscript (unpack . unMediaID -> mid) = do
  r <- asJSON =<< vbGet mid
  return $ r ^. responseBody