packages feed

fb 1.2.1 → 2.0.0

raw patch · 13 files changed

+835/−735 lines, 13 filesdep +cryptonitedep +memorydep −base16-bytestringdep −base64-bytestringdep −cerealdep ~attoparsecdep ~bytestringdep ~textPVP ok

version bump matches the API change (PVP)

Dependencies added: cryptonite, memory

Dependencies removed: base16-bytestring, base64-bytestring, cereal, crypto-api, cryptohash, cryptohash-cryptoapi, old-locale

Dependency ranges changed: attoparsec, bytestring, text, transformers, unliftio-core

API changes (from Hackage documentation)

+ Facebook: [appSecretProof] :: Credentials -> Bool
+ Facebook: addAppSecretProof :: Credentials -> Maybe (AccessToken anykind) -> SimpleQuery -> SimpleQuery
+ Facebook: getApiVersion :: MonadIO m => FacebookT anyAuth m ApiVersion
+ Facebook: makeAppSecretProof :: Credentials -> Maybe (AccessToken anyKind) -> SimpleQuery
+ Facebook: setApiVersion :: MonadIO m => ApiVersion -> FacebookT anyAuth m ()
+ Facebook: type ApiVersion = Text
- Facebook: Credentials :: Text -> Text -> Text -> Credentials
+ Facebook: Credentials :: Text -> Text -> Text -> Bool -> Credentials
- Facebook: beta_runFacebookT :: Credentials -> Manager -> FacebookT Auth m a -> m a
+ Facebook: beta_runFacebookT :: MonadIO m => Credentials -> Manager -> FacebookT Auth m a -> m a
- Facebook: beta_runNoAuthFacebookT :: Manager -> FacebookT NoAuth m a -> m a
+ Facebook: beta_runNoAuthFacebookT :: MonadIO m => Manager -> FacebookT NoAuth m a -> m a
- Facebook: createAction :: (MonadResource m, MonadUnliftIO m, MonadThrow m) => Action -> [Argument] -> Maybe AppAccessToken -> UserAccessToken -> FacebookT Auth m Id
+ Facebook: createAction :: (MonadResource m, MonadUnliftIO m, MonadThrow m, MonadIO m) => Action -> [Argument] -> Maybe AppAccessToken -> UserAccessToken -> FacebookT Auth m Id
- Facebook: createTestUser :: (MonadResource m, MonadUnliftIO m, MonadThrow m) => CreateTestUser -> AppAccessToken -> FacebookT Auth m TestUser
+ Facebook: createTestUser :: (MonadResource m, MonadUnliftIO m, MonadThrow m, MonadIO m) => CreateTestUser -> AppAccessToken -> FacebookT Auth m TestUser
- Facebook: disassociateTestuser :: (MonadUnliftIO m, MonadThrow m, MonadResource m) => TestUser -> AppAccessToken -> FacebookT Auth m Bool
+ Facebook: disassociateTestuser :: (MonadUnliftIO m, MonadThrow m, MonadResource m, MonadIO m) => TestUser -> AppAccessToken -> FacebookT Auth m Bool
- Facebook: extendUserAccessToken :: (MonadResource m, MonadUnliftIO m, MonadThrow m) => UserAccessToken -> FacebookT Auth m (Either FacebookException UserAccessToken)
+ Facebook: extendUserAccessToken :: (MonadResource m, MonadUnliftIO m, MonadThrow m, MonadIO m) => UserAccessToken -> FacebookT Auth m (Either FacebookException UserAccessToken)
- Facebook: getAppAccessToken :: (MonadResource m, MonadUnliftIO m, MonadThrow m) => FacebookT Auth m AppAccessToken
+ Facebook: getAppAccessToken :: (MonadResource m, MonadUnliftIO m, MonadThrow m, MonadIO m) => FacebookT Auth m AppAccessToken
- Facebook: getRealTimeUpdateNotifications :: (Monad m, FromJSON a) => ByteString -> ByteString -> FacebookT Auth m (Maybe (RealTimeUpdateNotification a))
+ Facebook: getRealTimeUpdateNotifications :: (Monad m, FromJSON a, MonadIO m) => ByteString -> ByteString -> FacebookT Auth m (Maybe (RealTimeUpdateNotification a))
- Facebook: getTestUsers :: (MonadResource m, MonadUnliftIO m, MonadThrow m) => AppAccessToken -> FacebookT Auth m (Pager TestUser)
+ Facebook: getTestUsers :: (MonadResource m, MonadUnliftIO m, MonadThrow m, MonadIO m) => AppAccessToken -> FacebookT Auth m (Pager TestUser)
- Facebook: getUserAccessTokenStep1 :: Monad m => RedirectUrl -> [Permission] -> FacebookT Auth m Text
+ Facebook: getUserAccessTokenStep1 :: (Monad m, MonadIO m) => RedirectUrl -> [Permission] -> FacebookT Auth m Text
- Facebook: getUserAccessTokenStep2 :: (MonadResource m, MonadUnliftIO m, MonadThrow m) => RedirectUrl -> [Argument] -> FacebookT Auth m UserAccessToken
+ Facebook: getUserAccessTokenStep2 :: (MonadResource m, MonadUnliftIO m, MonadThrow m, MonadIO m) => RedirectUrl -> [Argument] -> FacebookT Auth m UserAccessToken
- Facebook: parseSignedRequest :: (FromJSON a, Monad m) => ByteString -> FacebookT Auth m (Maybe a)
+ Facebook: parseSignedRequest :: (FromJSON a, Monad m, MonadIO m) => ByteString -> FacebookT Auth m (Maybe a)
- Facebook: runFacebookT :: Credentials -> Manager -> FacebookT Auth m a -> m a
+ Facebook: runFacebookT :: MonadIO m => Credentials -> Manager -> FacebookT Auth m a -> m a
- Facebook: runNoAuthFacebookT :: Manager -> FacebookT NoAuth m a -> m a
+ Facebook: runNoAuthFacebookT :: MonadIO m => Manager -> FacebookT NoAuth m a -> m a
- Facebook: verifyRealTimeUpdateNotifications :: Monad m => ByteString -> ByteString -> FacebookT Auth m (Maybe ByteString)
+ Facebook: verifyRealTimeUpdateNotifications :: (Monad m, MonadIO m) => ByteString -> ByteString -> FacebookT Auth m (Maybe ByteString)

Files

