pushover (empty) → 0.1.0.0
raw patch · 15 files changed
+1120/−0 lines, 15 filesdep +aesondep +basedep +bytestringsetup-changed
Dependencies added: aeson, base, bytestring, exceptions, http-client, http-client-tls, mtl, pushover, tasty, tasty-hunit, text, time, transformers, uri-encode
Files
- LICENSE +30/−0
- README.md +48/−0
- Setup.hs +2/−0
- pushover.cabal +60/−0
- src/Lib.hs +6/−0
- src/Network/Pushover.hs +64/−0
- src/Network/Pushover/Exceptions.hs +43/−0
- src/Network/Pushover/Execute.hs +121/−0
- src/Network/Pushover/Message.hs +217/−0
- src/Network/Pushover/Reader.hs +62/−0
- src/Network/Pushover/Request.hs +215/−0
- src/Network/Pushover/Response.hs +51/−0
- src/Network/Pushover/Token.hs +118/−0
- test/Network/Pushover/RequestTest.hs +73/−0
- test/Spec.hs +10/−0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright Dan Meakin (c) 2017++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Dan Meakin nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,48 @@+# Pushover++This is a small library for interacting with the Pushover API from within+Haskell.++Pushover exposes a straightforward API for sending notifications to users of+the Android and iOS Pushover app. Details of the API can be found at +https://pushover.net/api.++## Usage++This library exposes a number of types which represent a `Request` and a+`Response`, and fields contained within these values. It provides functions+which can be used to create and make requests.++### Basic++A basic request can be made as follows:-++```+>>> apiKey = makeTokenOrError "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"+>>> userKey = makeTokenOrError "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"+>>> sendMessage apiK usrK $ text "This is a test"+```++Assuming the keys are correct, this should immediately send the notification+to the user with `userKey`.++The `makeTokenOrError` function shown here should not generally be used. +Instead, the `makeToken` function checks the validity of the token and wraps+the output in Either.++### Reader-based++A more useful approach is to integrate Pushover into your existing Monad stack.+This can be accomplished using the `createKeys` and `sendMessageM` functions:-++```+>>> keys = createKeys "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"+>>> keys >>= runReaderT (sendMessageM $ text "This is a test")+```++The Monad-based functions (all with a trailing `M` in their name) require that+a request is executed within a stack including an instance of MonadError and+MonadIO. `sendMessageM` additionally requires a MonadReader and an instance of+`PushoverReader`. See +[the Reader module](https://github.com/DanMeakin/pushover/blob/master/src/Network/Pushover/Reader.hs)+for more details.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ pushover.cabal view
@@ -0,0 +1,60 @@+name: pushover+version: 0.1.0.0+synopsis: A Haskell Pushover API library+description: This package provides functionality to allow Haskell+ developers to interact with the Pushover API + (https://pushover.net).+homepage: https://github.com/DanMeakin/pushover#readme+license: BSD3+license-file: LICENSE+author: Dan Meakin+maintainer: dan@danmeakin.com+copyright: 2017 Dan Meakin+category: Web+build-type: Simple+extra-source-files: README.md+cabal-version: >=1.10++library+ hs-source-dirs: src+ exposed-modules: Lib+ , Network.Pushover+ , Network.Pushover.Exceptions+ , Network.Pushover.Execute+ , Network.Pushover.Message+ , Network.Pushover.Reader+ , Network.Pushover.Request+ , Network.Pushover.Response+ , Network.Pushover.Token+ build-depends: base >= 4.7 && < 5+ , aeson+ , bytestring+ , exceptions+ , http-client+ , http-client-tls+ , mtl+ , text+ , time+ , transformers+ , uri-encode+ default-language: Haskell2010++test-suite pushover-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ other-modules: Network.Pushover.RequestTest+ build-depends: base+ , bytestring+ , http-client+ , pushover+ , tasty+ , tasty-hunit+ , text+ , time+ ghc-options: -threaded -rtsopts -with-rtsopts=-N+ default-language: Haskell2010++source-repository head+ type: git+ location: https://github.com/DanMeakin/pushover
+ src/Lib.hs view
@@ -0,0 +1,6 @@+module Lib+ ( someFunc+ ) where++someFunc :: IO ()+someFunc = putStrLn "someFunc"
+ src/Network/Pushover.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE OverloadedStrings #-}+{-| This library provides functionality for interacting with the Pushover API+(@https://www.pushover.net@) from within a Haskell codebase.++Pushover exposes a straightforward API for sending notifications to users of+the Android and iOS Pushover app. Details of the API can be found at+@https://pushover.net/api@.++Requests are defined by the 'Request' type. The 'defaultRequest' function is+provided to allow for the easy creation of simple requests. Each request+requires an API token, user token and a message to be passed. All other+fields are optional and can be set as required.++-}+module Network.Pushover+ ( -- * Sending a notification+ -- ** Regular functions+ sendMessage+ , sendRequest+ , createRequest+ -- ** Monadic functions+ , sendMessageM+ , sendRequestM+ , createRequestM+ -- * Requests+ -- ** Constructing a request+ , Request+ , defaultRequest+ -- ** Constructing a request's message+ , Message+ , message+ , bold+ , italic+ , underline+ , color+ , link+ , text+ -- ** Authenticating a request+ , APIToken+ , UserKey+ , PushoverToken+ , makeToken+ -- ** Other request fields+ , URL (..)+ , Priority (..)+ , NotificationSound (..)+ -- * Response+ , Response (..)+ -- * Reader+ , PushoverReader (..)+ , PushoverKeys (..)+ , createKeys+ -- * Exceptions+ , PushoverException+ , errorMessage+ ) where++import Network.Pushover.Exceptions+import Network.Pushover.Execute+import Network.Pushover.Message+import Network.Pushover.Reader+import Network.Pushover.Request+import Network.Pushover.Response+import Network.Pushover.Token
+ src/Network/Pushover/Exceptions.hs view
@@ -0,0 +1,43 @@+module Network.Pushover.Exceptions where++import Control.Monad.Catch+import Control.Monad.Error.Class+import Control.Monad.Except+import Data.Typeable++-- | Defines possible exceptions which can be thrown from execution commands. +data PushoverException+ = ResponseDecodeException+ -- ^ This exception is thrown when a response is malformed and cannot be+ -- decoded.+ | InvalidTokenCharactersException+ -- ^ This exception is thrown when a token cannot be constructed because it+ -- contains invalid characters.+ | InvalidTokenLengthException+ -- ^ This exception is thrown when a token cannot be constructed because it+ -- is an incorrect length.+ | PushoverException String+ -- ^ This is a generic exception for other errors in Pushover.+ deriving (Show, Typeable)++instance Exception PushoverException++instance Error PushoverException where+ noMsg =+ PushoverException "Unknown exception"++ strMsg =+ PushoverException+++-- | Display a description error message for each 'PushoverException' +-- constructor.+errorMessage :: PushoverException -> String+errorMessage InvalidTokenLengthException =+ "token must be exactly 30 characters long"++errorMessage InvalidTokenCharactersException =+ "token must contain only alphanumeric characters"++errorMessage ResponseDecodeException =+ "unexpected format of Pushover response"
+ src/Network/Pushover/Execute.hs view
@@ -0,0 +1,121 @@+{-# LANGUAGE OverloadedStrings #-}+{-| This module exposes a set of functions used for constructing and submitting+requests to the Pushover API.++Each function has two versions: a monadic version and a non-monadic version.+The monadic versions are designed for use within an existing monad transformer+stack within an existing application. The non-monadic versions are designed+for standalone use.++-}+module Network.Pushover.Execute + ( -- * Regular functions+ sendMessage+ , sendRequest+ , createRequest+ -- * Monadic functions+ , sendMessageM+ , sendRequestM+ , createRequestM+ , PushoverException (..)+ ) where++import Control.Exception (throw)+import Control.Monad.Error.Class+import Control.Monad.Except+import Control.Monad.Reader+import Data.Aeson (decode)+import Network.Pushover.Message (Message)+import Network.Pushover.Request hiding (userKey)+import Network.Pushover.Response (Response)+import Network.Pushover.Reader (PushoverReader (..))+import Network.Pushover.Exceptions+import Network.Pushover.Token++import qualified Network.HTTP.Client as Http+import Network.HTTP.Client.TLS (newTlsManager)++-- | Send a request to the Pushover API.+--+-- Requires a pair of Pushover tokens, together with a 'Message' value. These+-- are used to construct a 'defaultRequest' which is then sent to the API.+sendMessage :: APIToken -> UserKey -> Message -> IO Response+sendMessage apiTk userK =+ sendRequest . defaultRequest apiTk userK++-- | Send a request to the Pushover API.+--+-- This is similar to 'sendMessage', except that a constructed 'Request' must+-- be passed instead of the tokens and message.+sendRequest :: Request -> IO Response+sendRequest =+ sendRequestM++-- | Send a request to the Pushover API.+--+-- This function is designed for use within an existing monad transformer +-- stack to make it easy to send Pushover notifications from inside existing+-- software.+--+-- The relevant tokens are read from a 'PushoverReader' MonadReader instance.+-- These are then used to construct a 'defaultRequest' containing the passed+-- 'Message' value.+sendMessageM :: ( Error e+ , MonadError e m+ , MonadIO m+ , MonadReader r m+ , PushoverReader r+ ) + => Message + -> m Response+sendMessageM message = do+ pushoverRequest <- createRequestM message+ liftIO $ sendRequest pushoverRequest+++-- | Send a request to the Pushover API.+--+-- This function is designed for use within an existing monad transformer +-- stack to make it easy to send Pushover notifications from inside existing+-- software.+--+-- This is similar to 'sendMessageM', except that a constructed 'Request' must+-- be passed instead of just the message.+sendRequestM :: ( Error e + , MonadError e m+ , MonadIO m+ ) + => Request+ -> m Response+sendRequestM pushoverRequest = do+ manager <- newTlsManager+ request <- liftIO $ makeHttpRequest pushoverRequest+ response <- liftIO $ Http.httpLbs request manager+ case decode $ Http.responseBody response of+ Just resp ->+ return resp++ Nothing ->+ throwError . strMsg $ "response decoder error"+++-- | Create a standard Pushover request.+--+-- This is an alias of the 'defaultRequest' function.+createRequest :: APIToken -> UserKey -> Message -> Request+createRequest =+ defaultRequest++-- | Create a standard Pushover request.+--+-- This is similar to 'createRequest', except that token information is read+-- from within the monad stack.+createRequestM :: ( MonadReader r m+ , PushoverReader r+ )+ => Message + -> m Request+createRequestM message = do+ apiTk <- asks apiToken+ usrK <- asks userKey+ return $ defaultRequest apiTk usrK message
+ src/Network/Pushover/Message.hs view
@@ -0,0 +1,217 @@+{-# LANGUAGE OverloadedStrings #-}+{-| This module provides functions for creating and encoding messages for use+within Pushover requests.++Pushover messages contain a very limited subset of HTML. Users can insert bold,+italic, underlined, and colored text, and URLs within messages. The 'Message'+type represents all of these possible formatting options.++Constructing a 'Message' is done through the use of the 'message', 'bold',+'italic', 'underline', 'color', 'link', and 'text' functions. The 'message'+function takes a list of parts created using the other functions, and+concatenates these into a single message. Different types of formatting can+be nested within each other by simply calling the functions on the results of+other function calls. For example:-++@+ bold + [ italic + [ text "This is bold & italic" + ]+ , text "This is bold"+ , underline+ [ text "This is bold & underlined"+ ]+ ]+@++will create a message with the text values formatted as described.++-}+module Network.Pushover.Message + ( -- * Creating a Message+ Message+ , message+ , bold+ , italic+ , underline+ , color+ , link+ , text+ -- * Encoding for inclusion in request+ , encodeMessage+ -- * Attributes+ , ColorCode+ , makeColorCode+ , Url+ ) where++import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Numeric (showHex)++-- | Represents a message sent to the Pushover API.+--+-- A Pushover message can be constructed with a very small subset of HTML.+-- This type represents the available HTML formatting for a message.+data Message+ = Parts [Message]+ | Bold [Message]+ | Italic [Message]+ | Underline [Message]+ | Color ColorCode [Message]+ | Link Url [Message]+ | MessageText Text+ deriving (Show, Eq)++-- | Represents an HTML color code.+--+-- A ColorCode consists of a red, a green and a blue element, each of which+-- must have a value of between 0 and 255. This type cannot enforce this+-- constraint, but see 'makeColorCode' which does.+data ColorCode = ColorCode+ { red :: Integer+ , green :: Integer+ , blue :: Integer+ } deriving (Show, Eq)++type Url+ = Text++-- | Make a message from a list of message parts.+message :: [Message] -> Message+message =+ Parts++-- | Make a bold message.+bold :: [Message] -> Message+bold =+ Bold++-- | Make an italic message.+italic :: [Message] -> Message+italic =+ Italic++-- | Make an underlined message.+underline :: [Message] -> Message+underline =+ Underline++-- | Make a message with colored text.+--+-- Accepts three integer arguments for red, green and blue color elements,+-- respectively.+color :: Integer -> Integer -> Integer -> [Message] -> Message+color r g b =+ Color (makeColorCode r g b)++-- | Make a url message.+link :: Url -> [Message] -> Message+link =+ Link++-- | Make a textual message.+text :: Text -> Message+text =+ MessageText++-- | Construct a 'ColorCode' value.+--+-- A 'ColorCode' requires a red, a green and a blue value for construction.+-- This function takes these as arguments and returns a constructed +-- 'ColorCode'.+--+-- This function checks that each element is within the required 0-255 range.+-- Any element which is not is rounded to the nearest extrema (0 for negative+-- values; 255 for values larger than that number).+makeColorCode :: Integer -> Integer -> Integer -> ColorCode+makeColorCode r g b =+ ColorCode+ { red = f r+ , green = f g+ , blue = f b+ }++ where+ f =+ max 0 . min 255 + +-- | Encode a 'Message' into a bytestring.+--+-- This function is intended to convert a 'Message' into a form useable within+-- a 'Request'. It generates a bytestring containing the HTML for the message.+encodeMessage :: Message -> ByteString+encodeMessage =+ enc++ where + enc msg =+ case msg of+ Parts msg ->+ encInner msg++ Bold msg ->+ wrapTag "b" "" msg++ Italic msg ->+ wrapTag "i" "" msg++ Underline msg ->+ wrapTag "u" "" msg++ Color colorCode msg ->+ wrapTag "font" (colorAttr colorCode) msg++ Link url msg ->+ wrapTag "a" (linkAttr url) msg++ MessageText txt ->+ T.encodeUtf8 txt++ encInner =+ B.concat . map enc++ wrapTag tagName attr inner =+ B.concat [ openTag tagName attr+ , encInner inner+ , closeTag tagName+ ]++ openTag tagName attr =+ B.concat [ "<"+ , tagName+ , attr+ , ">"+ ]++ closeTag tagName =+ B.concat [ "</"+ , tagName+ , ">"+ ]++ colorAttr =+ B.append " color=" . encodeColorCode++ linkAttr =+ B.append " href=" . T.encodeUtf8++-- | Encode a 'ColorCode' into a bytestring.+--+-- This converts a 'ColorCode' value into its corresponding hexadecimal +-- representation.+encodeColorCode :: ColorCode -> ByteString+encodeColorCode (ColorCode r g b) =+ B.pack $ '#':hexCode++ where + hexCode =+ padShow r . padShow g . padShow b $ ""++ padShow =+ (\hex rest -> replicate (2 - length (hex "")) '0' ++ hex rest) + <$> showHex
+ src/Network/Pushover/Reader.hs view
@@ -0,0 +1,62 @@+{-| This module exposes types intended to make it easy to incorporate Pushover+request functionality within an existing application.++The 'PushoverReader' class is designed to make it easy to extend an existing+reader environment to include the information required by Pushover in the making+of requests.++'PushoverKeys' is a simple type available for immediate use should a developer+not yet have a reader environment and wants quickly to use Pushover.+-}+module Network.Pushover.Reader where++import Control.Monad.Error.Class+import Control.Monad.Except+import Data.Text (Text)+import Network.Pushover.Exceptions+import Network.Pushover.Token++-- | The 'PushoverReader' class is intended to make it straightforward to+-- incorporate the making of Pushover requests within an existing monad+-- stack.+--+-- This class is intended to make it easy to add an API token and user key+-- to an existing reader monad environment.+class PushoverReader r where+ apiToken :: r -> APIToken+ userKey :: r -> UserKey++-- | A basic type for use in storing the pair of keys required for making+-- a request.+data PushoverKeys = PushoverKeys+ { _apiToken :: APIToken+ , _userKey :: UserKey+ } deriving Show++instance PushoverReader PushoverKeys where+ apiToken =+ _apiToken+ + userKey =+ _userKey++-- | An unvalidated API token.+type UnvalidatedAPIToken + = Text++-- | An unvalidated user key.+type UnvalidatedUserKey+ = Text++-- | Construct a 'PushoverKeys' value.+--+-- This attempts to create valid tokens/keys from a pair of unvalidated+-- tokens/keys, returning the result wrapped within a MonadError.+createKeys :: (Error e, MonadError e m)+ => UnvalidatedAPIToken + -> UnvalidatedUserKey + -> m PushoverKeys+createKeys apiTkn usrKey =+ PushoverKeys+ <$> makeTokenM apiTkn+ <*> makeTokenM usrKey
+ src/Network/Pushover/Request.hs view
@@ -0,0 +1,215 @@+{-# LANGUAGE OverloadedStrings #-}+module Network.Pushover.Request+ ( -- * Constructing a request+ Request (..)+ , defaultRequest+ -- * Other request parameters+ , URL (..)+ , Priority (..)+ , NotificationSound (..)+ -- * HTTP request helper+ , makeHttpRequest+ ) where++import Data.ByteString.Char8 (ByteString)+import qualified Data.ByteString.Char8 as B+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Data.Time (UTCTime)+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)+import qualified Network.HTTP.Client as Http+import Network.Pushover.Message (Message, encodeMessage)+import Network.Pushover.Token+import Network.URI.Encode++-- | Pushover API endpoint.+endpoint = "https://api.pushover.net/1/messages.json"++-- | Contains the contents of a Pushover notification request. This follows+-- the API specification at @https://pushover.net/api@.+data Request = Request+ { requestToken :: APIToken+ -- ^ The API token provided by your Pushover app's dashboard at+ -- @https://pushover.net/apps@.+ , requestUserKey :: UserKey+ -- ^ The user key of the user receiving this notification, found in the+ -- Pushover dashboard at @https://pushover.net/dashboard@.+ , requestMessage :: Message+ -- ^ The notification message to push to the user.+ , devices :: [Text]+ -- ^ An optional list of devices to which to send the notification. If empty,+ -- it will be sent to all of the user's devices.+ , title :: Maybe Text+ -- ^ An optional title for the message.+ , url :: Maybe URL+ -- ^ An optional URL for inclusion with the message.+ , priority :: Maybe Priority+ -- ^ The priority of this message. This affects way in which the notification+ -- is presented to the receiving user. See 'Priority' for more information.+ , timestamp :: Maybe UTCTime+ -- ^ An optional timestamp for the notification. If no timestamp is provided,+ -- the time the request is received by the Pushover API is used.+ , notificationSound :: Maybe NotificationSound+ -- ^ The notification sound to use. The default is 'Pushover', with 'None'+ -- provided for a silent notification.+ } deriving (Show, Eq)++-- | A URL for sending within a notification request.+--+-- A Pushover URL is optional within a request; if present, it may optionally+-- contain a title to display instead of the URL itself.+data URL = URL+ { urlPath :: Text+ , urlTitle :: Maybe Text+ } deriving (Show, Eq)++-- | Describes the priority of a particular message.+--+-- The different priority settings affect the way in which a notification is+-- presented to the user. See @https://pushover.net/api#priority@ for specific+-- details.+data Priority+ = Lowest+ | Low+ | Normal+ | High+ | Emergency+ deriving (Show, Eq)++-- | Describes the notification sound for a notification.+data NotificationSound+ = Pushover+ | Bike+ | Bugle+ | CashRegister+ | Classical+ | Cosmic+ | Falling+ | Gamelan+ | Incoming+ | Intermission+ | Magic+ | Mechanical+ | PianoBar+ | Siren+ | SpaceAlarm+ | TugBoat+ | AlienAlarm+ | Climb+ | Persistent+ | Echo+ | UpDown+ | None+ deriving (Show, Eq)++-- | Construct a default request value.+--+-- As a request requires, at a minimum, an API token, a user key and a+-- message, this function requires each of these values as an argument. Other+-- fields can then be initialised using the regular Haskell record syntax.+defaultRequest :: APIToken -> UserKey -> Message -> Request+defaultRequest apiToken usrKey msg =+ Request { requestToken = apiToken+ , requestUserKey = usrKey+ , requestMessage = msg+ , devices = []+ , title = Nothing+ , url = Nothing+ , priority = Nothing+ , timestamp = Nothing+ , notificationSound = Nothing+ }++-- | Construct an HTTP request out of a Pushover request value.+--+-- This function is exposed for use by the functions in the+-- "Network.Pushover.Execute" module. It is unlikely that the user will+-- require to call it directly.+makeHttpRequest :: Request -> IO Http.Request+makeHttpRequest pushoverRequest = do+ initialRequest <- Http.parseRequest endpoint+ return . Http.setQueryString (requestQueryPairs pushoverRequest)+ $ initialRequest { Http.method = "POST" }++-- | Create a set of HTTP query pairs from a 'Request'.+requestQueryPairs :: Request -> [(ByteString, Maybe ByteString)]+requestQueryPairs =+ filter present . makePairs++ where+ makePairs :: Request -> [(ByteString, Maybe ByteString)]+ makePairs request =+ (fmap . fmap) ($ request)+ [ ("token", Just . encodeToken . requestToken)+ , ("user", Just . encodeToken . requestUserKey)+ , ("message", Just . encodeMessage . requestMessage)+ , ("device", encodeValue . T.intercalate "," . devices)+ , ("title", encodeMaybe . title )+ , ("url", encodeMaybe . reqUrl)+ , ("url_title", encodeMaybe . reqUrlTitle)+ , ("priority", fmap encodePriority . priority)+ , ("timestamp", fmap encodeTimestamp . timestamp)+ , ("sound", fmap encodeSound . notificationSound)+ , ("html", const $ Just "1") -- Flag that HTML will be sent.+ ]++ where+ reqUrl req =+ urlPath <$> url req++ reqUrlTitle req =+ url req >>= urlTitle++ encodeMaybe =+ fmap T.encodeUtf8++ encodeValue =+ Just . T.encodeUtf8++ present (_, Nothing) = False+ present (_, Just _) = True++-- | Encode a timestamp into a bytestring.+--+-- Used when converting a 'Request' for sending.+encodeTimestamp :: UTCTime -> ByteString+encodeTimestamp =+ B.pack . show . round . utcTimeToPOSIXSeconds++-- | Encode a priority into a bytestring.+--+-- Used when converting a 'Request' for sending.+encodePriority :: Priority -> ByteString+encodePriority Emergency = "2"+encodePriority High = "1"+encodePriority Normal = "0"+encodePriority Low = "-1"+encodePriority Lowest = "-2"++-- | Encode a notification sound into a bytestring.+--+-- Used when converting a 'Request' for sending.+encodeSound :: NotificationSound -> ByteString+encodeSound Pushover = "pushover"+encodeSound Bike = "bike"+encodeSound Bugle = "bugle"+encodeSound CashRegister = "cashregister"+encodeSound Classical = "classical"+encodeSound Cosmic = "cosmic"+encodeSound Falling = "falling"+encodeSound Gamelan = "gamelan"+encodeSound Incoming = "incoming"+encodeSound Intermission = "intermission"+encodeSound Magic = "magic"+encodeSound Mechanical = "mechanical"+encodeSound PianoBar = "pianobar"+encodeSound Siren = "siren"+encodeSound SpaceAlarm = "spacealarm"+encodeSound TugBoat = "tugboat"+encodeSound AlienAlarm = "alienalarm"+encodeSound Climb = "climb"+encodeSound Persistent = "persistent"+encodeSound Echo = "echo"+encodeSound UpDown = "updown"+encodeSound None = "none"
+ src/Network/Pushover/Response.hs view
@@ -0,0 +1,51 @@+{-# LANGUAGE OverloadedStrings #-}+module Network.Pushover.Response where++import Data.Aeson hiding (Success)+import Data.Text (Text)+import qualified Data.Text as T++-- | Describes a response received to a notification request. This follows the+-- specification at @https://pushover.net/api#response@.+--+-- A request will either be successful or it will be unsuccessful. Where it is+-- successful, an appropriate status indicator is returned. Where unsuccessful,+-- the response will contain an errors key with a list of errors within the +-- request.+--+-- In both cases, a request parameter is returned which uniquely identifies the+-- request leading to the response.+data Response = Response+ { status :: ResponseStatus+ , request :: Text+ } deriving Show++instance FromJSON Response where+ parseJSON (Object v) =+ Response+ <$> parseJSON (Object v)+ <*> v .: "request"++-- | Describes a Pushover response status.+--+-- A request is either successful or unsuccessful. This is reflected in this+-- type. Success has no additional data associated with it; failure has a list+-- of errors associated.+data ResponseStatus + = Success+ | Failure [RequestError]+ deriving Show++instance FromJSON ResponseStatus where+ parseJSON (Object v) = do+ status <- v .: "status"+ case (status :: Integer) of+ 1 ->+ pure Success++ 0 ->+ Failure <$> v .: "errors"+ +type RequestError =+ Text+
+ src/Network/Pushover/Token.hs view
@@ -0,0 +1,118 @@+{-# LANGUAGE OverloadedStrings #-}+{-| This module contains functionality and types concerning tokens used to+authenticate and direct communications with the Pushover API.++The API requires that an API token be sent with every request for +authentication purposes, and a user key be sent with every request for the+purpose of identifying the recipient of the message. Both types of token/key+are of the same format, and the 'makeToken' functions work for constructing+both types of token/key.+-}+module Network.Pushover.Token + ( -- * Token type+ PushoverToken+ -- ** Aliases+ , APIToken+ , UserKey+ -- * Constructing a token+ , makeToken+ , makeTokenM+ , makeTokenOrError+ -- * Encoding for use in HTTP request+ , encodeToken+ ) where++import Control.Arrow (left)+import Control.Monad (unless)+import Control.Monad.Error.Class+import Control.Monad.Except+import Data.Aeson+import Data.ByteString.Char8 (ByteString)+import Data.Char (isAlphaNum)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Encoding as T+import Network.Pushover.Exceptions++-- | Define a type to represent the different types of token or key Pushover+-- requires.+--+-- Pushover requires API token and user keys to be send with requests. This is+-- intended to represent these tokens. It is intended that the 'makeToken'+-- function is used to construct validated tokens.+newtype PushoverToken = PushoverToken+ { getToken :: Text+ } deriving (Show, Eq)++instance ToJSON PushoverToken where+ toJSON =+ toJSON . getToken++instance FromJSON PushoverToken where+ parseJSON (String tkn) =+ case makeToken tkn of+ Right tok ->+ return tok++ Left err ->+ fail $ errorMessage err++ -- | API token for making a Pushover request.+type APIToken + = PushoverToken++-- | User key for the user receiving a notification.+type UserKey+ = PushoverToken++-- | Construct a 'PushoverToken' value.+--+-- A 'PushoverToken' consists of exactly 30 alphanumeric characters (both+-- uppercase and lowercase). The input key text is validated to ensure it is+-- the correct length and contains valid characters. +--+-- A descriptive error is returned where validation fails.+makeToken :: Text -> Either PushoverException PushoverToken+makeToken =+ makeTokenM++-- | Construct a 'PushoverToken' value.+--+-- This is similar to the 'makeToken' function, except that it is generalised+-- over the 'MonadError' monad.+makeTokenM :: (Error e, MonadError e m) + => Text + -> m PushoverToken+makeTokenM tokenText = do+ unless validateLength $+ throwError . strMsg $ errorMessage InvalidTokenLengthException+ unless validateChars $+ throwError . strMsg $ errorMessage InvalidTokenCharactersException+ return $ PushoverToken tokenText++ where + validateLength =+ T.length tokenText == 30++ validateChars =+ T.all isAlphaNum tokenText++-- | Construct a 'PushoverToken' value.+--+-- This is a version of 'makeToken' in which an invalid token will raise an+-- error. It should generally not be used, with 'makeToken' the preferred means+-- to create a token.+makeTokenOrError :: Text -> PushoverToken+makeTokenOrError tokenText =+ case makeToken tokenText of+ Right tkn ->+ tkn++ Left err ->+ error $ errorMessage err++-- | Encode a 'PushoverToken' into a bytestring for sending within an HTTP+-- request.+encodeToken :: PushoverToken -> ByteString+encodeToken =+ T.encodeUtf8 . getToken
+ test/Network/Pushover/RequestTest.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE OverloadedStrings #-}+module Network.Pushover.RequestTest where++import Data.ByteString.Char8 (ByteString)+import Network.HTTP.Client hiding (defaultRequest)+import Network.Pushover.Message (text)+import Network.Pushover.Request+import Network.Pushover.Token+import Test.Tasty+import Test.Tasty.HUnit++unitTests = testGroup "Unit tests"+ [ testCase "Default request initialises only passed fields" testDefaultRequest+ , testCase "Correct query string encoded from request" testQueryStrings+ ]++testRequest = Request+ { requestToken = makeTokenOrError "KzGDORePKggMaC0QOYAMyEEuzJnyUi"+ , requestUserKey = makeTokenOrError "e9e1495ec75826de5983cd1abc8031"+ , requestMessage = text "Backup of database \"example\" finished in 16 minutes."+ , devices = ["droid4"]+ , title = Just "Backup finished - SQL1"+ , url = Nothing+ , priority = Nothing+ , timestamp = Nothing+ , notificationSound = Nothing+ }++--------------------------------------------------------------------------------++testDefaultRequest =+ let tkn =+ makeTokenOrError "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"++ usrK =+ makeTokenOrError "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"++ msg =+ text "Msg"+ + in+ actualDefaultRequest tkn usrK msg @?= expectedDefaultRequest tkn usrK msg++expectedDefaultRequest tkn usrK msg = Request+ { requestToken = tkn+ , requestUserKey = usrK+ , requestMessage = msg+ , devices = []+ , title = Nothing+ , url = Nothing+ , priority = Nothing+ , timestamp = Nothing+ , notificationSound = Nothing+ }++actualDefaultRequest =+ defaultRequest++--------------------------------------------------------------------------------++testQueryStrings =+ actualQueryString >>= (@?= expectedQueryString)++actualQueryString =+ fmap queryString . makeHttpRequest $ testRequest++expectedQueryString =+ "?token=KzGDORePKggMaC0QOYAMyEEuzJnyUi&\+ \user=e9e1495ec75826de5983cd1abc8031&\+ \message=Backup%20of%20database%20%22example%22%20finished%20in%2016%20minutes.&\+ \device=droid4&\+ \title=Backup%20finished%20-%20SQL1&\+ \html=1"
+ test/Spec.hs view
@@ -0,0 +1,10 @@+import Network.Pushover.RequestTest++import Test.Tasty++main :: IO ()+main = defaultMain tests++tests :: TestTree+tests =+ testGroup "Tests" [unitTests]