push-notify-apn 0.1.0.8 → 0.1.1.0
raw patch · 4 files changed
+108/−65 lines, 4 filesdep +hspecdep +semigroupsPVP ok
version bump matches the API change (PVP)
Dependencies added: hspec, semigroups
API changes (from Hackage documentation)
+ Network.PushNotify.APN: ApnMessageResultBadDeviceToken :: ApnMessageResult
+ Network.PushNotify.APN: bodyMessage :: Text -> JsonApsMessage
+ Network.PushNotify.APN: setMessageBody :: Text -> JsonApsMessage -> JsonApsMessage
Files
- changelog.md +10/−0
- push-notify-apn.cabal +5/−1
- src/Network/PushNotify/APN.hs +92/−62
- test/Spec.hs +1/−2
changelog.md view
@@ -1,3 +1,13 @@+0.1.1.0+=======++- Let the client send alerts with no title++0.1.0.9+=======++- Add "semigroups" as a dependency for older ghc versions+ 0.1.0.8 =======
push-notify-apn.cabal view
@@ -1,5 +1,5 @@ name: push-notify-apn-version: 0.1.0.8+version: 0.1.1.0 synopsis: Send push notifications to mobile iOS devices description: push-notify-apn is a library and command line utility that can be used to send@@ -32,6 +32,7 @@ , http2 , http2-client , random+ , semigroups , text , time , tls@@ -48,6 +49,7 @@ , push-notify-apn , bytestring , optparse-applicative+ , semigroups , text default-language: Haskell2010 @@ -57,6 +59,8 @@ main-is: Spec.hs build-depends: base , push-notify-apn+ , aeson+ , hspec ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010
src/Network/PushNotify/APN.hs view
@@ -7,8 +7,9 @@ -- Portability: portable -- -- Send push notifications using Apple's HTTP2 APN API+{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE TupleSections #-} module Network.PushNotify.APN ( newSession@@ -20,8 +21,10 @@ , sendSilentMessage , sendRawMessage , alertMessage+ , bodyMessage , emptyMessage , setAlertMessage+ , setMessageBody , setBadge , setCategory , setSound@@ -39,60 +42,60 @@ , ApnToken ) where -import Control.Concurrent-import Control.Concurrent.QSem-import Control.Exception-import Control.Monad-import Data.Aeson-import Data.Aeson.Types-import Data.ByteString (ByteString)-import Data.Char (toLower)-import Data.Default (def)-import Data.Either-import Data.Int-import Data.IORef-import Data.Map.Strict (Map)-import Data.Maybe-import Data.Text (Text)-import Data.Time.Clock.POSIX-import Data.X509-import Data.X509.CertificateStore-import GHC.Generics-import Network.HTTP2.Client-import Network.HTTP2.Client.FrameConnection-import Network.HTTP2.Client.Helpers-import Network.TLS hiding (sendData)-import Network.TLS.Extra.Cipher-import System.IO.Error-import System.Mem.Weak-import System.Random+import Control.Concurrent+import Control.Concurrent.QSem+import Control.Exception+import Control.Monad+import Data.Aeson+import Data.Aeson.Types+import Data.ByteString (ByteString)+import Data.Char (toLower)+import Data.Default (def)+import Data.Either+import Data.Int+import Data.IORef+import Data.Map.Strict (Map)+import Data.Maybe+import Data.Text (Text)+import Data.Time.Clock.POSIX+import Data.X509+import Data.X509.CertificateStore+import GHC.Generics+import Network.HTTP2.Client+import Network.HTTP2.Client.FrameConnection+import Network.HTTP2.Client.Helpers+import Network.TLS hiding (sendData)+import Network.TLS.Extra.Cipher+import System.IO.Error+import System.Mem.Weak+import System.Random -import qualified Data.ByteString as S-import qualified Data.ByteString.Base16 as B16-import qualified Data.ByteString.Lazy as L-import qualified Data.List as DL-import qualified Data.Map.Strict as M-import qualified Data.Text as T-import qualified Data.Text.Encoding as TE+import qualified Data.ByteString as S+import qualified Data.ByteString.Base16 as B16+import qualified Data.ByteString.Lazy as L+import qualified Data.List as DL+import qualified Data.Map.Strict as M+import qualified Data.Text as T+import qualified Data.Text.Encoding as TE -import qualified Network.HTTP2 as HTTP2-import qualified Network.HPACK as HTTP2+import qualified Network.HPACK as HTTP2+import qualified Network.HTTP2 as HTTP2 -- | A session that manages connections to Apple's push notification service data ApnSession = ApnSession- { apnSessionPool :: !(IORef [ApnConnection])- , apnSessionConnectionInfo :: !ApnConnectionInfo- , apnSessionConnectionManager :: !ThreadId- , apnSessionOpen :: !(IORef Bool)}+ { apnSessionPool :: !(IORef [ApnConnection])+ , apnSessionConnectionInfo :: !ApnConnectionInfo+ , apnSessionConnectionManager :: !ThreadId+ , apnSessionOpen :: !(IORef Bool)} -- | Information about an APN connection data ApnConnectionInfo = ApnConnectionInfo- { aciCertPath :: !FilePath- , aciCertKey :: !FilePath- , aciCaPath :: !FilePath- , aciHostname :: !Text- , aciMaxConcurrentStreams :: !Int- , aciTopic :: !ByteString }+ { aciCertPath :: !FilePath+ , aciCertKey :: !FilePath+ , aciCaPath :: !FilePath+ , aciHostname :: !Text+ , aciMaxConcurrentStreams :: !Int+ , aciTopic :: !ByteString } -- | A connection to an APN API server data ApnConnection = ApnConnection@@ -128,6 +131,7 @@ -- | The result of a send request data ApnMessageResult = ApnMessageResultOk | ApnMessageResultFatalError+ | ApnMessageResultBadDeviceToken | ApnMessageResultTemporaryError | ApnMessageResultTokenNoLongerValid deriving (Enum, Eq, Show)@@ -137,29 +141,31 @@ -- | The specification of a push notification's message body data JsonApsAlert = JsonApsAlert- { jaaTitle :: !Text+ { jaaTitle :: !(Maybe Text) -- ^ A short string describing the purpose of the notification.- , jaaBody :: !Text+ , jaaBody :: !Text -- ^ The text of the alert message. } deriving (Generic, Show) instance ToJSON JsonApsAlert where toJSON = genericToJSON defaultOptions- { fieldLabelModifier = drop 3 . map toLower }+ { fieldLabelModifier = drop 3 . map toLower+ , omitNothingFields = True+ } -- | Push notification message's content data JsonApsMessage -- | Push notification message's content = JsonApsMessage- { jamAlert :: !(Maybe JsonApsAlert)+ { jamAlert :: !(Maybe JsonApsAlert) -- ^ A text to display in the notification- , jamBadge :: !(Maybe Int)+ , jamBadge :: !(Maybe Int) -- ^ A number to display next to the app's icon. If set to (Just 0), the number is removed.- , jamSound :: !(Maybe Text)+ , jamSound :: !(Maybe Text) -- ^ A sound to play, that's located in the Library/Sounds directory of the app -- This should be the name of a sound file in the application's main bundle, or -- in the Library/Sounds directory of the app.- , jamCategory :: !(Maybe Text)+ , jamCategory :: !(Maybe Text) -- ^ The category of the notification. Must be registered by the app beforehand. } deriving (Generic, Show) @@ -231,6 +237,14 @@ -- ^ The modified message alertMessage title text = setAlertMessage title text emptyMessage +-- | Create a new APN message with a body and no title+bodyMessage+ :: Text+ -- ^ The body of the message+ -> JsonApsMessage+ -- ^ The modified message+bodyMessage text = setMessageBody text emptyMessage+ -- | Set the alert part of an APN message setAlertMessage :: Text@@ -243,8 +257,22 @@ -- ^ The modified message setAlertMessage title text a = a { jamAlert = Just jam } where- jam = JsonApsAlert title text+ jam = JsonApsAlert (Just title) text +-- | Set the body of an APN message without affecting the title+setMessageBody+ :: Text+ -- ^ The body of the message+ -> JsonApsMessage+ -- ^ The message to alter+ -> JsonApsMessage+ -- ^ The modified message+setMessageBody text a = a { jamAlert = Just newJaa }+ where+ newJaa = case jamAlert a of+ Nothing -> JsonApsAlert Nothing text+ Just jaa -> jaa { jaaBody = text }+ -- | Remove the alert part of an APN message clearAlertMessage :: JsonApsMessage@@ -261,9 +289,9 @@ data JsonAps -- | A push notification message = JsonAps- { jaAps :: !JsonApsMessage+ { jaAps :: !JsonApsMessage -- ^ The main content of the message- , jaAppSpecificContent :: !(Maybe Text)+ , jaAppSpecificContent :: !(Maybe Text) -- ^ Extra information to be used by the receiving app } deriving (Generic, Show) @@ -334,11 +362,11 @@ -- to call this function. closeSession :: ApnSession -> IO () closeSession s = do- isOpen <- atomicModifyIORef' (apnSessionOpen s) (\a -> (False, a))+ isOpen <- atomicModifyIORef' (apnSessionOpen s) (False,) unless isOpen $ error "Session is already closed" killThread (apnSessionConnectionManager s) let ioref = apnSessionPool s- openConnections <- atomicModifyIORef' ioref (\conns -> ([], conns))+ openConnections <- atomicModifyIORef' ioref ([],) mapM_ closeApnConnection openConnections -- | Check whether a session is open or has been closed@@ -529,21 +557,23 @@ let pph hStreamId hStream hHeaders hIfc hOfc = print hHeaders response <- waitStream stream isfc pph- let (errOrHeaders, _, _) = response+ let (errOrHeaders, frameResponses, _) = response case errOrHeaders of Left err -> return ApnMessageResultTemporaryError Right hdrs1 -> do let Just status = DL.lookup ":status" hdrs1 return $ case status of "200" -> ApnMessageResultOk- "400" -> ApnMessageResultFatalError+ "400" -> if Right "{\"reason\":\"BadDeviceToken\"}" `DL.elem` frameResponses+ then ApnMessageResultBadDeviceToken+ else ApnMessageResultFatalError "403" -> ApnMessageResultFatalError "405" -> ApnMessageResultFatalError "410" -> ApnMessageResultTokenNoLongerValid "413" -> ApnMessageResultFatalError "429" -> ApnMessageResultTemporaryError "500" -> ApnMessageResultTemporaryError- "503" -> ApnMessageResultTemporaryError + "503" -> ApnMessageResultTemporaryError in StreamDefinition init handler case res of Left _ -> return ApnMessageResultTemporaryError -- Too much concurrency
test/Spec.hs view
@@ -1,2 +1,1 @@-main :: IO ()-main = putStrLn "Test suite not yet implemented"+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}