packages feed

pusher-http-haskell 1.0.0.0 → 1.1.0.0

raw patch · 9 files changed

+274/−182 lines, 9 files

Files

pusher-http-haskell.cabal view
@@ -1,5 +1,5 @@ name:                 pusher-http-haskell-version:              1.0.0.0+version:              1.1.0.0 cabal-version:        >=1.18 build-type:           Simple license:              MIT
src/Network/Pusher.hs view
@@ -13,17 +13,11 @@ well as functions for generating auth signatures for private and presence channels. -The idea is that you must create a Pusher data structure with your credentials,-then your write your block of code that calls functions from this library, and-finally "run" this code with the Pusher data structure you created.--The types of the functions are general enough to allow you to use the provided-PusherM as the return type, or PusherT if you wish to use other monads in your-applications monad transformer stack.+First create a 'Pusher' data structure with your Pusher 'Credentials', and then+call the functions defined in this module to make the HTTP requests. -The functions return a MonadError type which means any can fail, and you must-handle these failures in your client application. In the simplist case you can-instantiate the type to an Either and case split on it.+If any of the requests fail, the return values of the functions will result in+a 'Left' 'PusherError' when run.  An example of how you would use these functions: @@ -47,19 +41,37 @@ See https://pusher.com/docs/rest_api for more detail on the HTTP requests. -} module Network.Pusher (-  -- * The Pusher config type+  -- * Data types+  -- ** Pusher config type     Pusher(..)   , Credentials(..)+  , AppID+  , AppKey+  , AppSecret   , getPusher   , getPusherWithHost   , getPusherWithConnManager-  -- * Events+  -- ** Channels+  , Channel(..)+  , ChannelName+  , ChannelType(..)+  , renderChannel+  , renderChannelPrefix+  , parseChannel+  -- ** Events+  , Event+  , EventData+  , SocketID+  -- * HTTP Requests+  -- ** Trigger events   , trigger-  -- * Channel queries+  -- ** Channel queries   , channels   , channel   , users   -- * Authentication+  , AuthString+  , AuthSignature   , authenticatePresence   , authenticatePrivate   -- * Errors@@ -72,7 +84,6 @@ import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Maybe (maybeToList) import Data.Monoid ((<>))-import Data.Text.Encoding (encodeUtf8) import Network.HTTP.Client (Manager) import qualified Data.Aeson as A import qualified Data.ByteString as B@@ -80,29 +91,39 @@ import qualified Data.Text as T  import Network.Pusher.Data-  ( Pusher(..)+  ( AppID+  , AppKey+  , AppSecret+  , Channel(..)+  , ChannelName+  , ChannelType(..)   , Credentials(..)+  , Event+  , EventData+  , Pusher(..)+  , SocketID   , getPusher   , getPusherWithHost   , getPusherWithConnManager+  , parseChannel+  , renderChannel+  , renderChannelPrefix   ) import Network.Pusher.Error(PusherError(..)) import Network.Pusher.Internal.Auth-  ( authenticatePresence+  ( AuthSignature+  , AuthString+  , authenticatePresence   , authenticatePrivate   , makeQS   ) import Network.Pusher.Internal.Util (getTime) import Network.Pusher.Protocol-  ( Channel-  , ChannelInfoQuery+  ( ChannelInfoQuery   , ChannelsInfo   , ChannelsInfoQuery-  , ChannelType   , FullChannelInfo   , Users-  , renderChannel-  , renderChannelPrefix   , toURLParam   ) import qualified Network.Pusher.Internal as Pusher@@ -114,11 +135,10 @@   => Pusher   -> [Channel]   -- ^The list of channels to trigger to-  -> T.Text-  -- ^The event-  -> T.Text-  -- ^The data to send (often encoded JSON)-  -> Maybe T.Text+  -> Event+  -> EventData+  -- ^Often encoded JSON+  -> Maybe SocketID   -- ^An optional socket ID of a connection you wish to exclude   -> m (Either PusherError ()) trigger pusher chans event dat socketId =
src/Network/Pusher/Data.hs view
@@ -1,35 +1,64 @@+{-# LANGUAGE DeriveGeneric #-}+ {-| Module      : Network.Pusher.Data-Description : Data structure to store Pusher config+Description : Data structure representing Pusher concepts and config Copyright   : (c) Will Sewell, 2016 Licence     : MIT Maintainer  : me@willsewell.com Stability   : experimental -You must create an instance of this datatype with your particular Pusher app-credentials in order to run the main API functions.+You must create an instance of the Pusher datatype with your particular Pusher+app credentials in order to run the main API functions.++The other types represent Pusher channels and Pusher event fields. -}-module Network.Pusher.Data-  ( Pusher(..)+module Network.Pusher.Data (+  -- * Pusher config data type+    AppID+  , AppKey+  , AppSecret+  , Pusher(..)   , Credentials(..)   , getPusher   , getPusherWithHost   , getPusherWithConnManager+  -- * Channels+  , Channel(..)+  , ChannelName+  , ChannelType(..)+  , renderChannel+  , renderChannelPrefix+  , parseChannel+  -- Events+  , Event+  , EventData+  , SocketID   ) where  import Control.Applicative ((<$>), (<*>)) import Control.Monad.IO.Class (MonadIO, liftIO) import Data.Aeson ((.:))+import Data.Foldable (asum)+import Data.Hashable (Hashable) import Data.Maybe (fromMaybe) import Data.Monoid ((<>)) import Data.Text.Encoding (encodeUtf8)+import GHC.Generics (Generic) import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)+import Test.QuickCheck (Arbitrary, arbitrary, elements) import qualified Data.Aeson as A import qualified Data.ByteString as B import qualified Data.Text as T  import Network.Pusher.Internal.Util (failExpectObj, show') +type AppID = Integer++type AppKey = B.ByteString++type AppSecret = B.ByteString+ -- |All the required configuration needed to interact with the API. data Pusher = Pusher   { pusherHost :: T.Text@@ -40,9 +69,9 @@  -- |The credentials for the current app. data Credentials = Credentials-  { credentialsAppID :: Integer-  , credentialsAppKey :: B.ByteString-  , credentialsAppSecret :: B.ByteString+  { credentialsAppID :: AppID+  , credentialsAppKey :: AppKey+  , credentialsAppSecret :: AppSecret   }  instance A.FromJSON Credentials where@@ -79,3 +108,54 @@  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)++instance Hashable ChannelType++instance Arbitrary ChannelType where+  arbitrary = elements [Public, Private, Presence]++renderChannelPrefix :: ChannelType -> T.Text+renderChannelPrefix Public = ""+renderChannelPrefix Private = "private-"+renderChannelPrefix Presence = "presence-"++-- |The channel name (not including the channel type prefix) and its type.+data Channel = Channel+  { channelType :: ChannelType+  , channelName :: ChannelName+  } deriving (Eq, Generic, Show)++instance Hashable Channel++instance Arbitrary Channel where+  arbitrary = Channel <$> arbitrary <*> (T.pack <$> arbitrary)++renderChannel :: Channel -> T.Text+renderChannel (Channel cType cName) = renderChannelPrefix cType <> cName++-- |Convert string representation, e.g. private-chan into the datatype+parseChannel :: T.Text -> Channel+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+    -- 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++type Event = T.Text++type EventData = T.Text++type SocketID = T.Text
src/Network/Pusher/Internal.hs view
@@ -7,9 +7,7 @@ Stability   : experimental -} module Network.Pusher.Internal-  ( PusherRequestParams(..)-  , PusherRequestBody-  , mkTriggerRequest+  ( mkTriggerRequest   , mkChannelsRequest   , mkChannelRequest   , mkUsersRequest@@ -24,39 +22,41 @@ import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T -import Network.Pusher.Data (Pusher(..), Credentials(..))+import Network.Pusher.Data+  ( 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+  ) import Network.Pusher.Protocol-  ( Channel-  , ChannelInfoQuery+  ( ChannelInfoQuery   , ChannelsInfo   , ChannelsInfoQuery-  , ChannelType   , FullChannelInfo   , Users-  , renderChannel-  , renderChannelPrefix   , toURLParam   ) -data PusherRequestParams = PusherRequestParams-  { pusherRequestEndpoint :: T.Text-  -- ^The API endpoint, for example http://api.pusherapp.com/apps/123/events-  , pusherRequestQueryString :: [(B.ByteString, B.ByteString)]-  -- ^List of query string parameters as key-value tuples-  }--type PusherRequestBody = A.Value- mkTriggerRequest   :: Pusher   -> [Channel]-  -> T.Text-  -> T.Text-  -> Maybe T.Text+  -> Event+  -> EventData+  -> Maybe SocketID   -> Int-  -> Either PusherError (PusherRequestParams, PusherRequestBody)+  -> Either PusherError (RequestParams, RequestBody) mkTriggerRequest pusher chans event dat socketId time = do     when       (length chans > 10)@@ -79,7 +79,7 @@   -> T.Text   -> ChannelsInfoQuery   -> Int-  -> PusherRequestParams+  -> RequestParams mkChannelsRequest pusher channelTypeFilter prefixFilter attributes time =   let     prefix = maybe "" renderChannelPrefix channelTypeFilter <> prefixFilter@@ -95,7 +95,7 @@   -> Channel   -> ChannelInfoQuery   -> Int-  -> PusherRequestParams+  -> RequestParams mkChannelRequest pusher chan attributes time =   let     params = [("info", encodeUtf8 $ toURLParam attributes)]@@ -103,7 +103,7 @@   in     mkGetRequest pusher subPath params time -mkUsersRequest :: Pusher -> Channel -> Int -> PusherRequestParams+mkUsersRequest :: Pusher -> Channel -> Int -> RequestParams mkUsersRequest pusher chan time =   let     subPath = "channels/" <> renderChannel chan <> "/users"@@ -113,29 +113,29 @@ mkGetRequest   :: Pusher   -> T.Text-  -> [(B.ByteString, B.ByteString)]+  -> RequestQueryString   -> Int-  -> PusherRequestParams+  -> RequestParams mkGetRequest pusher subPath params time =   let     (ep, fullPath) = mkEndpoint pusher subPath     qs = mkQS pusher "GET" fullPath params "" time   in-    PusherRequestParams ep qs+    RequestParams ep qs  mkPostRequest   :: Pusher   -> T.Text-  -> [(B.ByteString, B.ByteString)]+  -> RequestQueryString   -> B.ByteString   -> Int-  -> PusherRequestParams+  -> RequestParams mkPostRequest pusher subPath params bodyBS time =   let     (ep, fullPath) = mkEndpoint pusher subPath     qs = mkQS pusher "POST" fullPath params bodyBS time   in-    PusherRequestParams ep qs+    RequestParams ep qs  -- |Build a full endpoint from the details in Pusher and the subPath. mkEndpoint@@ -153,10 +153,10 @@   :: Pusher   -> T.Text   -> T.Text-  -> [(B.ByteString, B.ByteString)]+  -> RequestQueryString   -> B.ByteString   -> Int-  -> [(B.ByteString, B.ByteString)]+  -> RequestQueryString mkQS pusher =   let     credentials = pusherCredentials pusher
src/Network/Pusher/Internal/Auth.hs view
@@ -13,7 +13,9 @@ users; these functions are re-exported in the main Pusher module. -} module Network.Pusher.Internal.Auth-  ( authenticatePresence+  ( AuthString+  , AuthSignature+  , authenticatePresence   , authenticatePresenceWithEncoder   , authenticatePrivate, makeQS   ) where@@ -22,29 +24,40 @@ import Data.Text.Encoding (encodeUtf8) import GHC.Exts (sortWith) import qualified Data.Aeson as A+import qualified Data.Aeson.Encode as A import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Base16 as B16 import qualified Data.ByteString.Lazy as BL 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 (Credentials(..))+import Network.Pusher.Data+  ( AppKey+  , AppSecret+  , Channel+  , Credentials(..)+  , SocketID+  , renderChannel+  )+import Network.Pusher.Internal.HTTP (RequestQueryString) import Network.Pusher.Internal.Util (show')  -- |Generate the required query string parameters required to send API requests -- to Pusher. makeQS-  :: B.ByteString-  -> B.ByteString+  :: AppKey+  -> AppSecret   -> T.Text   -> T.Text-  -> [(B.ByteString, B.ByteString)] -- ^Any additional parameters+  -> RequestQueryString -- ^Any additional parameters   -> B.ByteString   -> Int -- ^Current UNIX timestamp-  -> [(B.ByteString, B.ByteString)]+  -> RequestQueryString makeQS appKey appSecret method path params body ts =   let     -- Generate all required parameters and add them to the list of existing ones@@ -65,24 +78,34 @@     ("auth_signature", authSig) : allParams  -- |Render key-value tuple mapping of query string parameters into a string.-formQueryString :: [(B.ByteString, B.ByteString)] -> B.ByteString+formQueryString :: RequestQueryString -> B.ByteString 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++-- | A Pusher auth signature.+type AuthSignature = B.ByteString+ -- |Create a Pusher auth signature of a string using the provided credentials.-authSignature :: B.ByteString -> B.ByteString -> B.ByteString+authSignature :: AppSecret -> AuthString -> AuthSignature 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 -> B.ByteString -> B.ByteString -> B.ByteString-authenticatePrivate cred socketID channelName =+  :: Credentials+  -> SocketID+  -> Channel+  -> AuthSignature+authenticatePrivate cred socketID channel =   let     sig = authSignature       (credentialsAppSecret cred)-      (socketID <> ":" <> channelName)+      (encodeUtf8 $ socketID <> ":" <> renderChannel channel)   in     credentialsAppKey cred <> ":" <> sig @@ -90,27 +113,30 @@ -- presence channel. authenticatePresence   :: A.ToJSON a-  => Credentials -> B.ByteString -> B.ByteString -> a -> B.ByteString+  => Credentials+  -> SocketID+  -> Channel+  -> a+  -> AuthSignature authenticatePresence =-  authenticatePresenceWithEncoder (BL.toStrict . A.encode)+  authenticatePresenceWithEncoder+    (TL.toStrict . TL.toLazyText . A.encodeToTextBuilder . A.toJSON)  -- |As above, but allows the encoder of the user data to be specified. This is -- useful for testing because the encoder can be mocked; aeson's encoder enodes -- JSON object fields in arbitrary orders, which makes it impossible to test. authenticatePresenceWithEncoder   :: A.ToJSON a-  => (a -> B.ByteString) -- ^The encoder of the user data.+  => (a -> T.Text) -- ^The encoder of the user data.   -> Credentials-  -> B.ByteString-  -> B.ByteString+  -> SocketID+  -> Channel   -> a-  -> B.ByteString-authenticatePresenceWithEncoder userEncoder cred socketID channelName userData =+  -> AuthSignature+authenticatePresenceWithEncoder userEncoder cred socketID channel userData =   let-    sig = authSignature (credentialsAppSecret cred)-      ( socketID <> ":"-      <> channelName <> ":"-      <> userEncoder userData-      )+    authString =+      encodeUtf8 $ socketID <> ":" <> renderChannel channel <> ":" <> userEncoder userData+    sig = authSignature (credentialsAppSecret cred) authString   in     credentialsAppKey cred <> ":" <> sig
src/Network/Pusher/Internal/HTTP.hs view
@@ -13,7 +13,13 @@ values to the typclasses we are using in this library. Non 200 responses are converted into MonadError errors. -}-module Network.Pusher.Internal.HTTP (get, post) where+module Network.Pusher.Internal.HTTP+  ( RequestParams(..)+  , RequestQueryString+  , RequestBody+  , get+  , post+  ) where  import Control.Arrow (second) import Control.Exception (displayException)@@ -21,20 +27,6 @@ import Control.Monad.Trans.Except (ExceptT(ExceptT), throwE) import Data.Monoid ((<>)) import Data.Text.Encoding (decodeUtf8')-import Network.HTTP.Client-  ( Manager-  , Request-  , RequestBody(RequestBodyLBS)-  , Response-  , httpLbs-  , method-  , parseUrl-  , requestBody-  , requestHeaders-  , responseBody-  , responseStatus-  , setQueryString-  ) import Network.HTTP.Types.Header (hContentType) import Network.HTTP.Types.Method (methodPost) import Network.HTTP.Types.Status (statusCode, statusMessage)@@ -43,20 +35,31 @@ import qualified Data.ByteString.Char8 as BC import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T+import qualified Network.HTTP.Client as HTTP.Client  import Network.Pusher.Error (PusherError(..))-import Network.Pusher.Internal (PusherRequestParams(PusherRequestParams)) import Network.Pusher.Internal.Util (show') +data RequestParams = RequestParams+  { requestEndpoint :: T.Text+  -- ^The API endpoint, for example http://api.pusherapp.com/apps/123/events+  , requestQueryString :: RequestQueryString+  -- ^List of query string parameters as key-value tuples+  }++type RequestQueryString = [(B.ByteString, B.ByteString)]++type RequestBody = A.Value+ -- |Issue an HTTP GET request. On a 200 response, the response body is returned. -- On failure, an error will be thrown into the MonadError instance. get   :: A.FromJSON a-  => Manager-  -> PusherRequestParams+  => HTTP.Client.Manager+  -> RequestParams   -> ExceptT PusherError IO a   -- ^The body of the response-get connManager (PusherRequestParams ep qs) = do+get connManager (RequestParams ep qs) = do   req <- ExceptT $ return $ mkRequest ep qs   resp <- doReqest connManager req   either@@ -67,38 +70,41 @@ -- |Issue an HTTP POST request. post   :: A.ToJSON a-  => Manager-  -> PusherRequestParams+  => HTTP.Client.Manager+  -> RequestParams   -> a   -> ExceptT PusherError IO ()-post connManager (PusherRequestParams ep qs) body = do+post connManager (RequestParams ep qs) body = do   req <- ExceptT $ return $ mkPost (A.encode body) <$> mkRequest ep qs   _ <- doReqest connManager req   return ()  mkRequest   :: T.Text-  -> [(B.ByteString, B.ByteString)]-  -> Either PusherError Request+  -> RequestQueryString+  -> Either PusherError HTTP.Client.Request mkRequest ep qs =-  case parseUrl $ T.unpack ep of+  case HTTP.Client.parseRequest $ T.unpack ep of     Nothing -> Left $ PusherArgumentError $ "failed to parse url: " <> ep-    Just req -> Right $ setQueryString (map (second Just) qs) req+    Just req -> Right $ HTTP.Client.setQueryString (map (second Just) qs) req -mkPost :: BL.ByteString -> Request -> Request+mkPost :: BL.ByteString -> HTTP.Client.Request -> HTTP.Client.Request mkPost body req =   req-    { method = methodPost-    , requestHeaders = [(hContentType, "application/json")]-    , requestBody = RequestBodyLBS body+    { HTTP.Client.method = methodPost+    , HTTP.Client.requestHeaders = [(hContentType, "application/json")]+    , HTTP.Client.requestBody = HTTP.Client.RequestBodyLBS body     } -doReqest :: Manager -> Request -> ExceptT PusherError IO BL.ByteString+doReqest+  :: HTTP.Client.Manager+  -> HTTP.Client.Request+  -> ExceptT PusherError IO BL.ByteString doReqest connManager req = do-  response <- liftIO $ httpLbs req connManager-  let status = responseStatus response+  response <- liftIO $ HTTP.Client.httpLbs req connManager+  let status = HTTP.Client.responseStatus response   if statusCode status == 200 then-    return $ responseBody response+    return $ HTTP.Client.responseBody response   else     let decoded = decodeUtf8' $ statusMessage status in     throwE $
src/Network/Pusher/Protocol.hs view
@@ -15,20 +15,16 @@ There are also types for query string parameters. -} module Network.Pusher.Protocol-  ( Channel(..)-  , ChannelInfo(..)+  ( ChannelInfo(..)   , ChannelInfoAttributes(..)   , ChannelInfoQuery(..)   , ChannelsInfo(..)   , ChannelsInfoQuery(..)   , ChannelsInfoAttributes(..)-  , ChannelType(..)   , FullChannelInfo(..)   , User(..)   , Users(..)-  , parseChannel-  , renderChannel-  , renderChannelPrefix+  , ToURLParam   , toURLParam   ) where @@ -46,49 +42,7 @@ import qualified Data.Text as T  import Network.Pusher.Internal.Util (failExpectObj)---- |The possible types of Pusher channe.-data ChannelType = Public | Private | Presence deriving (Eq, Generic, Show)--instance Hashable ChannelType--instance Arbitrary ChannelType where-  arbitrary = elements [Public, Private, Presence]--renderChannelPrefix :: ChannelType -> T.Text-renderChannelPrefix Public = ""-renderChannelPrefix Private = "private-"-renderChannelPrefix Presence = "presence-"---- |The channel name (not including the channel type prefix) and its type.-data Channel = Channel-  { channelType :: ChannelType-  , channelName :: T.Text-  } deriving (Eq, Generic, Show)--instance Hashable Channel--instance Arbitrary Channel where-  arbitrary = Channel <$> arbitrary <*> (T.pack <$> arbitrary)--renderChannel :: Channel -> T.Text-renderChannel (Channel cType cName) = renderChannelPrefix cType <> cName---- |Convert string representation, e.g. private-chan into the datatype-parseChannel :: T.Text -> Channel-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-    -- 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+import Network.Pusher.Data (Channel, parseChannel)  -- |Types that can be serialised to a querystring parameter value. class ToURLParam a where
test/Auth.hs view
@@ -2,7 +2,11 @@  import Test.Hspec (Spec, describe, it, shouldBe) -import Network.Pusher (Credentials(..))+import Network.Pusher+  ( Channel(..)+  , ChannelType(Private, Presence)+  , Credentials(..)+  ) import Network.Pusher.Internal.Auth   ( authenticatePrivate   , authenticatePresenceWithEncoder@@ -29,7 +33,7 @@   describe "Auth.authenticatePrivate" $     it "works" $       -- Data from https://pusher.com/docs/auth_signatures#worked-example-      authenticatePrivate credentials "1234.1234" "private-foobar"+      authenticatePrivate credentials "1234.1234" (Channel Private "foobar")       `shouldBe` "278d425bdf160c739803:58df8b0c36d6982b82c3ecf6b4662e34fe8c25bba48f5369f135bf843651c3a4"    describe "Auth.authenticatePresenceWithEncoder" $@@ -42,7 +46,7 @@           (const userData)           credentials           "1234.1234"-          "presence-foobar"+          (Channel Presence "foobar")           ("doesn't matter" :: String)         `shouldBe` "278d425bdf160c739803:afaed3695da2ffd16931f457e338e6c9f2921fa133ce7dac49f529792be6304c" 
test/Protocol.hs view
@@ -7,16 +7,18 @@ import qualified Data.HashSet as HS import qualified Data.Text as T -import Network.Pusher.Protocol+import Network.Pusher   ( Channel(..)-  , ChannelInfo(..)-  , ChannelsInfo(..)   , ChannelType(Public, Presence, Private)+  , parseChannel+  , renderChannel+  )+import Network.Pusher.Protocol+  ( ChannelInfo(..)+  , ChannelsInfo(..)   , FullChannelInfo(..)   , User(..)   , Users(..)-  , parseChannel-  , renderChannel   )  test :: Spec