{-# LANGUAGE FlexibleContexts, FlexibleInstances, GeneralizedNewtypeDeriving, MultiParamTypeClasses, ScopedTypeVariables, TypeSynonymInstances, TypeFamilies #-} module Happstack.Facebook.Application where import Control.Applicative import Control.Arrow(first, second) import Control.Monad (liftM) import Control.Monad.Reader import Control.Monad.State import Control.Monad.Trans import qualified Data.ByteString.Char8 as P import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.ByteString.Lazy.UTF8 as L import Data.Function (on) import Data.List (isPrefixOf, sortBy) import Data.Maybe (fromJust) import Data.Time.Clock.POSIX (POSIXTime) import Happstack.Crypto.MD5 ( md5, stringMD5) import Happstack.Facebook.Common import Happstack.Facebook.FacebookT -- import qualified Happstack.Facebook.Feed as Feed -- import Happstack.Server.Extra (lookPairsUnicode) import Happstack.State (Proxy(..)) -- import HSP (HSPT,XML,XMLMetaData,evalHSPT,renderXML) import qualified Network.HTTP as HTTP import Network.Browser (Form(..),formToRequest, request, browse) import Network.URI import Happstack.Server -- |This data type holds all the informaton that facebook pass along with the request -- http://wiki.developers.facebook.com/index.php/Your_callback_page_and_you data FacebookData = FacebookData { fbPairs :: [(String, String)] , _fb_sig_added :: Bool , _fb_sig_in_canvas :: Bool , _fb_sig_request_method :: Method , _fb_sig_position_fix :: Bool , _fb_user :: Maybe User , _fb_sig_time :: POSIXTime , _fb_sig_api_key :: ApiKey -- , _fb_sig_app_id :: AppId , _fb_sig_locale :: String , _fb_sig :: String , _fb_sig_friends :: Maybe [User] , _fb_sig_session_key :: Maybe SessionKey , _fb_sig_expires :: Maybe POSIXTime , _fb_sig_profile_update_time :: Maybe POSIXTime , _fb_valid_sig :: Bool } deriving (Eq, Show) -- ** convenience functions for getting information from the FacebookData -- |has the user added our application fb_sig_added :: (HasFacebookData FacebookData m) => m Bool fb_sig_added = fbd _fb_sig_added fb_sig_in_canvas :: (HasFacebookData FacebookData m) => m Bool fb_sig_in_canvas = fbd _fb_sig_in_canvas fb_sig_request_method :: (HasFacebookData FacebookData m) => m Method fb_sig_request_method = fbd _fb_sig_request_method fb_sig_position_fix :: (HasFacebookData FacebookData m) => m Bool fb_sig_position_fix = fbd _fb_sig_position_fix -- |get the userid fb_user :: (HasFacebookData FacebookData m) => m (Maybe User) fb_user = fbd _fb_user -- |get current time fb_sig_time :: (HasFacebookData FacebookData m) => m POSIXTime fb_sig_time = fbd _fb_sig_time -- |your api key fb_sig_api_key :: (HasFacebookData FacebookData m) => m ApiKey fb_sig_api_key = fbd _fb_sig_api_key -- |the user's locale fb_sig_locale :: (HasFacebookData FacebookData m) => m String fb_sig_locale = fbd _fb_sig_locale -- |the signature for the request fb_sig :: (HasFacebookData FacebookData m) => m String fb_sig = fbd _fb_sig -- |is the signature valid fb_valid_sig :: (HasFacebookData FacebookData m) => m Bool fb_valid_sig = fbd _fb_valid_sig -- |list of the users friends -- NOTE: only available if the user is logged in fb_sig_friends :: (HasFacebookData FacebookData m) => m (Maybe [User]) fb_sig_friends = fbd _fb_sig_friends -- |session key -- NOTE: only available if the user is logged in fb_sig_session_key :: (HasFacebookData FacebookData m) => m (Maybe String) fb_sig_session_key = fbd _fb_sig_session_key -- |when this session key expires -- 0 == never -- otherwise, time in seconds since epoch` -- NOTE: only available if the user is logged in fb_sig_expires :: (HasFacebookData FacebookData m) => m (Maybe POSIXTime) fb_sig_expires = fbd _fb_sig_expires -- |time profile was last updated -- NOTE: only available if the user is logged in fb_sig_profile_update_time :: (HasFacebookData FacebookData m) => m (Maybe POSIXTime) fb_sig_profile_update_time = fbd _fb_sig_profile_update_time validateSignature :: AppSecret -> String -> [(String, String)] -> Bool validateSignature secret sig pairs = let fb_sigs = map (first (drop 7)) $ filter (\(k,v) -> isPrefixOf "fb_sig_" k) pairs (sig',_) = signature secret fb_sigs in sig == sig' -- todo: add code to validate the sent fb_sig withFacebookData :: (Monad m, MonadPlus m, ServerMonad m) => FacebookConfig -> (FacebookData -> m r) -> m r withFacebookData config f = withDataFn (do pairs <- lookPairs let lookupString k = case lookup k pairs of Nothing -> error ("Could not find required field " ++ k) (Just v) -> v read' str = case reads str of [(a,[])] -> a r -> error ("Failed to read " ++ str ++ " got " ++ show r) readTime :: String -> POSIXTime readTime str = realToFrac (read' str :: Double) fb_sig_added' = lookupBool "fb_sig_added" pairs fb_user' = case lookup "fb_sig_user" pairs of (Just uid) -> Just $ User (read' uid) Nothing -> case lookup "fb_sig_canvas_user" pairs of (Just uid) -> Just $ User (read' uid) Nothing -> Nothing fb_sig_time' = readTime $ lookupString "fb_sig_time" fb_sig_api_key' = ApiKey $ lookupString "fb_sig_api_key" fb_sig_locale' = lookupString "fb_sig_locale" fb_sig' = lookupString "fb_sig" fb_sig_friends' = fmap (\str -> let (Right fids) = parseUserIds str in fids) $ lookup "fb_sig_friends" pairs fb_sig_session_key' = lookup "fb_sig_session_key" pairs fb_sig_expires' = fmap readTime $ lookup "fb_sig_expires" pairs fb_sig_profile_update_time' = fmap readTime $ lookup "fb_sig_profile_update_time" pairs valid_sig = validateSignature (appSecret config) fb_sig' pairs return (FacebookData { fbPairs = pairs , _fb_sig_added = fb_sig_added' , _fb_sig_in_canvas = lookupBool "fb_sig_in_canvas" pairs , _fb_sig_request_method = case lookupString "fb_sig_request_method" of "GET" -> GET "POST" -> POST , _fb_sig_position_fix = lookupBool "fb_sig_position_fix" pairs , _fb_user = fb_user' , _fb_sig_time = fb_sig_time' , _fb_sig_api_key = fb_sig_api_key' , _fb_sig_locale = fb_sig_locale' , _fb_sig = fb_sig' , _fb_sig_friends = fb_sig_friends' , _fb_sig_session_key = fb_sig_session_key' , _fb_sig_expires = fb_sig_expires' , _fb_sig_profile_update_time = fb_sig_profile_update_time' , _fb_valid_sig = valid_sig })) f withFacebook :: (Monad m, MonadPlus m, ServerMonad m) => FacebookConfig -> FacebookT (FacebookState FacebookData) m a -> m a withFacebook config sp = withFacebookData config $ \fbd -> withFacebook' config fbd sp withUserSP :: ( HasFacebookConfig (FacebookT (s d) m) , HasFacebookData d (FacebookT (s d) m) , HasFacebookData FacebookData (FacebookT (s d) m) , MonadPlus m ) => FacebookT (FacebookStateU d) m a -> FacebookT (s d) m a withUserSP handler = do mUser <- fb_user case mUser of Nothing -> mzero (Just user) -> withUser user handler withSessionSP :: ( HasFacebookConfig (FacebookT (s d) m) , HasFacebookData d (FacebookT (s d) m) , HasFacebookData FacebookData (FacebookT (s d) m) , HasFacebookData FacebookData (FacebookT (FacebookStateU d) m) , MonadPlus m ) => FacebookT (FacebookStateS d) m a -> FacebookT (s d) m a withSessionSP handler = do mUser <- fb_user case mUser of Nothing -> mzero (Just user) -> withUser user $ do mSession <- fb_sig_session_key case mSession of Nothing -> mzero (Just sessionKey) -> withSession sessionKey handler {- withFacebook' :: (Monad m) => FacebookConfig -> FacebookData -> ServerPartT (FacebookT FacebookState m) a -> ServerPartT m a withFacebook' config facebookData sp = mapServerPartT doFacebook sp where doFacebook sp = runReaderT (unFacebookT sp) (FacebookState config facebookData) {- withFacebook' :: FacebookConfig -> FacebookData -> ServerPartT (HSPT (FacebookT IO)) a -> ServerPartT IO a withFacebook' config facebookData sp = mapServerPartT doHSPT sp where doHSPT hspt = evalStateT (unFacebookT (evalHSPT Nothing hspt >>= return .snd)) (FacebookState config facebookData) -} -- callMethodE :: (HasFacebookConfig m, MonadIO m, FacebookMethod m method) => -- method -> m (Either FacebookError (FacebookResponse method)) {- callMethodE :: forall method m. (HasFacebookConfig m, MonadIO m, FacebookMethod m method) => method -> m (Either FacebookError (FacebookResponse method)) callMethodE method = do configData <- askFacebookConfig params <- toParams method req <- return $ buildRequest configData params -- liftIO (putStrLn $ "FB Request: " ++ (show $ req (CallId 0))) res <- execRequest req -- liftIO (putStrLn $ "FB Response: " ++ (show res)) return $ parseResponse' (Proxy :: Proxy (m method)) res callMethod :: forall method m. (HasFacebookConfig m, MonadIO m, FacebookMethod m method) => method -> m (Either String (FacebookResponse method)) callMethod method = do configData <- askFacebookConfig params <- toParams method req <- return $ buildRequest configData params -- liftIO (putStrLn $ "FB Request: " ++ (show $ req (CallId 0))) res <- execRequest req -- liftIO (putStrLn $ "FB Response: " ++ (show res)) return $ parseResponse (Proxy :: Proxy (m method)) res -} {- class (Monad m) => HasFacebookData FacebookData m where askFacebookData :: m FacebookData class (Monad m) => HasUser m where askUser :: m User class (Monad m) => HasSessionKey m where askSessionKey :: m SessionKey -} {- class (Monad m) => HasFacebookConfig m where askFacebookConfig :: m FacebookConfig -}-} {- -- |our facebook code will live in the Facebook monad. Currently this -- monad just provides some environment data. The Facebook monad lets -- us embed Facebook Markup Language using literal XML via HSP. type Facebook = HSPT (FacebookT IO) instance Applicative Facebook where pure = return (<*>) = ap -} {- buildRequest :: (HasFacebookConfig m) => Parameters -> m (CallId -> HTTP.Request String) buildRequest parameters = do fbConfig <- askFacebookConfig -- uid <- liftM (maybe "" (show . uid)) fb_user -- FIXME : not all calls require this return $ \cid -> let (sig, args) = signature (appSecret fbConfig) $ [ ("api_key", unApiKey $ apiKey fbConfig) , ("call_id", show (toInteger cid)) , ("format","json") , ("uid", uid) , ("v","1.0") ] ++ parameters in formToRequest (Form HTTP.POST fbRESTURI (args ++ [("sig",sig)])) where fbRESTURI :: URI fbRESTURI = fromJust $ parseURI "http://api.facebook.com/restserver.php" -} {- class (Monad m) => HasFacebookData m where askFacebookData :: m FacebookData -- |the State that lives in the facebook monad data FacebookState = FacebookState { fbConfig :: FacebookConfig -- our api id, etc, , fbData :: FacebookData -- user specific information that facebook provides } instance (Monad m) => HasFacebookConfig (FacebookT FacebookState m) where askFacebookConfig = fbConfig <$> ask instance (Monad m) => HasFacebookData (FacebookT FacebookState m) where askFacebookData = fbData <$> ask data FacebookStateU = FacebookStateU { fbuConfig :: FacebookConfig -- our api id, etc, , fbuData :: FacebookData -- user specific information that facebook provides , fbuUser :: User } instance (Monad m) => HasFacebookConfig (FacebookT FacebookStateU m) where askFacebookConfig = fbuConfig <$> ask instance (Monad m) => HasFacebookData (FacebookT FacebookStateU m) where askFacebookData = fbuData <$> ask instance (Monad m) => HasUser (FacebookT FacebookStateU m) where askUser = fbuUser <$> ask withUser :: (HasFacebookConfig n, HasFacebookData n) => FacebookT FacebookStateU m a -> n (Maybe (m a)) withUser (FacebookT action) = do c <- askFacebookConfig d <- askFacebookData u <- fb_user case u of Nothing -> return Nothing (Just u) -> return $ Just (runReaderT action (FacebookStateU c d u)) -- ** Facebook with Session Key data FacebookStateS = FacebookStateS { fbsConfig :: FacebookConfig -- our api id, etc, , fbsData :: FacebookData -- user specific information that facebook provides , fbsUser :: User , fbsSessionKey :: SessionKey } instance (Monad m) => HasFacebookConfig (FacebookT FacebookStateS m) where askFacebookConfig = fbsConfig <$> ask instance (Monad m) => HasFacebookData (FacebookT FacebookStateS m) where askFacebookData = fbsData <$> ask instance (Monad m) => HasUser (FacebookT FacebookStateS m) where askUser = fbsUser <$> ask instance (Monad m) => HasSessionKey (FacebookT FacebookStateS m) where askSessionKey = fbsSessionKey <$> ask withSession :: (HasUser n, HasFacebookConfig n, HasFacebookData n) => FacebookT FacebookStateS m a -> n (Maybe (m a)) withSession (FacebookT action) = do c <- askFacebookConfig d <- askFacebookData u <- askUser s <- fb_sig_session_key case s of Nothing -> return Nothing (Just s) -> return $ Just (runReaderT action (FacebookStateS c d u s)) -- |function to read some FacebookData from the Facebook environment fbd :: (HasFacebookData m) => (FacebookData -> a) -> m a fbd select = do d <- askFacebookData return (select d) -}