packages feed

push-notify-general (empty) → 0.1.0.0

raw patch · 8 files changed

+588/−0 lines, 8 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, containers, data-default, hashable, http-conduit, http-types, push-notify, push-notify-ccs, text, unordered-containers, xml-conduit, yesod

Files

+ LICENSE view
@@ -0,0 +1,23 @@+Copyright (c) 2013 Marcos Pividori++Permission is hereby granted, free of charge, to any+person obtaining a copy of this software and associated+documentation files (the "Software"), to deal in the+Software without restriction, including without limitation+the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the+Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice+shall be included in all copies or substantial portions of+the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY+KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE+WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR+PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS+OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR+OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Network/PushNotify/General.hs view
@@ -0,0 +1,116 @@+module Network.PushNotify.General+    (+    -- * The purpose of this library+    -- $purpose+    +    -- * How to use this library+    -- $explanation++    -- * PushManager Subsite+    -- $subsite+   +    -- * Push Service+      startPushService+    , closePushService+    , sendPush+    , withPushManager+    -- * Push Manager+    , PushManager+    -- * Push Settings+    , Device(..)+    , PushServiceConfig(..)+    , RegisterResult(..)+    , PushConfig(..)+    , GCMConfig(..)+    -- * Push Message+    , PushNotification(..)+    , generalNotif+    -- * Push Result+    , PushResult(..)+    ) where++import Network.PushNotify.General.Send+import Network.PushNotify.General.Types+import Network.PushNotify.General.YesodPushApp++-- $purpose+-- This library defines a general API for communicating with iOS,+-- Android and WPhone powered devices, sending/receiving Push Notifications.+--+-- The main idea is to hide as much as possible the differences between the different services.+--+-- Push Notification services in general, are very similar. They all are asynchronous, best-effort services+-- that offer third-party developers a channel to send data to apps from a cloud service in a power-efficient manner.+-- The main differences between them, refer to details as: the maxim payload length, the quality of service,+-- queueing the messages or not, and the time limit for this, the way the messages are handled on devices, etc.+--+-- The registration of devices is very similar too. The device/app gets a unique ID provided by the different services,+-- and sends this identifier to your 3rd party server.+-- So then, when you want to send messages to these devices, you have to send them to the Push Servers, and+-- they will deliver the messages to the proper devices.+--+-- So, 'Device' will be our general identifier.+--++-- $explanation+-- All the communication with devices will be abstracted behind the 'PushManager' data type.+--+-- You will reference to it when intending to send notifications. Also, if you properly set the callback functions,+-- you will be able to receive messages from devices.+--+-- So, the mains steps:+--+-- * First you establish the main configuration for the push service: 'PushServiceConfig' . This means, set the configurations for the+-- different Push services you want to use, and also set the callbacks functions. These functions could be used to actualize a DB,+-- do some processing in background, etc.+--+-- * Then you start the service with the 'startPushService' function and you get the 'PushManager'.+--     So, you can add this subsite to your Yesod app or just ignore it if you don't want to receive messages from devices.+--+-- * When you want to send notifications:+--+--      (1) You have to specify the 'PushNotification' , setting the parameters. Here you have to do it+--           for each kind of notification, because they contain different structures.+--+--      2. Then, you can send these with the 'sendPush' function, passing the 'PushManager' as an argument. Also, you can get useful information+--           from the 'PushResult' value after communicating with servers.+--+-- * At the end, you stop the service with: 'closePushService'.+--+--+-- When your Apps on devices need to send an upstream message they have 2 options:+--+--      (1) Messages through CCS (only Android devices).+--         For this you have to set the proper CCS settings when starting the Push Service.+--+--      2. Messages as HTTP requests to 'PushManager' Yesod subsite (all devices).+--         (using the route of the Yesod subsite: something like \".../messages\" )+--+-- You can see many test examples on the GitHub repository: <https://github.com/MarcosPividori/GSoC-Communicating-with-mobile-devices>++-- $subsite+-- Everytime a device sends a message to our system as HTTP POST request it should provide the pair+-- {\"regId\" : identifier , \"system\" : system } specifing the ID and the system running on the device, so we can build +-- the 'Device' identifier.+--+-- Now we support APNS, MPNS and GCM. So the possible combinations are:+--+-- { \"regId\" : 'Network.PushNotify.Gcm.RegId' , \"system\" : \"ANDROID\" }+--+-- { \"regId\" : 'Network.PushNotify.Mpns.DeviceURI' , \"system\" : \"WPHONE\" }+--+-- { \"regId\" : 'Network.PushNotify.Apns.DeviceToken' , \"system\" : \"IOS\" } (token stored in hexadecimal representation)+-- +-- This 'PushManager' Yesod subsite consists of 2 routes for receiving JSON data:+--+--  * /register -> this route will receive the registration from devices. So devices have to send the pairs+--      {\"regId\" : identifier , \"system\" : system }  and any extra information. When a new intent for a registration arrives,+--      the 'newDeviceCallback' function will be called with the identifier and JSON data as arguments so you can ask for more information+--      as username and password, etc. Depending on the callback function result, the response to the POST request+--      will be set, so device can know if it has successfully registered on the server.+--+--  *  /messages -> this route will receive POST messages from devices. Again, devices have to send the pairs+--      {\"regId\" : identifier , \"system\" : system } and any other extra information.+--      Everytime a new messages arrives to the Yesod subsite, the 'newMessageCallback' function will be called with the identifier+--      and JSON data as arguments.+--      This abstraction lets us use the same callback function to handle the messages that arrive from CCS too (XMPP connection for GCM).
+ Network/PushNotify/General/Send.hs view
@@ -0,0 +1,132 @@+-- | This Module define the main functions to send Push Notifications.+{-# LANGUAGE ScopedTypeVariables #-}+module Network.PushNotify.General.Send(+    startPushService+  , closePushService+  , sendPush+  , withPushManager+  ) where++import Network.PushNotify.General.Types+import Network.PushNotify.General.YesodPushApp++import Yesod+import Data.Text                        (Text)+import Data.Maybe+import Data.Monoid+import Data.Default+import Control.Concurrent+import Control.Monad+import Control.Exception                as CE+import qualified Data.HashMap.Strict    as HM+import qualified Data.HashSet           as HS+import Network.HTTP.Conduit+import Network.PushNotify.Gcm+import Network.PushNotify.Apns+import Network.PushNotify.Mpns+import Network.PushNotify.Ccs++-- | 'startPushService' starts the PushService creating a PushManager.+startPushService :: PushServiceConfig -> IO PushManager+startPushService pConfig = do+                let cnfg    = pushConfig pConfig+                    gcmflag = case gcmConfig cnfg of+                                Just (Http _) -> True+                                _             -> False+                httpMan <- if gcmflag || isJust (mpnsConfig cnfg)+                             then do+                                    m <- newManager def+                                    return (Just m)+                             else return Nothing+                apnsMan <- case apnsConfig cnfg of+                             Just cnf -> do+                                    m <- startAPNS cnf+                                    return (Just m)+                             Nothing  -> return Nothing+                ccsMan  <- case gcmConfig cnfg of+                             Just (Ccs cnf) -> do+                                    m <- startCCS cnf (\d -> (newMessageCallback pConfig) (GCM d))+                                    return (Just m)+                             _                    -> return Nothing+                return $ PushManager httpMan apnsMan ccsMan pConfig++-- | 'closePushService' stops the Push service.+closePushService :: PushManager -> IO ()+closePushService man = do+                         case httpManager man of+                             Just m -> closeManager m+                             _      -> return ()+                         case apnsManager man of+                             Just m -> closeAPNS m+                             _      -> return ()+                         case ccsManager man of+                             Just m -> closeCCS m+                             _      -> return ()++-- | 'withPushManager' creates a new manager, uses it in the provided function, and then releases it.+withPushManager :: PushServiceConfig -> (PushManager -> IO a) -> IO a+withPushManager confg fun = CE.bracket (startPushService confg) closePushService fun++forgetConst :: Device -> Text+forgetConst (GCM  x) = x+forgetConst (APNS x) = x+forgetConst (MPNS x) = x++isGCM  (GCM  _) = True+isGCM  _        = False++isAPNS (APNS _) = True+isAPNS _        = False++isMPNS (MPNS _) = True+isMPNS _        = False++-- | 'sendPush' sends messages to the appropiate Push Servers.+sendPush :: PushManager -> PushNotification -> HS.HashSet Device -> IO PushResult+sendPush man notif devices = do+    let gcmDevices  = HS.map forgetConst $ HS.filter isGCM  devices+        apnsDevices = HS.map forgetConst $ HS.filter isAPNS devices+        mpnsDevices = HS.map forgetConst $ HS.filter isMPNS devices+        pConfig     = serviceConfig man+        config      = pushConfig pConfig++    r1 <- case (HS.null gcmDevices , gcmConfig config , gcmNotif  notif , httpManager man , ccsManager man) of+              (False,Just (Ccs cnf),Just msg,_,Just m)   -> do+                            let msg' = msg{registration_ids = gcmDevices}+                            CE.catch (sendCCS' m msg')+                              (\e -> let _ = e :: CE.SomeException in+                                     case httpManager man of+                                       Just hman -> sendGCM' hman def{apiKey = aPiKey cnf} msg'+                                       _ -> return $ exceptionResult (HM.fromList $ map (\d -> (GCM d,Right e)) $+                                                                      HS.toList $ registration_ids msg') )++              (False,Just (Http cnf),Just msg,Just m,_ ) -> sendGCM' m cnf msg{registration_ids = gcmDevices}++              _                                          -> return def++    r2 <- case (HS.null apnsDevices , apnsNotif notif , apnsManager man) of+              (False,Just msg,Just m) -> sendAPNS' m msg{deviceTokens = apnsDevices}+              _                       -> return def++    r3 <- case (HS.null mpnsDevices , mpnsConfig config , mpnsNotif notif , httpManager man) of+              (False,Just cnf,Just msg,Just m) -> sendMPNS' m cnf msg{deviceURIs = mpnsDevices}+              _                                -> return def++    let res = r1 <> r2 <> r3++    when (not $ HS.null $ unRegistered res) $ mapM_ (unRegisteredCallback pConfig) (HS.toList $ unRegistered res)++    when (not $ HM.null $ newIds res)       $ mapM_ (newIdCallback        pConfig) (HM.toList $ newIds res)++    return res++    where+      sendCCS'  a b   = sendCCS a b >>= return . toPushResult+      sendMPNS' a b c = sendMPNS a b c >>= return . toPushResult+      sendGCM'  a b c = CE.catch (sendGCM a b c >>= return . toPushResult)+                        (\(e :: CE.SomeException) -> return $ exceptionResult ( HM.fromList $ map (\d -> (GCM d,Right e)) $ +                                                                                HS.toList $ registration_ids c))+      sendAPNS' a b   = CE.catch (sendAPNS a b >>= return . toPushResult)+                        (\(e :: CE.SomeException) -> return $ exceptionResult ( HM.fromList $ map (\d -> (APNS d,Right e)) $+                                                                                HS.toList $ deviceTokens b))+      exceptionResult l = PushResult HS.empty l HS.empty HS.empty HM.empty
+ Network/PushNotify/General/Types.hs view
@@ -0,0 +1,200 @@+{-# LANGUAGE OverloadedStrings , DeriveGeneric #-}+-- | This Module defines the main data types for the Push Service.+module Network.PushNotify.General.Types+    ( -- * Push Settings     +      Device(..)+    , PushServiceConfig(..)+    , RegisterResult(..)+    , GCMConfig(..)+    , PushConfig(..)+      -- * Push Manager+    , PushManager(..)+      -- * Push Message+    , PushNotification(..)+    , generalNotif+      -- * Push Result+    , PushResult(..)+    , IsPushResult(..)+    ) where++import Network.PushNotify.Gcm+import Network.PushNotify.Apns+import Network.PushNotify.Mpns+import Network.PushNotify.Ccs+import Network.HTTP.Conduit+import Network.HTTP.Types.Status+import Control.Exception++import qualified Data.Map               as M+import qualified Data.ByteString.Lazy   as BL+import qualified Data.ByteString        as BS+import qualified Data.Text.Encoding     as E+import qualified Data.HashMap.Strict    as HM+import qualified Data.HashSet           as HS+import Data.Aeson+import Data.Default+import Data.Hashable+import Data.List+import Data.Monoid+import Data.Text     (Text,pack)+import Text.XML++-- | Unique identifier of an app/device.+data Device = GCM  RegId        -- ^ An Android app.+            | APNS DeviceToken  -- ^ An iOS app.+            | MPNS DeviceURI    -- ^ A WPhone app.+            deriving (Show, Read, Eq)++instance Hashable Device where+     hashWithSalt s (GCM n)   = s `hashWithSalt`+                                 (0::Int) `hashWithSalt` n+     hashWithSalt s (MPNS n)  = s `hashWithSalt`+                                 (1::Int) `hashWithSalt` n+     hashWithSalt s (APNS n)  = s `hashWithSalt`+                                 (2::Int) `hashWithSalt` n++-- | General notification to be sent.+data PushNotification = PushNotification {+        apnsNotif :: Maybe APNSmessage+    ,   gcmNotif  :: Maybe GCMmessage+    ,   mpnsNotif :: Maybe MPNSmessage+    } deriving Show++instance Default PushNotification where+    def = PushNotification Nothing Nothing Nothing++-- | 'generalNotif' builds a general notification from JSON data.+--+-- If data length exceeds 256 bytes (max payload limit for APNS) it will fails.+--+-- For MPNS, data will be XML-labeled as \"jsonData\".+generalNotif :: Object -> IO PushNotification+generalNotif dat = do+    let msg = PushNotification {+            apnsNotif = Just def{ rest        = Just dat}+        ,   gcmNotif  = Just def{ data_object = Just dat}+        ,   mpnsNotif = Just def{ restXML     = Document (Prologue [] Nothing [])+                                                         (Element (Name "jsonData" Nothing Nothing)+                                                                 M.empty+                                                                 [NodeContent $ E.decodeUtf8 $+                                                                    BS.concat . BL.toChunks $ encode dat])+                                                         []+                                }+        }+    if ((BL.length . encode . apnsNotif) msg > 256)+      then fail "Too long payload"+      else return msg++-- | Settings for GCM service.+data GCMConfig = Http GCMHttpConfig | Ccs GCMCcsConfig++-- | Main settings for the different Push Services. @Nothing@ means the service won't be used.+data PushConfig = PushConfig{+        gcmConfig  :: Maybe GCMConfig+    ,   apnsConfig :: Maybe APNSConfig+    ,   mpnsConfig :: Maybe MPNSConfig+    }++instance Default PushConfig where+    def = PushConfig Nothing Nothing Nothing++-- | 'RegisterResult' represents the result of a device attempting to register+data RegisterResult = SuccessfulReg | ErrorReg Text++-- | Main settings for the Push Service.+data PushServiceConfig = PushServiceConfig {+        pushConfig           :: PushConfig                   -- ^ Main configuration.+    ,   newMessageCallback   :: Device -> Value -> IO ()     -- ^ The callback function to be called when receiving messages from devices+                                                             -- (This means through the CCS connection or as POST requests+                                                             -- to the Yesod subsite).+    ,   newDeviceCallback    :: Device -> Value -> IO RegisterResult -- ^ The callback function to be called when+                                                                     -- a new device try to register on server.+    ,   unRegisteredCallback :: Device -> IO ()              -- ^ The callback function to be called when a device unregisters.+    ,   newIdCallback        :: (Device,Device) -> IO ()     -- ^ The callback function to be called when a device's identifier changes.+    }++instance Default PushServiceConfig where+    def = PushServiceConfig {+        pushConfig           = def+    ,   newMessageCallback   = \_ _ -> return ()+    ,   newDeviceCallback    = \_ _ -> return SuccessfulReg+    ,   unRegisteredCallback = \_ -> return ()+    ,   newIdCallback        = \_ -> return ()+    }++-- | Main manager for the Push Service.+--+-- This 'PushManager' will be used to send notifications and also can be added as a subsite to a Yesod app+-- in order to receive registrations and messages from devices as HTTP POST requests.+data PushManager = PushManager {+        httpManager    :: Maybe Manager       -- ^ Conduit manager for sending push notifications though POST requests.+    ,   apnsManager    :: Maybe APNSManager   -- ^ Apns manager for sending push notifications though APNS servers.+    ,   ccsManager     :: Maybe CCSManager    -- ^ Ccs manager for communicating with GCM through Cloud Connection Server.+    ,   serviceConfig  :: PushServiceConfig   -- ^ Main configuration.+    }++-- | PushResult represents a general result after communicating with a Push Server.+data PushResult = PushResult {+        successful   :: HS.HashSet Device -- ^ Notifications that were successfully sent.+    ,   failed       :: HM.HashMap Device (Either Text SomeException) -- ^ Notifications that were not successfully sent.+    ,   toResend     :: HS.HashSet Device -- ^ Failed notifications that you need to resend,+                                          -- because servers were not available or there was a problem with the connection.+    ,   unRegistered :: HS.HashSet Device -- ^ Set of unregistered devices.+    ,   newIds       :: HM.HashMap Device Device -- ^ Map of devices which have changed their identifiers. (old -> new)+    } deriving Show++instance Default PushResult where+    def = PushResult HS.empty HM.empty HS.empty HS.empty HM.empty++instance Monoid PushResult where+    mempty = def+    mappend (PushResult a1 b1 c1 d1 e1)+            (PushResult a2 b2 c2 d2 e2) = PushResult (HS.union a1 a2)  (HM.union b1 b2)+                                                     (HS.union  c1 c2) (HS.union  d1 d2) (HM.union e1 e2)++-- | This class represent the translation from a specific service's result into a general Push result.+class IsPushResult a where+    toPushResult :: a -> PushResult+++instance IsPushResult GCMresult where+    toPushResult r = def {+        successful   = HS.map GCM $ HS.fromList $ HM.keys $ messagesIds r+    ,   failed       = HM.fromList $ map (\(x,y) -> (GCM x,Left y)) (HM.toList $ errorRest r)+                    <> map (\x -> (GCM x,Left "UnregisteredError")) (HS.toList $ errorUnRegistered r)+                    <> map (\x -> (GCM x,Left "InternalError"))     (HS.toList $ errorToReSend     r)+    ,   toResend     = HS.map GCM $ errorToReSend r+    ,   unRegistered = HS.map GCM $ errorUnRegistered r <> (HS.fromList . HM.keys . errorRest) r+    -- I decide to unregister all regId with error different to Unregistered or Unavailable. (errorRest)+    -- Because these are non-recoverable error.+    ,   newIds       = HM.fromList $ map (\(x,y) -> (GCM x,GCM y)) $ HM.toList $ newRegids r+    }+++instance IsPushResult APNSresult where+    toPushResult r = def {+        successful   = HS.map APNS $ successfulTokens r+    ,   failed       = HM.fromList $ map (\x -> (APNS x , Left "CommunicatingError")) $ HS.toList $ toReSendTokens r+    ,   toResend     = HS.map APNS $ toReSendTokens r+    }++instance IsPushResult APNSFeedBackresult where+    toPushResult r = def {+        unRegistered = HS.fromList $ map APNS $ HM.keys $ unRegisteredTokens r+    }+++instance IsPushResult MPNSresult where+    toPushResult r = let (successList,failureList) = partition ((== Just Received) . notificationStatus . snd ) $ +                                                               HM.toList $ successfullResults r+                     in def {+        successful   = HS.fromList $ map (MPNS . fst) successList+    ,   failed       = (HM.fromList $ map (\(x,y) -> (MPNS x , Right y)) (HM.toList $ errorException r))+                    <> (HM.fromList $ map (\(x,y) -> (MPNS x , Left $ pack $ show $ notificationStatus y)) failureList)+    ,   toResend     = HS.map MPNS . HS.fromList . HM.keys . HM.filter error500 $ errorException r+    ,   unRegistered = HS.map MPNS . HS.fromList . HM.keys . HM.filter ((== Just Expired) . subscriptionStatus) $ successfullResults r+    } where+        error500 :: SomeException -> Bool+        error500 e = case (fromException e) :: Maybe HttpException of+                         Just (StatusCodeException status _ _) -> (statusCode status) >= 500+                         _                                     -> False
+ Network/PushNotify/General/YesodPushApp.hs view
@@ -0,0 +1,53 @@+{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell, FlexibleInstances, MultiParamTypeClasses, FlexibleContexts, QuasiQuotes #-}+-- | This Module defines the Yesod subsite to be used for the registration and reception of messages from devices.+module Network.PushNotify.General.YesodPushApp(+  PushManager(..)+  ) where++import Network.PushNotify.General.Types+import Network.PushNotify.General.YesodPushAppRoutes+import Yesod+import Control.Concurrent+import Data.Text+import Data.Aeson+import qualified Data.HashMap.Strict as HM++instance (RenderMessage master FormMessage, Yesod master) => YesodSubDispatch PushManager (HandlerT master IO) where+    yesodSubDispatch = $(mkYesodSubDispatch resourcesPushManager)++-- 'postRegister' allows a mobile device to register. (JSON POST messages to '/register')+postSubRegisterR :: (RenderMessage master FormMessage, Yesod master) => HandlerT PushManager (HandlerT master IO) ()+postSubRegisterR = do+    value  <- parseJsonBody_+    case value of+        Object v -> do+                      iden <- lookForIdentifier v+                      pushManager <- getYesod+                      res <- liftIO $ (newDeviceCallback $ serviceConfig pushManager) iden value+                      case res of+                        SuccessfulReg -> sendResponse $ RepJson emptyContent -- successful registration.+                        ErrorReg t    -> permissionDenied t                  -- error in registration.+        _        -> invalidArgs []++lookForIdentifier :: Object -> HandlerT PushManager (HandlerT master IO) Device+lookForIdentifier v = do+                    regId <- case (HM.lookup "regId" v) of+                                 Just (String s) -> return s+                                 _               -> invalidArgs []+                    case (HM.lookup "system" v) of+                      Just (String "ANDROID") -> return $ GCM  regId -- A Android device.+                      Just (String "WPHONE")  -> return $ MPNS regId -- A WPhone device.+                      Just (String "IOS")     -> return $ APNS regId -- A iOS device.+                      _                       -> invalidArgs []++-- 'postMessages' allows a mobile device to send a message. (JSON POST messages to '/messages')+postSubMessagesR :: (RenderMessage master FormMessage, Yesod master) => HandlerT PushManager (HandlerT master IO) ()+postSubMessagesR = do+    value  <- parseJsonBody_+    case value of+        Object v -> do+                      iden <- lookForIdentifier v+                      pushManager <- getYesod+                      liftIO $ forkIO $ (newMessageCallback $ serviceConfig pushManager) iden value+                      sendResponse $ RepJson emptyContent+        _        -> invalidArgs []
+ Network/PushNotify/General/YesodPushAppRoutes.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings, TypeFamilies, TemplateHaskell, QuasiQuotes #-}++module Network.PushNotify.General.YesodPushAppRoutes where++import Yesod+import Network.PushNotify.General.Types+import Control.Concurrent+import Data.Text++-- Yesod subsite to be used for the registration and reception of messages from devices.+mkYesodSubData "PushManager" [parseRoutes|+/register SubRegisterR POST+/messages SubMessagesR POST+|]
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ push-notify-general.cabal view
@@ -0,0 +1,48 @@+name:                push-notify-general+version:             0.1.0.0+synopsis:            A general library for sending/receiving push notif. through dif. services.++description:         This library offers an API for sending/receiving notifications, and handling the registration of devices on the server.+                     .+                     It provides a general abstraction which can be used to communicate through different services as APNS, GCM, MPNS.+                     .+                     For more information and test examples: <http://gsoc2013cwithmobiledevices.blogspot.com.ar/>+                     .+                     GitHub repository: <https://github.com/MarcosPividori/GSoC-Communicating-with-mobile-devices>++homepage:            http://gsoc2013cwithmobiledevices.blogspot.com.ar/+license:             MIT+license-file:        LICENSE+author:              Marcos Pividori, mentored by Michael Snoyman.+maintainer:          Marcos Pividori <marcos.pividori@gmail.com>++category:            Network, Cloud, Mobile+build-type:          Simple+cabal-version:       >=1.8++library+  exposed-modules:   Network.PushNotify.General++  other-modules:     Network.PushNotify.General.Send+                   , Network.PushNotify.General.Types+                   , Network.PushNotify.General.YesodPushApp+                   , Network.PushNotify.General.YesodPushAppRoutes+  +  build-depends:     base                 >=4.5  && <5+                   , aeson                >=0.6+                   , bytestring           >=0.9+                   , containers           >=0.4+                   , data-default         >=0.5+                   , hashable             >=1.2+                   , http-conduit         >=1.9+                   , http-types           >=0.8+                   , push-notify          >=0.1+                   , push-notify-ccs      >=0.1+                   , text                 >=0.11+                   , unordered-containers >=0.2+                   , xml-conduit          >=1.1+                   , yesod                >=1.2++source-repository head+  type:     git+  location: https://github.com/MarcosPividori/GSoC-Communicating-with-mobile-devices.git