{-# 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 Data.ByteString.Char8 (ByteString) import Data.Text (Text) import Data.Typeable (Typeable) import qualified Crypto.Classes as Crypto import qualified Crypto.HMAC as Crypto import qualified Crypto.Hash.SHA1 as SHA1 import qualified Data.Aeson as A import qualified Data.ByteString.Base16 as Base16 import qualified Data.ByteString.Char8 as B 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.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 -- | 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 :: (C.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 ByteString getSubscriptionsPath = do creds <- getCreds return $ B.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" <*> v A..: "fields" <*> v A..: "active" parseJSON _ = mzero -- | List current real-time update subscriptions. listSubscriptions :: (C.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 SHA1.Ctx SHA1.SHA1 key = Crypto.MacKey (appSecret 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" <*> v A..: "changed_fields" <*> v A..: "time" parseJSON _ = mzero