pusher-http-haskell 1.2.0.1 → 1.3.0.0
raw patch · 11 files changed
+418/−440 lines, 11 filesdep ~base
Dependency ranges changed: base
Files
- pusher-http-haskell.cabal +78/−78
- src/Network/Pusher.hs +32/−53
- src/Network/Pusher/Data.hs +84/−34
- src/Network/Pusher/Error.hs +4/−2
- src/Network/Pusher/Internal.hs +53/−89
- src/Network/Pusher/Internal/Auth.hs +38/−52
- src/Network/Pusher/Internal/HTTP.hs +24/−30
- src/Network/Pusher/Internal/Util.hs +11/−3
- src/Network/Pusher/Protocol.hs +32/−24
- test/Auth.hs +32/−34
- test/Protocol.hs +30/−41
pusher-http-haskell.cabal view
@@ -1,82 +1,82 @@-name: pusher-http-haskell-version: 1.2.0.1-cabal-version: >=1.18-build-type: Simple-license: MIT-license-file: LICENSE-copyright: (c) Will Sewell, 2016-author: Will Sewell-maintainer: me@willsewell.com-stability: experimental-homepage: https://github.com/pusher-community/pusher-http-haskell-bug-reports: https://github.com/pusher-community/pusher-http-haskell/issues-synopsis: Haskell client library for the Pusher HTTP API-description: Functions that correspond to endpoints of the Pusher HTTP- API. Messages can be triggered, and information about the- channel can be queried. Additionally there are functions- for authenticating users of private and presence channels.-category: Network-tested-with: GHC >= 7.10.2+name: pusher-http-haskell+version: 1.3.0.0+cabal-version: >=1.18+build-type: Simple+license: MIT+license-file: LICENSE+copyright: (c) Will Sewell, 2016+maintainer: me@willsewell.com+stability: experimental+homepage: https://github.com/pusher-community/pusher-http-haskell+bug-reports: https://github.com/pusher-community/pusher-http-haskell/issues+synopsis: Haskell client library for the Pusher HTTP API+description:+ Functions that correspond to endpoints of the Pusher HTTP+ API. Messages can be triggered, and information about the+ channel can be queried. Additionally there are functions+ for authenticating users of private and presence channels.+category: Network+author: Will Sewell+tested-with: GHC >=7.10.2 library- exposed-modules: Network.Pusher,- Network.Pusher.Internal,- Network.Pusher.Internal.Auth,- Network.Pusher.Internal.HTTP,- Network.Pusher.Protocol- other-modules: Network.Pusher.Data,- Network.Pusher.Error,- Network.Pusher.Internal.Util- default-language: Haskell2010- hs-source-dirs: src- default-extensions: OverloadedStrings- build-depends: aeson >=0.8 && <1.3,- base >=4.7 && <4.10,- bytestring ==0.10.*,- base16-bytestring ==0.1.*,- cryptohash ==0.11.*,- hashable ==1.2.*,- http-client >=0.4 && <0.6,- http-types >=0.8 && <0.10,- text ==1.2.*,- time >=1.5 && <1.7,- transformers >=0.4 && <0.6,- unordered-containers ==0.2.*- ghc-options: -Wall- if impl(ghc >= 8.0.0)- ghc-options: -Wcompat- -Whi-shadowing- -Wmissing-signatures- else- ghc-options: -fwarn-hi-shadowing- -fwarn-missing-signatures- -fwarn-tabs + if impl(ghc >=8.0.0)+ ghc-options: -Wcompat -Whi-shadowing -Wmissing-signatures+ else+ ghc-options: -fwarn-hi-shadowing -fwarn-missing-signatures -fwarn-tabs+ exposed-modules:+ Network.Pusher+ Network.Pusher.Internal+ Network.Pusher.Internal.Auth+ Network.Pusher.Internal.HTTP+ Network.Pusher.Protocol+ build-depends:+ aeson >=0.8 && <1.3,+ base >=4.7 && <4.11,+ bytestring ==0.10.*,+ base16-bytestring ==0.1.*,+ cryptohash ==0.11.*,+ hashable ==1.2.*,+ http-client >=0.4 && <0.6,+ http-types >=0.8 && <0.10,+ text ==1.2.*,+ time >=1.5 && <1.7,+ transformers >=0.4 && <0.6,+ unordered-containers ==0.2.*+ default-language: Haskell2010+ default-extensions: OverloadedStrings+ hs-source-dirs: src+ other-modules:+ Network.Pusher.Data+ Network.Pusher.Error+ Network.Pusher.Internal.Util+ ghc-options: -Wall+ test-suite tests- default-language: Haskell2010- default-extensions: OverloadedStrings- type: exitcode-stdio-1.0- main-is: Main.hs- other-modules: Auth,- Protocol- hs-source-dirs: test- build-depends: aeson,- base,- bytestring,- hspec,- http-client,- http-types,- pusher-http-haskell,- QuickCheck,- text,- transformers,- unordered-containers- ghc-options: -Wall- if impl(ghc >= 8.0.0)- ghc-options: -Wcompat- -Whi-shadowing- -Wmissing-signatures- else- ghc-options: -fwarn-hi-shadowing- -fwarn-missing-signatures- -fwarn-tabs++ if impl(ghc >=8.0.0)+ ghc-options: -Wcompat -Whi-shadowing -Wmissing-signatures+ else+ ghc-options: -fwarn-hi-shadowing -fwarn-missing-signatures -fwarn-tabs+ type: exitcode-stdio-1.0+ main-is: Main.hs+ build-depends:+ aeson -any,+ base -any,+ bytestring -any,+ hspec -any,+ http-client -any,+ http-types -any,+ pusher-http-haskell -any,+ QuickCheck -any,+ text -any,+ transformers -any,+ unordered-containers -any+ default-language: Haskell2010+ default-extensions: OverloadedStrings+ hs-source-dirs: test+ other-modules:+ Auth+ Protocol+ ghc-options: -Wall
src/Network/Pusher.hs view
@@ -24,9 +24,10 @@ @ let credentials = Credentials- { credentialsAppID = 123- , credentialsAppKey = wrd12344rcd234+ { credentialsAppID = 123+ , credentialsAppKey = wrd12344rcd234 , credentialsAppSecret = 124df34d545v+ , credentialsCluster = Nothing } pusher <- getPusher credentials result <-@@ -40,11 +41,12 @@ See https://pusher.com/docs/rest_api for more detail on the HTTP requests. -}-module Network.Pusher (+module Network.Pusher -- * Data types -- ** Pusher config type- Pusher(..)+ ( Pusher(..) , Credentials(..)+ , Cluster(..) , AppID , AppKey , AppSecret@@ -78,46 +80,26 @@ , PusherError(..) ) where -import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT) import Control.Monad.IO.Class (MonadIO, liftIO)+import Control.Monad.Trans.Except (ExceptT(ExceptT), runExceptT) import qualified Data.Text as T import Network.Pusher.Data- ( AppID- , AppKey- , AppSecret- , Channel(..)- , ChannelName- , ChannelType(..)- , Credentials(..)- , Event- , EventData- , Pusher(..)- , SocketID- , getPusher- , getPusherWithHost- , getPusherWithConnManager- , parseChannel- , renderChannel- , renderChannelPrefix- )-import Network.Pusher.Error(PusherError(..))+ (AppID, AppKey, AppSecret, Channel(..), ChannelName,+ ChannelType(..), Credentials(..), Cluster(..), Event, EventData,+ Pusher(..), SocketID, getPusher, getPusherWithHost,+ getPusherWithConnManager, parseChannel, renderChannel,+ renderChannelPrefix)+import Network.Pusher.Error (PusherError(..))+import qualified Network.Pusher.Internal as Pusher import Network.Pusher.Internal.Auth- ( AuthSignature- , AuthString- , authenticatePresence- , authenticatePrivate- )+ (AuthSignature, AuthString, authenticatePresence,+ authenticatePrivate)+import qualified Network.Pusher.Internal.HTTP as HTTP import Network.Pusher.Internal.Util (getTime) import Network.Pusher.Protocol- ( ChannelInfoQuery- , ChannelsInfo- , ChannelsInfoQuery- , FullChannelInfo- , Users- )-import qualified Network.Pusher.Internal as Pusher-import qualified Network.Pusher.Internal.HTTP as HTTP+ (ChannelInfoQuery, ChannelsInfo, ChannelsInfoQuery,+ FullChannelInfo, Users) -- |Trigger an event to one or more channels. trigger@@ -132,10 +114,11 @@ -- ^An optional socket ID of a connection you wish to exclude -> m (Either PusherError ()) trigger pusher chans event dat socketId =- liftIO $ runExceptT $ do+ liftIO $+ runExceptT $ do (requestParams, requestBody) <- ExceptT $- Pusher.mkTriggerRequest pusher chans event dat socketId <$> getTime+ Pusher.mkTriggerRequest pusher chans event dat socketId <$> getTime HTTP.post (pusherConnectionManager pusher) requestParams requestBody -- |Query a list of channels for information.@@ -148,18 +131,14 @@ -- ^A channel prefix you wish to filter on -> ChannelsInfoQuery -- ^Data you wish to query for, currently just the user count- -> m (Either PusherError ChannelsInfo)- -- ^The returned data+ -> m (Either PusherError ChannelsInfo) -- ^The returned data channels pusher channelTypeFilter prefixFilter attributes =- liftIO $ runExceptT $ do+ liftIO $+ runExceptT $ do requestParams <- liftIO $- Pusher.mkChannelsRequest- pusher- channelTypeFilter- prefixFilter- attributes <$>- getTime+ Pusher.mkChannelsRequest pusher channelTypeFilter prefixFilter attributes <$>+ getTime HTTP.get (pusherConnectionManager pusher) requestParams -- |Query for information on a single channel.@@ -171,7 +150,8 @@ -- ^Can query user count and also subscription count (if enabled) -> m (Either PusherError FullChannelInfo) channel pusher chan attributes =- liftIO $ runExceptT $ do+ liftIO $+ runExceptT $ do requestParams <- liftIO $ Pusher.mkChannelRequest pusher chan attributes <$> getTime HTTP.get (pusherConnectionManager pusher) requestParams@@ -179,10 +159,9 @@ -- |Get a list of users in a presence channel. users :: MonadIO m- => Pusher- -> Channel- -> m (Either PusherError Users)+ => Pusher -> Channel -> m (Either PusherError Users) users pusher chan =- liftIO $ runExceptT $ do+ liftIO $+ runExceptT $ do requestParams <- liftIO $ Pusher.mkUsersRequest pusher chan <$> getTime HTTP.get (pusherConnectionManager pusher) requestParams
src/Network/Pusher/Data.hs view
@@ -13,13 +13,18 @@ The other types represent Pusher channels and Pusher event fields. -}-module Network.Pusher.Data (+module Network.Pusher.Data -- * Pusher config data type- AppID+ ( AppID , AppKey , AppSecret , Pusher(..) , Credentials(..)+ , Cluster(..)+ , clusterMt1+ , clusterEu+ , clusterAp1+ , clusterAp2 , getPusher , getPusherWithHost , getPusherWithConnManager@@ -37,19 +42,21 @@ ) where import Control.Monad.IO.Class (MonadIO, liftIO)-import Data.Aeson ((.:))+import Data.Aeson ((.:), (.:?))+import qualified Data.Aeson as A+import qualified Data.ByteString as B import Data.Foldable (asum) import Data.Hashable (Hashable) import Data.Maybe (fromMaybe) import Data.Monoid ((<>))+import qualified Data.Text as T import Data.Text.Encoding (encodeUtf8) import GHC.Generics (Generic)-import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)-import qualified Data.Aeson as A-import qualified Data.ByteString as B-import qualified Data.Text as T+import Network.HTTP.Client+ (Manager, defaultManagerSettings, newManager) -import Network.Pusher.Internal.Util (failExpectObj, show')+import Network.Pusher.Internal.Util+ (failExpectObj, failExpectStr, show') type AppID = Integer @@ -70,24 +77,53 @@ { credentialsAppID :: AppID , credentialsAppKey :: AppKey , credentialsAppSecret :: AppSecret+ , credentialsCluster :: Maybe Cluster } instance A.FromJSON Credentials where- parseJSON (A.Object v) = Credentials- <$> v .: "app-id"- <*> (encodeUtf8 <$> v .: "app-key")- <*> (encodeUtf8 <$> v .: "app-secret")+ parseJSON (A.Object v) =+ Credentials <$> v .: "app-id" <*> (encodeUtf8 <$> v .: "app-key") <*>+ (encodeUtf8 <$> v .: "app-secret") <*>+ v .:? "app-cluster" parseJSON v2 = failExpectObj v2 +-- | The cluster the current app resides on. Common clusters include: mt1,eu,ap1,ap2+newtype Cluster = Cluster+ { clusterName :: T.Text+ }++clusterMt1, clusterEu, clusterAp1, clusterAp2 :: Cluster+clusterMt1 = Cluster "mt1"++clusterEu = Cluster "eu"++clusterAp1 = Cluster "ap1"++clusterAp2 = Cluster "ap2"++-- The possible cluster suffix given in a host name+renderClusterSuffix :: Cluster -> T.Text+renderClusterSuffix cluster = "-" <> clusterName cluster++instance A.FromJSON Cluster where+ parseJSON v =+ case v of+ A.String txt -> return . Cluster $ txt+ _ -> failExpectStr v+ -- |Use this to get an instance Pusher. This will fill in the host and path -- automatically.-getPusher :: MonadIO m => Credentials -> m Pusher+getPusher+ :: MonadIO m+ => Credentials -> m Pusher getPusher cred = do- connManager <- getConnManager- return $ getPusherWithConnManager connManager Nothing cred+ connManager <- getConnManager+ return $ getPusherWithConnManager connManager Nothing cred -- |Get a Pusher instance that uses a specific API endpoint.-getPusherWithHost :: MonadIO m => T.Text -> Credentials -> m Pusher+getPusherWithHost+ :: MonadIO m+ => T.Text -> Credentials -> m Pusher getPusherWithHost apiHost cred = do connManager <- getConnManager return $ getPusherWithConnManager connManager (Just apiHost) cred@@ -96,21 +132,35 @@ -- if you want to share a connection with your application code. getPusherWithConnManager :: Manager -> Maybe T.Text -> Credentials -> Pusher getPusherWithConnManager connManager apiHost cred =- let path = "/apps/" <> show' (credentialsAppID cred) <> "/" in- Pusher- { pusherHost = fromMaybe "http://api.pusherapp.com" apiHost- , pusherPath = path- , pusherCredentials = cred- , pusherConnectionManager = connManager- }+ let path = "/apps/" <> show' (credentialsAppID cred) <> "/"+ mCluster = credentialsCluster cred+ in Pusher+ { pusherHost = fromMaybe (mkHost mCluster) apiHost+ , pusherPath = path+ , pusherCredentials = cred+ , pusherConnectionManager = connManager+ } -getConnManager :: MonadIO m => m Manager+-- |Given a possible cluster, return the corresponding host+mkHost :: Maybe Cluster -> T.Text+mkHost mCluster =+ case mCluster of+ Nothing -> "http://api.pusherapp.com"+ Just c -> "http://api" <> renderClusterSuffix c <> ".pusher.com"++getConnManager+ :: MonadIO m+ => m Manager getConnManager = liftIO $ newManager defaultManagerSettings type ChannelName = T.Text -- |The possible types of Pusher channe.-data ChannelType = Public | Private | Presence deriving (Eq, Generic, Show)+data ChannelType+ = Public+ | Private+ | Presence+ deriving (Eq, Generic, Show) instance Hashable ChannelType @@ -132,19 +182,19 @@ -- |Convert string representation, e.g. private-chan into the datatype parseChannel :: T.Text -> Channel-parseChannel chan =+parseChannel chan -- Attempt to parse it as a private or presence channel; default to public+ = fromMaybe (Channel Public chan)- (asum [parseChanAs Private, parseChanAs Presence])- where- parseChanAs chanType =- let split = T.splitOn (renderChannelPrefix chanType) chan in+ (asum [parseChanAs Private, parseChanAs Presence])+ where+ parseChanAs chanType =+ let split = T.splitOn (renderChannelPrefix chanType) chan -- If the prefix appears at the start, then the first element will be empty- if length split > 1 && T.null (head split) then- Just $ Channel chanType (T.concat $ tail split)- else- Nothing+ in if length split > 1 && T.null (head split)+ then Just $ Channel chanType (T.concat $ tail split)+ else Nothing type Event = T.Text
src/Network/Pusher/Error.hs view
@@ -1,4 +1,6 @@-module Network.Pusher.Error (PusherError(..)) where+module Network.Pusher.Error+ ( PusherError(..)+ ) where import Control.Exception (Exception) import qualified Data.Text as T@@ -10,6 +12,6 @@ | PusherNon200ResponseError T.Text -- |Received unexpected data from Pusher. | PusherInvalidResponseError T.Text- deriving Show+ deriving (Show) instance Exception PusherError
src/Network/Pusher/Internal.hs view
@@ -14,37 +14,23 @@ ) where import Control.Monad (when)-import Data.Maybe (maybeToList)-import Data.Monoid ((<>))-import Data.Text.Encoding (encodeUtf8) import qualified Data.Aeson as A import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL+import Data.Maybe (maybeToList)+import Data.Monoid ((<>)) import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8) import Network.Pusher.Data- ( Channel- , ChannelType- , Credentials(..)- , Event- , EventData- , Pusher(..)- , SocketID- , renderChannel- , renderChannelPrefix- )-import Network.Pusher.Error(PusherError(..))+ (Channel, ChannelType, Credentials(..), Event, EventData,+ Pusher(..), SocketID, renderChannel, renderChannelPrefix)+import Network.Pusher.Error (PusherError(..)) import Network.Pusher.Internal.Auth (makeQS) import Network.Pusher.Internal.HTTP- ( RequestBody- , RequestParams(RequestParams)- , RequestQueryString- )+ (RequestBody, RequestParams(RequestParams), RequestQueryString) import Network.Pusher.Protocol- ( ChannelInfoQuery- , ChannelsInfoQuery- , toURLParam- )+ (ChannelInfoQuery, ChannelsInfoQuery, toURLParam) mkTriggerRequest :: Pusher@@ -55,20 +41,21 @@ -> Int -> Either PusherError (RequestParams, RequestBody) mkTriggerRequest pusher chans event dat socketId time = do- when- (length chans > 10)- (Left $ PusherArgumentError "Must be less than 10 channels")- let- body = A.object $+ when+ (length chans > 10)+ (Left $ PusherArgumentError "Must be less than 10 channels")+ let body =+ A.object $ [ ("name", A.String event) , ("channels", A.toJSON (map (A.String . renderChannel) chans)) , ("data", A.String dat)- ] ++ maybeToList (fmap (\sID -> ("socket_id", A.String sID)) socketId)+ ] +++ maybeToList (fmap (\sID -> ("socket_id", A.String sID)) socketId) bodyBS = BL.toStrict $ A.encode body- when- (B.length bodyBS > 10000)- (Left $ PusherArgumentError "Body must be less than 10000KB")- return (mkPostRequest pusher "events" [] bodyBS time, body)+ when+ (B.length bodyBS > 10000)+ (Left $ PusherArgumentError "Body must be less than 10000KB")+ return (mkPostRequest pusher "events" [] bodyBS time, body) mkChannelsRequest :: Pusher@@ -78,61 +65,44 @@ -> Int -> RequestParams mkChannelsRequest pusher channelTypeFilter prefixFilter attributes time =- let- prefix = maybe "" renderChannelPrefix channelTypeFilter <> prefixFilter- params =- [ ("info", encodeUtf8 $ toURLParam attributes)- , ("filter_by_prefix", encodeUtf8 prefix)- ]- in- mkGetRequest pusher "channels" params time+ let prefix = maybe "" renderChannelPrefix channelTypeFilter <> prefixFilter+ params =+ [ ("info", encodeUtf8 $ toURLParam attributes)+ , ("filter_by_prefix", encodeUtf8 prefix)+ ]+ in mkGetRequest pusher "channels" params time -mkChannelRequest- :: Pusher- -> Channel- -> ChannelInfoQuery- -> Int- -> RequestParams+mkChannelRequest :: Pusher+ -> Channel+ -> ChannelInfoQuery+ -> Int+ -> RequestParams mkChannelRequest pusher chan attributes time =- let- params = [("info", encodeUtf8 $ toURLParam attributes)]- subPath = "channels/" <> renderChannel chan- in- mkGetRequest pusher subPath params time+ let params = [("info", encodeUtf8 $ toURLParam attributes)]+ subPath = "channels/" <> renderChannel chan+ in mkGetRequest pusher subPath params time mkUsersRequest :: Pusher -> Channel -> Int -> RequestParams mkUsersRequest pusher chan time =- let- subPath = "channels/" <> renderChannel chan <> "/users"- in- mkGetRequest pusher subPath [] time+ let subPath = "channels/" <> renderChannel chan <> "/users"+ in mkGetRequest pusher subPath [] time -mkGetRequest- :: Pusher- -> T.Text- -> RequestQueryString- -> Int- -> RequestParams+mkGetRequest :: Pusher -> T.Text -> RequestQueryString -> Int -> RequestParams mkGetRequest pusher subPath params time =- let- (ep, fullPath) = mkEndpoint pusher subPath- qs = mkQS pusher "GET" fullPath params "" time- in- RequestParams ep qs+ let (ep, fullPath) = mkEndpoint pusher subPath+ qs = mkQS pusher "GET" fullPath params "" time+ in RequestParams ep qs -mkPostRequest- :: Pusher- -> T.Text- -> RequestQueryString- -> B.ByteString- -> Int- -> RequestParams+mkPostRequest :: Pusher+ -> T.Text+ -> RequestQueryString+ -> B.ByteString+ -> Int+ -> RequestParams mkPostRequest pusher subPath params bodyBS time =- let- (ep, fullPath) = mkEndpoint pusher subPath- qs = mkQS pusher "POST" fullPath params bodyBS time- in- RequestParams ep qs+ let (ep, fullPath) = mkEndpoint pusher subPath+ qs = mkQS pusher "POST" fullPath params bodyBS time+ in RequestParams ep qs -- |Build a full endpoint from the details in Pusher and the subPath. mkEndpoint@@ -140,11 +110,9 @@ -> T.Text -- ^The subpath of the specific request, e.g "events/channel-name" -> (T.Text, T.Text) -- ^The full endpoint, and just the path component mkEndpoint pusher subPath =- let- fullPath = pusherPath pusher <> subPath- endpoint = pusherHost pusher <> fullPath- in- (endpoint, fullPath)+ let fullPath = pusherPath pusher <> subPath+ endpoint = pusherHost pusher <> fullPath+ in (endpoint, fullPath) mkQS :: Pusher@@ -155,9 +123,5 @@ -> Int -> RequestQueryString mkQS pusher =- let- credentials = pusherCredentials pusher- in- makeQS- (credentialsAppKey credentials)- (credentialsAppSecret credentials)+ let credentials = pusherCredentials pusher+ in makeQS (credentialsAppKey credentials) (credentialsAppSecret credentials)
src/Network/Pusher/Internal/Auth.hs view
@@ -18,35 +18,31 @@ , AuthSignature , authenticatePresence , authenticatePresenceWithEncoder- , authenticatePrivate, makeQS+ , authenticatePrivate+ , makeQS ) where +import qualified Data.Aeson as A import Data.Monoid ((<>)) import Data.Text.Encoding (encodeUtf8) import GHC.Exts (sortWith)-import qualified Data.Aeson as A #if MIN_VERSION_aeson(1,0,0) import qualified Data.Aeson.Text as A #else import qualified Data.Aeson.Encode as A #endif+import qualified Crypto.Hash.MD5 as MD5+import qualified Crypto.Hash.SHA256 as SHA256+import qualified Crypto.MAC.HMAC as HMAC import qualified Data.ByteString as B import qualified Data.ByteString.Base16 as B16 import qualified Data.Text as T import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Builder as TL-import qualified Crypto.Hash.MD5 as MD5-import qualified Crypto.Hash.SHA256 as SHA256-import qualified Crypto.MAC.HMAC as HMAC import Network.Pusher.Data- ( AppKey- , AppSecret- , Channel- , Credentials(..)- , SocketID- , renderChannel- )+ (AppKey, AppSecret, Channel, Credentials(..), SocketID,+ renderChannel) import Network.Pusher.Internal.HTTP (RequestQueryString) import Network.Pusher.Internal.Util (show') @@ -62,28 +58,29 @@ -> Int -- ^Current UNIX timestamp -> RequestQueryString makeQS appKey appSecret method path params body ts =- let- -- Generate all required parameters and add them to the list of existing ones- allParams = sortWith fst $ params ++- [ ("auth_key", appKey)- , ("auth_timestamp", show' ts)- , ("auth_version", "1.0")- , ("body_md5", B16.encode (MD5.hash body))- ]+ let allParams+ -- Generate all required parameters and add them to the list of existing+ -- ones+ =+ sortWith fst $+ params +++ [ ("auth_key", appKey)+ , ("auth_timestamp", show' ts)+ , ("auth_version", "1.0")+ , ("body_md5", B16.encode (MD5.hash body))+ ] -- Generate the auth signature from the list of parameters- authSig = authSignature appSecret $ B.intercalate "\n"- [ encodeUtf8 method- , encodeUtf8 path- , formQueryString allParams- ]- in+ authSig =+ authSignature appSecret $+ B.intercalate+ "\n"+ [encodeUtf8 method, encodeUtf8 path, formQueryString allParams] -- Add the auth string to the list- ("auth_signature", authSig) : allParams+ in ("auth_signature", authSig) : allParams -- |Render key-value tuple mapping of query string parameters into a string. formQueryString :: RequestQueryString -> B.ByteString-formQueryString =- B.intercalate "&" . map (\(a, b) -> a <> "=" <> b)+formQueryString = B.intercalate "&" . map (\(a, b) -> a <> "=" <> b) -- | The bytestring to sign with the app secret to create a signature from. type AuthString = B.ByteString@@ -96,31 +93,21 @@ authSignature appSecret authString = B16.encode $ HMAC.hmac SHA256.hash 64 appSecret authString - -- |Generate an auth signature of the form "app_key:auth_sig" for a user of a -- private channel.-authenticatePrivate- :: Credentials- -> SocketID- -> Channel- -> AuthSignature+authenticatePrivate :: Credentials -> SocketID -> Channel -> AuthSignature authenticatePrivate cred socketID channel =- let- sig = authSignature- (credentialsAppSecret cred)- (encodeUtf8 $ socketID <> ":" <> renderChannel channel)- in- credentialsAppKey cred <> ":" <> sig+ let sig =+ authSignature+ (credentialsAppSecret cred)+ (encodeUtf8 $ socketID <> ":" <> renderChannel channel)+ in credentialsAppKey cred <> ":" <> sig -- |Generate an auth signature of the form "app_key:auth_sig" for a user of a -- presence channel. authenticatePresence :: A.ToJSON a- => Credentials- -> SocketID- -> Channel- -> a- -> AuthSignature+ => Credentials -> SocketID -> Channel -> a -> AuthSignature authenticatePresence = authenticatePresenceWithEncoder (TL.toStrict . TL.toLazyText . A.encodeToTextBuilder . A.toJSON)@@ -136,9 +123,8 @@ -> a -> AuthSignature authenticatePresenceWithEncoder userEncoder cred socketID channel userData =- let- authString =- encodeUtf8 $ socketID <> ":" <> renderChannel channel <> ":" <> userEncoder userData- sig = authSignature (credentialsAppSecret cred) authString- in- credentialsAppKey cred <> ":" <> sig+ let authString =+ encodeUtf8 $+ socketID <> ":" <> renderChannel channel <> ":" <> userEncoder userData+ sig = authSignature (credentialsAppSecret cred) authString+ in credentialsAppKey cred <> ":" <> sig
src/Network/Pusher/Internal/HTTP.hs view
@@ -26,16 +26,16 @@ import Control.Exception (displayException) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Except (ExceptT(ExceptT), throwE)-import Data.Monoid ((<>))-import Data.Text.Encoding (decodeUtf8')-import Network.HTTP.Types.Header (hContentType)-import Network.HTTP.Types.Method (methodPost)-import Network.HTTP.Types.Status (statusCode, statusMessage) import qualified Data.Aeson as A import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL+import Data.Monoid ((<>)) import qualified Data.Text as T+import Data.Text.Encoding (decodeUtf8') import qualified Network.HTTP.Client as HTTP.Client+import Network.HTTP.Types.Header (hContentType)+import Network.HTTP.Types.Method (methodPost)+import Network.HTTP.Types.Status (statusCode, statusMessage) import Network.Pusher.Error (PusherError(..)) @@ -56,8 +56,7 @@ :: A.FromJSON a => HTTP.Client.Manager -> RequestParams- -> ExceptT PusherError IO a- -- ^The body of the response+ -> ExceptT PusherError IO a -- ^The body of the response get connManager (RequestParams ep qs) = do req <- ExceptT $ return $ mkRequest ep qs resp <- doReqest connManager req@@ -69,37 +68,33 @@ -- |Issue an HTTP POST request. post :: A.ToJSON a- => HTTP.Client.Manager- -> RequestParams- -> a- -> ExceptT PusherError IO ()+ => HTTP.Client.Manager -> RequestParams -> a -> ExceptT PusherError IO () post connManager (RequestParams ep qs) body = do req <- ExceptT $ return $ mkPost (A.encode body) <$> mkRequest ep qs _ <- doReqest connManager req return () -mkRequest- :: T.Text- -> RequestQueryString- -> Either PusherError HTTP.Client.Request+mkRequest :: T.Text+ -> RequestQueryString+ -> Either PusherError HTTP.Client.Request mkRequest ep qs = case parseRequest $ T.unpack ep of Nothing -> Left $ PusherArgumentError $ "failed to parse url: " <> ep Just req -> Right $ HTTP.Client.setQueryString (map (second Just) qs) req- where+ where+ #if MIN_VERSION_http_client(0,4,30) parseRequest = HTTP.Client.parseRequest #else parseRequest = HTTP.Client.parseUrl #endif- mkPost :: BL.ByteString -> HTTP.Client.Request -> HTTP.Client.Request mkPost body req = req- { HTTP.Client.method = methodPost- , HTTP.Client.requestHeaders = [(hContentType, "application/json")]- , HTTP.Client.requestBody = HTTP.Client.RequestBodyLBS body- }+ { HTTP.Client.method = methodPost+ , HTTP.Client.requestHeaders = [(hContentType, "application/json")]+ , HTTP.Client.requestBody = HTTP.Client.RequestBodyLBS body+ } doReqest :: HTTP.Client.Manager@@ -108,12 +103,11 @@ doReqest connManager req = do response <- liftIO $ HTTP.Client.httpLbs req connManager let status = HTTP.Client.responseStatus response- if statusCode status == 200 then- return $ HTTP.Client.responseBody response- else- let decoded = decodeUtf8' $ statusMessage status in- throwE $- either- (PusherInvalidResponseError . T.pack . displayException)- PusherNon200ResponseError- decoded+ if statusCode status == 200+ then return $ HTTP.Client.responseBody response+ else let decoded = decodeUtf8' $ statusMessage status+ in throwE $+ either+ (PusherInvalidResponseError . T.pack . displayException)+ PusherNon200ResponseError+ decoded
src/Network/Pusher/Internal/Util.hs view
@@ -8,14 +8,15 @@ -} module Network.Pusher.Internal.Util ( failExpectObj+ , failExpectStr , getTime , show' ) where -import Data.String (IsString, fromString)-import Data.Time.Clock.POSIX (getPOSIXTime) import qualified Data.Aeson as A import qualified Data.Aeson.Types as A+import Data.String (IsString, fromString)+import Data.Time.Clock.POSIX (getPOSIXTime) -- |Get the system time as an Int. getTime :: IO Int@@ -26,6 +27,13 @@ failExpectObj :: A.Value -> A.Parser a failExpectObj = fail . ("Expected JSON object, got " ++) . show +-- |When decoding Aeson values, this can be used to return a failing parser+-- when an string was expected, but it was a different type of data.+failExpectStr :: A.Value -> A.Parser a+failExpectStr = fail . ("Expected JSON string, got " ++) . show+ -- | Generalised version of show-show' :: (Show a, IsString b) => a -> b+show'+ :: (Show a, IsString b)+ => a -> b show' = fromString . show
src/Network/Pusher/Protocol.hs view
@@ -29,23 +29,24 @@ ) where import Data.Aeson ((.:), (.:?))-import Data.Hashable (Hashable)-import GHC.Generics (Generic) import qualified Data.Aeson as A import qualified Data.HashMap.Strict as HM import qualified Data.HashSet as HS+import Data.Hashable (Hashable) import qualified Data.Text as T+import GHC.Generics (Generic) -import Network.Pusher.Internal.Util (failExpectObj) import Network.Pusher.Data (Channel, parseChannel)+import Network.Pusher.Internal.Util (failExpectObj) -- |Types that can be serialised to a querystring parameter value. class ToURLParam a where- -- |Convert the data into a querystring parameter value. toURLParam :: a -> T.Text -- |Enumeration of the attributes that can be queried about multiple channels.-data ChannelsInfoAttributes = ChannelsUserCount deriving (Eq, Generic)+data ChannelsInfoAttributes =+ ChannelsUserCount+ deriving (Eq, Generic) instance ToURLParam ChannelsInfoAttributes where toURLParam ChannelsUserCount = "user_count"@@ -55,10 +56,12 @@ -- |A set of requested ChannelsInfoAttributes. newtype ChannelsInfoQuery = ChannelsInfoQuery (HS.HashSet ChannelsInfoAttributes)- deriving ToURLParam+ deriving (ToURLParam) -- |Enumeration of the attributes that can be queried about a single channel.-data ChannelInfoAttributes = ChannelUserCount | ChannelSubscriptionCount+data ChannelInfoAttributes+ = ChannelUserCount+ | ChannelSubscriptionCount deriving (Eq, Generic) instance ToURLParam ChannelInfoAttributes where@@ -68,11 +71,12 @@ instance Hashable ChannelInfoAttributes -- |A set of requested ChannelInfoAttributes.-newtype ChannelInfoQuery = ChannelInfoQuery (HS.HashSet ChannelInfoAttributes)- deriving ToURLParam-+newtype ChannelInfoQuery =+ ChannelInfoQuery (HS.HashSet ChannelInfoAttributes)+ deriving (ToURLParam) -instance ToURLParam a => ToURLParam (HS.HashSet a) where+instance ToURLParam a =>+ ToURLParam (HS.HashSet a) where toURLParam hs = T.intercalate "," $ toURLParam <$> HS.toList hs -- |A map of channels to their ChannelInfo. The result of querying channel@@ -85,18 +89,19 @@ parseJSON (A.Object v) = do chansV <- v .: "channels" case chansV of- A.Object cs ->+ A.Object cs -- Need to go to and from list in order to map (parse) the keys- ChannelsInfo . HM.fromList- <$> mapM- (\(channel, info) -> (parseChannel channel,) <$> A.parseJSON info)- (HM.toList cs)+ ->+ ChannelsInfo . HM.fromList <$>+ mapM+ (\(channel, info) -> (parseChannel channel, ) <$> A.parseJSON info)+ (HM.toList cs) v1 -> failExpectObj v1 parseJSON v2 = failExpectObj v2 -- |The possible returned channel attributes when multiple when multiple -- channels are queried.-data ChannelInfo = ChannelInfo+newtype ChannelInfo = ChannelInfo { channelInfoUserCount :: Maybe Int } deriving (Eq, Show) @@ -113,14 +118,14 @@ instance A.FromJSON FullChannelInfo where parseJSON (A.Object v) =- FullChannelInfo- <$> v .: "occupied"- <*> v .:? "user_count"- <*> v .:? "subscription_count"+ FullChannelInfo <$> v .: "occupied" <*> v .:? "user_count" <*>+ v .:? "subscription_count" parseJSON v = failExpectObj v -- |A list of users returned by querying for users in a presence channel.-newtype Users = Users [User] deriving (Eq, Show)+newtype Users =+ Users [User]+ deriving (Eq, Show) instance A.FromJSON Users where parseJSON (A.Object v) = do@@ -128,8 +133,11 @@ Users <$> A.parseJSON users parseJSON v = failExpectObj v --- |The data about a user returned when querying for users in a presence channel.-data User = User { userID :: T.Text } deriving (Eq, Show)+-- |The data about a user returned when querying for users in a presence+-- channel.+newtype User = User+ { userID :: T.Text+ } deriving (Eq, Show) instance A.FromJSON User where parseJSON (A.Object v) = User <$> v .: "id"
test/Auth.hs view
@@ -3,56 +3,54 @@ import Test.Hspec (Spec, describe, it, shouldBe) import Network.Pusher- ( Channel(..)- , ChannelType(Private, Presence)- , Credentials(..)- )+ (Channel(..), ChannelType(Presence, Private), Credentials(..)) import Network.Pusher.Internal.Auth- ( authenticatePrivate- , authenticatePresenceWithEncoder- , makeQS- )+ (authenticatePresenceWithEncoder, authenticatePrivate, makeQS) test :: Spec test = do describe "Auth.makeQS" $ it "works" $ -- Happy case based on data from the docs: https://pusher.com/docs/rest_api#worked-authentication-example- let- body = "{\"name\":\"foo\",\"channels\":[\"project-3\"],\"data\":\"{\\\"some\\\":\\\"data\\\"}\"}"- in- makeQS (credentialsAppKey credentials) (credentialsAppSecret credentials) "POST" "/apps/3/events" [] body 1353088179- `shouldBe`- [ ("auth_signature", "da454824c97ba181a32ccc17a72625ba02771f50b50e1e7430e47a1f3f457e6c")- , ("auth_key","278d425bdf160c739803")- , ("auth_timestamp","1353088179")- , ("auth_version","1.0")- , ("body_md5","ec365a775a4cd0599faeb73354201b6f")- ]-+ let body =+ "{\"name\":\"foo\",\"channels\":[\"project-3\"],\"data\":\"{\\\"some\\\":\\\"data\\\"}\"}"+ in makeQS+ (credentialsAppKey credentials)+ (credentialsAppSecret credentials)+ "POST"+ "/apps/3/events"+ []+ body+ 1353088179 `shouldBe`+ [ ( "auth_signature"+ , "da454824c97ba181a32ccc17a72625ba02771f50b50e1e7430e47a1f3f457e6c")+ , ("auth_key", "278d425bdf160c739803")+ , ("auth_timestamp", "1353088179")+ , ("auth_version", "1.0")+ , ("body_md5", "ec365a775a4cd0599faeb73354201b6f")+ ] describe "Auth.authenticatePrivate" $ it "works" $ -- Data from https://pusher.com/docs/auth_signatures#worked-example- authenticatePrivate credentials "1234.1234" (Channel Private "foobar")- `shouldBe` "278d425bdf160c739803:58df8b0c36d6982b82c3ecf6b4662e34fe8c25bba48f5369f135bf843651c3a4"-+ authenticatePrivate credentials "1234.1234" (Channel Private "foobar") `shouldBe`+ "278d425bdf160c739803:58df8b0c36d6982b82c3ecf6b4662e34fe8c25bba48f5369f135bf843651c3a4" describe "Auth.authenticatePresenceWithEncoder" $ it "works for presence channels" $ -- Data from https://pusher.com/docs/auth_signatures#presence- let- userData = "{\"user_id\":10,\"user_info\":{\"name\":\"Mr. Pusher\"}}"- in- authenticatePresenceWithEncoder- (const userData)- credentials- "1234.1234"- (Channel Presence "foobar")- ("doesn't matter" :: String)- `shouldBe` "278d425bdf160c739803:afaed3695da2ffd16931f457e338e6c9f2921fa133ce7dac49f529792be6304c"+ let userData = "{\"user_id\":10,\"user_info\":{\"name\":\"Mr. Pusher\"}}"+ in authenticatePresenceWithEncoder+ (const userData)+ credentials+ "1234.1234"+ (Channel Presence "foobar")+ ("doesn't matter" :: String) `shouldBe`+ "278d425bdf160c739803:afaed3695da2ffd16931f457e338e6c9f2921fa133ce7dac49f529792be6304c" credentials :: Credentials-credentials = Credentials+credentials =+ Credentials { credentialsAppID = 3 , credentialsAppKey = "278d425bdf160c739803" , credentialsAppSecret = "7ad3773142a6692b25b8"+ , credentialsCluster = Nothing }
test/Protocol.hs view
@@ -1,32 +1,26 @@ module Protocol where -import Test.Hspec (Spec, describe, it, shouldBe)-import Test.QuickCheck (Arbitrary(..), elements, property) import qualified Data.Aeson as A import qualified Data.HashMap.Strict as HM import qualified Data.Text as T+import Test.Hspec (Spec, describe, it, shouldBe)+import Test.QuickCheck (Arbitrary(..), elements, property) import Network.Pusher- ( Channel(..)- , ChannelType(Public, Presence, Private)- , parseChannel- , renderChannel- )+ (Channel(..), ChannelType(Presence, Private, Public), parseChannel,+ renderChannel) import Network.Pusher.Protocol- ( ChannelInfo(..)- , ChannelsInfo(..)- , FullChannelInfo(..)- , User(..)- , Users(..)- )+ (ChannelInfo(..), ChannelsInfo(..), FullChannelInfo(..), User(..),+ Users(..)) -newtype TestChannel = TestChannel Channel deriving (Show)+newtype TestChannel =+ TestChannel Channel+ deriving (Show) instance Arbitrary TestChannel where arbitrary = do- let- testChannelType = elements [Public, Private, Presence]- channel = Channel <$> testChannelType <*> (T.pack <$> arbitrary)+ let testChannelType = elements [Public, Private, Presence]+ channel = Channel <$> testChannelType <*> (T.pack <$> arbitrary) TestChannel <$> channel test :: Spec@@ -34,18 +28,16 @@ describe "Protocol.Channel" $ do it "show instance works for public channels" $ renderChannel (Channel Public "test") `shouldBe` "test"- it "show instance works for private channels" $ renderChannel (Channel Private "test") `shouldBe` "private-test"- it "show instance is an inverse of parseChannel" $- property $ \(TestChannel chan) -> parseChannel (renderChannel chan) == chan-+ property $ \(TestChannel chan) ->+ parseChannel (renderChannel chan) == chan describe "Protocol.ChannelsInfo" $ it "parsing works" $ -- Data from https://pusher.com/docs/rest_api#successful-response-1- A.decode- "{\+ A.decode+ "{\ \ \"channels\": {\ \ \"presence-foobar\": {\ \ \"user_count\": 42\@@ -54,33 +46,30 @@ \ \"user_count\": 123\ \ }\ \ }\-\ }"- `shouldBe`- (Just $ ChannelsInfo $ HM.fromList- [ (Channel Presence "foobar", ChannelInfo $ Just 42)- , (Channel Presence "another", ChannelInfo $ Just 123)- ])-+\ }" `shouldBe`+ (Just $+ ChannelsInfo $+ HM.fromList+ [ (Channel Presence "foobar", ChannelInfo $ Just 42)+ , (Channel Presence "another", ChannelInfo $ Just 123)+ ]) describe "Protocol.FullChannelInfo" $ it "parsing works" $ -- Data from https://pusher.com/docs/rest_api#successful-response-2- A.decode- "{\+ A.decode+ "{\ \ \"occupied\": true,\ \ \"user_count\": 42,\ \ \"subscription_count\": 42\-\ }"- `shouldBe`- (Just $ FullChannelInfo True (Just 42) (Just 42))-+\ }" `shouldBe`+ (Just $ FullChannelInfo True (Just 42) (Just 42)) describe "Protocol.Users" $ it "parsing works" $- A.decode- "{\+ A.decode+ "{\ \ \"users\": [\ \ { \"id\": \"1\" },\ \ { \"id\": \"2\" }\ \ ]\-\ }"- `shouldBe`- (Just $ Users [User "1", User "2"])+\ }" `shouldBe`+ (Just $ Users [User "1", User "2"])