happstack-facebook-0.9: Happstack/Facebook/Application.hs
{-# 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)
-}