{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} module Facebook.TestUsers ( TestUser(..) , CreateTestUser(..) , CreateTestUserInstalled(..) , getTestUsers , disassociateTestuser , removeTestUser , createTestUser , makeFriendConn , incompleteTestUserAccessToken ) where import Control.Applicative ((<$>), (<*>)) import Control.Monad (unless, mzero) import Data.ByteString.Lazy (fromStrict) import Data.Default import Data.Text import Data.Text.Encoding (encodeUtf8) import Data.Time (UTCTime(..), Day(..)) import Data.Typeable (Typeable) import Data.Aeson import Data.Aeson.Types import qualified UnliftIO.Exception as E import qualified Control.Monad.Trans.Resource as R import qualified Data.Aeson as A import qualified Data.ByteString.Char8 as B import Facebook.Auth import Facebook.Base import Facebook.Graph import Facebook.Monad import Facebook.Types import Facebook.Pager -- | A Facebook test user. -- Ref: https://developers.facebook.com/docs/graph-api/reference/v2.8/app/accounts/test-users data TestUser = TestUser { tuId :: UserId , tuAccessToken :: Maybe AccessTokenData , tuLoginUrl :: Maybe Text , tuEmail :: Maybe Text , tuPassword :: Maybe Text } deriving (Eq, Ord, Show, Read, Typeable) instance A.FromJSON TestUser where parseJSON (A.Object v) = TestUser <$> v A..: "id" <*> v A..:? "access_token" <*> v A..:? "login_url" <*> v A..:? "email" <*> v A..:? "password" parseJSON _ = mzero -- | Data type used to hold information of a new test user. This type -- also accepts a Data.Default value. data CreateTestUser = CreateTestUser { ctuInstalled :: CreateTestUserInstalled , ctuName :: Maybe Text , ctuLocale :: Maybe Text } -- | Specify if the app is to be installed on the new test user. If -- it is, then you must tell what permissions should be given. data CreateTestUserInstalled = CreateTestUserNotInstalled | CreateTestUserInstalled { ctuiPermissions :: [Permission]} | CreateTestUserFbDefault -- ^ Uses Facebook's default. It seems that this is equivalent to -- @CreateTestUserInstalled []@, but Facebook's documentation is -- not clear about it. -- | Default instance for 'CreateTestUser'. instance Default CreateTestUser where def = CreateTestUser def def def -- | Default instance for 'CreateTestUserInstalled'. instance Default CreateTestUserInstalled where def = CreateTestUserFbDefault -- | Construct a query from a 'CreateTestUser'. createTestUserQueryArgs :: CreateTestUser -> [Argument] createTestUserQueryArgs (CreateTestUser installed name locale) = forInst installed ++ forField "name" name ++ forField "locale" locale where forInst (CreateTestUserInstalled p) = ["installed" #= True, "permissions" #= p] forInst CreateTestUserNotInstalled = ["installed" #= False] forInst CreateTestUserFbDefault = [] forField _ Nothing = [] forField fieldName (Just f) = [fieldName #= f] -- | 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) => CreateTestUser -- ^ How the test user should be -- created. -> AppAccessToken -- ^ Access token for your app. -> FacebookT Auth m TestUser createTestUser userInfo token = do creds <- getCreds let query = ("method", "post") : createTestUserQueryArgs userInfo getObject ("/" <> appId creds <> "/accounts/test-users") query (Just token) -- | Get a list of test users. getTestUsers :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m) => AppAccessToken -- ^ Access token for your app. -> FacebookT Auth m (Pager TestUser) getTestUsers token = do creds <- getCreds getObject ("/" <> appId creds <> "/accounts/test-users") [] (Just token) disassociateTestuser :: (R.MonadUnliftIO m, R.MonadThrow m, R.MonadResource m) => TestUser -> AppAccessToken -> FacebookT Auth m Bool disassociateTestuser testUser _token = do creds <- getCreds getObjectBool ("/" <> (appId creds) <> "/accounts/test-users") [("uid", encodeUtf8 $ idCode $ tuId testUser), ("method", "delete")] (Just _token) -- | Remove an existing test user. removeTestUser :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m) => TestUser -- ^ The TestUser to be removed. -> AppAccessToken -- ^ Access token for your app (ignored since fb 0.14.7). -> FacebookT Auth m Bool removeTestUser testUser _token = do getObjectBool ("/" <> (idCode $ tuId testUser)) [("method", "delete")] (Just _token) -- | Make a friend connection between two test users. -- -- This is how Facebook's API work: two calls must be made. The first -- call has the format: \"\/userA_id\/friends\/userB_id\" with the -- access token of user A as query parameter. The second call has the -- format: \"\/userB_id\/friends\/userA_id\" with the access token of -- user B as query parameter. The first call creates a friend request -- and the second call accepts the friend request. makeFriendConn :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m) => TestUser -> TestUser -> FacebookT Auth m () makeFriendConn (TestUser {tuAccessToken = Nothing}) _ = E.throwIO $ FbLibraryException "The test user passed on the first argument doesn't have a token. Both users must have a token." makeFriendConn _ (TestUser {tuAccessToken = Nothing}) = E.throwIO $ FbLibraryException "The test user passed on the second argument doesn't have a token. Both users must have a token." makeFriendConn (TestUser {tuId = id1 ,tuAccessToken = (Just token1)}) (TestUser {tuId = id2 ,tuAccessToken = (Just token2)}) = do let friendReq userId1 userId2 token = getObjectBool ("/" <> idCode userId1 <> "/friends/" <> idCode userId2) ["method" #= ("post" :: B.ByteString), "access_token" #= token] Nothing r1 <- friendReq id1 id2 token1 r2 <- friendReq id2 id1 token2 unless r1 $ E.throwIO $ FbLibraryException "Couldn't make friend request." unless r2 $ E.throwIO $ FbLibraryException "Couldn't accept friend request." return () -- | Create an 'UserAccessToken' from a 'TestUser'. It's incomplete -- because it will not have the right expiration time. incompleteTestUserAccessToken :: TestUser -> Maybe UserAccessToken incompleteTestUserAccessToken t = do tokenData <- tuAccessToken t let farFuture = UTCTime (ModifiedJulianDay 100000) 0 return (UserAccessToken (tuId t) tokenData farFuture) -- | Same as 'getObject', but instead of parsing the result -- as a JSON, it tries to parse either as "true" or "false". -- Used only by the Test User API bindings. getObjectBool :: (R.MonadResource m, R.MonadUnliftIO m, R.MonadThrow m) => Text -- ^ Path (should begin with a slash @\/@). -> [Argument] -- ^ Arguments to be passed to Facebook. -> Maybe (AccessToken anyKind) -- ^ Optional access token. -> FacebookT anyAuth m Bool getObjectBool path query mtoken = runResourceInFb $ do req <- fbreq path mtoken query response <- fbhttp req bs <- asBS response let respJson :: Maybe Value = decode (fromStrict bs) maybe (return False) (\val -> maybe (return False) return (parseMaybe isTrue val)) respJson where isTrue :: Value -> Parser Bool isTrue val = withObject "success" (\obj -> do (status :: Bool) <- obj .: "success" return status) val