CHANGELOG.md view
@@ -1,3 +1,23 @@+# Version 2.0.0++* Remove following dependency:+  - base16-bytestring+  - base64-bytestring+  - cereal+  - crypto-api+  - cryptohash+  - cryptohash-cryptoapi+  - old-locale+* Introduce new dependency:+  - cryptonite+  - memory+* Add new function setApiVersion+* Add Graph API version parameter. Avoid hardcoded `v2.8`.+* Expose setApiVersion and getApiVersion function+* Default API endpoint updated to `v3.2`+* Add appsecret_proof verification.+* Fix appsecret_proof verification encoding.+ # Version 1.2.1  * Make it work for ghc-8.4. See [#3](https://github.com/psibi/fb/issues/3)
README.md view
@@ -30,6 +30,7 @@   { appName = "Your_APP_Name"   , appId = "your_app_id"   , appSecret = "xxxxxxxxxxxxxxxxx"+  , appSecretProof = False   }  main :: IO ()
fb.cabal view
@@ -1,11 +1,13 @@--- This file has been generated from package.yaml by hpack version 0.21.2.+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.2. -- -- see: https://github.com/sol/hpack ----- hash: 1f1f3544be4d2d1dbdab5e0286e45bc861f216f4b24d04227e926c4cc51ab272+-- hash: 1165a61ce7ffe062813385420ff4c80c2e0089e64221cfd0d045a3cf1654b8a8  name:           fb-version:        1.2.1+version:        2.0.0 synopsis:       Bindings to Facebook's API. description:    This package exports bindings to Facebook's APIs (see                 <http://developers.facebook.com/>).  Does not have any external@@ -29,14 +31,12 @@ license:        BSD3 license-file:   LICENSE build-type:     Simple-cabal-version:  >= 1.10- extra-source-files:-    CHANGELOG.md-    example.hs-    README.md     tests/Main.hs     tests/tryIt.hs+    example.hs+    README.md+    CHANGELOG.md  source-repository head   type: git@@ -72,27 +72,22 @@   ghc-options: -Wall   build-depends:       aeson >=0.8.0.2-    , attoparsec >=0.10.4 && <0.14+    , attoparsec >=0.10.4     , base >=4 && <5-    , base16-bytestring >=0.1-    , base64-bytestring >=0.1.1-    , bytestring >=0.9 && <0.11-    , cereal >=0.3 && <0.6+    , bytestring >=0.9     , conduit >=1.3.0     , conduit-extra >=1.3.0-    , crypto-api >=0.11 && <0.14-    , cryptohash >=0.7-    , cryptohash-cryptoapi ==0.1.*+    , cryptonite     , data-default     , http-client >=0.4.30     , http-conduit >=2.3.0     , http-types+    , memory     , monad-logger >=0.3.28-    , old-locale     , resourcet >=1.2.0-    , text >=0.11 && <1.3+    , text >=0.11     , time >=1.4-    , transformers >=0.2 && <0.6+    , transformers >=0.2     , transformers-base     , unliftio     , unliftio-core
src/Facebook.hs view
@@ -1,7 +1,7 @@ module Facebook-  (-   -- * @FacebookT@ monad transformer-   FacebookT+  ( +    -- * @FacebookT@ monad transformer+    FacebookT   , runFacebookT   , runNoAuthFacebookT   , mapFacebookT@@ -17,8 +17,11 @@   , UserAccessToken   , AppAccessToken   , AccessTokenData+  , ApiVersion   , hasExpired   , isValid+  , setApiVersion+  , getApiVersion    -- ** App access token   , AppKind   , getAppAccessToken@@ -34,6 +37,8 @@   , DebugToken(..)    -- ** Signed requests   , parseSignedRequest+  , addAppSecretProof+  , makeAppSecretProof    -- * Facebook's Graph API    -- ** User   , User(..)@@ -126,18 +131,18 @@   , unPermission   ) where -import Facebook.Types-import Facebook.Monad-import Facebook.Base import Facebook.Auth-import Facebook.Pager+import Facebook.Base+import Facebook.FQL import Facebook.Graph-import Facebook.Object.Page-import Facebook.Object.User+import Facebook.Monad import Facebook.Object.Action import Facebook.Object.Checkin-import Facebook.Object.Order import Facebook.Object.FriendList+import Facebook.Object.Order+import Facebook.Object.Page+import Facebook.Object.User+import Facebook.Pager import Facebook.RealTime-import Facebook.FQL import Facebook.TestUsers+import Facebook.Types
src/Facebook/Auth.hs view
@@ -22,58 +22,56 @@ #if __GLASGOW_HASKELL__ <= 784 import Control.Applicative #endif-import Control.Monad (guard, liftM, mzero)-import Control.Monad.IO.Class (MonadIO(liftIO))+import Control.Monad (guard, mzero)+import Control.Monad.IO.Class import Control.Monad.Trans.Maybe (MaybeT(..))-import Crypto.Classes (constTimeEq)-import Crypto.Hash.CryptoAPI (SHA256)-import Crypto.HMAC (hmac', MacKey(..))-import Data.Aeson ((.:))-import Data.Aeson.Parser (json')-import Data.Maybe (fromMaybe)-import Data.Text (Text)-import Data.Time (getCurrentTime, addUTCTime, UTCTime)-import Data.Typeable (Typeable)-import Data.String (IsString(..))--import qualified UnliftIO.Exception as E import qualified Control.Monad.Trans.Resource as R+import Crypto.Hash.Algorithms (SHA256)+import Crypto.MAC.HMAC (HMAC(..), hmac)+import Data.Aeson ((.:)) import qualified Data.Aeson as AE+import Data.Aeson.Parser (json') import qualified Data.Aeson.Types as AE import qualified Data.Attoparsec.ByteString.Char8 as AB+import Data.ByteArray (ScrubbedBytes, convert)+import Data.ByteArray.Encoding (Base(..), convertFromBase) import qualified Data.ByteString as B-import qualified Data.ByteString.Base64.URL as Base64URL import qualified Data.ByteString.Char8 as B8 import qualified Data.List as L-import qualified Data.Serialize as Cereal+import Data.Maybe (fromMaybe)+import Data.String (IsString(..))+import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Text.Encoding.Error as TE+import Data.Time (UTCTime, addUTCTime, getCurrentTime)+import Data.Typeable (Typeable) import qualified Network.HTTP.Types as HT+import qualified UnliftIO.Exception as E -import Facebook.Types import Facebook.Base import Facebook.Monad+import Facebook.Types  -- | Get an app access token from Facebook using your -- credentials. -- Ref: https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow-getAppAccessToken-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+getAppAccessToken ::+     (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, MonadIO m)   => FacebookT Auth m AppAccessToken getAppAccessToken =-  runResourceInFb $-  do creds <- getCreds-     req <--       fbreq "/oauth/access_token" Nothing $-       tsq creds [("grant_type", "client_credentials")]-     response <- fbhttp req-     (token :: AE.Value) <- asJson response-     case AE.parseMaybe tokenParser token of-       Just appToken -> return $ AppAccessToken appToken-       _ ->-         E.throwIO $-         FbLibraryException ("Unable to parse: " <> (T.pack $ show token))+  runResourceInFb $ do+    creds <- getCreds+    req <-+      fbreq "/oauth/access_token" Nothing $+      tsq creds [("grant_type", "client_credentials")]+    response <- fbhttp req+    (token :: AE.Value) <- asJson response+    case AE.parseMaybe tokenParser token of+      Just appToken -> return $ AppAccessToken appToken+      _ ->+        E.throwIO $+        FbLibraryException ("Unable to parse: " <> (T.pack $ show token))   where     tokenParser :: AE.Value -> AE.Parser AccessTokenData     tokenParser val =@@ -88,29 +86,31 @@ -- Facebook URL you should redirect you user to.  Facebook will -- authenticate the user, authorize your app and then redirect -- the user back into the provider 'RedirectUrl'.-getUserAccessTokenStep1-  :: Monad m-  => RedirectUrl -> [Permission] -> FacebookT Auth m Text+getUserAccessTokenStep1 ::+     (Monad m, MonadIO m)+  => RedirectUrl+  -> [Permission]+  -> FacebookT Auth m Text getUserAccessTokenStep1 redirectUrl perms = do   creds <- getCreds-  withTier $-    \tier ->-       let urlBase =-             case tier of-               Production ->-                 "https://www.facebook.com/" <> apiVersion <>-                 "/dialog/oauth?client_id="-               Beta ->-                 "https://www.beta.facebook.com/" <> apiVersion <>-                 "/dialog/oauth?client_id="-       in T.concat $-          urlBase :-          appId creds :-          "&redirect_uri=" :-          redirectUrl :-          (case perms of-             [] -> []-             _ -> "&scope=" : L.intersperse "," (map unPermission perms))+  apiVersion <- getApiVersion+  withTier $ \tier ->+    let urlBase =+          case tier of+            Production ->+              "https://www.facebook.com/" <> apiVersion <>+              "/dialog/oauth?client_id="+            Beta ->+              "https://www.beta.facebook.com/" <> apiVersion <>+              "/dialog/oauth?client_id="+     in T.concat $+        urlBase :+        appId creds :+        "&redirect_uri=" :+        redirectUrl :+        (case perms of+           [] -> []+           _ -> "&scope=" : L.intersperse "," (map unPermission perms))  -- | The second step to get an user access token.  If the user is -- successfully authenticate and they authorize your application,@@ -119,8 +119,8 @@ -- request query parameters passed to your 'RedirectUrl' and give -- to this function that will complete the user authentication -- flow and give you an @'UserAccessToken'@.-getUserAccessTokenStep2-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+getUserAccessTokenStep2 ::+     (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, MonadIO m)   => RedirectUrl -- ^ Should be exactly the same      -- as in 'getUserAccessTokenStep1'.   -> [Argument] -- ^ Query parameters.@@ -130,23 +130,24 @@     [code@("code", _)] ->       runResourceInFb $       -- Get the access token data through Facebook's OAuth.-      do now <- liftIO getCurrentTime-         creds <- getCreds-         req <--           fbreq "/oauth/access_token" Nothing $-           tsq creds [code, ("redirect_uri", TE.encodeUtf8 redirectUrl)]-         response <- fbhttp req-         (userToken :: AE.Value) <- asJson response-         let (token, expire) = userAccessTokenParser now userToken+       do+        now <- liftIO getCurrentTime+        creds <- getCreds+        req <-+          fbreq "/oauth/access_token" Nothing $+          tsq creds [code, ("redirect_uri", TE.encodeUtf8 redirectUrl)]+        response <- fbhttp req+        (userToken :: AE.Value) <- asJson response+        let (token, expire) = userAccessTokenParser now userToken          -- Get user's ID throught Facebook's graph.-         userResponse <--           fbhttp =<<-           fbreq-             "/me"-             (Just (UserAccessToken "invalid id" token expire))-             [("fields", "id")]-         (userId :: UserId) <- asJson userResponse-         return $ UserAccessToken userId token expire+        userResponse <-+          fbhttp =<<+          fbreq+            "/me"+            (Just (UserAccessToken "invalid id" token expire))+            [("fields", "id")]+        (userId :: UserId) <- asJson userResponse+        return $ UserAccessToken userId token expire     _ ->       let [error_, errorReason, errorDescr] =             map@@ -154,13 +155,13 @@               ["error", "error_reason", "error_description"]           errorType = T.concat [t error_, " (", t errorReason, ")"]           t = TE.decodeUtf8With TE.lenientDecode-      in E.throwIO $ FacebookException errorType (t errorDescr)+       in E.throwIO $ FacebookException errorType (t errorDescr)  -- | Attoparsec parser for user access tokens returned by -- Facebook as a query string.  Returns an user access token with -- a broken 'UserId'.-userAccessTokenParser-  :: UTCTime -- ^ 'getCurrentTime'+userAccessTokenParser ::+     UTCTime -- ^ 'getCurrentTime'   -> AE.Value   -> (AccessTokenData, UTCTime) userAccessTokenParser now val =@@ -191,8 +192,8 @@ -- to prevent this bug, we suggest that you use 'isValid' before -- redirecting the user to the URL provided by 'getUserLogoutUrl' -- since this function doesn't do any validity checks.-getUserLogoutUrl-  :: Monad m+getUserLogoutUrl ::+     Monad m   => UserAccessToken      -- ^ The user's access token.   -> RedirectUrl@@ -203,19 +204,18 @@ -- @https:\/\/www.beta.facebook.com\/@ when -- using the beta tier). getUserLogoutUrl (UserAccessToken _ data_ _) next = do-  withTier $-    \tier ->-       let urlBase =-             case tier of-               Production -> "https://www.facebook.com/logout.php?"-               Beta -> "https://www.beta.facebook.com/logout.php?"-       in TE.decodeUtf8 $-          urlBase <>-          HT.renderQuery-            False-            [ ("next", Just (TE.encodeUtf8 next))-            , ("access_token", Just (TE.encodeUtf8 data_))-            ]+  withTier $ \tier ->+    let urlBase =+          case tier of+            Production -> "https://www.facebook.com/logout.php?"+            Beta -> "https://www.beta.facebook.com/logout.php?"+     in TE.decodeUtf8 $+        urlBase <>+        HT.renderQuery+          False+          [ ("next", Just (TE.encodeUtf8 next))+          , ("access_token", Just (TE.encodeUtf8 data_))+          ]  -- | URL where the user is redirected to after Facebook -- authenticates the user authorizes your application.  This URL@@ -236,12 +236,14 @@ -- > -- > perms :: [Permission] -- > perms = ["user_about_me", "email", "offline_access"]-newtype Permission = Permission-  { unPermission :: Text+newtype Permission =+  Permission+    { unPermission :: Text     -- ^ Retrieves the 'Text' back from a 'Permission'.  Most of     -- the time you won't need to use this function, but you may     -- need it if you're a library author.-  } deriving (Eq, Ord)+    }+  deriving (Eq, Ord)  instance Show Permission where   show = show . unPermission@@ -250,9 +252,7 @@   fromString = Permission . fromString  -- | @True@ if the access token has expired, otherwise @False@.-hasExpired-  :: (Functor m, MonadIO m)-  => AccessToken anyKind -> m Bool+hasExpired :: (Functor m, MonadIO m) => AccessToken anyKind -> m Bool hasExpired token =   case accessTokenExpires token of     Nothing -> return False@@ -263,9 +263,10 @@ -- access token may not be valid as well.  For example, in the -- case of an user access token, they may have changed their -- password, logged out from Facebook or blocked your app.-isValid-  :: (R.MonadResource m, R.MonadUnliftIO m)-  => AccessToken anyKind -> FacebookT anyAuth m Bool+isValid ::+     (R.MonadResource m, R.MonadUnliftIO m)+  => AccessToken anyKind+  -> FacebookT anyAuth m Bool isValid token = do   expired <- hasExpired token   if expired@@ -283,7 +284,7 @@              -- will actually work with user access tokens,              -- too, but they have another, better way of              -- being checked.-         in httpCheck =<< fbreq page (Just token) []+          in httpCheck =<< fbreq page (Just token) []  -- | Extend the expiration time of an user access token (see -- <https://developers.facebook.com/docs/offline-access-deprecation/>,@@ -296,8 +297,8 @@ -- have the same data and expiration time as before, but you -- can't assume this).  Note that expired access tokens can't be -- extended, only valid tokens.-extendUserAccessToken-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+extendUserAccessToken ::+     (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, MonadIO m)   => UserAccessToken   -> FacebookT Auth m (Either FacebookException UserAccessToken) extendUserAccessToken token@(UserAccessToken uid data_ _) = do@@ -307,23 +308,23 @@     else tryToExtend   where     tryToExtend =-      runResourceInFb $-      do creds <- getCreds-         req <--           fbreq "/oauth/access_token" Nothing $-           tsq-             creds-             [ ("grant_type", "fb_exchange_token")-             , ("fb_exchange_token", TE.encodeUtf8 data_)-             ]-         response <- fbhttp req-         userToken <- E.try $ asJson response-         case userToken of-           Right val -> do-             now <- liftIO getCurrentTime-             let (extendedtoken, expire) = userAccessTokenParser now val-             return $ Right $ UserAccessToken uid extendedtoken expire-           Left exc -> return (Left exc)+      runResourceInFb $ do+        creds <- getCreds+        req <-+          fbreq "/oauth/access_token" Nothing $+          tsq+            creds+            [ ("grant_type", "fb_exchange_token")+            , ("fb_exchange_token", TE.encodeUtf8 data_)+            ]+        response <- fbhttp req+        userToken <- E.try $ asJson response+        case userToken of+          Right val -> do+            now <- liftIO getCurrentTime+            let (extendedtoken, expire) = userAccessTokenParser now val+            return $ Right $ UserAccessToken uid extendedtoken expire+          Left exc -> return (Left exc)     hasExpiredExc =       mkExc         [ "the user access token has already expired, "@@ -335,38 +336,41 @@ -- (<https://developers.facebook.com/docs/authentication/signed_request/>), -- verifies its authencity and integrity using the HMAC and -- decodes its JSON object.-parseSignedRequest-  :: (AE.FromJSON a, Monad m)+parseSignedRequest ::+     (AE.FromJSON a, Monad m, MonadIO m)   => B8.ByteString -- ^ Encoded Facebook signed request   -> FacebookT Auth m (Maybe a) parseSignedRequest signedRequest =   runMaybeT $   -- Split, decode and JSON-parse-  do let (encodedSignature, encodedUnparsedPayloadWithDot) = B8.break (== '.') signedRequest-     ('.', encodedUnparsedPayload) <--       MaybeT $ return (B8.uncons encodedUnparsedPayloadWithDot)-     signature <- eitherToMaybeT $ Base64URL.decode $ addBase64Padding encodedSignature-     unparsedPayload <- eitherToMaybeT $ Base64URL.decode $ addBase64Padding encodedUnparsedPayload-     payload <- eitherToMaybeT $ AB.parseOnly json' unparsedPayload+   do+    let (encodedSignature, encodedUnparsedPayloadWithDot) =+          B8.break (== '.') signedRequest+    ('.', encodedUnparsedPayload) <-+      MaybeT $ return (B8.uncons encodedUnparsedPayloadWithDot)+    signature <-+      eitherToMaybeT $+      convertFromBase Base64 $ addBase64Padding encodedSignature+    unparsedPayload <-+      eitherToMaybeT $+      convertFromBase Base64 $ addBase64Padding encodedUnparsedPayload+    payload <- eitherToMaybeT $ AB.parseOnly json' unparsedPayload      -- Verify signature-     SignedRequestAlgorithm algo <- fromJson payload-     guard (algo == "HMAC-SHA256")-     hmacKey <- credsToHmacKey `liftM` lift getCreds-     let expectedSignature = Cereal.encode $ hmac' hmacKey encodedUnparsedPayload-     guard (signature `constTimeEq` expectedSignature)+    SignedRequestAlgorithm algo <- fromJson payload+    guard (algo == "HMAC-SHA256")+    creds <- lift getCreds+    let hmacKey = credsToHmacKey creds+        expectedSignature = hmac hmacKey encodedUnparsedPayload :: HMAC SHA256+    guard ((signature :: ScrubbedBytes) == (convert expectedSignature))      -- Parse user data type-     fromJson payload+    fromJson payload   where-    eitherToMaybeT-      :: Monad m-      => Either a b -> MaybeT m b+    eitherToMaybeT :: Monad m => Either a b -> MaybeT m b     eitherToMaybeT = MaybeT . return . either (const Nothing) Just-    fromJson-      :: (AE.FromJSON a, Monad m)-      => AE.Value -> MaybeT m a+    fromJson :: (AE.FromJSON a, Monad m) => AE.Value -> MaybeT m a     fromJson = eitherToMaybeT . AE.parseEither AE.parseJSON-    credsToHmacKey :: Credentials -> MacKey ctx SHA256-    credsToHmacKey = MacKey . appSecretBS+    -- credsToHmacKey :: Credentials -> MacKey ctx SHA256+    credsToHmacKey = appSecretBS  newtype SignedRequestAlgorithm =   SignedRequestAlgorithm Text@@ -393,8 +397,8 @@     drem = B.length bs `mod` 4  -- | Get detailed information about an access token.-debugToken-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+debugToken ::+     (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)   => AppAccessToken -- ^ Your app access token.   -> AccessTokenData -- ^ The access token you want to debug.   -> FacebookT Auth m DebugToken@@ -406,32 +410,31 @@   let muserToken =         UserAccessToken <$> dtUserId ret <*> return userTokenData <*>         dtExpiresAt ret-  return-    ret-    { dtAccessToken = muserToken-    }+  return ret {dtAccessToken = muserToken}  -- | Helper used in 'debugToken'.  Unfortunately, we can't use 'Pager' here.-data Undata a = Undata-  { undata :: a-  }+data Undata a =+  Undata+    { undata :: a+    } -instance AE.FromJSON a =>-         AE.FromJSON (Undata a) where+instance AE.FromJSON a => AE.FromJSON (Undata a) where   parseJSON (AE.Object v) = Undata <$> v AE..: "data"   parseJSON _ = mzero  -- | Detailed information about an access token (cf. 'debugToken').-data DebugToken = DebugToken-  { dtAppId :: Maybe Text-  , dtAppName :: Maybe Text-  , dtExpiresAt :: Maybe UTCTime-  , dtIsValid :: Maybe Bool-  , dtIssuedAt :: Maybe UTCTime-  , dtScopes :: Maybe [Permission]-  , dtUserId :: Maybe Id-  , dtAccessToken :: Maybe UserAccessToken-  } deriving (Eq, Ord, Show, Typeable)+data DebugToken =+  DebugToken+    { dtAppId :: Maybe Text+    , dtAppName :: Maybe Text+    , dtExpiresAt :: Maybe UTCTime+    , dtIsValid :: Maybe Bool+    , dtIssuedAt :: Maybe UTCTime+    , dtScopes :: Maybe [Permission]+    , dtUserId :: Maybe Id+    , dtAccessToken :: Maybe UserAccessToken+    }+  deriving (Eq, Ord, Show, Typeable)  -- | Note: this instance always sets 'dtAccessToken' to -- 'Nothing', but 'debugToken' will update this field before
src/Facebook/Base.hs view
@@ -2,6 +2,7 @@ {-#LANGUAGE FlexibleContexts#-} {-#LANGUAGE OverloadedStrings#-} {-#LANGUAGE CPP#-}+ module Facebook.Base     ( fbreq     , ToSimpleQuery(..)@@ -12,15 +13,12 @@     , fbhttp     , fbhttpHelper     , httpCheck-    , apiVersion     ) where  import Control.Applicative-import Control.Monad (mzero) import Control.Monad.IO.Class (MonadIO) import Data.ByteString.Char8 (ByteString) import Data.Text (Text)-import Data.Typeable (Typeable)  import qualified UnliftIO.Exception as E import Control.Monad.Trans.Class (MonadTrans)@@ -47,17 +45,23 @@ import Facebook.Types import Facebook.Monad -apiVersion :: Text-apiVersion = "v2.8"- -- | A plain 'H.Request' to a Facebook API.  Use this instead of -- 'def' when creating new 'H.Request'@s@ for Facebook.-fbreq :: Monad m =>-         Text                        -- ^ Path. Should start from "/".+fbreq :: MonadIO m+      => Text                        -- ^ Path. Should start from "/".       -> Maybe (AccessToken anyKind) -- ^ Access token.       -> HT.SimpleQuery              -- ^ Parameters.       -> FacebookT anyAuth m H.Request-fbreq path mtoken query =+fbreq path mtoken query = do++    apiVersion <- getApiVersion+    creds      <- getMCreds++    let appSecretProofAdder = case creds of+          Just c@( Credentials _ _ _ True ) -> addAppSecretProof c+          _ -> const id ++     withTier $ \tier ->       let host = case tier of                    Production -> "graph.facebook.com"@@ -68,8 +72,8 @@              , H.path          = TE.encodeUtf8 ("/" <> apiVersion <> path)              , H.redirectCount = 3              , H.queryString   =-                 HT.renderSimpleQuery False $-                 maybe id tsq mtoken query+                 HT.renderSimpleQuery False+                 $ appSecretProofAdder mtoken $ maybe id tsq mtoken query #if MIN_VERSION_http_client(0,5,0)              , H.responseTimeout = H.responseTimeoutMicro 120000000 -- 2 minutes #else@@ -126,26 +130,6 @@         H.Response (C.ConduitT () ByteString m ())      -> FacebookT anyAuth m ByteString asBS response = lift $ C.runConduit $ H.responseBody response .| fmap B.concat CL.consume----- | An exception that may be thrown by functions on this--- package.  Includes any information provided by Facebook.-data FacebookException =-    -- | An exception coming from Facebook.-    FacebookException { fbeType    :: Text-                      , fbeMessage :: Text-                      }-    -- | An exception coming from the @fb@ package's code.-  | FbLibraryException { fbeMessage :: Text }-    deriving (Eq, Ord, Show, Read, Typeable)--instance A.FromJSON FacebookException where-    parseJSON (A.Object v) =-        FacebookException <$> v A..: "type"-                          <*> v A..: "message"-    parseJSON _ = mzero--instance E.Exception FacebookException where   -- | Same as 'H.http', but tries to parse errors and throw
src/Facebook/Monad.hs view
@@ -8,6 +8,7 @@ {-# LANGUAGE InstanceSigs #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}  module Facebook.Monad   ( FacebookT@@ -18,69 +19,86 @@   , runNoAuthFacebookT   , beta_runFacebookT   , beta_runNoAuthFacebookT+  , getApiVersion   , getCreds+  , getMCreds   , getManager   , getTier   , withTier+  , addAppSecretProof+  , makeAppSecretProof   , runResourceInFb   , mapFacebookT+  , setApiVersion    -- * Re-export   , lift   ) where -import Control.Applicative (Applicative, Alternative)+import Control.Applicative (Alternative, Applicative) import Control.Monad (MonadPlus, liftM) import Control.Monad.Base (MonadBase(..))+import Control.Monad.Fail (MonadFail(..)) import Control.Monad.Fix (MonadFix) import Control.Monad.IO.Class (MonadIO) import Control.Monad.Logger (MonadLogger(..)) import Control.Monad.Trans.Class (MonadTrans(lift))-import UnliftIO import Control.Monad.Trans.Reader (ReaderT(..), ask, mapReaderT)-import Data.Typeable (Typeable) import qualified Control.Monad.Trans.Resource as R--import qualified Network.HTTP.Conduit as H-+import Crypto.Hash.Algorithms (SHA256)+import Crypto.MAC.HMAC (HMAC(..), hmac)+import Data.ByteArray.Encoding (Base(..), convertToBase)+import qualified Data.Text.Encoding as TE+import Data.Typeable (Typeable) import Facebook.Types+import qualified Network.HTTP.Conduit as H+import qualified Network.HTTP.Types as HT+import UnliftIO+import qualified UnliftIO.Exception as E  -- | @FacebookT auth m a@ is this library's monad transformer. -- Contains information needed to issue commands and queries to -- Facebook.  The phantom type @auth@ may be either 'Auth' (you -- have supplied your 'Credentials') or 'NoAuth' (you have not -- supplied any 'Credentials').-newtype FacebookT auth m a = F-  { unF :: ReaderT FbData m a -- FbData -> m a-  } deriving (Functor, Applicative, Alternative, Monad, MonadFix, MonadPlus, MonadIO, MonadTrans, R.MonadThrow)+newtype FacebookT auth m a =+  F+    { unF :: ReaderT FbData m a -- FbData -> m a+    }+  deriving ( Functor+           , Applicative+           , Alternative+           , Monad+           , MonadFix+           , MonadPlus+           , MonadIO+           , MonadTrans+           , R.MonadThrow+           , MonadFail+           )  deriving instance          (R.MonadResource m, MonadBase IO m) =>          R.MonadResource (FacebookT auth m) -instance MonadBase b m =>-         MonadBase b (FacebookT auth m) where+instance MonadBase b m => MonadBase b (FacebookT auth m) where   liftBase = lift . liftBase  -- askUnliftIO = ReaderT $ \r -> --                 withUnliftIO $ \u -> -- return (UnliftIO (unliftIO u . flip runReaderT r))-instance (MonadIO m, MonadUnliftIO m) =>-         MonadUnliftIO (FacebookT auth m) where+instance (MonadIO m, MonadUnliftIO m) => MonadUnliftIO (FacebookT auth m) where   askUnliftIO :: FacebookT auth m (UnliftIO (FacebookT auth m))   askUnliftIO =     F-      (ReaderT $-       \(r :: FbData) ->-          withUnliftIO $-          \(u :: UnliftIO m) ->-             return-               (UnliftIO-                  (\(x :: FacebookT auth m a) ->-                      (unliftIO u ((flip runReaderT r) (unF x))))))+      (ReaderT $ \(r :: FbData) ->+         withUnliftIO $ \(u :: UnliftIO m) ->+           return+             (UnliftIO+                (\(x :: FacebookT auth m a) ->+                   (unliftIO u ((flip runReaderT r) (unF x))))))  -- | Since @fb-0.14.8@.-instance MonadLogger m =>-         MonadLogger (FacebookT auth m) where+instance MonadLogger m => MonadLogger (FacebookT auth m) where   monadLoggerLog loc src lvl msg = lift (monadLoggerLog loc src lvl msg)  -- | Phantom type stating that you have provided your@@ -95,11 +113,14 @@   deriving (Typeable)  -- | Internal data kept inside 'FacebookT'.-data FbData = FbData-  { fbdCreds :: Credentials -- ^ Can be 'undefined'!-  , fbdManager :: !H.Manager-  , fbdTier :: !FbTier-  } deriving (Typeable)+data FbData =+  FbData+    { fbdCreds :: Maybe Credentials+    , fbdManager :: !H.Manager+    , fbdTier :: !FbTier+    , fbdApiVersion :: IORef ApiVersion+    }+  deriving (Typeable)  -- | Which Facebook tier should be used (see -- <https://developers.facebook.com/support/beta-tier/>).@@ -108,64 +129,114 @@   | Beta   deriving (Eq, Ord, Show, Read, Enum, Typeable) +defaultApiVersion :: ApiVersion+defaultApiVersion = "v3.2"++-- | Set the Graph API version.+setApiVersion :: (MonadIO m) => ApiVersion -> FacebookT anyAuth m ()+setApiVersion apiVersion = do+  ref <- fbdApiVersion `liftM` F ask+  atomicModifyIORef' ref (\_ -> (apiVersion, ()))+  return ()+ -- | Run a computation in the 'FacebookT' monad transformer with -- your credentials.-runFacebookT-  :: Credentials -- ^ Your app's credentials.+runFacebookT ::+     (MonadIO m)+  => Credentials -- ^ Your app's credentials.   -> H.Manager -- ^ Connection manager (see 'H.withManager').   -> FacebookT Auth m a   -> m a-runFacebookT creds manager (F act) =-  runReaderT act (FbData creds manager Production)+runFacebookT creds manager (F act) = do+  apiref <- newIORef defaultApiVersion+  runReaderT act (FbData (Just creds) manager Production apiref) +addAppSecretProof ::+     Credentials+  -> Maybe (AccessToken anykind)+  -> HT.SimpleQuery+  -> HT.SimpleQuery+addAppSecretProof (Credentials _ _ _ False) _ query = query+addAppSecretProof creds mtoken query = makeAppSecretProof creds mtoken <> query++-- | Make an appsecret_proof in case the given credentials access token is a+-- user access token.+-- See: https://developers.facebook.com/docs/graph-api/securing-requests/#appsecret_proof+makeAppSecretProof ::+     Credentials -- ^ App credentials+  -> Maybe (AccessToken anyKind) -- ^+  -> HT.SimpleQuery+makeAppSecretProof creds (Just (UserAccessToken _ accessToken _)) =+  [(TE.encodeUtf8 "appsecret_proof", proof)]+  where+    hmacData :: HMAC SHA256+    hmacData = hmac (appSecretBS creds) (TE.encodeUtf8 accessToken)+    proof = convertToBase Base16 hmacData+makeAppSecretProof _ _ = []+ -- | Run a computation in the 'FacebookT' monad without -- credentials.-runNoAuthFacebookT :: H.Manager -> FacebookT NoAuth m a -> m a-runNoAuthFacebookT manager (F act) =-  let creds = error "runNoAuthFacebookT: never here, serious bug"-  in runReaderT act (FbData creds manager Production)+runNoAuthFacebookT ::+     (MonadIO m)+  => H.Manager -- ^ Connection manager (see 'H.withManager').+  -> FacebookT NoAuth m a+  -> m a+runNoAuthFacebookT manager (F act) = do+  apiref <- newIORef defaultApiVersion+  runReaderT act (FbData Nothing manager Production apiref)  -- | Same as 'runFacebookT', but uses Facebook's beta tier (see -- <https://developers.facebook.com/support/beta-tier/>).-beta_runFacebookT :: Credentials -> H.Manager -> FacebookT Auth m a -> m a-beta_runFacebookT creds manager (F act) =-  runReaderT act (FbData creds manager Beta)+beta_runFacebookT ::+     (MonadIO m) => Credentials -> H.Manager -> FacebookT Auth m a -> m a+beta_runFacebookT creds manager (F act) = do+  apiref <- newIORef defaultApiVersion+  runReaderT act (FbData (Just creds) manager Beta apiref)  -- | Same as 'runNoAuthFacebookT', but uses Facebook's beta tier -- (see <https://developers.facebook.com/support/beta-tier/>).-beta_runNoAuthFacebookT :: H.Manager -> FacebookT NoAuth m a -> m a-beta_runNoAuthFacebookT manager (F act) =-  let creds = error "beta_runNoAuthFacebookT: never here, serious bug"-  in runReaderT act (FbData creds manager Beta)+beta_runNoAuthFacebookT ::+     (MonadIO m) => H.Manager -> FacebookT NoAuth m a -> m a+beta_runNoAuthFacebookT manager (F act) = do+  apiref <- newIORef defaultApiVersion+  runReaderT act (FbData Nothing manager Beta apiref) +-- | Get the user's credentials, fail if they are not available.+getCreds :: (Monad m, MonadIO m) => FacebookT Auth m Credentials+getCreds = do+  mCreds <- getMCreds+  case mCreds of+    Nothing -> E.throwIO $ FbLibraryException "Couldn't get credentials."+    Just creds -> return creds+ -- | Get the user's credentials.-getCreds-  :: Monad m-  => FacebookT Auth m Credentials-getCreds = fbdCreds `liftM` F ask+getMCreds :: Monad m => FacebookT anyAuth m (Maybe Credentials)+getMCreds = fbdCreds `liftM` F ask +-- | Get the Graph API version.+getApiVersion :: MonadIO m => FacebookT anyAuth m ApiVersion+getApiVersion = do+  ref <- fbdApiVersion `liftM` F ask+  apiVersion <- readIORef ref+  pure apiVersion+ -- | Get the 'H.Manager'.-getManager-  :: Monad m-  => FacebookT anyAuth m H.Manager+getManager :: Monad m => FacebookT anyAuth m H.Manager getManager = fbdManager `liftM` F ask  -- | Get the 'FbTier'.-getTier-  :: Monad m-  => FacebookT anyAuth m FbTier+getTier :: Monad m => FacebookT anyAuth m FbTier getTier = fbdTier `liftM` F ask  -- | Run a pure function that depends on the 'FbTier' being used.-withTier-  :: Monad m-  => (FbTier -> a) -> FacebookT anyAuth m a+withTier :: Monad m => (FbTier -> a) -> FacebookT anyAuth m a withTier = flip liftM getTier  -- | Run a 'ResourceT' inside a 'FacebookT'.-runResourceInFb-  :: (R.MonadResource m, MonadUnliftIO m)-  => FacebookT anyAuth (R.ResourceT m) a -> FacebookT anyAuth m a+runResourceInFb ::+     (R.MonadResource m, MonadUnliftIO m)+  => FacebookT anyAuth (R.ResourceT m) a+  -> FacebookT anyAuth m a runResourceInFb (F inner) = F $ ask >>= lift . R.runResourceT . runReaderT inner  -- | Transform the computation inside a 'FacebookT'.
src/Facebook/Object/Action.hs view
@@ -8,6 +8,7 @@   ) where  import Control.Arrow (first)+import Control.Monad.IO.Class import Data.Function (on) import Data.String (IsString(..)) import Data.Text (Text)@@ -27,7 +28,7 @@ -- >              , "when"   #= now ] -- >              token createAction-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, MonadIO m)   => Action -- ^ Action kind to be created.   -> [Argument] -- ^ Arguments of the action.   -> Maybe AppAccessToken
src/Facebook/Object/Page.hs view
@@ -72,7 +72,7 @@   -> [Argument] -- ^ Arguments to be passed to Facebook   -> Maybe AppAccessToken -- ^ Optional user access token   -> FacebookT anyAuth m Page-getPage_ id_ = getObject $ ("/" <> idCode id_)+getPage_ id_ = getObject $ "/" <> idCode id_  -- | Search pages by keyword. The user access token is optional. searchPages
src/Facebook/RealTime.hs view
@@ -17,30 +17,31 @@  import Control.Applicative ((<$>), (<*>)) import Control.Monad (liftM, mzero, void)-import Crypto.Hash.CryptoAPI (SHA1)-import Data.ByteString.Char8 (ByteString)-import Data.Text (Text)-import Data.Text.Encoding (encodeUtf8)-import Data.Typeable (Typeable)-+import Control.Monad.IO.Class import qualified Control.Monad.Trans.Resource as R-import qualified Crypto.Classes as Crypto-import qualified Crypto.HMAC as Crypto+import Crypto.Hash.Algorithms (SHA1)+import Crypto.MAC.HMAC (HMAC(..), hmac) import qualified Data.Aeson as A-import qualified Data.ByteString.Base16 as Base16+import Data.ByteArray (ScrubbedBytes, convert)+import Data.ByteArray.Encoding (Base(..), convertToBase)+import qualified Data.ByteString as B+import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.Conduit as C import qualified Data.Conduit.List as CL+import Data.Text (Text) import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8) import qualified Data.Text.Encoding as TE+import Data.Typeable (Typeable) import qualified Network.HTTP.Conduit as H import qualified Network.HTTP.Types as HT -import Facebook.Types-import Facebook.Monad import Facebook.Base import Facebook.Graph+import Facebook.Monad import Facebook.Pager+import Facebook.Types  -- | The type of objects that a real-time update refers to. data RealTimeUpdateObject@@ -89,8 +90,8 @@ -- If there was any previous subscription for the given -- 'RealTimeUpdateObject', it's overriden by this one (even if -- the other subscription had a different callback URL).-modifySubscription-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+modifySubscription ::+     (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)   => RealTimeUpdateObject      -- ^ Type of objects whose subscription you      -- and to add or modify.@@ -112,31 +113,27 @@         , "callback_url" #= callbackUrl         , "verify_token" #= verifyToken         ]-  runResourceInFb $-    do req <- fbreq path (Just apptoken) args-       void $-         fbhttp-           req-           { H.method = HT.methodPost-           }+  runResourceInFb $ do+    req <- fbreq path (Just apptoken) args+    void $ fbhttp req {H.method = HT.methodPost}   return ()  -- | (Internal)  Get the subscription's path.-getSubscriptionsPath-  :: Monad m-  => FacebookT Auth m Text+getSubscriptionsPath :: (Monad m, MonadIO m) => FacebookT Auth m Text getSubscriptionsPath = do   creds <- getCreds   return $ T.concat ["/", appId creds, "/subscriptions"]  -- | Information returned by Facebook about a real-time update -- notification subscription.-data RealTimeUpdateSubscription = RealTimeUpdateSubscription-  { rtusObject :: RealTimeUpdateObject-  , rtusCallbackUrl :: RealTimeUpdateUrl-  , rtusFields :: [RealTimeUpdateField]-  , rtusActive :: Bool-  } deriving (Eq, Ord, Show, Typeable)+data RealTimeUpdateSubscription =+  RealTimeUpdateSubscription+    { rtusObject :: RealTimeUpdateObject+    , rtusCallbackUrl :: RealTimeUpdateUrl+    , rtusFields :: [RealTimeUpdateField]+    , rtusActive :: Bool+    }+  deriving (Eq, Ord, Show, Typeable)  instance A.FromJSON RealTimeUpdateSubscription where   parseJSON (A.Object v) =@@ -146,24 +143,25 @@   parseJSON _ = mzero  -- | List current real-time update subscriptions.-listSubscriptions-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)-  => AppAccessToken -> FacebookT Auth m [RealTimeUpdateSubscription]+listSubscriptions ::+     (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+  => AppAccessToken+  -> FacebookT Auth m [RealTimeUpdateSubscription] listSubscriptions apptoken = do   path <- getSubscriptionsPath   pager <- getObject path [] (Just apptoken)   src <- fetchAllNextPages pager   lift $ C.runConduit $ src C..| CL.consume --- | Verifies the input's authenticity (i.e. it comes from+-- | Verifi(es the input's authenticity (i.e. it comes from, MonadIO m) -- Facebook) and integrity by calculating its HMAC-SHA1 (using -- your application secret as the key) and verifying that it -- matches the value from the HTTP request's @X-Hub-Signature@ -- header's value.  If it's not valid, @Nothing@ is returned, -- otherwise @Just data@ is returned where @data@ is the original -- data.-verifyRealTimeUpdateNotifications-  :: Monad m+verifyRealTimeUpdateNotifications ::+     (Monad m, MonadIO m)   => ByteString      -- ^ @X-Hub-Signature@ HTTP header's value.   -> L.ByteString@@ -171,12 +169,13 @@   -> FacebookT Auth m (Maybe L.ByteString) verifyRealTimeUpdateNotifications sig body = do   creds <- getCreds-  let key :: Crypto.MacKey ctx SHA1-      key = Crypto.MacKey (appSecretBS creds)-      hash = Crypto.hmac key body-      expected = "sha1=" <> Base16.encode (Crypto.encode hash)+  let hmacData :: HMAC SHA1+      hmacData = hmac (appSecretBS creds) (L.toStrict body)+      hash :: B.ByteString+      hash = convertToBase Base16 hmacData+      expected = "sha1=" <> hash   return $!-    if sig `Crypto.constTimeEq` expected+    if ((convert sig :: ScrubbedBytes) == (convert expected))       then Just body       else Nothing @@ -185,14 +184,15 @@ -- signature is invalid or the data can't be parsed (use -- 'verifyRealTimeUpdateNotifications' if you need to distinguish -- between these two error conditions).-getRealTimeUpdateNotifications-  :: (Monad m, A.FromJSON a)+getRealTimeUpdateNotifications ::+     (Monad m, A.FromJSON a, MonadIO m)   => ByteString      -- ^ @X-Hub-Signature@ HTTP header's value.   -> L.ByteString      -- ^ Request body with JSON-encoded notifications.   -> FacebookT Auth m (Maybe (RealTimeUpdateNotification a))-getRealTimeUpdateNotifications = (liftM (>>= A.decode) .) . verifyRealTimeUpdateNotifications+getRealTimeUpdateNotifications =+  (liftM (>>= A.decode) .) . verifyRealTimeUpdateNotifications  -- | When data changes and there's a valid subscription, Facebook -- will @POST@ to your 'RealTimeUpdateUrl' with a JSON-encoded@@ -207,23 +207,26 @@ -- the value of 'rtunObject'. -- -- We recommend using 'getRealTimeUpdateNotifications'.-data RealTimeUpdateNotification a = RealTimeUpdateNotification-  { rtunObject :: RealTimeUpdateObject-  , rtunEntries :: [a]-  } deriving (Eq, Ord, Show, Typeable)+data RealTimeUpdateNotification a =+  RealTimeUpdateNotification+    { rtunObject :: RealTimeUpdateObject+    , rtunEntries :: [a]+    }+  deriving (Eq, Ord, Show, Typeable) -instance A.FromJSON a =>-         A.FromJSON (RealTimeUpdateNotification a) where+instance A.FromJSON a => A.FromJSON (RealTimeUpdateNotification a) where   parseJSON (A.Object v) =     RealTimeUpdateNotification <$> v A..: "object" <*> v A..: "entry"   parseJSON _ = mzero  -- | A notification for the 'UserRTUO' object.-data RealTimeUpdateNotificationUserEntry = RealTimeUpdateNotificationUserEntry-  { rtuneUserId :: Id-  , rtuneChangedFields :: [RealTimeUpdateField]-  , rtuneTime :: Integer-  } deriving (Eq, Ord, Show, Typeable)+data RealTimeUpdateNotificationUserEntry =+  RealTimeUpdateNotificationUserEntry+    { rtuneUserId :: Id+    , rtuneChangedFields :: [RealTimeUpdateField]+    , rtuneTime :: Integer+    }+  deriving (Eq, Ord, Show, Typeable)  instance A.FromJSON RealTimeUpdateNotificationUserEntry where   parseJSON (A.Object v) =
src/Facebook/TestUsers.hs view
@@ -16,6 +16,7 @@  import Control.Applicative ((<$>), (<*>)) import Control.Monad (unless, mzero)+import Control.Monad.IO.Class import Data.ByteString.Lazy (fromStrict) import Data.Default import Data.Text@@ -94,7 +95,7 @@ -- | Create a new test user. -- Ref: https://developers.facebook.com/docs/graph-api/reference/v2.8/app/accounts/test-users#publish createTestUser-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, MonadIO m)   => CreateTestUser -- ^ How the test user should be      -- created.   -> AppAccessToken -- ^ Access token for your app.@@ -106,7 +107,7 @@  -- | Get a list of test users. getTestUsers-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m, MonadIO m)   => AppAccessToken -- ^ Access token for your app.   -> FacebookT Auth m (Pager TestUser) getTestUsers token = do@@ -114,7 +115,7 @@   getObject ("/" <> appId creds <> "/accounts/test-users") [] (Just token)  disassociateTestuser-  :: (R.MonadUnliftIO m, R.MonadThrow m, R.MonadResource m)+  :: (R.MonadUnliftIO m, R.MonadThrow m, R.MonadResource m, MonadIO m)   => TestUser -> AppAccessToken -> FacebookT Auth m Bool disassociateTestuser testUser _token = do   creds <- getCreds
src/Facebook/Types.hs view
@@ -7,6 +7,7 @@  module Facebook.Types   ( Credentials(..)+  , ApiVersion   , appIdBS   , appSecretBS   , AccessToken(..)@@ -23,10 +24,12 @@   , Argument   , (<>)   , FbUTCTime(..)+  , FacebookException(..)   ) where  import Control.Applicative ((<$>), (<*>), pure) import Control.Monad (mzero)+import qualified UnliftIO.Exception as E import Data.ByteString (ByteString) import Data.Int (Int64) #if !(MIN_VERSION_base(4,11,0))@@ -57,6 +60,7 @@   { appName :: Text -- ^ Your application name (e.g. for Open Graph calls).   , appId :: Text -- ^ Your application ID.   , appSecret :: Text -- ^ Your application secret key.+  , appSecretProof :: Bool -- ^ To enable app secret proof verification   } deriving (Eq, Ord, Show, Read, Typeable)  -- | 'appId' for 'ByteString'.@@ -67,6 +71,11 @@ appSecretBS :: Credentials -> ByteString appSecretBS = TE.encodeUtf8 . appSecret ++-- | Graph API version.+-- See: https://developers.facebook.com/docs/graph-api/changelog+type ApiVersion = Text+ -- | An access token.  While you can make some API calls without -- an access token, many require an access token and some will -- give you more information with an appropriate access token.@@ -234,3 +243,23 @@   parseJSON _ =     fail       "could not parse FbUTCTime from something which is not a string or number"++-- | An exception that may be thrown by functions on this+-- package.  Includes any information provided by Facebook.+data FacebookException =+    -- | An exception coming from Facebook.+    FacebookException { fbeType    :: Text+                      , fbeMessage :: Text+                      }+    -- | An exception coming from the @fb@ package's code.+  | FbLibraryException { fbeMessage :: Text }+    deriving (Eq, Ord, Show, Read, Typeable)++instance A.FromJSON FacebookException where+    parseJSON (A.Object v) =+        FacebookException <$> v A..: "type"+                          <*> v A..: "message"+    parseJSON _ = mzero++instance E.Exception FacebookException where+
tests/Main.hs view
@@ -6,22 +6,9 @@   , getCredentials   ) where -import Control.Applicative import Control.Monad (mzero) import Control.Monad.IO.Class (MonadIO(liftIO)) import Control.Monad.Trans.Class (lift)-import Data.Function (on)-import Data.Int (Int8, Int16, Int32, Int64)-import Data.Maybe (isJust, isNothing)-import Data.Text (Text)-import Data.Time (parseTime)-import Data.Word (Word, Word8, Word16, Word32, Word64)-import System.Environment (getEnv)-import System.Exit (exitFailure)-import System.IO.Error (isDoesNotExistError)-import Data.List ((\\))-import Data.Monoid ((<>))-import qualified UnliftIO.Exception as E import qualified Control.Monad.Trans.Resource as R import qualified Data.Aeson as A import qualified Data.Aeson.Types as A@@ -29,15 +16,24 @@ import qualified Data.Conduit as C import qualified Data.Conduit.List as CL import qualified Data.Default as D+import Data.Int (Int16, Int32, Int64, Int8)+import Data.List ((\\)) import qualified Data.Map as Map+import Data.Maybe (isJust, isNothing) import qualified Data.Maybe as M-import qualified Data.Set as S+import Data.Monoid ((<>))+import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Time as TI+import Data.Word (Word, Word16, Word32, Word64, Word8) import qualified Facebook as FB import qualified Network.HTTP.Conduit as H+import System.Environment (getEnv)+import System.Exit (exitFailure)+import System.IO.Error (isDoesNotExistError) import qualified Test.QuickCheck as QC+import qualified UnliftIO.Exception as E  import Test.HUnit ((@?=)) import Test.Hspec@@ -50,7 +46,8 @@     tryToGet = do       [appName, appId, appSecret] <-         mapM getEnv ["APP_NAME", "APP_ID", "APP_SECRET"]-      return $ FB.Credentials (T.pack appName) (T.pack appId) (T.pack appSecret)+      return $+        FB.Credentials (T.pack appName) (T.pack appId) (T.pack appSecret) True     showHelp exc       | not (isDoesNotExistError exc) = E.throwIO exc     showHelp _ = do@@ -78,12 +75,13 @@       exitFailure  invalidCredentials :: FB.Credentials-invalidCredentials = FB.Credentials "this" "isn't" "valid"+invalidCredentials = FB.Credentials "this" "isn't" "valid" False  invalidUserAccessToken :: FB.UserAccessToken-invalidUserAccessToken = FB.UserAccessToken (FB.Id "invalid") "user" farInTheFuture+invalidUserAccessToken =+  FB.UserAccessToken (FB.Id "invalid") "user" farInTheFuture   where-    Just farInTheFuture = parseTime (error "farInTheFuture") "%Y" "3000"+    Just farInTheFuture = TI.parseTimeM True TI.defaultTimeLocale "%Y" "3000"  -- It's actually important to use 'farInTheFuture' since we -- don't want any tests rejecting this invalid user access@@ -94,28 +92,29 @@ main :: IO () main = do   manager <- H.newManager H.tlsManagerSettings-  liftIO $-    do creds <- getCredentials-       hspec $+  liftIO $ do+    creds <- getCredentials+    hspec $        -- Run the tests twice, once in Facebook's production tier...-         do facebookTests-              "Production tier: "-              creds-              manager-              (R.runResourceT . FB.runFacebookT creds manager)-              (R.runResourceT . FB.runNoAuthFacebookT manager)+     do+      facebookTests+        "Production tier: "+        creds+        manager+        (R.runResourceT . (FB.runFacebookT creds manager))+        (R.runResourceT . (FB.runNoAuthFacebookT manager))             -- ...and the other in Facebook's beta tier.-            facebookTests-              "Beta tier: "-              creds-              manager-              (R.runResourceT . FB.beta_runFacebookT creds manager)-              (R.runResourceT . FB.beta_runNoAuthFacebookT manager)+      facebookTests+        "Beta tier: "+        creds+        manager+        (R.runResourceT . (FB.beta_runFacebookT creds manager))+        (R.runResourceT . (FB.beta_runNoAuthFacebookT manager))             -- Tests that don't depend on which tier is chosen.-            libraryTests manager+      libraryTests manager -facebookTests-  :: String+facebookTests ::+     String   -> FB.Credentials   -> H.Manager   -> (forall a. FB.FacebookT FB.Auth (R.ResourceT IO) a -> IO a)@@ -123,247 +122,233 @@   -> Spec facebookTests pretitle creds manager runAuth runNoAuth = do   let describe' = describe . (pretitle ++)-  describe' "getAppAccessToken" $-    do it "works and returns a valid app access token" $-         runAuth $-         do token <- FB.getAppAccessToken-            FB.isValid token #?= True-       it "throws a FacebookException on invalid credentials" $-         R.runResourceT $-         FB.runFacebookT invalidCredentials manager $-         do ret <- E.try $ FB.getAppAccessToken-            case ret of-              Right token -> fail $ show token-              Left (_ :: FB.FacebookException) ->-                lift $ lift (return () :: IO ())-  describe' "isValid" $-    do it "returns False on a clearly invalid user access token" $-         runNoAuth $ FB.isValid invalidUserAccessToken #?= False-       it "returns False on a clearly invalid app access token" $-         runNoAuth $ FB.isValid invalidAppAccessToken #?= False-  describe' "debugToken" $-    do it "works on a test user access token" $-         do runAuth $-              withTestUser D.def $-              \testUser -> do-                Just testUserAccessTokenData <--                  return (FB.tuAccessToken testUser)-                appToken <- FB.getAppAccessToken-                ret <- FB.debugToken appToken testUserAccessTokenData-                now <- liftIO TI.getCurrentTime-                FB.dtAppId ret &?= Just (FB.appId creds)-                FB.dtAppName ret &?= Just (FB.appName creds)-                case FB.dtExpiresAt ret of-                  Nothing -> fail "dtExpiresAt is Nothing"-                  Just t -> compare t now &?= GT-                FB.dtIsValid ret &?= Just True-                case FB.dtIssuedAt ret of-                  Nothing -> return () -- ok since it's a test user-                  Just t -> compare t now &?= LT-                isJust (FB.dtScopes ret) &?= True-                FB.dtUserId ret &?= Just (FB.tuId testUser)-                case FB.dtAccessToken ret of-                  Nothing -> fail "dtAccessToken is Nothing"-                  Just t -> do-                    let f-                          :: FB.UserAccessToken-                          -> FB.FacebookT FB.Auth (R.ResourceT IO) ()-                        f (FB.UserAccessToken uid dt exps) = do-                          uid &?= FB.tuId testUser-                          dt &?= testUserAccessTokenData-                          Just exps &?= FB.dtExpiresAt ret-                    f t-  describe' "getObject" $-    do it "is able to fetch Facebook's own page" $-         do val <--              runAuth $ -- Needs permission now: https://developers.facebook.com/docs/graph-api/reference/page#Reading-              do token <- FB.getAppAccessToken-                 A.Object obj <- FB.getObject "/19292868552" [] (Just token)-                 let Just r =-                       flip A.parseMaybe () $-                       const $ (,) <$> obj A..:? "id" <*> obj A..:? "name"-                 return r-            val `shouldBe`-              ( Just "19292868552" :: Maybe Text-              , Just "Facebook for Developers" :: Maybe Text)-  describe' "getPage" $-    do it "works for FB Developers" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 page <- FB.getPage_ (FB.Id "19292868552") [] (Just token)-                 FB.pageId page &?= (FB.Id "19292868552")-                 FB.pageName page &?= Just "Facebook for Developers"-                 FB.pageCategory page &?= Nothing-                 FB.pageIsPublished page &?= Nothing-                 FB.pageCanPost page &?= Nothing-                 FB.pagePhone page &?= Nothing-                 FB.pageCheckins page &?= Nothing-                 FB.pageWebsite page &?= Nothing-  describe' "listSubscriptions" $-    do it "returns something" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 val <- FB.listSubscriptions token-                 length val `seq` return ()-  describe' "fetchNextPage" $-    do let fetchNextPageWorks :: FB.Pager A.Value-                              -> FB.FacebookT anyAuth (R.ResourceT IO) ()-           fetchNextPageWorks pager-             | isNothing (FB.pagerNext pager) = return ()-             | otherwise =-               FB.fetchNextPage pager >>= maybe not_ (\_ -> return ())-             where-               not_ =-                 fail "Pager had a next page but fetchNextPage didn't work."-       it "seems to work on a public list of comments" $-         do runAuth $+  describe' "getAppAccessToken" $ do+    it "works and returns a valid app access token" $+      runAuth $ do+        token <- FB.getAppAccessToken+        FB.isValid token #?= True+    it "throws a FacebookException on invalid credentials" $+      R.runResourceT $+      FB.runFacebookT invalidCredentials manager $ do+        ret <- E.try $ FB.getAppAccessToken+        case ret of+          Right token -> fail $ show token+          Left (_ :: FB.FacebookException) -> lift $ lift (return () :: IO ())+  describe' "setApiVersion" $ do+    it "Check default apiVersion" $ runNoAuth $ FB.getApiVersion #?= "v3.2"+    it "Change apiVersion" $+      (runNoAuth $ do+         FB.setApiVersion "v100"+         FB.getApiVersion) #?=+      "v100"+  describe' "isValid" $ do+    it "returns False on a clearly invalid user access token" $+      runNoAuth $ FB.isValid invalidUserAccessToken #?= False+    it "returns False on a clearly invalid app access token" $+      runNoAuth $ FB.isValid invalidAppAccessToken #?= False+  describe' "debugToken" $ do+    it "works on a test user access token" $ do+      runAuth $+        withTestUser D.def $ \testUser -> do+          Just testUserAccessTokenData <- return (FB.tuAccessToken testUser)+          appToken <- FB.getAppAccessToken+          ret <- FB.debugToken appToken testUserAccessTokenData+          now <- liftIO TI.getCurrentTime+          FB.dtAppId ret &?= Just (FB.appId creds)+          FB.dtAppName ret &?= Just (FB.appName creds)+          case FB.dtExpiresAt ret of+            Nothing -> fail "dtExpiresAt is Nothing"+            Just t -> compare t now &?= GT+          FB.dtIsValid ret &?= Just True+          case FB.dtIssuedAt ret of+            Nothing -> return () -- ok since it's a test user+            Just t -> compare t now &?= LT+          isJust (FB.dtScopes ret) &?= True+          FB.dtUserId ret &?= Just (FB.tuId testUser)+          case FB.dtAccessToken ret of+            Nothing -> fail "dtAccessToken is Nothing"+            Just t -> do+              let f :: FB.UserAccessToken+                    -> FB.FacebookT FB.Auth (R.ResourceT IO) ()+                  f (FB.UserAccessToken uid dt exps) = do+                    uid &?= FB.tuId testUser+                    dt &?= testUserAccessTokenData+                    Just exps &?= FB.dtExpiresAt ret+              f t+  describe' "getObject" $ do+    it "is able to fetch Facebook's own page" $ do+      val <-+        runAuth $ -- Needs permission now: https://developers.facebook.com/docs/graph-api/reference/page#Reading+         do+          token <- FB.getAppAccessToken+          A.Object obj <- FB.getObject "/220746347971798" [] (Just token)+          let Just r =+                flip A.parseMaybe () $+                const $ (,) <$> obj A..:? "id" <*> obj A..:? "name"+          return r+      val `shouldBe`+        (Just "220746347971798" :: Maybe Text, Just "Ruskin Bond" :: Maybe Text)+  describe' "getPage" $ do+    it "works for FB Developers" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        page <- FB.getPage_ (FB.Id "220746347971798") [] (Just token)+        FB.pageId page &?= (FB.Id "220746347971798")+        FB.pageName page &?= Just "Ruskin Bond"+        FB.pageCategory page &?= Nothing+        FB.pageIsPublished page &?= Nothing+        FB.pageCanPost page &?= Nothing+        FB.pagePhone page &?= Nothing+        FB.pageCheckins page &?= Nothing+        FB.pageWebsite page &?= Nothing+  describe' "listSubscriptions" $ do+    it "returns something" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        val <- FB.listSubscriptions token+        length val `seq` return ()+  describe' "fetchNextPage" $ do+    let fetchNextPageWorks ::+             FB.Pager A.Value -> FB.FacebookT anyAuth (R.ResourceT IO) ()+        fetchNextPageWorks pager+          | isNothing (FB.pagerNext pager) = return ()+          | otherwise = FB.fetchNextPage pager >>= maybe not_ (\_ -> return ())+          where+            not_ = fail "Pager had a next page but fetchNextPage didn't work."+    it "seems to work on a public list of comments" $ do+      runAuth $             -- Postid: https://www.facebook.com/nytimes/posts/10150628170209999             -- Page id found using this technique: https://www.facebook.com/help/community/question/?id=529591157094317-              do token <- FB.getAppAccessToken-                 fetchNextPageWorks =<<-                   FB.getObject-                     "/5281959998_10150628170209999/comments"-                     []-                     (Just token)-       it "seems to work on a private list of app insights" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 fetchNextPageWorks =<<-                   FB.getObject-                     ("/" <> FB.appId creds <> "/app_insights/api_calls")-                     []-                     (Just token)-  describe' "fetchNextPage/fetchPreviousPage" $-    do let backAndForthWorks :: FB.Pager A.Value-                             -> FB.FacebookT anyAuth (R.ResourceT IO) ()-           backAndForthWorks pager = do-             pager2 <- FB.fetchNextPage pager-             case pager2 of-               Nothing -> True &?= True-               Just pager2' -> do-                 Just pager3 <- FB.fetchPreviousPage pager2'-                 pager3 &?= pager-       it "seems to work on a public list of comments" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 backAndForthWorks =<<-                   FB.getObject-                     "/5281959998_10150628170209999/comments"-                     [("filter", "stream")]-                     (Just token)-       it "seems to work on a private list of app insights" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 backAndForthWorks =<<-                   FB.getObject-                     ("/" <> FB.appId creds <> "/app_insights/api_calls")-                     []-                     (Just token)-  describe' "fetchAllNextPages" $-    do let hasAtLeast :: C.Source IO A.Value -> Int -> IO ()-           src `hasAtLeast` n = src C.$$ go n-             where-               go 0 = return ()-               go m = C.await >>= maybe not_ (\_ -> go (m - 1))-               not_ =-                 fail $-                 "Source does not have at least " ++ show n ++ " elements."-       it "seems to work on a public list of comments" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 pager <--                   FB.getObject-                     "/63441126719_10154249531391720/comments"-                     []-                     (Just token)-                 src <- FB.fetchAllNextPages pager-                 liftIO $ src `hasAtLeast` 200 -- items-       it "seems to work on a private list of app insights" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 pager <--                   FB.getObject-                     ("/" <> FB.appId creds <> "/app_insights/api_calls")-                     []-                     (Just token)-                 src <- FB.fetchAllNextPages pager-                 let firstPageElms = length (FB.pagerData pager)-                     hasNextPage = isJust (FB.pagerNext pager)-                 if hasNextPage-                   then liftIO $ src `hasAtLeast` (firstPageElms * 3) -- items-                   else return () -- fail "This isn't an insightful app =(."-  describe' "createTestUser/removeTestUser/getTestUser" $-    do it "creates and removes a new test user" $-         do runAuth $-              do token <- FB.getAppAccessToken+       do+        token <- FB.getAppAccessToken+        fetchNextPageWorks =<<+          FB.getObject "/5281959998_10150628170209999/comments" [] (Just token)+    it "seems to work on a private list of app insights" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        fetchNextPageWorks =<<+          FB.getObject+            ("/" <> FB.appId creds <> "/app_insights/api_calls")+            []+            (Just token)+  describe' "fetchNextPage/fetchPreviousPage" $ do+    let backAndForthWorks ::+             FB.Pager A.Value -> FB.FacebookT anyAuth (R.ResourceT IO) ()+        backAndForthWorks pager = do+          pager2 <- FB.fetchNextPage pager+          case pager2 of+            Nothing -> True &?= True+            Just pager2' -> do+              Just pager3 <- FB.fetchPreviousPage pager2'+              pager3 &?= pager+    it "seems to work on a public list of comments" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        backAndForthWorks =<<+          FB.getObject+            "/5281959998_10150628170209999/comments"+            [("filter", "stream")]+            (Just token)+    it "seems to work on a private list of app insights" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        backAndForthWorks =<<+          FB.getObject+            ("/" <> FB.appId creds <> "/app_insights/api_calls")+            []+            (Just token)+  describe' "fetchAllNextPages" $ do+    let hasAtLeast :: C.ConduitT () A.Value IO () -> Int -> IO ()+        src `hasAtLeast` n = C.runConduit $ src C..| go n+          where+            go 0 = return ()+            go m = C.await >>= maybe not_ (\_ -> go (m - 1))+            not_ =+              fail $ "Source does not have at least " ++ show n ++ " elements."+    it "seems to work on a public list of comments" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        pager <-+          FB.getObject "/63441126719_10154249531391720/comments" [] (Just token)+        src <- FB.fetchAllNextPages pager+        liftIO $ src `hasAtLeast` 200 -- items+    it "seems to work on a private list of app insights" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        pager <-+          FB.getObject+            ("/" <> FB.appId creds <> "/app_insights/api_calls")+            []+            (Just token)+        src <- FB.fetchAllNextPages pager+        let firstPageElms = length (FB.pagerData pager)+            hasNextPage = isJust (FB.pagerNext pager)+        if hasNextPage+          then liftIO $ src `hasAtLeast` (firstPageElms * 3) -- items+          else return () -- fail "This isn't an insightful app =(."+  describe' "createTestUser/removeTestUser/getTestUser" $ do+    it "creates and removes a new test user" $ do+      runAuth $ do+        token <- FB.getAppAccessToken                  -- New test user information-                 let installed =-                       FB.CreateTestUserInstalled-                         ["read_stream", "read_friendlists", "publish_stream"]-                     userInfo =-                       FB.CreateTestUser-                       { FB.ctuInstalled = installed-                       , FB.ctuName = Just "Gabriel"-                       , FB.ctuLocale = Just "en_US"-                       }+        let installed =+              FB.CreateTestUserInstalled+                ["read_stream", "read_friendlists", "publish_stream"]+            userInfo =+              FB.CreateTestUser+                { FB.ctuInstalled = installed+                , FB.ctuName = Just "Gabriel"+                , FB.ctuLocale = Just "en_US"+                }                  -- Create the test user-                 newTestUser <- FB.createTestUser userInfo token-                 let newTestUserToken =-                       (M.fromJust $ FB.incompleteTestUserAccessToken newTestUser)+        newTestUser <- FB.createTestUser userInfo token+        let newTestUserToken =+              (M.fromJust $ FB.incompleteTestUserAccessToken newTestUser)                  -- Get the created user-                 createdUser <--                   FB.getUser (FB.tuId newTestUser) [] (Just newTestUserToken)+        createdUser <-+          FB.getUser (FB.tuId newTestUser) [] (Just newTestUserToken)                  -- Remove the test user-                 removed <- FB.removeTestUser newTestUser token+        removed <- FB.removeTestUser newTestUser token                  -- Check user attributes-                 FB.userId createdUser &?= FB.tuId newTestUser-                 FB.userName createdUser &?= Just "Gabriel"-                 -- FB.userLocale createdUser &?= Just "en_US"   -- fix this test later+        FB.userId createdUser &?= FB.tuId newTestUser+        FB.userName createdUser &?= Just "Gabriel"+        -- FB.userLocale createdUser &?= Just "en_US" -- fix this test later                  -- Check if the token is valid-                 FB.isValid newTestUserToken #?= False-                 removed &?= True-  describe' "makeFriendConn" $-    do it "creates two new test users, makes them friends and deletes them" $-         do runAuth $-              withTestUser D.def $-              \testUser1 ->-                 withTestUser D.def $-                 \testUser2 -> do-                   let Just tokenUser1 = FB.incompleteTestUserAccessToken testUser1-                   let Just tokenUser2 = FB.incompleteTestUserAccessToken testUser2+        FB.isValid newTestUserToken #?= False+        removed &?= True+  describe' "makeFriendConn" $ do+    it "creates two new test users, makes them friends and deletes them" $ do+      runAuth $+        withTestUser D.def $ \testUser1 ->+          withTestUser D.def $ \testUser2 -> do+            let Just tokenUser1 = FB.incompleteTestUserAccessToken testUser1+            let Just tokenUser2 = FB.incompleteTestUserAccessToken testUser2                    -- Check if the new test users' tokens are valid.-                   FB.isValid tokenUser1 #?= True-                   FB.isValid tokenUser2 #?= True+            FB.isValid tokenUser1 #?= True+            FB.isValid tokenUser2 #?= True                    -- Create a friend connection between the new test users.-                   FB.makeFriendConn testUser1 testUser2+            FB.makeFriendConn testUser1 testUser2                    -- Verify that one is a friend of the other.-                   user1 <- FB.getUser (FB.tuId testUser1) [] (Just tokenUser1)-                   user2 <- FB.getUser (FB.tuId testUser2) [] (Just tokenUser2)-                   friends1 <- FB.getUserFriends (FB.tuId testUser1) [] tokenUser1-                   friends2 <- FB.getUserFriends (FB.tuId testUser2) [] tokenUser2-                   FB.pagerData friends1 &?=-                     [ FB.Friend-                         (FB.tuId testUser2)-                         (M.fromJust (FB.userName user2))-                     ]-                   FB.pagerData friends2 &?=-                     [ FB.Friend-                         (FB.tuId testUser1)-                         (M.fromJust (FB.userName user1))-                     ]-  describe' "getTestUsers" $-    do it "gets a list of test users" $-         do runAuth $-              do token <- FB.getAppAccessToken-                 pager <- FB.getTestUsers token-                 src <- FB.fetchAllNextPages pager-                 oldList <- liftIO $ R.runResourceT $ src C.$$ CL.consume-                 withTestUser D.def $-                   \testUser -> do-                     newList <- FB.pagerData <$> FB.getTestUsers token-                     let newList' = map FB.tuId newList-                         oldList' = map FB.tuId oldList-                     ((FB.tuId testUser) `elem` (newList' \\ oldList')) &?= True+            user1 <- FB.getUser (FB.tuId testUser1) [] (Just tokenUser1)+            user2 <- FB.getUser (FB.tuId testUser2) [] (Just tokenUser2)+            friends1 <- FB.getUserFriends (FB.tuId testUser1) [] tokenUser1+            friends2 <- FB.getUserFriends (FB.tuId testUser2) [] tokenUser2+            FB.pagerData friends1 &?=+              [FB.Friend (FB.tuId testUser2) (M.fromJust (FB.userName user2))]+            FB.pagerData friends2 &?=+              [FB.Friend (FB.tuId testUser1) (M.fromJust (FB.userName user1))]+  describe' "getTestUsers" $ do+    it "gets a list of test users" $ do+      runAuth $ do+        token <- FB.getAppAccessToken+        pager <- FB.getTestUsers token+        src <- FB.fetchAllNextPages pager+        oldList <- liftIO $ R.runResourceT $ C.runConduit $ src C..| CL.consume+        withTestUser D.def $ \testUser -> do+          newList <- FB.pagerData <$> FB.getTestUsers token+          let newList' = map FB.tuId newList+              oldList' = map FB.tuId oldList+          ((FB.tuId testUser) `elem` (newList' \\ oldList')) &?= True  newtype PageName =   PageName Text@@ -375,138 +360,140 @@  libraryTests :: H.Manager -> Spec libraryTests manager = do-  describe "SimpleType" $-    do it "works for Bool" $ (map FB.encodeFbParam [True, False]) @?= ["1", "0"]-       let day = TI.fromGregorian 2012 12 21-           time = TI.TimeOfDay 11 37 22-           diffTime = TI.secondsToDiffTime (11 * 3600 + 37 * 60)-           utcTime = TI.UTCTime day diffTime-           localTime = TI.LocalTime day time-           zonedTime = TI.ZonedTime localTime (TI.minutesToTimeZone 30)-       it "works for Day" $ FB.encodeFbParam day @?= "2012-12-21"-       it "works for UTCTime" $ FB.encodeFbParam utcTime @?= "20121221T1137Z"-       it "works for ZonedTime" $-         FB.encodeFbParam zonedTime @?= "20121221T1107Z"-       let propShowRead-             :: (Show a, Read a, Eq a, FB.SimpleType a)-             => a -> Bool-           propShowRead x = read (B.unpack $ FB.encodeFbParam x) == x-       prop "works for Float" (propShowRead :: Float -> Bool)-       prop "works for Double" (propShowRead :: Double -> Bool)-       prop "works for Int" (propShowRead :: Int -> Bool)-       prop "works for Int8" (propShowRead :: Int8 -> Bool)-       prop "works for Int16" (propShowRead :: Int16 -> Bool)-       prop "works for Int32" (propShowRead :: Int32 -> Bool)-       prop "works for Int64" (propShowRead :: Int64 -> Bool)-       prop "works for Word" (propShowRead :: Word -> Bool)-       prop "works for Word8" (propShowRead :: Word8 -> Bool)-       prop "works for Word16" (propShowRead :: Word16 -> Bool)-       prop "works for Word32" (propShowRead :: Word32 -> Bool)-       prop "works for Word64" (propShowRead :: Word64 -> Bool)-       let propShowReadL-             :: (Show a, Read a, Eq a, FB.SimpleType a)-             => [a] -> Bool-           propShowReadL x =-             read ('[' : B.unpack (FB.encodeFbParam x) ++ "]") == x-       prop "works for [Float]" (propShowReadL :: [Float] -> Bool)-       prop "works for [Double]" (propShowReadL :: [Double] -> Bool)-       prop "works for [Int]" (propShowReadL :: [Int] -> Bool)-       prop "works for [Int8]" (propShowReadL :: [Int8] -> Bool)-       prop "works for [Int16]" (propShowReadL :: [Int16] -> Bool)-       prop "works for [Int32]" (propShowReadL :: [Int32] -> Bool)-       prop "works for [Int64]" (propShowReadL :: [Int64] -> Bool)-       prop "works for [Word]" (propShowReadL :: [Word] -> Bool)-       prop "works for [Word8]" (propShowReadL :: [Word8] -> Bool)-       prop "works for [Word16]" (propShowReadL :: [Word16] -> Bool)-       prop "works for [Word32]" (propShowReadL :: [Word32] -> Bool)-       prop "works for [Word64]" (propShowReadL :: [Word64] -> Bool)-       prop "works for Text" (\t -> FB.encodeFbParam t == TE.encodeUtf8 t)-       prop "works for Id" $-         \i ->-            let toId :: Int -> FB.Id-                toId = FB.Id . T.pack . show-                j = abs i-            in FB.encodeFbParam (toId j) == FB.encodeFbParam j-  describe "parseSignedRequest" $-    do let exampleSig, exampleData :: B.ByteString-           exampleSig = "vlXgu64BQGFSQrY0ZcJBZASMvYvTHu9GQ0YM9rjPSso"-           exampleData =-             "eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsIjAiOiJwYXlsb2FkIn0"-           exampleCreds = FB.Credentials "name" "id" "secret"-           runExampleAuth :: FB.FacebookT FB.Auth (R.ResourceT IO) a -> IO a-           runExampleAuth = R.runResourceT . FB.runFacebookT exampleCreds manager-       it "works for Facebook example" $-         do runExampleAuth $-              do ret <--                   FB.parseSignedRequest-                     (B.concat [exampleSig, ".", exampleData])-                 ret &?=-                   Just-                     (A.object-                        [ "algorithm" A..= ("HMAC-SHA256" :: Text)-                        , "0" A..= ("payload" :: Text)-                        ])-       it "fails to parse the Facebook example when signature is corrupted" $-         do let corruptedSig = B.cons 'a' (B.tail exampleSig)-            runExampleAuth $-              do ret <--                   FB.parseSignedRequest-                     (B.concat [corruptedSig, ".", exampleData])-                 ret &?= (Nothing :: Maybe A.Value)-  describe "FQLTime" $-    do it "seems to work" $-         do let input = "[1348678357]"-                output = FB.FQLTime (read "2012-09-26 16:52:37 UTC")-            A.decode input @?= Just [output]-  describe "FbUTCTime" $-    do let output = FB.FbUTCTime (read "2012-09-26 16:52:37 UTC")-       it "seems to work (string)" $-         do let input = "[\"2012-09-26T16:52:37+0000\"]"-            A.decode input @?= Just [output]-       it "seems to work (unix epoch)" $-         do let input = "[1348678357]"-            A.decode input @?= Just [output]-  describe "FQLList" $-    do let j :: [Int] -> Maybe (FB.FQLList Int)-           j = Just . FB.FQLList-       it "parses []" $ do A.decode "[]" @?= j []-       it "parses {}" $ do A.decode "{}" @?= j []-       it "parses [1234]" $ do A.decode "[1234]" @?= j [1234]-       it "parses {\"1234\": 1234}" $-         do A.decode "{\"1234\": 1234}" @?= j [1234]-  describe "FQLObject" $-    do let j :: [(Text, Int)] -> Maybe (FB.FQLObject (Map.Map Text Int))-           j = Just . FB.FQLObject . Map.fromList-       it "parses []" $ do A.decode "[]" @?= j []-       it "parses {}" $ do A.decode "{}" @?= j []-       it "parses {\"abc\": 1234}" $-         do A.decode "{\"abc\": 1234}" @?= j [("abc", 1234)]-       it "does not parse [1234]" $-         do A.decode "[1234]" @?= (Nothing `asTypeOf` j [])-  describe "Id" $-    do it "can be parsed from a string" $-         do A.decode "[\"1234\"]" @?= Just [FB.Id "1234"]-       it "can be parsed from an integer" $-         do A.decode "[1234]" @?= Just [FB.Id "1234"]-       it "can be parsed from an object with a string" $-         do A.decode "{\"id\": \"1234\"}" @?= Just (FB.Id "1234")-       it "can be parsed from an object with an integer" $-         do A.decode "{\"id\": 1234}" @?= Just (FB.Id "1234")-  describe "AccessToken" $-    do it "can be round-tripped with ToJSON/FromJSON (UserKind)" $-         do A.eitherDecode (A.encode invalidUserAccessToken) @?= Right invalidUserAccessToken-       it "can be round-tripped with ToJSON/FromJSON (AppKind)" $-         do A.eitherDecode (A.encode invalidAppAccessToken) @?= Right invalidAppAccessToken+  describe "SimpleType" $ do+    it "works for Bool" $ (map FB.encodeFbParam [True, False]) @?= ["1", "0"]+    let day = TI.fromGregorian 2012 12 21+        time = TI.TimeOfDay 11 37 22+        diffTime = TI.secondsToDiffTime (11 * 3600 + 37 * 60)+        utcTime = TI.UTCTime day diffTime+        localTime = TI.LocalTime day time+        zonedTime = TI.ZonedTime localTime (TI.minutesToTimeZone 30)+    it "works for Day" $ FB.encodeFbParam day @?= "2012-12-21"+    it "works for UTCTime" $ FB.encodeFbParam utcTime @?= "20121221T1137Z"+    it "works for ZonedTime" $ FB.encodeFbParam zonedTime @?= "20121221T1107Z"+    let propShowRead :: (Show a, Read a, Eq a, FB.SimpleType a) => a -> Bool+        propShowRead x = read (B.unpack $ FB.encodeFbParam x) == x+    prop "works for Float" (propShowRead :: Float -> Bool)+    prop "works for Double" (propShowRead :: Double -> Bool)+    prop "works for Int" (propShowRead :: Int -> Bool)+    prop "works for Int8" (propShowRead :: Int8 -> Bool)+    prop "works for Int16" (propShowRead :: Int16 -> Bool)+    prop "works for Int32" (propShowRead :: Int32 -> Bool)+    prop "works for Int64" (propShowRead :: Int64 -> Bool)+    prop "works for Word" (propShowRead :: Word -> Bool)+    prop "works for Word8" (propShowRead :: Word8 -> Bool)+    prop "works for Word16" (propShowRead :: Word16 -> Bool)+    prop "works for Word32" (propShowRead :: Word32 -> Bool)+    prop "works for Word64" (propShowRead :: Word64 -> Bool)+    let propShowReadL :: (Show a, Read a, Eq a, FB.SimpleType a) => [a] -> Bool+        propShowReadL x = read ('[' : B.unpack (FB.encodeFbParam x) ++ "]") == x+    prop "works for [Float]" (propShowReadL :: [Float] -> Bool)+    prop "works for [Double]" (propShowReadL :: [Double] -> Bool)+    prop "works for [Int]" (propShowReadL :: [Int] -> Bool)+    prop "works for [Int8]" (propShowReadL :: [Int8] -> Bool)+    prop "works for [Int16]" (propShowReadL :: [Int16] -> Bool)+    prop "works for [Int32]" (propShowReadL :: [Int32] -> Bool)+    prop "works for [Int64]" (propShowReadL :: [Int64] -> Bool)+    prop "works for [Word]" (propShowReadL :: [Word] -> Bool)+    prop "works for [Word8]" (propShowReadL :: [Word8] -> Bool)+    prop "works for [Word16]" (propShowReadL :: [Word16] -> Bool)+    prop "works for [Word32]" (propShowReadL :: [Word32] -> Bool)+    prop "works for [Word64]" (propShowReadL :: [Word64] -> Bool)+    prop "works for Text" (\t -> FB.encodeFbParam t == TE.encodeUtf8 t)+    prop "works for Id" $ \i ->+      let toId :: Int -> FB.Id+          toId = FB.Id . T.pack . show+          j = abs i+       in FB.encodeFbParam (toId j) == FB.encodeFbParam j+  describe "parseSignedRequest" $ do+    let exampleSig, exampleData :: B.ByteString+        exampleSig = "vlXgu64BQGFSQrY0ZcJBZASMvYvTHu9GQ0YM9rjPSso"+        exampleData = "eyJhbGdvcml0aG0iOiJITUFDLVNIQTI1NiIsIjAiOiJwYXlsb2FkIn0"+        exampleCreds = FB.Credentials "name" "id" "secret" False+        runExampleAuth :: FB.FacebookT FB.Auth (R.ResourceT IO) a -> IO a+        runExampleAuth = R.runResourceT . FB.runFacebookT exampleCreds manager+    it "works for Facebook example" $ do+      runExampleAuth $ do+        ret <- FB.parseSignedRequest (B.concat [exampleSig, ".", exampleData])+        ret &?=+          Just+            (A.object+               [ "algorithm" A..= ("HMAC-SHA256" :: Text)+               , "0" A..= ("payload" :: Text)+               ])+    it "fails to parse the Facebook example when signature is corrupted" $ do+      let corruptedSig = B.cons 'a' (B.tail exampleSig)+      runExampleAuth $ do+        ret <- FB.parseSignedRequest (B.concat [corruptedSig, ".", exampleData])+        ret &?= (Nothing :: Maybe A.Value)+  describe "addAppSecretProof" $ do+    it "appends appsecret_proof to the query when passing an access token" $ do+      now <- liftIO TI.getCurrentTime+      let token = FB.UserAccessToken "id" "token" now+          query = [("test", "whatever")]+          secretProofQ creds = FB.makeAppSecretProof creds (Just token)+      creds <- getCredentials+      FB.addAppSecretProof creds (Just token) query @?= secretProofQ creds <>+        query+  describe "FQLTime" $ do+    it "seems to work" $ do+      let input = "[1348678357]"+          output = FB.FQLTime (read "2012-09-26 16:52:37 UTC")+      A.decode input @?= Just [output]+  describe "FbUTCTime" $ do+    let output = FB.FbUTCTime (read "2012-09-26 16:52:37 UTC")+    it "seems to work (string)" $ do+      let input = "[\"2012-09-26T16:52:37+0000\"]"+      A.decode input @?= Just [output]+    it "seems to work (unix epoch)" $ do+      let input = "[1348678357]"+      A.decode input @?= Just [output]+  describe "FQLList" $ do+    let j :: [Int] -> Maybe (FB.FQLList Int)+        j = Just . FB.FQLList+    it "parses []" $ do A.decode "[]" @?= j []+    it "parses {}" $ do A.decode "{}" @?= j []+    it "parses [1234]" $ do A.decode "[1234]" @?= j [1234]+    it "parses {\"1234\": 1234}" $ do A.decode "{\"1234\": 1234}" @?= j [1234]+  describe "FQLObject" $ do+    let j :: [(Text, Int)] -> Maybe (FB.FQLObject (Map.Map Text Int))+        j = Just . FB.FQLObject . Map.fromList+    it "parses []" $ do A.decode "[]" @?= j []+    it "parses {}" $ do A.decode "{}" @?= j []+    it "parses {\"abc\": 1234}" $ do+      A.decode "{\"abc\": 1234}" @?= j [("abc", 1234)]+    it "does not parse [1234]" $ do+      A.decode "[1234]" @?= (Nothing `asTypeOf` j [])+  describe "Id" $ do+    it "can be parsed from a string" $ do+      A.decode "[\"1234\"]" @?= Just [FB.Id "1234"]+    it "can be parsed from an integer" $ do+      A.decode "[1234]" @?= Just [FB.Id "1234"]+    it "can be parsed from an object with a string" $ do+      A.decode "{\"id\": \"1234\"}" @?= Just (FB.Id "1234")+    it "can be parsed from an object with an integer" $ do+      A.decode "{\"id\": 1234}" @?= Just (FB.Id "1234")+  describe "AccessToken" $ do+    it "can be round-tripped with ToJSON/FromJSON (UserKind)" $ do+      A.eitherDecode (A.encode invalidUserAccessToken) @?=+        Right invalidUserAccessToken+    it "can be round-tripped with ToJSON/FromJSON (AppKind)" $ do+      A.eitherDecode (A.encode invalidAppAccessToken) @?=+        Right invalidAppAccessToken+  describe "makeAppSecretProof" $ do+    it "generates correct hmac" $+      let creds = FB.Credentials "name" "id" "secret" True+          uToken = FB.UserAccessToken (FB.Id "user") "accesstoken" undefined+          proof = FB.makeAppSecretProof creds $ Just uToken+          expectedProof =+            "65138b7ea24e641d38c91befa22b6281953980d1bfd0322956bc29959e1a910c"+       in proof @?= [("appsecret_proof", expectedProof)]  -- Wrappers for HUnit operators using MonadIO-(&?=)-  :: (Eq a, Show a, MonadIO m)-  => a -> a -> m ()+(&?=) :: (Eq a, Show a, MonadIO m) => a -> a -> m () v &?= e = liftIO (v @?= e) -( #?= )-  :: (Eq a, Show a, MonadIO m)-  => m a -> a -> m ()+(#?=) :: (Eq a, Show a, MonadIO m) => m a -> a -> m () m #?= e = m >>= (&?= e)  -- | Sad, orphan instance.@@ -516,8 +503,8 @@  -- | Perform an action with a new test user. Remove the new test user -- after the action is performed.-withTestUser-  :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)+withTestUser ::+     (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m)   => FB.CreateTestUser   -> (FB.TestUser -> FB.FacebookT FB.Auth m a)   -> FB.FacebookT FB.Auth m a