{-# LANGUAGE FlexibleContexts, OverloadedStrings #-} module Facebook.RealTime ( RealTimeUpdateObject(..) , RealTimeUpdateField , RealTimeUpdateUrl , RealTimeUpdateToken , modifySubscription , RealTimeUpdateSubscription(..) , listSubscriptions , verifyRealTimeUpdateNotifications , getRealTimeUpdateNotifications , RealTimeUpdateNotification(..) , RealTimeUpdateNotificationUserEntry(..) ) where import Control.Applicative ((<$>), (<*>)) import Control.Monad.Trans.Control (MonadBaseControl) 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 qualified Control.Monad.Trans.Resource as R import qualified Crypto.Classes as Crypto import qualified Crypto.HMAC as Crypto import qualified Data.Aeson as A import qualified Data.ByteString.Base16 as Base16 import qualified Data.ByteString.Lazy.Char8 as L import qualified Data.Conduit as C import qualified Data.Conduit.List as CL import qualified Data.Text as T import qualified Data.Text.Encoding as TE 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.Pager -- | The type of objects that a real-time update refers to. data RealTimeUpdateObject = UserRTUO | PermissionsRTUO | PageRTUO | ErrorsRTUO | OtherRTUO Text deriving (Eq, Ord, Show, Typeable) rtuoToBS :: RealTimeUpdateObject -> ByteString rtuoToBS (UserRTUO) = "user" rtuoToBS (PermissionsRTUO) = "permissions" rtuoToBS (PageRTUO) = "page" rtuoToBS (ErrorsRTUO) = "errors" rtuoToBS (OtherRTUO other) = TE.encodeUtf8 other instance A.FromJSON RealTimeUpdateObject where parseJSON (A.String "user") = return UserRTUO parseJSON (A.String "permissions") = return PermissionsRTUO parseJSON (A.String "page") = return PageRTUO parseJSON (A.String "errors") = return ErrorsRTUO parseJSON (A.String other) = return (OtherRTUO other) parseJSON _ = mzero instance A.ToJSON RealTimeUpdateObject where toJSON = A.String . TE.decodeUtf8 . rtuoToBS -- | A field of a 'RealTimeUpdateObject' that you would like to -- receive notifications when changed. type RealTimeUpdateField = ByteString -- | The URL on your server that will receive the real-time -- updates. Please refer to Facebook's documentation in order to -- see what this URL needs to implement. type RealTimeUpdateUrl = Text -- | A token that is sent back by Facebook's servers to your -- server in order to verify that you really were trying to -- modify your subscription. type RealTimeUpdateToken = ByteString -- | Add or modify a subscription for real-time updates. If -- there were no previous subscriptions for the given -- 'RealTimeUpdateObject', then a new subscription is created. -- 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, MonadBaseControl IO m) => RealTimeUpdateObject -- ^ Type of objects whose subscription you -- and to add or modify. -> [RealTimeUpdateField] -- ^ Fields that you are interested in -- receiving updates. -> RealTimeUpdateUrl -- ^ Your callback URL. -> RealTimeUpdateToken -- ^ A verification token. -> AppAccessToken -- ^ Access token for your app. -> FacebookT Auth m () modifySubscription object fields callbackUrl verifyToken apptoken = do path <- getSubscriptionsPath let args = [ "object" #= rtuoToBS object , "fields" #= fields , "callback_url" #= callbackUrl , "verify_token" #= verifyToken ] 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 = 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) instance A.FromJSON RealTimeUpdateSubscription where parseJSON (A.Object v) = RealTimeUpdateSubscription <$> v A..: "object" <*> v A..: "callback_url" <*> fmap (map encodeUtf8) (v A..: "fields") <*> v A..: "active" parseJSON _ = mzero -- | List current real-time update subscriptions. listSubscriptions :: (R.MonadResource m, MonadBaseControl IO m) => AppAccessToken -> FacebookT Auth m [RealTimeUpdateSubscription] listSubscriptions apptoken = do path <- getSubscriptionsPath pager <- getObject path [] (Just apptoken) src <- fetchAllNextPages pager lift $ src C.$$ CL.consume -- | Verifies the input's authenticity (i.e. it comes from -- 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 => ByteString -- ^ @X-Hub-Signature@ HTTP header's value. -> L.ByteString -- ^ Request body with JSON-encoded notifications. -> 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) return $! if sig `Crypto.constTimeEq` expected then Just body else Nothing -- | Same as 'verifyRealTimeUpdateNotifications' but also parses -- the response as JSON. Returns @Nothing@ if either the -- 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) => 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 -- | When data changes and there's a valid subscription, Facebook -- will @POST@ to your 'RealTimeUpdateUrl' with a JSON-encoded -- object containing the notifications. A -- 'RealTimeUpdateNotification a' represents such object where -- 'a' is type of the entries (e.g., -- 'RealTimeUpdateNotificationUserEntry'). -- -- If you have a single 'RealTimeUpdateUrl' for different kinds -- of notifications, you may parse a @RealTimeUpdateNotification -- 'A.Value'@ and then manually parse the 'A.Value' depending on -- the value of 'rtunObject'. -- -- We recommend using 'getRealTimeUpdateNotifications'. data RealTimeUpdateNotification a = RealTimeUpdateNotification { rtunObject :: RealTimeUpdateObject , rtunEntries :: [a] } deriving (Eq, Ord, Show, Typeable) 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) instance A.FromJSON RealTimeUpdateNotificationUserEntry where parseJSON (A.Object v) = RealTimeUpdateNotificationUserEntry <$> v A..: "uid" <*> fmap (map encodeUtf8) (v A..: "changed_fields") <*> v A..: "time" parseJSON _ = mzero